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__";