import { createWebHistory, createRouter } from 'vue-router'; import lazyLoadComponent from '@/router/dynamic-routing/component-loader'; import issPageValues from '@/router/router-constants/issPage-values'; import routerTitles from '@/router/router-constants/router-titles'; import routingTable from '@/router/router-constants/routing-table'; import { useMainStore } from '@/store'; import eventBus from '@/helpers/event-bus/event-bus'; import { globalEvents, globalEventTypes } from '@/constants/events'; import baseMixin from '@/mixins/base-mixin'; import { isSavedSessionStillActive } from '@/helpers/session-helper'; import { getDeviceIdValue, getISSCookie, updateOrCreateISSCookie, updateSessionIdCookie } from '@/helpers/cookie-helper'; import { experimentTriggers } from '@/constants/experiments'; import applicationConfig from '@/constants/application-config'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; import analyticsMixin from '@/mixins/analytics-mixin'; import { saveSession } from '@/helpers/order-helper.js'; import routerParams from '@/router/router-constants/router-params'; import canBailoutNavigateBack from '@/helpers/bailout-helper'; import submitType from '@/constants/submit-type'; import navigationScenarios from './router-constants/navigation-scenarios'; const routes = [ { path: '/', name: 'root', async beforeEnter(to, from, next) { const issPageToUse = !to.query.issPage ? issPageValues.WELCOME_PAGE : to.query.issPage; const fromQueryPage = from.query?.issPage; if ((issPageToUse === issPageValues.ACCESS_DENIED || (issPageToUse !== issPageValues.ENTRY_PAGE && !useMainStore().issConfig.parentAccountNumber)) && process.env.VUE_APP_CURRENT_ENVIRONMENT !== 'Localhost' ) { return await GoToAccessIsDenied(next); } // Do not run these for the main entry page - as it is not part of the user flow. if (issPageToUse !== issPageValues.ENTRY_PAGE) { if (analyticsMixin.methods.noSession()) { await analyticsMixin.methods.initSession(); } else { updateSessionIdCookie(); } await runExperiments(issPageToUse); // fmg has this further down } // Intercept all navigation if a submitted order exists in storage if (useMainStore().hasSubmittedOrder()) { if (to.query.issPage !== issPageValues.ENTRY_PAGE) { return await GoToConfirmationPage(next, useMainStore().getSubmittedOrder()); } } // If the saved session has timed out, clear the session, execute 404 logic. if (getISSCookie() !== null && !isSavedSessionStillActive()) { // await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE); await GoToStartOn404(next); } // Process ISS cookie. // Skip if Entry Page or Refreshing Welcome page if (issPageToUse !== issPageValues.ENTRY_PAGE && !(issPageToUse === issPageValues.WELCOME_PAGE && fromQueryPage === undefined)) { updateOrCreateISSCookie(); } if (router.hasRoute(issPageToUse)) { // Since our route is already in scope, we can grab the component and call the arePagePrerequisitesValid function. let component = router.getRoutes().filter((x) => x.name === issPageToUse)[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 GoToStartOn404(next); } return next({name: issPageToUse, query: to.query, params: to.params}); } const routeData = await GetRouteInfoFromPageName(issPageToUse); if (routeData[0].name.toLowerCase() === 'error') { throw new Error('Page not found!'); } // 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)) { const tempMsgCopy = 'Pre Requisites failed, need to handle. Default is Welcome Page.'; const tempMsgHeadline = `${issPageToUse}: pre-req failed...`; await GoToStartOn404(next, tempMsgCopy, tempMsgHeadline); } // Assign current query string parameters, as well as our issPage one. next({ name: routeData[0].name, query: Object.assign(to.query, {issPage: routeData[0].name}), params: to.params }); return null; } } ]; const router = createRouter({ history: createWebHistory('/'), routes, scrollBehavior() { // always scroll to top return { top: 0 }; } }); router.beforeEach(async (to, from) => { const fromQueryPage = from.query?.issPage; if (fromQueryPage === undefined) { showIssLoadingModal(true); } const toQueryPage = to.query?.issPage; const notToPayInAdvanceReturn = toQueryPage !== issPageValues.PAYMENT_RETURN; const isInIframe = window !== window.top || fromQueryPage === issPageValues.PAYMENT_PAGE; // isFromPaymentPageToOrderConfirmation workaround for navigating from an iframe but // isInIframe evaluates to false for some reason when navigating from payment to confirmation const isFromPaymentPageToOrderConfirmation = fromQueryPage === issPageValues.PAYMENT_PAGE && toQueryPage === issPageValues.ORDER_CONFIRMATION; if ((isInIframe && notToPayInAdvanceReturn) || isFromPaymentPageToOrderConfirmation) { // need to set window.top.location.href directly when navigating out of an iframe // especially when navigating with browser buttons const newUrl = `${window.top.location.origin}${to.href}`; window.top.location.href = newUrl; return false; } const store = useMainStore(); // Prevent navigating backwards if we enter a bailout that we are not allowed to go back on if (store.isBailout && from.name === issPageValues.BAILOUT_PAGE && to.name !== 'root' && to.name !== issPageValues.CONTACT_CONFIRMATION && !canBailoutNavigateBack()) { return false; } return true; }); router.afterEach(async (to, from) => { const store = useMainStore(); // Update lastPageVisited in the store store.updateLastPageVisited(to.name); if (from.redirectedFrom === undefined) { store.clearSaveSessionPromise(); } const skipSaveSession = !!router.options.history.state[routerParams.SKIP_SAVE_SESSION]; if (from.name !== undefined && !skipSaveSession && !store.hasSubmittedOrder()) { await saveSession({ bailoutOnError: from.name === issPageValues.ENTRY_PAGE}); } if (to.query.issPage !== issPageValues.ENTRY_PAGE) { document.title = routerTitles[to.query.issPage] || 'Safelite Solutions®'; // digital consumer logging analyticsMixin.methods.logDigitalConsumer(); // digital consumer ISS session logging to snowflake analyticsMixin.methods.pushIssSessionData(); // Push page view to GA analyticsMixin.methods.pushPageViewToGA(); // Push experiments to Data Layer analyticsMixin.methods.pushExperimentsToDataLayer(); // Push values to GA analyticsMixin.methods.pushValueToGA(); // Push current order status to Data Layer analyticsMixin.methods.pushOrderToDataLayer(); } }); router.navigateWithoutSaving = ( scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {} ) => { navigate(scenario, currentRoute, optionalQuery, optionalParams, optionalPageData); }; router.navigateToExternalUrl = (url, optionalQuery = {}) => { navigateToUrl(url, optionalQuery); }; // 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 useMainStore().getRouteInfo(pageName); const jsonFromResponse = JSON.parse(response.data.Result); const 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; } // 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.navigate = ( scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {} ) => { navigate(scenario, currentRoute, optionalQuery, optionalParams, optionalPageData); }; router.navigateWithSpinner = ( scenario, currentRoute ) => { navigate(scenario, currentRoute, undefined, undefined, undefined, true); }; // Navigate to the next route, depending on the scenario. function navigate( scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}, forceSpinner = false ) { if (!scenario) { window.console.error('No scenario provided. Please review the routing table.'); return; } // Match our maps up and navigate if we have a destination. let matchingScenarioMap = getNavigationMap(scenario, currentRoute); if (!matchingScenarioMap && scenario === navigationScenarios.BAILOUT) { matchingScenarioMap = { destinationIssPageValue: issPageValues.BAILOUT_PAGE } } if (!matchingScenarioMap) { window.console.error('No matching scenario found. Please review the routing table.'); return; } if (forceSpinner) { showIssLoadingModal(true); } if (scenario === navigationScenarios.CLICKED_BACK_PREVIOUS) { // Update page to the prevous page in the router. // If we need to worry about typing this in from outside of the app, // we'll need to pre check history.length or if there is a previous page stored in state. router.go(-1); } else if (matchingScenarioMap.destinationIssPageValue) { // Update page data to the store for next page if provided. Otherwise, keep existing page data or set to empty object const existingPageDataForPage = useMainStore().pageData(matchingScenarioMap.destinationIssPageValue); baseMixin.methods.savePageDataToStore( matchingScenarioMap.destinationIssPageValue, Object.keys(optionalPageData).length > 0 ? optionalPageData : existingPageDataForPage ?? {} ); // We're always pushing the same path, just changing query strings. // Make sure our optional query strings get combined with our issPage one. router.push({ name: 'root', query: Object.assign(optionalQuery, { issPage: matchingScenarioMap.destinationIssPageValue }), state: optionalParams }); } else if (matchingScenarioMap.destinationUrl) { navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery); } } // Navigate to an external url. function navigateToUrl(url, optionalQuery = {}) { // possibly show some loading screen in the future here. const externalUrl = new URL(url); for (const queryKey in optionalQuery) { externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); } window.location.assign(externalUrl); } router.navigateBailout = (bailoutData = null) => { if (bailoutData != null && !useMainStore().isBailout) { useMainStore().setBailout(bailoutData) } router.navigate( navigationScenarios.BAILOUT, router.currentRoute.value, {}, { [routerParams.SKIP_SAVE_SESSION]: true } ); } // Get navigation map depending on the scenario and the current 'page' you're on. function getNavigationMap(scenario, currentRoute) { const issPageValue = currentRoute.query.issPage; const matchedQueryValue = routingTable(useMainStore()) .filter((item) => item.issPageValue === issPageValue && item.maps.filter((map) => map.scenario === scenario).length > 0); const maps = matchedQueryValue ? matchedQueryValue.map((m) => m.maps.filter((map) => map.scenario === scenario))[0] : undefined; return maps ? maps.filter((x) => x.filter === true || x.filter === undefined)[0] : undefined; } async function GoToAccessIsDenied(next) { const errorPageName = issPageValues.ACCESS_DENIED; router.addRoute({ path: '/', name: errorPageName, component: lazyLoadComponent(errorPageName) }); next({ name: errorPageName, query: { issPage: errorPageName } }); } async function GoToConfirmationPage(next, order) { const nextPageName = getConfirmationPageFromOrder(order); router.addRoute({ path: '/', name: nextPageName, component: lazyLoadComponent(nextPageName) }); next({ name: nextPageName, query: { issPage: nextPageName } }); } function getConfirmationPageFromOrder(order) { switch (order.submitType) { case submitType.SAFELITE: return issPageValues.ORDER_CONFIRMATION; case submitType.TPA: return issPageValues.TPA_CONFIRMATION; case submitType.BAILOUT: return issPageValues.CONTACT_CONFIRMATION; default: return issPageValues.ORDER_CONFIRMATION; } } async function GoToStartOn404(next, msgCopy = null, msgHeadline = null) { const errorPageName = issPageValues.WELCOME_PAGE; router.addRoute({ path: '/', name: errorPageName, component: lazyLoadComponent(errorPageName) }); // Put item on the bus eventBus.addEventToBus(globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND, { isDismissible: true, messageCopy: msgCopy ?? 'You can get a quote by starting on this page.', messageHeadline: msgHeadline ?? "We're sorry, something went wrong.", type: globalEventTypes.Danger }); next({ name: errorPageName, query: { issPage: errorPageName } }); } // Checks arePagePrerequisitesValid on the component passed in. function arePagePrerequisitesValid(component) { return component.default.methods.arePagePrerequisitesValid === undefined || component.default.methods.arePagePrerequisitesValid(); } // Run SiteEntry and PageEntry triggers for experiments async function runExperiments(nextPage) { const store = useMainStore(); if (!store.applicationUser.triggeredSiteEntry) { await store.runExperimentsForTrigger({ deviceId: getDeviceIdValue(), triggerEvent: experimentTriggers.SITE_ENTRY, triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE }); } await store.runExperimentsForTrigger({ deviceId: getDeviceIdValue(), triggerEvent: experimentTriggers.PAGE_ENTRY, triggerValue: nextPage }); } export default router;