Merge pull request #2643 from Safelite/feature/7.17-without-schedule-parity
7.17 release without schedule parity
This commit is contained in:
commit
63cc08459d
23 changed files with 400 additions and 141 deletions
|
|
@ -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! */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap");
|
||||
body {
|
||||
font-family: Roboto;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
background-color: white;
|
||||
padding: 8px;
|
||||
}
|
||||
body h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
body .validation-info {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -136,6 +140,7 @@ body .buttonContainer button {
|
|||
border-radius: 8px;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
|
|
@ -268,6 +273,7 @@ body .cc-error-container #alert-message {
|
|||
#fmg-checkout-shared #other-payments-container label span.paymentOptionText {
|
||||
margin: auto 0;
|
||||
padding-left: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
#fmg-checkout-shared #other-payments-container #afterpayParentLink label > span:first-of-type {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -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! */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap");
|
||||
body {
|
||||
font-family: Roboto;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
background-color: white;
|
||||
padding: 8px;
|
||||
|
||||
h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.validation-info {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -146,6 +151,7 @@ body {
|
|||
border-radius: 8px;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
|
|
@ -325,6 +331,7 @@ body {
|
|||
&.paymentOptionText {
|
||||
margin: auto 0;
|
||||
padding-left: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,10 @@ const endpoints = {
|
|||
url: "/analytics/api/v1/analytics/log-part-questions",
|
||||
method: "POST",
|
||||
},
|
||||
LogDigitalConsumer: {
|
||||
url: "/analytics/api/v1/analytics/digitalconsumer-log",
|
||||
method: "POST",
|
||||
},
|
||||
GetExperimentsByUser: {
|
||||
url: "/analytics/api/v1/analytics/get-experiments",
|
||||
method: "GET",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const experimentSettings = {
|
|||
FOSTER_LOVE: "DisplayFosterLove",
|
||||
SKIP_TO_INSURANCE: "SkipToInsurance",
|
||||
DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay",
|
||||
AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD: "AfterPayExtendedPayOptionThreshold",
|
||||
DYNAMO_LOGGING: "DynamoLogging",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ const storeActions = {
|
|||
LOG_CUSTOM_EVENT: "logCustomEvent",
|
||||
INITIALIZE_SESSION: "initializeSession",
|
||||
LOG_PART_QUESTIONS: "logPartQuestions",
|
||||
LOG_DIGITALCONSUMER: "logDigitalConsumer",
|
||||
|
||||
// DEPENDENCY MUTATIONS
|
||||
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleStateAndDependencies",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
<script>
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
import store from "@/store";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
|
||||
export default {
|
||||
name: "sierraWebchat",
|
||||
mixins: [analyticsMixin],
|
||||
|
|
@ -24,7 +28,10 @@ export default {
|
|||
},
|
||||
createSierraConfig() {
|
||||
return {
|
||||
variables: { application: "funnel" },
|
||||
variables: {
|
||||
application: "funnel",
|
||||
...this.getSierraContext(),
|
||||
},
|
||||
display: "corner",
|
||||
onLoad: () => this.openSierraChatModal(),
|
||||
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() {
|
||||
window.sierraConfig = this.createSierraConfig();
|
||||
// Preload CSS if not already present
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row-align d-flex px-4">
|
||||
<div>
|
||||
<img class="w-100 mt-1 me-4" :src="LogoImage" />
|
||||
<div class="logo-block">
|
||||
<img class="w-100" :src="LogoImage" />
|
||||
<span>EIN: 26-3043727</span>
|
||||
<!-- This will likely never change so hard coded -->
|
||||
</div>
|
||||
<div>
|
||||
<p class="small subheadline mb-2" v-html="DonationSubHeadline"></p>
|
||||
|
|
@ -79,7 +81,8 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<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>
|
||||
</form>
|
||||
|
|
@ -121,6 +124,9 @@ export default {
|
|||
DonationFooterText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
DonationFooterText2() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
|
||||
},
|
||||
LogoImage() {
|
||||
return this.getCmsContent("DonationWidget", "Image");
|
||||
},
|
||||
|
|
@ -195,8 +201,26 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 5rem;
|
||||
.logo-block {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@
|
|||
<span>{{ amountPaidText }}</span>
|
||||
<span>{{ getLineItemAmount(amountPaid) }}</span>
|
||||
</div>
|
||||
<div v-if="donationCartItem" class="donation-amount">
|
||||
<span>{{ donationCartItemName }}</span>
|
||||
<span>{{ getLineItemAmount(donationCartItem.sellingPrice) }}</span>
|
||||
</div>
|
||||
<div class="amount-due">
|
||||
<span>{{ amountDueText }}</span>
|
||||
<span>{{ getLineItemAmount(amountDue, showCoverageAsPending) }}</span>
|
||||
|
|
@ -178,6 +182,7 @@ import {
|
|||
getAmountDue,
|
||||
getSubTotal,
|
||||
getSalesTax,
|
||||
getAmountDueWithDonation,
|
||||
} from "@/helpers/pricing-helper.js";
|
||||
|
||||
// Constants
|
||||
|
|
@ -207,6 +212,7 @@ export default {
|
|||
isNoComp: Boolean,
|
||||
isExpandedOnLoad: Boolean,
|
||||
isMSRFeeApplicable: Boolean,
|
||||
donationCartItem: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -472,10 +478,6 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.donationCartItem) {
|
||||
cartItems.push(this.donationCartItem);
|
||||
}
|
||||
|
||||
return cartItems;
|
||||
},
|
||||
},
|
||||
|
|
@ -1083,33 +1085,6 @@ export default {
|
|||
donationCartItemName() {
|
||||
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() {
|
||||
return this.getCmsContent("RemoveCartItemTextWidget", "Text");
|
||||
},
|
||||
|
|
@ -1156,9 +1131,15 @@ export default {
|
|||
amountDue() {
|
||||
if (!this.lineItems || this.lineItems.length < 1) return;
|
||||
if (this.showAsPaid) return 0;
|
||||
return this.isInsurance || !this.shouldHideRecalibration
|
||||
? getAmountDue(this.lineItems) // calculate with recal (if on order)
|
||||
: getAmountDue(this.lineItemsWithoutRecal); // calculated without recal
|
||||
if (this.isInsurance || !this.shouldHideRecalibration) {
|
||||
return this.donationCartItem
|
||||
? getAmountDueWithDonation(this.lineItems, this.donationCartItem)
|
||||
: getAmountDue(this.lineItems);
|
||||
} else {
|
||||
return this.donationCartItem
|
||||
? getAmountDueWithDonation(this.lineItemsWithoutRecal, this.donationCartItem)
|
||||
: getAmountDue(this.lineItemsWithoutRecal);
|
||||
}
|
||||
},
|
||||
amountPaid() {
|
||||
if (!this.showAsPaid) {
|
||||
|
|
@ -1309,8 +1290,9 @@ export default {
|
|||
.sub-total,
|
||||
.sales-tax,
|
||||
.amount-due,
|
||||
.amount-paid {
|
||||
font-weight: 500;
|
||||
.amount-paid,
|
||||
.donation-amount {
|
||||
font-family: UrbanistSemibold;
|
||||
color: $black;
|
||||
}
|
||||
.sub-total {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
|
|||
import { getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { headerKeys } from "@/constants/header-keys";
|
||||
import axiosResponseInterceptorMessages from "@/constants/axios-response-interceptor-messages.js";
|
||||
import { endpoints } from "@/constants/endpoints";
|
||||
|
||||
// Add a response interceptor for global axios error handing.
|
||||
axios.interceptors.response.use(
|
||||
|
|
@ -100,34 +101,38 @@ export default {
|
|||
return resolve(response);
|
||||
},
|
||||
(error) => {
|
||||
if (logApiCall) {
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
GaCategories.API_RESPONSE,
|
||||
GaActions.RESULT,
|
||||
`${GaLabels.ERROR}_${endpoint}`,
|
||||
true
|
||||
);
|
||||
if (endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url)) {
|
||||
return resolve({ data: null, error: "Ignore errors when logging" });
|
||||
} else {
|
||||
if (logApiCall) {
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -339,3 +339,7 @@ export function splitCMSCopyOnParagraphTag(copy) {
|
|||
// filter removes empty strings that are a result of string.split with regex
|
||||
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
|
||||
}
|
||||
|
||||
export function splitCMSCopyOnBR(copy) {
|
||||
return copy.split("<br>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import store from "@/store";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
|
||||
export function getDisplayAmountDue(lineItemsObject, includeTax = true) {
|
||||
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) {
|
||||
// Get the Pricing By Day Part
|
||||
const basePriceByDayPart = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@
|
|||
:damage="damageInfo"
|
||||
:availableVaps="vaps"
|
||||
:allowItemRemoval="false"
|
||||
v-model="lineItems"
|
||||
v-model="lineItemsWithoutDonation"
|
||||
:donationCartItem="donationLineItem"
|
||||
:showAsPaid="isPia"
|
||||
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||
:isInsurance="isInsurance"
|
||||
|
|
@ -123,6 +124,7 @@ import experimentMixin from "@/mixins/experiment-mixin.js";
|
|||
import { containsRecalParts } from "@/helpers/recal-helper.js";
|
||||
import donationBlock from "@/experiment-components/donation-block.vue";
|
||||
import { partNumberStrings } from "@/constants/part-number-strings";
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
|
||||
export default {
|
||||
|
|
@ -229,7 +231,7 @@ export default {
|
|||
};
|
||||
|
||||
const donationItems = lineItemsFromSubmittedOrder.supportingItems?.filter(
|
||||
(item) => item.partNumber === partNumberStrings.DONATION
|
||||
(item) => item.partType === partTypeStrings.DONATION
|
||||
);
|
||||
const donationAmount = donationItems?.length > 0 ? donationItems[0].sellingPrice : 0;
|
||||
|
||||
|
|
@ -243,7 +245,7 @@ export default {
|
|||
vm.shouldDisplayFosterLove = hasFosterLoveExperiment;
|
||||
vm.donationAmount = donationAmount;
|
||||
vm.showDonationSuccess = lineItemsFromSubmittedOrder.supportingItems?.some(
|
||||
(item) => item.partType === partNumberStrings.DONATION
|
||||
(item) => item.partType === partTypeStrings.DONATION
|
||||
);
|
||||
|
||||
vm.pushAnalytics();
|
||||
|
|
@ -490,6 +492,22 @@ export default {
|
|||
donationValues() {
|
||||
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: {
|
||||
arePagePrerequisitesValid() {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@
|
|||
<span>Amount Due</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>6 or 12 monthly payment plans available</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -208,6 +210,12 @@ export default {
|
|||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
> p {
|
||||
padding-top: 1rem;
|
||||
font-size: $font-size-12;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.payment-section {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div class="alert fade show my-2 py-2 border-0 alert-info" role="alert">
|
||||
<div id="afterpay-banner" role="alert">
|
||||
<component
|
||||
:is="'script'"
|
||||
src="https://js.squarecdn.com/square-marketplace.js"
|
||||
async></component>
|
||||
|
||||
<div
|
||||
class="mx-4 my-0 alert-heading text-center"
|
||||
:class="[isAfterpayBreakoutDisplay ? 'fw-bolder' : 'fw-bold']">
|
||||
<div>
|
||||
<span v-for="token in headerCopyTokens" :key="token" v-html="token"></span>
|
||||
</div>
|
||||
<div :id="showAfterpayExtendedPayOption ? 'extended-pay-option' : ''">
|
||||
<span v-for="token in afterpayCopyTokens" :key="token">
|
||||
<span v-if="isAfterpayPriceToken(token)">{{ afterpayPrice }}</span>
|
||||
<span v-else-if="isInlineImageToken(token)">
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper";
|
||||
import { getArrayOfAllLineItemsAndChildParts } from "@/store";
|
||||
|
|
@ -55,8 +56,8 @@ export default {
|
|||
props: {
|
||||
cmsWidgetName: String,
|
||||
lineItems: Array,
|
||||
isAfterpayBreakoutDisplay: Boolean,
|
||||
isInsuranceSelected: Boolean,
|
||||
afterpayExtendedPayOptionThreshold: Number,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
|
@ -73,27 +74,24 @@ export default {
|
|||
hasImage() {
|
||||
return !!this.imageUrl;
|
||||
},
|
||||
headerCopyTokens() {
|
||||
return splitCMSCopyOnBR(this.getCmsContent(this.cmsWidgetName, "HeaderText"));
|
||||
},
|
||||
afterpayCopyTokens() {
|
||||
if (this.isAfterpayBreakoutDisplay) {
|
||||
// Text for Afterpay Breakout experiment
|
||||
var afterpayBannerCopy = this.getCmsContent(this.cmsWidgetName, "BodyText2");
|
||||
afterpayBannerCopy =
|
||||
afterpayBannerCopy &&
|
||||
afterpayBannerCopy.replaceAll(
|
||||
"{custom:paymentType}",
|
||||
this.isInsuranceSelected ? "deductible" : "spending"
|
||||
);
|
||||
return splitCopyOnCMSPlaceHolder(afterpayBannerCopy);
|
||||
if (this.showAfterpayExtendedPayOption) {
|
||||
return splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.cmsWidgetName, "BodyText")
|
||||
);
|
||||
} else {
|
||||
return splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.cmsWidgetName, "HeaderText")
|
||||
this.getCmsContent(this.cmsWidgetName, "BodyText2")
|
||||
);
|
||||
}
|
||||
},
|
||||
modalCopy() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
afterpayPrice() {
|
||||
getTierOnePackagePrice() {
|
||||
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
|
||||
if (this.lineItems.promos) {
|
||||
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT");
|
||||
|
|
@ -103,11 +101,6 @@ export default {
|
|||
baseMixin.methods.filterOutFees(allLineItems)
|
||||
);
|
||||
|
||||
let vapsLineItemsForSelectedPackage = this.lineItems.vaps;
|
||||
vapsLineItemsForSelectedPackage.forEach((lineItem) => {
|
||||
price += baseMixin.methods.getTotalLineItemPrice(lineItem);
|
||||
});
|
||||
|
||||
if (this.lineItems.promos) {
|
||||
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
|
||||
const promos = getPromosThatMatchLineItemsOnOrder(
|
||||
|
|
@ -119,9 +112,13 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
const adjusted = (price / 4).toFixed(2);
|
||||
|
||||
return `$${adjusted}`;
|
||||
return price;
|
||||
},
|
||||
showAfterpayExtendedPayOption() {
|
||||
return (
|
||||
this.afterpayExtendedPayOptionThreshold &&
|
||||
this.getTierOnePackagePrice >= this.afterpayExtendedPayOptionThreshold
|
||||
);
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
|
|
@ -129,28 +126,54 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.alert {
|
||||
padding: 0.675rem 0;
|
||||
&.alert-info {
|
||||
background-color: $blue-100;
|
||||
.alert-heading {
|
||||
color: $blue-700;
|
||||
#afterpay-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $blue-150;
|
||||
justify-content: center;
|
||||
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 {
|
||||
font-weight: 600;
|
||||
}
|
||||
img {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
svg {
|
||||
fill: $blue-700;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: column;
|
||||
border-right: 1px solid;
|
||||
padding-right: 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 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<afterpayModalBanner
|
||||
v-if="showAfterpayBanner"
|
||||
cmsWidgetName="AfterpayModalWidget"
|
||||
:isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay"
|
||||
:afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
:lineItems="lineItems" />
|
||||
|
||||
|
|
@ -561,8 +561,12 @@ export default {
|
|||
skipToInsurance: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.attachCustomEventsForAnalytics();
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
this.$refs?.servicePackage?.savePackageInfoToPageData?.();
|
||||
},
|
||||
computed: {
|
||||
lineItemsCloneForWatcher() {
|
||||
|
|
@ -591,10 +595,7 @@ export default {
|
|||
);
|
||||
},
|
||||
showAfterpayBanner() {
|
||||
return (
|
||||
(this.isAfterpayBreakoutDisplay || !this.isInsuranceSelected) &&
|
||||
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
|
||||
);
|
||||
return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
|
||||
},
|
||||
isRecalPriceRemove() {
|
||||
return (
|
||||
|
|
@ -606,14 +607,12 @@ export default {
|
|||
showRecalDisclaimer() {
|
||||
return this.isRecalibrationOnOrder && this.isRecalPriceRemove;
|
||||
},
|
||||
isAfterpayBreakoutDisplay() {
|
||||
return (
|
||||
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) &&
|
||||
experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
|
||||
"true"
|
||||
)
|
||||
afterpayExtendedPayOptionThreshold() {
|
||||
let thresholdAmount = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD
|
||||
);
|
||||
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
|
||||
return thresholdAmount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-helper";
|
||||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||
|
||||
export default {
|
||||
name: "servicePackageQuestion",
|
||||
|
|
@ -434,6 +435,15 @@ export default {
|
|||
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: {
|
||||
buttonQuestion,
|
||||
|
|
|
|||
|
|
@ -542,10 +542,17 @@ export default {
|
|||
forwardButtonAction() {
|
||||
this.updateSupportingItems();
|
||||
|
||||
if (this.waitListRequested) {
|
||||
var gaLabel = store.getters.isMobileAppointment
|
||||
? "checked_mobile"
|
||||
: "checked_inshop";
|
||||
if (this.displayWaitList) {
|
||||
let gaLabel = "";
|
||||
let status = this.waitListRequested ? "checked" : "unchecked";
|
||||
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 dateString = this.selectableDatesData.days[0].date;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {
|
|||
setSessionKeyIfUnset,
|
||||
} from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { experimentSettings, experimentUniverses } from "@/constants/experiments";
|
||||
import experimentMixin from "@/mixins/experiment-mixin";
|
||||
import {
|
||||
analyticsPageEvents,
|
||||
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) {
|
||||
const currentPageName = getPageNameFromRouter();
|
||||
const value = `2.0_${currentPageName}`;
|
||||
|
|
|
|||
|
|
@ -197,6 +197,18 @@ export default {
|
|||
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() {
|
||||
return JSON.parse(
|
||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
|
||||
export async function afterEach(to, from) {
|
||||
// digital consumer logging
|
||||
analyticsMixin.methods.logDigitalConsumer();
|
||||
|
||||
// Push page view to GA
|
||||
analyticsMixin.methods.pushPageViewToGA();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import { experimentTriggers } from "@/constants/experiments";
|
|||
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
|
||||
import { singleWindshieldCarIds } from "@/constants/single-windshield-carids";
|
||||
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 baseMixin from "@/mixins/base-mixin.js";
|
||||
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
|
||||
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId, eon }) {
|
||||
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
|
||||
|
|
|
|||
|
|
@ -133,4 +133,4 @@ caption,
|
|||
url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ $black-100: #0a0a0a;
|
|||
|
||||
// Blues
|
||||
$blue-100: #e4f1f7; // Used in theme
|
||||
$blue-150: #e5f1fa;
|
||||
$blue-200: #c1dfee;
|
||||
$blue-300: #9fcee6;
|
||||
$blue-400: #69adcf; // Used in theme
|
||||
|
|
@ -59,6 +60,7 @@ $gray: #b0b3b3; // Default Gray
|
|||
$gray-500: #8e9292; // Used in theme
|
||||
$gray-550: #727676; // Used in theme
|
||||
$gray-600: #4d5151; // Used in theme
|
||||
$gray-650: #525656;
|
||||
$gray-700: #303333; // Used in theme
|
||||
$gray-800: #222424; // Used in theme
|
||||
$gray-900: #181a1a; // Used in theme
|
||||
|
|
@ -121,13 +123,16 @@ $body-color: $gray-600;
|
|||
|
||||
//Fonts
|
||||
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||
$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
$font-family-monospace:
|
||||
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
// stylelint-enable value-keyword-case
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
$font-family-code: $font-family-monospace;
|
||||
$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)
|
||||
$font-size-xsm: $font-size-base * 0.75;
|
||||
$font-sizes: (
|
||||
|
|
|
|||
Loading…
Reference in a new issue