Merge branch 'release/2025.05.22' into rlsmerge/2025.05.22-to-develop

This commit is contained in:
Chloe Herd 2025-05-13 15:54:10 -04:00
commit 7d19e1d931
30 changed files with 512 additions and 277 deletions

View file

@ -23,7 +23,6 @@ const GaActions = {
SUBMITTED: "Submitted", SUBMITTED: "Submitted",
DISPLAYED: "Displayed", DISPLAYED: "Displayed",
CASH_QUOTE_DISPLAYED: "Cash quote displayed", CASH_QUOTE_DISPLAYED: "Cash quote displayed",
SERVICE_PACKAGE_PRICE: "Service package price",
MOBILE_AVAILABLE: "mobile_available", MOBILE_AVAILABLE: "mobile_available",
SHOPS_FIRST_DISPLAYED: "shops_first_displayed", SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
MORE_LOCATIONS_CLICKED: "more_locations_clicked", MORE_LOCATIONS_CLICKED: "more_locations_clicked",

View file

@ -69,6 +69,7 @@ const storeMutations = {
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
ADD_EVENT_TO_BUS: "addEventToBus", ADD_EVENT_TO_BUS: "addEventToBus",

View file

@ -35,4 +35,9 @@ export const inputButtonProps = {
default: false, default: false,
}, },
suppressError: Boolean, suppressError: Boolean,
labelBold: {
type: Boolean,
required: false,
default: false,
},
}; };

View file

@ -9,7 +9,9 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
v-if="questionText && answers && answers.length > 0" v-if="questionText && answers && answers.length > 0"
class="question-text d-flex" class="question-text d-flex"
:class="{ 'small-question-text': isSmallQuestionText }"> :class="{ 'small-question-text': isSmallQuestionText }">
<span class="fw-bold w-100">{{ questionText }}</span> <span class="fw-bold w-100" :class="[labelBold ? 'span-bold' : '']">
{{ questionText }}
</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
@ -57,6 +59,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
:setLastValuePushedToGa="setLastValuePushedToGa" :setLastValuePushedToGa="setLastValuePushedToGa"
:isInsuranceSelected="isInsuranceSelected" :isInsuranceSelected="isInsuranceSelected"
:suppressError="suppressError" :suppressError="suppressError"
:labelBold="labelBold"
@buttonEvent="handleButtonEvent" @buttonEvent="handleButtonEvent"
v-model="selectedValues" /> v-model="selectedValues" />
<!-- For nested questions --> <!-- For nested questions -->
@ -141,6 +144,11 @@ export default {
default: false, default: false,
}, },
isInsuranceSelected: Boolean, isInsuranceSelected: Boolean,
labelBold: {
type: Boolean,
required: false,
default: false,
},
}, },
setup(props) { setup(props) {
const propsClone = Object.assign({}, props); const propsClone = Object.assign({}, props);
@ -360,6 +368,9 @@ export default {
& > span { & > span {
text-align: center; text-align: center;
} }
.span-bold {
font-weight: 600;
}
} }
.small-question-text { .small-question-text {
&.question-text { &.question-text {

View file

@ -1042,6 +1042,9 @@ export default {
min-width: 3rem; min-width: 3rem;
width: 3rem; width: 3rem;
} }
&:hover {
border: 2px solid $blue;
}
} }
} }
&.unavailable-day { &.unavailable-day {
@ -1083,7 +1086,6 @@ export default {
} }
input[type="radio"]:checked + label:after { input[type="radio"]:checked + label:after {
background-color: $black; background-color: $black;
top: 2rem;
} }
} }
} }

View file

@ -6,6 +6,7 @@
:for="dropdownId" :for="dropdownId"
:aria-label="questionText" :aria-label="questionText"
class="form-label" class="form-label"
:class="[labelBold ? 'label-bold' : '']"
v-html="questionText"></label> v-html="questionText"></label>
<select <select
v-model="selectedOption" v-model="selectedOption"
@ -61,6 +62,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
labelBold: {
type: Boolean,
required: false,
default: false,
},
}, },
setup(props) { setup(props) {
const uuid = uuidv4(); const uuid = uuidv4();
@ -133,6 +139,10 @@ export default {
color: $black; color: $black;
font-weight: 500; font-weight: 500;
} }
.label-bold {
color: $black;
font-weight: 600;
}
.form-label { .form-label {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }

View file

@ -119,12 +119,14 @@ export default {
}, },
onModalOpened() { onModalOpened() {
this.onModalOpenedCallback?.(); this.onModalOpenedCallback?.();
this.$emit("isModalOpened", true);
}, },
onModalClosed() { onModalClosed() {
this.resetButtonStyle(); this.resetButtonStyle();
this.onModalClosedCallback?.(); this.onModalClosedCallback?.();
if (this.suppressPageScroll) if (this.suppressPageScroll)
document.querySelector("body").classList.remove("prevent-modal-scroll"); document.querySelector("body").classList.remove("prevent-modal-scroll");
this.$emit("isModalOpened", false);
}, },
openModal() { openModal() {
if (this.suppressPageScroll) if (this.suppressPageScroll)

View file

@ -1,5 +1,7 @@
<template> <template>
<span class="salesforce-webchat"></span> <span class="salesforce-webchat">
<button v-show="hideSalesforceWebchatLaunchButton" class="salesforce-chat-button"></button>
</span>
</template> </template>
<script> <script>
@ -49,12 +51,121 @@ export default {
}; };
</script> </script>
<style type="text/css"> <style lang="scss">
.embeddedServiceHelpButton .helpButton .uiButton { .salesforce-webchat {
background-color: #167cac; .salesforce-chat-button {
font-family: "Arial", sans-serif; width: 1.5rem;
height: 1.5rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' id='Layer_2' x='0' y='0' style='enable-background:new 0 0 24 24' version='1.1' viewBox='0 0 24 24'%3E%3Cstyle%3E .st0%7Bfill:%232e6fb6%7D %3C/style%3E%3Ccircle cx='12' cy='12' r='12' class='st0'/%3E%3Cpath d='M18.3 12c0 3.5-2.8 6.3-6.3 6.3-1.2 0-2.4-.4-3.4-1l-2.3.7s-.5.1-.3-.4.5-1.7.7-2.2c-.6-1-1-2.1-1-3.3 0-3.5 2.8-6.3 6.3-6.3 3.5-.1 6.3 2.7 6.3 6.2z' style='fill:%23fff'/%3E%3Ccircle cx='10.2' cy='12' r='.6' class='st0'/%3E%3Ccircle cx='12' cy='12' r='.6' class='st0'/%3E%3Ccircle cx='13.8' cy='12' r='.6' class='st0'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 1.5rem 1.5rem;
background-position: center;
border: none;
background-color: transparent;
}
} }
.embeddedServiceHelpButton .helpButton .uiButton:focus {
outline: 1px solid #167cac; .dockableContainer {
&.showDockableContainer {
font-family: AvertaRegular;
color: $gray-600;
.sidebarBody {
button {
background: $blue;
color: $white;
&:hover {
background: $blue-700 !important;
}
}
.form-element__help {
color: $red;
}
.embeddedServiceSidebarFormField {
.uiInput {
.uiLabel-left {
color: $gray-600;
}
}
}
.embeddedServiceSidebarButton {
&.uiButton--inverse {
.label {
color: $white;
text-decoration: underline;
}
&:not(:disabled):focus {
background-color: $blue;
}
}
}
.embeddedServiceLiveAgentStateChatAvatar {
&.isLightningOutContext {
.agentIconColor0 {
background-color: $blue;
}
}
}
.embeddedServiceLiveAgentStateChatInputFooter {
.footerMenuWrapper {
.footer-menu-items {
.slds-dropdown__item {
> a {
padding-left: 0;
max-width: 100%;
display: flex;
justify-content: center;
}
}
}
}
}
.embeddedServiceLiveAgentStateChatInputFooter {
.footerMenuWrapper {
.footer-menu-items {
a {
margin: 0 auto;
}
}
}
}
}
.chatHeaderBranding {
background-color: $blue;
}
span {
&.required {
color: $red;
}
}
}
}
.embeddedServiceHelpButton {
.helpButton {
.uiButton {
background-color: $blue !important;
}
}
.embeddedServiceIcon {
display: inline-block !important;
}
}
.modalContainer {
&.sidebarMinimized {
&.layout-docted {
&.embeddedServiceSidebar {
.embeddedServiceSidebarMinimizedDefaultUI {
.helpButton,
.sidebarHeader {
&.minimizedContainer {
.embeddedServiceSidebarMinimizedDefaultUI {
background-color: $blue;
border: 1px solid $blue;
}
}
}
}
}
}
}
} }
</style> </style>

