Merging from develop

This commit is contained in:
Leah Schumann 2022-04-22 07:56:28 -04:00
commit 14f6174a76
47 changed files with 1189 additions and 370 deletions

View file

@ -79,4 +79,44 @@ stages:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__) __VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)
# QA Build/Deploy
- stage: Qa
variables:
- group: FixMyGlassQa
jobs:
- deployment: qaBuildDeployment
displayName: Build and Deploy FMG - QA
environment: digitalCloud-qa
container: node
workspace:
clean: all
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: templates/digital/step-build-vue.yml@AzureDevOps
parameters:
buildOutputDir: dist
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters:
artifactName: vueDist
awsProfile: $(qaDeploymentProfile)
outputPath: /fmg/
deployBuckets:
safelite-qa-fmg-us-east-1:
clearFolder: true
deployFolder: ''
region: us-east-1
appDeployVariables:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
cfDistributionId: $(cfDistributionId) cfDistributionId: $(cfDistributionId)

View file

@ -17,11 +17,9 @@ module.exports = {
"!src/layouts/vin-lookup/vin-lookup.vue", "!src/layouts/vin-lookup/vin-lookup.vue",
"!src/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question.vue", "!src/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question.vue",
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue", "!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/address-poc/address-poc.vue",
"!src/layouts/nested-radio-poc/nested-radio.vue",
"!src/layouts/button-question-examples/**/*.vue",
"!src/layouts/part-questions/**/*.vue", "!src/layouts/part-questions/**/*.vue",
"!src/layouts/reveal/**/*.vue", "!src/layouts/reveal/**/*.vue",
"!src/layouts/estimate/**/*.vue",
// REMOVE THESE AFTER WRITING UNIT TESTS // REMOVE THESE AFTER WRITING UNIT TESTS
"!src/layouts/address-lookup/customer-questions/customer-questions.vue", "!src/layouts/address-lookup/customer-questions/customer-questions.vue",
@ -31,7 +29,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 87, statements: 86,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
}, },
}, },

View file

@ -1,6 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-US"> <html lang="en-US">
<head> <head>
<script>
window.dataLayer = [{}];
</script>
<script><%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY %></script>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
@ -11,9 +15,17 @@
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
</head> </head>
<body> <body>
<!-- Google Tag Manager -->
<noscript>
<iframe src="<%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC %>"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<noscript> <noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>

View file

@ -6,7 +6,9 @@
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''"> <fieldset class="w-100" :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''">
<legend class="sr-only">{{groupName}}</legend> <legend class="sr-only" :data-focus-target="groupName" tabindex="-1">
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
</legend>
<div :class="getComponentWrapperClasses"> <div :class="getComponentWrapperClasses">
<component <component
:is="buttonType" :is="buttonType"
@ -33,6 +35,7 @@
:selectedValues="selectedValues" :selectedValues="selectedValues"
data-test="button" data-test="button"
:validationRules="validationRules" :validationRules="validationRules"
:class="[suppressError ? 'alertError' : '']"
/> />
</div> </div>
</fieldset> </fieldset>

View file

