Merge branch 'release/2025.07.10' into rlsmerge/2025.07.10-to-dev

This commit is contained in:
CarlNation 2025-06-26 11:39:53 -04:00
commit 6ed0ee47f6
9 changed files with 129 additions and 84 deletions

View file

@ -26,6 +26,7 @@ const experimentSettings = {
FOSTER_LOVE: "DisplayFosterLove", FOSTER_LOVE: "DisplayFosterLove",
SKIP_TO_INSURANCE: "SkipToInsurance", SKIP_TO_INSURANCE: "SkipToInsurance",
DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay", DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay",
AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD: "AfterPayExtendedPayOptionThreshold",
DYNAMO_LOGGING: "DynamoLogging", DYNAMO_LOGGING: "DynamoLogging",
}; };

View file

@ -339,3 +339,7 @@ export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex // filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== ""); return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
} }
export function splitCMSCopyOnBR(copy) {
return copy.split("<br>");
}

View file

@ -82,6 +82,8 @@
<span>Amount Due</span> <span>Amount Due</span>
</div> </div>
</div> </div>
<p>6 or 12 monthly payment plans available</p>
</div> </div>
</div> </div>
</div> </div>
@ -208,6 +210,12 @@ export default {
overflow: hidden; overflow: hidden;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
> p {
padding-top: 1rem;
font-size: $font-size-12;
margin: 0;
}
} }
} }
.payment-section { .payment-section {

View file

@ -1,13 +1,14 @@
<template> <template>
<div class="alert fade show my-2 py-2 border-0 alert-info" role="alert"> <div id="afterpay-banner" role="alert">
<component <component
:is="'script'" :is="'script'"
src="https://js.squarecdn.com/square-marketplace.js" src="https://js.squarecdn.com/square-marketplace.js"
async></component> async></component>
<div <div>
class="mx-4 my-0 alert-heading text-center" <span v-for="token in headerCopyTokens" :key="token" v-html="token"></span>
:class="[isAfterpayBreakoutDisplay ? 'fw-bolder' : 'fw-bold']"> </div>
<div :id="showAfterpayExtendedPayOption ? 'extended-pay-option' : ''">
<span v-for="token in afterpayCopyTokens" :key="token"> <span v-for="token in afterpayCopyTokens" :key="token">
<span v-if="isAfterpayPriceToken(token)">{{ afterpayPrice }}</span> <span v-if="isAfterpayPriceToken(token)">{{ afterpayPrice }}</span>
<span v-else-if="isInlineImageToken(token)"> <span v-else-if="isInlineImageToken(token)">
@ -29,7 +30,7 @@
</template> </template>
<script> <script>
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper"; import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper"; import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper";
import { getArrayOfAllLineItemsAndChildParts } from "@/store"; import { getArrayOfAllLineItemsAndChildParts } from "@/store";
@ -55,8 +56,8 @@ export default {
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
lineItems: Array, lineItems: Array,
isAfterpayBreakoutDisplay: Boolean,
isInsuranceSelected: Boolean, isInsuranceSelected: Boolean,
afterpayExtendedPayOptionThreshold: Number,
}, },
data() { data() {
return {}; return {};
@ -73,27 +74,24 @@ export default {
hasImage() { hasImage() {
return !!this.imageUrl; return !!this.imageUrl;
}, },
headerCopyTokens() {
return splitCMSCopyOnBR(this.getCmsContent(this.cmsWidgetName, "HeaderText"));
},
afterpayCopyTokens() { afterpayCopyTokens() {
if (this.isAfterpayBreakoutDisplay) { if (this.showAfterpayExtendedPayOption) {
// Text for Afterpay Breakout experiment return splitCopyOnCMSPlaceHolder(
var afterpayBannerCopy = this.getCmsContent(this.cmsWidgetName, "BodyText2"); this.getCmsContent(this.cmsWidgetName, "BodyText")
afterpayBannerCopy = );
afterpayBannerCopy &&
afterpayBannerCopy.replaceAll(
"{custom:paymentType}",
this.isInsuranceSelected ? "deductible" : "spending"
);
return splitCopyOnCMSPlaceHolder(afterpayBannerCopy);
} else { } else {
return splitCopyOnCMSPlaceHolder( return splitCopyOnCMSPlaceHolder(
this.getCmsContent(this.cmsWidgetName, "HeaderText") this.getCmsContent(this.cmsWidgetName, "BodyText2")
); );
} }
}, },
modalCopy() { modalCopy() {
return this.getCmsContent(this.cmsWidgetName, "BodyText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
afterpayPrice() { getTierOnePackagePrice() {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
if (this.lineItems.promos) { if (this.lineItems.promos) {
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT"); allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT");
@ -119,38 +117,76 @@ export default {
}); });
} }
return price;
},
afterpayPrice() {
let price = this.getTierOnePackagePrice;
const adjusted = (price / 4).toFixed(2); const adjusted = (price / 4).toFixed(2);
return `$${adjusted}`; return `$${adjusted}`;
}, },
showAfterpayExtendedPayOption() {
return (
this.afterpayExtendedPayOptionThreshold &&
this.getTierOnePackagePrice >= this.afterpayExtendedPayOptionThreshold
);
},
}, },
components: {}, components: {},
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.alert { #afterpay-banner {
padding: 0.675rem 0; display: flex;
&.alert-info { flex-direction: column;
background-color: $blue-100; background-color: $blue-150;
.alert-heading { justify-content: center;
color: $blue-700; align-items: center;
padding: 1rem;
border-radius: 1rem;
@include media-breakpoint-up(md) {
flex-direction: row;
}
> div:first-of-type {
display: flex;
color: $black;
font-size: $font-size-20;
font-weight: $font-weight-600;
line-height: 2rem;
@include media-breakpoint-down(md) {
border-bottom: 1px solid;
padding-bottom: 1rem;
width: 100%;
justify-content: center;
> span:first-of-type {
margin-right: 0.25rem;
}
} }
&.fw-bolder { @include media-breakpoint-up(md) {
font-weight: 600; flex-direction: column;
} border-right: 1px solid;
img { padding-right: 1rem;
vertical-align: text-top;
}
svg {
fill: $blue-700;
width: 1rem;
height: 1rem;
} }
} }
& .alert-heading {
font-size: 0.875rem; > div:last-of-type {
color: $gray-650;
text-align: left;
@include media-breakpoint-down(md) {
padding-top: 1rem;
}
@include media-breakpoint-up(md) {
padding-left: 1rem;
&#extended-pay-option {
max-width: 28rem;
}
}
} }
#afterpay-learnmore { #afterpay-learnmore {
white-space: nowrap; white-space: nowrap;
} }