View file

@ -13,6 +13,7 @@
:class="[ :class="[
questionAlignment === 'center' ? 'text-center w-100 mb-5' : '', questionAlignment === 'center' ? 'text-center w-100 mb-5' : '',
hideInput ? 'hide-input' : '', hideInput ? 'hide-input' : '',
labelBold ? 'label-bold' : '',
]" ]"
v-html="questionText"></label> v-html="questionText"></label>
<div <div
@ -132,6 +133,11 @@ export default {
isRequired: false, isRequired: false,
}, },
autocomplete: String, autocomplete: String,
labelBold: {
type: Boolean,
required: false,
default: false,
},
}, },
setup(props) { setup(props) {
const uuid = uuidv4(); const uuid = uuidv4();
@ -251,6 +257,9 @@ export default {
color: $black; color: $black;
font-weight: 500; font-weight: 500;
} }
.label-bold {
font-weight: 600;
}
.input-wrapper { .input-wrapper {
position: relative; position: relative;
&.has-search-icon { &.has-search-icon {

View file

@ -14,7 +14,8 @@
hasIcon hasIcon
validationRules="street-address-required" validationRules="street-address-required"
autocomplete="street-address" autocomplete="street-address"
@keydown.enter.prevent /> @keydown.enter.prevent
:labelBold="labelBold" />
</div> </div>
</div> </div>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@ -27,7 +28,8 @@
customInputId="apartmentNumberOrBusinessName" customInputId="apartmentNumberOrBusinessName"
cmsWidgetName="ApartmentNumberOrBusinessNameQuestionWidget" cmsWidgetName="ApartmentNumberOrBusinessNameQuestionWidget"
v-model="addressModel.apartmentNumberOrBusinessName" v-model="addressModel.apartmentNumberOrBusinessName"
ref="apartmentNumberOrBusinessName" /> ref="apartmentNumberOrBusinessName"
:labelBold="labelBold" />
</div> </div>
</div> </div>
</transition> </transition>
@ -40,7 +42,8 @@
v-model="addressModel.city" v-model="addressModel.city"
ref="city" ref="city"
validationRules="city-required" validationRules="city-required"
autocomplete="address-level2" /> autocomplete="address-level2"
:labelBold="labelBold" />
</div> </div>
</div> </div>
</transition> </transition>
@ -54,7 +57,8 @@
ref="state" ref="state"
:options="stateOptions" :options="stateOptions"
validationRules="state-required" validationRules="state-required"
autocomplete="address-level1" /> autocomplete="address-level1"
:labelBold="labelBold" />
</div> </div>
<div class="col"> <div class="col">
<textboxQuestion <textboxQuestion
@ -64,7 +68,8 @@
ref="zipCode" ref="zipCode"
mask="#####" mask="#####"
validationRules="zip-code-required|zip-code-format" validationRules="zip-code-required|zip-code-format"
autocomplete="postal-code" /> autocomplete="postal-code"
:labelBold="labelBold" />
</div> </div>
</div> </div>
</transition> </transition>
@ -124,6 +129,11 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
labelBold: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {

View file

@ -1,13 +1,21 @@
<template> <template>
<div <div class="funnel-header" v-if="imageSrc">
class="funnel-header d-flex justify-content-center align-items-center flex-column" <div class="d-flex justify-content-between align-items-center">
v-if="imageSrc"> <div class="button-container">
<!-- HIDDEN FOR 05.08 <salesforceWebchat hideSalesforceWebchatLaunchButton />--> <!-- HIDDEN FOR 05.08 <salesforceWebchat hideSalesforceWebchatLaunchButton /> -->
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" /> </div>
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. --> <div class="site-logo">
<menuModal /> <img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. --> </div>
<progressBar /> <div class="button-container">
<!-- // 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. -->
</div>
</div>
<div class="d-flex w-100">
<progressBar />
</div>
</div> </div>
<template v-for="globalAlert in globalAlertMessages" :key="globalAlert.id"> <template v-for="globalAlert in globalAlertMessages" :key="globalAlert.id">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@ -45,7 +53,7 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
hideSalesforceWebchatLaunchButton: { hideSalesforceWebchatLaunchButton: {
type: Boolean, type: Boolean,
default: false, default: true,
}, },
}, },
computed: { computed: {
@ -100,16 +108,23 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.menu-modal-container) {
display: none; // This is temporary until the hamburger menu is re-implemented for use with the progress bar
}
.funnel-header { .funnel-header {
position: relative; position: relative;
padding-top: 0.97rem; padding: 1rem 0;
}
.logo-image { .button-container {
width: auto; width: 1.5rem;
height: 1.1875rem; height: 1.625rem;
}
.logo-image {
width: auto;
height: 1.1875rem;
margin: 0 auto;
}
:deep(.menu-modal-container) {
display: none; // This is temporary until the hamburger menu is re-implemented for use with the progress bar
}
} }
</style> </style>

View file

@ -11,21 +11,6 @@
<div class="bar3"></div> <div class="bar3"></div>
</button> </button>
</div> </div>
<div class="menu-modal-container">
<button
aria-hidden="true"
tabindex="-1"
class="menu-button"
type="button"
:class="[isActive ? 'active' : '']"
@click="toggleModal"
aria-label="Hamburger Menu (modal window)">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</button>
</div>
<!-- Modal -->
<div <div
class="modal menu-modal fade" class="modal menu-modal fade"
data-bs-backdrop="false" data-bs-backdrop="false"
@ -123,9 +108,6 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.menu-modal-container { .menu-modal-container {
position: absolute;
padding: 1.47rem 0 1.47rem 1.47rem;
right: 0;
button { button {
border: none; border: none;
&.menu-button { &.menu-button {
@ -164,7 +146,7 @@ export default {
} }
.modal { .modal {
&.menu-modal { &.menu-modal {
left: auto; //left: auto;
height: calc(100% - 56px); height: calc(100% - 56px);
top: 56px; top: 56px;
border-top: 1px solid $gray-300; border-top: 1px solid $gray-300;

View file

@ -53,6 +53,7 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
lineItems: Array, lineItems: Array,
isAfterpayBreakoutDisplay: Boolean, isAfterpayBreakoutDisplay: Boolean,
isInsuranceSelected: Boolean,
}, },
data() { data() {
return {}; return {};
@ -72,9 +73,14 @@ export default {
afterpayCopyTokens() { afterpayCopyTokens() {
if (this.isAfterpayBreakoutDisplay) { if (this.isAfterpayBreakoutDisplay) {
// Text for Afterpay Breakout experiment // Text for Afterpay Breakout experiment
return splitCopyOnCMSPlaceHolder( var afterpayBannerCopy = this.getCmsContent(this.cmsWidgetName, "BodyText2");
this.getCmsContent(this.cmsWidgetName, "BodyText2") 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, "HeaderText")

View file

@ -12,8 +12,30 @@ import baseMixin from "@/mixins/base-mixin.js";
import { containsLineItemWithPartType } from "../../helpers/service-package-helper"; import { containsLineItemWithPartType } from "../../helpers/service-package-helper";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
commit: jest.fn(), commit: jest.fn(), // Mock store.commit
dispatch: jest.fn(), dispatch: jest.fn(),
getters: {
order: {
lineItems: {
glassParts: [],
supportingItems: [],
vaps: [],
promos: [],
},
payment: {
insuranceCoverage: {},
isInsurance: false,
parentAccountNumber: 167132,
},
},
payment: {
insuranceCoverage: {},
isInsurance: false,
},
customer: {
emailAddress: "test@test.com",
},
},
})); }));
// Mock fetchCmsContentForPage // Mock fetchCmsContentForPage
@ -157,6 +179,7 @@ afterEach(() => {
describe("quote.vue", () => { describe("quote.vue", () => {
test("IsInsurance false should navigateWithSaving", async () => { test("IsInsurance false should navigateWithSaving", async () => {
//Arrange //Arrange
store.commit = jest.fn(); // Ensure store.commit is mocked
store.getters.payment = { store.getters.payment = {
insuranceCoverage: {}, insuranceCoverage: {},
isInsurance: false, isInsurance: false,
@ -785,6 +808,7 @@ describe("quote.vue", () => {
}); });
test("On forward button action save promos", async () => { test("On forward button action save promos", async () => {
//Arrange //Arrange
store.commit = jest.fn(); // Ensure store.commit is mocked
store.getters.payment = { store.getters.payment = {
insuranceCoverage: {}, insuranceCoverage: {},
isInsurance: false, isInsurance: false,
@ -809,7 +833,8 @@ describe("quote.vue", () => {
expect(wrapper.vm.lineItems.promos !== null).toBe(true); expect(wrapper.vm.lineItems.promos !== null).toBe(true);
}); });
test("On forward button action save supporting lineItems", async () => { test("On forward button action save supporting lineItems", async () => {
//Arrange // Arrange
store.commit = jest.fn(); // Ensure store.commit is mocked
store.getters.payment = { store.getters.payment = {
insuranceCoverage: {}, insuranceCoverage: {},
isInsurance: false, isInsurance: false,
@ -820,6 +845,7 @@ describe("quote.vue", () => {
parentAccountNumber: 167132, parentAccountNumber: 167132,
}, },
}; };
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
customMountOptions: { customMountOptions: {
router: { router: {

View file

@ -47,6 +47,7 @@
v-if="showAfterpayBanner" v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget" cmsWidgetName="AfterpayModalWidget"
:isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay" :isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay"
:isInsuranceSelected="isInsuranceSelected"
:lineItems="lineItems" /> :lineItems="lineItems" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. --> <!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
@ -168,6 +169,7 @@ import {
} from "@/helpers/heritage-integration/cookie-helper"; } from "@/helpers/heritage-integration/cookie-helper";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeMutations } from "@/constants/store-mutations";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -588,7 +590,7 @@ export default {
}, },
showAfterpayBanner() { showAfterpayBanner() {
return ( return (
!this.isInsuranceSelected && (this.isAfterpayBreakoutDisplay || !this.isInsuranceSelected) &&
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration) (!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
); );
}, },
@ -766,6 +768,22 @@ export default {
this.$route this.$route
); );
} }
const lineItems = store.getters.order?.lineItems ?? {};
const combinedLineItems = [
...(lineItems.glassParts || []),
...(lineItems.supportingItems || []),
...(lineItems.vaps || []),
...(lineItems.promos || []),
];
const cashPriceSubtotal = combinedLineItems.length
? baseMixin.methods
.getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false)
.toFixed(2)
: "";
store.commit(storeMutations.UPDATE_CASH_PRICE_SUBTOTAL, cashPriceSubtotal);
}, },
attachCustomEventsForAnalytics() { attachCustomEventsForAnalytics() {
this.prependActionToMethod(this, this.forwardButtonAction, () => { this.prependActionToMethod(this, this.forwardButtonAction, () => {
@ -789,8 +807,10 @@ export default {
}); });
this.$nextTick(() => { this.$nextTick(() => {
const availablePackageNames = this.$refs.servicePackage?.servicePackageAnswers; const availablePackageNames = this.$refs.servicePackage?.servicePackageAnswers;
var eventLabel = ""; var priceLabel = "";
availablePackageNames?.forEach((tier) => {
if (availablePackageNames?.length > 1 || availablePackageNames?.length == 1) {
var tier = availablePackageNames[0];
if (tier) { if (tier) {
let lineItemsToPrice = this.availableLineItems; let lineItemsToPrice = this.availableLineItems;
if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) { if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) {
@ -815,14 +835,11 @@ export default {
baseMixin.methods.filterOutFees(lineItemsToPrice) baseMixin.methods.filterOutFees(lineItemsToPrice)
); );
price += this.$refs.servicePackage.getVapsPrice(tier.value); price += this.$refs.servicePackage.getVapsPrice(tier.value);
const formattedPrice = parseFloat(price).toFixed(2); priceLabel = price.toFixed(2);
eventLabel += tier.value + "_" + formattedPrice;
} }
eventLabel += ","; }
}); const subTotalLabel = this.Variables?.CASH_SUBTOTAL;
eventLabel = eventLabel.slice(0, -1); this.pushVariableToDataLayer?.({ [subTotalLabel]: priceLabel });
this.pushEventToGA("quote", this.GaActions.SERVICE_PACKAGE_PRICE, eventLabel, true);
}); });
}, },
async skip(insuranceSelection, packageSelection) { async skip(insuranceSelection, packageSelection) {

View file

@ -21,6 +21,8 @@ import servicePackageRadioExperiment from "@/experiment-components/service-packa
import servicePackageRadioAfterpayExperiment from "@/experiment-components/service-package-radio-for-afterpay"; import servicePackageRadioAfterpayExperiment from "@/experiment-components/service-package-radio-for-afterpay";
import { partTypeStrings } from "@/constants/part-type-strings"; import { partTypeStrings } from "@/constants/part-type-strings";
import { packageNames } from "@/constants/package-names"; import { packageNames } from "@/constants/package-names";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
import { import {
shouldFrontWipersBeAvailable, shouldFrontWipersBeAvailable,
shouldRearWipersBeAvailable, shouldRearWipersBeAvailable,
@ -161,6 +163,7 @@ export default {
isInsuranceSelected: this.isInsuranceSelected, isInsuranceSelected: this.isInsuranceSelected,
}, },
})); }));
this.$emit("currentNumberOfPackages", modifiedAnswers.length); this.$emit("currentNumberOfPackages", modifiedAnswers.length);
return modifiedAnswers; return modifiedAnswers;
}, },

View file

@ -375,6 +375,10 @@ export default {
setTimeout(() => { setTimeout(() => {
this.$refs.datePicker.showAnotherMonth().then((moreSelectableDatesData) => { this.$refs.datePicker.showAnotherMonth().then((moreSelectableDatesData) => {
this.selectableDatesData = moreSelectableDatesData; this.selectableDatesData = moreSelectableDatesData;
if (selectableDatesData?.days?.length > 0) {
this.selectedDate = selectableDatesData.days[0].date;
}
this.setDisplayWaitList(); this.setDisplayWaitList();
}); });
}, 50); }, 50);

View file

@ -11,7 +11,8 @@
v-model="selectedValue" v-model="selectedValue"
:suppressError="suppressError" :suppressError="suppressError"
:validationRules="validationRules" :validationRules="validationRules"
isRequired /> isRequired
:labelBold="labelBold" />
</div> </div>
</transition> </transition>
</template> </template>
@ -34,6 +35,12 @@ export default {
isServiceableInshop: Boolean, isServiceableInshop: Boolean,
isServiceableDropoff: Boolean, isServiceableDropoff: Boolean,
mobileFeeApplies: Boolean, mobileFeeApplies: Boolean,
labelBold: {
type: Boolean,
required: false,
default: false,
},
zipCode: String,
}, },
computed: { computed: {
questionText() { questionText() {
@ -47,6 +54,7 @@ export default {
const shouldShowInshop = this.isServiceableInshop; const shouldShowInshop = this.isServiceableInshop;
const shouldShowDropoff = const shouldShowDropoff =
this.isServiceableDropoff && !this.$store.getters.damage.isRepair; this.isServiceableDropoff && !this.$store.getters.damage.isRepair;
const zipCode = this.zipCode;
var answers = this.answersFromCms var answers = this.answersFromCms
? this.answersFromCms.filter((answer) => { ? this.answersFromCms.filter((answer) => {
@ -87,12 +95,15 @@ export default {
watch: { watch: {
answersToDisplay: { answersToDisplay: {
handler(newValue) { handler(newValue) {
// If there is only one option to display and that option is 'Mobile' then select it // If there is only one option to display and that option is 'Mobile' or 'Inshop' then select it
if ( if (newValue.length == 1) {
newValue.length == 1 && const name = newValue[0].Name;
newValue.findIndex((answer) => answer.Name == "Mobile") != -1 if (
) { name === AppointmentTypeStrings.MOBILE ||
this.selectedValue = "Mobile"; name === AppointmentTypeStrings.IN_SHOP
) {
this.selectedValue = name;
}
} }
}, },
immediate: true, immediate: true,
@ -100,14 +111,14 @@ export default {
isMobileOnly: { isMobileOnly: {
handler(newValue) { handler(newValue) {
if (newValue) { if (newValue) {
this.selectedValue = "Mobile"; this.selectedValue = AppointmentTypeStrings.MOBILE;
} }
}, },
}, },
isInshopOnly: { isInshopOnly: {
handler(newValue) { handler(newValue) {
if (newValue) { if (newValue) {
this.selectedValue = "Inshop"; this.selectedValue = AppointmentTypeStrings.IN_SHOP;
} }
}, },
}, },

View file

@ -8,6 +8,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
} }
const zipCodeData = await getZipCodeData(serviceZipCode); const zipCodeData = await getZipCodeData(serviceZipCode);
const providerData = await getShopProviderData(serviceZipCode);
// Get the Mobile Fee Part // Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging( const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging(
@ -15,6 +16,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
{ {
serviceZipCode: serviceZipCode, serviceZipCode: serviceZipCode,
serviceZipCodeCtu: zipCodeData.zipCodeCtu, serviceZipCodeCtu: zipCodeData.zipCodeCtu,
mobileProviderNumber: providerData.data.mobileProviderNumber,
}, },
pageNameToLog, pageNameToLog,
false false

View file

@ -91,6 +91,7 @@ const mockStoreActionPriceOrderItemsAndSaveServerData =
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA; storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS; const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS; const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS;
const mockStoreActionGetProviders = storeActions.GET_PROVIDERS;
const mockGetShopTimeSlotsGoodAvailability = { const mockGetShopTimeSlotsGoodAvailability = {
data: { data: {
@ -236,6 +237,15 @@ jest.mock("@/mixins/base-mixin.js", () => ({
return Promise.resolve(mockGetShopTimeSlotsLowAvailability); return Promise.resolve(mockGetShopTimeSlotsLowAvailability);
} }
if (actionName === mockStoreActionGetProviders) {
return Promise.resolve({
data: {
mobileProviderNumber: "001820",
shopProviders: [{}],
},
});
}
}), }),
}, },
})); }));

View file

@ -189,41 +189,7 @@ describe("mobile-location-modal-questions.vue", () => {
expect(wrapper.vm.internalModel.serviceZipCode).toEqual("55555"); expect(wrapper.vm.internalModel.serviceZipCode).toEqual("55555");
}); });
it("Should open the modal when the 'Enter your service address' link is clicked", async () => { it("Should emit setMobileLocation on setMobileLocation for a valid address and vehicle protection answer", async () => {
// Arrange
const mobileLocationQuestions = {
addressQuestions: {
streetAddress: "555 Some St",
apartmentNumberOrBusinessName: "Apt 1",
city: "Funkytown",
state: "OH",
zipCode: "55555",
},
isVehicleProtected: true,
serviceZipCode: "55555",
};
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: mobileLocationQuestions,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.MobileLocationModalWidget.openModal = jest.fn();
const mobileLocationLink = wrapper.findComponent({ ref: "mobileLocationLink" });
// // Act
await mobileLocationLink.trigger("click-event");
// // Assert
expect(wrapper.vm.$refs.MobileLocationModalWidget.openModal).toHaveBeenCalled();
});
it("Should emit setMobileLocationAndProceed on setMobileLocation for a valid address and vehicle protection answer", async () => {
// Arrange // Arrange
const mobileLocationQuestions = { const mobileLocationQuestions = {
addressQuestions: { addressQuestions: {
@ -258,7 +224,6 @@ describe("mobile-location-modal-questions.vue", () => {
}, },
attachTo: document.body, attachTo: document.body,
}); });
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
// Act // Act
wrapper.vm.internalModel = newMobileLocationQuestions; wrapper.vm.internalModel = newMobileLocationQuestions;
@ -266,13 +231,13 @@ describe("mobile-location-modal-questions.vue", () => {
await wrapper.vm.setMobileLocation(); await wrapper.vm.setMobileLocation();
// Assert // Assert
expect(wrapper.emitted()["setMobileLocationAndProceed"]).toBeTruthy(); expect(wrapper.emitted()["setMobileLocation"]).toBeTruthy();
expect(wrapper.emitted()["setMobileLocationAndProceed"][0][0]).toStrictEqual( expect(wrapper.emitted()["setMobileLocation"][0][0]).toStrictEqual(
newMobileLocationQuestions newMobileLocationQuestions
); );
}); });
it("Should not emit setMobileLocationAndProceed on setMobileLocation for an invalid address zip code", async () => { it("Should not emit setMobileLocation on setMobileLocation for an invalid address zip code", async () => {
// Arrange // Arrange
const mobileLocationQuestions = { const mobileLocationQuestions = {
addressQuestions: { addressQuestions: {
@ -307,15 +272,13 @@ describe("mobile-location-modal-questions.vue", () => {
}, },
attachTo: document.body, attachTo: document.body,
}); });
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
wrapper.vm.resetModalButtonStyle = jest.fn();
// Act // Act
wrapper.vm.internalModel = newMobileLocationQuestions; wrapper.vm.internalModel = newMobileLocationQuestions;
await wrapper.vm.setMobileLocation(); await wrapper.vm.setMobileLocation();
// Assert // Assert
expect(wrapper.emitted("setMobileLocationAndProceed")).not.toBeTruthy(); expect(wrapper.emitted("setMobileLocation")).not.toBeTruthy();
}); });
it("Should display invalid zip alert for invalid address zip inputs", async () => { it("Should display invalid zip alert for invalid address zip inputs", async () => {

View file

@ -1,52 +1,48 @@
<template> <template>
<div class="mobile-location-questions"> <transition name="fade" mode="out-in">
<div class="text-center" :id="componentId"> <div class="mobile-location-questions">
<label <div class="text-center" :id="componentId">
for="mobileLocationLinkPromptId" <label
:aria-label="mobileLocationLinkPromptText" for="mobileLocationLinkPromptId"
class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black" :aria-label="mobileLocationLinkPromptText"
v-html="mobileLocationLinkPromptText"></label> class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black"
<div class="update-mobile-location-text-link"> v-html="mobileLocationLinkPromptText"></label>
<textLink <div class="update-mobile-location-text-link">
ref="mobileLocationLink" <textLink
id="mobileLocationLinkPromptId" ref="mobileLocationLink"
linkType="text" id="mobileLocationLinkPromptId"
:text="mobileLocationLinkText" linkType="text"
href="#!" :text="mobileLocationLinkText"
@click-event="openModal" /> href="#!"
@click-event="toggleMobileLocation" />
</div>
<div v-show="errorMessage" class="row my-1 form-test-error">
<span
class="d-inline-flex small mt-0 center-error-message"
aria-atomic="true"
aria-live="polite">
{{ errorMessage }}
</span>
</div>
<textBlock
v-if="mobileFeeApplies"
:customText="mobileFeeText"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="caption" />
</div> </div>
<div v-show="errorMessage" class="row my-1 form-test-error"> <div v-if="isMobileLocationOpened" class="address-questions-container">
<span <div v-html="headerText" class="HeaderText" />
class="d-inline-flex small mt-0 center-error-message"
aria-atomic="true"
aria-live="polite">
{{ errorMessage }}
</span>
</div>
<textBlock
v-if="mobileFeeApplies"
:customText="mobileFeeText"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="caption" />
</div>
<modal
:ref="modalName"
:headerText="modalHeaderText"
:footerButtonText="modalFooterText"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
@isModalOpened="setModalStatus"
@footer-button-event="setMobileLocation">
<template v-if="isModalOpened">
<addressQuestions <addressQuestions
ref="addressQuestions" ref="addressQuestions"
v-model="internalModel.addressQuestions" v-model="internalModel.addressQuestions"
captureApartmentNumberOrBusinessName="true" captureApartmentNumberOrBusinessName="true"
preserveCityAndStateOnReset="true" /> preserveCityAndStateOnReset="true"
labelBold="true" />
<vehicleProtectedQuestion <vehicleProtectedQuestion
ref="vehicleProtectedQuestion" ref="vehicleProtectedQuestion"
v-model="internalModel.isVehicleProtected" v-model="internalModel.isVehicleProtected"
cmsWidgetName="VehicleProtectedQuestionWidget" /> cmsWidgetName="VehicleProtectedQuestionWidget"
labelBold="true" />
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" /> <textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
<alert <alert
ref="alertInvalidZip" ref="alertInvalidZip"
@ -62,16 +58,14 @@
cmsWidgetName="AlertMismatchStateAndZipWidget" cmsWidgetName="AlertMismatchStateAndZipWidget"
alertClass="alert-danger" alertClass="alert-danger"
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
</template> </div>
</modal> </div>
</div> </transition>
</template> </template>
<script> <script>
// Components // Components
import textLink from "@/ux-components/text-link/text-link"; import textLink from "@/ux-components/text-link/text-link";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
import modal from "@/digital-components/modal/modal";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import addressQuestions from "@/fmg-components/address-questions/address-questions"; import addressQuestions from "@/fmg-components/address-questions/address-questions";
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question"; import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
@ -96,13 +90,14 @@ export default {
"updated-mobile-fee-part", "updated-mobile-fee-part",
"updated-contains-military-base", "updated-contains-military-base",
"updated-bill-to-account-number", "updated-bill-to-account-number",
"setMobileLocationAndProceed", "setMobileLocation",
"set-mobile-location-invalid",
], ],
data() { data() {
return { return {
internalModel: deepClone(this.modelValue), internalModel: deepClone(this.modelValue),
displayInvalidZipAlert: false, displayInvalidZipAlert: false,
isModalOpened: false, isMobileLocationOpened: false,
displayMismatchStateAndZipAlert: false, displayMismatchStateAndZipAlert: false,
}; };
}, },
@ -188,23 +183,29 @@ export default {
this.mobileFeePart.kitPrice this.mobileFeePart.kitPrice
); );
}, },
modalName() { headerText() {
return "MobileLocationModalWidget";
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText"); return this.getCmsContent(this.modalWidgetName, "HeaderText");
}, },
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
modal() {
return this.$refs[this.modalName];
},
addressModel: { addressModel: {
get: function () { get: function () {
return this.modelValue.addressQuestions; return this.modelValue.addressQuestions;
}, },
}, },
isMobileAddressAvailable: {
get: function () {
return (
this.addressModel.streetAddress &&
this.addressModel.streetAddress !== "" &&
this.addressModel.city &&
this.addressModel.city !== "" &&
this.addressModel.state &&
this.addressModel.state !== "" &&
this.addressModel.zipCode &&
this.addressModel.zipCode !== "" &&
this.internalModel.isVehicleProtected !== null
);
},
},
}, },
methods: { methods: {
isMobileAddressComplete() { isMobileAddressComplete() {
@ -220,24 +221,14 @@ export default {
this.internalModel.isVehicleProtected !== null this.internalModel.isVehicleProtected !== null
); );
}, },
openModal() { getIsMobleLocationOpened() {
this.modal.openModal(); if (this.isServiceAddressFromStoreAvailable) {
return false;
}
return true;
}, },
onModalOpened() { setMobileLocationInvalid(isFormInvalid) {
this.internalModel = deepClone(this.modelValue); this.$emit("set-mobile-location-invalid", isFormInvalid);
},
setModalStatus(isOpened) {
this.isModalOpened = isOpened;
},
closeModal() {
this.modal.closeModal();
},
onModalClosed() {
this.resetAlerts();
this.internalModel = deepClone(this.modelValue);
},
resetModalButtonStyle() {
this.modal.resetButtonStyle();
}, },
resetAlerts() { resetAlerts() {
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
@ -246,6 +237,10 @@ export default {
async setMobileLocation() { async setMobileLocation() {
this.resetAlerts(); this.resetAlerts();
// Validate the Zip Code // Validate the Zip Code
if (this.internalModel.addressQuestions.zipCode === "") {
this.setMobileLocationInvalid(true);
return;
}
const zipCodeData = await this.getZipCodeData( const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode, this.internalModel.addressQuestions.zipCode,
"service-location" "service-location"
@ -253,11 +248,11 @@ export default {
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
this.resetModalButtonStyle(); this.setMobileLocationInvalid(true);
return; return;
} else if (zipCodeData.state != this.internalModel.addressQuestions.state) { } else if (zipCodeData.state != this.internalModel.addressQuestions.state) {
this.displayMismatchStateAndZipAlert = true; this.displayMismatchStateAndZipAlert = true;
this.resetModalButtonStyle(); this.setMobileLocationInvalid(true);
return; return;
} else if ( } else if (
this.internalModel.addressQuestions.zipCode !== this.internalModel.addressQuestions.zipCode !==
@ -296,10 +291,21 @@ export default {
this.$emit("updated-bill-to-account-number", billToAccountNumber); this.$emit("updated-bill-to-account-number", billToAccountNumber);
} }
// update the page level model // update the page level model
this.$emit("setMobileLocationAndProceed", this.internalModel); this.$emit("setMobileLocation", this.internalModel);
this.setMobileLocationInvalid(false);
},
toggleMobileLocation() {
this.isMobileLocationOpened = !this.isMobileLocationOpened;
}, },
}, },
watch: { watch: {
internalModel: {
handler() {
this.setMobileLocation();
},
deep: true,
},
modelValue: { modelValue: {
handler(newValue) { handler(newValue) {
this.internalModel = deepClone(newValue); this.internalModel = deepClone(newValue);
@ -308,15 +314,11 @@ export default {
deep: true, deep: true,
}, },
}, },
unmounted() {
if (this.isModalOpened) this.closeModal();
},
components: { components: {
textLink,
modal,
addressQuestions, addressQuestions,
vehicleProtectedQuestion, vehicleProtectedQuestion,
textBlock, textBlock,
textLink,
alert, alert,
}, },
}; };
@ -327,28 +329,34 @@ export default {
.center-error-message { .center-error-message {
justify-content: center !important; justify-content: center !important;
} }
}
.update-zip-text-link::before { .update-zip-text-link::before {
content: ""; content: "";
display: inline-block; display: inline-block;
width: 13px; width: 13px;
height: 16px; height: 16px;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
background-size: contain; background-size: contain;
vertical-align: middle; vertical-align: middle;
margin-right: 0.5em; margin-right: 0.5em;
} }
.address-questions { .update-mobile-location-text-link {
margin-top: 0em; white-space: pre-line;
} }
.address-questions-container {
margin-top: 1rem;
}
.update-mobile-location-text-link { .text-black {
white-space: pre-line; color: $black;
} }
.HeaderText {
.text-black { font-size: 1rem;
color: $black; font-weight: 600;
line-height: 1.625rem;
color: $black;
text-align: center;
}
} }
</style> </style>

View file

@ -7,7 +7,8 @@
textPosition="text-center" textPosition="text-center"
v-model="selectedValue" v-model="selectedValue"
validationRules="option-required" validationRules="option-required"
isRequired /> isRequired
:labelBold="labelBold" />
</template> </template>
<script> <script>
@ -27,6 +28,11 @@ export default {
isVehicleProtected: Boolean, isVehicleProtected: Boolean,
}, },
cmsWidgetName: String, cmsWidgetName: String,
labelBold: {
type: Boolean,
required: false,
default: false,
},
}, },
components: { components: {
buttonQuestion, buttonQuestion,

View file

@ -343,7 +343,6 @@ describe("service-location.vue", () => {
zipCode: "43054", zipCode: "43054",
}, },
isVehicleProtected: true, isVehicleProtected: true,
isMobileSelected: true,
}; };
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
@ -356,7 +355,6 @@ describe("service-location.vue", () => {
zipCode: "61606", zipCode: "61606",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false,
}; };
// Act // Act
@ -459,7 +457,6 @@ describe("service-location.vue", () => {
zipCode: "", zipCode: "",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false,
}; };
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
const newMobileLocationQuestions = { const newMobileLocationQuestions = {
@ -471,15 +468,14 @@ describe("service-location.vue", () => {
zipCode: "43054", zipCode: "43054",
}, },
isVehicleProtected: true, isVehicleProtected: true,
isMobileSelected: true,
}; };
wrapper.vm.closeModalAction = jest.fn(); //wrapper.vm.closeModalAction = jest.fn();
// Act // Act
// Trigger the event // Trigger the event
await mobileLocationQuestionsComponent.vm.$emit( await mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed", "setMobileLocation",
newMobileLocationQuestions newMobileLocationQuestions
); );
@ -521,7 +517,7 @@ describe("service-location.vue", () => {
zipCode: "", zipCode: "",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false, isMobileSelectedAndModalClosed: false,
mobileFeePart: null, mobileFeePart: null,
}; };
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
@ -535,7 +531,7 @@ describe("service-location.vue", () => {
zipCode: "43081", zipCode: "43081",
}, },
isVehicleProtected: true, isVehicleProtected: true,
isMobileSelected: true, isMobileSelectedAndModalClosed: true,
mobileFeePart: null, mobileFeePart: null,
}; };
@ -556,7 +552,7 @@ describe("service-location.vue", () => {
// Trigger the event // Trigger the event
await mobileLocationQuestionsComponent.vm.$emit( await mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed", "setMobileLocation",
newMobileLocationQuestions newMobileLocationQuestions
); );
@ -599,15 +595,12 @@ describe("service-location.vue", () => {
zipCode: "43081", zipCode: "43081",
}, },
isVehicleProtected: "YesAnswer", isVehicleProtected: "YesAnswer",
isMobileSelected: true, isMobileSelectedAndModalClosed: true,
}; };
wrapper.vm.closeModalAction = jest.fn(); wrapper.vm.closeModalAction = jest.fn();
// Act // Act
mobileLocationQuestionsComponent.vm.$emit( mobileLocationQuestionsComponent.vm.$emit("setMobileLocation", mobileLocationQuestions);
"setMobileLocationAndProceed",
mobileLocationQuestions
);
// Assert // Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual("Mobile"); expect(wrapper.vm.selectedAppointmentType).toStrictEqual("Mobile");

View file

@ -79,10 +79,12 @@
:isServiceableDropoff="isServiceableDropoff" :isServiceableDropoff="isServiceableDropoff"
:isDisplayed="isAppointmentTypeDisplayed" :isDisplayed="isAppointmentTypeDisplayed"
:mobileFeeApplies="mobileFeeApplies" :mobileFeeApplies="mobileFeeApplies"
:zipCode="zipCode"
ref="appointmentTypeQuestion" ref="appointmentTypeQuestion"
groupName="appointmentTypeQuestion" groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget" cmsWidgetName="AppointmentTypeQuestionWidget"
validationRules="option-required" /> validationRules="option-required"
labelBold="true" />
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
@ -102,7 +104,8 @@
ref="mobileLocationQuestions" ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget" linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget" modalWidgetName="MobileLocationModalWidget"
@setMobileLocationAndProceed="setMobileLocationQuestions" /> @setMobileLocation="setMobileLocationQuestions"
@set-mobile-location-invalid="setMobileLocationInValid" />
<shopQuestion <shopQuestion
ref="shopQuestion" ref="shopQuestion"
@ -111,15 +114,14 @@
:selectedAppointmentType="selectedAppointmentType" :selectedAppointmentType="selectedAppointmentType"
:shopProviderData="shopProviderData" :shopProviderData="shopProviderData"
:isDisplayed="isShopQuestionDisplayed" :isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget" cmsWidgetName="ShopQuestionWidget" />
:isInshopOnly="isInshopOnly" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" /> <contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="navbar" ref="navbar"
:isForwardActionDisabled="!meta.valid || displayNoShopsAlert" :isForwardActionDisabled="!meta.valid || isForwardActionDisabled"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
</div> </div>
@ -173,12 +175,12 @@ const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => { defineRule("mobile-location-required", (value) => {
if ( if (
value.isMobileSelected &&
(!value.addressQuestions.streetAddress || (!value.addressQuestions.streetAddress ||
!value.addressQuestions.city || !value.addressQuestions.city ||
!value.addressQuestions.state || !value.addressQuestions.state ||
!value.addressQuestions.zipCode || !value.addressQuestions.zipCode ||
!value.isVehicleProtected) !value.isVehicleProtected) &&
value.isMobileSelected
) { ) {
return errorMessages.MOBILE_LOCATION_REQUIRED; return errorMessages.MOBILE_LOCATION_REQUIRED;
} }
@ -212,6 +214,7 @@ export default {
billToAccountNumber: null, billToAccountNumber: null,
shopProviderData: null, shopProviderData: null,
navigatingForward: false, navigatingForward: false,
isMobileAddressValid: true,
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -302,7 +305,6 @@ export default {
zipCode: this.zipCode, zipCode: this.zipCode,
}, },
isVehicleProtected: this.isVehicleProtected, isVehicleProtected: this.isVehicleProtected,
isMobileSelected: this.selectedAppointmentType === "Mobile",
}; };
}, },
}, },
@ -416,10 +418,8 @@ export default {
isNoComp() { isNoComp() {
return store.getters.order.policy.isNoComp; return store.getters.order.policy.isNoComp;
}, },
isInshopOnly() { isForwardActionDisabled() {
return ( return this.displayNoShopsAlert || !this.isMobileAddressValid;
this.isServiceableInshop && !this.isServiceableMobile && !this.isServiceableDropoff
);
}, },
}, },
methods: { methods: {
@ -523,10 +523,7 @@ export default {
} }
if (this.isServiceableMobile) { if (this.isServiceableMobile) {
this.setMobileLocation(newValue); this.setMobileLocation(newValue);
//proceed to next page
this.forwardButtonAction();
} else { } else {
this.closeModalAction("mobileLocationQuestions");
await getZipCodeData(newZipCode).then((zipCodeData) => { await getZipCodeData(newZipCode).then((zipCodeData) => {
this.serviceZipCodeQuestion = { this.serviceZipCodeQuestion = {
state: zipCodeData.state, state: zipCodeData.state,
@ -694,7 +691,7 @@ export default {
}, },
async forwardButtonAction() { async forwardButtonAction() {
this.navigatingForward = true; this.navigatingForward = true;
var ctu = this.zipCodeCtu;
if ( if (
this.selectedAppointmentType == AppointmentTypeStrings.MOBILE && this.selectedAppointmentType == AppointmentTypeStrings.MOBILE &&
this.selectedProvider && this.selectedProvider &&
@ -705,18 +702,13 @@ export default {
this.selectedProvider.address.state = null; this.selectedProvider.address.state = null;
this.selectedProvider.address.zipCode = null; this.selectedProvider.address.zipCode = null;
this.selectedProvider.address.zipCodeCtu = null; this.selectedProvider.address.zipCodeCtu = null;
} } else {
var ctu = this.zipCodeCtu;
if (this.selectedAppointmentType == AppointmentTypeStrings.IN_SHOP) {
// if we have a provider.zipCodeCtu that's different than the servicelocation.zipCodeCtu then they // if we have a provider.zipCodeCtu that's different than the servicelocation.zipCodeCtu then they
// may have selected a shop in a different ctu. change the servicelocation zip/ctu if different // may have selected a shop in a different ctu. change the servicelocation zip/ctu if different
if (this.zipCodeCtu != this.selectedProvider.address.zipCodeCtu) { if (this.zipCodeCtu != this.selectedProvider.address.zipCodeCtu) {
ctu = this.selectedProvider.address.zipCodeCtu; ctu = this.selectedProvider.address.zipCodeCtu;
} }
this.resetMobileLocation();
this.city = null;
this.streetAddress = null;
} }
await this.dispatchStoreAction( await this.dispatchStoreAction(
@ -756,11 +748,15 @@ export default {
this.updateAndSaveIsMSRFeeApplicable(); this.updateAndSaveIsMSRFeeApplicable();
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
}, },
openModalAction(modalName) { setMobileLocationInValid(isMobileLocationInValid) {
this.$refs[modalName].openModal(); this.isMobileAddressValid = !isMobileLocationInValid;
}, },
closeModalAction(modalName) { displayMobileAddressQuestion(openMobileLocation) {
this.$refs[modalName].closeModal(); var mobileLocationQuestionsRef = this.$refs.mobileLocationQuestions;
var isMobileAddressComplete = mobileLocationQuestionsRef.isMobileAddressComplete;
if (isMobileAddressComplete) {
mobileLocationQuestionsRef.isMobileLocationOpened = openMobileLocation;
}
}, },
}, },
watch: { watch: {
@ -773,8 +769,10 @@ export default {
this.selectedProvider = new Provider( this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber this.shopProviderData.mobileProviderNumber
); );
this.displayMobileAddressQuestion(true);
} else { } else {
this.selectedProvider = new Provider(); this.selectedProvider = new Provider();
this.displayMobileAddressQuestion(false);
} }
}); });
} }
@ -786,12 +784,10 @@ export default {
this.selectedProvider = new Provider( this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber this.shopProviderData.mobileProviderNumber
); );
var mobileLocationQs = this.$refs.mobileLocationQuestions; this.displayMobileAddressQuestion(true);
if (!mobileLocationQs.isMobileAddressComplete()) {
mobileLocationQs.openModal();
}
} else { } else {
this.selectedProvider = new Provider(); this.selectedProvider = new Provider();
this.displayMobileAddressQuestion(false);
} }
}, },
}, },

View file

@ -77,7 +77,6 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
validationRules: String, validationRules: String,
isDisplayed: Boolean, isDisplayed: Boolean,
isInshopOnly: Boolean,
}, },
computed: { computed: {
questionText() { questionText() {
@ -234,13 +233,9 @@ export default {
// nothing to do with this component. We could mitigate this by showing / hiding this component with v-if but that messes // nothing to do with this component. We could mitigate this by showing / hiding this component with v-if but that messes
// up the component initialization on page load. // up the component initialization on page load.
async handler() { async handler() {
//We do not need to run this handler in case of Inshop only else it will ended running twice function getNextShopsFromList and will cause pagination await nextTick();
if (!this.isInshopOnly) { this.resetAnswers();
this.resetAnswers(); this.getNextShopsFromList();
await nextTick();
this.getNextShopsFromList();
}
}, },
}, },
shopProviders: { shopProviders: {

View file

@ -30,6 +30,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import router from "@/router/index.js"; import router from "@/router/index.js";
import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { Variables } from "../constants/analytics";
export default { export default {
methods: { methods: {
@ -120,6 +121,10 @@ export default {
} }
}, },
async pushVariableToDataLayer(data) {
pushToDataLayerIfDefined(data);
},
async pushPageViewToGA() { async pushPageViewToGA() {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
const pageViewEvent = { const pageViewEvent = {
@ -306,6 +311,18 @@ export default {
} else { } else {
payload.priceSubTotal = ""; payload.priceSubTotal = "";
} }
// Cash Quote or Cash Price Sub Total
if (isPricingAvailable) {
const subtotal = baseMixin.methods
.getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false)
.toFixed(2);
payload.cashPriceSubTotal = parseFloat(subtotal);
} else {
payload.cashPriceSubTotal = "";
}
//unverified (in scenarios we dont display the price) //unverified (in scenarios we dont display the price)
if ( if (
order.payment.isInsurance && order.payment.isInsurance &&
@ -770,6 +787,9 @@ export default {
ValueToLogTypes() { ValueToLogTypes() {
return ValueToLogTypes; return ValueToLogTypes;
}, },
Variables() {
return Variables;
},
}, },
}; };

View file

@ -340,6 +340,10 @@ export const mutations = {
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable; state.order.isMSRFeeApplicable = isMSRFeeApplicable;
}, },
updateCashPriceSubTotal(state, cashPriceSubTotal) {
state.order.cashPriceSubTotal =
cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString();
},
updateCCToken(state, ccToken) { updateCCToken(state, ccToken) {
state.order.payment.ccToken.subscriptionId = ccToken.subscriptionId; state.order.payment.ccToken.subscriptionId = ccToken.subscriptionId;
state.order.payment.ccToken.expMonth = ccToken.expMonth; state.order.payment.ccToken.expMonth = ccToken.expMonth;
@ -1732,7 +1736,10 @@ export const actions = {
return response; return response;
}, },
getMobileFeePart(context, { payload: { serviceZipCode, serviceZipCodeCtu }, pageNameToLog }) { getMobileFeePart(
context,
{ payload: { serviceZipCode, serviceZipCodeCtu, mobileProviderNumber }, pageNameToLog }
) {
const serviceType = context.getters.damage.isRepair ? "Repair" : "Install"; const serviceType = context.getters.damage.isRepair ? "Repair" : "Install";
const facilityType = "Mobile"; const facilityType = "Mobile";
const parentAccountNumber = context.getters.payment.parentAccountNumber; const parentAccountNumber = context.getters.payment.parentAccountNumber;
@ -1750,6 +1757,7 @@ export const actions = {
order.serviceLocation?.zipCode; order.serviceLocation?.zipCode;
var providerNumber = var providerNumber =
mobileProviderNumber ??
serviceZipCodeCtu ?? serviceZipCodeCtu ??
order.serviceLocation?.provider?.providerNumber ?? order.serviceLocation?.provider?.providerNumber ??
order.serviceLocation?.zipCodeCtu; order.serviceLocation?.zipCodeCtu;
@ -2339,6 +2347,7 @@ export const actions = {
lockToken: order.lockToken, lockToken: order.lockToken,
isRecalAckOptIn: order.isRecalAckOptIn, isRecalAckOptIn: order.isRecalAckOptIn,
isMSRFeeApplicable: order.isMSRFeeApplicable, isMSRFeeApplicable: order.isMSRFeeApplicable,
cashPriceSubTotal: order.cashPriceSubTotal,
}, },
}, },
logApiCall: true, logApiCall: true,

View file

@ -6,7 +6,7 @@
<div <div
:aria-label="buttonLabel" :aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4 p-md-4"> class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4 p-md-4">
<span class="m-0" :class="textPosition"> <span class="m-0" :class="[textPosition, labelBold ? 'span-bold' : '']">
{{ buttonLabel }} {{ buttonLabel }}
</span> </span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition"> <span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
@ -88,6 +88,9 @@ export default {
&:checked + .list-button-content span { &:checked + .list-button-content span {
font-weight: 500; font-weight: 500;
} }
&:checked + .list-button-content .span-bold {
font-weight: 600;
}
&:checked + .list-button-content span:nth-child(2) { &:checked + .list-button-content span:nth-child(2) {
font-weight: 400; font-weight: 400;
color: $gray-600; color: $gray-600;

View file

@ -15,7 +15,10 @@
:class="!isWide ? 'order-1' : 'ms-auto order-3'" :class="!isWide ? 'order-1' : 'ms-auto order-3'"
:src="buttonImage" :src="buttonImage"
:alt="altText" /> :alt="altText" />
<p v-if="!isWide" class="small order-3" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'"> <p
v-if="!isWide"
class="small order-3"
:class="[isMultiSelect ? 'm-0' : 'mt-2 mb-0', labelBold ? 'p-bold' : '']">
{{ buttonLabel }} {{ buttonLabel }}
</p> </p>
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy"> <p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy">
@ -122,6 +125,9 @@ export default {
color: $black; color: $black;
font-weight: 500; font-weight: 500;
} }
.p-bold {
font-weight: 600;
}
.sub-copy { .sub-copy {
color: $gray-600; color: $gray-600;