@ -22,13 +22,13 @@
isPrimary isPrimary
:buttonText="buttonText" :buttonText="buttonText"
loaderColor="white" loaderColor="white"
:class="isDisabled && 'form-test-invalid'" :class="isForwardActionDisabled && 'form-test-invalid'"
:aria-disabled="isDisabled" :aria-disabled="isForwardActionDisabled"
:isDisabled="isDisabled" :isDisabled="isForwardActionDisabled"
@click-event="buttonClick" @click-event="buttonClick"
/> />
</div> </div>
<div class="col-auto link-col py-1 text-break"> <div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
<textLink <textLink
linkType="navigation" linkType="navigation"
:text="backLink" :text="backLink"
@ -48,8 +48,10 @@ import buttonMain from "@/ux-components/button-main/button-main";
export default { export default {
name: "funnelFooter", name: "funnelFooter",
props: { props: {
isDisabled: Boolean, isForwardActionDisabled: Boolean,
isBackButtonHidden: {type: Boolean, default: false},
cmsWidgetName: String, cmsWidgetName: String,
}, },
components: { components: {
textLink, textLink,
@ -70,6 +72,9 @@ export default {
beforeUnmount() { beforeUnmount() {
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
}, },
unmounted() {
document.onkeydown = null;
},
computed: { computed: {
backLink(){ backLink(){
return this.getCmsContent(this.cmsWidgetName, 'BackButtonText'); return this.getCmsContent(this.cmsWidgetName, 'BackButtonText');
@ -87,8 +92,15 @@ export default {
}, },
removeLoader(){ removeLoader(){
this.$refs.buttonMain.removeLoader(); this.$refs.buttonMain.removeLoader();
document.onkeydown = function (e) {
return true;
};
}, },
buttonClick() { buttonClick() {
//prevent keyboard input after button click
document.onkeydown = function (e) {
return false;
};
this.$emit("ForwardClicked"); this.$emit("ForwardClicked");
}, },
linkClick() { linkClick() {

View file

@ -1,25 +1,25 @@
<template> <template>
<div class="textbox-question" :class="hasError ? 'has-error' : ''"> <div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label> <label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<input v-model="value" <input v-model="value"
v-maska="mask" v-maska="mask"
:type="type" :type="type"
class="form-control" class="form-control"
:ref="inputId" :ref="inputId"
:id="inputId" :id="inputId"
:name="inputId" :name="inputId"
:placeholder="placeholderText" :placeholder="placeholderText"
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
:disabled="isDisabled" :disabled="isDisabled"
:aria-required="isRequired" :aria-required="isRequired"
autocomplete="off" autocomplete="off"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']" :class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules" :validationRules="validationRules"
@input="handleChange" @input="handleChange"
@blur="handleBlur" /> @blur="handleBlur" />
<div v-show="errorMessage" class="row mt-2 form-test-error"> <div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span> <span role="alert">{{ errorMessage }}</span>
</div> </div>
</div> </div>
</template> </template>
@ -56,7 +56,7 @@ export default {
setup(props) { setup(props) {
const fieldOptions = { const fieldOptions = {
type: "text", type: "text",
value: props.modelValue, value: props.modelValue,
}; };
const { const {
@ -64,15 +64,17 @@ export default {
handleBlur, handleBlur,
handleChange, handleChange,
meta, meta,
validate validate,
errors,
} = useField(props.inputId, props.validationRules, fieldOptions); } = useField(props.inputId, props.validationRules, fieldOptions);
return { return {
errorMessage, errorMessage,
handleBlur, handleBlur,
handleChange, handleChange,
validate, validate,
meta, meta,
errors,
}; };
}, },
computed: { computed: {
@ -86,7 +88,7 @@ export default {
set: function(newValue) { set: function(newValue) {
this.$emit("update:modelValue", newValue); this.$emit("update:modelValue", newValue);
} }
}, },
labelText: { labelText: {
get: function () { get: function () {
var thisQuestionText = ""; var thisQuestionText = "";
@ -102,7 +104,7 @@ export default {
return thisQuestionText; return thisQuestionText;
} }
} }
}, },
watch: { watch: {
value(newValue) { value(newValue) {

View file

@ -1,5 +1,10 @@
const cookieNames = { const cookieNames = {
FUNNEL_SESSION_INFO: "FunnelSessionInfo", FUNNEL_SESSION_INFO: "FunnelSessionInfo",
// Existing Safelite.com cookies
DXDEV: "dxdev",
SESSION_ID: "sid",
SESSION_KEY: "skey"
}; };
export { cookieNames }; export { cookieNames };

View file

@ -0,0 +1,7 @@
const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
ROUTER_LINK: "routerLink"
};
export { dynamicStrings };

View file

@ -67,6 +67,10 @@ const endpoints = {
url: "/location/api/v1/location/zip", url: "/location/api/v1/location/zip",
method: "GET", method: "GET",
}, },
LogExperimentExposureIfAssigned:{
url: "/analytics/api/v1/analytics/log-experiment-exposure",
method: "POST",
}
}; };
export { endpoints }; export { endpoints };

View file

@ -19,6 +19,8 @@ const errorMessages = {
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address", EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP", SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP", SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP",
VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Please enter a valid VIN",
}; };
export { errorMessages }; export { errorMessages };

View file

@ -0,0 +1,6 @@
const experimentUniverses = {
CONCEPT_FUNNEL: 'ConceptFunnel'
};
export { experimentUniverses };

View file

@ -18,6 +18,7 @@ const storeActions = {
LOAD_ORDER: "loadOrder", LOAD_ORDER: "loadOrder",
SET_REFERRAL_INFORMATION: "setReferralInformation", SET_REFERRAL_INFORMATION: "setReferralInformation",
VALIDATE_ZIP: "validateZip", VALIDATE_ZIP: "validateZip",
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",

View file

@ -30,6 +30,7 @@ export default {
} }
}, },
(error) => { (error) => {
console.error(error);
return reject(error.response); return reject(error.response);
} }
); );

View file

@ -1,5 +1,6 @@
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
import store from "@/store"; import store from "@/store";
import { dynamicStrings } from "../constants/dynamic-strings";
export function fetchCmsContentForPage(fmgPage) { export function fetchCmsContentForPage(fmgPage) {
return store return store
@ -40,12 +41,15 @@ export function fetchCmsContentForPage(fmgPage) {
function mapStringToState(str) { function mapStringToState(str) {
// Pull all matches out of the string. // Pull all matches out of the string.
const regexExp = new RegExp("{(.*?):(.*?)}", "g"); const regexExp = new RegExp("{(.*?):(.*?)}", "g");
const matches = [...str.matchAll(regexExp)]; const regexMatches = [...str.matchAll(regexExp)];
const globalStateMatches = regexMatches.filter(match => {
return match[1] === dynamicStrings.GLOBAL_STATE;
})
// Our final string value that will be built from the matches. // Our final string value that will be built from the matches.
let stringBuilder = ""; let stringBuilder = "";
for (const match of matches) { for (const match of globalStateMatches) {
// Reset store state for each match. // Reset store state for each match.
let storeState = store.state; let storeState = store.state;
@ -60,7 +64,7 @@ function mapStringToState(str) {
const stringWithReplacement = str.replace(match[0], storeState); const stringWithReplacement = str.replace(match[0], storeState);
// If we still have values we need to substitute, call this function again. // If we still have values we need to substitute, call this function again.
if (stringWithReplacement.includes("{globalState:")) { if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
return mapStringToState(stringWithReplacement); return mapStringToState(stringWithReplacement);
} }
@ -96,7 +100,7 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
function processWidgetItemForReplacement(widgetModel, key) { function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced. // If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === "string") { if (typeof widgetModel[key] === "string") {
if (widgetModel[key].includes("{globalState:")) { if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]); widgetModel[key] = mapStringToState(widgetModel[key]);
} }
return widgetModel[key]; return widgetModel[key];
@ -116,4 +120,4 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have something else like a number, boolean, etc. just return it // If we have something else like a number, boolean, etc. just return it
return widgetModel[key]; return widgetModel[key];
} }

View file

@ -0,0 +1,22 @@
import store from "@/store";
export function getDamageString() {
return store.getters.damage.glassToReplace.length > 1 ? "match" : store.getters.damage.glassToReplace[0].location;
}
export function compareGlassOptions(newOptions, currentOptions){
const optionsMap = {
Windshield: "windshieldOptions",
Driver: "driverSideOptions",
Passenger: "passengerSideOptions",
Rear: "backGlassOptions"
}
for(const option of currentOptions){
if(!newOptions[optionsMap[option.location]].availableReplacementOptions.includes(option.name)){
return true;
}
}
return false;
}

View file

@ -0,0 +1,37 @@
import {getDamageString, compareGlassOptions} from "./damage-helper";
jest.mock("@/store", () => ({
getters: {damage: {
glassToReplace: [{location: "TEST"}]
}
}
}));
describe("damage-helper.js", () => {
it("Should return damage getter info", () => {
const damage = getDamageString();
expect(damage).toEqual("TEST")
});
});
describe("damage-helper.js", () => {
it("Should return false if no mismatches between each array", () => {
const newOptions = {
windshieldOptions: {availableReplacementOptions: ["windshield"]}
}
const currentOptions = [{location: "Windshield", name: "windshield"}];
const misMatch = compareGlassOptions(newOptions, currentOptions);
expect(misMatch).toEqual(false);
});
});
describe("damage-helper.js", () => {
it("Should return true if there are any mismatches between arrays", () => {
const newOptions = {
windshieldOptions: {availableReplacementOptions: ["window"]}
}
const currentOptions = [{location: "Windshield", name: "windshield"}];
const misMatch = compareGlassOptions(newOptions, currentOptions);
expect(misMatch).toEqual(true);
});
});

View file

@ -18,19 +18,20 @@ export function updateOrCreateFunnelCookie() {
ReferralNumber: store.getters.order.referralNumber, ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate, ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId, ReferralCorrelationId: store.getters.order.referralCorrelationId,
ReferralParentAccountNumber: store.getters.order.parentAccountNumber,
}); });
} }
/* /*
Gets the current instance of the funnel cookie. Gets the current instance of the funnel cookie.
Returns null if cookie isn't valid JSON. Returns null if cookie isn't valid JSON.
*/ */
export function getFunnelCookie() { export function getFunnelCookie() {
const cookieJson = document.cookie const cookieJson = document.cookie
?.split("; ") ?.split("; ")
?.find(row => row.startsWith(`${cookieNames.FUNNEL_SESSION_INFO}=`)) ?.find(row => row.startsWith(`${cookieNames.FUNNEL_SESSION_INFO}=`))
?.split("=")[1]; ?.split("=")[1];
try { try {
return JSON.parse(cookieJson); return JSON.parse(cookieJson);
} catch (error) { } catch (error) {
@ -45,6 +46,55 @@ export function deleteFunnelCookie() {
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=; Max-Age=0; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()}`; document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=; Max-Age=0; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()}`;
} }
/*
Gets cookie domain value. Localhost will be empty "".
*/
export function getCookieDomainValue() {
return location.hostname.includes("localhost") ? "" : `domain=${getDomainWithoutSubdomain()};`;
}
/*
Gets value of dxdev cookie, and then extracts "did" value from it.
Returns empty string if cookie not found or "did" string not present.
*/
export function getDeviceIdValue(){
// Sometimes these cookie contains more than the device ID.
const cookieValue = getCookieValueByName(cookieNames.DXDEV);
const cookieValuesSplit = cookieValue.split('=');
// If this is the only value, just use that.
if(cookieValuesSplit.length === 2 && cookieValuesSplit[0] === 'did'){
return cookieValuesSplit[1];
}
const cookieValueMatch = cookieValue.match("^did=[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}");
if(cookieValueMatch){
return cookieValueMatch[0].split('=')[1];
}
return '';
}
/*
Gets value of skey cookie, returns 0 if not found.
*/
export function getSessionKeyValue(){
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY);
if(cookieValue){
return cookieValue;
}
return 0;
}
/*
===========================
= PRIVATE FUNCTIONS =
===========================
*/
/* /*
Used to set properties on the funnel cookie. Used to set properties on the funnel cookie.
Takes an object with properties to set. Will overwrite existing properties. Takes an object with properties to set. Will overwrite existing properties.
@ -65,10 +115,9 @@ function setFunnelCookieProperties(properties) {
} }
} }
export function getCookieDomainValue() { /*
return location.hostname.includes("localhost") ? "" : `domain=${getDomainWithoutSubdomain()};`; Gets current domain without the subdomain for cookie.
} */
function getDomainWithoutSubdomain() { function getDomainWithoutSubdomain() {
let url = location.hostname; let url = location.hostname;
if (url.includes("localhost")) { if (url.includes("localhost")) {
@ -82,3 +131,16 @@ function getDomainWithoutSubdomain() {
.slice(-(urlParts.length === 4 ? 3 : 2)) .slice(-(urlParts.length === 4 ? 3 : 2))
.join('.')}`; .join('.')}`;
} }
/*
Gets cookie value by name, returns empty string if not found.
*/
function getCookieValueByName(name) {
const value = "; " + document.cookie;
const parts = value.split("; " + name + "=");
if (parts.length === 2) {
return parts.pop().split(";").shift();
}
return "";
}

View file

@ -1,4 +1,4 @@
import {getFunnelCookie} from "@/helpers/heritage-integration/cookie-helper.js"; import {getFunnelCookie, getDeviceIdValue, getSessionKeyValue} from "@/helpers/heritage-integration/cookie-helper.js";
import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper"; import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper";
describe("cookies", () => { describe("cookies", () => {
@ -100,5 +100,31 @@ describe("cookies", () => {
expect(actualCookieValue).toBeNull(); expect(actualCookieValue).toBeNull();
}); });
}) })
describe("getDeviceIdValue", () => {
test("getDeviceIdValue, should return GUID", () => {
// Arrange
setupCookies({});
// Act
const result = getDeviceIdValue();
//Assert
expect(result).toBe('21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe');
});
test("getSessionKeyValue, should return session key int", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionKeyValue();
//Assert
expect(result).toBe('12345');
});
});
}) })

View file

@ -2,55 +2,38 @@ import { queryStrings } from "@/constants/query-strings";
import { externalUrls } from "@/router/router-constants/externalUrl-values"; import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { saveOrder } from "@/helpers/heritage-integration/order-helper.js"; import { saveOrder } from "@/helpers/heritage-integration/order-helper.js";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import store from "@/store"; import store from "@/store";
import router from "@/router"; import router from "@/router";
/* /*
If the user has visited the funnel before this method will determine the bets place to If the user has visited the funnel before this method will determine the bets place to
drop them so they don't start at the beginning again. This method will return 'heritage' if drop them so they don't start at the beginning again. This method will return 'heritage' if
the user has an existing order and they come back in from the Safelite.com CTA. the user has an existing order and they come back in from the Safelite.com CTA.
*/ */
export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) { export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) {
// If the user is coming in via the Safelite.Com CTA // If the user is coming in via the Safelite.Com CTA
if (toRoute.query[queryStrings.START_TYPE] === 'fmg') { if (toRoute.query[queryStrings.START_TYPE] === 'fmg') {
// If they have an existing order, return 'heritage' for the page name. // If they have an existing order, return 'heritage' for the page name.
if (existingHeritageOrder) { if (existingHeritageOrder) {
return 'heritage'; return 'heritage';
} }
return await getLatestPageForRedirection();
} }
// If navigating to a specific page, and that page is not part of the vin pages.
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites. // Return that page, so that it can navigate like normal.
// This also works if a user has a 'fmg' start_type query string but no current order. if (toRoute.query[queryStrings.FMG_PAGE] !== undefined && !isVinRelatedPage(toRoute)) {
// That shouldn't happen, but it's possible. return overrideYmmsDirectionIfNeeded(toRoute);
const vehicleMakeComponent = (await lazyLoadComponent('vehicle-make')()).default;
const vehicleModelComponent = (await lazyLoadComponent('vehicle-model')()).default;
const vehicleStyleComponent = (await lazyLoadComponent('vehicle-style')()).default;
const vehicleDamageComponent = (await lazyLoadComponent('vehicle-damage')()).default;
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-year";
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-make";
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-model";
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-style";
} else if (store.getters.damage.isRepair == null || !store.getters.vehicle.carId) {
return 'vehicle-damage'
} else {
if (store.getters.vehicle.vin) {
return 'vehicle-damage';
//return "vin-lookup"; (uncomment)
} else {
return 'vehicle-damage';
//return "estimate" (uncomment)
}
} }
// If this is not a direct link to a page using fmgPage, not from Safelite.com CTA or this is a vin related page.
// Get the latest page for redirection.
const latestPageRoute = await getLatestPageForRedirection();
return latestPageRoute;
} }
/* /*
@ -65,10 +48,81 @@ export async function navigateToHeritageFunnel() {
externalUrls.HERITAGE_FUNNEL, externalUrls.HERITAGE_FUNNEL,
{ {
corid: store.getters.order.referralCorrelationId, corid: store.getters.order.referralCorrelationId,
src: "concept-funnel", src: "concept-funnel"
// TODO CSR-28, remove this
cns: "all",
experiments: "RemoveServiceAreaPage=ServAreaRemoval_V7=NoShowPackages_CONTROL=true,ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_TEST=true"
} }
); );
} }
/*
Logic for getting the last "valid" page a user visited.
*/
async function getLatestPageForRedirection() {
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites.
// This also works if a user has a 'fmg' start_type query string but no current order.
// That shouldn't happen, but it's possible.
const vehicleMakeComponent = (await lazyLoadComponent('vehicle-make')()).default;
const vehicleModelComponent = (await lazyLoadComponent('vehicle-model')()).default;
const vehicleStyleComponent = (await lazyLoadComponent('vehicle-style')()).default;
const vehicleDamageComponent = (await lazyLoadComponent('vehicle-damage')()).default;
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_YEAR;
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MAKE;
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MODEL;
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_STYLE;
} else if (store.getters.damage.isRepair == null || !store.getters.vehicle.carId) {
return fmgPageValues.VEHICLE_DAMAGE;
} else {
if (store.getters.vehicle.vin) {
return fmgPageValues.VIN_LOOKUP;
} else {
return fmgPageValues.ESTIMATE;
}
}
}
/*
Overrides functionality to go to the YMMS pages in certain cases.
If this is not one of the cases, it returns the 'to' fmgPage value.
*/
/* istanbul ignore next */
function overrideYmmsDirectionIfNeeded(toRoute) {
const fmgPageValue = toRoute.query[queryStrings.FMG_PAGE];
if (store.getters.payment.insuranceCoverage.isVerified) {
switch (fmgPageValue) {
case fmgPageValues.VEHICLE_YEAR:
case fmgPageValues.VEHICLE_MAKE:
case fmgPageValues.VEHICLE_MODEL:
case fmgPageValues.VEHICLE_STYLE:
{
return fmgPageValues.VEHICLE_DAMAGE;
}
default: {
return fmgPageValue;
}
}
} else {
return fmgPageValue;
}
}
/*
Determine if the page is a vin related page.
*/
/* istanbul ignore next */
function isVinRelatedPage(toRoute) {
const fmgPageValue = toRoute.query[queryStrings.FMG_PAGE];
return fmgPageValue === fmgPageValues.VIN_LOOKUP ||
fmgPageValue === fmgPageValues.LICENSE_PLATE_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_VEHICLES ||
fmgPageValue === fmgPageValues.ESTIMATE;
}

