Merge pull request #2643 from Safelite/feature/7.17-without-schedule-parity

7.17 release without schedule parity
This commit is contained in:
scottkiener-at-safelite 2025-07-14 09:45:19 -04:00 committed by GitHub
commit 63cc08459d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 400 additions and 141 deletions

View file

@ -2,14 +2,18 @@
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
@import url("https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap");
body { body {
font-family: Roboto; font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 400; font-weight: 400;
color: #000; color: #000;
margin: 0; margin: 0;
background-color: white; background-color: white;
padding: 8px; padding: 8px;
} }
body h3 {
font-weight: 700;
}
body .validation-info { body .validation-info {
display: none; display: none;
} }
@ -136,6 +140,7 @@ body .buttonContainer button {
border-radius: 8px; border-radius: 8px;
color: #fff; color: #fff;
justify-content: center; justify-content: center;
font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;
height: 48px; height: 48px;
@ -268,6 +273,7 @@ body .cc-error-container #alert-message {
#fmg-checkout-shared #other-payments-container label span.paymentOptionText { #fmg-checkout-shared #other-payments-container label span.paymentOptionText {
margin: auto 0; margin: auto 0;
padding-left: 0; padding-left: 0;
font-weight: 400;
} }
#fmg-checkout-shared #other-payments-container #afterpayParentLink label > span:first-of-type { #fmg-checkout-shared #other-payments-container #afterpayParentLink label > span:first-of-type {
width: 100%; width: 100%;

View file

@ -2,14 +2,19 @@
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
@import url("https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap");
body { body {
font-family: Roboto; font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 400; font-weight: 400;
color: #000; color: #000;
margin: 0; margin: 0;
background-color: white; background-color: white;
padding: 8px; padding: 8px;
h3 {
font-weight: 700;
}
.validation-info { .validation-info {
display: none; display: none;
} }
@ -146,6 +151,7 @@ body {
border-radius: 8px; border-radius: 8px;
color: #fff; color: #fff;
justify-content: center; justify-content: center;
font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;
height: 48px; height: 48px;
@ -325,6 +331,7 @@ body {
&.paymentOptionText { &.paymentOptionText {
margin: auto 0; margin: auto 0;
padding-left: 0; padding-left: 0;
font-weight: 400;
} }
} }
} }

View file

