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/jest.config.js b/jest.config.js index ce4aa3ad6..052e1c47b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,6 +27,7 @@ module.exports = { "!src/layouts/insurance/*.vue", // Temp test exclusion while in development "!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development "!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development + "!src/layouts/return-user/*.vue", // Temp test exclusion while in development // END ], // ! means exclude from coverage. testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], 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/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index bf1fad5f5..461d144ae 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -217,11 +217,11 @@ export default { flex: none; } .textbox-question { - padding: .25rem 0; + padding: 0.25rem 0; } } } - + &.modal-dialog-centered { align-items: flex-end; min-height: 100%; @@ -286,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/save-progress-modal-question/save-progress-modal-question.vue b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue index c8465fef7..a25224f69 100644 --- a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue +++ b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue @@ -1,7 +1,8 @@ @@ -38,7 +38,6 @@ import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; import baseMixin from "@/mixins/base-mixin.js"; -import { saveQuote } from "@/helpers/heritage-integration/order-helper.js"; // DEFINE VALIDATION RULES defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); @@ -65,7 +64,7 @@ export default { // Call the "next" function to complete the transition to this page. next(async (vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0; + vm.showSaveProgressModal = !(emailFromStore?.length > 0); if (store.getters.externalParameterState?.isExternalParameter) { if (store.getters.externalParameterServiceZip.zipCode) { await baseMixin.methods.dispatchStoreAction( @@ -87,7 +86,7 @@ export default { questionsData: null, selectedAnswers: {}, currentGlassIndex: 0, - isEmailInStoreOnPageLoad: null, + showSaveProgressModal: null, }; }, computed: { @@ -157,9 +156,6 @@ export default { this.questionsData ); }, - saveProgress() { - saveQuote({ pageNameToLog: "capability-questions" }); - }, async forwardButtonAction() { const questionAnswersArray = this.questionsData.map((glass) => { // get answerResult2 of returned answer diff --git a/src/layouts/molding-questions/molding-questions.vue b/src/layouts/molding-questions/molding-questions.vue index 2f2cc346c..1bcc3d7a8 100644 --- a/src/layouts/molding-questions/molding-questions.vue +++ b/src/layouts/molding-questions/molding-questions.vue @@ -16,8 +16,8 @@ + pageName="molding-questions" + v-if="showSaveProgressModal" /> @@ -38,7 +38,6 @@ import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; import baseMixin from "@/mixins/base-mixin.js"; -import { saveQuote } from "@/helpers/heritage-integration/order-helper.js"; // DEFINE VALIDATION RULES defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); @@ -65,7 +64,7 @@ export default { // Call the "next" function to complete the transition to this page. next(async (vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0; + vm.showSaveProgressModal = !(emailFromStore?.length > 0); if (store.getters.externalParameterState?.isExternalParameter) { if (store.getters.externalParameterServiceZip.zipCode) { await baseMixin.methods.dispatchStoreAction( @@ -88,7 +87,7 @@ export default { questionsData: null, selectedAnswers: {}, currentGlassIndex: 0, - isEmailInStoreOnPageLoad: null, + showSaveProgressModal: null, }; }, computed: { @@ -157,9 +156,6 @@ export default { this.questionsData ); }, - saveProgress() { - saveQuote({ pageNameToLog: "molding-questions" }); - }, async forwardButtonAction() { const questionAnswersArray = this.questionsData.map((glass) => { return { diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index a5441acd6..f905e86c7 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -16,8 +16,8 @@ + pageName="part-questions" + v-if="showSaveProgressModal" /> @@ -38,7 +38,6 @@ import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; import baseMixin from "@/mixins/base-mixin.js"; -import { saveQuote } from "@/helpers/heritage-integration/order-helper.js"; // DEFINE VALIDATION RULES defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); @@ -65,7 +64,7 @@ export default { // Call the "next" function to complete the transition to this page. next(async (vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0; + vm.showSaveProgressModal = !(emailFromStore?.length > 0); if (store.getters.externalParameterState?.isExternalParameter) { if (store.getters.externalParameterServiceZip.zipCode) { const serviceState = store.getters.order.serviceLocation.state; @@ -89,7 +88,7 @@ export default { questionsData: null, selectedAnswers: {}, currentGlassIndex: 0, - isEmailInStoreOnPageLoad: null, + showSaveProgressModal: null, }; }, computed: { @@ -156,9 +155,6 @@ export default { this.questionsData ); }, - saveProgress() { - saveQuote({ pageNameToLog: "part-questions" }); - }, async forwardButtonAction() { const questionAnswersArray = this.questionsData.map((glass) => { return { diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 1a3fb9174..c45a5c98c 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -621,7 +621,7 @@ export default { : this.$store.getters.order.serviceLocation?.appointmentType; }, ServiceLocationFullAddressText() { - return `${this.ServiceLocationAddress.toLowerCase()}${this.ServiceLocationAddress2 ? ", " + this.ServiceLocationAddress2 : ""}, ${this.ServiceLocationCity.toLowerCase()}, ${this.ServiceLocationState} ${this.ServiceLocationZipCode}`; + return `${this.ServiceLocationAddress?.toLowerCase()}${this.ServiceLocationAddress2 ? ", " + this.ServiceLocationAddress2 : ""}, ${this.ServiceLocationCity?.toLowerCase()}, ${this.ServiceLocationState} ${this.ServiceLocationZipCode}`; }, ServiceLocationFullAddress() { if (this.AppointmentType === AppointmentTypeStrings.MOBILE) { diff --git a/src/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue b/src/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue index e4fa9ffa3..32118bb95 100644 --- a/src/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue +++ b/src/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue @@ -26,7 +26,7 @@ export default { computed: { displayContent() { return [ - `${this.addressInfo.address.toLowerCase()}${this.addressInfo.address2 ? ", " : ""}${this.addressInfo.address2.toLowerCase()}, ${this.addressInfo.city.toLowerCase()}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`, + `${this.addressInfo?.address?.toLowerCase()}${this.addressInfo?.address2 ? ", " : ""}${this.addressInfo?.address2?.toLowerCase()}, ${this.addressInfo?.city?.toLowerCase()}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`, ]; }, addressInfo() { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 7557e0156..221eae723 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -82,8 +82,9 @@ + v-if="showSaveProgressModal" + pageName="quote" + @save-progress-saved="updateSaveProgressAsSaved" /> @@ -209,6 +211,7 @@ export default { const emailFromStore = store.getters.order.customer.emailAddress; const isEmailInStoreOnPageLoad = emailFromStore?.length > 0; const showSaveProgressPopup = isEmailInStoreOnPageLoad ? false : true; + const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true; const lineItems = deepClone(store.getters.order.lineItems); lineItems.vaps = lineItems.vaps ?? []; @@ -290,8 +293,8 @@ export default { // Call the "next" function to complete the transition to this page. next(async (vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.isEmailInStoreOnPageLoad = isEmailInStoreOnPageLoad; vm.showSaveProgressPopup = showSaveProgressPopup; + vm.showSaveProgressModal = showSaveProgressModal; vm.addableVaps = addableVaps; vm.lineItems = lineItems; vm.availableLineItems = pricingResults; @@ -438,10 +441,14 @@ export default { servicePackage: null, holdParentAccountNumber: null, holdBillToAccountNumber: null, - isEmailInStoreOnPageLoad: null, showSaveProgressPopup: null, + showSaveProgressModal: null, + isSaveProgressComplete: null, }; }, + mounted() { + this.attachCustomEvents(); + }, computed: { lineItemsCloneForWatcher() { return Object.assign({}, this.lineItems); @@ -526,10 +533,8 @@ export default { backButtonAction() { vehicleQuestionsMixin.methods.navigateBack(this); }, - saveProgress() { - this.isEmailInStoreOnPageLoad = true; - this.showSaveProgressPopup = false; - saveQuote({ pageNameToLog: "quote" }); + updateSaveProgressAsSaved() { + this.showSaveProgressModal = false; }, closeSaveProgressPopup() { this.showSaveProgressPopup = false; @@ -677,9 +682,6 @@ export default { }); }, }, - mounted() { - this.attachCustomEvents(); - }, watch: { lineItemsCloneForWatcher: { handler(newValue, oldValue) { diff --git a/src/layouts/return-user/return-user.vue b/src/layouts/return-user/return-user.vue new file mode 100644 index 000000000..5facfde0a --- /dev/null +++ b/src/layouts/return-user/return-user.vue @@ -0,0 +1,124 @@ + + + + diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index b0a1ffcd9..fd9b90b79 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,7 @@ export default { // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0; + 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 +114,7 @@ export default { selectedGlassParts: {}, alertWidgetData: Object, alreadyPopulatedPartsData: [], - isEmailInStoreOnPageLoad: null, + showSaveProgressModal: null, }; }, computed: { @@ -234,9 +233,6 @@ export default { }); }); }, - saveProgress() { - saveQuote({ pageNameToLog: "vehicle-parts" }); - }, }, mounted() { this.LoadInitialPartsData(); diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index e970e94a8..136c0ef14 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -26,6 +26,7 @@ import store from "@/store"; import baseMixin from "@/mixins/base-mixin"; import { applicationConfig } from "../constants/application-config"; +import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; export default { methods: { @@ -650,6 +651,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) @@ -702,6 +710,21 @@ export default { }, async validateSession() { + // The noSession function checks the cookies related to analytics logging(sid). it is not the funnel info cookie. + // The sid cookie for analytics will expire every 30 minutes and get recreated in initSession. If this happens + // and we also have the funnel cookie present, that indicates they have an existing session that is now expired + // so route them to the return user page. + const funnelCookieLastTouched = getFunnelCookie()?.LastTouched; + if ( + this.noSession() && + funnelCookieLastTouched !== null && + funnelCookieLastTouched !== undefined + ) { + await this.initSession(); + window.location.href = applicationConfig.RETURN_USER_PAGE; + return; + } + if (this.noSession()) { await this.initSession(); } diff --git a/src/router/index.js b/src/router/index.js index 3f31a2f21..f04c0613d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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,6 +54,7 @@ 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(); @@ -65,11 +67,22 @@ 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 fromHeritage = getQuerystringParameter(queryStrings.FROM_HERITAGE) === "true"; + const fromReturnUser = from?.name === fmgPageValues.RETURN_USER; + // Intercept all navigation if a submitted order exists in storage if (window.sessionStorage.getItem("submittedOrder") !== null) { if (to.query.fmgPage !== funnelStartPageName) { @@ -78,7 +91,21 @@ 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 non-funnel check and see if there is already a funnel cookie. + // if so, send them to return-user page + const funnelCookieLastTouched = getFunnelCookie()?.LastTouched; + if ( + to?.query?.fmgPage !== fmgPageValues.RETURN_USER && + !fromReturnUser && + !fromHeritage && + funnelCookieLastTouched !== null && + funnelCookieLastTouched !== undefined + ) { + log(" --navigate to return user"); + NavigateToReturnUser(next, to); + } + // 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"); @@ -137,6 +164,11 @@ const routes = [ store.commit(storeMutations.RESET_GLASS_PARTS_STATE); } + // if coming from the return user page, clear the destination page so implicit navigation runs + if (fromReturnUser && to.query) { + to.query[queryStrings.FMG_PAGE] = ""; + } + const pageToRedirectTo = await getPageToRouteExistingOrderTo(to); log(" --pageToRedirectTo ", JSON.stringify(pageToRedirectTo)); @@ -206,7 +238,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 +253,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 +284,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 +314,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 +342,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 +394,7 @@ router.beforeEach(async (to, from, next) => { } else { next(); } + log("------------- router index.js beforeEach end -----------------"); }); router.afterEach(async (to, from) => { @@ -397,8 +461,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. @@ -587,8 +651,55 @@ function GetRouteInfoFromPageName(pageName) { return routeData; } +async function NavigateToReturnUser(next, to) { + const returnRoute = GetRouteInfoFromPageName(fmgPageValues.RETURN_USER); + log("--returnRoute:", returnRoute); + + 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 === fmgPageValues.RETURN_USER)[0].components; + + log("--return-user component:", component); + + // If the component hasn't been loaded fully, load it before we check prerequisites. + if (component.default.methods === undefined) { + component = await component.default(); + } + + log(" --has route for return-user:", fmgPageValues.RETURN_USER); + return next({ name: fmgPageValues.RETURN_USER, query: to.query, params: to.params }); + } else { + log(" --add route for return-user:", fmgPageValues.RETURN_USER); + router.addRoute({ + path: returnRoute[0].path, // Always the same path, because we control it with query strings. + name: returnRoute[0].name, + component: returnRoute[0].component, + }); + + next({ + name: returnRoute[0].name, + query: Object.assign(to.query, { fmgPage: returnRoute[0].name }), + params: to.params, + }); + return; + } +} + // 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 +718,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, 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/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__";