Merge branch 'develop' into jnou/add-oem-test

This commit is contained in:
JennyNou 2025-11-14 15:54:29 -05:00 committed by GitHub
commit 557fe7cf9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 433 additions and 143 deletions

View file

@ -114,7 +114,7 @@ body {
debug: true,
},
{
name: 'Test',
name: 'SysTest',
hostName: 'www-test2.safelite.com',
apiHostname: 'digitalapi.test.safelite.io',
debug: true,
@ -142,83 +142,178 @@ body {
return match;
}
function getExperimentSettingsFromVuex(vuexData) {
const experiments = vuexData?.applicationUser?.experiments;
if(experiments) {
return experiments
.filter((e) => !!e.isActive)
.map((e) => e.settings)
.reduce((prev, next) => Object.assign(prev, next), {}) ?? {};
}
return {};
}
function extractDataFromVuex(existingVuexData) {
let bailoutInfo = [];
let headerInfo = [];
try {
// =============== Collect diagnostic data:
bailoutInfo = [];
// App & Site Name
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
// Not included (yet) from heritage:
// - Site ID
// - Code
// - Module
// - Page
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
// - URL
// - Server
const sessionId = getCookieValueByName('sid');
bailoutInfo.push({ name: 'Session Id', value: sessionId });
const userAgent = window.navigator.userAgent;
bailoutInfo.push({ name: 'User Agent', value: userAgent });
// - IP
// - Session Log Sequence Number
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
// - Referral Insured Zip
// - Referral Insured Service Zip
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
const vehicle = existingVuexData.order?.vehicle;
const vehicleString = vehicle?.year
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
: null;
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
const environmentData = getCurrentEnvironmentData();
const userIdCookieName = `FunnelUserId-${environmentData?.name}`;
bailoutInfo.push({ name: 'User Id', value: getCookieValueByName(userIdCookieName) });
// - Parent Account Name
// - Client GUID
// =============== Collect header data:
headerInfo = [];
headerInfo.push({ name: 'X-Experiment-Data', value: JSON.stringify(getExperimentSettingsFromVuex(existingVuexData)) });
headerInfo.push({ name: 'X-Application-Name', value: 'FixMyGlass' });
const sessionKeyCookieName = `FunnelSessionKey-${environmentData?.name}`;
headerInfo.push({ name: 'X-Session-Sequence-Number', value: getCookieValueByName(sessionKeyCookieName) });
headerInfo.push({ name: 'X-Referral-Sequence-Number', value: existingVuexData?.order?.referralSequenceNumber });
headerInfo.push({ name: 'X-Enterprise-Order-Number', value: existingVuexData?.order?.eon });
headerInfo.push({ name: 'log-enabled', value: existingVuexData?.applicationUser?.loggingOption ?? false });
} finally {
return {
headerInfo: headerInfo,
bailoutInfo: bailoutInfo
};
}
}
function generateRequestHeader(headerInfo) {
let headers = {};
headerInfo.forEach(
keyPair => {
headers[keyPair.name] = keyPair.value;
}
);
headers['X-Transaction-Id'] = crypto.randomUUID();
headers['Content-Type'] = 'application/json';
return headers;
}
function getFieldWithName(info, name) {
const match = info?.find(
(row) => row.name === name
);
return match?.value;
}
function clearApplicationData() {
window.localStorage.removeItem('vuex');
const environmentData = getCurrentEnvironmentData();
const cookieNames = [
`FunnelUserId-${environmentData?.name}`,
`FunnelSessionKey-${environmentData?.name}`,
`FunnelSessionInfo-${environmentData?.name}`,
`sid`,
`dxdev`,
];
cookieNames.forEach(
(cookieName) => {
let cookieToAdd = `${cookieName}=undefined; path=/; domain=${environmentData.hostName}; max-age=0`;
document.cookie = cookieToAdd;
}
)
}
window.onload = async () => {
// =============== Check for session info:
const existingVuexDataJSON = window.localStorage.getItem('vuex');
const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null;
const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo');
const existingBailoutInfo = existingBailoutInfoJSON ? JSON.parse(existingBailoutInfoJSON) : null;
const existingHeaderInfoJSON = window.sessionStorage.getItem('headerInfo');
const existingHeaderInfo = existingHeaderInfoJSON ? JSON.parse(existingHeaderInfoJSON) : null;
console.log(`================ VUEX DATA`);
console.log(existingVuexData);
console.log(`================ PRIOR BAILOUT DATA`);
console.log(existingBailoutInfo);
console.log(`================ PRIOR HEADER DATA`);
console.log(existingHeaderInfo);
let bailoutInfo = null;
let headerInfo = null;
if(existingVuexData) {
try {
// =============== Collect diagnostic data:
bailoutInfo = [];
const results = extractDataFromVuex(existingVuexData);
bailoutInfo = results.bailoutInfo;
headerInfo = results.headerInfo;
// App & Site Name
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
clearApplicationData();
// Not included (yet) from heritage:
// - Site ID
// - Code
// - Module
// - Page
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
// - URL
// - Server
const sessionId = getCookieValueByName('sid');
bailoutInfo.push({ name: 'Session Id', value: sessionId });
const userAgent = window.navigator.userAgent;
bailoutInfo.push({ name: 'User Agent', value: userAgent });
// - IP
// - Session Log Sequence Number
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
// - Referral Insured Zip
// - Referral Insured Service Zip
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
const vehicle = existingVuexData.order?.vehicle;
const vehicleString = vehicle?.year
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
: null;
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
// - Parent Account Name
// - Client GUID
} finally {
// If any information gathered, write to session storage.
if(bailoutInfo) {
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
}
// Then *always* clear vuex data.
window.localStorage.removeItem('vuex');
if(bailoutInfo) {
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
}
} else if(existingBailoutInfo) {
// Proceed with prior data.
bailoutInfo = existingBailoutInfo;
if(headerInfo) {
window.sessionStorage.setItem('headerInfo', JSON.stringify(headerInfo));
}
} else {
bailoutInfo = existingBailoutInfo ?? [];
headerInfo = existingHeaderInfo ?? [];
}
const environmentInfo = getCurrentEnvironmentData();
@ -275,11 +370,11 @@ body {
);
const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`;
const headers = generateRequestHeader(headerInfo);
const request = new Request(endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: headers,
body: JSON.stringify({
entry: entryString,
}),
@ -292,6 +387,38 @@ body {
console.error(`=== ERROR SENDING LOGGING`);
console.error(e);
}
try {
const endpointUrl = `https://${apiHostname}/analytics/api/v1/analytics/log-page-view`;
const headers = generateRequestHeader(headerInfo);
const payload = {
AppName: "FixMyGlass",
action: "",
applicationName: "FixMyGlassNextGen",
event: "ENTRY",
experimentsForUser: [],
pageName: "static-error",
parentAccountNumber: getFieldWithName(bailoutInfo, 'Parent Account Number') ?? 0,
referralSequenceNumber: getFieldWithName(bailoutInfo, 'Referral Seq Number'),
sessionId: getFieldWithName(bailoutInfo, 'Session Id'),
sessionKey: getFieldWithName(headerInfo, 'X-Session-Sequence-Number'),
shouldUseSessionId: false,
userId: getFieldWithName(bailoutInfo, 'User Id'),
};
const request = new Request(endpointUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
});
const response = await fetch(request);
} catch(e) {
console.error(`=== ERROR SENDING PAGEVIEW`);
console.error(e);
}
}
};
</script>