View file

@ -228,8 +228,7 @@ describe("getPageToRouteExistingOrderTo", () => {
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
//expect(result).toBe('vin-lookup'); expect(result).toBe('vin-lookup');
expect(result).toBe('vehicle-damage');
}); });
test("getPageToRouteExistingOrderTo, should return estimate", async () => { test("getPageToRouteExistingOrderTo, should return estimate", async () => {
@ -284,8 +283,7 @@ describe("getPageToRouteExistingOrderTo", () => {
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
//expect(result).toBe('estimate'); expect(result).toBe('estimate');
expect(result).toBe('vehicle-damage');
}); });
test("getPageToRouteExistingOrderTo, existing order, should return heritage", async () => { test("getPageToRouteExistingOrderTo, existing order, should return heritage", async () => {

View file

@ -25,7 +25,7 @@ export async function loadOrderIfPresent() {
// Load referral if there is a cookie, and it doesn't indicate it needs a state reset. // Load referral if there is a cookie, and it doesn't indicate it needs a state reset.
return (await loadOrder(funnelCookie.ReferralNumber, funnelCookie.ReferralDate, funnelCookie.ReferralCorrelationId)).data; return (await loadOrder(funnelCookie.ReferralNumber, funnelCookie.ReferralDate, funnelCookie.ReferralCorrelationId, funnelCookie.ReferralParentAccountNumber)).data;
} }
/* /*
@ -54,12 +54,13 @@ export async function saveOrder() {
Calls API to load order given the referral number, referralDate, and referralCorrelationId Calls API to load order given the referral number, referralDate, and referralCorrelationId
and returns the response. and returns the response.
*/ */
async function loadOrder(referralNumber, referralDate, referralCorrelationId) { async function loadOrder(referralNumber, referralDate, referralCorrelationId, accountNumber) {
const response = await baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOAD_ORDER, const response = await baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOAD_ORDER,
{ {
referralNumber: referralNumber.toString(), referralNumber: referralNumber.toString(),
referralDate: referralDate, referralDate: referralDate,
referralCorrelationId: referralCorrelationId referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber?.toString()
}, false); }, false);
return response; return response;

View file

@ -15,10 +15,10 @@ export function settleAllPromises(promiseResultMap) {
// when returned, so other promises do. Map the results to the object // when returned, so other promises do. Map the results to the object
// so that the object is the return data. // so that the object is the return data.
if (results[i].value.data === undefined) { if (results[i]?.value?.data === undefined) {
resultMap[promiseName] = results[i].value; resultMap[promiseName] = results[i]?.value;
} else { } else {
resultMap[promiseName] = results[i].value.data; resultMap[promiseName] = results[i]?.value?.data;
} }
} }

View file

@ -60,7 +60,9 @@ export const cookies = {
[cookieNames.FUNNEL_SESSION_INFO]: `{"ReferralNumber":"1566818","ReferralDate":"2022-03-15T10:56:24.597","ReferralCorrelationId":"404d2b04-f86e-45c3-b373-127b6217b060","ShouldResetState":false,"DidHeritageFunnelUpdateLast":true}`, [cookieNames.FUNNEL_SESSION_INFO]: `{"ReferralNumber":"1566818","ReferralDate":"2022-03-15T10:56:24.597","ReferralCorrelationId":"404d2b04-f86e-45c3-b373-127b6217b060","ShouldResetState":false,"DidHeritageFunnelUpdateLast":true}`,
"UNIQUE_SESSION_ID": "33756020-b58e-4ec7-b8b8-3f1576719c40", "UNIQUE_SESSION_ID": "33756020-b58e-4ec7-b8b8-3f1576719c40",
"anotherCookie": "{}", "anotherCookie": "{}",
"someOtherCookie": "{}" "someOtherCookie": "{}",
"dxdev": "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
"skey": "12345"
}; };
export function removeAllTestCookies() { export function removeAllTestCookies() {

View file

@ -44,6 +44,7 @@
ref="funnelFooter" ref="funnelFooter"
:isDisabled="!meta.valid" :isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
/> />
</div> </div>

View file

@ -0,0 +1,13 @@
<template>
<p> Estimate </p>
</template>
<script>
export default {
name: "estimate",
methods: {
arePagePrerequisitesValid() {
return true;
},
}
};
</script>

View file

@ -44,7 +44,7 @@
/> />
<funnel-footer <funnel-footer
ref="funnelFooter" ref="funnelFooter"
:isDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
/> />

View file

@ -26,7 +26,8 @@
</div> </div>
<alert <alert
class="my-3" class="my-3"
cmsWidgetName="NoServiceZipWidget" :manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-if="newServiceZipRequired" v-if="newServiceZipRequired"
alertClass="alert-danger" alertClass="alert-danger"
/> />
@ -38,7 +39,8 @@
/> />
<alert <alert
class="my-3" class="my-3"
cmsWidgetName="MatchedDifferentVehicleAlertWidget" :manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-if="vinDoesNotMatchCarId" v-if="vinDoesNotMatchCarId"
alertClass="alert-warning" alertClass="alert-warning"
/> />
@ -50,7 +52,7 @@
<funnelFooter <funnelFooter
ref="funnelFooter" ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
:isDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
/> />
@ -75,8 +77,10 @@ import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { getDamageString, compareGlassOptions } from "@/helpers/damage-helper";
import { required, regex } from "@/helpers/validation-rules"; import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED)); defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
@ -118,8 +122,32 @@ export default {
zip: '', zip: '',
email: '', email: '',
serviceZip: '', serviceZip: '',
carIdEntered: '',
customAlertData: {},
newCarId: false,
glassOptionsMismatch: false,
}; };
}, },
computed: {
MatchedDifferentVehicleAlertHeader(){
let text = this.getCmsContent("MatchedDifferentVehicleAlertWidget", "HeadlineText").replaceAll("{custom:damage}", getDamageString());
return text;
},
MatchedDifferentVehicleAlertBody(){
let text = this.getCmsContent("MatchedDifferentVehicleAlertWidget", "BodyText").replaceAll("{custom:damage}", getDamageString()).replaceAll("{custom:plateLookupYear}", this.customAlertData?.vehicleInfo?.year).replaceAll("{custom:plateLookupMake}", this.customAlertData?.vehicleInfo?.make).replaceAll("{custom:plateLookupModel}", this.customAlertData?.vehicleInfo?.model);
return text;
},
NoServiceZipHeader(){
let text = this.getCmsContent("NoServiceZipWidget", "HeadlineText").replaceAll("{custom:zip}", this.zip);
return text;
},
NoServiceZipBody(){
return this.getCmsContent("NoServiceZipWidget", "BodyText");
}
},
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null; return store.getters.vehicle.carId !== null;
@ -137,43 +165,58 @@ export default {
async forwardButtonAction() { async forwardButtonAction() {
const zipValidation = this.serviceZip ? await this.validateZip(this.serviceZip) : await this.validateZip(this.zip); const zipValidation = this.serviceZip ? await this.validateZip(this.serviceZip) : await this.validateZip(this.zip);
if (!zipValidation.data.isServiceable) { if (!zipValidation.data.isServiceable) {
this.customAlertData.zip = this.zip;
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
this.vinDoesNotMatchCarId = false;
this.vinNotValid = false;
this.newServiceZipRequired = true; this.newServiceZipRequired = true;
return; return;
} }
const vinLookup = await this.lookupVin(this.licensePlate, zipValidation.data.state).catch(() => { const vinLookup = await this.lookupVin(this.licensePlate, zipValidation.data.state).catch(() => {
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
this.vinDoesNotMatchCarId = false;
this.vinNotValid = true; this.vinNotValid = true;
return; return;
}); });
if (vinLookup.data.vehicle.carId !== store.getters.vehicle.carId) {
if ((vinLookup.data.vehicle.carId !== store.getters.vehicle.carId) && (vinLookup.data.vehicle.carId !== this.carIdEntered)) {
this.carIdEntered = vinLookup.data.vehicle.carId;
this.customAlertData.vehicleInfo = vinLookup.data.vehicle;
this.newCarId = true;
const glassOptions = await baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: vinLookup.data.vehicle.carId }
);
this.glassOptionsMismatch = compareGlassOptions(glassOptions.data, store.getters.damage.glassToReplace);
this.$refs.funnelFooter.updateButtonText(`Continue with ${vinLookup.data.vin} ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}`); this.$refs.funnelFooter.updateButtonText(`Continue with ${vinLookup.data.vin} ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}`);
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
this.vinNotValid = false;
this.vinDoesNotMatchCarId = true; this.vinDoesNotMatchCarId = true;
return; return;
} }
this.updateCustomerInfo(vinLookup.data.vin, vinLookup.data.vehicle, zipValidation.data.state);
const partsData = await baseMixin.methods.dispatchNonBlockingStoreAction( const partsData = await baseMixin.methods.dispatchNonBlockingStoreAction(
this.storeActions.GET_PARTS_OR_QUESTIONS, this.storeActions.GET_PARTS_OR_QUESTIONS,
{ {
carId: store.getters.vehicle.carId, carId: vinLookup.data.vehicle.carId,
glassArray: store.getters.damage.glassToReplace, glassArray: store.getters.damage.glassToReplace ? store.getters.damage.glassToReplace : [],
zipCode: this.zip, zipCode: this.serviceZip ? this.serviceZip : this.zip,
vin: vinLookup.vin vin: vinLookup.data.vin
}, },
false false
); );
this.navigateForward(partsData); this.navigateForward(partsData);
}, },
navigateForward(partsData){ navigateForward(partsData){
if(partsData.data.partsOrQuestions[0].partQuestions && partsData.data.partsOrQuestions[0].partQuestions.length > 0){ if(this.newCarId && this.glassOptionsMismatch){
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_PARTS_QUESTION, this.$route, {}, {}, partsData.data); this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD, this.$route, {}, { displayVehicleChangeAlert: true }, partsData.data);
return; return;
} else if((!partsData.data.partsOrQuestions[0].partQuestions || partsData.data.partsOrQuestions[0].partQuestions.length < 1) && partsData.data.partsOrQuestions[0].parts.length > 1) {
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_PARTS, this.$route, {}, {}, partsData.data);
return;
} else { } else {
this.$router.navigate(this.navigationScenarios.CONTINUING_WITH_SINGLE_PART, this.$route); navigateToHeritageFunnel();
return;
} }
}, },
validateZip(zip) { validateZip(zip) {
@ -188,27 +231,32 @@ export default {
{ licensePlate: plate, licenseState: state } { licensePlate: plate, licenseState: state }
); );
}, },
updateStore() { updateCustomerInfo(vin, vehicleInfo, registrationState) {
// if(vehicleDamage){ if(this.newCarId && this.glassOptionsMismatch){
// store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
// } }
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null); store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, null); store.commit(storeMutations.UPDATE_YEAR, vehicleInfo.year);
store.commit(storeMutations.UPDATE_MAKE, null); store.commit(storeMutations.UPDATE_MAKE, vehicleInfo.make);
store.commit(storeMutations.UPDATE_MODEL, null); store.commit(storeMutations.UPDATE_MODEL, vehicleInfo.model);
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, vehicleInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, vehicleInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicleInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor);
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, null); store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, this.licensePlate);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, null); store.commit(storeMutations.UPDATE_REGISTRATION_STATE, registrationState);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, null); store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.zip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP, null); store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP, this.serviceZip);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, null); store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
}, },
}, },
watch: {
licensePlate() {
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
}
},
components: { components: {
Form, Form,
funnelHeader, funnelHeader,

View file

@ -1,10 +1,12 @@
<template> <template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall"> <div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage="false" /> <vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false />
<funnelSubHeader ref="funnelSubHeader" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<h1>Part Questions Page Placeholder</h1> <h1>Part Questions Page Placeholder</h1>
<funnelFooter ref="funnelFooter" @back-clicked="backButtonAction" /> <funnel-footer
cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction"
/>
</div> </div>
</template> </template>
@ -49,18 +51,7 @@ export default {
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.$refs.funnelHeader.initializeComponent( vm.setCmsContent(resultMap.cmsContent);
resultMap.cmsContent.FunnelHeaderWidget
);
vm.$refs.vehicleBanner.initializeComponent(
resultMap.cmsContent.VehicleBannerWidget
);
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
}); });
}, },
methods: { methods: {

View file

@ -36,6 +36,11 @@ jest.mock("@/store", () => ({
vehicle: { vehicle: {
carId: "C00000000", carId: "C00000000",
image: "test.jpg", image: "test.jpg",
payment: {
insuranceCoverage: {
isVerified: false
}
}
}, },
eventBusItem: jest.fn(), eventBusItem: jest.fn(),
damage: { damage: {
@ -134,6 +139,7 @@ describe("vehicle-damage.vue", () => {
store: { store: {
getters: { getters: {
vehicle: {}, vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
}, },
}, },
}, },
@ -201,6 +207,7 @@ describe("vehicle-damage.vue", () => {
store: { store: {
getters: { getters: {
vehicle: {}, vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
}, },
}, },
}, },
@ -291,6 +298,7 @@ describe("vehicle-damage.vue", () => {
store: { store: {
getters: { getters: {
vehicle: {}, vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
}, },
}, },
}, },
@ -336,6 +344,7 @@ describe("vehicle-damage.vue", () => {
store: { store: {
getters: { getters: {
vehicle: {}, vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
}, },
}, },
}, },
@ -734,6 +743,7 @@ function setupMocks({
store: { store: {
getters: { getters: {
vehicle: {}, vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
}, },
}, },
}, },

View file

@ -47,7 +47,8 @@
/> />
<funnel-footer <funnel-footer
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
:isDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
:isBackButtonHidden=shouldHideBackButton
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
/> />
@ -145,9 +146,11 @@ export default {
} }
return false; return false;
}, },
resetDependentState() { resetDependentState() {
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
}, },
backButtonAction() { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate( this.$router.navigate(
@ -155,19 +158,20 @@ export default {
this.$route this.$route
); );
}, },
getDamageLocationsFromStore() { getDamageLocationsFromStore() {
var glassSelections = []; var glassSelections = [];
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD }) || if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD }) ||
store.getters.damage.isRepair) { store.getters.damage.isRepair) {
glassSelections.push(damageLocationsSelected.WINDSHIELD); glassSelections.push(damageLocationsSelected.WINDSHIELD);
} }
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.DRIVER || if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.DRIVER ||
glass.location === damageLocationsSelected.PASSENGER })) { glass.location === damageLocationsSelected.PASSENGER })) {
glassSelections.push(damageLocationsSelected.SIDEDOOR); glassSelections.push(damageLocationsSelected.SIDEDOOR);
} }
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.REAR })) { if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.REAR })) {
glassSelections.push(damageLocationsSelected.REARWINDOW); glassSelections.push(damageLocationsSelected.REARWINDOW);
} }
@ -180,19 +184,19 @@ export default {
if (store.getters.damage.isRepair === undefined) return windshieldOptions; if (store.getters.damage.isRepair === undefined) return windshieldOptions;
if (!store.getters.damage.isRepair) { if (!store.getters.damage.isRepair) {
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD && if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.SINGLE })) { glass.name === damageLocationsSelected.SINGLE })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE); windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE);
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.SINGLE); windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.SINGLE);
} }
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD && if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.DRIVER })) { glass.name === damageLocationsSelected.DRIVER })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE); windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE);
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.DRIVER); windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.DRIVER);
} }
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD && if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.PASSENGER })) { glass.name === damageLocationsSelected.PASSENGER })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE); windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE);
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.PASSENGER); windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.PASSENGER);
@ -210,11 +214,11 @@ export default {
getDoorSidesFromStore() { getDoorSidesFromStore() {
var doorSides = []; var doorSides = [];
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.DRIVER })){ if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.DRIVER })){
doorSides.push(damageLocationsSelected.DRIVERSIDE); doorSides.push(damageLocationsSelected.DRIVERSIDE);
} }
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.PASSENGER })){ if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.PASSENGER })){
doorSides.push(damageLocationsSelected.PASSENGERSIDE); doorSides.push(damageLocationsSelected.PASSENGERSIDE);
} }
@ -224,7 +228,7 @@ export default {
getDriverSideReplaceOptionsFromStore() { getDriverSideReplaceOptionsFromStore() {
var driverSideReplaceOptions = []; var driverSideReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => { store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.DRIVER){ if (glass.location === damageLocationsSelected.DRIVER){
driverSideReplaceOptions.push(glass.name); driverSideReplaceOptions.push(glass.name);
} }
@ -236,7 +240,7 @@ export default {
getPassengerSideReplaceOptionsFromStore() { getPassengerSideReplaceOptionsFromStore() {
var passengerSideReplaceOptions = []; var passengerSideReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => { store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.PASSENGER){ if (glass.location === damageLocationsSelected.PASSENGER){
passengerSideReplaceOptions.push(glass.name); passengerSideReplaceOptions.push(glass.name);
} }
@ -248,7 +252,7 @@ export default {
getRearReplaceOptionsFromStore(){ getRearReplaceOptionsFromStore(){
var rearReplaceOptions = []; var rearReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => { store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.REAR){ if (glass.location === damageLocationsSelected.REAR){
rearReplaceOptions.push(glass.name); rearReplaceOptions.push(glass.name);
} }
@ -275,13 +279,20 @@ export default {
}, },
navigateForward(partsData){ navigateForward(partsData){
// CSR-98 TEMP
const vehicleYearsToShowHeritageFunnel = [ "2001", "2002", "2010" ]; // Temporary easter egg to navigate to heritage funnel.
const vehicleYearsToShowHeritageFunnel = [ 2001, 2002, 2010, 2016 ];
if (vehicleYearsToShowHeritageFunnel.includes(store.getters.vehicle.year)) { if (vehicleYearsToShowHeritageFunnel.includes(store.getters.vehicle.year)) {
navigateToHeritageFunnel(); navigateToHeritageFunnel();
return; return;
} }
// If vin already exists, navigate directly to vin-lookup
if(this.$store.getters.vehicle.vin){
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, this.$route);
return;
}
//found problem questions //found problem questions
if (partsData.data.partsOrQuestions.some(pq => pq.partQuestions != null && pq.partQuestions.length > 0)){ if (partsData.data.partsOrQuestions.some(pq => pq.partQuestions != null && pq.partQuestions.length > 0)){
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_DAMAGE_WITH_PART_QUESTIONS, this.$route, {}, {}, partsData.data); this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_DAMAGE_WITH_PART_QUESTIONS, this.$route, {}, {}, partsData.data);
@ -382,7 +393,7 @@ export default {
return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair; return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair;
}, },
hasSplitSingleConflict() { hasSplitSingleConflict() {
if (!this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false; if (!this.selectedDamageLocations || !this.selectedDamageLocations.includes("Windshield") || !this.selectedWindshieldOptions.selectedWindshieldDamageType || !this.selectedWindshieldOptions.selectedWindshieldDamageType.includes("Replace") || !this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false;
return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield => return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield =>
{ {
@ -398,6 +409,9 @@ export default {
}) })
); );
}, },
shouldHideBackButton(){
return this.$store.getters.payment.insuranceCoverage.isVerified;
}
}, },
components: { components: {

View file

@ -17,11 +17,16 @@ import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { experimentUniverses } from "@/constants/experiments";
import { getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import baseMixin from "@/mixins/base-mixin";
import store from "@/store"; import store from "@/store";
export default { export default {
@ -36,8 +41,16 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const yearQuestionInitialDataPromise = const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
yearQuestion.methods.loadInitialData();
// Log experiment exposure
const logExperimentExposurePromise = baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOG_EXPERIMENT_EXPOSURE,
{
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
universeName: experimentUniverses.CONCEPT_FUNNEL
}, false);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
@ -49,6 +62,10 @@ export default {
resultKey: "yearQuestionInitialData", resultKey: "yearQuestionInitialData",
promise: yearQuestionInitialDataPromise, promise: yearQuestionInitialDataPromise,
}, },
{
resultKey: "logExperimentExposure",
promise: logExperimentExposurePromise,
},
]; ];
let resultMap = await settleAllPromises(promiseResultMap); let resultMap = await settleAllPromises(promiseResultMap);
@ -64,8 +81,8 @@ export default {
watch: { watch: {
selectedYear(year) { selectedYear(year) {
const parsedYear = parseInt(year);
this.$store.commit(this.storeMutations.UPDATE_YEAR, year); this.$store.commit(this.storeMutations.UPDATE_YEAR, parsedYear);
this.$router.navigateAfterSave( this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_YEAR, this.navigationScenarios.SELECTED_YEAR,
this.$route this.$route

View file

@ -1,41 +1,272 @@
<template> <template>
<div class="container-fluid shadow rounded-3 px-5 position-relative make-tall"> <Form
<funnelHeader ref="funnelHeader" /> @submit="onSubmit"
<vehicleBanner ref="vehicleBanner" /> @invalid-submit="onInvalidSubmit"
<funnelSubHeader ref="funnelSubHeader" /> ref="theForm"
<vinInformation v-slot="{ meta }"
class="mb-5" >
/> <div class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5">
<funnel-footer <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
ref="funnelFooter" <vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
:isDisabled="!meta.valid" <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
@back-clicked="backButtonAction" <div class="row my-2">
@ForwardClicked="forwardButtonAction" <div class="col">
/> <textboxQuestion cmsWidgetName="VinNumber" v-model="vin" inputId="vin" disableAutoFill validationRules="vin-required|vin-format" :isDisabled=isVinFieldReadOnly />
</div> </div>
</div>
<div class="row my-2">
<div class="col">
<vinInformation />
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="ServiceZIP" v-model="zip" inputId="zip" mask="#####" disableAutoFill validationRules="zip-required" />
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="EmailAddress" v-model="email" inputId="email" disableAutoFill validationRules="email-address-required|email-address-format" />
</div>
</div>
<alert
class="my-3"
v-model="customAlertData"
v-if="matchedDifferentVehicle"
alertClass="alert-danger"
cmsWidgetName="MatchedDifferentVehicle"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="noMatchAlert"
alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="noServiceZip"
alertClass="alert-warning"
cmsWidgetName="NoServiceZipWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinFound"
alertClass="alert-warning"
cmsWidgetName="VinFoundWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinFoundReadOnly"
alertClass="alert-warning"
cmsWidgetName="VinFoundReadOnlyWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="foundWindshieldAlert"
alertClass="alert-warning"
cmsWidgetName="FoundWindshieldAlert"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinNotFound"
alertClass="alert-warning"
cmsWidgetName="VinNotFound"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="perfectMatchNewVinAlert"
alertClass="alert-warning"
cmsWidgetName="PerfectMatchNewVinAlert"
/>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</Form>
</template> </template>
<script> <script>
// Components // Components
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information"; import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
// DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.ZIP_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
defineRule("vin-required", required(errorMessages.VIN_REQUIRED));
defineRule("vin-format", regex(/^[A-HJ-NPR-Z0-9]{17}$/, errorMessages.VIN_FORMAT));
export default { export default {
name: "vin-lookup", name: "vin-lookup",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
props: {
validationRules: String,
},
data() {
return {
matchedDifferentVehicle: false,
noMatchAlert: false,
noServiceZip: false,
vinFound: false,
vinFoundReadOnly: false,
foundWindshieldAlert: false,
vinNotFound: false,
perfectMatchNewVinAlert: false,
vin: '',
zip: '',
email: '',
customAlertData: {},
};
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
resetDependentState() {
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, null);
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, null);
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, null);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, null);
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
const zipValidation = await this.validateZip(this.zip);
if (!zipValidation.data.isServiceable) {
this.customAlertData.zip = this.zip;
this.$refs.funnelFooter.removeLoader();
this.noServiceZip = true;
return;
}
const vinLookup = await this.lookupVin(this.vin).catch(() => {
this.$refs.funnelFooter.removeLoader();
this.noMatchAlert = true;
return;
});
if (vinLookup.data.carId !== store.getters.vehicle.carId) {
this.customAlertData.vehicleInfo = vinLookup.data.vehicle;
this.$refs.funnelFooter.removeLoader();
this.foundWindshieldAlert = true;
return;
}
const carInfo = this.vinDoesNotMatchCarId ? vinLookup.data : store.getters.vehicle;
this.updateStore(carInfo)
const partsData = await baseMixin.methods.dispatchNonBlockingStoreAction(
this.storeActions.GET_PARTS_OR_QUESTIONS,
{
carId: store.getters.vehicle.carId,
glassArray: store.getters.damage.glassToReplace,
zipCode: this.zip,
vin: vinLookup.vin
},
false
);
this.navigateForward(partsData);
},
navigateForward(partsData){
if(partsData.data.partsOrQuestions[0].partQuestions && partsData.data.partsOrQuestions[0].partQuestions.length > 0){
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_PARTS_QUESTION, this.$route, {}, {}, partsData.data);
return;
} else if((!partsData.data.partsOrQuestions[0].partQuestions || partsData.data.partsOrQuestions[0].partQuestions.length < 1) && partsData.data.partsOrQuestions[0].parts.length > 1) {
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_PARTS, this.$route, {}, {}, partsData.data);
return;
} else {
this.$router.navigate(this.navigationScenarios.CONTINUING_WITH_SINGLE_PART, this.$route);
}
},
validateZip(zip) {
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.VALIDATE_ZIP,
{ zip }
);
},
lookupVin(vin) {
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin }
);
},
updateStore(carInfo) {
// if(vehicleDamage){
// store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
// }
store.commit(storeMutations.UPDATE_VEHICLE_VIN, this.vin);
store.commit(storeMutations.UPDATE_YEAR, carInfo.year);
store.commit(storeMutations.UPDATE_MAKE, carInfo.make);
store.commit(storeMutations.UPDATE_MODEL, carInfo.model);
store.commit(storeMutations.UPDATE_STYLE, carInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, carInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, carInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, carInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, carInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, carInfo.imageColor);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP, this.zip);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
},
},
computed: {
isVinFieldReadOnly(){
return this.$store.getters.payment.insuranceCoverage.isVerified;
}
},
components: { components: {
Form,
funnelHeader, funnelHeader,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
vinInformation, textboxQuestion,
alert,
funnelFooter, funnelFooter,
vinInformation,
}, },
}; };
</script> </script>
<style lang="scss">
</style>

View file

@ -3,6 +3,7 @@ import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js"; import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { vehicleCategories } from "@/constants/vehicle-categories.js"; import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { queryStrings } from "@/constants/query-strings";
export default { export default {
data() { data() {
@ -11,10 +12,10 @@ export default {
}; };
}, },
methods: { methods: {
setCmsContent(cmsContent){ setCmsContent(cmsContent) {
this.$root.cmsContentByWidget = cmsContent; this.$root.cmsContentByWidget = cmsContent;
}, },
getCmsContent(widgetName, fieldName){ getCmsContent(widgetName, fieldName) {
return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : ''; return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : '';
}, },
dispatchNonBlockingStoreAction(type, payload, encodePayload = true) { dispatchNonBlockingStoreAction(type, payload, encodePayload = true) {
@ -25,10 +26,10 @@ export default {
return store.dispatch(type, payload); return store.dispatch(type, payload);
}, },
savePageDataToStore(page, data){ savePageDataToStore(page, data) {
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data }); store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data });
}, },
onSubmit() {}, // DO NOT REMOVE; needed to prevent default form submit behavior onSubmit() { }, // DO NOT REMOVE; needed to prevent default form submit behavior
onInvalidSubmit({ values, errors, results }) { onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it // identify the first error field and put focus on it
// get error names array // get error names array
@ -40,6 +41,28 @@ export default {
el && el.focus(); el && el.focus();
} }
}, },
pushEventToGA(category, action, label, value, pageName) {
const eventToBePushed = {
'event': 'ga_event',
'category': category,
'action': action,
'label': label,
'value': value,
'path': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`
}
pushToDataLayerIfDefined(eventToBePushed);
},
pushPageViewToGA(pageName) {
const pageViewEvent = {
'event': 'logPageview',
'pagePath': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`,
'pageTitle': pageName
};
pushToDataLayerIfDefined(pageViewEvent);
}
}, },
computed: { computed: {
storeActions() { storeActions() {
@ -65,3 +88,9 @@ function encodeUriData(payload) {
}); });
} }
} }
function pushToDataLayerIfDefined(data) {
if (window.dataLayer !== undefined) {
window.dataLayer.push(data);
}
}

