Merge pull request #2225 from Safelite/rlsmerge/2025.01.23-to-develop

Rlsmerge/2025.01.23 to develop
This commit is contained in:
CarlNation 2025-01-20 08:00:32 -05:00 committed by GitHub
commit 32561fdd19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 406 additions and 82 deletions

View file

@ -100,6 +100,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
@ -160,6 +161,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
@ -220,6 +222,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
@ -280,6 +283,7 @@ stages:
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)

View file

@ -27,6 +27,7 @@ module.exports = {
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
"!src/layouts/return-user/*.vue", // Temp test exclusion while in development
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

View file

@ -37,6 +37,8 @@ const applicationConfig = {
FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging",
AFFILIATE_COOKIE_CONTAINING_NAME: "_Track",
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
SESSION_TIMEOUT_MINUTES: process.env.VUE_APP_SESSION_TIMEOUT_MINUTES,
RETURN_USER_PAGE: "/fmg/?fmgPage=return-user",
};
export { applicationConfig };

View file

@ -16,7 +16,7 @@ const cookieNames = {
};
const cookieExpirations = {
SESSION_ID: convertToSeconds({ minutes: 30 }),
SESSION_ID: convertToSeconds({ minutes: applicationConfig.SESSION_TIMEOUT_MINUTES }),
DXDEV: convertToSeconds({ years: 1 }),
FUNNEL_USER_ID: convertToSeconds({ weeks: 1 }),
FUNNEL_SESSION_KEY: convertToSeconds({ minutes: 30 }),

View file

@ -45,6 +45,7 @@ const queryStrings = {
ORGANIC: "organic",
ORGANIC_SOCIAL: "organic_social",
EXPERIMENTS: "experiments",
FROM_HERITAGE: "fromheritage",
};
export { queryStrings };

View file

@ -217,11 +217,11 @@ export default {
flex: none;
}
.textbox-question {
padding: .25rem 0;
padding: 0.25rem 0;
}
}
}
&.modal-dialog-centered {
align-items: flex-end;
min-height: 100%;
@ -286,6 +286,10 @@ export default {
.modal-disclaimer {
font-size: 0.75rem;
order: 2;
text-align: left;
a {
text-decoration: none;
}
}
.modal-footer {
margin: 0 0 1.5rem 0;

View file

@ -128,7 +128,7 @@ export default {
@include blue-gradient;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow:

View file

@ -1,7 +1,8 @@
<template>
<div class="save-progress-modal-question">
<div class="save-progress-modal-question" :class="isProgressSaved ? 'progress-saved' : ''">
<buttonMain
ref="buttonMain"
type="button"
v-if="!isProgressSaved"
:buttonText="buttonText"
loaderColor="white"
@ -17,6 +18,7 @@
:ref="modalName"
:headerText="modalHeaderText"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalButtonText"
@footer-button-event="saveProgress">
<p class="modal-body-inner">{{ modalBodyText }}</p>
@ -35,6 +37,7 @@ import modal from "@/digital-components/modal/modal";
import { storeActions } from "@/constants/store-actions.js";
import buttonMain from "@/ux-components/button-main/button-main";
import alert from "@/ux-components/alert/alert";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
export default {
name: "save-progress-modal-question",
@ -50,6 +53,10 @@ export default {
modalWidgetName: String,
pageName: String,
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal.closeModal();
},
computed: {
buttonText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
@ -78,17 +85,24 @@ export default {
this.modal.openModal();
this.modal.resetButtonStyle();
},
onModalClosed() {
this.userInput = "";
this.modal.resetForm();
},
async saveProgress() {
// save email address to store
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.userInput, false);
// send store call to send to new API (that triggers an email send)
await saveQuote({ pageNameToLog: this.pageName });
// hide save progress button; show success message alert
this.isProgressSaved = true;
this.modal.closeModal();
// communicate to parent the new status
this.$emit("save-progress-saved");
// send store call to send to new API (that triggers an email send)
this.$emit("save-progress");
this.modal.closeModal();
},
},
@ -113,6 +127,10 @@ export default {
align-items: center;
border-radius: 0.5rem;
&.progress-saved {
background: none;
}
.save-progress-button {
order: 4;
position: relative;
@ -125,7 +143,7 @@ export default {
width: auto;
height: auto;
text-decoration: underline;
text-underline-offset: 0.375rem;
text-underline-offset: 0.25rem;
line-height: 1rem;
padding: 1rem;
margin: 0 auto;
@ -148,6 +166,7 @@ export default {
:deep(.alert) {
border-radius: $border-radius-lg;
border: 1px solid $green;
margin-top: 0;
}
:deep(.alert-heading) {
position: relative;

View file

@ -16,6 +16,7 @@
class="save-progress-popup-question" />
<buttonMain
ref="skipButton"
type="button"
:buttonText="buttonText"
loaderColor="white"
:suppressLoader="true"
@ -37,6 +38,7 @@ import modal from "@/digital-components/modal/modal";
import { storeActions } from "@/constants/store-actions.js";
import buttonMain from "@/ux-components/button-main/button-main";
import alert from "@/ux-components/alert/alert";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
export default {
name: "save-progress-popup-question",
@ -50,6 +52,10 @@ export default {
mounted() {
if (this.showSaveProgressPopup) this.modal.openModal();
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal.closeModal();
},
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
props: {
modelValue: Object,
@ -94,15 +100,14 @@ export default {
// save email address to store
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.userInput, false);
// send store call to send to new API (that triggers an email send)
await saveQuote({ pageNameToLog: this.pageName });
// hide save progress button; show success message alert
this.isProgressSaved = true;
// send store call to send to new API (that triggers an email send)
this.$emit("save-progress");
// send call to close popup
this.closeModal();
this.$emit("close-save-progress-popup");
// communicate to parent the new status
this.$emit("save-progress-saved");
},
},
@ -138,11 +143,12 @@ export default {
}
}
.skip-button {
.btn.btn-secondary.skip-button {
order: 4;
position: relative;
color: $black;
border: none;
border-radius: .25rem;
font-weight: 900;
font-size: 0.875rem;
text-align: center;
@ -150,7 +156,7 @@ export default {
width: auto;
height: auto;
text-decoration: underline;
text-underline-offset: 0.375rem;
text-underline-offset: 0.25rem;
line-height: 1rem;
padding: 0 0 4px 0;
margin: 0 auto 1.5rem auto;
@ -159,12 +165,17 @@ export default {
&:hover,
&:active,
&:focus,
&:focus-visible {
&:focus {
color: $blue-700;
background: none;
box-shadow: none;
}
&:focus-visible {
outline: none;
box-shadow:
0 0 0 3px $white,
0 0 0 5.5px $blue-300;
}
&.delay {
// fixes flicker while transitioning between states
@ -200,9 +211,6 @@ export default {
}
.modal-disclaimer {
order: 4;
a {
text-decoration: none;
}
}
&.progress-saved {

View file

@ -110,7 +110,13 @@ export default {
if (error.response.status && error.response.status != "404") {
// Do not route to error logic when no wipers found or no promo found (404s)
router.navigateError();
const errorPayload = {
cause: `Response error ${error.response.status}`,
currentPage: pageNameToLog,
endpoint: endpoint,
};
router.navigateError(errorPayload);
// do not log 404 errors from services because we return NotFound
// when a service doesn't return an object

View file

@ -16,8 +16,8 @@
<!-- save progress goes into <slot> on questions-page-layout -->
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
pageName="capability-questions"
v-if="showSaveProgressModal" />
</questions-page-layout>
</Form>
</template>
@ -38,7 +38,6 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -65,7 +64,7 @@ export default {
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
vm.showSaveProgressModal = !(emailFromStore?.length > 0);
if (store.getters.externalParameterState?.isExternalParameter) {
if (store.getters.externalParameterServiceZip.zipCode) {
await baseMixin.methods.dispatchStoreAction(
@ -87,7 +86,7 @@ export default {
questionsData: null,
selectedAnswers: {},
currentGlassIndex: 0,
isEmailInStoreOnPageLoad: null,
showSaveProgressModal: null,
};
},
computed: {
@ -157,9 +156,6 @@ export default {
this.questionsData
);
},
saveProgress() {
saveQuote({ pageNameToLog: "capability-questions" });
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
// get answerResult2 of returned answer

View file

@ -16,8 +16,8 @@
<!-- save progress goes into <slot> on questions-page-layout -->
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
pageName="molding-questions"
v-if="showSaveProgressModal" />
</questions-page-layout>
</Form>
</template>
@ -38,7 +38,6 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -65,7 +64,7 @@ export default {
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
vm.showSaveProgressModal = !(emailFromStore?.length > 0);
if (store.getters.externalParameterState?.isExternalParameter) {
if (store.getters.externalParameterServiceZip.zipCode) {
await baseMixin.methods.dispatchStoreAction(
@ -88,7 +87,7 @@ export default {
questionsData: null,
selectedAnswers: {},
currentGlassIndex: 0,
isEmailInStoreOnPageLoad: null,
showSaveProgressModal: null,
};
},
computed: {
@ -157,9 +156,6 @@ export default {
this.questionsData
);
},
saveProgress() {
saveQuote({ pageNameToLog: "molding-questions" });
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
return {

View file

@ -16,8 +16,8 @@
<!-- save progress goes into <slot> on questions-page-layout -->
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
pageName="part-questions"
v-if="showSaveProgressModal" />
</questions-page-layout>
</Form>
</template>
@ -38,7 +38,6 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -65,7 +64,7 @@ export default {
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
vm.showSaveProgressModal = !(emailFromStore?.length > 0);
if (store.getters.externalParameterState?.isExternalParameter) {
if (store.getters.externalParameterServiceZip.zipCode) {
const serviceState = store.getters.order.serviceLocation.state;
@ -89,7 +88,7 @@ export default {
questionsData: null,
selectedAnswers: {},
currentGlassIndex: 0,
isEmailInStoreOnPageLoad: null,
showSaveProgressModal: null,
};
},
computed: {
@ -156,9 +155,6 @@ export default {
this.questionsData
);
},
saveProgress() {
saveQuote({ pageNameToLog: "part-questions" });
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
return {

View file

@ -621,7 +621,7 @@ export default {
: this.$store.getters.order.serviceLocation?.appointmentType;
},
ServiceLocationFullAddressText() {
return `${this.ServiceLocationAddress.toLowerCase()}${this.ServiceLocationAddress2 ? ", " + this.ServiceLocationAddress2 : ""}, ${this.ServiceLocationCity.toLowerCase()}, ${this.ServiceLocationState} ${this.ServiceLocationZipCode}`;
return `${this.ServiceLocationAddress?.toLowerCase()}${this.ServiceLocationAddress2 ? ", " + this.ServiceLocationAddress2 : ""}, ${this.ServiceLocationCity?.toLowerCase()}, ${this.ServiceLocationState} ${this.ServiceLocationZipCode}`;
},
ServiceLocationFullAddress() {
if (this.AppointmentType === AppointmentTypeStrings.MOBILE) {

View file

@ -26,7 +26,7 @@ export default {
computed: {
displayContent() {
return [
`${this.addressInfo.address.toLowerCase()}${this.addressInfo.address2 ? ", " : ""}${this.addressInfo.address2.toLowerCase()}, ${this.addressInfo.city.toLowerCase()}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`,
`${this.addressInfo?.address?.toLowerCase()}${this.addressInfo?.address2 ? ", " : ""}${this.addressInfo?.address2?.toLowerCase()}, ${this.addressInfo?.city?.toLowerCase()}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`,
];
},
addressInfo() {

View file

@ -82,8 +82,9 @@
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
modalName="SaveProgressModal"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
v-if="showSaveProgressModal"
pageName="quote"
@save-progress-saved="updateSaveProgressAsSaved" />
<textBlock
cmsWidgetName="quoteDisclaimer"
@ -98,9 +99,10 @@
<saveProgressPopupQuestion
modalWidgetName="SaveProgressPopupWidget"
modalName="SaveProgressPopup"
v-if="!isEmailInStoreOnPageLoad && showSaveProgressPopup"
pageName="quote"
v-if="showSaveProgressPopup"
:showSaveProgressPopup="showSaveProgressPopup"
@save-progress="saveProgress"
@save-progress-saved="updateSaveProgressAsSaved"
@close-save-progress-popup="closeSaveProgressPopup" />
</div>
</div>
@ -209,6 +211,7 @@ export default {
const emailFromStore = store.getters.order.customer.emailAddress;
const isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
const showSaveProgressPopup = isEmailInStoreOnPageLoad ? false : true;
const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true;
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.vaps = lineItems.vaps ?? [];
@ -290,8 +293,8 @@ export default {
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = isEmailInStoreOnPageLoad;
vm.showSaveProgressPopup = showSaveProgressPopup;
vm.showSaveProgressModal = showSaveProgressModal;
vm.addableVaps = addableVaps;
vm.lineItems = lineItems;
vm.availableLineItems = pricingResults;
@ -438,10 +441,14 @@ export default {
servicePackage: null,
holdParentAccountNumber: null,
holdBillToAccountNumber: null,
isEmailInStoreOnPageLoad: null,
showSaveProgressPopup: null,
showSaveProgressModal: null,
isSaveProgressComplete: null,
};
},
mounted() {
this.attachCustomEvents();
},
computed: {
lineItemsCloneForWatcher() {
return Object.assign({}, this.lineItems);
@ -526,10 +533,8 @@ export default {
backButtonAction() {
vehicleQuestionsMixin.methods.navigateBack(this);
},
saveProgress() {
this.isEmailInStoreOnPageLoad = true;
this.showSaveProgressPopup = false;
saveQuote({ pageNameToLog: "quote" });
updateSaveProgressAsSaved() {
this.showSaveProgressModal = false;
},
closeSaveProgressPopup() {
this.showSaveProgressPopup = false;
@ -677,9 +682,6 @@ export default {
});
},
},
mounted() {
this.attachCustomEvents();
},
watch: {
lineItemsCloneForWatcher: {
handler(newValue, oldValue) {

View file

@ -0,0 +1,124 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="return-user small-question-text">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 mt-4">
<funnelSubHeader
ref="funnelSubHeader"
cmsWidgetName="FunnelSubHeaderWidget" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
buttonSize
:isBackButtonHidden="true"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 start-over">
<a @click="startOver" alt="Start Over Button"
><img v-bind:src="startOverImage" />{{ startOverText }}</a
>
</div>
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form } from "vee-validate";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { storeActions } from "@/constants/store-actions";
export default {
name: "return-user",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.startOverText = resultMap.cmsContent.StartOverWidget.HeaderText;
vm.startOverImage = resultMap.cmsContent.StartOverWidget.Image;
});
},
data() {
return {
startOverText: "",
startOverImage: null,
};
},
methods: {
arePagePrerequisitesValid() {
return getFunnelCookie() !== null;
},
async forwardButtonAction() {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
async startOver() {
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.CLICKED,
"Start over",
true
);
deleteFunnelCookie();
await this.dispatchStoreAction(storeActions.RESET_STATE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
},
components: {
Form,
funnelHeader,
funnelSubHeader,
navbar,
},
};
</script>
<style lang="scss" scoped>
.start-over {
text-align: center;
a {
font-weight: 600;
color: $black;
cursor: pointer;
text-decoration: underline;
}
}
</style>

View file

@ -48,8 +48,8 @@
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
pageName="vehicle-parts"
v-if="showSaveProgressModal" />
</div>
</div>
</div>
@ -75,7 +75,6 @@ import store from "@/store";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
export default {
name: "vehicle-parts",
@ -95,7 +94,7 @@ export default {
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
vm.showSaveProgressModal = !(emailFromStore?.length > 0);
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
@ -115,7 +114,7 @@ export default {
selectedGlassParts: {},
alertWidgetData: Object,
alreadyPopulatedPartsData: [],
isEmailInStoreOnPageLoad: null,
showSaveProgressModal: null,
};
},
computed: {
@ -234,9 +233,6 @@ export default {
});
});
},
saveProgress() {
saveQuote({ pageNameToLog: "vehicle-parts" });
},
},
mounted() {
this.LoadInitialPartsData();

View file

@ -26,6 +26,7 @@ import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { applicationConfig } from "../constants/application-config";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export default {
methods: {
@ -650,6 +651,13 @@ export default {
});
},
pushPageErrorToDataLayer(error) {
pushToDataLayerIfDefined({
event: "page-error",
error: error,
});
},
prependActionToMethod(object, method, actionToPrepend) {
const baseMethodName = method.name.startsWith("bound ")
? method.name.substring(6)
@ -702,6 +710,21 @@ export default {
},
async validateSession() {
// The noSession function checks the cookies related to analytics logging(sid). it is not the funnel info cookie.
// The sid cookie for analytics will expire every 30 minutes and get recreated in initSession. If this happens
// and we also have the funnel cookie present, that indicates they have an existing session that is now expired
// so route them to the return user page.
const funnelCookieLastTouched = getFunnelCookie()?.LastTouched;
if (
this.noSession() &&
funnelCookieLastTouched !== null &&
funnelCookieLastTouched !== undefined
) {
await this.initSession();
window.location.href = applicationConfig.RETURN_USER_PAGE;
return;
}
if (this.noSession()) {
await this.initSession();
}

View file

@ -26,6 +26,7 @@ import { loadSessionIfPresent, saveSession } from "@/helpers/heritage-integratio
import {
getPageToRouteExistingOrderTo,
navigateToHeritageFunnel,
getImplicitNavigation,
} from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin";
@ -53,6 +54,7 @@ const routes = [
log("------------- router index.js beforeEnter start -----------------");
log(" --to: ", to);
log(" --cookie: ", getFunnelCookie());
log(` --from.redirectedFrom:>${JSON.stringify(from.redirectedFrom)}<`, "");
await analyticsMixin.methods.validateSession();
@ -65,11 +67,22 @@ const routes = [
// If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
log(" --save session timeout go to start");
const errorPayload = {
cause: "expired session",
currentPage: from?.query?.fmgPage,
nextPage: to?.query?.fmgPage,
};
await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
deleteFunnelCookie();
GoToFunnelStartOn404(next);
GoToFunnelStartOn404(next, errorPayload);
return;
}
const fromHeritage = getQuerystringParameter(queryStrings.FROM_HERITAGE) === "true";
const fromReturnUser = from?.name === fmgPageValues.RETURN_USER;
// Intercept all navigation if a submitted order exists in storage
if (window.sessionStorage.getItem("submittedOrder") !== null) {
if (to.query.fmgPage !== funnelStartPageName) {
@ -78,7 +91,21 @@ const routes = [
}
}
// On entering the funnel "fresh", read cookie information, decide what to do next.
else if (from.redirectedFrom === undefined) {
else if (from.redirectedFrom === undefined || fromReturnUser) {
// if entering the funnel from non-funnel check and see if there is already a funnel cookie.
// if so, send them to return-user page
const funnelCookieLastTouched = getFunnelCookie()?.LastTouched;
if (
to?.query?.fmgPage !== fmgPageValues.RETURN_USER &&
!fromReturnUser &&
!fromHeritage &&
funnelCookieLastTouched !== null &&
funnelCookieLastTouched !== undefined
) {
log(" --navigate to return user");
NavigateToReturnUser(next, to);
}
// clear the saveSessionPromise - if it exists in the vuex store but a new instance was created
// the saveSessionPromise will no longer point to a valid promise
log(" --clear promise");
@ -137,6 +164,11 @@ const routes = [
store.commit(storeMutations.RESET_GLASS_PARTS_STATE);
}
// if coming from the return user page, clear the destination page so implicit navigation runs
if (fromReturnUser && to.query) {
to.query[queryStrings.FMG_PAGE] = "";
}
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to);
log(" --pageToRedirectTo ", JSON.stringify(pageToRedirectTo));
@ -206,7 +238,14 @@ const routes = [
return;
}
GoToFunnelStartOn404(next);
const errorPayload = {
cause: "invalid page prerequisites for page",
currentPage: from?.query?.fmgPage,
nextPage: to?.query?.fmgPage,
};
GoToFunnelStartOn404(next, errorPayload);
return;
}
log(" --has route:", to.query.fmgPage);
@ -214,13 +253,19 @@ const routes = [
}
if (!isExistingFmgPageName(to.query.fmgPage)) {
console.log("Not an existing fmg page name:" + to.query.fmgPage);
GoToFunnelStartOn404(next);
const errorPayload = {
cause: "invalid page name",
currentPage: from?.query?.fmgPage,
nextPage: to?.query?.fmgPage,
};
GoToFunnelStartOn404(next, errorPayload);
return;
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
const routeData = GetRouteInfoFromPageName(to.query.fmgPage);
log("--routeData:", routeData);
// Add our dynamic route.
router.addRoute({
@ -239,7 +284,15 @@ const routes = [
if (!arePagePrerequisitesValid(nextComponent)) {
// prettier-ignore
console.log("Page Prereqs not valid for next component: " + nextComponent.default.name);
GoToFunnelStartOn404(next);
const errorPayload = {
cause: "invalid page prerequisites for page",
currentPage: from?.query?.fmgPage,
nextPage: to?.query?.fmgPage,
};
GoToFunnelStartOn404(next, errorPayload);
return;
}
log("------------- router index.js beforeEnter end -----------------");
@ -261,7 +314,16 @@ const routes = [
console.log(new Date() + " Exception in beforeEnter:" + JSON.stringify(error));
// If we don't have a route, go to our 404 page.
GoToFunnelStartOn404(next);
const errorPayload = {
cause: "uncaught error in beforeEnter",
currentPage: from?.query?.fmgPage,
nextPage: to?.query?.fmgPage,
fullError: error,
errorStack: error?.stack,
};
GoToFunnelStartOn404(next, errorPayload);
return;
}
},
},
@ -280,6 +342,7 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.beforeEach(async (to, from, next) => {
log("------------- router index.js beforeEach start -----------------");
// set lastNavigationPage here to capture state before API calls for analytics.
// use current page url query string name when to.name is "root" (due to unresolved navigation in beforeEach)
router.lastNavigationPage = to.name == "root" ? analyticsMixin.methods.getPageName() : to.name;
@ -331,6 +394,7 @@ router.beforeEach(async (to, from, next) => {
} else {
next();
}
log("------------- router index.js beforeEach end -----------------");
});
router.afterEach(async (to, from) => {
@ -397,8 +461,8 @@ router.navigateToExternalUrl = (url, optionalQuery = {}) => {
navigateToUrl(url, optionalQuery);
};
router.navigateError = () => {
DisplayPageError();
router.navigateError = (errorPayload = null) => {
DisplayPageError(errorPayload);
};
//Use this navigation when you need to call next() explicitly. beforeRouteEnter is a good example.
@ -587,8 +651,55 @@ function GetRouteInfoFromPageName(pageName) {
return routeData;
}
async function NavigateToReturnUser(next, to) {
const returnRoute = GetRouteInfoFromPageName(fmgPageValues.RETURN_USER);
log("--returnRoute:", returnRoute);
if (router.hasRoute(to.query.fmgPage)) {
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
let component = router
.getRoutes()
.filter((x) => x.name === fmgPageValues.RETURN_USER)[0].components;
log("--return-user component:", component);
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
}
log(" --has route for return-user:", fmgPageValues.RETURN_USER);
return next({ name: fmgPageValues.RETURN_USER, query: to.query, params: to.params });
} else {
log(" --add route for return-user:", fmgPageValues.RETURN_USER);
router.addRoute({
path: returnRoute[0].path, // Always the same path, because we control it with query strings.
name: returnRoute[0].name,
component: returnRoute[0].component,
});
next({
name: returnRoute[0].name,
query: Object.assign(to.query, { fmgPage: returnRoute[0].name }),
params: to.params,
});
return;
}
}
// Go to our start page on a 404.
function GoToFunnelStartOn404(next) {
function GoToFunnelStartOn404(next, errorPayload = null) {
if (errorPayload !== null) {
errorPayload.type = "GoToFunnelStartOn404";
analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
console.log(
"%cGoToFunnelStartOn404()... errorPayload",
"color: white; background-color: blue; padding: 5px;",
errorPayload
);
}
// Put item on the bus
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
@ -607,7 +718,28 @@ function GoToFunnelStartOn404(next) {
});
}
async function DisplayPageError() {
async function DisplayPageError(errorPayload = null) {
console.log("%c running DisplayPageError()... ", "font-size: 20px; color: purple;");
if (errorPayload !== null) {
errorPayload.type = "DisplayPageError";
analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
} else {
// very cautiously to avoid additional errors:
var currentPage = "";
try {
currentPage = getQuerystringParameter(queryStrings.FMG_PAGE);
} catch (e) {
// pass
}
errorPayload = {
type: "DisplayPageError",
cause: "Unknown page error",
currentPage: currentPage,
};
analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
}
// Put item on the bus
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,

View file

@ -22,8 +22,10 @@ const fmgPageValues = {
PAYMENT: "payment",
PAYMENT_PIA_RETURN: "payment-pia-return",
CONFIRMATION: "confirmation",
RETURN_USER: "return-user",
};
const funnelStartPageName = fmgPageValues.VEHICLE;
const returnUserPageName = fmgPageValues.RETURN_USER;
export { fmgPageValues, funnelStartPageName };
export { fmgPageValues, funnelStartPageName, returnUserPageName };

View file

@ -530,6 +530,16 @@ const routingTable = function (store) {
{
fmgPageValue: fmgPageValues.CONFIRMATION,
},
// The destination page for return user does not matter. the router will invoke implicit navigation
{
fmgPageValue: fmgPageValues.RETURN_USER,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE,
},
],
},
];
};

View file

@ -7,6 +7,7 @@ process.env.VUE_APP_MY_ACCOUNT = "https://myaccountdev.safelite.com/";
//process.env.VUE_APP_SAFELITE_HOP = "http://localhost:60966/fmgCheckoutShared.aspx";
process.env.VUE_APP_SAFELITE_HOP = "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx";
process.env.VUE_APP_SOLARWINDS_MONITORING = "";
process.env.VUE_APP_SESSION_TIMEOUT_MINUTES = 30;
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY =

View file

@ -5,6 +5,7 @@ process.env.VUE_APP_CURRENT_ENVIRONMENT = "__VUE_APP_CURRENT_ENVIRONMENT__";
process.env.VUE_APP_MY_ACCOUNT = "__VUE_APP_MY_ACCOUNT__";
process.env.VUE_APP_SAFELITE_HOP = "__VUE_APP_SAFELITE_HOP__";
process.env.VUE_APP_SOLARWINDS_MONITORING_SCRIPT = "__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__";
process.env.VUE_APP_SESSION_TIMEOUT_MINUTES = 30;
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__";