View file

@ -509,7 +509,7 @@
'AccountService': {
baseUrl: apiUrl + '/account/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/account/insurance-companies', method: 'GET' },
{ path: '/account/account-details/373341', method: 'GET' },
{ path: '/account/bill-to-account-number?ParentAccountNumber=373341&ProviderNumber=01820&IsItac=false&TypeOfClaim=Personal&LineOfBusiness=Commercial', method: 'GET' }
@ -518,7 +518,7 @@
'LocationService': {
baseUrl: apiUrl + '/location/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/location/zip/43015/Replace', method: 'GET' },
{ path: '/location/alert-reasons/03357', method: 'GET' },
{ path: '/location/providers/43015/Replace/100/167132/true/CR00046193/false', method: 'GET' },
@ -528,7 +528,7 @@
'VehicleService': {
baseUrl: apiUrl + '/vehicle/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/vehicle/years', method: 'GET' },
{ path: '/vehicle/makes/1997', method: 'GET' },
{ path: '/vehicle/Models/1997/Toyota', method: 'GET' },
@ -539,7 +539,7 @@
'PartService': {
baseUrl: apiUrl + '/parts/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/parts/damage-options/CR00046193', method: 'GET' },
{ path: '/parts/rain-repel', method: 'GET' },
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
@ -553,7 +553,7 @@
'PriceService': {
baseUrl: apiUrl + '/price/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/price/taxed-order-items?ParentAccountNumber=167132&BillToAccountNumber=87291&ProviderNumber=003357&AppointmentType=Mobile&ServiceLocation.City=asdf&ServiceLocation.State=OH&ServiceLocation.ZipCode=43015&lineItems[0].partNumber=FW02018GBNN&lineItems[0].laborAmount=60&lineItems[0].kitPrice=0&lineItems[0].sellingPrice=297.33&lineItems[1].partNumber=GGG%202018&lineItems[1].laborAmount=0&lineItems[1].kitPrice=0&lineItems[1].sellingPrice=13.4&lineItems[2].partNumber=RECYCLE%20FEE&lineItems[2].laborAmount=39.99&lineItems[2].kitPrice=0&lineItems[2].sellingPrice=0&ServerData=' + encodeURIComponent(environmentContext.serverData), method: 'GET' },
{ path: '/price/order-items', method: 'POST', body: { "parentAccountNumber": 167132, "billToAccountNumber": "87291", "providerNumber": "03357", "carId": "CR00046193", "year": 1997, "make": "Toyota", "model": "Camry", "eon": environmentContext.eon, "state": "OH", "referralNumber": environmentContext.referralNumber, "referralSequenceNumber": environmentContext.referralNumber, "zipCode": "43015", "serviceZipCode": "43015", "referralDate": "2025-10-29T10:55:27.18", "isReplacement": true, "deductible": 0, "coverageStatus": null, "lineItems": [ { "partNumber": "RECYCLE FEE" }, { "partNumber": "FW02018GBNN" }, { "partNumber": "GGG 2018" }, { "partNumber": "SBB19" }, { "partNumber": "SBB21" } ], "serverData": "", "AppName": "FixMyGlass" } },
{ path: '/price/order-promo', method: 'POST', body: {"promoCode":"WEBSL45","addableVaps":[{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null}],"order":{"appointmentType":"Mobile","carId":"CR00046193","correlationId":"b0261e48-1111-44fc-abc9-34b41380687c","eon":environmentContext.eon,"isInsurance":false,"isRepair":false,"glassToReplace":[{"location":"Windshield","name":"Single"}],"lineItemsOnOrder":[{"partNumber":"FW02018GBNN","basePartNumber":"FW02018","description":"solar","color":"Green Tint, Blue Shade","partType":"WINDSHIELD","canSafeliteRecalibrate":false,"requiresRecalibration":false,"requiresCapabilityQuestions":false,"recalibrationType":null,"childParts":[{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94}],"kitPrice":0,"sellingPrice":368.94,"laborAmount":60,"id":"4326cee6-b39e-41ff-bb29-df9220e32298","isChildPart":false,"salesTax":30.02},{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94},{"partNumber":"RECYCLE FEE","description":null,"partType":"REPLACE FEE","isInsurable":null,"isChildPart":false,"laborAmount":39.99,"sellingPrice":0,"kitPrice":0,"salesTax":2.8,"id":"3771aed5-0de1-4f0e-b1ad-394f73e08f38","cartItemType":"REPLACE FEE"},{"partNumber":"MOBILE FEE","description":null,"partType":"MOBILE FEE","isInsurable":true,"isChildPart":false,"laborAmount":0,"sellingPrice":0,"kitPrice":0,"salesTax":0,"id":"2bc936ec-ef5e-4fe2-95fb-e946792b1ae5"},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":3.15,"cartItemType":"RAIN REPEL"}],"parentAccountNumber":"167132","referralSequenceNumber":environmentContext.referralNumber,"serviceState":"OH","serverData":environmentContext.serverData,"vehicleYear":"1997","zipCodeOrProviderCtu":"03357"},"AppName":"FixMyGlass"} },
@ -563,7 +563,7 @@
'ScheduleService': {
baseUrl: apiUrl + '/schedule/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/schedule/mobile-time-slots', method: 'POST', body: {"startDate":startDate,"endDate":endDate,"applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"SBB19","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"SBB21","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"RAIN REPEL","partType":"RAIN REPEL","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"zipCode":"43015","AppName":"FixMyGlass"} },
{ path: '/schedule/shop-time-slots', method: 'POST', body: {"providerNumber":"003417","startDate":startDate,"endDate":endDate,"shopAppointmentType":"InshopOrDropoff","applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"AppName":"FixMyGlass"} }
]
@ -571,25 +571,25 @@
'AnalyticsService': {
baseUrl: apiUrl + '/analytics/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
]
},
'ExperimentService': {
baseUrl: apiUrl + '/experiments/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
]
},
'OrderService': {
baseUrl: apiUrl + '/order/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
]
},
'ContentService': {
baseUrl: apiUrl + '/content/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/content/fmg/vehicle', method: 'GET' },
]
}
@ -674,7 +674,7 @@
}
function updateAtAGlanceText() {
const healthyCount = Object.values(healthData).filter(service => service.status === 'healthy').length;
const healthyCount = Object.values(healthData).filter(service => (service.status === 'healthy' || service.status === 'degraded')).length;
const totalCount = Object.values(healthData).length;
let text = 'Healthy';
let className = 'stat-pill stat-pill-healthy';
@ -763,14 +763,15 @@
// Determine overall service status
const endpoints = healthData[serviceName].endpoints;
const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length;
const unhealthyCount = endpoints.filter(ep => isUnhealthy(ep)).length;
const totalCount = endpoints.length;
if (healthyCount === totalCount) {
healthData[serviceName].status = 'healthy';
} else if (healthyCount > 0) {
healthData[serviceName].status = 'degraded';
} else {
} else if (unhealthyCount > 0) {
healthData[serviceName].status = 'unhealthy';
} else {
healthData[serviceName].status = 'degraded';
}
}

