// 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"; // Heritage integration import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; import { updateOrCreateFunnelCookie, getFunnelCookie, updateSessionIdCookie, } 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 review from "@/layouts/review/review"; import paymentMethod from "@/layouts/payment-method/payment-method"; const routes = [ { path: "/payment-method", // This is a temporary route for testing. name: "payment-method", component: paymentMethod, }, { path: "/", name: "root", async beforeEnter(to, from, next) { // If we have no query string, or we don't have the FmgPage query string. try { if (analyticsMixin.methods.noSession()) { await analyticsMixin.methods.initSession(); } else { updateSessionIdCookie(); } 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); await GoToFunnelStartOn404(next); } // On entering the funnel "fresh", read cookie information, decide what to do next. 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); // Remove the parameter after quote release const loadSessionResponse = await loadSessionIfPresent( to.query.isInsurance != null ? to.query.isInsurance == "true" ? true : false : null ); const pageToRedirectTo = await getPageToRouteExistingOrderTo(to); // Assign our fmgPage so it will load normally like the other pages. to.query.fmgPage = pageToRedirectTo; } 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)) { await GoToFunnelStartOn404(next); } return next({ name: to.query.fmgPage, query: to.query, params: to.params }); } // Get route info for the given url. Names will have a 1:1 relationship with names in the Cms. const routeData = await 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)) { await 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) { console.log(error); // If we don't have a route, go to our 404 page. await 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; 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({}); } } // Push page view to GA analyticsMixin.methods.pushPageViewToGA(); // Push experiments to Data Layer analyticsMixin.methods.pushExperimentsToDataLayer(); }); 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); }; //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); 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) { queryStringsObject[queryStrings.PROMO] = promo; } 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 reach out to the Cms and there is a 1:1 relationship between page names and route names. async function GetRouteInfoFromPageName(pageName) { const response = await store.dispatch(storeActions.GET_ROUTE_INFO_ACTION, { pageName: pageName, }); const jsonFromResponse = JSON.parse(response.data.Result); let routeData = []; // Add our route data and return our array. Object.keys(jsonFromResponse).forEach((key) => { routeData.push({ path: "/", name: `${key}`, component: lazyLoadComponent(jsonFromResponse[key].LayoutName), }); }); return routeData; } // Go to our start page on a 404. async function GoToFunnelStartOn404(next) { const apiResponse = await store.dispatch(storeActions.GET_HOMEPAGE_NAME); const homepageName = apiResponse.data.Result; // 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: homepageName }, }); } // 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.dispatchStoreAction(storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, { userId: getDeviceIdValue(), triggerEvent: experimentTriggers.SITE_ENTRY, triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE, }); } await baseMixin.methods.dispatchStoreAction(storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, { userId: getDeviceIdValue(), triggerEvent: experimentTriggers.PAGE_ENTRY, triggerValue: nextPage, }); } export default router;