Merge pull request #2512 from Safelite/rlsmerge/2025.05.22-to-develop

Rlsmerge/2025.05.22 to develop
This commit is contained in:
chloeherdsafelite 2025-05-13 16:27:48 -04:00 committed by GitHub
commit d53b88ee80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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,14 +1,22 @@
<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 /> -->
</div>
<div class="site-logo">
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" /> <img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
</div>
<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. --> <!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
<menuModal /> <menuModal />
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. --> <!-- // 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 /> <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">
<alert <alert
@ -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: 1.5rem;
height: 1.625rem;
}
.logo-image {
width: auto; width: auto;
height: 1.1875rem; 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 (newValue.length == 1) {
const name = newValue[0].Name;
if ( if (
newValue.length == 1 && name === AppointmentTypeStrings.MOBILE ||
newValue.findIndex((answer) => answer.Name == "Mobile") != -1 name === AppointmentTypeStrings.IN_SHOP
) { ) {
this.selectedValue = "Mobile"; 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,4 +1,5 @@
<template> <template>
<transition name="fade" mode="out-in">
<div class="mobile-location-questions"> <div class="mobile-location-questions">
<div class="text-center" :id="componentId"> <div class="text-center" :id="componentId">
<label <label
@ -13,7 +14,7 @@
linkType="text" linkType="text"
:text="mobileLocationLinkText" :text="mobileLocationLinkText"
href="#!" href="#!"
@click-event="openModal" /> @click-event="toggleMobileLocation" />
</div> </div>
<div v-show="errorMessage" class="row my-1 form-test-error"> <div v-show="errorMessage" class="row my-1 form-test-error">
<span <span
@ -29,24 +30,19 @@
cmsWidgetName="MobileFeeDisclaimerWidget" cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="caption" /> typeStyle="caption" />
</div> </div>
<modal <div v-if="isMobileLocationOpened" class="address-questions-container">
:ref="modalName" <div v-html="headerText" class="HeaderText" />
: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>
</modal>
</div> </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,9 +329,8 @@ 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;
@ -338,17 +339,24 @@ export default {
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;
}
.update-mobile-location-text-link {
white-space: pre-line; white-space: pre-line;
} }
.address-questions-container {
margin-top: 1rem;
}
.text-black { .text-black {
color: $black; color: $black;
}
.HeaderText {
font-size: 1rem;
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
if (!this.isInshopOnly) {
this.resetAnswers();
await nextTick(); await nextTick();
this.resetAnswers();
this.getNextShopsFromList(); 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;