diff --git a/src/App.vue b/src/App.vue index 422ae2893..f4c5754aa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -36,6 +36,9 @@ export default { loadingModal, funnelFooter, }, + mounted() { + showFmgLoadingModal(true); + }, }; // Add a global Javascript exception handler for logging exceptions, this handler will log when there is an uncaught exception diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 14f195cfc..a03e5f010 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -15,21 +15,12 @@ const applicationConfig = { CASH_DEFAULT_BILL_TO_ACCOUNT_NUMBER: "87291", MY_ACCOUNT: process.env.VUE_APP_MY_ACCOUNT, PIA_RESPONSE_URL: - location.protocol + - "//" + - location.host + - "/fmg/?fmgPage=payment-pia-return&src=concept-funnel", + location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel", PIA_CANCEL_URL: - location.protocol + - "//" + - location.host + - "/fmg/?fmgPage=payment-method&src=concept-funnel", + location.protocol + "//" + location.host + "/fmg/payment-method?src=concept-funnel", PIA_ERROR_URL: - location.protocol + - "//" + - location.host + - "/fmg/?fmgPage=payment-pia-return&src=concept-funnel", - CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/?fmgPage=confirmation", + location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel", + CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/confirmation", GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE", YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60", OUTLOOK_CALENDAR: @@ -38,7 +29,7 @@ const applicationConfig = { 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", + RETURN_USER_PAGE: "/fmg/return-user", }; export { applicationConfig }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 7b76d4064..9f0965480 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -117,6 +117,7 @@ const storeActions = { UPDATE_EXTERNAL_PARAMETER_MMS: "updateExternalParameterMMS", SAVE_LOGGING_OPTION: "saveLoggingOption", + UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 5064864d0..f7e8d9f4b 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -94,6 +94,7 @@ const storeMutations = { UPDATE_SAVE_SESSION_PROMISE: "updateSaveSessionPromise", UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LOGGING_OPTION: "updateLoggingOption", + UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", // EXPERIMENT MUTATIONS UPDATE_EXPERIMENTS: "updateExperiments", diff --git a/src/digital-components/text-block/text-block.vue b/src/digital-components/text-block/text-block.vue index b84f12283..c7bb61215 100644 --- a/src/digital-components/text-block/text-block.vue +++ b/src/digital-components/text-block/text-block.vue @@ -58,7 +58,7 @@ export default { getRouterLinkDisplayTextFromCopy, getExternalLink, navigateWithScenario(scenarioName) { - this.$router.navigateWithoutSaving(scenarioName, this.$route); + this.$router.navigateWithoutSaving(scenarioName, this.pageName); }, }, computed: { diff --git a/src/fmg-components/funnel-header/progress-bar/progress-bar.vue b/src/fmg-components/funnel-header/progress-bar/progress-bar.vue index ab6a00b3a..da370e415 100644 --- a/src/fmg-components/funnel-header/progress-bar/progress-bar.vue +++ b/src/fmg-components/funnel-header/progress-bar/progress-bar.vue @@ -17,7 +17,7 @@ export default { }, methods: { getProgress() { - var pageName = store.getters.applicationUser.lastPageVisited; + var pageName = this.pageName; return getProgressBarPercentage(pageName); }, }, diff --git a/src/global-methods.js b/src/global-methods.js index ba6cec1ec..784ba6782 100644 --- a/src/global-methods.js +++ b/src/global-methods.js @@ -1,7 +1,7 @@ import axios from "axios"; import analyticsMixIn from "@/mixins/analytics-mixin.js"; import baseMixin from "@/mixins/base-mixin.js"; -import router from "@/router"; +import router from "@/router/a-rewrite"; import store from "@/store"; import { applicationConfig } from "@/constants/application-config.js"; @@ -117,7 +117,7 @@ export default { endpoint: endpoint, }; - router.navigateError(errorPayload); + router.bailout(errorPayload); // do not log 404 errors from services because we return NotFound // when a service doesn't return an object diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index 76729e49f..1c0922ba0 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -59,25 +59,15 @@ export async function saveSession({ submitAfterSave = false, createUnscheduledStatusWorkOrderForPIA = false, }) { - var saveSessionPromise; - if (store.getters.applicationUser.saveSessionPromise) { - // queue newest request after current saveSessionPromise resolves - saveSessionPromise = store.getters.applicationUser.saveSessionPromise.then(() => { - // get a new saveSessionPromise - return saveSessionHelper( - pageNameToLog, - submitAfterSave, - createUnscheduledStatusWorkOrderForPIA - ); - }); - } else { - // create an initial saveSessionPromise - saveSessionPromise = saveSessionHelper( + // Add next save session to end of queue + const saveSessionPromise = flushSaveSessionQueue().then(() => { + return saveSessionHelper( pageNameToLog, submitAfterSave, createUnscheduledStatusWorkOrderForPIA ); - } + }); + store.commit(storeMutations.UPDATE_SAVE_SESSION_PROMISE, saveSessionPromise); // await here to allow for a caller to await and make the function synchronous if (!store.getters.applicationUser.savedSessionId || shouldAwaitSaveSessionQueue) { @@ -133,7 +123,7 @@ async function loadSession( pageNameToLog ) { // await the saveSessionPromise in the store to make sure we're loading up to date information - await store.getters.applicationUser.saveSessionPromise; + await flushSaveSessionQueue(); const response = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.LOAD_SESSION, @@ -193,3 +183,7 @@ async function saveSessionHelper( // Update the cookie with the referral information when saved. updateOrCreateFunnelCookie(); } + +export async function flushSaveSessionQueue() { + return Promise.resolve(store.getters.applicationUser.saveSessionPromise); +} diff --git a/src/layouts/address-lookup/address-lookup.vue b/src/layouts/address-lookup/address-lookup.vue index 54020b7b0..6a82ea26c 100644 --- a/src/layouts/address-lookup/address-lookup.vue +++ b/src/layouts/address-lookup/address-lookup.vue @@ -137,7 +137,7 @@ export default { mixins: [vinPagesMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ @@ -193,7 +193,10 @@ export default { }, backButtonAction() { // route to move backwards - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, attachCustomEvents() { this.prependActionToMethod(this, this.forwardButtonAction, () => { @@ -442,18 +445,14 @@ export default { ) { this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true } + this.pageName ); } else if (matchingCars.length === 1) { await this.navigateForwardWithSingleCarMatch(); } else { - this.$router.navigateWithSaving( + this.$router.navigateWithPageData( this.navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES, - this.$route, - {}, - {}, + this.pageName, carsFound ); } diff --git a/src/layouts/address-vehicles/address-vehicles.vue b/src/layouts/address-vehicles/address-vehicles.vue index f44cdc65a..e094f6147 100644 --- a/src/layouts/address-vehicles/address-vehicles.vue +++ b/src/layouts/address-vehicles/address-vehicles.vue @@ -82,7 +82,7 @@ export default { mixins: [vinPagesMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ @@ -166,7 +166,10 @@ export default { return false; }, backButtonAction() { - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, async forwardButtonAction() { const vinLookup = await this.dispatchStoreActionWithLogging( @@ -205,9 +208,7 @@ export default { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true } + this.pageName ); } else { await this.navigateForwardWithSingleCarMatch(); diff --git a/src/layouts/bailout/bailout.vue b/src/layouts/bailout/bailout.vue index b1f159fbe..a64f7a416 100644 --- a/src/layouts/bailout/bailout.vue +++ b/src/layouts/bailout/bailout.vue @@ -37,7 +37,7 @@ export default { name: "bailout", async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ diff --git a/src/layouts/capability-questions/capability-questions.vue b/src/layouts/capability-questions/capability-questions.vue index 1896513a1..e04f0f18e 100644 --- a/src/layouts/capability-questions/capability-questions.vue +++ b/src/layouts/capability-questions/capability-questions.vue @@ -47,7 +47,7 @@ export default { mixins: [vehicleQuestionsMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index 40736b2c2..e65eacc3c 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -123,6 +123,7 @@ import experimentMixin from "@/mixins/experiment-mixin.js"; import { containsRecalParts } from "@/helpers/recal-helper.js"; import donationBlock from "@/experiment-components/donation-block.vue"; import { partNumberStrings } from "@/constants/part-number-strings"; +import analyticsMixin from "@/mixins/analytics-mixin"; export default { name: "confirmation", @@ -179,7 +180,7 @@ export default { // Call APIs const submittedOrder = baseMixin.methods.getSubmittedOrder(); - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_WIPERS, @@ -244,6 +245,8 @@ export default { vm.showDonationSuccess = lineItemsFromSubmittedOrder.supportingItems?.some( (item) => item.partType === partNumberStrings.DONATION ); + + vm.pushAnalytics(); }); }, data() { @@ -556,6 +559,16 @@ export default { this.$refs.donationBlock.removeLoader(); } }, + pushAnalytics() { + //Push Ecommerce Cart to Data Layer + analyticsMixin.methods.pushECommerceCartToDataLayer(); + + //Push Product Array to Data Layer + analyticsMixin.methods.pushProductArrayToDataLayer(); + + //Push Commission Junction Gtm Data To Data Layer + analyticsMixin.methods.pushCommissionJunctionGtmDataToDataLayer(); + }, }, components: { funnelHeader, diff --git a/src/layouts/customer-details/customer-details.vue b/src/layouts/customer-details/customer-details.vue index baa8636be..8cb17673b 100644 --- a/src/layouts/customer-details/customer-details.vue +++ b/src/layouts/customer-details/customer-details.vue @@ -84,6 +84,7 @@ import { required, regex } from "@/helpers/validation-rules"; import { Form, defineRule } from "vee-validate"; import store from "@/store"; import { paymentMethods } from "@/constants/payment-method-constants"; +import { flushSaveSessionQueue } from "@/helpers/heritage-integration/order-helper"; // DEFINE VALIDATION RULES defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED)); @@ -102,7 +103,7 @@ export default { name: "customer-details", async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ @@ -157,7 +158,10 @@ export default { return store.getters.order.serviceLocation.techNotes; }, backButtonAction() { - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, async forwardButtonAction() { this.dispatchStoreAction( @@ -186,10 +190,13 @@ export default { paymentMethods.NONE, false ); - await store.getters.applicationUser.saveSessionPromise; + await flushSaveSessionQueue(); } - this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); + this.$router.navigateWithSaving( + this.navigationScenarios.CLICKED_FORWARD, + this.pageName + ); }, }, components: { diff --git a/src/layouts/estimate/estimate.vue b/src/layouts/estimate/estimate.vue index b9fdf1958..0a5db7d74 100644 --- a/src/layouts/estimate/estimate.vue +++ b/src/layouts/estimate/estimate.vue @@ -62,6 +62,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; import { nextTick } from "vue"; +import { consumeQueryFromStash } from "@/router/a-rewrite/logic/querystring-stash"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -89,7 +90,7 @@ export default { async beforeRouteEnter(to, from, next) { //Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); @@ -98,9 +99,9 @@ export default { lowerCaseParams.append(name.toLowerCase(), value); } - const zip = lowerCaseParams.get(queryStrings.ZIP_CODE) - ? lowerCaseParams.get(queryStrings.ZIP_CODE) - : store.getters.order.serviceLocation.zipCode; + const zip = + consumeQueryFromStash(queryStrings.ZIP_CODE) ?? + store.getters.order.serviceLocation.zipCode; var vinByAddressPromise; if (zip) { @@ -184,32 +185,35 @@ export default { }, backButtonAction() { // route to move backwards - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, async forwardButtonAction() { if (this.selectedVinLookupMethod === vinLookupMethodSelections.MANUALVIN) { await this.dispatchStoreAction(storeActions.CLEAR_VIN); return this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_MANUAL_VIN, - this.$route + this.pageName ); } if (this.selectedVinLookupMethod === vinLookupMethodSelections.LICENSEPLATE) { return this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_LICENSE_PLATE, - this.$route + this.pageName ); } if (this.selectedVinLookupMethod === vinLookupMethodSelections.HOMEADDRESS) { return this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_HOME_ADDRESS, - this.$route + this.pageName ); } if (this.selectedVinLookupMethod === vinLookupMethodSelections.DECLINE) { return this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_NO_VIN, - this.$route + this.pageName ); } }, diff --git a/src/layouts/insurance-company/insurance-company.vue b/src/layouts/insurance-company/insurance-company.vue index c9409eb71..7efde9a68 100644 --- a/src/layouts/insurance-company/insurance-company.vue +++ b/src/layouts/insurance-company/insurance-company.vue @@ -58,6 +58,7 @@ import { experimentSettings } from "@/constants/experiments"; import { partNumberStrings } from "@/constants/part-number-strings"; import { deepClone } from "@/helpers/object-helper"; import store from "@/store"; +import { flushSaveSessionQueue } from "@/helpers/heritage-integration/order-helper"; export default { name: "insurance-company", @@ -81,7 +82,7 @@ export default { } // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const insuranceCompanyListPromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_INSURANCE_COMPANY_LIST, {}, @@ -146,18 +147,20 @@ export default { // Go back to Quote page if (forceCashSelectionOnQuote) { this.$router.navigateWithoutSaving( - this.navigationScenarios.CLICKED_BACK, - this.$route, - { isCashSelected: true } + this.navigationScenarios.CLICKED_PAY_ON_MY_OWN, + this.pageName ); return; } - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, async forwardButtonAction() { // await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it - await store.getters.applicationUser.saveSessionPromise; + await flushSaveSessionQueue(); await this.dispatchStoreAction( this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER, diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue index 223ca9ee7..a6bb07bab 100644 --- a/src/layouts/license-plate-lookup/license-plate-lookup.vue +++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue @@ -119,6 +119,8 @@ import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; import vinPagesMixin from "@/mixins/vin-pages-mixin"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; +import { consumeQueryFromStash } from "@/router/a-rewrite/logic/querystring-stash"; +import { queryStrings } from "@/constants/query-strings"; // DEFINE VALIDATION RULES defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED)); @@ -138,7 +140,7 @@ export default { mixins: [vinPagesMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ @@ -163,7 +165,8 @@ export default { data() { return { licensePlate: this.getLicensePlateFromStore(), - registrationZipCode: this.getRegistrationZipFromStore() ?? this.$route.query.zipcode, + registrationZipCode: + this.getRegistrationZipFromStore() ?? consumeQueryFromStash(queryStrings.ZIP_CODE), emailOrSms: this.getEmailOrSmsFromStore(), serviceZipCode: this.getServiceZipFromStore(), carId: this.getCarIdfromStore(), @@ -187,7 +190,10 @@ export default { }, backButtonAction() { // route to move backwards - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, attachCustomEvents() { this.prependActionToMethod(this, this.forwardButtonAction, () => { @@ -392,9 +398,7 @@ export default { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true } + this.pageName ); } else { await this.navigateForwardWithSingleCarMatch(); diff --git a/src/layouts/molding-questions/molding-questions.vue b/src/layouts/molding-questions/molding-questions.vue index 2c5f5a7c3..82b1e1619 100644 --- a/src/layouts/molding-questions/molding-questions.vue +++ b/src/layouts/molding-questions/molding-questions.vue @@ -47,7 +47,7 @@ export default { mixins: [vehicleQuestionsMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 1d7771474..89568fa37 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -47,7 +47,7 @@ export default { mixins: [vehicleQuestionsMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index d5c7d93c3..d07434c1f 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -168,6 +168,7 @@ import { getSubTotal, getSalesTax, } from "@/helpers/pricing-helper.js"; +import { consumeQueryFromStash } from "@/router/a-rewrite/logic/querystring-stash"; defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); @@ -179,7 +180,7 @@ export default { }, async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const lineItemsFromStore = deepClone(store.getters.order.lineItems); @@ -281,7 +282,7 @@ export default { const oldActivePromos = store.getters.lineItems.promos?.slice(0); const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0); - const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO); + const promoCodeFromQueryString = consumeQueryFromStash(queryStrings.PROMO); const { validatePromoResponse, revalidatePromoResponse } = await revalidatePromosAndValidateQueryStringPromo( promoCodeFromQueryString, @@ -374,7 +375,7 @@ export default { doesCopyContainRouterLink, getRouterLinkRouteFromCopy, navigateWithScenario(scenarioName) { - this.$router.navigateWithoutSaving(scenarioName, this.$route); + this.$router.navigateWithoutSaving(scenarioName, this.pageName); }, arePagePrerequisitesValid() { // Service Location @@ -537,7 +538,10 @@ export default { return this.$store.getters.order.isRecalAckOptIn; }, backButtonAction() { - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, async forwardButtonAction() { this.forwardClicked = true; @@ -575,16 +579,14 @@ export default { await submitWorkOrder({ pageNameToLog: "payment-method", submitAfterSave: true }); this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_FORWARD, - this.$route, - {}, - { [routerParams.DISPLAY_PIA_ALERT]: false } + this.pageName ); } else { if (this.paymentMethod == paymentMethods.INSURANCE) { this.dispatchStoreAction(this.storeActions.SAVE_PAYMENT_TYPE, true, false); return this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_INSURANCE, - this.$route + this.pageName ); } else { this.setupPia(); @@ -603,16 +605,14 @@ export default { } catch (error) { console.log("error: response from pia submit work order:" + error.message); this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - this.$route.query[queryStrings.DISPLAY_PIA_ALERT] = true; + this.$route.query.piaError = true; return; } this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_PAY_NOW, - this.$route, - {}, - { [routerParams.DISPLAY_PIA_ALERT]: false } + this.pageName ); }, hasSubmittedOrder() { @@ -797,10 +797,7 @@ export default { return this.paymentMethodInternalModel; }, shouldDisplayPiaAlert() { - return ( - this.$route.query[queryStrings.DISPLAY_PIA_ALERT] || - getBoolFromString(window.history.state.displayPiaAlert) - ); + return getBoolFromString(this.$route?.query?.piaError); }, // Necessary to make the watcher of lineItems work // JavaScript does not keep a record of the old value, only a reference to it's location in the memory. diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index f0b4433ea..b39726948 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -39,20 +39,22 @@ export default { store.getters.order.payment.piaType === paymentMethods.APPLE_PAY || store.getters.order.payment.piaType === paymentMethods.AFTERPAY; if (paymentPageNavScenario) { - this.$router.navigateWithoutSaving( - this.navigationScenarios.PIA_CC_ERROR, - this.$route, - { - [queryStrings.DISPLAY_PIA_ALERT]: store.getters.order.payment.piaType, - } + const scenarios = { + [paymentMethods.CREDIT_CARD]: this.navigationScenarios.PIA_CC_ERROR, + [paymentMethods.APPLE_PAY]: this.navigationScenarios.PIA_APPLE_ERROR, + [paymentMethods.AFTERPAY]: this.navigationScenarios.PIA_AFTERPAY_ERROR, + }; + + const relevantScenario = scenarios[store.getters.order.payment.piaType]; + + await this.$router.navigateAndForceTopLevelNavigation( + relevantScenario, + this.pageName ); } else { - this.$router.navigateWithoutSaving( + await this.$router.navigateAndForceTopLevelNavigation( this.navigationScenarios.PIA_ERROR, - this.$route, - { - [queryStrings.DISPLAY_PIA_ALERT]: true, - } + this.pageName ); } } else { @@ -84,11 +86,9 @@ export default { } else { var msg = "Unknown Pia type: " + store.getters.order.payment.piaType; console.log(msg); - this.$router.navigateWithoutSaving( + await this.$router.navigateAndForceTopLevelNavigation( this.navigationScenarios.PIA_ERROR, - this.$route, - {}, - { [routerParams.DISPLAY_PIA_ALERT]: true } + this.pageName ); } } @@ -123,11 +123,9 @@ export default { " " + store.getters.order.referralSequenceNumber ); - this.$router.navigateWithoutSaving( + this.$router.navigateAndForceTopLevelNavigation( this.navigationScenarios.PIA_ERROR, - this.$route, - {}, - { [routerParams.DISPLAY_PIA_ALERT]: true } + this.pageName ); } else { const expMonth = getQuerystringParameter(queryStrings.CARD_EXPIRATION_MONTH); @@ -181,11 +179,9 @@ export default { }); } catch (error) { console.log("error: response from submit work order:" + error.message); - this.$router.navigateWithoutSaving( + this.$router.navigateAndForceTopLevelNavigation( this.navigationScenarios.PIA_ERROR, - this.$route, - {}, - { [routerParams.DISPLAY_PIA_ALERT]: true } + this.pageName ); this.$refs.loadingModal.isModalVisible = false; return; @@ -193,7 +189,10 @@ export default { this.$refs.loadingModal.isModalVisible = false; await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE); - this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_SUCCESS, this.$route); + this.$router.navigateAndForceTopLevelNavigation( + this.navigationScenarios.PIA_SUCCESS, + this.pageName + ); }, }, components: { diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 3c87f85f2..f2bdb6733 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -289,7 +289,7 @@ export default { }, async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const signaturePromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_SIGNATURE, @@ -684,12 +684,16 @@ export default { }); await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE); - window.location = applicationConfig.CONFIRMATION_URL; + this.$router.navigateAndForceTopLevelNavigation( + this.navigationScenarios.CLICKED_PAY_LATER, + this.pageName + ); + //window.location = applicationConfig.CONFIRMATION_URL; } catch (error) { console.log("error: response from submit work order(payment pg):" + error.message); this.$router.navigateWithoutSaving( this.navigationScenarios.PIA_ERROR, - this.$route, + this.pageName, {}, { [routerParams.DISPLAY_PIA_ALERT]: true } ); @@ -698,16 +702,13 @@ export default { } }, backButtonAction() { - // The only way I could figure out how to get navigation to work on a page with the iframe. - // This is also how a cancel from Paypal would work. Just a redirect to the payment-method page. - // The normal navigation sort of works but the page does not render. The "next" callback function does not get - // invoked in payment-method's beforeroute enter. I also spotted an error in vue-router.esm-bundler.js when this - // happens but was unable to ignore it with a try catch. The navigation guard error is: - // Error: Redirected from "/?fmgPage=payment-method" to "{ params: {isSavingNation: "false"}, query: "fmgPage: "payment-method"}" - // I don't know if the navigation guard thinks the to and from are the same page. - // Also tried overriding with a $router.push - window.location = applicationConfig.PIA_CANCEL_URL; - //this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + // Used to set the url directly here. + // Now hooks into a shared set of logic for doing this, also used with `payment-pia-return`. + // And uses nav scenarios properly. + this.$router.navigateAndForceTopLevelNavigation( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, submitHopForm() { this.$nextTick(() => { @@ -804,10 +805,7 @@ export default { } }, shouldDisplayPiaAlert(payMethod) { - return ( - this.$route.query[queryStrings.DISPLAY_PIA_ALERT] === payMethod || - getBoolFromString(window.history.state.displayPiaAlert) - ); + return this.$route?.query?.piaErrorType === payMethod; }, }, components: { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 663f778c1..f100a9f32 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -169,17 +169,23 @@ import { } from "@/helpers/heritage-integration/cookie-helper"; import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; +import { consumeQueryFromStash } from "@/router/a-rewrite/logic/querystring-stash"; import { storeMutations } from "@/constants/store-mutations"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); const INSURANCE_TAB_TO_DISPLAY_THRESHOLD_DEFAULT = 300; +const ACCEPTED_QUOTE_TYPES = { + CASH: "cash", + INSURANCE: "insurance", +}; + export default { name: "quote", async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_WIPERS, { @@ -216,7 +222,7 @@ export default { userId: getUserIdValue(), deviceId: getDeviceIdValue(), sessionKey: getSessionKeyValue(), - pageName: to.query.fmgPage, + pageName: to.name, experiment: skipQuoteExperiment, }, "quote", @@ -246,7 +252,7 @@ export default { userId: getUserIdValue(), deviceId: getDeviceIdValue(), sessionKey: getSessionKeyValue(), - pageName: to.query.fmgPage, + pageName: to.name, experiment: afterpayBreakoutDisplayExperiment, }, "quote", @@ -353,7 +359,7 @@ export default { const oldActivePromos = store.getters.lineItems.promos?.slice(0); const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0); - const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO); + const promoCodeFromQueryString = consumeQueryFromStash(queryStrings.PROMO); const { validatePromoResponse, revalidatePromoResponse } = await revalidatePromosAndValidateQueryStringPromo( promoCodeFromQueryString, @@ -403,10 +409,15 @@ export default { } const getIsInsuranceSelectedValue = (availableLineItems, insuranceThreshold) => { - // query param checked here is set on insurance-selection "Pay on my own" link click - if (to.query?.isCashSelected || to.query?.iscashselected) { + // Override auto-select if specified + const quoteTypeParam = to.query?.offer?.toLowerCase(); + + if (quoteTypeParam === ACCEPTED_QUOTE_TYPES.CASH) { return false; + } else if (quoteTypeParam === ACCEPTED_QUOTE_TYPES.INSURANCE) { + return true; } + const serviceLocationState = store.getters.order.serviceLocation.state; // usually true when returning from heritage but can be false when returning from heritage on a save quote @@ -760,12 +771,12 @@ export default { if (payment.isInsurance) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_INSURANCE, - this.$route + this.pageName ); } else { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_CASH, - this.$route + this.pageName ); } diff --git a/src/layouts/return-user/return-user.vue b/src/layouts/return-user/return-user.vue index d655a0c14..6b2a36f7e 100644 --- a/src/layouts/return-user/return-user.vue +++ b/src/layouts/return-user/return-user.vue @@ -55,7 +55,7 @@ export default { name: "return-user", async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ { @@ -85,7 +85,7 @@ export default { async forwardButtonAction() { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_FORWARD, - this.$route + this.pageName ); }, @@ -96,11 +96,9 @@ export default { "Start over", true ); - deleteFunnelCookie(); - await this.dispatchStoreAction(storeActions.RESET_STATE); this.$router.navigateWithoutSaving( - this.navigationScenarios.CLICKED_FORWARD, - this.$route + this.navigationScenarios.CLICKED_RESTART, + this.pageName ); }, }, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ae05b9358..b47de1fa8 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -267,7 +267,7 @@ export default { } // Set up promises - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const alertReasonsPromise = locationAlerts.methods.loadInitialData( store.getters.order.serviceLocation.zipCodeCtu, @@ -536,7 +536,10 @@ export default { } }, backButtonAction() { - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, forwardButtonAction() { this.updateSupportingItems(); @@ -585,7 +588,10 @@ export default { ); } - this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); + this.$router.navigateWithSaving( + this.navigationScenarios.CLICKED_FORWARD, + this.pageName + ); }, setDisplayWaitList() { if ( diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 3b37c8406..752d0c4c9 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -219,7 +219,7 @@ export default { }, async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const serviceZipCode = store.getters.order.serviceLocation.zipCode; const zipCodeDataPromise = getZipCodeData(serviceZipCode); @@ -611,7 +611,7 @@ export default { } else { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK, - this.$route + this.pageName ); } }, @@ -746,7 +746,10 @@ export default { this.updateAndSaveSupportingItems(); this.updateAndSaveIsMSRFeeApplicable(); - this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); + this.$router.navigateWithSaving( + this.navigationScenarios.CLICKED_FORWARD, + this.pageName + ); }, setMobileLocationInValid(isMobileLocationInValid) { this.isMobileAddressValid = !isMobileLocationInValid; diff --git a/src/layouts/service-zip/service-zip.vue b/src/layouts/service-zip/service-zip.vue index ae6ff4ecf..16c6f7f72 100644 --- a/src/layouts/service-zip/service-zip.vue +++ b/src/layouts/service-zip/service-zip.vue @@ -96,6 +96,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; +import { consumeQueryFromStash } from "@/router/a-rewrite/logic/querystring-stash"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -115,7 +116,7 @@ export default { mixins: [vinPagesMixin], data() { return { - serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode, + serviceZipCode: this.getZipFromStore() ?? consumeQueryFromStash(queryStrings.ZIP_CODE), emailOrSms: this.getEmailOrSmsFromStore(), carId: this.getCarIdfromStore(), isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(), @@ -127,7 +128,7 @@ export default { async beforeRouteEnter(to, from, next) { //Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); //Settle promises and get results const promiseResultMap = [ @@ -214,12 +215,12 @@ export default { if (skipVin) { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK_WITH_SKIP_VIN, - this.$route + this.pageName ); } else { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK, - this.$route + this.pageName ); } }, @@ -317,7 +318,7 @@ export default { await saveSession({ pageNameToLog: "service-zip" }); return this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_NO_QUESTIONS, - this.$route + this.pageName ); } else { // if we're skipping the vin-lookup but it's not a repair, we still need to get the parts diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index 46efb4728..488ad1eb4 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -110,7 +110,7 @@ export default { name: "vehicle-damage", async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const damageOptionsPromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_DAMAGE_OPTIONS, @@ -249,7 +249,10 @@ export default { backButtonAction() { // route to move backwards - this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, getDamageLocationsFromStore() { @@ -455,34 +458,26 @@ export default { if (skipVin) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } else { if (store.getters.damage.isRepair) { this.$router.navigateWithSaving( this.navigationScenarios .CLICKED_FORWARD_WITH_REPAIR_AND_VERIFIED_INSURANCE, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } else { if (store.getters.vehicle.vin) { this.$router.navigateWithSaving( this.navigationScenarios .CLICKED_FORWARD_WITH_REPLACE_AND_VERIFIED_INSURANCE, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } else { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } } @@ -492,23 +487,17 @@ export default { else if (store.getters.vehicle.vin) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } else if (skipVin) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } else { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } + this.pageName ); } }, @@ -637,7 +626,7 @@ export default { ); }, shouldDisplayVehicleChangeAlert() { - return getBoolFromString(window.history.state.displayVehicleChangeAlert); + return getBoolFromString(this.$route?.query?.displayVehicleChangeAlert); }, shouldHideBackButton() { return this.$store.getters.requiresVerifiedRedirecting; diff --git a/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue b/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue index 280edfc6c..5f06391c1 100644 --- a/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue +++ b/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue @@ -154,7 +154,7 @@ export default { }, PartDataFromApi() { - return this.$store.getters.pageData(this.$route.query.fmgPage) ?? {}; + return this.$store.getters.pageData(this.pageName) ?? {}; }, }, methods: { diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index 4b68007f9..4298b74cb 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.vue +++ b/src/layouts/vehicle-parts/vehicle-parts.vue @@ -81,7 +81,7 @@ export default { mixins: [vehicleQuestionsMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ { diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index bad52fb02..1d2e7d2bb 100644 --- a/src/layouts/vehicle/vehicle.vue +++ b/src/layouts/vehicle/vehicle.vue @@ -110,6 +110,10 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { applicationConfig } from "../../constants/application-config"; import { queryStrings } from "@/constants/query-strings"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { + consumeQueryFromStash, + peekQueryFromStash, +} from "@/router/a-rewrite/logic/querystring-stash"; //define validation rules defineRule("year-required", required(errorMessages.YEAR_REQUIRED)); @@ -133,7 +137,7 @@ export default { imageUrl: this.getImagefromStore(), imageVifNumber: this.getImageVifNumberfromStore(), imageVifColor: this.getImageVifColorfromStore(), - serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode, + serviceZipCode: this.getZipFromStore() ?? peekQueryFromStash(queryStrings.ZIP_CODE), yearOptions: [], makeOptions: [], modelOptions: [], @@ -154,7 +158,7 @@ export default { ); } // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); const experimentForLogging = store.getters.applicationUser.experiments.find( (e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL ); @@ -213,7 +217,7 @@ export default { userId: getUserIdValue(), deviceId: getDeviceIdValue(), sessionKey: getSessionKeyValue(), - pageName: to.query.fmgPage, + pageName: to.name, experiment: experimentForLogging, }, "vehicle", @@ -536,7 +540,10 @@ export default { } } - this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); + this.$router.navigateWithSaving( + this.navigationScenarios.CLICKED_FORWARD, + this.pageName + ); }, initializeYearComponent(initialData) { this.yearOptions = initialData; diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 23875e3e9..5db93d76d 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -160,6 +160,8 @@ import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; import vinPagesMixin from "@/mixins/vin-pages-mixin"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; +import { consumeQueryFromStash } from "@/router/a-rewrite/logic/querystring-stash"; +import { queryStrings } from "@/constants/query-strings"; // DEFINE VALIDATION RULES defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -180,7 +182,7 @@ export default { mixins: [vinPagesMixin], async beforeRouteEnter(to, from, next) { // Call APIs - const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const cmsContentPromise = fetchCmsContentForPage(to.name); // Settle promises and get results const promiseResultMap = [ @@ -203,7 +205,7 @@ export default { data() { return { vin: this.getVinFromStore(), - serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode, + serviceZipCode: this.getZipFromStore() ?? consumeQueryFromStash(queryStrings.ZIP_CODE), emailOrSms: this.getEmailOrSmsFromStore(), carId: this.getCarIdfromStore(), isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(), @@ -247,12 +249,12 @@ export default { if (this.$store.getters.vehicle.vin) { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK_WITH_VIN, - this.$route + this.pageName ); } else { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK, - this.$route + this.pageName ); } }, @@ -474,9 +476,7 @@ export default { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS, - this.$route, - {}, - { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true } + this.pageName ); } else { await this.navigateForwardWithSingleCarMatch(); diff --git a/src/main.js b/src/main.js index 1017651f8..5b823125b 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,7 @@ import { createApp } from "vue"; import LoadScript from "vue-plugin-load-script"; import Maska from "maska"; import App from "./App.vue"; -import router from "./router"; +import router from "./router/a-rewrite"; import store from "@/store"; import baseMixin from "@/mixins/base-mixin.js"; import analyticsMixin from "@/mixins/analytics-mixin.js"; diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index b29eae8c1..e39908a8f 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -2,7 +2,7 @@ import store from "@/store"; import { storeActions } from "@/constants/store-actions.js"; import { storeMutations } from "@/constants/store-mutations.js"; import { sessionStorageKeyConstants } from "@/constants/session-storage.js"; -import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; +import { navigationScenarios } from "@/router/a-rewrite/constants/navigation-scenarios"; import { vehicleCategories } from "@/constants/vehicle-categories.js"; import { routerParams } from "@/router/router-constants/router-params"; import { queryStrings } from "@/constants/query-strings"; @@ -15,6 +15,7 @@ export default { data() { return { cmsContentByWidget: {}, + _componentPageName: null, }; }, methods: { @@ -246,9 +247,15 @@ export default { cssClassNameForCmsWidget() { return "widget-name-" + this.cmsWidgetName; }, + pageName() { + return this._componentPageName ?? this.$route.name; + }, }, mounted() { + // Store route immutably so that components know their originating page even during page transition. + this._componentPageName = this.$route.name; + if (!store.getters.externalParameterState?.isExternalParameter) { showFmgLoadingModal(false); } diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js index 517fc7c90..ab39462ce 100644 --- a/src/mixins/vehicle-questions-mixin.js +++ b/src/mixins/vehicle-questions-mixin.js @@ -65,10 +65,10 @@ export default { orderedVehicleQuestionPages.indexOf(fmgPage) ); }, - currentPageComesBeforePage(currentPage = this.$route.query.fmgPage, fmgPage) { + currentPageComesBeforePage(currentPage = this.pageName, fmgPage) { return this.comparePageIndices(currentPage, fmgPage) < 0; }, - currentPageComesAfterPage(currentPage = this.$route.query.fmgPage, fmgPage) { + currentPageComesAfterPage(currentPage = this.pageName, fmgPage) { return this.comparePageIndices(currentPage, fmgPage) > 0; }, setupInitialData(glass, index, alreadyAnsweredQuestions, vm) { @@ -359,7 +359,7 @@ export default { // Can't use `this` because navigateForward is also called from vin-pages-mixin async navigateForward(partsOrQuestions, vm) { const self = vm ?? this; - const currentPage = self.$route.query.fmgPage; + const currentPage = self.pageName; const hasPartQuestions = this.hasPartQuestions(partsOrQuestions); const hasGlassLocationWithMultipleParts = @@ -371,11 +371,9 @@ export default { hasPartQuestions && this.currentPageComesBeforePage(currentPage, fmgPageValues.PART_QUESTIONS) ) { - self.$router.navigateWithSaving( + self.$router.navigateWithPageData( self.navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, - self.$route, - {}, - {}, + self.pageName, { partsOrQuestions: partsOrQuestions } ); } else if ( @@ -384,11 +382,9 @@ export default { ) { // if multiple parts on any glass // go to vehicle-parts page and pass the partsData - self.$router.navigateWithSaving( + self.$router.navigateWithPageData( self.navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, - self.$route, - {}, - {}, + self.pageName, { partsOrQuestions: partsOrQuestions } ); } else if ( @@ -397,11 +393,9 @@ export default { ) { // if any childpart questions // go to molding-questions page and pass the partsData - self.$router.navigateWithSaving( + self.$router.navigateWithPageData( self.navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, - self.$route, - {}, - {}, + self.pageName, { partsOrQuestions: partsOrQuestions } ); } else if ( @@ -438,11 +432,9 @@ export default { } } - self.$router.navigateWithSaving( + self.$router.navigateWithPageData( self.navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, - self.$route, - {}, - {}, + self.pageName, { partsOrQuestions } ); } else { @@ -477,7 +469,7 @@ export default { } else { self.$router.navigateWithSaving( self.navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, - self.$route + self.pageName ); } } @@ -485,7 +477,7 @@ export default { // Can't use `this` because navigateForward is also called from quote async navigateBack(vm) { const self = vm ?? this; - const currentPage = self.$route.query.fmgPage; + const currentPage = self.pageName; const pageDataCapabilityQuestions = self.$store.getters.pageData( fmgPageValues.CAPABILITY_QUESTIONS ); @@ -539,7 +531,7 @@ export default { backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS; } - self.$router.navigateWithoutSaving(backNavigationScenario, self.$route); + self.$router.navigateWithoutSaving(backNavigationScenario, self.pageName); }, }, }; diff --git a/src/router/a-rewrite/constants/auto-route-table.js b/src/router/a-rewrite/constants/auto-route-table.js new file mode 100644 index 000000000..0bf3503b9 --- /dev/null +++ b/src/router/a-rewrite/constants/auto-route-table.js @@ -0,0 +1,13 @@ +import { routeData } from "./routes"; + +export const autoRouteTable = [ + routeData.QUOTE, + routeData.CAPABILITY_QUESTIONS, + routeData.MOLDING_QUESTIONS, + routeData.VEHICLE_PARTS, + routeData.PART_QUESTIONS, + routeData.VIN_LOOKUP, + routeData.ESTIMATE, + routeData.VEHICLE_DAMAGE, + routeData.VEHICLE, +]; diff --git a/src/router/a-rewrite/constants/navigation-scenarios.js b/src/router/a-rewrite/constants/navigation-scenarios.js new file mode 100644 index 000000000..a6e117948 --- /dev/null +++ b/src/router/a-rewrite/constants/navigation-scenarios.js @@ -0,0 +1,69 @@ +/** + * We want to name these to describe the scenario rather than what page(s) they're used on. + * + * Naming convention: [PAST_TENSE_VERB]_[DESCRIPTOR_OF_STATE] + * ex: Rather than "HAS_PART_QUESTIONS", we might name the + * scenario "CLICKED_FORWARD_WITH_PART_QUESTIONS" + */ + +const navigationScenarios = { + // General + CLICKED_BACK: "CLICKED_BACK", + CLICKED_FORWARD: "CLICKED_FORWARD", + CLICKED_FORWARD_WITH_CASH: "CLICKED_FORWARD_WITH_CASH", + CLICKED_FORWARD_WITH_INSURANCE: "CLICKED_FORWARD_WITH_INSURANCE", + + // TODO: use virtual page? + // Vin selection + CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN", + CLICKED_BACK_WITH_SKIP_VIN: "CLICKED_BACK_WITH_SKIP_VIN", + CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN", + CLICKED_FORWARD_WITH_REPAIR_AND_VERIFIED_INSURANCE: + "CLICKED_FORWARD_WITH_REPAIR_AND_VERIFIED_INSURANCE", + CLICKED_FORWARD_WITH_REPLACE_AND_VERIFIED_INSURANCE: + "CLICKED_FORWARD_WITH_REPLACE_AND_VERIFIED_INSURANCE", + CLICKED_FORWARD_WITHOUT_VIN: "CLICKED_FORWARD_WITHOUT_VIN", + CLICKED_FORWARD_WITH_SKIP_VIN: "CLICKED_FORWARD_WITH_SKIP_VIN", + CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES: "CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES", + SELECTED_VIN_WITH_MISMATCHED_GLASS: "SELECTED_VIN_WITH_MISMATCHED_GLASS", + SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN", + SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE", + SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS", + SELECTED_NO_VIN: "SELECTED_NO_VIN", + CLICKED_FORWARD_WITH_NO_QUESTIONS: "CLICKED_FORWARD_WITH_NO_QUESTIONS", + CLICKED_VIN_RETRY: "CLICKED_VIN_RETRY", + + // Part selection + CLICKED_FORWARD_WITH_PART_QUESTIONS: "CLICKED_FORWARD_WITH_PART_QUESTIONS", + CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE: "CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE", + CLICKED_FORWARD_WITH_MOLDING_QUESTIONS: "CLICKED_FORWARD_WITH_MOLDING_QUESTIONS", + CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS: "CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS", + CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS: "CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS", + CLICKED_BACK_WITH_PART_QUESTIONS: "CLICKED_BACK_WITH_PART_QUESTIONS", + CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE: "CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE", + CLICKED_BACK_WITH_MOLDING_QUESTIONS: "CLICKED_BACK_WITH_MOLDING_QUESTIONS", + CLICKED_BACK_WITH_CAPABILITY_QUESTIONS: "CLICKED_BACK_WITH_CAPABILITY_QUESTIONS", + CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS", + CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS", + + // Insurance-Company + CLICKED_PAY_ON_MY_OWN: "CLICKED_PAY_ON_MY_OWN", + + // Schedule + CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION", + + // Payment + CLICKED_INSURANCE: "CLICKED_INSURANCE", + CLICKED_PAY_NOW: "CLICKED_PAY_NOW", + CLICKED_PAY_LATER: "CLICKED_PAY_LATER", + PIA_ERROR: "PIA_ERROR", + PIA_CC_ERROR: "PIA_CC_ERROR", + PIA_APPLE_ERROR: "PIA_APPLE_ERROR", + PIA_AFTERPAY_ERROR: "PIA_AFTERPAY_ERROR", + PIA_SUCCESS: "PIA_SUCCESS", + + // Return User + CLICKED_RESTART: "CLICKED_RESTART", +}; + +export { navigationScenarios }; diff --git a/src/router/a-rewrite/constants/route-prefix.js b/src/router/a-rewrite/constants/route-prefix.js new file mode 100644 index 000000000..7e5c6f1b9 --- /dev/null +++ b/src/router/a-rewrite/constants/route-prefix.js @@ -0,0 +1,3 @@ +export const ROUTE_PREFIX_TRIMMED = "fmg"; + +export const ROUTE_PREFIX_FULL = `/${ROUTE_PREFIX_TRIMMED}/`; diff --git a/src/router/a-rewrite/constants/routes.js b/src/router/a-rewrite/constants/routes.js new file mode 100644 index 000000000..d03954867 --- /dev/null +++ b/src/router/a-rewrite/constants/routes.js @@ -0,0 +1,123 @@ +export const routeData = { + VEHICLE: { + name: "vehicle", + path: "/vehicle", + }, + VEHICLE_DAMAGE: { + name: "vehicle-damage", + path: "/vehicle-damage", + }, + ESTIMATE: { + name: "estimate", + path: "/estimate", + }, + ADDRESS_LOOKUP: { + name: "address-lookup", + path: "/address-lookup", + }, + VIN_LOOKUP: { + name: "vin-lookup", + path: "/vin-lookup", + }, + LICENSE_PLATE_LOOKUP: { + name: "license-plate-lookup", + path: "/license-plate-lookup", + }, + SERVICE_ZIP: { + name: "service-zip", + path: "/service-zip", + }, + ADDRESS_VEHICLES: { + name: "address-vehicles", + path: "/address-vehicles", + }, + VEHICLE_PARTS: { + name: "vehicle-parts", + path: "/vehicle-parts", + }, + PART_QUESTIONS: { + name: "part-questions", + path: "/part-questions", + }, + MOLDING_QUESTIONS: { + name: "molding-questions", + path: "/molding-questions", + }, + CAPABILITY_QUESTIONS: { + name: "capability-questions", + path: "/capability-questions", + }, + QUOTE: { + name: "quote", + path: "/quote", + }, + INSURANCE_COMPANY: { + name: "insurance-company", + path: "/insurance-company", + }, + SERVICE_LOCATION: { + name: "service-location", + path: "/service-location", + }, + HERITAGE: { + name: "heritage", + path: "/virtual/heritage", + virtual: true, + }, + SCHEDULE: { + name: "schedule", + path: "/schedule", + }, + CUSTOMER_DETAILS: { + name: "customer-details", + path: "/customer-details", + }, + PAYMENT_METHOD: { + name: "payment-method", + path: "/payment-method", + }, + PAYMENT: { + name: "payment", + path: "/payment", + }, + PAYMENT_PIA_RETURN: { + name: "payment-pia-return", + path: "/payment-pia-return", + }, + CONFIRMATION: { + name: "confirmation", + path: "/confirmation", + }, + RETURN_USER: { + name: "return-user", + path: "/return-user", + }, + LOAD_SESSION: { + name: "load-session", + path: "/virtual/load-session", + virtual: true, + }, + LANDING: { + name: "landing", + path: "/", + virtual: true, + }, + AUTO_ROUTE: { + name: "auto-route", + path: "/virtual/auto-route", + virtual: true, + }, + // TODO: RENAME FROM BAILOUT + ERROR: { + name: "error", + path: "/virtual/error", + virtual: true, + }, + RESTART: { + name: "restart", + path: "/virtual/restart", + virtual: true, + }, +}; + +export const FUNNEL_START_PAGE = routeData.VEHICLE; diff --git a/src/router/a-rewrite/constants/routing-table.js b/src/router/a-rewrite/constants/routing-table.js new file mode 100644 index 000000000..10ee7ae90 --- /dev/null +++ b/src/router/a-rewrite/constants/routing-table.js @@ -0,0 +1,596 @@ +import { routeData } from "@/router/a-rewrite/constants/routes"; +import { navigationScenarios } from "@/router/a-rewrite/constants/navigation-scenarios"; +import store from "@/store"; +import { paymentMethods } from "../../../constants/payment-method-constants"; + +// Get as function to live-evaluate filters +const routingTable = function () { + return [ + { + pageName: routeData.VEHICLE.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.VEHICLE_DAMAGE, + }, + ], + }, + { + pageName: routeData.VEHICLE_DAMAGE.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.VEHICLE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, + destinationPageData: routeData.ESTIMATE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: + navigationScenarios.CLICKED_FORWARD_WITH_REPAIR_AND_VERIFIED_INSURANCE, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: + navigationScenarios.CLICKED_FORWARD_WITH_REPLACE_AND_VERIFIED_INSURANCE, + destinationPageData: routeData.VIN_LOOKUP, + }, + ], + }, + { + pageName: routeData.VIN_LOOKUP.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.ESTIMATE, + }, + { + scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS, + destinationPageData: routeData.VEHICLE_DAMAGE, + params: { + displayVehicleChangeAlert: true, + }, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_VIN, + destinationPageData: routeData.VEHICLE_DAMAGE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_VIN_RETRY, + destinationPageData: routeData.ESTIMATE, + }, + ], + }, + { + pageName: routeData.LICENSE_PLATE_LOOKUP.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.ESTIMATE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.VEHICLE_DAMAGE, + params: { + displayVehicleChangeAlert: true, + }, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_VIN_RETRY, + destinationPageData: routeData.ESTIMATE, + }, + ], + }, + { + pageName: routeData.ADDRESS_LOOKUP.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.ESTIMATE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES, + destinationPageData: routeData.ADDRESS_VEHICLES, + }, + { + scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS, + destinationPageData: routeData.VEHICLE_DAMAGE, + params: { + displayVehicleChangeAlert: true, + }, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_VIN_RETRY, + destinationPageData: routeData.ESTIMATE, + }, + ], + }, + { + pageName: routeData.ADDRESS_VEHICLES.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.ADDRESS_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.VEHICLE_DAMAGE, + params: { + displayVehicleChangeAlert: true, + }, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_VIN_RETRY, + destinationPageData: routeData.ESTIMATE, + }, + ], + }, + { + pageName: routeData.ESTIMATE.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.VEHICLE_DAMAGE, + }, + { + scenario: navigationScenarios.SELECTED_MANUAL_VIN, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.SELECTED_LICENSE_PLATE, + destinationPageData: routeData.LICENSE_PLATE_LOOKUP, + }, + { + scenario: navigationScenarios.SELECTED_HOME_ADDRESS, + destinationPageData: routeData.ADDRESS_LOOKUP, + }, + { + scenario: navigationScenarios.SELECTED_NO_VIN, + destinationPageData: routeData.SERVICE_ZIP, + }, + ], + }, + { + pageName: routeData.SERVICE_ZIP.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.ESTIMATE, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_SKIP_VIN, + destinationPageData: routeData.VEHICLE_DAMAGE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + ], + }, + { + pageName: routeData.PART_QUESTIONS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + ], + }, + { + pageName: routeData.VEHICLE_PARTS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + ], + }, + { + pageName: routeData.MOLDING_QUESTIONS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + ], + }, + { + pageName: routeData.CAPABILITY_QUESTIONS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + destinationPageData: routeData.QUOTE, + }, + ], + }, + { + pageName: routeData.QUOTE.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS, + destinationPageData: routeData.VIN_LOOKUP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS, + destinationPageData: routeData.SERVICE_ZIP, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS, + destinationPageData: routeData.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE, + destinationPageData: routeData.VEHICLE_PARTS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS, + destinationPageData: routeData.MOLDING_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_CAPABILITY_QUESTIONS, + destinationPageData: routeData.CAPABILITY_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_CASH, + destinationPageData: routeData.SERVICE_LOCATION, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD_WITH_INSURANCE, + destinationPageData: routeData.INSURANCE_COMPANY, + }, + ], + }, + { + pageName: routeData.INSURANCE_COMPANY.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_PAY_ON_MY_OWN, + destinationPageData: routeData.QUOTE, + params: { + offer: "cash", + }, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.INSURANCE_DETAILS, + }, + ], + }, + { + pageName: routeData.SERVICE_LOCATION.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.SCHEDULE, + }, + ], + }, + { + pageName: routeData.SCHEDULE.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.SERVICE_LOCATION, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.CUSTOMER_DETAILS, + }, + { + scenario: navigationScenarios.CLICKED_CHANGE_LOCATION, + destinationPageData: routeData.SERVICE_LOCATION, + }, + ], + }, + { + pageName: routeData.CUSTOMER_DETAILS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.SCHEDULE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.PAYMENT_METHOD, + }, + { + scenario: navigationScenarios.CLICKED_PAY_NOW, + destinationPageData: routeData.PAYMENT_METHOD, + }, + ], + }, + { + pageName: routeData.PAYMENT_METHOD.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.CUSTOMER_DETAILS, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.CONFIRMATION, + }, + { + scenario: navigationScenarios.CLICKED_PAY_NOW, + destinationPageData: routeData.PAYMENT, + }, + { + scenario: navigationScenarios.CLICKED_INSURANCE, + destinationPageData: routeData.INSURANCE_COMPANY, + }, + ], + }, + { + pageName: routeData.PAYMENT.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.PAYMENT_METHOD, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.CONFIRMATION, + }, + { + scenario: navigationScenarios.PIA_ERROR, + destinationPageData: routeData.PAYMENT_METHOD, + params: { + piaError: "true", + }, + }, + ], + }, + { + pageName: routeData.PAYMENT_PIA_RETURN.name, + maps: [ + { + scenario: navigationScenarios.PIA_ERROR, + destinationPageData: routeData.PAYMENT_METHOD, + params: { + piaError: "true", + }, + }, + { + scenario: navigationScenarios.PIA_CC_ERROR, + destinationPageData: routeData.PAYMENT, + params: { + piaErrorType: paymentMethods.CREDIT_CARD, + }, + }, + { + scenario: navigationScenarios.PIA_APPLE_ERROR, + destinationPageData: routeData.PAYMENT, + params: { + piaErrorType: paymentMethods.APPLE_PAY, + }, + }, + { + scenario: navigationScenarios.PIA_AFTERPAY_ERROR, + destinationPageData: routeData.PAYMENT, + params: { + piaErrorType: paymentMethods.AFTERPAY, + }, + }, + { + scenario: navigationScenarios.PIA_SUCCESS, + destinationPageData: routeData.CONFIRMATION, + }, + ], + }, + { + pageName: routeData.CONFIRMATION.name, + }, + { + pageName: routeData.RETURN_USER.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.LOAD_SESSION, + }, + { + scenario: navigationScenarios.CLICKED_RESTART, + destinationPageData: routeData.RESTART, + }, + ], + }, + ]; +}; + +export { routingTable }; diff --git a/src/router/a-rewrite/index.js b/src/router/a-rewrite/index.js new file mode 100644 index 000000000..3a84a1730 --- /dev/null +++ b/src/router/a-rewrite/index.js @@ -0,0 +1,421 @@ +import { routeData, FUNNEL_START_PAGE } from "@/router/a-rewrite/constants/routes"; +import { createRoute, createVirtualRoute } from "@/router/a-rewrite/logic/create-route"; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; +import { createWebHistory, createRouter } from "vue-router"; +import { getDestination } from "@/router/a-rewrite/logic/get-destination"; +import analyticsMixin from "@/mixins/analytics-mixin"; +import { + getFunnelCookie, + deleteFunnelCookie, + updateOrCreateFunnelCookie, +} from "@/helpers/heritage-integration/cookie-helper"; +import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; +import { loadSessionIfPresent, saveSession } from "@/helpers/heritage-integration/order-helper"; +import store from "@/store"; +import { storeActions } from "@/constants/store-actions"; +import { storeMutations } from "@/constants/store-mutations"; +import { sessionStorageKeyConstants } from "@/constants/session-storage"; +import { savePageData } from "@/router/a-rewrite/logic/save-page-data"; +import eventBus from "@/helpers/event-bus/event-bus"; +import { globalEvents, globalEventTypes } from "@/constants/events"; +import { autoRouteTable } from "@/router/a-rewrite/constants/auto-route-table"; +import { queryStrings } from "@/constants/query-strings"; +import { consumeReferralQuerystrings } from "@/router/a-rewrite/logic/consume-referral-info"; +import { initializeFromQueryStrings } from "@/router/a-rewrite/logic/initialize-from-querystrings"; +import { getBoolFromString } from "@/helpers/boolean-helper"; +import { isVerifiedInsurance } from "@/router/a-rewrite/logic/requires-verified-redirect"; +import baseMixin from "@/mixins/base-mixin"; +import { runExperiments } from "@/router/a-rewrite/logic/run-experiments"; +import { ROUTE_PREFIX_FULL } from "@/router/a-rewrite/constants/route-prefix"; +import { buildManualUrl } from "@/router/a-rewrite/logic/build-manual-url"; +import { consumeQueryFromStash, stashAllQueries } from "@/router/a-rewrite/logic/querystring-stash"; + +const routes = [ + // Non-virtual pages. + createRoute(routeData.VEHICLE, async (to, from) => { + // If cookie indicates that vehicle has changed during insurance flow, reset parts info. + const hasVehicleChanged = getBoolFromString(getFunnelCookie()?.HasDelayedClaimRegistration); + if (hasVehicleChanged) { + store.commit(storeMutations.RESET_GLASS_PARTS_STATE); + } + + if (isVerifiedInsurance()) { + return { + name: routeData.VEHICLE_DAMAGE.name, + replace: true, + }; + } + }), + createRoute(routeData.VEHICLE_DAMAGE), + createRoute(routeData.ESTIMATE), + createRoute(routeData.ADDRESS_LOOKUP), + createRoute(routeData.VIN_LOOKUP), + createRoute(routeData.LICENSE_PLATE_LOOKUP), + createRoute(routeData.SERVICE_ZIP), + createRoute(routeData.ADDRESS_VEHICLES), + createRoute(routeData.VEHICLE_PARTS), + createRoute(routeData.PART_QUESTIONS), + createRoute(routeData.MOLDING_QUESTIONS), + createRoute(routeData.CAPABILITY_QUESTIONS), + createRoute(routeData.QUOTE, async (to, from) => { + if (isVerifiedInsurance()) { + return { + name: routeData.HERITAGE.name, + replace: true, + }; + } + + const offerQuote = to.query?.[queryStrings.OFFER_QUOTE]; + + if (offerQuote === "true") { + store.commit(storeMutations.UPDATE_IS_INSURANCE, false); + } + }), + createRoute(routeData.INSURANCE_COMPANY, async (to, from) => { + if (isVerifiedInsurance()) { + return { + name: routeData.HERITAGE.name, + replace: true, + }; + } + }), + createRoute(routeData.SERVICE_LOCATION), + createRoute(routeData.SCHEDULE), + createRoute(routeData.CUSTOMER_DETAILS), + createRoute(routeData.PAYMENT_METHOD), + createRoute(routeData.PAYMENT), + createRoute(routeData.PAYMENT_PIA_RETURN), + createRoute(routeData.CONFIRMATION), + createRoute(routeData.RETURN_USER), + + // Virtual pages (resolve to a non-virtual page.) + createVirtualRoute(routeData.LANDING, async (to, from) => { + await initializeFromQueryStrings(); + // Save querystrings to temporary stash so they can be referred to later. + stashAllQueries(to); + // Change referral info. For use in local testing for insurance crossover. + await consumeReferralQuerystrings(); + + // Check for fmgpage parameter + const fmgPage = to?.query?.fmgPage; + if (fmgPage && router.hasRoute(fmgPage)) { + return { + name: fmgPage, + replace: true, + }; + } + + // If a session is still in memory, go to return-user. + if (store.getters.vehicle?.year > 0) { + console.log(`trying to redirect!`); + return { + name: routeData.RETURN_USER.name, + replace: true, + }; + } + + // Otherwise, load session and auto-route + return { + name: routeData.LOAD_SESSION.name, + replace: true, + }; + }), + createVirtualRoute(routeData.HERITAGE, async (to, from) => { + await navigateToHeritageFunnel({ shouldSaveSession: false, pageNameToLog: to.name }); + }), + createVirtualRoute(routeData.LOAD_SESSION, async (to, from) => { + const loadSessionResponse = await loadSessionIfPresent(false, to.name); + + return { + name: routeData.AUTO_ROUTE.name, + replace: true, + }; + }), + createVirtualRoute(routeData.AUTO_ROUTE, async (to, from) => { + for (let i = 0; i < autoRouteTable.length; i++) { + const testRouteData = autoRouteTable[i]; + + const isReady = await router.checkPagePrerequisites(testRouteData.name); + + if (isReady) { + return { + name: testRouteData.name, + replace: true, + }; + } + } + + return { + name: FUNNEL_START_PAGE, + replace: true, + }; + }), + createVirtualRoute(routeData.ERROR, async (to, from) => { + // If we've already encountered one error, clear session to avoid more. + // Otherwise mark that we encoutnered an error here. + if (store.getters.applicationUser.hasAlreadyTriggeredError) { + return { + name: routeData.RESTART.name, + replace: true, + }; + } else { + await store.dispatch(storeActions.UPDATE_HAS_TRIGGERED_ERROR, true); + } + + // Add error event to bus + eventBus.addEventToBus( + globalEvents.Categories.GLOBAL_ALERT, + globalEvents.SubCategories.UNKNOWN_ERROR, + { + isDismissable: true, + messageCopy: "", + messageHeadline: "We're sorry, something went wrong.", + type: globalEventTypes.Danger, + } + ); + + let nextPage = FUNNEL_START_PAGE.name; + + baseMixin.methods?.ResetExternalParamsAndHideModal(); + + if (store.getters.payment?.insuranceCoverage?.isVerified) { + nextPage = routeData.VEHICLE_DAMAGE.name; + } + + if (from.name === nextPage) { + // Router won't load the page if we redirect to the already existing page + // Force refresh in this scenario. + router.go(0); + return false; + } + + return { + name: nextPage, + replace: false, + }; + }), + createVirtualRoute(routeData.RESTART, async (to, from) => { + await store.dispatch(storeActions.RESET_STATE); + deleteFunnelCookie(); + + if (from.name === FUNNEL_START_PAGE.name) { + // Router won't load the page if we redirect to the already existing page + // Force refresh in this scenario. + router.go(0); + return false; + } + + return { + name: FUNNEL_START_PAGE.name, + replace: true, + }; + }), +]; + +const router = createRouter({ + history: createWebHistory(ROUTE_PREFIX_FULL), + routes, + // Scroll to top on navigation. + scrollBehavior(to, from, savedPosition) { + return { top: 0 }; + }, +}); + +////-- Global Guards + +router.beforeEach(async (to, from) => { + console.log(to); + try { + await analyticsMixin.methods.validateSession(); + + if (getFunnelCookie()?.SuppressConceptFunnel) { + // Redirect to heritage. + return { + name: routeData.HERITAGE.name, + }; + } + + // Ensure session has not expired + if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { + const errorPayload = { + cause: "expired session", + currentPage: from?.name, + nextPage: to?.name, + }; + + router.bailout(errorPayload, true); + return false; + } + + // Block navigation if an order has been submitted. + if (window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) !== null) { + if (to.name !== FUNNEL_START_PAGE.name && to.name !== routeData.CONFIRMATION.name) { + return false; + } + } + + // Process funnel cookie. + updateOrCreateFunnelCookie(); + + // If sent from heritage, need to load session. + const fromHeritageFlag = consumeQueryFromStash(queryStrings.FROM_HERITAGE); + if (fromHeritageFlag) { + await loadSessionIfPresent(true, routeData.SERVICE_LOCATION.name); + } + + // Ensure page-prerequisites are fulfilled + const arePagePrerequisitesValid = await router.checkPagePrerequisites(to.name); + + if (!arePagePrerequisitesValid) { + const errorPayload = { + cause: "invalid page prerequisites for page", + currentPage: from?.name, + nextPage: to?.name, + }; + + router.bailout(errorPayload); + return; + } + + await runExperiments(to.name); + } catch (error) { + const errorPayload = { + cause: "Uncaught exception in `beforeEach`.", + currentPage: from?.name, + nextPage: to?.name, + }; + + console.log(error); + + router.bailout(errorPayload); + return; + } +}); + +router.afterEach(async (to, from) => { + // Push page view to GA + analyticsMixin.methods.pushPageViewToGA(); + + // Push experiments to Data Layer + analyticsMixin.methods.pushExperimentsToDataLayer(); + + // Push current order status to Data Layer + analyticsMixin.methods.pushOrderToDataLayer(); +}); + +////-- End Global Guards + +////-- Bound Functions + +router.getRoute = (pageName) => { + return router.getRoutes().find((r) => r.name === pageName); +}; + +router.checkPagePrerequisites = async (pageName) => { + const route = router.getRoute(pageName); + + if (!route) { + return false; + } + + let component = route.components; + + // Virtual routes have no component, so skip check. + if (!component) { + return true; + } + + if (component?.default?.methods === undefined) { + component = await component?.default(); + } + + return component?.default?.methods?.arePagePrerequisitesValid(); +}; + +router.navigateWithoutSaving = async (scenario, currentPageName) => { + return await router.navigate(currentPageName, scenario, false); +}; + +router.navigateWithSaving = async (scenario, currentPageName) => { + return await router.navigate(currentPageName, scenario, true); +}; + +router.navigateWithPageData = async (scenario, currentPageName, pageData = {}) => { + const nextPage = getDestination(currentPageName, scenario); + await savePageData(nextPage.name, pageData); + + return await router.navigate(currentPageName, scenario, true); +}; + +router.navigateAndForceTopLevelNavigation = async (scenario, currentPageName, saving = false) => { + return await router.navigate(currentPageName, scenario, saving, true); +}; + +router.bailout = (errorPayload, forceRestart = false) => { + analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload); + + if (forceRestart) { + router.push({ + name: routeData.RESTART.name, + }); + } else { + router.push({ + name: routeData.ERROR.name, + }); + } +}; + +router.navigate = async ( + currentPageName, + scenario, + withSaving = false, + forceTopLevelNav = false +) => { + // Check to see if calling page is same as current page. + // If not, cancel navigation before it begins. + const callingRoute = currentPageName; + const actualRoute = router.currentRoute.value.name; + + if (callingRoute !== actualRoute) { + return false; + } + + const nextPage = getDestination(currentPageName, scenario); + + if (!nextPage) { + console.error( + `Could not navigate from ${currentPageName} via scenario ${scenario}. Ensure routing table is configured correctly.` + ); + + const errorPayload = { + cause: "invalid page name", + currentPage: currentPageName, + nextPage: nextPage?.name, + }; + + router.bailout(errorPayload); + return; + } + + if (withSaving) { + await saveSession({ pageNameToLog: nextPage.name }); + } + + // Some pages require manually setting the next url + // to work with iframes. + if (forceTopLevelNav) { + const newUrl = buildManualUrl(nextPage); + window.top.location.href = newUrl; + return; + } + + // All other pages use router.push + router.push({ + name: nextPage.name, + query: nextPage.params ?? {}, + }); +}; + +////-- End Router Functions + +export default router; diff --git a/src/router/a-rewrite/logic/build-manual-url.js b/src/router/a-rewrite/logic/build-manual-url.js new file mode 100644 index 000000000..017816d6b --- /dev/null +++ b/src/router/a-rewrite/logic/build-manual-url.js @@ -0,0 +1,27 @@ +import { ROUTE_PREFIX_TRIMMED } from "@/router/a-rewrite/constants/route-prefix"; + +export function buildManualUrl(nextPage) { + const queries = nextPage.params; + const querystring = buildSearchParams(queries); + + const url = `${window.top.location.origin}/${ROUTE_PREFIX_TRIMMED}${nextPage.path}${querystring}`; + + return url; +} + +function buildSearchParams(paramList) { + if (!paramList) { + return ""; + } + + const keys = Object.keys(paramList); + if (keys?.length > 0) { + const queryFormattedParams = keys.map((k) => `${k}=${paramList[k]}`); + + const querystring = queryFormattedParams.reduce((a, b) => `${a}&${b}`); + + return `?${querystring}`; + } + + return ""; +} diff --git a/src/router/a-rewrite/logic/consume-referral-info.js b/src/router/a-rewrite/logic/consume-referral-info.js new file mode 100644 index 000000000..5c1dceafc --- /dev/null +++ b/src/router/a-rewrite/logic/consume-referral-info.js @@ -0,0 +1,18 @@ +import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { queryStrings } from "@/constants/query-strings"; +import store from "@/store"; +import { storeMutations } from "@/constants/store-mutations"; +import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; + +export async function consumeReferralQuerystrings() { + const referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER); + const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT); + const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID); + if (referralNumber) { + store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); + store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount); + store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId); + // log(" --update cookie"); + updateOrCreateFunnelCookie(); + } +} diff --git a/src/router/a-rewrite/logic/create-route.js b/src/router/a-rewrite/logic/create-route.js new file mode 100644 index 000000000..44e27b568 --- /dev/null +++ b/src/router/a-rewrite/logic/create-route.js @@ -0,0 +1,39 @@ +import { lazyLoadComponent } from "@/router/a-rewrite/logic/lazy-load-component"; +import { routeData } from "@/router/a-rewrite/constants/routes"; + +export function createRoute(routeDatum, beforeEnter = async (to, from) => undefined) { + return { + name: routeDatum.name, + path: routeDatum.path, + component: lazyLoadComponent(routeDatum.name), + beforeEnter: async (to, from) => { + try { + return await beforeEnter(to, from); + } catch { + return { + name: routeData.BAILOUT.name, + replace: true, + }; + } + }, + }; +} + +export function createVirtualRoute(routeDatum, beforeEnter = async (to, from) => undefined) { + return { + name: routeDatum.name, + path: routeDatum.path, + beforeEnter: async (to, from) => { + try { + return await beforeEnter(to, from); + } catch (error) { + console.log(error); + return { + name: routeData.BAILOUT.name, + replace: true, + }; + } + }, + virtual: true, + }; +} diff --git a/src/router/a-rewrite/logic/get-destination.js b/src/router/a-rewrite/logic/get-destination.js new file mode 100644 index 000000000..cd04ddaad --- /dev/null +++ b/src/router/a-rewrite/logic/get-destination.js @@ -0,0 +1,18 @@ +import { routingTable } from "@/router/a-rewrite/constants/routing-table"; + +export function getDestination(sourcePageName, navigationScenario) { + const table = routingTable(); + + const pageEntry = table.find((entry) => entry.pageName === sourcePageName); + + const map = pageEntry?.maps?.find((map) => map.scenario === navigationScenario); + + if (!map || map?.filter === false) { + return null; + } + + return { + ...map.destinationPageData, + params: map.params, + }; +} diff --git a/src/router/a-rewrite/logic/initialize-from-querystrings.js b/src/router/a-rewrite/logic/initialize-from-querystrings.js new file mode 100644 index 000000000..2e12f88eb --- /dev/null +++ b/src/router/a-rewrite/logic/initialize-from-querystrings.js @@ -0,0 +1,121 @@ +import { + setupAdvertiserTracking, + getAffiliateCookies, +} from "@/helpers/heritage-integration/cookie-helper"; +import store from "@/store"; +import { storeMutations } from "@/constants/store-mutations"; +import { storeActions } from "@/constants/store-actions"; +import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { queryStrings } from "@/constants/query-strings"; +import { externalParameterStatus } from "@/constants/external-parameters"; + +export async function initializeFromQueryStrings() { + // Get advertiser info + setupAdvertisersAndAffiliates(); + + // Read all external paramter queries and write to store. + updateExternalParameterState(); +} + +function setupAdvertisersAndAffiliates() { + const affiliateCookies = getAffiliateCookies(); + if (affiliateCookies != null && affiliateCookies.length > 0) { + store.commit(storeMutations.UPDATE_AFFILIATE_COOKIES, affiliateCookies); + } + setupAdvertiserTracking(); +} + +function updateExternalParameterState() { + const externalParameterYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR); + const externalParameterMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE); + const externalParameterModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL); + const externalParameterStyle = getQuerystringParameter(queryStrings.VEHICLE_STYLE); + const externalParameterDamage = getQuerystringParameter(queryStrings.VEHICLE_DAMAGE); + const externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP); + const externalParameterEmail = getQuerystringParameter(queryStrings.EMAIL); + const externalParameterIsInsurance = getQuerystringParameter(queryStrings.IS_INSURANCE); + const externalParameterVinSelection = getQuerystringParameter(queryStrings.VIN_SELECTION); + const externalParameterServicePackage = getQuerystringParameter(queryStrings.SERVICE_PACKAGE); + const externalParameterNumberOfChips = getQuerystringParameter(queryStrings.NUMBER_OF_CHIPS); + const externalParameterSource = getQuerystringParameter(queryStrings.EXPERIMENTS); + const externalParameterPhoneNumber = getQuerystringParameter(queryStrings.PHONE_NUMBER); + if ( + externalParameterYear && + externalParameterMake && + 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); + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MODEL, externalParameterModel); + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_STYLE, externalParameterStyle); + } + if ( + externalParameterDamage && + (externalParameterDamage.toUpperCase() != "WINDSHIELDREPAIR" || + externalParameterNumberOfChips?.length > 0) + ) { + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_DAMAGE_TYPE, externalParameterDamage); + if (externalParameterDamage.toUpperCase() == "WINDSHIELDREPLACE") { + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_IS_REPAIR, false); + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_NUMBER_OF_CHIPS, null); + } else if (externalParameterDamage.toUpperCase() == "WINDSHIELDREPAIR") { + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_IS_REPAIR, true); + store.commit( + storeMutations.UPDATE_EXTERNAL_PARAMETER_NUMBER_OF_CHIPS, + externalParameterNumberOfChips + ); + } + } + if (externalParameterZipCode) { + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_ZIP_CODE, externalParameterZipCode); + } + if (externalParameterEmail) { + store.commit( + storeMutations.UPDATE_EXTERNAL_PARAMETER_EMAIL_ADDRESS, + externalParameterEmail + ); + } + if (externalParameterIsInsurance?.toUpperCase() == "TRUE") { + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_IS_INSURANCE, true); + store.commit( + storeMutations.UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE, + externalParameterServicePackage?.toLowerCase() + ); + } + if (externalParameterDamage?.toUpperCase() != "WINDSHIELDREPAIR") { + store.commit( + storeMutations.UPDATE_EXTERNAL_PARAMETER_VIN_SELECTION, + externalParameterVinSelection + ); + } + if (externalParameterSource) { + store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_SOURCE, externalParameterSource); + } + if (externalParameterPhoneNumber) { + store.commit( + storeMutations.UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER, + externalParameterPhoneNumber + ); + } +} + +// 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); + } +} diff --git a/src/router/a-rewrite/logic/lazy-load-component.js b/src/router/a-rewrite/logic/lazy-load-component.js new file mode 100644 index 000000000..cb91c06c0 --- /dev/null +++ b/src/router/a-rewrite/logic/lazy-load-component.js @@ -0,0 +1,3 @@ +export function lazyLoadComponent(componentName) { + return () => import(`@/layouts/${componentName}/${componentName}.vue`); +} diff --git a/src/router/a-rewrite/logic/querystring-stash.js b/src/router/a-rewrite/logic/querystring-stash.js new file mode 100644 index 000000000..96b206572 --- /dev/null +++ b/src/router/a-rewrite/logic/querystring-stash.js @@ -0,0 +1,58 @@ +import { reactive } from "vue"; + +const queryStash = reactive({ + queries: [], +}); + +function stash(key, value) { + const existingValue = getStashedQuery(key); + + if (existingValue) { + existingValue.value = value; + existingValue.used = false; + return; + } + + queryStash.queries.push({ + key: key, + value: value, + used: false, + }); +} + +export function stashAllQueries(toRoute) { + const keys = Object.keys(toRoute.query); + + keys.forEach((k) => stash(k, toRoute.query[k])); +} + +function getStashedQuery(key) { + const match = queryStash.queries.find( + (entry) => entry?.key?.toLowerCase() === key?.toLowerCase() + ); + + return match; +} + +export function peekQueryFromStash(key) { + return getStashedQuery(key)?.value; +} + +export function consumeQueryFromStash(key) { + const match = getStashedQuery(key); + + if (match) { + if (!match.used) { + match.used = true; + return match.value; + } + + return null; + } + + return null; +} + +export function clearStash() { + queryStash.queries = []; +} diff --git a/src/router/a-rewrite/logic/requires-verified-redirect.js b/src/router/a-rewrite/logic/requires-verified-redirect.js new file mode 100644 index 000000000..19f088687 --- /dev/null +++ b/src/router/a-rewrite/logic/requires-verified-redirect.js @@ -0,0 +1,8 @@ +import store from "@/store"; + +export function isVerifiedInsurance() { + return ( + store.getters.payment?.insuranceCoverage?.isVerified || + store.getters.order?.referralNumber?.length === 6 + ); +} diff --git a/src/router/a-rewrite/logic/run-experiments.js b/src/router/a-rewrite/logic/run-experiments.js new file mode 100644 index 000000000..97472ad0c --- /dev/null +++ b/src/router/a-rewrite/logic/run-experiments.js @@ -0,0 +1,30 @@ +import store from "@/store"; +import baseMixin from "@/mixins/base-mixin"; +import { storeActions } from "@/constants/store-actions"; +import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper"; +import { experimentTriggers } from "@/constants/experiments"; +import { applicationConfig } from "@/constants/application-config"; + +export async function runExperiments(nextPageName) { + if (!store.getters.applicationUser.triggeredSiteEntry) { + await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, + { + deviceId: getDeviceIdValue(), + triggerEvent: experimentTriggers.SITE_ENTRY, + triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE, + }, + nextPageName + ); + } + + await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, + { + deviceId: getDeviceIdValue(), + triggerEvent: experimentTriggers.PAGE_ENTRY, + triggerValue: nextPageName, + }, + nextPageName + ); +} diff --git a/src/router/a-rewrite/logic/save-page-data.js b/src/router/a-rewrite/logic/save-page-data.js new file mode 100644 index 000000000..1299f8900 --- /dev/null +++ b/src/router/a-rewrite/logic/save-page-data.js @@ -0,0 +1,11 @@ +import store from "@/store"; +import { storeMutations } from "@/constants/store-mutations"; + +export async function savePageData(page, data) { + const existingPageData = store.getters.pageData(page); + + const dataToSave = Object.keys(data).length > 0 ? data : (existingPageData ?? {}); + + // Save data if non-null + store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: dataToSave }); +} diff --git a/src/store/index.js b/src/store/index.js index 753d8509b..468a9e9eb 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -199,6 +199,7 @@ const getDefaultState = () => { triggeredSiteEntry: false, affiliateCookies: [], loggingOption: false, + hasAlreadyTriggeredError: false, }, }; }; @@ -824,6 +825,9 @@ export const mutations = { updateAffiliateCookies(state, affiliateCookies) { state.applicationUser.affiliateCookies = affiliateCookies; }, + updateHasTriggeredError(state, hasTriggeredError) { + state.applicationUser.hasAlreadyTriggeredError = hasTriggeredError; + }, }; // Export Getters @@ -3428,6 +3432,10 @@ export const actions = { } } }, + + updateHasTriggeredError(context, hasAlreadyTriggeredError) { + context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, hasAlreadyTriggeredError); + }, }; export default createStore({