diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 3ac871d20..23d5e6806 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -100,6 +100,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
+ __VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
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__)
@@ -160,6 +161,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
+ __VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
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__)
@@ -220,6 +222,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
+ __VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
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__)
@@ -280,6 +283,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
+ __VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
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__)
diff --git a/src/constants/application-config.js b/src/constants/application-config.js
index e49426afb..14f195cfc 100644
--- a/src/constants/application-config.js
+++ b/src/constants/application-config.js
@@ -37,6 +37,8 @@ const applicationConfig = {
FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging",
AFFILIATE_COOKIE_CONTAINING_NAME: "_Track",
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
+ SESSION_TIMEOUT_MINUTES: process.env.VUE_APP_SESSION_TIMEOUT_MINUTES,
+ RETURN_USER_PAGE: "/fmg/?fmgPage=return-user",
};
export { applicationConfig };
diff --git a/src/constants/cookie-names.js b/src/constants/cookie-names.js
index 72e6168fb..9a6c9a2d1 100644
--- a/src/constants/cookie-names.js
+++ b/src/constants/cookie-names.js
@@ -16,7 +16,7 @@ const cookieNames = {
};
const cookieExpirations = {
- SESSION_ID: convertToSeconds({ minutes: 30 }),
+ SESSION_ID: convertToSeconds({ minutes: applicationConfig.SESSION_TIMEOUT_MINUTES }),
DXDEV: convertToSeconds({ years: 1 }),
FUNNEL_USER_ID: convertToSeconds({ weeks: 1 }),
FUNNEL_SESSION_KEY: convertToSeconds({ minutes: 30 }),
diff --git a/src/constants/query-strings.js b/src/constants/query-strings.js
index 3cdfd655f..210ac28f0 100644
--- a/src/constants/query-strings.js
+++ b/src/constants/query-strings.js
@@ -45,6 +45,7 @@ const queryStrings = {
ORGANIC: "organic",
ORGANIC_SOCIAL: "organic_social",
EXPERIMENTS: "experiments",
+ FROM_HERITAGE: "fromheritage",
};
export { queryStrings };
diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js
index 740c5a47e..f625e27cf 100644
--- a/src/constants/store-mutations.js
+++ b/src/constants/store-mutations.js
@@ -16,6 +16,8 @@ const storeMutations = {
UPDATE_VEHICLE_IMAGE_COLOR: "updateVehicleImageColor",
UPDATE_VEHICLE_VIN: "updateVehicleVin",
UPDATE_VEHICLE: "updateVehicle",
+ UPDATE_VEHICLE_MOBILE_STATIC_RECALIBRATION_APPLICABLE:
+ "updateIsMobileStaticRecalibrationApplicable",
UPDATE_IS_REPAIR: "updateIsRepair",
UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips",
diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue
index b060167cb..f9c1b6279 100644
--- a/src/digital-components/button-question/button-question.vue
+++ b/src/digital-components/button-question/button-question.vue
@@ -226,7 +226,7 @@ export default {
if (this.buttonTypeString == "radio") {
classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") {
- classes = "package-wrapper col-md-4";
+ classes = "package-wrapper col";
}
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {
diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue
index 5ca708288..58260e831 100644
--- a/src/digital-components/modal/modal.vue
+++ b/src/digital-components/modal/modal.vue
@@ -27,17 +27,17 @@
-
+
+
@@ -173,7 +173,7 @@ export default {
}
}
.modal-footer {
- padding: 1rem 0;
+ padding: 1rem 1.5rem;
position: sticky;
width: 100%;
bottom: 0;
@@ -216,8 +216,12 @@ export default {
overflow: auto;
flex: none;
}
+ .textbox-question {
+ padding: 0.25rem 0;
+ }
}
}
+
&.modal-dialog-centered {
align-items: flex-end;
min-height: 100%;
@@ -269,8 +273,6 @@ export default {
flex-direction: column;
.textbox-question {
- padding: 0;
-
label {
text-align: left;
font-weight: 900;
@@ -284,6 +286,10 @@ export default {
.modal-disclaimer {
font-size: 0.75rem;
order: 2;
+ text-align: left;
+ a {
+ text-decoration: none;
+ }
}
.modal-footer {
margin: 0 0 1.5rem 0;
diff --git a/src/digital-components/modal/ux-components/modal-button-main/modal-button-main.vue b/src/digital-components/modal/ux-components/modal-button-main/modal-button-main.vue
index 687ae6858..41f9d2d8a 100644
--- a/src/digital-components/modal/ux-components/modal-button-main/modal-button-main.vue
+++ b/src/digital-components/modal/ux-components/modal-button-main/modal-button-main.vue
@@ -128,7 +128,7 @@ export default {
@include blue-gradient;
}
&:focus, // Mouse, touch, stylus focus
- &:focus-visible {
+ &:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow:
diff --git a/src/fmg-components/nav-bar/nav-bar.vue b/src/fmg-components/nav-bar/nav-bar.vue
index 631e5c0e5..f23ecd244 100644
--- a/src/fmg-components/nav-bar/nav-bar.vue
+++ b/src/fmg-components/nav-bar/nav-bar.vue
@@ -45,6 +45,9 @@
+
diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js
index 1e7b6bdb3..0e86c4143 100644
--- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js
+++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js
@@ -11,7 +11,10 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
// Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_FEE_PART,
- null,
+ {
+ serviceZipCode: serviceZipCode,
+ serviceZipCodeCtu: zipCodeData.zipCodeCtu,
+ },
pageNameToLog,
false
);
diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue
index 84087cb6e..d88bd53e2 100644
--- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue
+++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue
@@ -241,57 +241,46 @@ export default {
this.displayMismatchStateAndZipAlert = false;
},
async setMobileLocation() {
- if (
- this.internalModel.addressQuestions.zipCode !==
- this.modelValue.addressQuestions.zipCode
- ) {
- this.resetAlerts();
- // Validate the Zip Code
- const zipCodeData = await this.getZipCodeData(
- this.internalModel.addressQuestions.zipCode,
+ this.resetAlerts();
+ // Validate the Zip Code
+ const zipCodeData = await this.getZipCodeData(
+ this.internalModel.addressQuestions.zipCode,
+ "service-location"
+ );
+
+ if (!zipCodeData.isValid) {
+ this.displayInvalidZipAlert = true;
+ this.resetModalButtonStyle();
+ } else if (zipCodeData.state != this.internalModel.addressQuestions.state) {
+ this.displayMismatchStateAndZipAlert = true;
+ this.resetModalButtonStyle();
+ } else {
+ // retrieve mobile fee part
+ const serviceZipCode = this.internalModel.addressQuestions.zipCode;
+ const mobileFeePart = await getPricedMobileFeePart(
+ serviceZipCode,
"service-location"
);
- if (!zipCodeData.isValid) {
- this.displayInvalidZipAlert = true;
- this.resetModalButtonStyle();
- } else if (zipCodeData.state != this.internalModel.addressQuestions.state) {
- this.displayMismatchStateAndZipAlert = true;
- this.resetModalButtonStyle();
- } else {
- // retrieve mobile fee part
- const serviceZipCode = this.internalModel.addressQuestions.zipCode;
- const mobileFeePart = await getPricedMobileFeePart(
- serviceZipCode,
- "service-location"
- );
+ // retrieve serviceability details
+ const serviceabilityDetails = await getServiceabilityDetails(
+ serviceZipCode,
+ null,
+ "service-location"
+ );
- // retrieve serviceability details
- const serviceabilityDetails = await getServiceabilityDetails(
- serviceZipCode,
- null,
- "service-location"
- );
+ const billToAccountNumber = await getBillToAccountNumber(
+ this.internalModel.zipCodeCtu
+ );
- const billToAccountNumber = await getBillToAccountNumber(
- this.internalModel.zipCodeCtu
- );
+ // update content related to service zip code
+ this.$emit("updated-mobile-fee-part", mobileFeePart);
+ this.$emit("updated-serviceability", serviceabilityDetails.data);
+ this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
+ this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
+ this.$emit("updated-bill-to-account-number", billToAccountNumber);
- // update content related to service zip code
- this.$emit("updated-mobile-fee-part", mobileFeePart);
- this.$emit("updated-serviceability", serviceabilityDetails.data);
- this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
- this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
- this.$emit("updated-bill-to-account-number", billToAccountNumber);
-
- // update the page level model
- this.$emit("update:modelValue", this.internalModel);
-
- //Page advance to Schedule page
- this.$emit("mobileLocationSelected");
- }
- } else {
- // Update the page level model
+ // update the page level model
this.$emit("update:modelValue", this.internalModel);
//Page advance to Schedule page
diff --git a/src/layouts/service-location/service-location.spec.js b/src/layouts/service-location/service-location.spec.js
index eea946b89..6333082ec 100644
--- a/src/layouts/service-location/service-location.spec.js
+++ b/src/layouts/service-location/service-location.spec.js
@@ -181,6 +181,9 @@ beforeEach(() => {
zipCode: "43235",
state: "OH",
},
+ vehicle: {
+ isMobileStaticRecalibrationApplicable: true,
+ },
},
damage: {
isRepair: false,
diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue
index 4017088c4..71bfa4b01 100644
--- a/src/layouts/service-location/service-location.vue
+++ b/src/layouts/service-location/service-location.vue
@@ -159,6 +159,7 @@ import { defineRule } from "vee-validate";
import { errorMessages } from "@/constants/error-messages";
const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
+const MOBILE_STATIC_RECAL_FEE_PART_NUMBER = "RECAL MOBILE";
// DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => {
@@ -188,6 +189,8 @@ export default {
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
+ isVehicleMobileStaticRecalibrationApplicable:
+ this.getIsVehicleMobileStaticRecalibrationApplicableFromStore(),
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
@@ -306,11 +309,21 @@ export default {
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
- return this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile;
+ return (
+ (this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile) ||
+ this.isMobileStaticRecalibrationApplicable
+ );
} else {
return this.isGlassServiceableMobile;
}
},
+ isMobileStaticRecalibrationApplicable() {
+ return (
+ this.isVehicleMobileStaticRecalibrationApplicable &&
+ this.mobileFeePart?.partNumber == MOBILE_STATIC_RECAL_FEE_PART_NUMBER &&
+ (this.isInsurance ? this.mobileFeePart?.isInsurable : true)
+ );
+ },
mobileFeeApplies() {
if (
this.mobileFeePart?.laborAmount > 0 ||
@@ -349,7 +362,8 @@ export default {
return (
this.isServiceableInshop &&
this.isGlassServiceableMobile &&
- this.isRecalibrationServiceableMobile === false
+ this.isRecalibrationServiceableMobile === false &&
+ !this.isMobileStaticRecalibrationApplicable
);
},
displayRecalibrationWarning() {
@@ -485,6 +499,9 @@ export default {
getSelectedProvider() {
return store.getters.order.serviceLocation.provider;
},
+ getIsVehicleMobileStaticRecalibrationApplicableFromStore() {
+ return store.getters.order.vehicle.isMobileStaticRecalibrationApplicable;
+ },
resetMobileLocation() {
this.streetAddress = "";
this.apartmentNumberOrBusinessName = "";
diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue
index b0a1ffcd9..832589043 100644
--- a/src/layouts/vehicle-parts/vehicle-parts.vue
+++ b/src/layouts/vehicle-parts/vehicle-parts.vue
@@ -48,8 +48,8 @@
+ pageName="vehicle-parts"
+ v-if="showSaveProgressModal" />
@@ -75,7 +75,6 @@ import store from "@/store";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings";
-import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
export default {
name: "vehicle-parts",
@@ -95,7 +94,10 @@ export default {
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
- vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
+ // turned off Save Your Progress for 1/23/25 release
+ // to restore, uncomment line below
+ // vm.showSaveProgressModal = !(emailFromStore?.length > 0);
+
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
@@ -115,7 +117,7 @@ export default {
selectedGlassParts: {},
alertWidgetData: Object,
alreadyPopulatedPartsData: [],
- isEmailInStoreOnPageLoad: null,
+ showSaveProgressModal: null,
};
},
computed: {
@@ -234,9 +236,6 @@ export default {
});
});
},
- saveProgress() {
- saveQuote({ pageNameToLog: "vehicle-parts" });
- },
},
mounted() {
this.LoadInitialPartsData();
diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue
index 1009b9713..d23588d37 100644
--- a/src/layouts/vehicle/vehicle.vue
+++ b/src/layouts/vehicle/vehicle.vue
@@ -117,6 +117,7 @@ export default {
modelOptions: [],
styleOptions: [],
displayNoServiceAlert: false,
+ isMobileStaticRecalibrationApplicable: this.getIsMobileStaticRecalibrationApplicable(),
};
},
@@ -438,6 +439,8 @@ export default {
this.imageVifNumber = result?.data.imageVifNumber;
this.imageVifColor = result?.data.imageVifColor;
this.displayNoServiceAlert = !result?.data.canSafeliteService;
+ this.isMobileStaticRecalibrationApplicable =
+ result?.data.isMobileStaticRecalibrationApplicable;
},
resetAlert() {
this.displayNoServiceAlert = false;
@@ -456,6 +459,8 @@ export default {
imageUrl: this.imageUrl,
imageVifNumber: this.imageVifNumber,
imageVifColor: this.imageVifColor,
+ isMobileStaticRecalibrationApplicable:
+ this.isMobileStaticRecalibrationApplicable,
},
false
);
@@ -531,6 +536,9 @@ export default {
getImageVifColorfromStore() {
return store.getters.vehicle.imageVifColor;
},
+ getIsMobileStaticRecalibrationApplicable() {
+ return store.getters.vehicle.isMobileStaticRecalibrationApplicable;
+ },
},
components: {
diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js
index e970e94a8..9bb8f0cda 100644
--- a/src/mixins/analytics-mixin.js
+++ b/src/mixins/analytics-mixin.js
@@ -26,6 +26,10 @@ import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { applicationConfig } from "../constants/application-config";
+import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
+import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
+import router from "@/router/index.js";
+import { getQuerystringParameter } from "@/helpers/querystring-helper";
export default {
methods: {
@@ -650,6 +654,13 @@ export default {
});
},
+ pushPageErrorToDataLayer(error) {
+ pushToDataLayerIfDefined({
+ event: "page-error",
+ error: error,
+ });
+ },
+
prependActionToMethod(object, method, actionToPrepend) {
const baseMethodName = method.name.startsWith("bound ")
? method.name.substring(6)
@@ -701,7 +712,22 @@ export default {
return !areAllSessionCookiesSet();
},
+ // sessionExpired is true when one of the analytics cookies(sid, dxdev) has expired but we still have the vehicle year in vuex
+ sessionExpired() {
+ const fromHeritage = getQuerystringParameter(queryStrings.FROM_HERITAGE) === "true";
+ if (this.noSession() && !fromHeritage && store.getters.order.vehicle?.year > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+
async validateSession() {
+ // do not init session if it is expired. let the click event on nav-bar handle session expired logic
+ if (this.sessionExpired()) {
+ return;
+ }
+
if (this.noSession()) {
await this.initSession();
}
diff --git a/src/router/index.js b/src/router/index.js
index da3c1914c..045527a45 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -6,7 +6,7 @@ import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"
import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events";
import { queryStrings } from "@/constants/query-strings";
-import { getQuerystringParameter } from "@/helpers/querystring-helper";
+import { getQuerystringParameter, buildQuerystringObject } from "@/helpers/querystring-helper";
import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
import { fmgPageValues, funnelStartPageName } from "@/router/router-constants/fmgPage-values";
@@ -26,6 +26,7 @@ import { loadSessionIfPresent, saveSession } from "@/helpers/heritage-integratio
import {
getPageToRouteExistingOrderTo,
navigateToHeritageFunnel,
+ getImplicitNavigation,
} from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin";
@@ -53,9 +54,15 @@ const routes = [
log("------------- router index.js beforeEnter start -----------------");
log(" --to: ", to);
log(" --cookie: ", getFunnelCookie());
+ log(` --from.redirectedFrom:>${JSON.stringify(from.redirectedFrom)}<`, "");
await analyticsMixin.methods.validateSession();
+ // after session is validated, remove the fromHeritage querystring if it exists so session expiration works
+ if (to.query) {
+ delete to.query[queryStrings.FROM_HERITAGE];
+ }
+
if (getFunnelCookie()?.SuppressConceptFunnel) {
log(" --go to heritage suppressConceptFunnel: ");
await navigateToHeritageFunnel({ shouldSaveSession: false });
@@ -65,11 +72,25 @@ const routes = [
// If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
log(" --save session timeout go to start");
+
+ const errorPayload = {
+ cause: "expired session",
+ currentPage: from?.query?.fmgPage,
+ nextPage: to?.query?.fmgPage,
+ };
+
await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
deleteFunnelCookie();
- GoToFunnelStartOn404(next);
+ GoToFunnelStartOn404(next, errorPayload);
+ return;
}
+ const fromReturnUser = from?.name === fmgPageValues.RETURN_USER;
+ const fromContentSite = getQuerystringParameter(queryStrings.START_TYPE) === "fmg";
+ const toReturnUserPage = to.query?.fmgPage === fmgPageValues.RETURN_USER;
+ log(" --fromReturnUser ", fromReturnUser);
+ log(" --to.query.fmgPage ", to.query?.fmgPage);
+
// Intercept all navigation if a submitted order exists in storage
if (window.sessionStorage.getItem("submittedOrder") !== null) {
if (to.query.fmgPage !== funnelStartPageName) {
@@ -78,7 +99,29 @@ const routes = [
}
}
// On entering the funnel "fresh", read cookie information, decide what to do next.
- else if (from.redirectedFrom === undefined) {
+ else if (from.redirectedFrom === undefined || fromReturnUser) {
+ // if entering the funnel from the content site, check and see if there is already a vehicle year in vuex.
+ // if so, send them to return-user page.
+ if (
+ fromContentSite &&
+ !toReturnUserPage &&
+ store.getters.order.vehicle?.year > 0
+ ) {
+ log(" --from content site navigate to return user");
+ var qso = {
+ fmgPage: fmgPageValues.RETURN_USER,
+ };
+
+ const newQueryString = buildQuerystringObject(qso, true);
+ log(" -- returnUser add querystring: " + JSON.stringify(newQueryString));
+
+ router.push({
+ path: "/",
+ query: Object.assign({}, newQueryString),
+ });
+ return;
+ }
+
// clear the saveSessionPromise - if it exists in the vuex store but a new instance was created
// the saveSessionPromise will no longer point to a valid promise
log(" --clear promise");
@@ -125,18 +168,28 @@ const routes = [
log(" --load session end");
//Update external parameter state but not when returning from heritage
- if (!to.query.fmgPage?.startsWith("service-location")) {
+ if (!to.query.fmgPage?.startsWith("service-location") && !fromReturnUser) {
updateExternalParameterState();
}
// clear part related state because heritage selected a new vehicle
if (
to.query.fmgPage === fmgPageValues.VEHICLE &&
- eval(getFunnelCookie()?.HasDelayedClaimRegistration)
+ eval(getFunnelCookie()?.HasDelayedClaimRegistration && !fromReturnUser)
) {
store.commit(storeMutations.RESET_GLASS_PARTS_STATE);
}
+ // if coming from the return user page, clear the destination page so implicit navigation runs
+ log(" --to.query ", JSON.stringify(to.query));
+ if (fromReturnUser && to.query) {
+ log(" --clear to.query");
+ delete to.query[queryStrings.FMG_PAGE];
+
+ //to.query[queryStrings.FMG_PAGE] = "";
+ log(" --to.query cleared ", JSON.stringify(to.query));
+ }
+
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to);
log(" --pageToRedirectTo ", JSON.stringify(pageToRedirectTo));
@@ -206,7 +259,14 @@ const routes = [
return;
}
- GoToFunnelStartOn404(next);
+ const errorPayload = {
+ cause: "invalid page prerequisites for page",
+ currentPage: from?.query?.fmgPage,
+ nextPage: to?.query?.fmgPage,
+ };
+
+ GoToFunnelStartOn404(next, errorPayload);
+ return;
}
log(" --has route:", to.query.fmgPage);
@@ -214,13 +274,19 @@ const routes = [
}
if (!isExistingFmgPageName(to.query.fmgPage)) {
- console.log("Not an existing fmg page name:" + to.query.fmgPage);
- GoToFunnelStartOn404(next);
+ const errorPayload = {
+ cause: "invalid page name",
+ currentPage: from?.query?.fmgPage,
+ nextPage: to?.query?.fmgPage,
+ };
+
+ GoToFunnelStartOn404(next, errorPayload);
return;
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
const routeData = GetRouteInfoFromPageName(to.query.fmgPage);
+ log("--routeData:", routeData);
// Add our dynamic route.
router.addRoute({
@@ -239,7 +305,15 @@ const routes = [
if (!arePagePrerequisitesValid(nextComponent)) {
// prettier-ignore
console.log("Page Prereqs not valid for next component: " + nextComponent.default.name);
- GoToFunnelStartOn404(next);
+
+ const errorPayload = {
+ cause: "invalid page prerequisites for page",
+ currentPage: from?.query?.fmgPage,
+ nextPage: to?.query?.fmgPage,
+ };
+
+ GoToFunnelStartOn404(next, errorPayload);
+ return;
}
log("------------- router index.js beforeEnter end -----------------");
@@ -261,7 +335,16 @@ const routes = [
console.log(new Date() + " Exception in beforeEnter:" + JSON.stringify(error));
// If we don't have a route, go to our 404 page.
- GoToFunnelStartOn404(next);
+ const errorPayload = {
+ cause: "uncaught error in beforeEnter",
+ currentPage: from?.query?.fmgPage,
+ nextPage: to?.query?.fmgPage,
+ fullError: error,
+ errorStack: error?.stack,
+ };
+
+ GoToFunnelStartOn404(next, errorPayload);
+ return;
}
},
},
@@ -280,6 +363,7 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.beforeEach(async (to, from, next) => {
+ log("------------- router index.js beforeEach start -----------------");
// set lastNavigationPage here to capture state before API calls for analytics.
// use current page url query string name when to.name is "root" (due to unresolved navigation in beforeEach)
router.lastNavigationPage = to.name == "root" ? analyticsMixin.methods.getPageName() : to.name;
@@ -331,6 +415,7 @@ router.beforeEach(async (to, from, next) => {
} else {
next();
}
+ log("------------- router index.js beforeEach end -----------------");
});
router.afterEach(async (to, from) => {
@@ -397,8 +482,8 @@ router.navigateToExternalUrl = (url, optionalQuery = {}) => {
navigateToUrl(url, optionalQuery);
};
-router.navigateError = () => {
- DisplayPageError();
+router.navigateError = (errorPayload = null) => {
+ DisplayPageError(errorPayload);
};
//Use this navigation when you need to call next() explicitly. beforeRouteEnter is a good example.
@@ -546,6 +631,8 @@ function getNavigationMap(scenario, currentRoute) {
function log(message, data) {
const log = getQuerystringParameter(queryStrings.LOG);
+ baseMixin.methods.dispatchStoreAction(storeActions.SAVE_LOGGING_OPTION, log, false);
+
data = data ?? "";
const outData = typeof data === "object" ? JSON.stringify(data) : data;
@@ -588,7 +675,18 @@ function GetRouteInfoFromPageName(pageName) {
}
// Go to our start page on a 404.
-function GoToFunnelStartOn404(next) {
+function GoToFunnelStartOn404(next, errorPayload = null) {
+ if (errorPayload !== null) {
+ errorPayload.type = "GoToFunnelStartOn404";
+ analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
+
+ console.log(
+ "%cGoToFunnelStartOn404()... errorPayload",
+ "color: white; background-color: blue; padding: 5px;",
+ errorPayload
+ );
+ }
+
// Put item on the bus
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
@@ -607,7 +705,28 @@ function GoToFunnelStartOn404(next) {
});
}
-async function DisplayPageError() {
+async function DisplayPageError(errorPayload = null) {
+ console.log("%c running DisplayPageError()... ", "font-size: 20px; color: purple;");
+
+ if (errorPayload !== null) {
+ errorPayload.type = "DisplayPageError";
+ analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
+ } else {
+ // very cautiously to avoid additional errors:
+ var currentPage = "";
+ try {
+ currentPage = getQuerystringParameter(queryStrings.FMG_PAGE);
+ } catch (e) {
+ // pass
+ }
+ errorPayload = {
+ type: "DisplayPageError",
+ cause: "Unknown page error",
+ currentPage: currentPage,
+ };
+ analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
+ }
+
// Put item on the bus
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
@@ -619,7 +738,11 @@ async function DisplayPageError() {
type: globalEventTypes.Danger,
}
);
- store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, externalParameterStatus.INACTIVE);
+ if (
+ store.getters.externalParameterState?.isExternalParameter == externalParameterStatus.ACTIVE
+ ) {
+ store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, externalParameterStatus.INACTIVE);
+ }
baseMixin.methods.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
const pageError = getQuerystringParameter(queryStrings.PAGE_ERROR);
@@ -700,22 +823,6 @@ function getRedirectedStartPage() {
}
function updateExternalParameterState() {
- // if there is an existing external parameter state then they have already been through from an external source(LeadGen) and
- // are returning. clear out prior related things that need to be selected again like parts and damage type.
- if (
- store.getters.externalParameterState?.isExternalParameter ==
- externalParameterStatus.INACTIVE
- ) {
- store.dispatch(storeActions.RESET_VEHICLE_STATE_AND_DEPENDENCIES);
- store.dispatch(storeActions.SAVE_SERVICE_ZIP_CODE_INFO, {
- state: null,
- zipCode: null,
- zipCodeCtu: null,
- });
- store.dispatch(storeActions.SAVE_EMAIL, null);
- store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
- }
-
const externalParameterYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR);
const externalParameterMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE);
const externalParameterModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL);
@@ -734,6 +841,7 @@ function updateExternalParameterState() {
externalParameterModel &&
externalParameterStyle
) {
+ resetStoreForExternalParameter();
store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, externalParameterStatus.ACTIVE);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_YEAR, externalParameterYear);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MAKE, externalParameterMake);
@@ -784,4 +892,22 @@ function updateExternalParameterState() {
}
}
+// if there is an existing external parameter state then they have already been through from an external source(LeadGen) and
+// are returning. clear out prior related things that need to be selected again like parts and damage type.
+function resetStoreForExternalParameter() {
+ if (
+ store.getters.externalParameterState?.isExternalParameter ==
+ externalParameterStatus.INACTIVE
+ ) {
+ store.dispatch(storeActions.RESET_VEHICLE_STATE_AND_DEPENDENCIES);
+ store.dispatch(storeActions.SAVE_SERVICE_ZIP_CODE_INFO, {
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ });
+ store.dispatch(storeActions.SAVE_EMAIL, null);
+ store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
+ }
+}
+
export default router;
diff --git a/src/router/router-constants/fmgPage-values.js b/src/router/router-constants/fmgPage-values.js
index 6b4ec77ea..5fade6506 100644
--- a/src/router/router-constants/fmgPage-values.js
+++ b/src/router/router-constants/fmgPage-values.js
@@ -22,8 +22,10 @@ const fmgPageValues = {
PAYMENT: "payment",
PAYMENT_PIA_RETURN: "payment-pia-return",
CONFIRMATION: "confirmation",
+ RETURN_USER: "return-user",
};
const funnelStartPageName = fmgPageValues.VEHICLE;
+const returnUserPageName = fmgPageValues.RETURN_USER;
-export { fmgPageValues, funnelStartPageName };
+export { fmgPageValues, funnelStartPageName, returnUserPageName };
diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js
index 3e3dbd4bf..8891e7cff 100644
--- a/src/router/router-constants/routing-table.js
+++ b/src/router/router-constants/routing-table.js
@@ -530,6 +530,16 @@ const routingTable = function (store) {
{
fmgPageValue: fmgPageValues.CONFIRMATION,
},
+ // The destination page for return user does not matter. the router will invoke implicit navigation
+ {
+ fmgPageValue: fmgPageValues.RETURN_USER,
+ maps: [
+ {
+ scenario: navigationScenarios.CLICKED_FORWARD,
+ destinationFmgPageValue: fmgPageValues.VEHICLE,
+ },
+ ],
+ },
];
};
diff --git a/src/store/index.js b/src/store/index.js
index f423fb58d..5a674b317 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -68,6 +68,7 @@ const getDefaultState = () => {
registration: {
licensePlate: null,
},
+ isMobileStaticRecalibrationApplicable: false,
},
serviceLocation: {
address: null,
@@ -225,6 +226,10 @@ export const mutations = {
updateVehicleVin(state, vin) {
state.order.vehicle.vin = vin;
},
+ updateIsMobileStaticRecalibrationApplicable(state, isMobileStaticRecalibrationApplicable) {
+ state.order.vehicle.isMobileStaticRecalibrationApplicable =
+ isMobileStaticRecalibrationApplicable;
+ },
updateIsRepair(state, isRepair) {
state.order.damage.isRepair = isRepair;
},
@@ -350,6 +355,8 @@ export const mutations = {
state.order.vehicle.imageUrl = vehicleInfo.imageUrl;
state.order.vehicle.imageVifNumber = vehicleInfo.imageVifNumber;
state.order.vehicle.imageColor = vehicleInfo.imageVifColor;
+ state.order.vehicle.isMobileStaticRecalibrationApplicable =
+ vehicleInfo.isMobileStaticRecalibrationApplicable;
},
updateRegistration(state, registrationInfo) {
state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate;
@@ -534,6 +541,7 @@ export const mutations = {
state.order.vehicle.imageUrl = null;
state.order.vehicle.imageVifNumber = null;
state.order.vehicle.imageColor = null;
+ state.order.vehicle.isMobileStaticRecalibrationApplicable = false;
},
resetDamageState(state) {
state.order.damage.isRepair = null;
@@ -1600,7 +1608,7 @@ export const actions = {
return response;
},
- getMobileFeePart(context, { pageNameToLog }) {
+ getMobileFeePart(context, { payload: { serviceZipCode, serviceZipCodeCtu }, pageNameToLog }) {
const serviceType = context.getters.damage.isRepair ? "Repair" : "Install";
const facilityType = "Mobile";
const parentAccountNumber = context.getters.payment.parentAccountNumber;
@@ -1612,16 +1620,31 @@ export const actions = {
const coverageStatus = coverageStatusEnum(order.payment?.insuranceCoverage?.coverageStatus);
const coverageType = coverageTypeEnum(order.payment?.insuranceCoverage?.coverageType);
const isItacOptimized = order.policy?.isItac ?? false;
+ const isMobileStaticRecalibrationApplicable =
+ order.vehicle?.isMobileStaticRecalibrationApplicable;
const zipCode =
- order.serviceLocation?.provider?.address?.zipCode ?? order.serviceLocation?.zipCode;
+ serviceZipCode ??
+ order.serviceLocation?.provider?.address?.zipCode ??
+ order.serviceLocation?.zipCode;
var providerNumber =
- order.serviceLocation?.provider?.providerNumber ?? order.serviceLocation?.zipCodeCtu;
+ serviceZipCodeCtu ??
+ order.serviceLocation?.provider?.providerNumber ??
+ order.serviceLocation?.zipCodeCtu;
if (providerNumber.startsWith("00") && providerNumber.length > 5) {
providerNumber = providerNumber.substring(1);
}
var endPoint = `${endpoints.GetMobileFeePart.url}/?serviceType=${serviceType}&facilityType=${facilityType}&parentAccountNumber=${parentAccountNumber}&billToAccountNumber=${billToAccountNumber}&providerNumber=${providerNumber}&isItacOptimized=${isItacOptimized}&zipCode=${zipCode}`;
+
+ if (isMobileStaticRecalibrationApplicable) {
+ const staticRecalPartNumber = getStaticRecalPartNumber(order.lineItems?.glassParts[0]);
+ const carId = order.vehicle?.carId;
+ if (staticRecalPartNumber && carId) {
+ endPoint = `${endPoint}&partNumbers=${staticRecalPartNumber}&carId=${carId}`;
+ }
+ }
+
if (coverageStatus) {
endPoint = `${endPoint}&coverageStatus=${coverageStatus}`;
}
@@ -2222,7 +2245,18 @@ export const actions = {
// Vehicle
saveVehicle(
context,
- { year, make, model, style, carId, category, imageUrl, imageVifNumber, imageVifColor }
+ {
+ year,
+ make,
+ model,
+ style,
+ carId,
+ category,
+ imageUrl,
+ imageVifNumber,
+ imageVifColor,
+ isMobileStaticRecalibrationApplicable,
+ }
) {
if (
context.state.order.vehicle.year != year ||
@@ -2243,6 +2277,10 @@ export const actions = {
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor);
+ context.commit(
+ storeMutations.UPDATE_VEHICLE_MOBILE_STATIC_RECALIBRATION_APPLICABLE,
+ isMobileStaticRecalibrationApplicable
+ );
}
},
@@ -3646,3 +3684,13 @@ function getExternalParameterDefaultState() {
function saveExternalParameterState(externalParameterState) {
window.sessionStorage.setItem("externalParameterState", JSON.stringify(externalParameterState));
}
+
+//This function checks if static recalibration is available for the vehicle and returns the part number for it.
+function getStaticRecalPartNumber(glassPartsArray) {
+ const recalPart = glassPartsArray.childParts.find((item) => item.partNumber === "RECAL STATIC");
+ if (recalPart) {
+ return recalPart.partNumber;
+ } else {
+ return null;
+ }
+}
diff --git a/src/ux-components/button-main/button-main.vue b/src/ux-components/button-main/button-main.vue
index 848cf3866..7980adf99 100644
--- a/src/ux-components/button-main/button-main.vue
+++ b/src/ux-components/button-main/button-main.vue
@@ -5,7 +5,7 @@
:class="[
isPrimary ? 'btn-primary' : 'btn-secondary',
isFloat ? 'float-end' : '',
- (isLoaderDisplayed && !suppressLoader) ? 'has-loader' : '',
+ isLoaderDisplayed && !suppressLoader ? 'has-loader' : '',
]"
@click="clicked">
{{ this.buttonText }}
diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue
index 8bcd7bd3a..4b66023e0 100644
--- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue
+++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue
@@ -83,7 +83,6 @@ export default {
.list-button-horizontal-content {
outline: none;
position: relative;
- background: $white;
transition: all 150ms linear;
border: 1px solid $gray-500;
border-radius: 0;
@@ -110,8 +109,8 @@ export default {
}
.list-button-horizontal-content:hover {
- background-color: $blue-700;
- color: $white;
+ background-color: transparent;
+ color: $blue;
box-shadow: none;
}
@@ -220,4 +219,121 @@ export default {
}
}
}
+// Special styles when there are multiple list-button-horizontal buttons
+// (number of chips, pay w/cash/insurance)
+.windshield-chip-count-question {
+ fieldset {
+ .d-flex {
+ .col {
+ &:first-child {
+ label {
+ &.list-group.list-button-horizontal {
+ z-index: 2;
+ &:hover {
+ background-color: $white;
+ border-top-left-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fieldset {
+ .d-flex {
+ .col {
+ label {
+ &.list-group.list-button-horizontal {
+ &:hover {
+ background-color: $white;
+ border-radius: 0;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fieldset {
+ .d-flex {
+ .col {
+ &:last-child {
+ label {
+ &.list-group.list-button-horizontal {
+ z-index: 2;
+ &:hover {
+ background-color: $white;
+ border-top-right-radius: 0.5rem;
+ border-bottom-right-radius: 0.5rem;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+//NOT Windshiled Chip Count
+fieldset {
+ .d-flex {
+ .col {
+ &:first-child {
+ label {
+ &.list-group.list-button-horizontal {
+ z-index: 6;
+ &:hover {
+ background-color: $white;
+ border-top-left-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fieldset {
+ .d-flex {
+ .col {
+ label {
+ &.list-group.list-button-horizontal {
+ &:hover {
+ background-color: $white;
+ border-radius: 0;
+ }
+ }
+ }
+ }
+ }
+}
+
+fieldset {
+ .d-flex {
+ .col {
+ &:last-child {
+ label {
+ &.list-group.list-button-horizontal {
+ z-index: 6;
+ &:hover {
+ background-color: $white;
+ border-top-right-radius: 0.5rem;
+ border-bottom-right-radius: 0.5rem;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/vue.config.js b/vue.config.js
index 28d20e406..d8cb42355 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -7,6 +7,7 @@ process.env.VUE_APP_MY_ACCOUNT = "https://myaccountdev.safelite.com/";
//process.env.VUE_APP_SAFELITE_HOP = "http://localhost:60966/fmgCheckoutShared.aspx";
process.env.VUE_APP_SAFELITE_HOP = "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx";
process.env.VUE_APP_SOLARWINDS_MONITORING = "";
+process.env.VUE_APP_SESSION_TIMEOUT_MINUTES = 30;
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY =
diff --git a/vue.release.config.js b/vue.release.config.js
index 2c215c8ad..350a78ce3 100644
--- a/vue.release.config.js
+++ b/vue.release.config.js
@@ -5,6 +5,7 @@ process.env.VUE_APP_CURRENT_ENVIRONMENT = "__VUE_APP_CURRENT_ENVIRONMENT__";
process.env.VUE_APP_MY_ACCOUNT = "__VUE_APP_MY_ACCOUNT__";
process.env.VUE_APP_SAFELITE_HOP = "__VUE_APP_SAFELITE_HOP__";
process.env.VUE_APP_SOLARWINDS_MONITORING_SCRIPT = "__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__";
+process.env.VUE_APP_SESSION_TIMEOUT_MINUTES = 30;
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__";