View file

@ -4,12 +4,13 @@ import { storeActions } from "@/constants/store-actions";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { routingTable } from "@/router/router-constants/routing-table.js"; import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events"; import { globalEvents, globalEventTypes } from "@/constants/events";
import { queryStrings } from "@/constants/query-strings";
// Heritage integration // Heritage integration
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
import { updateOrCreateFunnelCookie,getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { updateOrCreateFunnelCookie, getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper"; import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper";
import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel} from "@/helpers/heritage-integration/navigation-helper"; import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import eventBus from "@/helpers/event-bus/event-bus"; import eventBus from "@/helpers/event-bus/event-bus";
@ -35,84 +36,79 @@ const routes = [
name: "root", name: "root",
async beforeEnter(to, from, next) { async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string. // If we have no query string, or we don't have the FmgPage query string.
if (to.query.fmgPage === undefined) { try {
await GoToFunnelStartOn404(next);
} else {
try {
// If the saved session has timed out, clear the session, execute 404 logic. // If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
await GoToFunnelStartOn404(next);
}
// On entering the funnel "fresh", read cookie information, decide what to do next.
if (from.redirectedFrom === undefined) {
const loadOrderResponse = await loadOrderIfPresent();
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse);
// If getPageToRouteExistingOrderTo determines that the return user needs to
// go back to heritage funnel, send them there and stop our current navigation.
if (pageToRedirectTo === 'heritage') {
await navigateToHeritageFunnel();
return next(false);
}
// Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo;
}
// Process funnel cookie.
updateOrCreateFunnelCookie();
// If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) {
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === to.query.fmgPage)[0].components;
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
}
if (!arePagePrerequisitesValid(component)) {
await GoToFunnelStartOn404(next);
}
return next({ name: to.query.fmgPage, query: to.query, params: to.params });
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
const routeData = await GetRouteInfoFromPageName(to.query.fmgPage);
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component,
});
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router.getRoutes().filter((x) => x.name === routeData[0].name)[0].components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
await GoToFunnelStartOn404(next);
}
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { fmgPage: routeData[0].name }),
params: to.params
});
} catch (error) {
console.log(error);
// If we don't have a route, go to our 404 page.
await GoToFunnelStartOn404(next); await GoToFunnelStartOn404(next);
} }
// On entering the funnel "fresh", read cookie information, decide what to do next.
if (from.redirectedFrom === undefined) {
const loadOrderResponse = await loadOrderIfPresent();
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse);
// If getPageToRouteExistingOrderTo determines that the return user needs to
// go back to heritage funnel, send them there and stop our current navigation.
if (pageToRedirectTo === 'heritage') {
await navigateToHeritageFunnel();
return next(false);
}
// Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo;
}
// Process funnel cookie.
updateOrCreateFunnelCookie();
// If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) {
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === to.query.fmgPage)[0].components;
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
}
if (!arePagePrerequisitesValid(component)) {
await GoToFunnelStartOn404(next);
}
return next({ name: to.query.fmgPage, query: to.query, params: to.params });
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
const routeData = await GetRouteInfoFromPageName(to.query.fmgPage);
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component,
});
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router.getRoutes().filter((x) => x.name === routeData[0].name)[0].components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
await GoToFunnelStartOn404(next);
}
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { fmgPage: routeData[0].name }),
params: to.params
});
} catch (error) {
console.log(error);
// If we don't have a route, go to our 404 page.
await GoToFunnelStartOn404(next);
} }
}, },
}, },
@ -125,6 +121,10 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ---------------------------------------------------------- //---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.afterEach((to, from) => {
baseMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]);
});
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => { router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => {
navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData); navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData);
} }
@ -205,6 +205,13 @@ function navigateToUrl(url, optionalQuery = {}) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
} }
/////////////////////////////////////////////////////
// TEMP CODE FOR TESTING WITH SPECIFIC EXPERIMENTS //
/////////////////////////////////////////////////////
if (externalUrl.search.indexOf("corid=") != -1)
externalUrl.search = externalUrl.search + '&experiments=CollectEmailOnQuote=CollectEmailOnQuote_V1=YesCollectEmail_TEST1=true,RemoveServiceAreaPage=ServAreaRemoval_V7=ServAreaNoRemove_V7_TEST=true,VINeducationV2=VINeducation_V2=NoShowVINmodalV2_CONTROL=true,ServicePackages=ServicePackages_V1=NoShowPackages_CONTROL=true,PhotoUploadRedesign=PhotoUploadRedesign_V1=CurrentPhotoUpload_CONTROL=true,ScheduleDetailsServiceType=ScheduleBeforeServiceType_V1=ServTypeThenSched_CONTROL=true';
///////////// END TEMP CODE /////////////////////////
window.location.assign(externalUrl); window.location.assign(externalUrl);
} }