@ -176,6 +176,10 @@ const endpoints = {
url: "/analytics/api/v1/analytics/log-part-questions", url: "/analytics/api/v1/analytics/log-part-questions",
method: "POST", method: "POST",
}, },
LogDigitalConsumer: {
url: "/analytics/api/v1/analytics/digitalconsumer-log",
method: "POST",
},
GetExperimentsByUser: { GetExperimentsByUser: {
url: "/analytics/api/v1/analytics/get-experiments", url: "/analytics/api/v1/analytics/get-experiments",
method: "GET", method: "GET",

View file

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

View file

@ -63,6 +63,7 @@ const storeActions = {
LOG_CUSTOM_EVENT: "logCustomEvent", LOG_CUSTOM_EVENT: "logCustomEvent",
INITIALIZE_SESSION: "initializeSession", INITIALIZE_SESSION: "initializeSession",
LOG_PART_QUESTIONS: "logPartQuestions", LOG_PART_QUESTIONS: "logPartQuestions",
LOG_DIGITALCONSUMER: "logDigitalConsumer",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleStateAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleStateAndDependencies",

View file

@ -4,6 +4,10 @@
<script> <script>
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
import store from "@/store";
import { routeData } from "@/router/constants/routes";
import { deepClone } from "@/helpers/object-helper";
export default { export default {
name: "sierraWebchat", name: "sierraWebchat",
mixins: [analyticsMixin], mixins: [analyticsMixin],
@ -24,7 +28,10 @@ export default {
}, },
createSierraConfig() { createSierraConfig() {
return { return {
variables: { application: "funnel" }, variables: {
application: "funnel",
...this.getSierraContext(),
},
display: "corner", display: "corner",
onLoad: () => this.openSierraChatModal(), onLoad: () => this.openSierraChatModal(),
onOpen: () => {}, onOpen: () => {},
@ -35,6 +42,39 @@ export default {
), ),
}; };
}, },
getSierraContext() {
const glassDamage = store.getters?.order?.damage?.glassToReplace;
const glassType =
glassDamage && glassDamage.length > 0
? glassDamage.length > 1
? "Multiglass"
: glassDamage[0].glassType // todo: clarify?
: "";
const damageType = store.getters.order?.damage?.isRepair ? "repair" : "replace";
const paymentType = store.getters.oreder?.payment?.isInsurance ? "insurance" : "cash";
const glassParts = store.getters.order?.lineItems?.glassParts;
const partId = glassParts && glassParts.length > 0 ? glassParts[0].partId : "";
const packageData = deepClone(store.getters?.pageData(routeData.QUOTE.name));
return {
current_page: this.pageName,
vehicle_year: store.getters.order?.vehicle?.year ?? "",
vehicle_make: store.getters.order?.vehicle?.make ?? "",
vehicle_model: store.getters.order?.vehicle?.model ?? "",
vehicle_style: store.getters.order?.vehicle?.style ?? "",
service_zip: store.getters.order?.serviceLocation?.serviceZip ?? "",
glass_type: glassType ?? "",
damage_type: damageType ?? "",
payment_type: paymentType ?? "",
quote: packageData ?? [],
part_id: partId ?? "",
recalibration: store.getters?.isRecalibrationOnOrder ?? "",
};
},
launchSierraChat() { launchSierraChat() {
window.sierraConfig = this.createSierraConfig(); window.sierraConfig = this.createSierraConfig();
// Preload CSS if not already present // Preload CSS if not already present

View file

@ -7,8 +7,10 @@
</div> </div>
</div> </div>
<div class="row-align d-flex px-4"> <div class="row-align d-flex px-4">
<div> <div class="logo-block">
<img class="w-100 mt-1 me-4" :src="LogoImage" /> <img class="w-100" :src="LogoImage" />
<span>EIN: 26-3043727</span>
<!-- This will likely never change so hard coded -->
</div> </div>
<div> <div>
<p class="small subheadline mb-2" v-html="DonationSubHeadline"></p> <p class="small subheadline mb-2" v-html="DonationSubHeadline"></p>
@ -79,7 +81,8 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<p class="mt-2 mb-4 mx-0 text-center caption" v-html="DonationFooterText"></p> <p class="mt-2 mb-2 mx-0 text-center caption" v-html="DonationFooterText"></p>
<p class="mb-4 mx-0 text-center caption" v-html="DonationFooterText2"></p>
</div> </div>
</div> </div>
</form> </form>
@ -121,6 +124,9 @@ export default {
DonationFooterText() { DonationFooterText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
DonationFooterText2() {
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
},
LogoImage() { LogoImage() {
return this.getCmsContent("DonationWidget", "Image"); return this.getCmsContent("DonationWidget", "Image");
}, },
@ -195,8 +201,26 @@ export default {
} }
} }
img { .logo-block {
width: 5rem; display: flex;
flex-direction: column;
background: white;
align-items: flex-start;
justify-content: center;
padding: 0 0.5rem;
margin-right: 1rem;
height: max-content;
img {
width: 88px;
}
span {
font-size: 0.625rem;
margin: 0 auto;
padding: 0.5rem 0;
font-family: UrbanistSemibold;
}
} }
.donation-slider { .donation-slider {

View file

@ -117,6 +117,10 @@
<span>{{ amountPaidText }}</span> <span>{{ amountPaidText }}</span>
<span>{{ getLineItemAmount(amountPaid) }}</span> <span>{{ getLineItemAmount(amountPaid) }}</span>
</div> </div>
<div v-if="donationCartItem" class="donation-amount">
<span>{{ donationCartItemName }}</span>
<span>{{ getLineItemAmount(donationCartItem.sellingPrice) }}</span>
</div>
<div class="amount-due"> <div class="amount-due">
<span>{{ amountDueText }}</span> <span>{{ amountDueText }}</span>
<span>{{ getLineItemAmount(amountDue, showCoverageAsPending) }}</span> <span>{{ getLineItemAmount(amountDue, showCoverageAsPending) }}</span>
@ -178,6 +182,7 @@ import {
getAmountDue, getAmountDue,
getSubTotal, getSubTotal,
getSalesTax, getSalesTax,
getAmountDueWithDonation,
} from "@/helpers/pricing-helper.js"; } from "@/helpers/pricing-helper.js";
// Constants // Constants
@ -207,6 +212,7 @@ export default {
isNoComp: Boolean, isNoComp: Boolean,
isExpandedOnLoad: Boolean, isExpandedOnLoad: Boolean,
isMSRFeeApplicable: Boolean, isMSRFeeApplicable: Boolean,
donationCartItem: Object,
}, },
data() { data() {
return { return {
@ -472,10 +478,6 @@ export default {
}); });
} }
if (this.donationCartItem) {
cartItems.push(this.donationCartItem);
}
return cartItems; return cartItems;
}, },
}, },
@ -1083,33 +1085,6 @@ export default {
donationCartItemName() { donationCartItemName() {
return this.getCmsContent("DonationCartTextWidget", "Text"); return this.getCmsContent("DonationCartTextWidget", "Text");
}, },
donationCartItem() {
if (!this.lineItems || !this.lineItems.supportingItems) {
return null;
}
const donationLineItem = this.lineItems.supportingItems.find(
(item) => item.partType === partTypeStrings.DONATION
);
if (donationLineItem) {
return {
name: this.donationCartItemName,
category: cartItemCategories.SUPPORTING_ITEMS,
cartItemType: cartItemTypes.DONATION,
isDisplayed: true,
isRemovable: false,
subTotal: donationLineItem.sellingPrice,
salesTax: 0,
lineItems: [],
isCoveredByInsurance: cartItemTypesCoveredByInsurance.includes(
cartItemTypes.DONATION
),
};
}
return null;
},
removeLinkText() { removeLinkText() {
return this.getCmsContent("RemoveCartItemTextWidget", "Text"); return this.getCmsContent("RemoveCartItemTextWidget", "Text");
}, },
@ -1156,9 +1131,15 @@ export default {
amountDue() { amountDue() {
if (!this.lineItems || this.lineItems.length < 1) return; if (!this.lineItems || this.lineItems.length < 1) return;
if (this.showAsPaid) return 0; if (this.showAsPaid) return 0;
return this.isInsurance || !this.shouldHideRecalibration if (this.isInsurance || !this.shouldHideRecalibration) {
? getAmountDue(this.lineItems) // calculate with recal (if on order) return this.donationCartItem
: getAmountDue(this.lineItemsWithoutRecal); // calculated without recal ? getAmountDueWithDonation(this.lineItems, this.donationCartItem)
: getAmountDue(this.lineItems);
} else {
return this.donationCartItem
? getAmountDueWithDonation(this.lineItemsWithoutRecal, this.donationCartItem)
: getAmountDue(this.lineItemsWithoutRecal);
}
}, },
amountPaid() { amountPaid() {
if (!this.showAsPaid) { if (!this.showAsPaid) {
@ -1309,8 +1290,9 @@ export default {
.sub-total, .sub-total,
.sales-tax, .sales-tax,
.amount-due, .amount-due,
.amount-paid { .amount-paid,
font-weight: 500; .donation-amount {
font-family: UrbanistSemibold;
color: $black; color: $black;
} }
.sub-total { .sub-total {

View file

@ -9,6 +9,7 @@ import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
import { getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper"; import { getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import { headerKeys } from "@/constants/header-keys"; import { headerKeys } from "@/constants/header-keys";
import axiosResponseInterceptorMessages from "@/constants/axios-response-interceptor-messages.js"; import axiosResponseInterceptorMessages from "@/constants/axios-response-interceptor-messages.js";
import { endpoints } from "@/constants/endpoints";
// Add a response interceptor for global axios error handing. // Add a response interceptor for global axios error handing.
axios.interceptors.response.use( axios.interceptors.response.use(
@ -100,34 +101,38 @@ export default {
return resolve(response); return resolve(response);
}, },
(error) => { (error) => {
if (logApiCall) { if (endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url)) {
analyticsMixIn.methods.pushEventToGA( return resolve({ data: null, error: "Ignore errors when logging" });
GaCategories.API_RESPONSE, } else {
GaActions.RESULT, if (logApiCall) {
`${GaLabels.ERROR}_${endpoint}`, analyticsMixIn.methods.pushEventToGA(
true GaCategories.API_RESPONSE,
); GaActions.RESULT,
`${GaLabels.ERROR}_${endpoint}`,
true
);
}
if (error.response.status && error.response.status != "404") {
// Do not route to error logic when no wipers found or no promo found (404s)
const errorPayload = {
cause: `Response error ${error.response.status}`,
currentPage: pageNameToLog,
endpoint: endpoint,
};
router.bailout(errorPayload);
// do not log 404 errors from services because we return NotFound
// when a service doesn't return an object
global.$logger.logError(
`${method}: ${endpoint}: ${error.message}`,
error.response
);
}
return reject(error.response);
} }
if (error.response.status && error.response.status != "404") {
// Do not route to error logic when no wipers found or no promo found (404s)
const errorPayload = {
cause: `Response error ${error.response.status}`,
currentPage: pageNameToLog,
endpoint: endpoint,
};
router.bailout(errorPayload);
// do not log 404 errors from services because we return NotFound
// when a service doesn't return an object
global.$logger.logError(
`${method}: ${endpoint}: ${error.message}`,
error.response
);
}
return reject(error.response);
} }
); );
}); });

View file

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

View file

@ -1,6 +1,7 @@
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { deepClone } from "@/helpers/object-helper";
export function getDisplayAmountDue(lineItemsObject, includeTax = true) { export function getDisplayAmountDue(lineItemsObject, includeTax = true) {
return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", { return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", {
@ -62,6 +63,13 @@ export function getSalesTax(lineItemsObject) {
); );
} }
export function getAmountDueWithDonation(lineItemsObject, donationLineItem) {
// this is amount due with Donation added
const lineItemsCloneWithDonation = deepClone(lineItemsObject);
lineItemsCloneWithDonation.supportingItems.push(donationLineItem);
return getAmountDue(lineItemsCloneWithDonation);
}
export async function getPricingByDayPartWithPrice(pageNameToLog) { export async function getPricingByDayPartWithPrice(pageNameToLog) {
// Get the Pricing By Day Part // Get the Pricing By Day Part
const basePriceByDayPart = await baseMixin.methods.dispatchStoreActionWithLogging( const basePriceByDayPart = await baseMixin.methods.dispatchStoreActionWithLogging(

View file

@ -55,7 +55,8 @@
:damage="damageInfo" :damage="damageInfo"
:availableVaps="vaps" :availableVaps="vaps"
:allowItemRemoval="false" :allowItemRemoval="false"
v-model="lineItems" v-model="lineItemsWithoutDonation"
:donationCartItem="donationLineItem"
:showAsPaid="isPia" :showAsPaid="isPia"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
:isInsurance="isInsurance" :isInsurance="isInsurance"
@ -123,6 +124,7 @@ import experimentMixin from "@/mixins/experiment-mixin.js";
import { containsRecalParts } from "@/helpers/recal-helper.js"; import { containsRecalParts } from "@/helpers/recal-helper.js";
import donationBlock from "@/experiment-components/donation-block.vue"; import donationBlock from "@/experiment-components/donation-block.vue";
import { partNumberStrings } from "@/constants/part-number-strings"; import { partNumberStrings } from "@/constants/part-number-strings";
import { partTypeStrings } from "@/constants/part-type-strings";
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
export default { export default {
@ -229,7 +231,7 @@ export default {
}; };
const donationItems = lineItemsFromSubmittedOrder.supportingItems?.filter( const donationItems = lineItemsFromSubmittedOrder.supportingItems?.filter(
(item) => item.partNumber === partNumberStrings.DONATION (item) => item.partType === partTypeStrings.DONATION
); );
const donationAmount = donationItems?.length > 0 ? donationItems[0].sellingPrice : 0; const donationAmount = donationItems?.length > 0 ? donationItems[0].sellingPrice : 0;
@ -243,7 +245,7 @@ export default {
vm.shouldDisplayFosterLove = hasFosterLoveExperiment; vm.shouldDisplayFosterLove = hasFosterLoveExperiment;
vm.donationAmount = donationAmount; vm.donationAmount = donationAmount;
vm.showDonationSuccess = lineItemsFromSubmittedOrder.supportingItems?.some( vm.showDonationSuccess = lineItemsFromSubmittedOrder.supportingItems?.some(
(item) => item.partType === partNumberStrings.DONATION (item) => item.partType === partTypeStrings.DONATION
); );
vm.pushAnalytics(); vm.pushAnalytics();
@ -490,6 +492,22 @@ export default {
donationValues() { donationValues() {
return [1, 3, 5]; return [1, 3, 5];
}, },
lineItemsWithoutDonation() {
const lineItemsClone = deepClone(this.lineItems);
lineItemsClone.supportingItems = lineItemsClone.supportingItems?.filter(
(item) => item.partType !== partTypeStrings.DONATION
);
return lineItemsClone;
},
donationLineItem() {
if (!this.lineItems) return null;
const donationLineItems = this.lineItems.supportingItems?.filter(
(item) => item.partType === partTypeStrings.DONATION
);
let output =
donationLineItems && donationLineItems.length > 0 ? donationLineItems[0] : null;
return output;
},
}, },
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {

View file

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

View file

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

View file

@ -46,7 +46,7 @@
<afterpayModalBanner <afterpayModalBanner
v-if="showAfterpayBanner" v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget" cmsWidgetName="AfterpayModalWidget"
:isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay" :afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
:isInsuranceSelected="isInsuranceSelected" :isInsuranceSelected="isInsuranceSelected"
:lineItems="lineItems" /> :lineItems="lineItems" />
@ -561,8 +561,12 @@ export default {
skipToInsurance: null, skipToInsurance: null,
}; };
}, },
mounted() { async mounted() {
this.attachCustomEventsForAnalytics(); this.attachCustomEventsForAnalytics();
await this.$nextTick();
this.$refs?.servicePackage?.savePackageInfoToPageData?.();
}, },
computed: { computed: {
lineItemsCloneForWatcher() { lineItemsCloneForWatcher() {
@ -591,10 +595,7 @@ export default {
); );
}, },
showAfterpayBanner() { showAfterpayBanner() {
return ( return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
(this.isAfterpayBreakoutDisplay || !this.isInsuranceSelected) &&
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
);
}, },
isRecalPriceRemove() { isRecalPriceRemove() {
return ( return (
@ -606,14 +607,12 @@ export default {
showRecalDisclaimer() { showRecalDisclaimer() {
return this.isRecalibrationOnOrder && this.isRecalPriceRemove; return this.isRecalibrationOnOrder && this.isRecalPriceRemove;
}, },
isAfterpayBreakoutDisplay() { afterpayExtendedPayOptionThreshold() {
return ( let thresholdAmount = experimentMixin.methods.getSettingValue(
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) && experimentSettings.AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
"true"
)
); );
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
return thresholdAmount;
}, },
}, },
methods: { methods: {

View file

@ -44,6 +44,7 @@ import {
import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-helper"; import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-helper";
import experimentMixin from "@/mixins/experiment-mixin.js"; import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { savePageData } from "@/router/methods/helpers/save-page-data";
export default { export default {
name: "servicePackageQuestion", name: "servicePackageQuestion",
@ -434,6 +435,15 @@ export default {
return null; return null;
} }
}, },
async savePackageInfoToPageData() {
const answers = this.servicePackageAnswers;
const toLog = answers.map((ans) => ({
packageName: ans.buttonLabel,
price: ans.buttonAuxillaryCopy,
}));
await savePageData(this.pageName, { packages: toLog });
},
}, },
components: { components: {
buttonQuestion, buttonQuestion,

View file

@ -542,10 +542,17 @@ export default {
forwardButtonAction() { forwardButtonAction() {
this.updateSupportingItems(); this.updateSupportingItems();
if (this.waitListRequested) { if (this.displayWaitList) {
var gaLabel = store.getters.isMobileAppointment let gaLabel = "";
? "checked_mobile" let status = this.waitListRequested ? "checked" : "unchecked";
: "checked_inshop"; if (
this.waitListRequested === null &&
this.$store.state.order.customer.waitListRequested === true
) {
status = "checked";
}
const type = store.getters.isMobileAppointment ? "mobile" : "inshop";
gaLabel = `${status}_${type}`;
const currentDate = new Date(); const currentDate = new Date();
const dateString = this.selectableDatesData.days[0].date; const dateString = this.selectableDatesData.days[0].date;

View file

@ -12,7 +12,8 @@ import {
setSessionKeyIfUnset, setSessionKeyIfUnset,
} from "@/helpers/heritage-integration/cookie-helper"; } from "@/helpers/heritage-integration/cookie-helper";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings, experimentUniverses } from "@/constants/experiments";
import experimentMixin from "@/mixins/experiment-mixin";
import { import {
analyticsPageEvents, analyticsPageEvents,
GaCategories, GaCategories,
@ -128,6 +129,60 @@ export default {
} }
}, },
async logDigitalConsumer() {
const currentPageName = getPageNameFromRouter();
const universes = store.getters.applicationUser.experiments;
let hasDynamoLogging = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DYNAMO_LOGGING,
"true",
universes
);
if (!hasDynamoLogging) {
return;
}
const variationNames = universes
.filter((item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL)
.map((item) => item.variationName)
.filter(Boolean); // removes undefined/null
const conceptVariation = variationNames.length > 0 ? variationNames[0] : "";
const isConceptExposed = universes.find(
(item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL
)?.isExposed;
const submittedOrder = baseMixin.methods.getSubmittedOrder();
const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder();
const hasSubmittedOrderAtConfirmationPage =
hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name;
var payload = {
actionName: `Browser page:${currentPageName}`,
referralSequenceNumber: hasSubmittedOrderAtConfirmationPage
? submittedOrder.referralSequenceNumber
: store.getters.order.referralSequenceNumber,
referralNumber: hasSubmittedOrderAtConfirmationPage
? submittedOrder.referralNumber
: store.getters.order.referralNumber,
workOrderId: hasSubmittedOrderAtConfirmationPage
? submittedOrder.workOrderId
: store.getters.order.workOrderId,
workOrderNumber: hasSubmittedOrderAtConfirmationPage
? submittedOrder.workOrderNumber
: store.getters.order.workOrderNumber,
conceptVariation: conceptVariation,
isConceptExposed: isConceptExposed,
};
await baseMixin.methods.dispatchStoreAction(
storeActions.LOG_DIGITALCONSUMER,
payload,
false
);
},
async pushEventForChatsToGA(category, action, label, pushToLogApp = false) { async pushEventForChatsToGA(category, action, label, pushToLogApp = false) {
const currentPageName = getPageNameFromRouter(); const currentPageName = getPageNameFromRouter();
const value = `2.0_${currentPageName}`; const value = `2.0_${currentPageName}`;

View file

@ -197,6 +197,18 @@ export default {
return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)]; return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)];
} }
}, },
isMobileDevice() {
const userAgent = navigator.userAgent;
return (
userAgent.includes("Android") ||
userAgent.includes("Mobile") ||
userAgent.includes("iPod") ||
userAgent.includes("iPhone") ||
userAgent.includes("IEMobile") ||
userAgent.includes("BlackBerry") ||
userAgent.includes("webOS")
);
},
getSubmittedOrder() { getSubmittedOrder() {
return JSON.parse( return JSON.parse(
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)

View file

@ -1,6 +1,9 @@
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
export async function afterEach(to, from) { export async function afterEach(to, from) {
// digital consumer logging
analyticsMixin.methods.logDigitalConsumer();
// Push page view to GA // Push page view to GA
analyticsMixin.methods.pushPageViewToGA(); analyticsMixin.methods.pushPageViewToGA();

View file

@ -11,7 +11,11 @@ import { experimentTriggers } from "@/constants/experiments";
import { damageLocationsSelected } from "@/constants/damage-locations-selected"; import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { singleWindshieldCarIds } from "@/constants/single-windshield-carids"; import { singleWindshieldCarIds } from "@/constants/single-windshield-carids";
import { routeData } from "@/router/constants/routes"; import { routeData } from "@/router/constants/routes";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js"; import {
deleteFunnelCookie,
getSessionIdValue,
getDeviceIdValue,
} from "@/helpers/heritage-integration/cookie-helper.js";
import { deepEqual } from "@/helpers/object-helper"; import { deepEqual } from "@/helpers/object-helper";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { import {
@ -1551,6 +1555,38 @@ export const actions = {
}); });
}, },
logDigitalConsumer(
context,
{
actionName,
referralSequenceNumber,
referralNumber,
workOrderId,
workOrderNumber,
conceptVariation,
isConceptExposed,
}
) {
var payload = {
sessionId: getSessionIdValue(),
deviceId: getDeviceIdValue(),
actionName: actionName ?? "",
referralSequenceNumber: referralSequenceNumber ?? "",
referralNumber: referralNumber ?? "",
applicationName: baseMixin.methods.isMobileDevice() ? "2.0 Mobile" : "2.0",
workOrderId: workOrderId ?? "",
workOrderNumber: workOrderNumber ?? "",
conceptVariation: conceptVariation,
isConceptExposed: isConceptExposed,
};
return globalMethods.callHttpClient({
method: endpoints.LogDigitalConsumer.method,
endpoint: endpoints.LogDigitalConsumer.url,
payload: payload,
});
},
// Misc Actions // Misc Actions
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId, eon }) { setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId, eon }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);

View file

@ -133,4 +133,4 @@ caption,
url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff"); url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }

View file

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