Merge branch 'develop' into rlsmerge/2025.04.24-to-develop

This commit is contained in:
CarlNation 2025-04-23 08:53:09 -04:00
commit ee3b6518c3
23 changed files with 715 additions and 1285 deletions

View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.65567 6.44287H13.35" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.4256 2V3.97449" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.57864 2V3.97449" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5427 2.94792H5.4625C3.70011 2.94792 2.60001 3.92933 2.60001 5.73323V11.1637C2.60001 12.9962 3.70011 14.0003 5.4625 14.0003H10.5375C12.3051 14.0003 13.4001 13.0137 13.4001 11.2091V5.73323C13.4052 3.92933 12.3103 2.94792 10.5427 2.94792Z" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 748 B

View file

@ -0,0 +1,6 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.15567 6.44287H13.85" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.9256 2V3.97449" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.07864 2V3.97449" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0427 2.94791H5.9625C4.20011 2.94791 3.10001 3.92931 3.10001 5.73321V11.1637C3.10001 12.9962 4.20011 14.0003 5.9625 14.0003H11.0375C12.8051 14.0003 13.9001 13.0137 13.9001 11.2091V5.73321C13.9052 3.92931 12.8103 2.94791 11.0427 2.94791Z" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 748 B

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12C3 16.9699 7.02908 21 12 21C16.9709 21 21 16.9699 21 12C21 7.02908 16.9709 3 12 3C7.02908 3 3 7.02908 3 12Z" stroke="#0070D1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.0057 15.6932V11.3936M12 8.35426V8.29102" stroke="#0070D1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 13.8143H13.5894" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.20013 2.88802L3.08796 7.00022C2.09364 7.99456 2.17407 9.32289 3.17099 10.3198L5.86013 13.0096C6.85706 14.0065 8.18213 14.0896 9.18039 13.092L13.2919 8.98042C14.2901 7.98222 14.2065 6.6571 13.2095 5.66018L10.5197 2.97039C9.52286 1.97348 8.19773 1.89046 7.20013 2.88802Z" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.6315 10.782L10.9809 3.4326" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.51559 11.2542L8.72786 10.042" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 800 B

View file

@ -0,0 +1,6 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 13.8143H14.0894" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.70014 2.88802L3.58797 7.00022C2.59365 7.99456 2.67408 9.32289 3.671 10.3198L6.36014 13.0096C7.35707 14.0065 8.68214 14.0896 9.68041 13.092L13.7919 8.98042C14.7901 7.98222 14.7065 6.6571 13.7095 5.66018L11.0197 2.97039C10.0229 1.97348 8.69774 1.89046 7.70014 2.88802Z" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.13152 10.782L11.4809 3.43262" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.01558 11.2543L9.22785 10.042" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 802 B

View file

@ -0,0 +1,26 @@
const pagePercentageMapper = {
vehicle: 4,
"vehicle-damage": 16,
estimate: 28,
"service-zip": 32,
"vin-lookup": 32,
"license-plate-lookup": 32,
"address-lookup": 32,
"address-vehicles": 36,
"part-questions": 40,
"molding-questions": 40,
"vehicle-parts": 40,
"capability-questions": 40,
quote: 48,
"insurance-company": 52,
"service-location": 60,
schedule: 72,
"customer-details": 84,
"payment-method": 92,
payment: 96,
confirmation: 100,
};
export const getProgressBarPercentage = (page) => {
return pagePercentageMapper[page] || 0;
};

View file