View file

@ -11,6 +11,7 @@ const fmgPageValues = {
LICENSE_PLATE_LOOKUP: "license-plate-lookup", LICENSE_PLATE_LOOKUP: "license-plate-lookup",
REVEAL: "reveal", REVEAL: "reveal",
ESTIMATE: "estimate", ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles",
}; };
export { fmgPageValues }; export { fmgPageValues };

View file

@ -4,7 +4,9 @@ const navigationScenarios = {
SELECTED_MAKE: "SELECTED_MAKE", SELECTED_MAKE: "SELECTED_MAKE",
SELECTED_STYLE: "SELECTED_STYLE", SELECTED_STYLE: "SELECTED_STYLE",
CLICKED_BACK: "CLICKED_BACK", CLICKED_BACK: "CLICKED_BACK",
CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN",
CLICKED_FORWARD: "CLICKED_FORWARD", CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN",
SELECTED_PARTS: "SELECTED_PARTS", SELECTED_PARTS: "SELECTED_PARTS",
SELECTED_DAMAGE_WITH_SINGLE_PART: "SELECTED_DAMAGE_WITH_SINGLE_PART", SELECTED_DAMAGE_WITH_SINGLE_PART: "SELECTED_DAMAGE_WITH_SINGLE_PART",
SELECTED_DAMAGE_WITH_MULTIPLE_PARTS: "SELECTED_DAMAGE_WITH_MULTIPLE_PARTS", SELECTED_DAMAGE_WITH_MULTIPLE_PARTS: "SELECTED_DAMAGE_WITH_MULTIPLE_PARTS",

View file

@ -1,6 +1,5 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { applicationConfig } from "@/constants/application-config";
const routingTable = [ const routingTable = [
{ {
@ -59,6 +58,10 @@ const routingTable = [
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE, destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
}, },
// UNDO THESE CHANGES BEFORE MERGING TO develop // UNDO THESE CHANGES BEFORE MERGING TO develop
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
},
{ {
scenario: navigationScenarios.SELECTED_DAMAGE_WITH_SINGLE_PART, scenario: navigationScenarios.SELECTED_DAMAGE_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP
@ -109,6 +112,10 @@ const routingTable = [
maps: [ maps: [
{ {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
}, },
{ {
@ -136,6 +143,10 @@ const routingTable = [
scenario: navigationScenarios.CONTINUING_WITH_SINGLE_PART, scenario: navigationScenarios.CONTINUING_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.REVEAL, destinationFmgPageValue: fmgPageValues.REVEAL,
}, },
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
], ],
}, },
{ {

View file

@ -45,10 +45,16 @@ const getDefaultState = () => {
glassParts: null, glassParts: null,
otherParts: null otherParts: null
}, },
payment:{
isInsurance: null,
insuranceCoverage: {
isVerified: null
}
},
referralNumber: null, referralNumber: null,
referralDate: null, referralDate: null,
referralCorrelationId: null, referralCorrelationId: null,
parentAccountNumber: null, parentAccountNumber: 0,
}, },
applicationUser: { applicationUser: {
eventBus: [], eventBus: [],
@ -93,7 +99,7 @@ export const mutations = {
updateVehicleVin(state, vin) { updateVehicleVin(state, vin) {
state.order.vehicle.vin = vin; state.order.vehicle.vin = vin;
}, },
updateIsRepair(state, isRepair){ updateIsRepair(state, isRepair) {
state.order.damage.isRepair = isRepair; state.order.damage.isRepair = isRepair;
}, },
updateNumberOfChips(state, numberOfChips) { updateNumberOfChips(state, numberOfChips) {
@ -120,6 +126,27 @@ export const mutations = {
updateParentAcctNumber(state, parentAcctNumber) { updateParentAcctNumber(state, parentAcctNumber) {
state.order.parentAccountNumber = parentAcctNumber; state.order.parentAccountNumber = parentAcctNumber;
}, },
updateIsInsurance(state, isInsurance) {
state.order.payment.isInsurance = isInsurance;
},
updateInsuranceVerifiedStatus(state, isVerified) {
state.order.payment.insuranceCoverage.isVerified = isVerified;
},
updateRegistrationLicensePlate(state, licensePlate){
state.order.vehicle.registration.licensePlate = licensePlate;
},
updateRegistrationState(state, registrationState){
state.order.vehicle.registration.state = registrationState;
},
updateRegistrationZipCode(state, reistrationZipCode){
state.order.vehicle.registration.zipCode = reistrationZipCode;
},
updateServiceLocationZip(state, serviceLocationZip){
state.order.serviceLocation.zip = serviceLocationZip;
},
updateCustomerEmailAddress(state, customerEmailAddress){
state.order.customer.emailAddress = customerEmailAddress;
},
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
@ -176,23 +203,30 @@ export const mutations = {
state.order.referralNumber = orderInformation.referralNumber; state.order.referralNumber = orderInformation.referralNumber;
state.order.referralDate = orderInformation.referralDate; state.order.referralDate = orderInformation.referralDate;
state.order.referralCorrelationId = orderInformation.referralCorrelationId; state.order.referralCorrelationId = orderInformation.referralCorrelationId;
state.order.vehicle = {
state.order.vehicle = Object.assign(state.order.vehicle, {
year: orderInformation.vehicle?.year, year: orderInformation.vehicle?.year,
make: orderInformation.vehicle?.make, make: orderInformation.vehicle?.make,
model: orderInformation.vehicle?.model, model: orderInformation.vehicle?.model,
style: orderInformation.vehicle?.style, style: orderInformation.vehicle?.style,
vin: orderInformation.vehicle?.vin,
carId: orderInformation.vehicle?.carId, carId: orderInformation.vehicle?.carId,
category: orderInformation.vehicle?.category, category: orderInformation.vehicle?.category,
imageUrl: orderInformation.vehicle?.imageUrl, imageUrl: orderInformation.vehicle?.imageUrl,
imageVifNumber: orderInformation.vehicle?.imageVifNumber, imageVifNumber: orderInformation.vehicle?.imageVifNumber,
imageColor: orderInformation.vehicle?.imageVifColor imageColor: orderInformation.vehicle?.imageVifColor,
}; });
state.order.damage.glassToReplace = orderInformation.glassToReplace; state.order.damage.glassToReplace = orderInformation.glassToReplace;
state.order.damage.isRepair = orderInformation.isRepair; state.order.damage.isRepair = orderInformation.isRepair;
state.order.damage.numberOfChips = orderInformation.numberOfChips; state.order.damage.numberOfChips = orderInformation.numberOfChips;
state.order.lineItems.glassParts = orderInformation.parts; state.order.lineItems.glassParts = orderInformation.parts;
state.order.parentAccountNumber = orderInformation.parentAccountNumber; state.order.parentAccountNumber = orderInformation.parentAccountNumber;
state.order.serviceLocation.zipCode = orderInformation.zipCode; // TODO CSR-416 Make sure this is correct state.order.serviceLocation.zipCode = orderInformation.zipCode;
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified;
} }
} }
@ -210,11 +244,10 @@ export const getters = {
eventBus: (state) => state.applicationUser.eventBus, eventBus: (state) => state.applicationUser.eventBus,
damage: (state) => state.order.damage, damage: (state) => state.order.damage,
lineItems: (state) => state.order.lineItems, lineItems: (state) => state.order.lineItems,
pageData: (state) => (page) => { pageData: (state) => (page) => { return state.applicationUser.pageData[page]; },
return state.applicationUser.pageData[page];
},
applicationUser: (state) => state.applicationUser, applicationUser: (state) => state.applicationUser,
order: (state) => state.order, order: (state) => state.order,
payment: (state) => state.order.payment,
} }
// Export Actions // Export Actions
@ -248,7 +281,7 @@ export const actions = {
method: endpoints.LookupVinByPlate.method, method: endpoints.LookupVinByPlate.method,
endpoint: endpoints.LookupVinByPlate.url, endpoint: endpoints.LookupVinByPlate.url,
payload: { payload: {
licensePlate: licensePlate, licensePlate: licensePlate,
licenseState: licenseState licenseState: licenseState
}, },
}); });
@ -304,12 +337,12 @@ export const actions = {
}, },
getDamageOptions(context, { carId }) { getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method, methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`, endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {}, payload: {},
}); });
}, },
validateZip(context, {zip}) { validateZip(context, { zip }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.ValidateZip.method, methods: endpoints.ValidateZip.method,
endpoint: `${endpoints.ValidateZip.url}/${zip}` endpoint: `${endpoints.ValidateZip.url}/${zip}`
@ -375,6 +408,19 @@ export const actions = {
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
}, },
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) {
return globalMethods.callHttpClient({
method: endpoints.LogExperimentExposureIfAssigned.method,
endpoint: endpoints.LogExperimentExposureIfAssigned.url,
payload: {
userId: userId,
sessionKey: sessionKey,
pageName: pageName,
universeName: universeName
}
});
},
// Parts API Actions // Parts API Actions
getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) { getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -414,16 +460,18 @@ export const actions = {
}); });
}, },
loadOrder(context, { referralNumber, referralDate, referralCorrelationId }) { loadOrder(context, { referralNumber, referralDate, referralCorrelationId, accountNumber}) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LoadOrder.method, method: endpoints.LoadOrder.method,
endpoint: endpoints.LoadOrder.url, endpoint: endpoints.LoadOrder.url,
payload: { payload: {
referralNumber: referralNumber, referralNumber: referralNumber,
referralDate: referralDate, referralDate: referralDate,
referralCorrelationId: referralCorrelationId referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber
}, },
}).then((response) => { }).then((response) => {
context.commit(storeMutations.RESET_STATE);
context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data); context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data);
return response; return response;
}); });

View file

@ -222,6 +222,7 @@ describe("Mutations", () => {
numberOfChips: 0, numberOfChips: 0,
parts: [], parts: [],
parentAccountNumber: "123456789", parentAccountNumber: "123456789",
insuranceInfo: {}
}); });
// Assert // Assert
@ -232,6 +233,17 @@ describe("Mutations", () => {
expect(storeState.order.vehicle.model).toEqual("ILX"); expect(storeState.order.vehicle.model).toEqual("ILX");
}); });
it("updateInsuranceVerifiedStatus, should set isVerified flag", () => {
// Arrange
const storeState = state;
// Act
mutations.updateInsuranceVerifiedStatus(storeState, true);
// Assert
expect(storeState.order.payment.insuranceCoverage.isVerified).toEqual(true);
});
}); });
describe("Actions", () => { describe("Actions", () => {
@ -690,4 +702,15 @@ describe("Getters", () => {
}); });
it("Payment getter, should return payment data", () => {
// Arrange
const storeState = state;
//Act
mutations.updateInsuranceVerifiedStatus(storeState, true );
//Assert
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);
});
}); });

View file

@ -1,92 +1,126 @@
.has-error { html {
&.list-button, .has-error {
&.list-card { &.list-button,
border: 1px solid $red; &.list-card {
color: $red;
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 0 2.5px $red;
}
input[type=checkbox]:focus + label:hover,
input[type=radio]:focus + label {
box-shadow: 0 0 0 1px $red;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px !important;
border: 1px solid $red;
}
}
input[type=checkbox]:checked + label,
input[type=radio]:checked + label {
box-shadow: 0 0 0 2.5px transparent !important;
}
&.list-button-horizontal {
color: $red;
label {
border: 1px solid $red; border: 1px solid $red;
color: $red;
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 0 2.5px $red;
}
input[type=checkbox]:checked + label {
box-shadow: 0 0 0 1px $red;
}
&:hover { &:hover {
box-shadow: 0px 0px 0px 4px $red-200; box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px;
} }
} }
input[type=checkbox]:focus + label, &.list-button-horizontal {
input[type=radio]:focus + label {
box-shadow: 0 0 1px $red !important;
}
}
&.ui-radio,
&.ui-checkbox {
input[type=checkbox],
input[type=radio],
input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
color: $red; color: $red;
} label {
input, border: 1px solid $red;
select { &:hover {
border: 1px solid $red; box-shadow: 0px 0px 0px 4px $red-200;
&:focus { }
border: 1px solid transparent; }
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 1px $red;
} }
} }
select { &.ui-radio,
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e"); &.ui-checkbox {
background-repeat: no-repeat; input[type=checkbox],
background-position: right 0.75rem center; input[type=radio],
background-size: 16px 12px; input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
color: $red;
}
input,
select {
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
}
}
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
}
}
}
//Restore to default style if alert box is present
.alertError {
.has-error {
&.list-button,
&.list-card {
border: 1px solid $gray-500;
input:not(:focus) {
+ label {
box-shadow: 0 0 0 1px $gray-500;
border-radius: .5rem;
}
}
input:checked:focus {
+ label {
box-shadow: 0 0 0 1px $blue;
border-radius: .5rem;
}
}
input:checked:not(:focus) {
+ label {
box-shadow: 0 0 0 1px $blue;
border-radius: .5rem;
}
}
input:focus {
border: 1px solid $gray-500;
+ label {
box-shadow: 0 0 0 2.5px transparent;
}
}
&:hover {
box-shadow: 0 0 0 4px $blue-300;
+ label {
box-shadow: 0 0 0 2.5px transparent;
border: 1px solid $blue;
}
}
}
}
}
.form-test-error {
color: $red;
font-size: .875rem;
font-weight: 500;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
background: $gray-200;
cursor: pointer;
pointer-events: all;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200;
box-shadow: none;
} }
} }
} }
.form-test-error {
color: $red;
font-size: .875rem;
font-weight: 500;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
background: $gray-200;
cursor: pointer;
pointer-events: all;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200 !important;
box-shadow: none !important;
}
}

View file

@ -1,4 +1,4 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import alert from "./alert"; import alert from "./alert";
describe("alert.vue", () => { describe("alert.vue", () => {
@ -9,6 +9,13 @@ describe("alert.vue", () => {
propsData: { propsData: {
isDismissible: true isDismissible: true
}, },
computed: {
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin] mixins: [mockMixin]
}); });
@ -24,6 +31,13 @@ describe("alert.vue", () => {
propsData: { propsData: {
alertClass: 'warning' alertClass: 'warning'
}, },
computed: {
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin] mixins: [mockMixin]
}); });

View file

@ -5,7 +5,15 @@
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]" :class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
> >
<p class="m-0 fw-bold small alert-heading">{{ alertHeadline }}</p> <p class="m-0 fw-bold small alert-heading">{{ alertHeadline }}</p>
<p class="m-0 text-body small">{{ alertCopy }}</p> <p v-if="splitAlertCopyForLink.length">
<template v-for="copy in splitAlertCopyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="m-0 text-body small">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link>
</span>
<span v-else class="m-0 text-body small" v-html="copy"></span>
</template>
</p>
<p v-else class="m-0 text-body small" v-html="alertCopy"></p>
<button <button
type="button" type="button"
class="btn-close p-2" class="btn-close p-2"
@ -26,6 +34,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "alert", name: "alert",
props: { props: {
@ -49,6 +58,10 @@ export default {
alertCopy(){ alertCopy(){
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy; return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
}, },
splitAlertCopyForLink(){
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.alertCopy.split(/{(.*?)}/g);
}
}, },
}; };
</script> </script>
@ -115,5 +128,9 @@ export default {
height: 1rem; height: 1rem;
} }
} }
& p {
font-size: 14px;
margin-bottom: 0px;
}
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@mouseup="handleClick(value)" @mouseup="handleClick(value)"
@keyup.space="handleClick(value)" @keyup.space="handleClick(value)"
> >
@ -11,7 +11,6 @@
:name="groupName" :name="groupName"
:value="value" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''" @change="!selectingInitiatesLoad ? handleCheckChange() : ''"
/> />
@ -143,11 +142,11 @@ export default {
height: 0; height: 0;
position: absolute; position: absolute;
&:focus-visible + label { &:focus-visible + label {
box-shadow: 0 0 0 2px $blue; box-shadow: 0 0 0 2.5px $blue;
z-index: 2; z-index: 2;
} }
&:focus + label { &:focus + label {
box-shadow: 0 0 0 2px $blue; box-shadow: 0 0 0 2.5px $blue;
z-index: 3; z-index: 3;
} }
&:checked + label { &:checked + label {
@ -156,6 +155,9 @@ export default {
outline: none; outline: none;
z-index: 2; z-index: 2;
} }
&:checked:focus + label {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + label p:first-child { &:checked + label p:first-child {
font-weight: 500; font-weight: 500;
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="list-group list-button d-flex flex-column w-100 mb-2" class="list-group list-button d-flex flex-column w-100 mb-2"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@mouseup="handleClick(value)" @mouseup="handleClick(value)"
@keyup.space="handleClick(value)" @keyup.space="handleClick(value)"
> >
@ -11,7 +11,6 @@
:name="groupName" :name="groupName"
:value="value" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''" @change="!selectingInitiatesLoad ? handleCheckChange() : ''"
> >
@ -97,7 +96,7 @@ export default {
} }
this.handleChange(value); this.handleChange(value);
}, },
handleCheckChange(newValue, oldValue){ handleCheckChange(value, oldValue){
const isInitialization = typeof(oldValue) === 'function'; const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) { if (!isInitialization) {
const emitEvent = { const emitEvent = {

View file

@ -4,8 +4,7 @@
class="list-card w-100 rounded-3 d-flex align-items-center h-100" class="list-card w-100 rounded-3 d-flex align-items-center h-100"
:class="[ :class="[
isWide ? 'horizontal' : '', isWide ? 'horizontal' : '',
errors.length > 0 ? 'has-error' : '', (errors.length > 0 || hasError) ? 'has-error' : '',
hasError ? 'has-error' : '',
]" ]"
@mouseup="handleChange(value)" @mouseup="handleChange(value)"
@keyup.space="handleChange(value)" @keyup.space="handleChange(value)"
@ -16,7 +15,6 @@
:name="groupName" :name="groupName"
:value="value" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="handleCheckChange(value)" @change="handleCheckChange(value)"
/> />
@ -315,8 +313,8 @@ export default {
&:checked + label::after { &:checked + label::after {
content: ""; content: "";
margin: -0.25rem 0 0 0; margin: -0.15rem 0 0 0;
left: 0.875rem; left: 1.175rem;
} }
&:checked + label { &:checked + label {

View file

@ -3,7 +3,13 @@ process.env.VUE_APP_CONSUMER_API_GATEWAY =
process.env.VUE_APP_HERITAGE_FUNNEL = process.env.VUE_APP_HERITAGE_FUNNEL =
"http://localhost:38000/default.aspx"; "http://localhost:38000/default.aspx";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = process.env.VUE_APP_GOOGLE_PLACES_API_KEY =
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo" "AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo";
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-M6XCRH');";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC = "https://www.googletagmanager.com/ns.html?id=GTM-M6XCRH&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x";
module.exports = { module.exports = {
outputDir: "dist/fmg", outputDir: "dist/fmg",

View file

@ -2,6 +2,10 @@ process.env.VUE_APP_CONSUMER_API_GATEWAY = "__VUE_APP_CONSUMER_API_GATEWAY__";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "__VUE_APP_GOOGLE_PLACES_API_KEY__"; process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "__VUE_APP_GOOGLE_PLACES_API_KEY__";
process.env.VUE_APP_HERITAGE_FUNNEL = "__VUE_APP_HERITAGE_FUNNEL__"; process.env.VUE_APP_HERITAGE_FUNNEL = "__VUE_APP_HERITAGE_FUNNEL__";
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC = "__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__"
module.exports = { module.exports = {
outputDir: "dist/fmg", outputDir: "dist/fmg",
publicPath: "/fmg", publicPath: "/fmg",