View file

@ -33,6 +33,7 @@
<div class="modal-footer">
<slot name="modal-footer-slot"></slot>
<modalButtonMain
v-if="!isFooterButtonSuppressed"
:isPrimary="isFooterButtonPrimary"
class="w-100 modal-footer-button"
:id="modalId + '-modalbtn'"
@ -67,6 +68,7 @@ export default {
staticBackdrop: Boolean,
footerButtonDisabled: Boolean,
isFooterButtonPrimary: Boolean,
isFooterButtonSuppressed: Boolean,
onModalOpenedCallback: {
type: Function,
},

View file

@ -1,10 +1,5 @@
<template>
<div id="afterpay-banner" role="alert">
<component
:is="'script'"
src="https://js.squarecdn.com/square-marketplace.js"
async></component>
<div class="afterpay-modal-banner" role="alert">
<div>
<span
class="callout"
@ -22,22 +17,52 @@
<span v-else v-html="token"></span>
<span class="nbsp">&nbsp;</span>
</span>
<a
id="afterpay-learnmore"
href="#"
data-afterpay-modal="en_US"
data-bind="click:afterpayLearnMore">
{{ modalCopy }}
</a>
<textLink
linkType="text"
:text="bannerCta"
href="javascript:void(0)"
@click-event="openModal" />
</div>
</div>
<modal
:ref="modalName"
class="afterpay-modal"
:headerText="modalHeaderText"
:isFooterButtonSuppressed="true">
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot>
<span class="afterpay-sections">
<span
v-for="section in afterpaySectionsContent"
:key="section"
class="afterpay-section">
<span class="section-header">
<img v-if="section.AnswerImageUrl" :src="section.AnswerImageUrl" />
<h3 v-if="section.Text" v-html="section.Text"></h3>
</span>
<span v-html="section.content"></span>
</span>
</span>
</template>
<template v-slot:modal-footer-slot>
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
</template>
</modal>
</template>
<script>
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";
import {
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
splitCMSCopyOnBR,
} from "@/helpers/cms-content-helper";
import textLink from "@/ux-components/text-link/text-link";
import modal from "@/digital-components/modal/modal";
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
@ -48,20 +73,16 @@ function isInlineImageToken(token) {
function isAfterpayPriceToken(token) {
return token.includes(AFTERPAY_PRICE_TOKEN);
}
function getInlineAltText(token) {
const innerTokens = token.split(",");
return innerTokens[1] ?? "";
}
export default {
name: "afterpay-modal-banner",
name: "afterpay-banner",
props: {
cmsWidgetName: String,
lineItems: Array,
isInsuranceSelected: Boolean,
afterpayExtendedPayOptionThreshold: Number,
modalWidgetName: String,
},
data() {
return {};
@ -70,6 +91,17 @@ export default {
isInlineImageToken,
isAfterpayPriceToken,
getInlineAltText,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
openModal() {
this.modal?.openModal();
},
closeModal() {
this.modal?.closeModal();
},
},
computed: {
imageUrl() {
@ -84,39 +116,40 @@ export default {
afterpayCopyTokens() {
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
},
modalCopy() {
bannerCta() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
getTierOnePackagePrice() {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
if (this.lineItems.promos) {
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT");
}
let price = baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(allLineItems)
);
if (this.lineItems.promos) {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
const promos = getPromosThatMatchLineItemsOnOrder(
this.lineItems.promos,
allLineItems
);
promos.forEach((promo) => {
price += baseMixin.methods.getTotalLineItemPrice(promo);
});
}
return price;
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalDisclaimerText() {
return this.getCmsContent(this.modalWidgetName, "BodyText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
afterpaySectionsContent() {
const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
if (!sections) return [];
sections.forEach((section) => {
const sectionContent = this.getCmsContent(section.Name, "BodyText");
section["content"] = sectionContent;
});
return sections;
},
},
components: {},
components: {
textLink,
modal,
},
};
</script>
<style lang="scss" scoped>
#afterpay-banner {
.afterpay-modal-banner {
display: flex;
flex-direction: column;
background-color: $blue-150;
@ -130,6 +163,7 @@ export default {
.callout {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
white-space: nowrap;
}
> div:first-of-type {
@ -163,15 +197,119 @@ export default {
}
@include media-breakpoint-up(md) {
padding-left: 1rem;
max-width: 32rem;
max-width: 36rem;
}
}
& .alert-heading {
font-size: 0.875rem;
}
.afterpay-modal {
&.modal.modal-component {
:deep(.modal-dialog) {
margin: 0 1rem;
top: 0;
@include media-breakpoint-up(md) {
width: 100%;
max-width: 840px;
top: 3.4rem;
right: 0;
left: 0;
transform: none;
margin: 0 auto;
padding: 0 1rem;
}
.modal-content {
border-radius: 0.5rem;
margin: 1.5rem 0;
.modal-header.mt-6 {
background-color: $blue-100;
margin-top: 0;
padding: 1.5rem 2rem 1.5rem 1.5rem;
font-family: UrbanistSemibold;
.modal-title.justify-content-center {
// OVERRIDE
justify-content: flex-start;
}
.btn-close {
top: 1.25rem;
right: 1.5rem;
}
}
.afterpay-section {
display: block;
border: 1px solid $gray-200;
border-radius: 0.25rem;
padding: 1rem;
.section-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
img {
width: 40px;
height: 40px;
}
h3 {
font-family: UrbanistSemibold;
font-size: 1rem;
margin: 0 0 0 0.5rem;
}
}
p {
font-size: 0.75rem;
}
strong {
font-family: UrbanistSemibold;
font-size: 0.875rem;
}
}
.modal-body {
padding: 1.5rem;
.afterpay-sections {
@include media-breakpoint-up(md) {
display: flex;
column-gap: 1rem;
align-items: stretch;
}
.afterpay-section {
margin-bottom: 1rem;
@include media-breakpoint-up(md) {
flex: 0 1 33%;
margin-bottom: 0;
}
&:last-of-type {
margin-bottom: 0;
}
}
}
}
}
&.modal-dialog-centered {
height: auto;
min-height: auto;
}
}
}
:deep(.modal-footer) {
position: relative;
background: $gray-100;
.modal-disclaimer {
font-style: italic;
font-size: 0.75rem;
a {
padding: 0;
line-height: 1.625;
}
}
}
}
#afterpay-learnmore {
font-weight: 600;
font-size: 0.875rem;
}
</style>

View file

@ -45,10 +45,8 @@
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
<afterpayModalBanner
v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget"
:afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
:isInsuranceSelected="isInsuranceSelected"
:lineItems="lineItems" />
cmsWidgetName="AfterpayBannerWidget"
modalWidgetName="AfterpayModalWidget" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
<recalDisclaimer

View file

@ -958,6 +958,8 @@ function getPageNameFromRouter() {
) {
return router.currentRoute.value.name;
}
return window.location.href.replace(/\/$/, "").split("/").pop();
}
function getValueToLog(value, valueToLogType) {

View file

@ -3,16 +3,33 @@ import { queryStrings } from "@/constants/query-strings";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function consumeReferralQuerystrings() {
const referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
let referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
let parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
let correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
let referralDate = null;
// If no referral number in querystring, check if heritage funnel updated last.
// This would indicate a customer that went to heritage but did not return through the
// normal route. ie: left the site in search of a discount and returned without querystrings.
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
if (didHeritageFunnelUpdateLast && !referralNumber) {
referralNumber = getFunnelCookie().ReferralNumber;
parentAccount = getFunnelCookie().ReferralParentAccountNumber;
correlationId = getFunnelCookie().ReferralCorrelationId;
referralDate = getFunnelCookie().ReferralDate;
}
if (referralNumber) {
store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount);
store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId);
// log(" --update cookie");
if (referralDate) {
store.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
}
updateOrCreateFunnelCookie();
}
}

View file

@ -2,11 +2,13 @@ import { queryStrings } from "@/constants/query-strings";
import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info";
import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper";
import { routeData } from "@/router/constants/routes";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function handleHeritageReturn(to, from) {
const fromHeritageFlag = to.query[queryStrings.FROM_HERITAGE];
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
if (fromHeritageFlag) {
if (fromHeritageFlag || didHeritageFunnelUpdateLast) {
// Get new referral info from querystring in case passed there.
await consumeReferralQuerystrings();

View file

@ -6,6 +6,7 @@ import router from "@/router";
import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
import store from "@/store";
import { handleHardError } from "@/router/methods/error";
import analyticsMixin from "@/mixins/analytics-mixin";
export async function errorBeforeEnter(to, from) {
// Check if `hasAlreadyTriggeredErrror` is available.
@ -18,6 +19,7 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name,
};
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload);
return;
}
@ -31,6 +33,7 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name,
};
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload);
return;
} else {

View file

@ -123,8 +123,8 @@ $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;