@ -1,5 +1,7 @@
<template>
<div class="date-picker text-center" :class="calendarViewDirection">
<div
class="date-picker text-center"
:class="`${calendarViewDirection} ${isPricingByDayClass} ${showPricingByDayClass}`">
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Select a day and time</legend>
<div
@ -60,18 +62,24 @@
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
date.dayClasses,
date.isSelectable ? 'selectable-day' : '',
date.isPricingByDayUpchargeDay ? 'upch-day' : '',
]">
<input
:disabled="!date.isSelectable"
type="radio"
name="day-of-month"
v-model="selectedDate"
@click="fireDateSelectedEvent"
@keypress.enter="fireDateSelectedEvent"
@click="fireDateSelectedEvent($event, date)"
@keypress.enter="fireDateSelectedEvent($event, date)"
:value="date.inputValue"
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
<span>{{ date.dateNum.toString() }}</span>
<span
v-if="isPricingByDayExperiment && showPricingByDay && date.isSelectable"
class="price">
{{ date.priceString }}
</span>
</label>
</div>
</div>
@ -99,8 +107,17 @@
// Supporting files
import loader from "@/ux-components/loader/loader";
import store from "@/store";
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
import {
TIMINGFUNC_MAP,
BUFFER_OFFSET,
MONTHS_OF_YEAR,
DAYS_OF_WEEK,
} from "@/digital-components/date-picker/mixins/constants";
import {
selectableDaysOptions,
requiredParameter,
forceTwoDigitString,
} from "@/digital-components/date-picker/mixins/helpers";
import {
convertDateToDateString,
convertDateStringToDate,
@ -148,6 +165,10 @@ export default {
type: String,
default: "",
},
showPricingByDay: Boolean,
pricingByDayBasePrice: Number,
pricingByDayUpcharge: Number,
isPricingByDayExperiment: Boolean,
},
setup(props) {
const uuid = uuidv4();
@ -199,6 +220,12 @@ export default {
if (this.selectableDatesSetting === "custom") return "future";
return "past";
},
isPricingByDayClass() {
return this.isPricingByDayExperiment ? "pricing-by-day" : "";
},
showPricingByDayClass() {
return this.showPricingByDay ? "show-pricing-by-day" : "";
},
selectedDate: {
get() {
return this.modelValue;
@ -212,12 +239,12 @@ export default {
initializeComponent(initialData) {
this.setCalendarData(initialData);
},
fireDateSelectedEvent(event) {
fireDateSelectedEvent(event, date) {
// Ignore if arrow key selected radioButton
if (event.screenX === 0 && event.screenY === 0) {
return;
}
this.$emit("date-clicked");
this.$emit("date-clicked", date);
},
getWeekStartDate(dateString) {
const date = convertDateStringToDate(dateString);
@ -440,6 +467,8 @@ export default {
initialViewEndDate: config.initialViewEndDate,
hideSecondMonth: hideSecondMonth,
preSelectedDate: config.preSelectedDate,
pricingByDayBasePrice: config.pricingByDayBasePrice,
pricingByDayUpcharge: config.pricingByDayUpcharge,
};
if (direction === "future") {
// first 0, then 1
@ -573,6 +602,17 @@ export default {
("0" + monthNum).slice(-2) +
"-" +
("0" + i).slice(-2);
const dayIndex = convertDateStringToDate(dateString).getDay();
const dayObject = DAYS_OF_WEEK[dayIndex];
const isSelectable =
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
? true
: false;
const isPricingByDayUpchargeDay = dayObject.isPricingByDayUpchargeDay;
const displayPrice = isPricingByDayUpchargeDay
? options.pricingByDayBasePrice + options.pricingByDayUpcharge
: options.pricingByDayBasePrice;
const priceString = "$" + displayPrice;
if (offset === 0 && i === this.todayDateNum) {
dayClasses += " current-day";
@ -583,8 +623,8 @@ export default {
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
dayClasses += " unavailable-day";
}
if (convertDateStringToDate(dateString).getDay() === 0) {
dayClasses += " sunday";
if (dayIndex === 0) {
dayClasses += " " + dayObject.cssClass;
}
if (
this.hideSomeDaysForInitialView &&
@ -598,10 +638,9 @@ export default {
dateNum: i,
dayClasses: dayClasses,
inputValue: dateString,
isSelectable:
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
? true
: false,
priceString: priceString,
isSelectable: isSelectable,
isPricingByDayUpchargeDay: isPricingByDayUpchargeDay,
};
dates.push(dateObject);
}
@ -897,7 +936,6 @@ export default {
}
}
&:hover,
&:checked {
@include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px transparent;
@ -924,9 +962,7 @@ export default {
&.selectable-day {
label {
color: $blue;
background-color: $blue-100;
border: 1px solid $blue;
min-width: 2.5rem;
width: 2.5rem;
border-radius: 50%;
@ -934,12 +970,11 @@ export default {
}
&.unavailable-day {
label {
color: $gray-500;
background-color: $gray-100;
border: none;
pointer-events: none;
}
}
//NEEDED?
&.unavailable-day:not(&.sunday) {
label {
&::before {
@ -949,7 +984,6 @@ export default {
left: -1rem;
width: 1rem;
height: 2.5rem;
background: $gray-100;
}
}
}
@ -1056,7 +1090,107 @@ export default {
}
}
}
// pricing by day override styles
&.pricing-by-day {
.calendar-grid-container .grid-item {
margin: 0 0 0.15rem 0;
}
.month-year {
grid-area: 1 / 1 / 2 / 8;
}
.legend {
display: none;
}
.radio-wrapper {
align-items: flex-start;
width: 100%;
height: 2.5rem;
color: $black;
input[type="radio"] {
&:focus-visible + label {
box-shadow: none;
}
&:focus + label,
&:checked:focus + label {
box-shadow: none;
background-color: $white;
color: $black;
}
&:checked + label {
background: $blue-100;
border: 2px solid $blue;
border-radius: 0.25rem;
color: $black;
span {
font-family: AvertaSemibold;
font-weight: 400;
}
}
}
label {
flex-direction: column;
height: 2.5rem;
width: 100%;
font-size: 0.75rem;
line-height: 1.2;
justify-content: center;
padding: 0.25rem;
}
&.selectable-day {
label {
color: $black;
background-color: $blue-100;
min-width: 2.5rem;
width: 2.5rem;
border-radius: 0.25rem;
}
}
&.current-day {
label:after {
margin-top: 0.2rem;
position: relative;
top: 0;
}
}
}
&.show-pricing-by-day {
.radio-wrapper {
&.selectable-day label span {
text-decoration: none;
color: $black;
font-weight: 400;
font-family: "AvertaSemibold";
&.price {
font-family: "AvertaRegular";
font-weight: 400;
}
}
&.upch-day label span.price {
color: $gray-600;
font-weight: 400;
font-family: $font-family-sans-serif;
}
input[type="radio"] {
&:focus + label,
&:checked:focus + label {
color: $gray-600;
}
&:checked + label {
color: $blue;
span {
font-weight: 400;
font-family: "AvertaSemibold";
}
}
}
}
}
}
}
.btn-link {
display: block;
position: relative;

File diff suppressed because it is too large Load diff

View file

@ -32,17 +32,13 @@
linkType="navigation"
text="Cookie preferences"
href="javascript:OneTrust.ToggleInfoDisplay();" />
<textLink
linkType="newWindowLink"
text="Warranty"
href="//www.safelite.com/national-lifetime-warranty"
target="_blank" />
<textLink
linkType="newWindowLink"
text="Notice at collection"
href="https://www.safelite.com/ccpa-privacy-policy"
target="_blank" />
</div>
<p class="mx-3 my-0 pb-1">&copy; 2025 Safelite Group</p>
</div>
</template>
@ -64,8 +60,9 @@ export default {
flex-direction: column;
width: 100%;
background: transparent;
align-items: end;
align-items: center;
margin-top: auto;
padding: 0 0 2rem 0;
.skyline {
height: 96px;
@ -74,22 +71,34 @@ export default {
height: 48px;
}
p {
font-size: 0.875rem;
margin-top: 0.25rem;
@include media-breakpoint-up(md) {
font-size: 1rem;
margin-top: 0.5rem;
}
}
.footer-inner-wrapper {
display: flex;
flex-direction: column;
border-top: 1px solid $gray;
padding: 2rem 0;
padding: 2rem 0 0 0;
width: 100%;
justify-content: center;
align-items: center;
color: $gray-600;
:deep(a) {
text-decoration: none;
&.new-window-link {
margin: 0 0.75rem;
color: $gray-600;
}
&.navigation-link {
margin: 0 0.75rem;
color: $gray-600;
}
}

View file

@ -6,6 +6,7 @@
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
<menuModal />
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
<progressBar />
</div>
<template v-for="globalAlert in globalAlertMessages" :key="globalAlert.id">
<transition name="fade" mode="out-in">
@ -26,6 +27,7 @@ import alert from "@/ux-components/alert/alert";
import eventBus from "@/helpers/event-bus/event-bus";
import { globalEvents } from "@/constants/events";
import menuModal from "@/fmg-components/funnel-header/menu-modal/menu-modal";
import progressBar from "@/fmg-components/funnel-header/progress-bar/progress-bar";
// Constants
const ALERT_DURATION = 3000; // millisecond time to display alert before dismissal
@ -62,6 +64,7 @@ export default {
components: {
alert,
menuModal,
progressBar,
},
mounted() {
// Check if alert event is on the bus

View file

@ -0,0 +1,84 @@
import { shallowMount } from "@vue/test-utils";
import progressBar from "./progress-bar";
import store from "@/store";
describe("progressBar", () => {
test("progress should be 0", () => {
// Arrange
// Act
const wrapper = shallowMount(progressBar, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.progress).toBe(0);
wrapper.unmount();
});
});
describe("progressBar", () => {
test("progress should be 4%", () => {
// Arrange
store.getters = {
applicationUser: {
lastPageVisited: "vehicle",
},
};
// Act
const wrapper = shallowMount(progressBar, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.progress).toBe(4);
wrapper.unmount();
});
});
describe("progressBar", () => {
test("progress should be 48%", () => {
// Arrange
store.getters = {
applicationUser: {
lastPageVisited: "quote",
},
};
// Act
const wrapper = shallowMount(progressBar, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.progress).toBe(48);
wrapper.unmount();
});
});
describe("progressBar", () => {
test("progress should be 100%", () => {
// Arrange
store.getters = {
applicationUser: {
lastPageVisited: "confirmation",
},
};
// Act
const wrapper = shallowMount(progressBar, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.progress).toBe(100);
wrapper.unmount();
});
});
const mockMixin = {
methods: {
getProgress: jest.fn(),
},
};

View file

@ -0,0 +1,60 @@
<template>
<div id="progress-bar-container">
<progress v-if="progress > 0" :value="progress" max="100" v-html="progress + '%'" />
</div>
</template>
<script>
import store from "@/store";
import { getProgressBarPercentage } from "@/constants/progress-bar-mapper";
export default {
name: "progressBar",
data() {
return {
progress: this.getProgress(),
};
},
methods: {
getProgress() {
var pageName = store.getters.applicationUser.lastPageVisited;
return getProgressBarPercentage(pageName);
},
},
};
</script>
<style lang="scss">
#progress-bar-container {
padding: 0.75rem 0;
width: 100%;
progress {
height: 10px;
width: 100%;
/* Firefox */
appearance: none;
background-color: $blue-100;
border: 0;
border-radius: $border-radius-pill;
box-shadow: $box-shadow-inset;
&::-moz-progress-bar {
background-color: $blue;
border-radius: $border-radius-pill;
}
/* Webkit */
-webkit-appearance: none;
&::-webkit-progress-bar {
background-color: $blue-100;
border-radius: $border-radius-pill;
box-shadow: $box-shadow-inset;
}
&::-webkit-progress-value {
background-color: $blue;
border-radius: $border-radius-pill;
}
}
}
</style>

View file

@ -172,7 +172,7 @@ export default {
},
serviceZipCode: this.getServiceZipFromStore(),
carId: this.getCarIdfromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false,
@ -232,8 +232,8 @@ export default {
getCarIdfromStore() {
return store.getters.vehicle.carId;
},
getIsVehicleHeavyTruckFromStore() {
return store.getters.isHeavyTruckVehicle;
getIsVehicleHeavyTruckServiceableFromStore() {
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
},
async findClosestApplicableShops() {
return await this.dispatchStoreActionWithLogging(

View file

@ -56,7 +56,10 @@
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
<textBlock
class="mb-5"
cmsWidgetName="DisclaimerCopyWidget"
typeStyle="caption" />
</div>
</div>
</div>

View file

@ -203,3 +203,9 @@ export default {
},
};
</script>
<style lang="scss">
.ui-autocomplete {
z-index: 3;
}
</style>

View file

@ -76,7 +76,6 @@
:manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning"
v-bind:isDismissible="false" />
<alert
ref="AlertNoService"
v-if="displayNoServiceAlert"
@ -90,7 +89,7 @@
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
@ForwardClicked="forwardButtonAction || displayNoServiceAlert" />
</div>
</div>
</div>
@ -167,7 +166,7 @@ export default {
emailOrSms: this.getEmailOrSmsFromStore(),
serviceZipCode: this.getServiceZipFromStore(),
carId: this.getCarIdfromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false,
@ -214,8 +213,8 @@ export default {
getCarIdfromStore() {
return store.getters.vehicle.carId;
},
getIsVehicleHeavyTruckFromStore() {
return store.getters.isHeavyTruckVehicle;
getIsVehicleHeavyTruckServiceableFromStore() {
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
},
async findClosestApplicableShops() {
return await this.dispatchStoreActionWithLogging(

View file

@ -0,0 +1,259 @@
<template>
<div class="alert fade show my-2 py-2 border-0 alert-info" role="alert">
<component :is="'script'" src="https://js.afterpay.com/afterpay-1.x.js" async></component>
<div class="my-4 alert-heading text-center">
<span v-html="afterpayHeaderCopy" />
&nbsp;
<span class="afterpay-amount">{{ this.afterpayPrice }}</span>
<br />
<span class="alert-sub-heading" v-html="afterpayHeaderSubHeaderCopy" />
&nbsp;
<img :src="afterPayImageUrl" />
&nbsp;
<a
id="afterpay-learnmore"
href="#"
data-afterpay-modal="en_US-safelite"
data-bind="click:afterpayLearnMore">
<img :src="infoIcon" alt="Info Icon" class="info-icon" />
</a>
<div class="after-pay">
<div
class="after-pay-toggle"
:class="[isExpanded ? 'expanded' : '']"
@click="toggleIsExpanded">
<a href="#!" v-html="paymentPlanToggleHeading" />
</div>
<div class="after-pay-details">
<div class="payment-section">
<div class="payment-card">
<div class="calendar-section">
<div class="icon"><img :src="calendarIcon" /></div>
<div>Today</div>
</div>
<div class="payment-amount-section">
<div class="icon"><img :src="paymentIcon" /></div>
<div class="amount-due">{{ this.afterpayPrice }}</div>
</div>
</div>
<div class="payment-card">
<div class="calendar-section">
<div class="icon"><img :src="calendarIcon" /></div>
<div>2 weeks</div>
</div>
<div class="payment-amount-section">
<div class="icon"><img :src="paymentIcon" /></div>
<div class="amount-due">{{ this.afterpayPrice }}</div>
</div>
</div>
<div class="payment-card">
<div class="calendar-section">
<div class="icon"><img :src="calendarIcon" /></div>
<div>4 weeks</div>
</div>
<div class="payment-amount-section">
<div class="icon"><img :src="paymentIcon" /></div>
<div class="amount-due">{{ this.afterpayPrice }}</div>
</div>
</div>
<div class="payment-card">
<div class="calendar-section">
<div class="icon"><img :src="calendarIcon" /></div>
<div>6 weeks</div>
</div>
<div class="payment-amount-section">
<div class="icon"><img :src="paymentIcon" /></div>
<div class="amount-due">{{ this.afterpayPrice }}</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<img :src="calendarKeyIcon" />
<span>Payment Schedule</span>
</div>
<div class="legend-item">
<img :src="paymentKeyIcon" />
<span>Amount Due</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "afterpay-breakout",
props: {
cmsWidgetName: String,
totalAmount: Number,
},
data() {
return { isExpanded: false };
},
methods: {
toggleIsExpanded() {
this.isExpanded = !this.isExpanded;
},
},
computed: {
afterPayImageUrl() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
hasImage() {
return !!this.imageUrl;
},
afterpayHeaderCopy() {
// Text for Afterpay Breakout experiment
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
afterpayHeaderSubHeaderCopy() {
// Text for Afterpay Breakout experiment
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
afterpayPrice() {
const adjusted = (this.totalAmount / 4).toFixed(2);
return `$${adjusted}`;
},
infoIcon() {
return require(`@/assets/img/icons/info-circle.svg`);
},
calendarIcon() {
return require(`@/assets/img/icons/calendar-icon-black.svg`);
},
paymentIcon() {
return require(`@/assets/img/icons/payment-icon-green.svg`);
},
calendarKeyIcon() {
return require(`@/assets/img/icons/calendar-icon-grey.svg`);
},
paymentKeyIcon() {
return require(`@/assets/img/icons/payment-icon-grey.svg`);
},
paymentPlanToggleHeading() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
},
};
</script>
<style lang="scss" scoped>
.alert {
border-radius: 5px;
margin-top: 1rem;
&.alert-info {
.alert-heading {
color: #000000;
font-weight: 600;
margin-left: 0.5rem;
margin-right: 0.5rem;
@include media-breakpoint-up(md) {
margin-left: 1rem;
margin-right: 1rem;
}
}
}
.afterpay-amount {
color: #0c7e47;
}
.alert-sub-heading {
color: #4b4c4e;
font-weight: 400;
}
.after-pay {
.after-pay-toggle {
cursor: pointer;
&:after {
content: "";
transition: all 0.5s ease;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.9' xml:space='preserve'%3e%3cpath d='M8 8.9c-.2 0-.5-.1-.6-.3L.3 1.5C.1 1.4 0 1.1 0 .9 0 .7.1.4.3.3.4.1.7 0 .9 0c.2 0 .5.1.6.3L8 6.7 14.5.2c.1-.1.4-.2.6-.2.2 0 .5.1.6.3s.3.4.3.6c0 .2-.1.5-.3.6L8.6 8.6c-.1.2-.4.3-.6.3z' fill='%231474a2'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right center;
margin-left: 0.5rem;
width: 1rem;
height: 9px;
display: inline-flex;
}
&.expanded:after {
transform: rotate(180deg);
}
&.expanded + .after-pay-details {
max-height: 500px;
transition: all 250ms ease-in;
opacity: 1;
visibility: visible;
}
}
.after-pay-toggle a {
font-weight: 600;
color: #0070d1;
text-decoration: none;
}
.after-pay-details {
max-height: 0;
transition: all 250ms ease-out;
overflow: hidden;
opacity: 0;
visibility: hidden;
}
}
.payment-section {
display: flex;
justify-content: space-around;
gap: 0.5rem;
margin-top: 1rem;
@include media-breakpoint-up(md) {
gap: 0rem;
}
}
.payment-card {
display: flex;
flex-direction: column;
border-radius: 6px;
border: 0.25px #b3b4b5;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.calendar-section {
background-color: #fff; /* White background for calendar area */
padding: 0.5rem;
text-align: center;
gap: 2px;
font-weight: 700;
white-space: nowrap;
}
.payment-amount-section {
background-color: #e0f7e9; /* Light green background for payment area */
padding: 0.5rem;
text-align: center;
font-weight: 700;
}
.amount-due {
color: #00723e; /* Green text */
}
.legend {
display: flex;
justify-content: center;
margin-top: 1rem;
gap: 1rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 2px;
white-space: nowrap;
}
.legend-item span {
font-weight: 400;
}
}
</style>

View file

@ -117,6 +117,12 @@ function setupMocks() {
return "false";
}),
hasSettingEqualTo: jest.fn((settingName) => {
if (settingName === experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
return true;
}
return false;
}),
getCmsContent: jest.fn().mockImplementation(() => {
return "test text";
}),

View file

@ -39,6 +39,10 @@
:isExpandedOnLoad="false"
:isMSRFeeApplicable="isMSRFeeApplicable" />
<afterpayBreakout
v-if="isAfterpayBreakoutDisplay"
cmsWidgetName="AfterpayBreakoutAlertWidget"
:totalAmount="totalAmountDue" />
<hr />
<div>
@ -118,6 +122,7 @@ import alert from "@/ux-components/alert/alert";
import textBlock from "@/digital-components/text-block/text-block";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import afterpayBreakout from "@/layouts/payment-method/afterpay-breakout/afterpay-breakout";
// Supporting Items
import baseMixin from "@/mixins/base-mixin.js";
@ -131,7 +136,6 @@ import {
} from "@/helpers/cms-content-helper";
import { paymentMethods } from "@/constants/payment-method-constants";
import { experimentSettings } from "@/constants/experiments";
import experimentMixin from "@/mixins/experiment-mixin.js";
import {
revalidatePromosAndValidateQueryStringPromo,
buildToastMessagesFromRevalidateOrValidatePromoResponse,
@ -805,6 +809,18 @@ export default {
lineItemsCloneForWatcher() {
return Object.assign({}, this.lineItems);
},
isRecalPriceRemove() {
return (
this.getSettingValue(experimentSettings.RECAL_PRICE_REMOVE)?.toLowerCase() ===
"true"
);
},
isAfterpayBreakoutDisplay() {
return (
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) &&
this.hasSettingEqualTo(experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY, "true")
);
},
},
watch: {
customCtaCopy(newValue) {
@ -856,6 +872,7 @@ export default {
textBlock,
checkboxQuestion,
contentGroupModal,
afterpayBreakout,
},
};
</script>

View file

@ -275,8 +275,8 @@ export default {
const emailFromStore = store.getters.order.customer.emailAddress;
const isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
const isPopupSkipped =
store.getters.pageData(fmgPageValues.QUOTE).saveProgressPopupSkipped === true;
const showSaveProgressPopup = isEmailInStoreOnPageLoad || isPopupSkipped ? false : true;
store.getters.pageData(fmgPageValues.QUOTE)?.saveProgressPopupSkipped === true;
const showSaveProgressPopup = !(isEmailInStoreOnPageLoad || isPopupSkipped);
const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true;
const shouldSkipToInsurance =
getBoolFromString(
@ -891,7 +891,7 @@ export default {
&.funnel-header-wrapper,
&.save-progress-popup {
display: block;
display: flex;
}
}

View file

@ -18,7 +18,7 @@
marginTopSizeOverride="1" />
</template>
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePickerForPricingByDay
<datePicker
customComponentId="dateQuestion"
selectableDatesSetting="custom"
ref="datePicker"
@ -77,7 +77,7 @@ import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate";
import datePickerForPricingByDay from "@/experiment-components/date-picker-for-pricing-by-day";
import datePicker from "@/digital-components/date-picker/date-picker";
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
import textBlock from "@/digital-components/text-block/text-block";
@ -298,14 +298,13 @@ export default {
);
// While Pricing By Day Experiment is active, using the updated datePicker
const datePickerInitialDataPromise =
await datePickerForPricingByDay.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedDate,
});
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedDate,
});
// Get pricingByDayUpcharge needed for Pricing By Day
const pricingByDayUpchargePartPromise = showPricingByDay
@ -735,7 +734,7 @@ export default {
funnelSubHeader,
Form,
loadingModal,
datePickerForPricingByDay,
datePicker,
locationAlerts,
timeSlotModalQuestion,
textBlock,

View file

@ -119,7 +119,7 @@ export default {
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
emailOrSms: this.getEmailOrSmsFromStore(),
carId: this.getCarIdfromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
displayInvalidZipAlert: false,
displayNonServiceableZipAlert: false,
displayNoServiceAlert: false,
@ -198,8 +198,8 @@ export default {
getCarIdfromStore() {
return store.getters.vehicle.carId;
},
getIsVehicleHeavyTruckFromStore() {
return store.getters.isHeavyTruckVehicle;
getIsVehicleHeavyTruckServiceableFromStore() {
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
},
async findClosestApplicableShops() {
return await this.dispatchStoreActionWithLogging(
@ -208,6 +208,7 @@ export default {
"service-zip"
);
},
async backButtonAction() {
const skipVin = await skipVinLookup();
// route to move backwards
@ -286,6 +287,16 @@ export default {
return this.$refs.navbar.removeLoader();
}
}
if (this.isHeavyTruck) {
const closestShops = await this.findClosestApplicableShops(
this.serviceZipCode,
this.carId
);
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
if (this.displayNoServiceAlert) {
return this.$refs.navbar.removeLoader();
}
}
const payment = this.$store.getters.payment;
const policy = this.$store.getters.policy;
@ -344,6 +355,7 @@ export default {
serviceZipCode() {
this.displayNonServiceableZipAlert = false;
this.displayNoServiceAlert = false;
this.displayNoServiceAlert = false;
},
},
components: {

View file

@ -208,7 +208,7 @@ export default {
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
emailOrSms: this.getEmailOrSmsFromStore(),
carId: this.getCarIdfromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
isCarIdDifferent: false,
customAlertData: {},
previouslyEnteredCarId: "",
@ -264,6 +264,9 @@ export default {
);
}
},
getIsVehicleHeavyTruckServiceableFromStore() {
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
},
async findClosestApplicableShops() {
return await this.dispatchStoreActionWithLogging(
storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
@ -366,6 +369,18 @@ export default {
},
false
);
if (this.isHeavyTruck) {
closestShops = await this.findClosestApplicableShops(
this.serviceZipCode,
this.carId
);
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
if (this.displayNoServiceAlert) {
return this.$refs.navbar.removeLoader();
}
}
// if no value due to field being optional, blank both phone and email address
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
@ -430,16 +445,6 @@ export default {
false
);
}
closestShops = await this.findClosestApplicableShops(
this.serviceZipCode,
this.carId
);
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
if (this.displayNoServiceAlert) {
return this.$refs.navbar.removeLoader();
}
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
@ -523,6 +528,7 @@ export default {
this.displayVinNotFoundAlert = false;
this.displayVinScanFailedAlert = false;
this.displayNoServiceAlert = false;
this.displayNoServiceAlert = false;
},
},
mounted() {