From 3938eea60cadc77308bcc19ffdbdf3975a806671 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Wed, 11 Mar 2026 08:53:18 -0400 Subject: [PATCH 01/11] commit to transfer laptop --- src/constants/experiments.js | 4 + src/layouts/schedule-page/schedule-page.vue | 113 ++++++++++++++++++++ vue.config.js | 2 +- 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index 9e959331..c336503b 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -2,6 +2,7 @@ const experimentUniverses = Object.freeze({ ISS_FUNNEL: 'ISSFunnel', ISS_FEATURETOGGLE_AREFEES_HIDDEN: 'NextGenISS_FeatureToggle_AreFeesHidden', ISS_FEATURETOGGLE_AREFEES_OVERRIDDEN: 'NextGenISS_FeatureToggle_AreFeesOverridden', + ISS_MOBILE_FIRST_APPOINTMENTS: 'ISS_Mobile_First_Appointments', ADYEN_PAYMENT_TEST: 'NextGenAdyenPaymentTest' }); @@ -12,6 +13,9 @@ const experimentSettings = Object.freeze({ ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_OVERRIDDEN: 'OverrideMobileFee', ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_HIDDEN: 'HideRecycleFee', ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_OVERRIDDEN: 'OverrideRecycleFee', + ISS_MOBILE_FIRST_MAX_MOBILE_DAYS: 'MaxMobileDays', + ISS_MOBILE_FIRST_MAX_PM_MOBILE_DAYS: 'MaxPmMobileDays', + ISS_MOBILE_FIRST_SHOW_FIRST_MOBILE_APPOINTMENT: 'ShowMobileFirstAppointment', ISS_ENABLE_ADYEN_V1: 'ISS_Enable_Adyen_V1' }); diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index b4a68eb0..610ececc 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -72,6 +72,7 @@ import serviceLocation from '@/layouts/schedule-page/service-location/service-lo import siteFooter from '@/iss-components/site-footer/site-footer.vue'; // Supporting files +import { experimentSettings } from '@/constants/experiments'; import { AppointmentTypeStrings, GET_MOBILE_TIME_SLOTS, @@ -323,9 +324,20 @@ export default { zipCodeCtu: resultMap.zipCodeData?.zipCodeCtu }; + const mobileFirstObject = { + mobileProviderNumber: resultMap.providers?.mobileProviderNumber, + serviceabilityDetails: resultMap.serviceabilityDetails, + zipCode: initialServiceLocationObj.zipCode + } + vm.setCmsContent(resultMap.cmsContent); vm.$refs.serviceLocation.initializeComponent(serviceLocationData); await vm.setData(resultMap.premiumFeeWithPrice, initialServiceLocationObj); + console.log('Schedule page loaded with all API calls settled'); + console.log('Checking mobile first experience eligibility...'); + console.log(resultMap); + console.log('Mobile first experience eligibility object:', mobileFirstObject); + await vm.checkMobileFirstExperience(mobileFirstObject); showIssLoadingModal(false); }); }, @@ -432,6 +444,98 @@ export default { arePagePrerequisitesValid() { return useMainStore().order.serviceLocation.zipCode !== null; }, + async checkMobileFirstExperience(mobileFirstObject = {}) { + const showMobileFirst = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_SHOW_FIRST_MOBILE_APPOINTMENT); + console.log('Mobile first experience enabled:', showMobileFirst); + if (showMobileFirst) { + console.log('Checking mobile first experience eligibility...'); + const hasSelectedDate = !!this.getSelectedDate(); + const isServicableMobile = mobileFirstObject.serviceabilityDetails?.isGlassServiceableMobile; + if (!hasSelectedDate && isServicableMobile && mobileFirstObject.mobileProviderNumber && mobileFirstObject.zipCode) { + console.log('Get Mobile Dates, Select first one and call modal... Gonna be fantastical!'); + const todayDateObject = new Date(); + const todayDateString = convertDateToDateString(todayDateObject); + const calendarViewDirection = 'future'; + const initialDays = 15; + + const initialViewStartDate = todayDateString; + const initialEndDate = new Date(); + initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1)); + const initialViewEndDate = convertDateToDateString(initialEndDate); + const preSelectedDate = this.selectedDate; + + const initialData = await getAvailableDates( + initialViewStartDate, + initialViewEndDate, + AppointmentTypeStrings.MOBILE, + mobileFirstObject.mobileProviderNumber, + mobileFirstObject.zipCode + ).then((response) => ({ + todayDate: todayDateString, + initialViewStartDate, + initialViewEndDate, + calendarViewDirection, + initialShopTimeSlotsResponse: response, + preSelectedDate, + initialDaysLoaded: initialDays, + daysFromStart: null + })) + .catch(() => { + return { + todayDate: todayDateString, + initialViewStartDate, + initialViewEndDate, + calendarViewDirection, + initialShopTimeSlotsResponse: { days: [] }, + preSelectedDate, + initialDaysLoaded: initialDays, + daysFromStart: null + }; + }); + + console.log('Mobile first experience initial data...'); + console.log(initialData); + + const retTimeSlotsResponse = []; + initialData.initialShopTimeSlotsResponse.days.forEach((selectableDate) => { + const dateObjectToPush = { + date: selectableDate.date, + timeSlots: selectableDate.timeSlots, + morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => { + const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10); + return timeHour < 12; + }).map((timeSlot) => this.mapTimeSlot(timeSlot, 'morning')), + afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => { + const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10); + return timeHour >= 12; + }).map((timeSlot) => this.mapTimeSlot(timeSlot, 'afternoon')), + isSelected: false + }; + + if (dateObjectToPush.morningTimeSlots.length > 0) { + const findIndex = dateObjectToPush.morningTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF')); + if (findIndex !== -1) { + const dropOffSlot = dateObjectToPush.morningTimeSlots.splice(findIndex, 1)[0]; + dateObjectToPush.morningTimeSlots.unshift(dropOffSlot); + } + } + if (dateObjectToPush.afternoonTimeSlots.length > 0) { + const findIndex = dateObjectToPush.afternoonTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF')); + if (findIndex !== -1) { + const dropOffSlot = dateObjectToPush.afternoonTimeSlots.splice(findIndex, 1)[0]; + dateObjectToPush.afternoonTimeSlots.push(dropOffSlot); + } + } + retTimeSlotsResponse.push(dateObjectToPush); + }); + + console.log('Mobile first experience mapped time slots response...'); + console.log(retTimeSlotsResponse); + + this.mobileDatesData = initialData; + } + } + }, async setData(premiumFeeWithPriceResponse, initialServiceLocationObj) { this.selectedServiceLocation = initialServiceLocationObj; this.mobilePremiumAppointmentFee = premiumFeeWithPriceResponse @@ -486,6 +590,7 @@ export default { this.selectedMobileZipCode = null; }, dateSelectedFromPicker(date) { + console.log('Schedule selected date from picker', date); this.selectedDate = date; this.showDatePickerError = false; }, @@ -677,6 +782,13 @@ export default { this.selectedServiceLocation = inShopZipServiceLocation; } }, + mapTimeSlot(timeSlot, timeOfDay) { + return { + ...timeSlot, + isDropoff: timeSlot.id.endsWith('DROP OFF'), + timeOfDay + }; + }, mobileZipUpdatedFromServiceLocation(mobileZipServiceLocation) { if (mobileZipServiceLocation) { this.mobileProviderNumber = mobileZipServiceLocation.mobileProviderNumber; @@ -711,6 +823,7 @@ export default { this.selectableDatesData = data.initialShopTimeSlotsResponse; this.$refs.datePicker.initializeComponent(data); this.isDatePickerRefreshing = false; + console.log('Date picker refreshed with new provider/zip code data'); showIssLoadingModal(false); }); }, diff --git a/vue.config.js b/vue.config.js index d9d97c60..0a9dc98e 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,4 +1,4 @@ -process.env.VUE_APP_CONSUMER_CF_DISTRO = 'https://digitalapi.dev.safelite.io'; +process.env.VUE_APP_CONSUMER_CF_DISTRO = 'https://digitalapi.test.safelite.io'; process.env.VUE_APP_CURRENT_ENVIRONMENT = 'Localhost'; process.env.VUE_APP_CUSTOMER_PORTAL_URL = 'https://myaccountdev.safelite.com/'; process.env.VUE_APP_GOOGLE_PLACES_API_KEY = 'AIzaSyCuLhQcDdZTTb4JzpUFms1OCch2dk5lHF0'; From bc0a509c663199fb6c407f490b8daef1960f3af3 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Wed, 11 Mar 2026 16:25:57 -0400 Subject: [PATCH 02/11] just to have --- src/helpers/service-location-helper.js | 52 ++--- src/layouts/schedule-page/schedule-page.vue | 75 +++++-- .../suggest-timeslot-modal.vue | 186 ++++++++++++++++++ 3 files changed, 279 insertions(+), 34 deletions(-) create mode 100644 src/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js index 72572256..13aced81 100644 --- a/src/helpers/service-location-helper.js +++ b/src/helpers/service-location-helper.js @@ -15,8 +15,23 @@ function processZipCodeResults(request) { })); } -export async function getZipCodeData(zipCode) { - return await processZipCodeResults(useMainStore().validateZip({ zip: zipCode })); +export async function getAvailabilityRating( + startDate, + endDate, + shopAppointmentType, + providerNumber +) { + // For a given shop provider number and date range, get the appointment time slots available + const shopTimeSlots = await useMainStore().getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber); + + const numberOfDaysToEvaluate = 2; + const isGoodAvailability = + shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length + >= numberOfDaysToEvaluate; + + const shopStatus = isGoodAvailability ? 'high' : 'low'; + + return Promise.resolve(shopStatus); } export async function getMobileZipCodeData(zipCode) { @@ -42,25 +57,6 @@ export async function getPricedMobileFeePart(serviceZipCode) { return Promise.resolve(pricingResults[0]); } -export async function getAvailabilityRating( - startDate, - endDate, - shopAppointmentType, - providerNumber -) { - // For a given shop provider number and date range, get the appointment time slots available - const shopTimeSlots = await useMainStore().getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber); - - const numberOfDaysToEvaluate = 2; - const isGoodAvailability = - shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length - >= numberOfDaysToEvaluate; - - const shopStatus = isGoodAvailability ? 'high' : 'low'; - - return Promise.resolve(shopStatus); -} - export function getProviderData() { const mainStore = useMainStore(); const storeServiceLocation = mainStore.order.serviceLocation; @@ -73,6 +69,18 @@ export function getProviderData() { return { storeServiceLocation, isMobileAppointment, storeSelectedProvider, serviceZipCode } } +export async function getZipCodeData(zipCode) { + return await processZipCodeResults(useMainStore().validateZip({ zip: zipCode })); +} + +export function mapTimeSlot(timeSlot, timeOfDay) { + return { + ...timeSlot, + isDropoff: timeSlot.id.endsWith('DROP OFF'), + timeOfDay + }; +} + export async function onProviderChanged() { const { isMobileAppointment, storeSelectedProvider, serviceZipCode } = getProviderData(); if (!storeSelectedProvider) { @@ -101,4 +109,4 @@ export async function onProviderChanged() { } await settleAllPromises(promiseResultMap); -} \ No newline at end of file +} diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 610ececc..7a57cd9f 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -60,6 +60,9 @@ + + + \ No newline at end of file From d068ff44464229ea10a045bce725400fc64c2b17 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Mon, 23 Mar 2026 17:26:41 -0400 Subject: [PATCH 03/11] updates for mobile first feel like i removed something --- src/constants/analytics.js | 15 ++- src/layouts/schedule-page/schedule-page.vue | 104 ++++++++++----- .../suggest-timeslot-modal.vue | 126 +++++++++++++----- src/mixins/experiment-mixin.js | 4 + src/store/index.js | 3 + 5 files changed, 185 insertions(+), 67 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index c0171209..8cf147e8 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -11,22 +11,29 @@ const GaEvents = Object.freeze({ const GaCategories = Object.freeze({ API_RESPONSE: 'Api_Response', + CONFIRMATION_CLICKED: "confirmation clicked", + CONFIRMATION_CLICKED_INSHOP: "inshop confirmation clicked", + CONFIRMATION_CLICKED_MOBILE: "mobile confirmation clicked", EVOX: 'Evox' }); const GaActions = Object.freeze({ - RESULT: 'Result', CLICKED: 'Clicked', + NO: "no", + RESULT: 'Result', + SUBMITTED: 'Submitted', VIF: 'vif', - SUBMITTED: 'Submitted' + YES: "yes", }); const GaLabels = Object.freeze({ - SUCCESS: 'Success', + ADDRESS_LOOKUP: 'Address_Look_up', ERROR: 'Error', LICENSE_PLATE_LOOKUP: 'License_Plate_Look_Up', + NO: "no", + SUCCESS: 'Success', VIN_LOOKUP: 'Vin_Look_Up', - ADDRESS_LOOKUP: 'Address_Look_up' + YES: "yes", }); const ValueToLogTypes = Object.freeze({ diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 7a57cd9f..aca58bb4 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -62,7 +62,9 @@ + cmsWidgetName="SuggestTimeslotModal" + @confirmAppointmentClicked="autoSelectTimeslotConfirmed" + @seeMoreOptionsClicked="seeMoreOptions"/> diff --git a/src/mixins/experiment-mixin.js b/src/mixins/experiment-mixin.js index 5b1f7142..7f9a761e 100644 --- a/src/mixins/experiment-mixin.js +++ b/src/mixins/experiment-mixin.js @@ -8,6 +8,10 @@ export default { hasSetting(settingName) { return Object.hasOwn(useMainStore().experimentSettings, settingName); }, + getExperimentByName(experimentName) { + const experiment = useMainStore().applicationUser.experiments.find((e) => e.universeName === experimentName); + return experiment ? experiment : null; + }, getSettingValue(settingName) { return this.hasSetting(settingName) ? useMainStore().experimentSettings[settingName] diff --git a/src/store/index.js b/src/store/index.js index 8a4f1b34..2d2b6040 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2587,6 +2587,9 @@ export const useMainStore = defineStore({ } ); }, + logMobileFirstExperimentExposure() { + this.logExperimentIfExists(issPageValues.SCHEDULE_PAGE, experimentUniverses.ISS_MOBILE_FIRST_APPOINTMENTS); + }, logWelcomePageExperiments(issPage) { this.logExperimentIfExists(issPage, experimentUniverses.ISS_FEATURETOGGLE_AREFEES_HIDDEN); this.logExperimentIfExists(issPage, experimentUniverses.ISS_FEATURETOGGLE_AREFEES_OVERRIDDEN); From 9902c70b66b5cf172e5eed91e712b32cd84a4160 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Mon, 23 Mar 2026 17:35:41 -0400 Subject: [PATCH 04/11] dev url --- vue.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue.config.js b/vue.config.js index 0a9dc98e..d9d97c60 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,4 +1,4 @@ -process.env.VUE_APP_CONSUMER_CF_DISTRO = 'https://digitalapi.test.safelite.io'; +process.env.VUE_APP_CONSUMER_CF_DISTRO = 'https://digitalapi.dev.safelite.io'; process.env.VUE_APP_CURRENT_ENVIRONMENT = 'Localhost'; process.env.VUE_APP_CUSTOMER_PORTAL_URL = 'https://myaccountdev.safelite.com/'; process.env.VUE_APP_GOOGLE_PLACES_API_KEY = 'AIzaSyCuLhQcDdZTTb4JzpUFms1OCch2dk5lHF0'; From 372213eefdea781b4f742e1848c5005c83d5ccf4 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Tue, 24 Mar 2026 15:34:56 -0400 Subject: [PATCH 05/11] update for loading modal some unused code clean up repair/replace added to cms --- src/layouts/schedule-page/schedule-page.vue | 10 +++------- .../suggest-timeslot-modal/suggest-timeslot-modal.vue | 10 ++++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index aca58bb4..43c73f67 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -63,8 +63,7 @@ + @confirmAppointmentClicked="autoSelectTimeslotConfirmed" /> diff --git a/src/store/index.js b/src/store/index.js index 4321a8f4..305ba605 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -238,7 +238,8 @@ export const getDefaultState = () => ({ triggeredSiteEntry: false, // TODO not in save session duplicateOrders: [], hasSentSaveQuoteEmail: null, - coverageLookupAttempts: 0 + coverageLookupAttempts: 0, + firstHit: true }, issConfig: { clientName: 'Generic Insurance', // this is the default and will be overriden by the client's name From 6acefcb307e3abef0629057e6e642b09e76f52e3 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Tue, 24 Mar 2026 17:28:26 -0400 Subject: [PATCH 08/11] Move accountName logic to store getter, better required field search --- src/iss-components/continue-modal/continue-modal.vue | 9 +++------ src/layouts/welcome-page/welcome-page.vue | 8 +++----- src/store/index.js | 3 ++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/iss-components/continue-modal/continue-modal.vue b/src/iss-components/continue-modal/continue-modal.vue index 062cb86b..8bca85ce 100644 --- a/src/iss-components/continue-modal/continue-modal.vue +++ b/src/iss-components/continue-modal/continue-modal.vue @@ -64,14 +64,11 @@ export default { modalSubHeaderText() { return this.getCmsContent('ContinueReferralModalWidget', 'SubheaderText') .replaceAll('{custom:mmFound}', this.getMakeModelString.toUpperCase()); - }, - accountName() { - return this.mainStore.issConfig.clientName.replaceAll(" ", "").replaceAll("&", "amp"); } }, methods: { openModal() { - this.pushEventToGA("existing_claim", "pop_up_displayed", this.accountName, true); + this.pushEventToGA("existing_claim", "pop_up_displayed", this.mainStore.accountNameForEvents, true); this.$refs.continueModal.openModal(); }, setModalStatus(isOpened) { @@ -81,11 +78,11 @@ export default { this.$refs.continueModal.closeModal(); }, continueReferral() { - this.pushEventToGA("existing_claim", "finish_claim", this.accountName, true); + this.pushEventToGA("existing_claim", "finish_claim", this.mainStore.accountNameForEvents, true); this.$emit('continue-previous-referral'); }, startNewReferral() { - this.pushEventToGA("existing_claim", "start_new_claim", this.accountName, true); + this.pushEventToGA("existing_claim", "start_new_claim", this.mainStore.accountNameForEvents, true); this.$emit('start-new-referral'); } } diff --git a/src/layouts/welcome-page/welcome-page.vue b/src/layouts/welcome-page/welcome-page.vue index 11584f52..2b80ca23 100644 --- a/src/layouts/welcome-page/welcome-page.vue +++ b/src/layouts/welcome-page/welcome-page.vue @@ -262,7 +262,8 @@ export default { this.pushEventToGA("policy_info", "info_prefilled", isInfoPrefilled.toString(), true); this.$nextTick(() => { - const requiredFieldCount = document.querySelectorAll('[aria-required="true"]').length; + const formRoot = this.$el; + const requiredFieldCount = formRoot.querySelectorAll('[aria-required="true"]').length; this.pushEventToGA("policy_info", "required_fields", requiredFieldCount.toString(), true); }); @@ -279,7 +280,7 @@ export default { this.pushEventToGA("visitor_info_welcome", "referring_site", "ClientSite", null); } - this.pushEventToGA("visitor_info_welcome", "client_name", this.accountName, null); + this.pushEventToGA("visitor_info_welcome", "client_name", this.mainStore.accountNameForEvents, null); }, computed: { DamageCauseOptions() { @@ -329,9 +330,6 @@ export default { }, phoneMask() { return MaskaFormattedMasks.PHONE_NUMBER; - }, - accountName() { - return this.mainStore.issConfig.clientName.replaceAll(" ", "").replaceAll("&", "amp"); } }, methods: { diff --git a/src/store/index.js b/src/store/index.js index 305ba605..dc5c549f 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -445,7 +445,8 @@ export const useMainStore = defineStore({ .map((x) => x.settings) .reduce((r, c) => Object.assign(r, c), {}) ?? {}, originalDeductible: (state) => (state.order.damage.isRepair ? state.order.originalDeductible.repair : state.order.originalDeductible.replace), - currentDeductible: (state) => (state.order.damage.isRepair ? state.order.currentDeductible.repair : state.order.currentDeductible.replace) + currentDeductible: (state) => (state.order.damage.isRepair ? state.order.currentDeductible.repair : state.order.currentDeductible.replace), + accountNameForEvents: (state) => state.issConfig.clientName.replaceAll(" ", "").replaceAll("&", "amp") }, actions: { From a9ae7666b605999c0c61da2be394d2820dee7ac1 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Wed, 25 Mar 2026 10:29:14 -0400 Subject: [PATCH 09/11] mobile styling --- .../suggest-timeslot-modal.vue | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue b/src/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue index c632a79e..710d8200 100644 --- a/src/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue +++ b/src/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue @@ -146,10 +146,19 @@ export default {