View file

@ -46,7 +46,7 @@
<afterpayModalBanner <afterpayModalBanner
v-if="showAfterpayBanner" v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget" cmsWidgetName="AfterpayModalWidget"
:isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay" :afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
:isInsuranceSelected="isInsuranceSelected" :isInsuranceSelected="isInsuranceSelected"
:lineItems="lineItems" /> :lineItems="lineItems" />
@ -591,10 +591,7 @@ export default {
); );
}, },
showAfterpayBanner() { showAfterpayBanner() {
return ( return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
(this.isAfterpayBreakoutDisplay || !this.isInsuranceSelected) &&
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
);
}, },
isRecalPriceRemove() { isRecalPriceRemove() {
return ( return (
@ -606,14 +603,12 @@ export default {
showRecalDisclaimer() { showRecalDisclaimer() {
return this.isRecalibrationOnOrder && this.isRecalPriceRemove; return this.isRecalibrationOnOrder && this.isRecalPriceRemove;
}, },
isAfterpayBreakoutDisplay() { afterpayExtendedPayOptionThreshold() {
return ( let thresholdAmount = experimentMixin.methods.getSettingValue(
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) && experimentSettings.AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
"true"
)
); );
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
return thresholdAmount;
}, },
}, },
methods: { methods: {

View file

@ -12,10 +12,7 @@
buttonTypeString="timeSlotModalListButton" buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeSlotModalListButton" :buttonTypeObject="timeSlotModalListButton"
class="mt-4" class="mt-4"
:class="[ :class="[isMobileAppointment ? 'is-mobile-appointment' : '']"
isMobileAppointment ? 'is-mobile-appointment' : '',
isDropOffAppointment ? 'is-drop-off-appointment' : '',
]"
:answers="availableTimeSlots" :answers="availableTimeSlots"
groupName="chooseTimeSlot" groupName="chooseTimeSlot"
textPosition="text-center" textPosition="text-center"
@ -97,7 +94,7 @@ const cmsWidgetFieldMappings = {
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
export default { export default {
name: "time-slot-no-modal-question", name: "time-slot-question",
emits: ["update:modelValue", "TimeSlotSelected", "click-event"], emits: ["update:modelValue", "TimeSlotSelected", "click-event"],
props: { props: {
modelValue: { modelValue: {
@ -240,6 +237,9 @@ export default {
); );
}, },
disclaimerTextBlockCopy() { disclaimerTextBlockCopy() {
/* AppointmentTypeStrings.DROP_OFF should never be used per CASH-803 epic
but I'm leaving this in assuming that a future card in this epic
will handle disclaimers in a different way */
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) { if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) { if (this.isSameDay) {
@ -329,11 +329,7 @@ export default {
return null; return null;
} }
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return this.getAvailableTimeSlotsForDropOff(
this.timeSlotsForSelectedDate.timeSlots
);
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots); return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots);
} else { } else {
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots); return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots);
@ -357,9 +353,6 @@ export default {
isMobileAppointment() { isMobileAppointment() {
return this.appointmentType === AppointmentTypeStrings.MOBILE; return this.appointmentType === AppointmentTypeStrings.MOBILE;
}, },
isDropOffAppointment() {
return this.appointmentType === AppointmentTypeStrings.DROP_OFF;
},
}, },
methods: { methods: {
async setSelectedTimeSlot() { async setSelectedTimeSlot() {
@ -382,23 +375,20 @@ export default {
} }
}, },
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) { getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
return timeSlotsForSelectedDate.map((timeSlot) => { let dropOffIndex = -1;
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot, index) => {
const readableTime = militaryToTwelveHourTime(timeSlot.startTime); const readableTime = militaryToTwelveHourTime(timeSlot.startTime);
return { let buttonLabelValue = readableTime;
value: timeSlot.id, if (timeSlot.id.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
buttonLabel: readableTime, dropOffIndex = index;
}; if (this.isSameDay) {
}); buttonLabelValue = this.sameDayDropoffButtonText;
}, } else {
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) { buttonLabelValue = this.dropoffButtonText;
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => { }
let buttonLabelValue; } else if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) { dropOffIndex = index;
buttonLabelValue = this.overnightDropoffButtonText; buttonLabelValue = this.overnightDropoffButtonText;
} else if (this.isSameDay) {
buttonLabelValue = this.sameDayDropoffButtonText;
} else {
buttonLabelValue = this.dropoffButtonText;
} }
return { return {
value: timeSlot.id, value: timeSlot.id,
@ -406,6 +396,10 @@ export default {
}; };
}); });
if (dropOffIndex) {
const dropOffTimeSlot = availableTimeSlots.splice(dropOffIndex, 1)[0];
availableTimeSlots.unshift(dropOffTimeSlot);
}
return availableTimeSlots; return availableTimeSlots;
}, },
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) { getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {

View file

@ -132,7 +132,6 @@ export default {
async logDigitalConsumer() { async logDigitalConsumer() {
const currentPageName = getPageNameFromRouter(); const currentPageName = getPageNameFromRouter();
const universes = store.getters.applicationUser.experiments; const universes = store.getters.applicationUser.experiments;
console.log(JSON.stringify(universes));
let hasDynamoLogging = experimentMixin.methods.hasSettingEqualTo( let hasDynamoLogging = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DYNAMO_LOGGING, experimentSettings.DYNAMO_LOGGING,
@ -150,15 +149,15 @@ export default {
const conceptVariation = variationNames.length > 0 ? variationNames[0] : ""; const conceptVariation = variationNames.length > 0 ? variationNames[0] : "";
const isConceptExposed = universes.find(
(item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL
)?.isExposed;
const submittedOrder = baseMixin.methods.getSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder();
const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder();
const hasSubmittedOrderAtConfirmationPage = const hasSubmittedOrderAtConfirmationPage =
hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name; hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name;
const refSequenceNum = hasSubmittedOrderAtConfirmationPage
? submittedOrder.referralSequenceNumber
: store.getters.order.referralSequenceNumber;
var payload = { var payload = {
actionName: `Browser page:${currentPageName}`, actionName: `Browser page:${currentPageName}`,
referralSequenceNumber: hasSubmittedOrderAtConfirmationPage referralSequenceNumber: hasSubmittedOrderAtConfirmationPage
@ -174,6 +173,7 @@ export default {
? submittedOrder.workOrderNumber ? submittedOrder.workOrderNumber
: store.getters.order.workOrderNumber, : store.getters.order.workOrderNumber,
conceptVariation: conceptVariation, conceptVariation: conceptVariation,
isConceptExposed: isConceptExposed,
}; };
await baseMixin.methods.dispatchStoreAction( await baseMixin.methods.dispatchStoreAction(

View file

@ -1570,6 +1570,7 @@ export const actions = {
workOrderId, workOrderId,
workOrderNumber, workOrderNumber,
conceptVariation, conceptVariation,
isConceptExposed,
} }
) { ) {
var payload = { var payload = {
@ -1582,6 +1583,7 @@ export const actions = {
workOrderId: workOrderId ?? "", workOrderId: workOrderId ?? "",
workOrderNumber: workOrderNumber ?? "", workOrderNumber: workOrderNumber ?? "",
conceptVariation: conceptVariation, conceptVariation: conceptVariation,
isConceptExposed: isConceptExposed,
}; };
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({

View file

@ -7,6 +7,7 @@ $black-100: #0a0a0a;
// Blues // Blues
$blue-100: #e4f1f7; // Used in theme $blue-100: #e4f1f7; // Used in theme
$blue-150: #e5f1fa;
$blue-200: #c1dfee; $blue-200: #c1dfee;
$blue-300: #9fcee6; $blue-300: #9fcee6;
$blue-400: #69adcf; // Used in theme $blue-400: #69adcf; // Used in theme
@ -59,6 +60,7 @@ $gray: #b0b3b3; // Default Gray
$gray-500: #8e9292; // Used in theme $gray-500: #8e9292; // Used in theme
$gray-550: #727676; // Used in theme $gray-550: #727676; // Used in theme
$gray-600: #4d5151; // Used in theme $gray-600: #4d5151; // Used in theme
$gray-650: #525656;
$gray-700: #303333; // Used in theme $gray-700: #303333; // Used in theme
$gray-800: #222424; // Used in theme $gray-800: #222424; // Used in theme
$gray-900: #181a1a; // Used in theme $gray-900: #181a1a; // Used in theme
@ -121,13 +123,16 @@ $body-color: $gray-600;
//Fonts //Fonts
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif; $font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", $font-family-monospace:
monospace; UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
// stylelint-enable value-keyword-case // stylelint-enable value-keyword-case
$font-family-base: $font-family-sans-serif; $font-family-base: $font-family-sans-serif;
$font-family-code: $font-family-monospace; $font-family-code: $font-family-monospace;
$font-size-base: 1rem; // Assumes the browser default, typically `16px` $font-size-base: 1rem; // Assumes the browser default, typically `16px`
$font-size-12: $font-size-base * 0.75; // 12px
$font-size-20: $font-size-base * 1.25; // 20px
//Custom Font size (extra small) //Custom Font size (extra small)
$font-size-xsm: $font-size-base * 0.75; $font-size-xsm: $font-size-base * 0.75;
$font-sizes: ( $font-sizes: (