// Supporting files import { createWebHistory, createRouter } from "vue-router"; import { storeActions } from "@/constants/store-actions"; import { storeMutations } from "../constants/store-mutations"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { routingTable } from "@/router/router-constants/routing-table.js"; import { globalEvents, globalEventTypes } from "@/constants/events"; import { queryStrings } from "@/constants/query-strings"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper"; import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { fmgPageValues, funnelStartPageName } from "@/router/router-constants/fmgPage-values"; // Heritage integration import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; import { updateOrCreateFunnelCookie, getFunnelCookie, updateSessionIdCookie, deleteFunnelCookie, } from "@/helpers/heritage-integration/cookie-helper"; import { loadSessionIfPresent, saveSession } from "@/helpers/heritage-integration/order-helper"; import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel, } from "@/helpers/heritage-integration/navigation-helper"; import baseMixin from "@/mixins/base-mixin"; import eventBus from "@/helpers/event-bus/event-bus"; import store from "@/store"; import analyticsMixin from "@/mixins/analytics-mixin"; import { experimentTriggers } from "../constants/experiments"; import { applicationConfig } from "../constants/application-config"; import { shouldStripPromoQueryString } from "@/helpers/promotions-helper"; import bailout from "@/layouts/bailout/bailout"; import { nextTick } from "vue"; const routes = [ { path: "/bailout", name: "bailout", component: bailout, }, { path: "/", name: "root", async beforeEnter(to, from, next) { // If we have no query string, or we don't have the FmgPage query string. try { await analyticsMixin.methods.validateSession(); if (getFunnelCookie()?.SuppressConceptFunnel) { await navigateToHeritageFunnel({ shouldSaveSession: false }); return next(false); } // If the saved session has timed out, clear the session, execute 404 logic. if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE); deleteFunnelCookie(); GoToFunnelStartOn404(next); } // Intercept all navigation if a submitted order exists in storage if (store.getters.hasSubmittedOrder) { if (to.query.fmgPage !== funnelStartPageName) { to.query.fmgPage = fmgPageValues.CONFIRMATION; } } // On entering the funnel "fresh", read cookie information, decide what to do next. else if (from.redirectedFrom === undefined) { // clear the saveSessionPromise - if it exists in the vuex store but a new instance was created // the saveSessionPromise will no longer point to a valid promise baseMixin.methods.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); if (!to.query.fmgPage?.startsWith("payment")) { //payment pages used to return from safelitehop so exclude here // Remove the parameter after quote release // for testing end to end locally, see if there is a referralNumber on the querystring // that could have changed in heritage and then stick it in the store and then update the cookie // so load-session runs properly. this can be removed when heritage goes away const referralNumber = getQuerystringParameter( queryStrings.REFERRAL_NUMBER ); const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT); const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID); const leadGenYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR); const leadGenMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE); const leadGenModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL); const leadGenStyle = getQuerystringParameter(queryStrings.VEHICLE_STYLE); const leadGenDamage = getQuerystringParameter(queryStrings.VEHICLE_DAMAGE); const leadGenZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP); const leadGenEmail = getQuerystringParameter(queryStrings.EMAIL); const leadGenIsInsurance = getQuerystringParameter( queryStrings.IS_INSURANCE ); const leadGenVinSelection = getQuerystringParameter( queryStrings.VIN_SELECTION ); const leadGenServicePackage = getQuerystringParameter( queryStrings.SERVICE_PACKAGE ); const leadGenNumberOfChips = getQuerystringParameter( queryStrings.NUMBER_OF_CHIPS ); 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 ); updateOrCreateFunnelCookie(); } if ( leadGenYear && leadGenMake && leadGenModel && leadGenStyle && leadGenDamage && leadGenZipCode && leadGenEmail && leadGenIsInsurance && (leadGenDamage.toUpperCase() == "WINDSHIELDREPAIR" || leadGenVinSelection?.length > 0) && (leadGenIsInsurance == "false" || leadGenServicePackage?.length > 0) && (leadGenDamage.toUpperCase() != "WINDSHIELDREPAIR" || leadGenNumberOfChips?.length > 0) ) { if (!store.getters.applicationUser.triggeredSiteEntry) { store.commit(storeMutations.UPDATE_IS_LEAD_GEN, true); store.commit(storeMutations.UPDATE_LEAD_GEN_YEAR, leadGenYear); store.commit(storeMutations.UPDATE_LEAD_GEN_MAKE, leadGenMake); store.commit(storeMutations.UPDATE_LEAD_GEN_MODEL, leadGenModel); store.commit(storeMutations.UPDATE_LEAD_GEN_STYLE, leadGenStyle); store.commit( storeMutations.UPDATE_LEAD_GEN_DAMAGE_TYPE, leadGenDamage ); if (leadGenDamage.toUpperCase() == "WINDSHIELDREPLACE") { store.commit(storeMutations.UPDATE_LEAD_GEN_IS_REPAIR, false); store.commit( storeMutations.UPDATE_LEAD_GEN_NUMBER_OF_CHIPS, null ); } else if (leadGenDamage.toUpperCase() == "WINDSHIELDREPAIR") { store.commit(storeMutations.UPDATE_LEAD_GEN_IS_REPAIR, true); store.commit( storeMutations.UPDATE_LEAD_GEN_NUMBER_OF_CHIPS, leadGenNumberOfChips ); } store.commit( storeMutations.UPDATE_LEAD_GEN_ZIP_CODE, leadGenZipCode ); store.commit( storeMutations.UPDATE_LEAD_GEN_EMAIL_ADDRESS, leadGenEmail ); if (leadGenIsInsurance == "true") { store.commit(storeMutations.UPDATE_LEAD_GEN_IS_INSURANCE, true); store.commit( storeMutations.UPDATE_LEAD_GEN_SERVICE_PACKAGE, leadGenServicePackage ); } if (leadGenDamage.toUpperCase() != "WINDSHIELDREPAIR") { store.commit( storeMutations.UPDATE_LEAD_GEN_VIN_SELECTION, leadGenVinSelection ); } } } const loadSessionResponse = await loadSessionIfPresent( to.query.isInsurance != null ? to.query.isInsurance == "true" ? true : false : null, to.query.fmgPage ); const pageToRedirectTo = await getPageToRouteExistingOrderTo(to); // This logic may determine that the user should be sent to heritage -- if so, do that here. if (pageToRedirectTo === fmgPageValues.HERITAGE) { await navigateToHeritageFunnel({ shouldSaveSession: false }); return next(false); } // Assign our fmgPage so it will load normally like the other pages. to.query.fmgPage = pageToRedirectTo; } } // Reroute around pages that should not be available. // Exception 1: Integrated Users should not see vehicle or quote pages. if (store.getters.requiresVerifiedRedirecting) { if (to.query.fmgPage === fmgPageValues.VEHICLE) { to.query.fmgPage = fmgPageValues.VEHICLE_DAMAGE; } else if ( to.query.fmgPage === fmgPageValues.QUOTE || to.query.fmgPage === fmgPageValues.INSURANCE_COMPANY ) { // Push to heritage if going to quote & integrated. await navigateToHeritageFunnel({ shouldSaveSession: false }); return next(false); } } await runExperiments(to.query.fmgPage); // Process funnel cookie. updateOrCreateFunnelCookie(); // If we already have our route, go to it. if (router.hasRoute(to.query.fmgPage)) { // Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function. let component = router .getRoutes() .filter((x) => x.name === to.query.fmgPage)[0].components; // If the component hasn't been loaded fully, load it before we check prerequisites. if (component.default.methods === undefined) { component = await component.default(); } if (!arePagePrerequisitesValid(component)) { console.log("Page prereq error: " + component.default.name); GoToFunnelStartOn404(next); } return next({ name: to.query.fmgPage, query: to.query, params: to.params }); } if (!isExistingFmgPageName(to.query.fmgPage)) { console.log("Not an existing fmg page name:" + to.query.fmgPage); GoToFunnelStartOn404(next); return; } // Get route info for the given url. Names will have a 1:1 relationship with names in the Cms. const routeData = GetRouteInfoFromPageName(to.query.fmgPage); // Add our dynamic route. router.addRoute({ path: routeData[0].path, // Always the same path, because we control it with query strings. name: routeData[0].name, component: routeData[0].component, }); // Call the next components arePagePrerequisitesValid method before load. // If it returns false, use the 404 logic. const nextComponent = await router .getRoutes() .filter((x) => x.name === routeData[0].name)[0] .components.default(); if (!arePagePrerequisitesValid(nextComponent)) { console.log( "Page Prereqs not valid for next component: " + nextComponent.default.name ); GoToFunnelStartOn404(next); } // Assign current query string parameters, as well as our fmgPage one. next({ name: routeData[0].name, query: Object.assign(to.query, { fmgPage: routeData[0].name }), params: to.params, }); } catch (error) { global.$logger.logError(`Routing ${error.stack}`); if (to.query?.fmgPage === funnelStartPageName) { deleteFunnelCookie(); await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE); } // If we don't have a route, go to our 404 page. GoToFunnelStartOn404(next); } }, }, ]; const router = createRouter({ history: createWebHistory("/fmg/"), routes, //Cause "page" to begin at the top when route chanages. scrollBehavior(to, from, savedPosition) { // always scroll to top return { top: 0 }; }, }); //---------------------------------------------------------- Router Functions ---------------------------------------------------------- router.beforeEach(async (to, from, next) => { // set lastNavigationPage here to capture state before API calls for analytics. // use current page url query string name when to.name is "root" (due to unresolved navigation in beforeEach) router.lastNavigationPage = to.name == "root" ? analyticsMixin.methods.getPageName() : to.name; const fromQueryPage = from.query?.fmgPage; if (fromQueryPage === undefined) { showFmgLoadingModal(true); } const toQueryPage = to.query?.fmgPage; const notToPIAReturn = toQueryPage != fmgPageValues.PAYMENT_PIA_RETURN; const isInIframe = window !== window.top; if (store.getters.isLeadGen) { // Check if alert event is on the bus const alertEvent = eventBus.readEventFromBus( globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND ); //Uknown alerts most likely were added by a failed api call in global-methods const unknownAlertEvent = eventBus.readEventFromBus( globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.UNKNOWN_ERROR ); // If alert event is on the bus, then display the alert if (alertEvent !== undefined || unknownAlertEvent !== undefined) { store.commit(storeMutations.UPDATE_IS_LEAD_GEN, false); } } // fromPaymentToConfirmation workaround for navigating from an iframe but // isInIframe evaluates to false for some reason when navigating from payment to confirmation const fromPaymentToConfirmation = fromQueryPage === fmgPageValues.PAYMENT && toQueryPage === fmgPageValues.CONFIRMATION; if ((isInIframe && notToPIAReturn) || fromPaymentToConfirmation) { const newUrl = `${window.top.location.origin}${to.href}`; window.top.location.href = newUrl; // Refresh page if navigating to self to prevent locking. // For now only carved out for vehicle; all modals are opened through anchor tags at the moment, // which also self navigate, but relied on the page remaining the same on self-navigation. } else if (toQueryPage === fromQueryPage && toQueryPage === getRedirectedStartPage()) { router.go(0); } else { next(); } }); router.afterEach(async (to, from) => { // Update lastPageVisited in the store store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); // If saving on navigation is requested, check for saved SessionId or EmailAddress to determine if saving is appropriate if (eval(to.params.isSavingNavigation)) { if ( store.getters.applicationUser.savedSessionId || store.getters.order.customer?.emailAddress ) { await saveSession({ pageNameToLog: to.query.fmgPage }); } } if (!store.getters.isLeadGen) { showFmgLoadingModal(false); } // 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(); }); router.navigateWithoutSaving = ( scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {} ) => { navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData); }; router.navigateWithSaving = ( scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {} ) => { navigate(scenario, currentRoute, true, optionalQuery, optionalParams, optionalPageData); }; router.navigateToExternalUrl = (url, optionalQuery = {}) => { navigateToUrl(url, optionalQuery); }; router.navigateError = () => { DisplayPageError(); }; //Use this navigation when you need to call next() explicitly. beforeRouteEnter is a good example. router.overrideNavigation = ( scenario, currentRoute, next, isSavingNavigation, optionalQuery = {}, optionalParams = {}, optionalPageData ) => { navigate( scenario, currentRoute, isSavingNavigation, optionalQuery, optionalParams, optionalPageData ); next(); }; router.getNextPage = () => nextPageName; // PRIVATE VARIABLES var nextPageName; // PRIVATE FUNCTIONS // Navigate to the next route, depending on the scenario. async function navigate( scenario, currentRoute, isSavingNavigation, optionalQuery = {}, optionalParams = {}, optionalPageData = {} ) { if (!scenario) { console.error("No scenario provided. Please review the routing table."); return; } // Match our maps up and navigate if we have a destination. const matchingScenarioMap = getNavigationMap(scenario, currentRoute); const destinationFmgPageValue = matchingScenarioMap.destinationFmgPageValue; if (destinationFmgPageValue !== undefined) { // We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one. nextPageName = destinationFmgPageValue; // Update page data to the store for next page if provided. Otherwise, keep existing page data or set to empty object const existingPageDataForPage = store.getters.pageData(destinationFmgPageValue); baseMixin.methods.savePageDataToStore( destinationFmgPageValue, Object.keys(optionalPageData).length > 0 ? optionalPageData : existingPageDataForPage ?? {} ); optionalParams.isSavingNavigation = isSavingNavigation; // add querystring params to the route. if they are already in the store then no need to add them var queryStringsObject = { fmgPage: destinationFmgPageValue, }; const hasZip = getQuerystringParameter(queryStrings.ZIP_CODE); const zip = getQuerystringParameter(queryStrings.ZIP_CODE); const promo = getQuerystringParameter(queryStrings.PROMO); const pageError = getQuerystringParameter(queryStrings.PAGE_ERROR); if ( hasZip && (store.getters.order.serviceLocation.zipCode === undefined || store.getters.order.serviceLocation.zipCode == null) && (store.getters.order.vehicle.registration.zipCode === undefined || store.getters.order.vehicle.registration.zipCode == null) ) { queryStringsObject[queryStrings.ZIP_CODE] = zip; } if (promo && !shouldStripPromoQueryString(currentRoute.query.fmgPage)) { queryStringsObject[queryStrings.PROMO] = promo; } if (pageError) { queryStringsObject[queryStrings.PAGE_ERROR] = pageError; } router.push({ name: "root", query: Object.assign(optionalQuery, queryStringsObject), params: optionalParams, }); } else if (matchingScenarioMap.destinationUrl !== undefined) { navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery); } } // Get navigation map depending on the scenario and the current 'page' you're on. function getNavigationMap(scenario, currentRoute) { const fmgPageValue = currentRoute.query.fmgPage; const matchedQueryValue = routingTable(store) .filter( (item) => item.fmgPageValue === fmgPageValue && item.maps.filter((map) => map.scenario === scenario).length > 0 ) .map((m) => m.maps.filter((map) => map.scenario === scenario))[0] .filter((x) => x.filter === true || x.filter === undefined); return matchedQueryValue[0]; } //---------------------------------------------------------- Private Functions ---------------------------------------------------------- // Navigate to an external url. function navigateToUrl(url, optionalQuery = {}) { // possibly show some loading screen in the future here. var externalUrl = new URL(url); for (const queryKey in optionalQuery) { externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); } window.location.assign(externalUrl); } // Get route information by page name. // This will no longer reach out to the Cms. There is a 1:1 relationship between page names and route names and layouts. function GetRouteInfoFromPageName(pageName) { // check if pagename is a valid route/component if (!isExistingFmgPageName(pageName)) { return []; } const routeData = [ { path: "/", name: pageName, component: lazyLoadComponent(pageName), }, ]; return routeData; } // Go to our start page on a 404. function GoToFunnelStartOn404(next) { // Put item on the bus eventBus.addEventToBus( globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND, { isDismissible: true, messageCopy: "You can get a quote by starting on this page.", messageHeadline: "We're sorry, something went wrong.", type: globalEventTypes.Danger, } ); next({ path: "/", query: { fmgPage: funnelStartPageName }, }); } async function DisplayPageError() { // Put item on the bus eventBus.addEventToBus( globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.UNKNOWN_ERROR, { isDismissible: true, messageCopy: "", messageHeadline: "We're sorry, something went wrong.", type: globalEventTypes.Danger, } ); store.commit(storeMutations.UPDATE_IS_LEAD_GEN, false); baseMixin.methods.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); const pageError = getQuerystringParameter(queryStrings.PAGE_ERROR); // we use the pageError querystring as a counter to how many times a user has experienced an error and been routed here. // once they receive more than 1 pageerrors, we'll reset their state to hopefully correct any issues they may be having. var navPage = fmgPageValues.VEHICLE; if (store.getters.requiresVerifiedRedirecting) { navPage = fmgPageValues.VEHICLE_DAMAGE; } if (pageError) { var errorCount = Number(pageError); if (errorCount > 1) { deleteFunnelCookie(); await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE); router.push({ path: "/", query: { fmgPage: navPage }, }); } else { errorCount++; router.push({ path: "/", query: { fmgPage: navPage, pageError: errorCount }, }); } } else { router.push({ path: "/", query: { fmgPage: navPage, pageError: "1" }, }); } } function isExistingFmgPageName(pageName) { const names = Object.values(fmgPageValues); return names.some((name) => name === pageName); } // Checks arePagePrerequisitesValid on the component passed in. function arePagePrerequisitesValid(component) { return component.default.methods.arePagePrerequisitesValid(); } // Run SiteEntry and PageEntry triggers for experiments async function runExperiments(nextPage) { if (!store.getters.applicationUser.triggeredSiteEntry) { await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, { userId: getDeviceIdValue(), triggerEvent: experimentTriggers.SITE_ENTRY, triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE, }, nextPage ); } await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, { userId: getDeviceIdValue(), triggerEvent: experimentTriggers.PAGE_ENTRY, triggerValue: nextPage, }, nextPage ); } function getRedirectedStartPage() { if (store.getters.requiresVerifiedRedirecting) { return fmgPageValues.VEHICLE_DAMAGE; } else { return funnelStartPageName; } } export default router;