Merge pull request #3026 from Safelite/rlsmerge/2026.01.29-to-develop

Rlsmerge/2026.01.29 to develop
This commit is contained in:
CarlNation 2026-01-22 11:48:44 -05:00 committed by GitHub
commit 56979a542f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 395 additions and 4 deletions

View file

@ -15,6 +15,7 @@ const GaCategories = {
APPOINTMENT: "Appointment",
SERVICE_LOCATION: "service-location",
CONFIRMATION_CLICKED: "confirmation clicked",
RECAL_DISCLAIMER: "Recalibration disclaimer",
};
const GaActions = {
@ -29,6 +30,7 @@ const GaActions = {
MORE_LOCATIONS_CLICKED: "more_locations_clicked",
YES: "yes",
NO: "no",
ACKNOWLEDGED: "Acknowledged",
};
const GaLabels = {

View file

@ -12,6 +12,9 @@ const experimentSettings = {
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience",
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional",

View file

@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
const RECAL_ACK_YES = "Yes";
const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
@ -31,4 +33,5 @@ export {
PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags,
cmsWidgetFieldMappings,
RECAL_ACK_YES,
};

View file

@ -111,6 +111,7 @@ const storeActions = {
SAVE_PAYPAL_TOKEN: "savePaypalToken",
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",

View file

@ -69,6 +69,7 @@ const storeMutations = {
LOCK_TOKEN: "updateLockToken",
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",

View file

@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION];
export function anyPartWithRequiresRecalFlag(lineItems) {
if (!lineItems) {
return false;
}
return lineItems.glassParts?.some((li) => li.requiresRecalibration === true);
}
export function isRecalPartOrHasChildRecalPart(lineItem) {
if (lineItem.childParts && lineItem.childParts.length > 0) {
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);

View file

@ -36,6 +36,10 @@
:isItac="isItac"
:isNoComp="isNoComp"
:isCollapsible="false" />
<textBlock
class="payment-disclaimer"
marginTopSizeOverride="0"
cmsWidgetName="PaymentDisclaimerWidget" />
</div>
</div>
@ -56,6 +60,7 @@ import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert";
import textBlock from "@/digital-components/text-block/text-block";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
@ -645,6 +650,7 @@ export default {
navbar,
cart,
alert,
textBlock,
},
};
</script>
@ -671,5 +677,10 @@ export default {
margin: 0 0.5rem;
}
}
.payment-disclaimer p {
font-size: 0.875rem;
line-height: 1.2;
}
}
</style>

View file

@ -810,11 +810,51 @@ export default {
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
if (this.isInsurance) {
return piaInsurance === "true" && this.currentDeductible > 0;
return (
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
this.currentDeductible > 0
);
} else {
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
if (piaExperience?.includes("PIA Disabled")) {
return this.isConditionalPIAEnabled;
} else {
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
}
}
},
isConditionalPIAEnabled() {
const conditionalProviderNumbers = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS
);
const conditionalProviderNumbers2 = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS_2
);
const conditionalProviderShutoffDate = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_SHUTOFF_DATE
);
const allConditionalProviderNumbers = [
conditionalProviderNumbers,
conditionalProviderNumbers2,
]
.filter(Boolean)
.join(",");
const providerNumber =
this.$store.getters.order.serviceLocation.provider.providerNumber;
const appointmentDate = new Date(this.$store.getters.order.schedule.date);
const shutoffDate = new Date(conditionalProviderShutoffDate);
if (allConditionalProviderNumbers && conditionalProviderShutoffDate) {
const providerNumbersArray = allConditionalProviderNumbers
.split(",")
.map((num) => num.trim());
if (!providerNumbersArray.includes(providerNumber)) {
return false;
} else {
return appointmentDate <= shutoffDate;
}
}
return false;
},
paymentMethod() {
if (!this.isPiaEnabled) {
return paymentMethods.LATER;

View file

@ -0,0 +1,223 @@
// This modal is used to acknowledge the recal disclaimer during scheduling. // It will trigger when
there is a glass part with requiresRecalibration = true // but there is no recalibration part in
lineItems. This mostly occurs when // a zip code that does mobile only is selected but the vehicle
requires // recalibration.
<template>
<div id="recal-ack-modal-container">
<modal
:ref="modalName"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:isFooterButtonPrimary="true"
@footer-button-event="ackRecalDisclaimer">
<p class="modal-body-inner" v-html="modalBodyText"></p>
<div>
<div class="learn-more">
<a href="javascript:;" @click="learnMore = !learnMore">{{ learnMoreLabel }}</a>
<div v-if="learnMore">{{ learnMoreText }}</div>
</div>
<buttonQuestion
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonTypeString="listButtonHorizontal"
v-model="selectedValues"
isRequired />
</div>
<template v-slot:modal-header-slot>
<span>{{ modalHeaderText }}</span>
</template>
</modal>
</div>
</template>
<script>
import modal from "@/digital-components/modal/modal";
import buttonQuestion from "@/digital-components/button-question/button-question";
import { RECAL_ACK_YES } from "@/constants/schedule-constants";
export default {
name: "recal-ack-modal",
props: {
agreementWidgetName: String,
modalWidgetName: String,
groupName: String,
},
data() {
return {
learnMore: false,
isModalOpen: true,
isRecalAcknowledged: "",
};
},
methods: {
openModal() {
this.modal.openModal();
},
onModalClosed() {
this.$emit("recal-ack-modal");
},
closeModal() {
this.modal.closeModal();
},
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
ackRecalDisclaimer() {
this.modal.closeModal();
if (this.isRecalAcknowledged === RECAL_ACK_YES) {
this.pushEventToGA(
this.GaCategories.RECAL_DISCLAIMER,
this.GaActions.ACKNOWLEDGED,
this.isRecalAcknowledged,
true
);
this.$emit("recalAcknowledged", this.isRecalAcknowledged);
}
},
},
computed: {
questionText() {
return this.getCmsContent(this.agreementWidgetName, "QuestionText");
},
answersFromCms() {
return this.getCmsContent(this.agreementWidgetName, "Answers");
},
learnMoreLabel() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
learnMoreText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
return this.getCmsContent(this.modalWidgetName, "BodyText");
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
selectedValues: {
get: function () {
return this.isRecalAcknowledged;
},
set: function (newValue) {
this.isRecalAcknowledged = this.answersFromCms.find(
(a) => a.Name === newValue
)?.Text;
},
},
},
components: { modal, buttonQuestion },
};
</script>
<style lang="scss">
#recal-ack-modal-container {
.modal-component {
@include media-breakpoint-up(md) {
.modal-dialog {
left: 0;
align-content: center;
flex-wrap: wrap;
width: 34.875rem;
transform: translate(0, 0);
.modal-content {
border-radius: $border-radius-lg;
}
}
}
.modal-dialog {
.modal-content {
.btn-close {
display: none;
}
.modal-header {
width: 75%;
padding: 0 1.25rem 0 1.25rem;
flex-direction: column;
& > span {
color: $black;
font-size: 1.25rem;
font-weight: normal;
}
.modal-title {
font-size: $font-size-20;
font-weight: $font-weight-normal;
}
}
.modal-body {
padding: 0.875rem 1.25rem 1.875rem 1.25rem;
.modal-body-inner {
ul {
list-style: none;
padding: 1rem;
background-color: #f4f4f4;
font-size: $font-size-14;
}
p {
font-size: 0.875rem;
}
}
.button-inner-wrapper > .col:first-child .list-button-horizontal {
border-bottom-left-radius: 50rem;
border-top-left-radius: 50rem;
}
.col:first-of-type .list-button-horizontal .list-button-horizontal-content,
.list-group:first-of-type
.list-button-horizontal
.list-button-horizontal-content {
border-bottom-left-radius: 50rem;
border-top-left-radius: 50rem;
}
.button-inner-wrapper > .col:last-child .list-button-horizontal {
border-bottom-right-radius: 50rem;
border-top-right-radius: 50rem;
}
.col:last-of-type .list-button-horizontal .list-button-horizontal-content,
.list-group:first-of-type
.list-button-horizontal
.list-button-horizontal-content {
border-bottom-right-radius: 50rem;
border-top-right-radius: 50rem;
}
.learn-more {
padding-bottom: 0;
font-size: 0.875rem;
a {
font-weight: 600;
}
}
.question-text {
margin-top: 0.875rem;
}
}
.modal-footer {
flex-flow: wrap-reverse;
padding-top: 0;
padding-bottom: 2rem;
}
.modal-footer .modal-footer-button:hover {
background-color: $red-1000;
border-color: $red-1100;
}
}
}
}
}
</style>

View file

@ -6,6 +6,12 @@
modalWidgetName="MobileFirstModalWidget"
ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" />
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
groupName="recalAckGroup"
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
ref="recalAckModal" />
<div
class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
@ -190,6 +196,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
@ -216,6 +223,7 @@ import {
RouteCodeFlags,
PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE,
RECAL_ACK_YES,
} from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -242,6 +250,7 @@ import {
isDropOffRouteCode,
getTodayDate,
} from "@/layouts/schedule/helpers/schedule-helper";
import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper";
import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
import { required } from "@/helpers/validation-rules";
@ -430,6 +439,7 @@ export default {
name: "schedule",
data() {
return {
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
selectedDate: this.getSelectedDateFromStore(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
mobilePremiumAppointmentFee: null,
@ -1233,6 +1243,9 @@ export default {
this.isShowMobileFirstAppt && this.hideLoadingModal();
},
getIsRecalAcknowledgedForSchedulingFromStore() {
return store.getters.order.isRecalAcknowledgedForScheduling;
},
getScheduleApiResponse,
getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu;
@ -1351,7 +1364,25 @@ export default {
hideLoadingModal() {
this.$refs.loadingModal.hideModal();
},
async closeRecalAcknowledgementPopup() {
this.showRecalAcknowledgementPopup = false;
},
async forwardButtonAction() {
const showRecalAcknowledgementModal = this.showRecalAcknowledgementModal();
if (
showRecalAcknowledgementModal &&
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
) {
this.showRecalAcknowledgementPopup = true;
return;
}
this.dispatchStoreAction(
storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
this.isRecalAcknowledgedForScheduling,
false
);
this.navigatingForward = true;
var ctu = this.zipCodeCtu;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
@ -1675,7 +1706,27 @@ export default {
this.selectedProvider = this.shopProviderData.shopProviders[0];
}
},
handleZipCodeChange(newZipCode) {
async handleZipCodeChange(newZipCode) {
// if we were showing the recal ack modal and now are changing the zip, we need to navigate
// to service-zip so we can get call parts again to add the recal part if needed
if (this.shouldShowRecalAckModal()) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
zipCode: newZipCode.zipCode,
state: newZipCode.state,
zipCodeCtu: newZipCode.zipCodeCtu,
},
false
);
this.$router.navigateWithSaving(
this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
this.pageName
);
return;
}
this.resetMobileLocation();
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
@ -1752,6 +1803,32 @@ export default {
setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate();
},
showRecalAcknowledgementModal() {
if (this.shouldShowRecalAckModal()) {
this.$refs.navbar.removeLoader();
this.$refs.recalAckModal.openModal();
return true;
}
this.closeRecalAcknowledgementPopup();
return false;
},
shouldShowRecalAckModal() {
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
const order = this.$store.getters.order;
const containsRecalPart = containsRecalParts(order.lineItems);
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(order.lineItems);
if (
!containsRecalPart &&
hasPartWithRecalRequirement &&
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
) {
return true;
}
return false;
},
async showMobileFirstModal() {
if (!this.selectedRouteCodeData?.routeCode) {
const pmMobileDays = experimentMixin.methods.getSettingValue(
@ -1875,6 +1952,10 @@ export default {
this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction();
},
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
},
},
watch: {
appointmentTypeFromAppointmentTypeQuestion: {
@ -1920,7 +2001,7 @@ export default {
textBlock,
timeSlotQuestion,
mobileFirstModal,
recalAckModal,
alert,
serviceZipModalQuestion,
appointmentTypeQuestion,

View file

@ -57,6 +57,7 @@ const navigationScenarios = {
// Schedule
CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION",
ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK",
// Payment
CLICKED_INSURANCE: "CLICKED_INSURANCE",

View file

@ -486,6 +486,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
destinationPageData: routeData.MOBILE_DETAILS,
},
{
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
destinationPageData: routeData.SERVICE_ZIP,
},
],
},
{

View file

@ -190,6 +190,7 @@ const getDefaultState = () => {
lockToken: null,
settledTenderAmount: 0,
isRecalAckOptIn: false,
isRecalAcknowledgedForScheduling: "",
isMSRFeeApplicable: false,
},
applicationUser: {
@ -343,6 +344,9 @@ export const mutations = {
updateIsRecalAckOptIn(state, isRecalAckOptIn) {
state.order.isRecalAckOptIn = isRecalAckOptIn;
},
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
},
updateIsOemGlassSelected(state, isOemGlassSelected) {
state.order.damage.installOemGlass = isOemGlassSelected;
},
@ -2965,6 +2969,13 @@ export const actions = {
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn);
},
saveIsRecalAcknowledgedForScheduling(context, isRecalAcknowledgedForScheduling) {
context.commit(
storeMutations.UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
isRecalAcknowledgedForScheduling
);
},
saveIsOemGlassSelected(context, isOemGlassSelected) {
context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected);
},

View file

@ -27,6 +27,8 @@ $red-600: #ac160b;
$red-700: #840900;
$red-800: #5b0600;
$red-900: #330300;
$red-1000: #a80019;
$red-1100: #791710;
// Greens
$green-100: #e3f2ea;