From 1a36c380719fb1a1504d23a78dc1342787b78667 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 12:54:54 -0400 Subject: [PATCH 01/25] CASH-2820 | Load more shops New textLink component for "View more shops" button Add in method which adds in new shops New data member which tracks all providers New data member which tracks if new shops are being loaded --- src/layouts/scheduling/scheduling.vue | 112 +++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue index 2b2a8e803..9674802ca 100644 --- a/src/layouts/scheduling/scheduling.vue +++ b/src/layouts/scheduling/scheduling.vue @@ -1,6 +1,6 @@ @@ -98,15 +104,21 @@ import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/in import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay"; import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question"; import schedulingZipSearch from "@/layouts/scheduling/scheduling-zip-search/scheduling-zip-search"; +import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue"; import textLink from "@/ux-components/text-link/text-link"; import store from "@/store"; import { storeActions } from "@/constants/store-actions"; -import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants"; +import { + AppointmentTypeStrings, + PREMIUM_FEE_PART_TYPE, + RECAL_ACK_YES, +} from "@/constants/schedule-constants"; import { isDropOffRouteCode } from "@/layouts/schedule/helpers/schedule-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper"; +import { partTypeStrings } from "@/constants/part-type-strings"; import { flushPagePrereqsLogs, hasServiceZipInfo, @@ -394,6 +406,8 @@ export default { zipSearchCode: store.getters.order.serviceLocation.zipCode ?? "", datePickerKey: 0, billToAccountNumber: store.getters.order.payment?.billToAccountNumber ?? null, + isRecalAcknowledgedForScheduling: + store.getters.order.isRecalAcknowledgedForScheduling ?? "", }; }, methods: { @@ -731,9 +745,20 @@ export default { ); }, async forwardButtonAction() { + if (this.shouldShowRecalAckModal()) { + this.$refs.navbar.removeLoader(); + this.$refs.recalAckModal.openModal(); + return; + } + const selectedScheduling = this.selectedScheduling; const selectedDate = this.selectedDate; + await store.dispatch( + storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING, + this.isRecalAcknowledgedForScheduling + ); + const appointmentType = this.getInShopOrDropOffApptType(selectedScheduling); const selectedProvider = this.getSelectedProvider(appointmentType, selectedScheduling); const serviceLocation = store.getters.order.serviceLocation; @@ -826,6 +851,23 @@ export default { ); } }, + shouldShowRecalAckModal() { + if (this.isRecalAcknowledgedForScheduling === RECAL_ACK_YES) { + return false; + } + + const glassParts = store.getters.order.lineItems?.glassParts ?? []; + return glassParts.some( + (part) => + part.partType === partTypeStrings.WINDSHIELD && + part.requiresRecalibration === true && + part.canSafeliteRecalibrate === false + ); + }, + onRecalAcknowledged(isAcknowledged) { + this.isRecalAcknowledgedForScheduling = isAcknowledged; + this.forwardButtonAction(); + }, }, components: { funnelHeader, @@ -838,6 +880,7 @@ export default { interceptOverlay, waitlistQuestion, schedulingZipSearch, + recalAckModal, textLink, }, }; From 6797b3fd718b1635043f0665b1a63e28bb284adf Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Mon, 3 Aug 2026 09:33:19 -0400 Subject: [PATCH 23/25] CASH-3105 - changing zip must re-run parts selection when recal ack modal should show --- src/layouts/scheduling/scheduling.spec.js | 31 +++++++++++++++++++++++ src/layouts/scheduling/scheduling.vue | 9 +++++++ 2 files changed, 40 insertions(+) diff --git a/src/layouts/scheduling/scheduling.spec.js b/src/layouts/scheduling/scheduling.spec.js index 105bf8fac..d6daabebb 100644 --- a/src/layouts/scheduling/scheduling.spec.js +++ b/src/layouts/scheduling/scheduling.spec.js @@ -85,6 +85,7 @@ function setupMocks({ isAppleBrowser = false } = {}) { CLICKED_FORWARD: "clickedForward", CLICKED_FORWARD_WITH_MOBILE_SERVICE: "clickedForwardWithMobileService", CLICKED_BACK: "clickedBack", + ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK", }; }, pageName() { @@ -284,6 +285,36 @@ describe("scheduling.vue", () => { expect(wrapper.vm.$refs.schedulingZipSearch.focusZipInput).toHaveBeenCalled(); wrapper.unmount(); }); + + test("navigates to service-zip when zip changes and recal acknowledgement applies", async () => { + store.getters.order.lineItems = { + glassParts: [ + { + partType: "WINDSHIELD", + requiresRecalibration: true, + canSafeliteRecalibrate: false, + }, + ], + }; + try { + const { wrapper } = setupMocks(); + await wrapper.setData({ + isRecalAcknowledgedForScheduling: "", + isLoadingDates: false, + }); + + await wrapper.vm.onZipSearched({ zipCode: "44101", billToAccountNumber: "87291" }); + + expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith( + "ZIP_CODE_CHANGED_RECAL_ACK", + "scheduling" + ); + expect(wrapper.vm.isLoadingDates).toBe(false); + wrapper.unmount(); + } finally { + store.getters.order.lineItems = { glassParts: [] }; + } + }); }); describe("forwardButtonAction", () => { diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue index 24706d075..ce27ed3b2 100644 --- a/src/layouts/scheduling/scheduling.vue +++ b/src/layouts/scheduling/scheduling.vue @@ -517,6 +517,15 @@ export default { this.$refs.schedulingZipSearch?.focusZipInput(); }, async onZipSearched({ zipCode, billToAccountNumber }) { + // When recal ack applies, changing zip must re-run parts selection via service-zip. + if (this.shouldShowRecalAckModal()) { + this.$router.navigateWithSaving( + this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK, + this.pageName + ); + return; + } + this.billToAccountNumber = billToAccountNumber; this.isLoadingDates = true; try { From fe886fa77dbf6846df7219f035ce042c8716362c Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Mon, 3 Aug 2026 10:24:07 -0400 Subject: [PATCH 24/25] CASH-2802 - GA for Mobile Availability --- src/layouts/scheduling/scheduling.spec.js | 25 +++++++++++++++++++++++ src/layouts/scheduling/scheduling.vue | 11 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/layouts/scheduling/scheduling.spec.js b/src/layouts/scheduling/scheduling.spec.js index d6daabebb..02e182056 100644 --- a/src/layouts/scheduling/scheduling.spec.js +++ b/src/layouts/scheduling/scheduling.spec.js @@ -3,6 +3,7 @@ import scheduling from "./scheduling"; import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; +import { GaActions, GaCategories, GaLabels } from "@/constants/analytics"; import store from "@/store"; jest.mock("@/store", () => ({ @@ -286,6 +287,30 @@ describe("scheduling.vue", () => { wrapper.unmount(); }); + test.each([ + ["yes", "12345", GaLabels.YES], + ["no", null, GaLabels.NO], + ])( + "pushes mobile_available %s when mobileProviderNumber is %s", + async (_label, mobileProviderNumber, expectedGaLabel) => { + settleAllPromises.mockResolvedValueOnce({}); + const { wrapper } = setupMocks(); + wrapper.vm.pushEventToGA.mockClear(); + + await wrapper.vm.loadSchedulingData("43235", { + providersResult: { shopProviders: [], mobileProviderNumber }, + }); + + expect(wrapper.vm.pushEventToGA).toHaveBeenCalledWith( + GaCategories.APPOINTMENT, + GaActions.MOBILE_AVAILABLE, + expectedGaLabel, + true + ); + wrapper.unmount(); + } + ); + test("navigates to service-zip when zip changes and recal acknowledgement applies", async () => { store.getters.order.lineItems = { glassParts: [ diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue index ce27ed3b2..f8f2da86b 100644 --- a/src/layouts/scheduling/scheduling.vue +++ b/src/layouts/scheduling/scheduling.vue @@ -439,6 +439,17 @@ export default { ? { providerNumber: mobileProviderNumber, timeSlots: null } : null; + var gaLabel = this.GaLabels.NO; + if (this.mobileProviderAndTimeSlot) { + gaLabel = this.GaLabels.YES; + } + this.pushEventToGA( + this.GaCategories.APPOINTMENT, + this.GaActions.MOBILE_AVAILABLE, + gaLabel, + true + ); + const startDate = toDateString(0); const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1); const providerNumbers = providers.map((provider) => provider.providerNumber); From 6852896f45ff8004453f614c291583337e82598c Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Mon, 3 Aug 2026 13:20:25 -0400 Subject: [PATCH 25/25] CASH-1524: Add Save progress SMS consent management --- src/constants/save-progress-cms-widgets.js | 10 ++++ .../save-progress-modal-question.spec.js | 36 ++++++++---- .../save-progress-modal-question.vue | 57 ++++++++----------- .../save-progress-popup-question.vue | 19 +++---- .../coverage-statement/coverage-statement.vue | 2 +- 5 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 src/constants/save-progress-cms-widgets.js diff --git a/src/constants/save-progress-cms-widgets.js b/src/constants/save-progress-cms-widgets.js new file mode 100644 index 000000000..d80960ace --- /dev/null +++ b/src/constants/save-progress-cms-widgets.js @@ -0,0 +1,10 @@ +const saveProgressCmsWidgets = { + PHONE_SPECIFIC: "SaveProgressPopupWidget_PhoneSpecific", + EMAIL_SPECIFIC: "SaveProgressPopupWidget_EmailSpecific", + MODAL_PHONE_QUESTION: "SaveProgressPhoneQuestionWidget", + MODAL_EMAIL_QUESTION: "SaveProgressEmailQuestionWidget", + POPUP_PHONE_QUESTION: "SaveProgressPopupPhoneQuestionWidget", + POPUP_EMAIL_QUESTION: "SaveProgressPopupEmailQuestionWidget", +}; + +export { saveProgressCmsWidgets }; diff --git a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js index f5c30e323..0f27858de 100644 --- a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js +++ b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js @@ -1,7 +1,8 @@ -import { mount, flushPromises } from "@vue/test-utils"; +import { mount } from "@vue/test-utils"; import saveProgressModalQuestion from "./save-progress-modal-question"; import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { storeActions } from "@/constants/store-actions"; +import { saveProgressCmsWidgets } from "@/constants/save-progress-cms-widgets"; import { saveQuote } from "@/helpers/heritage-integration/order-helper.js"; import experimentMixin from "@/mixins/experiment-mixin.js"; import { experimentSettings } from "@/constants/experiments"; @@ -114,14 +115,14 @@ describe("save-progress-modal-question", () => { expect(wrapper.vm.isPhoneTabSelected).toBe(true); }); - test("should use modal-specific tab labels from CMS widgets", () => { + test("should use popup-specific tab labels from CMS widgets", () => { const { wrapper } = setupMocks({ props: { modalWidgetName: "SaveProgressModalWidget", }, cmsContent: { - SaveProgressModalWidget_PhoneSpecific: { HeadlineText: "PHONE" }, - SaveProgressModalWidget_EmailSpecific: { HeadlineText: "EMAIL" }, + SaveProgressPopupWidget_PhoneSpecific: { HeadlineText: "PHONE" }, + SaveProgressPopupWidget_EmailSpecific: { HeadlineText: "EMAIL" }, }, }); @@ -131,14 +132,29 @@ describe("save-progress-modal-question", () => { ]); }); - test("should keep the legacy email disclaimer on the email tab", async () => { + test("should use the modal question CMS widgets for phone and email inputs", () => { + const { wrapper } = setupMocks({ + props: { + modalWidgetName: "SaveProgressModalWidget", + }, + }); + + expect(wrapper.vm.phoneQuestionWidgetName).toBe( + saveProgressCmsWidgets.MODAL_PHONE_QUESTION + ); + expect(wrapper.vm.emailQuestionWidgetName).toBe( + saveProgressCmsWidgets.MODAL_EMAIL_QUESTION + ); + }); + + test("should use the email-specific popup disclaimer on the email tab", async () => { const { wrapper } = setupMocks({ props: { modalWidgetName: "SaveProgressModalWidget", }, cmsContent: { - SaveProgressModalWidget: { FooterText2: "Email disclaimer" }, - SaveProgressModalWidget_PhoneSpecific: { BodyText: "Phone disclaimer" }, + SaveProgressPopupWidget_EmailSpecific: { BodyText: "Email disclaimer" }, + SaveProgressPopupWidget_PhoneSpecific: { BodyText: "Phone disclaimer" }, }, }); @@ -148,14 +164,14 @@ describe("save-progress-modal-question", () => { expect(wrapper.vm.modalDisclaimerText).toBe("Email disclaimer"); }); - test("should use the phone-specific disclaimer on the phone tab", () => { + test("should use the phone-specific popup disclaimer on the phone tab", () => { const { wrapper } = setupMocks({ props: { modalWidgetName: "SaveProgressModalWidget", }, cmsContent: { - SaveProgressModalWidget: { FooterText2: "Email disclaimer" }, - SaveProgressModalWidget_PhoneSpecific: { BodyText: "Phone disclaimer" }, + SaveProgressPopupWidget_EmailSpecific: { BodyText: "Email disclaimer" }, + SaveProgressPopupWidget_PhoneSpecific: { BodyText: "Phone disclaimer" }, }, }); diff --git a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue index 779236632..361a31869 100644 --- a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue +++ b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue @@ -62,18 +62,13 @@ :showConsentErrors="showConsentErrors" :showConsentManagement="showConsentManagement" /> - + :cmsWidgetName="emailQuestionWidgetName" + :isDisabled="isProgressSaved" />