DigitalConsumer.ISS/src/layouts/payment-method/payment-method.vue
2026-07-24 23:42:06 -04:00

737 lines
28 KiB
Vue

<template>
<Form
ref="theForm"
v-slot="{ meta }"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<div class="fade-on-route-transition">
<div class="justify-content-center">
<siteHeader
ref="siteHeader"
cmsWidgetName="SiteHeaderWidget" />
</div>
<div class="iss-heritage-container-width payment-method-container row">
<div class="order-info-container col-md-7">
<alert
class=""
alertClass="alert-warning"
cmsWidgetName="AlertIncompleteWidget" />
<siteSubHeader
class="subheader"
cmsWidgetName="SiteSubHeaderWidget" />
<textBlock
v-if="stateSpecificText"
class="state-specific-text"
:customText="stateSpecificText" />
<div class="main-content-container">
<hr class="section-divider" />
<reviewDropdown
ref="reviewDropdown"
@edit-clicked="handleEditClicked"
/>
<div class="cart-header">
<span class="cart-label">{{ cartHeaderText }}</span>
<span v-if="showCartTotal" class="cart-value">{{ cartTotal }}</span>
</div>
<div class="cart-dropdown-container">
<cartDropdown
:showAsPaid="false"
:readOnly="false"
@switchToInShop="handleSwitchToInShop" />
</div>
<alert
v-if="displayPayInAdvanceAlert"
name="payInAdvanceErrorAlert"
class="my-4"
cmsWidgetName="PayInAdvanceErrorAlertWidget"
alertClass="alert-danger"
:isDismissible="false" />
<hr
v-if="!isPayInAdvanceDisabled"
class="pia-divider" />
<paymentMethodQuestion
v-if="!isPayInAdvanceDisabled"
v-model="paymentMethod"
:cmsWidgetName="paymentMethodWidgetName"
:validationRules="rules.optionRequired" />
<div
v-if="!hideSMSOptIn"
class="sms-opt-in-section">
<hr class="sms-divider" />
<div class="sms-opt-in-header">{{ smsOptInHeaderText }}</div>
<buttonQuestion
class="sms-opt-in-question"
v-model="smsOptIn"
groupName="sms-opt-in"
buttonTypeString="listButtonHorizontal"
:questionText="smsOptinQuestionText"
:answers="smsOptinQuestionAnswers"
isRequired
:validationRules="rules.optionRequired" />
<textboxQuestion
v-if="smsOptIn === 'Yes'"
v-model="smsPhoneNumber"
inputId="phoneNumber"
:cmsWidgetName="widget.smsPhoneNumber"
isRequired
:mask="phoneMask"
placeholderText="###-###-####"
disableAutoFill
:validationRules="rules.phoneNumber" />
</div>
<siteFooter
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@backClicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
<div
v-if="!hideSMSOptIn"
class="disclaimer">
<textBlock :cmsWidgetName="widget.smsDisclaimer" />
</div>
</div>
</div>
<div class="offer-container col-lg-4 offset-lg-1 col-md-5 d-none d-md-flex">
<div
v-if="offerWipers"
class="wiper-offer-panel">
<div class="wiper-logo">
<img
:src="wiperOfferPanel.image"
alt="Wiper Offer Logo" />
</div>
<div class="panel-body">
<div class="wiper-header">{{ wiperOfferPanel.header }}</div>
<div class="wiper-body">{{ wiperOfferPanel.body }}</div>
</div>
<div class="panel-footer">
<buttonMain
class="edit-wiper-offer-button"
:buttonText="wiperOfferPanel.edit"
@click="handleEditClicked('wipers')" />
</div>
</div>
</div>
</div>
<contactDetailsDrawer
ref="contactDetailsDrawer"
class="contact-details-drawer"
@updateContactDetails="refreshContactDetails"
@closeWithoutSaving="closeContactDetailsWithoutSaving" />
</div>
</Form>
</template>
<script>
// Components
import baseFormMixin from '@/mixins/base-form-mixin';
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import reviewDropdown from '@/layouts/payment-method/review-dropdown/review-dropdown.vue';
import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue';
import paymentMethodQuestion from '@/layouts/payment-method/payment-method-question/payment-method-question.vue';
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
import contactDetailsDrawer from '@/iss-components/contact-details-drawer/contact-details-drawer.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
// Supporting Items
import settleAllPromises from '@/helpers/layout-helper';
import { useMainStore } from '@/store';
import { fetchCmsContentForPage, processIfStatements } from '@/helpers/cms-content-helper';
import { paymentMethods } from '@/constants/payment-method-constants';
import queryStrings from '@/constants/query-strings';
import globalRules from '@/constants/global-rules';
import { Form } from 'vee-validate';
import issPageValues from '@/router/router-constants/issPage-values';
import bailoutMessage from '@/constants/bailoutMessage';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import { saveSession, submitWorkOrder } from '@/helpers/order-helper.js';
import { experimentSettings } from '@/constants/experiments';
import submitType from '@/constants/submit-type';
import routerParams from '@/router/router-constants/router-params';
import { supportsApplePay } from '@/helpers/browser-helper';
import { getCartTotal } from '@/helpers/cart-helper';
import { formatAmountInDollars } from '@/helpers/text-helper';
import widgetFields from '@/constants/cms-widget-fields';
import MaskaFormattedMasks from '@/constants/maska-masks';
import { containsRecalParts } from '@/helpers/recal-helper';
import coverageType from '@/constants/coverage-type';
import partTypeStrings from '@/constants/part-type-strings';
export default {
name: 'payment-method',
components: {
Form,
siteHeader,
siteSubHeader,
siteFooter,
reviewDropdown,
cartDropdown,
paymentMethodQuestion,
alert,
textBlock,
buttonQuestion,
textboxQuestion,
contactDetailsDrawer,
buttonMain
},
mixins: [baseFormMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const pageData = useMainStore().pageData(to.query.issPage);
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise
}
];
const resultMap = await settleAllPromises(promiseResultMap);
await useMainStore().setPriceAndSalesTaxForOrderLineItems();
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.initializeComponent(pageData);
vm.updateFooterButtonText(vm.customCallToActionButtonCopy);
});
},
mounted() {
this.handleAnalyticsEventsOnMounted();
},
data() {
return {
paymentMethod: null,
widget: {
paymentMethod: 'PaymentMethodWidget',
paymentMethodApplePay: 'PaymentMethodWidgetApplePay',
amountDue: 'AmountDueCartHeaderTextWidget',
payAtAppointment: 'PayAtAppointmentTextWidget',
smsOptInHeader: 'SMSOptInHeaderWidget',
smsOptInQuestion: 'SMSOptInQuestionWidget',
smsPhoneNumber: 'SMSPhoneNumberWidget',
smsDisclaimer: 'SMSDisclaimerWidget',
wiperOffer: 'WiperOfferPanelWidget',
stateSpecificText: 'StateSpecificTextWidget'
},
rules: {
optionRequired: globalRules.OPTION_REQUIRED,
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`
},
smsOptIn: '',
smsPhoneNumber: ''
};
},
computed: {
customCallToActionButtonCopy() {
switch (this.paymentMethod) {
case paymentMethods.PayNow:
case paymentMethods.AFTERPAY:
return 'Continue to checkout';
default:
return null;
}
},
displayPayInAdvanceAlert() {
return this.$route.query[queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT];
},
isPayInAdvanceDisabled() {
const piaExperience = this.getSettingValue(experimentSettings.ISS_DISPLAY_PAY_IN_ADVANCE);
const isEnabled = piaExperience === 'true';
const isDeductibleTotalEqualToZero = useMainStore().currentDeductible === 0;
return !isEnabled || useMainStore().isUnverified || (useMainStore().isDeductible && isDeductibleTotalEqualToZero);
},
isAdyenEnabled() {
const adyenEnabled = this.getSettingValue(experimentSettings.ISS_ENABLE_ADYEN_V1);
return adyenEnabled === 'true';
},
paymentMethodWidgetName() {
let showApplePay = false;
if (this.isAdyenEnabled) {
// Apple Pay is Available in Adyen if the protocol is HTTPS
if (window.location.protocol === 'https:') {
showApplePay = true;
}
}
else {
showApplePay = supportsApplePay();
}
return showApplePay ? this.widget.paymentMethodApplePay : this.widget.paymentMethod;
},
showCartTotal() {
return this.mainStore.isVerified && (this.mainStore.isITAC || this.mainStore.isNoComp
|| this.mainStore.order.lineItems?.vaps?.length > 0);
},
cartHeaderText() {
if (this.isPayInAdvanceDisabled) {
return this.getCmsContent(
this.widget.payAtAppointment,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
} else {
return this.getCmsContent(
this.widget.amountDue,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
}
},
cartTotal() {
return formatAmountInDollars(getCartTotal(this.mainStore.order));
},
hideSMSOptIn() {
return false;
},
smsOptInHeaderText() {
return this.getCmsContent(
this.widget.smsOptInHeader,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
smsOptinQuestionText() {
return this.getCmsContent(this.widget.smsOptInQuestion, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
},
smsOptinQuestionAnswers() {
return this.getCmsContent(this.widget.smsOptInQuestion, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS) || [];
},
phoneMask() {
return MaskaFormattedMasks.PHONE_NUMBER;
},
offerWipers() {
const orderHasWipers = this.mainStore.lineItems.vaps?.some((part) => part.partType.toLowerCase().includes('wiper'));
const wipersAvailable = this.mainStore.order.availableVaps?.some((part) => part.partType.toLowerCase().includes('wiper'));
return !orderHasWipers && wipersAvailable;
},
wiperOfferPanel() {
const wiperImage = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.IMAGE);
const wiperHeader = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
const wiperBody = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
const wiperEdit = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT);
return {
image: wiperImage,
header: wiperHeader,
body: wiperBody,
edit: wiperEdit
};
},
stateSpecificText() {
const rawStateSpecificText = this.getCmsContent(this.widget.stateSpecificText, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
if (rawStateSpecificText) {
return processIfStatements(rawStateSpecificText, 'custom', this.getStateSpecificText);
}
return null;
}
},
watch: {
customCallToActionButtonCopy(newValue) {
this.updateFooterButtonText(newValue);
}
},
methods: {
arePagePrerequisitesValid() {
// Vehicle
const { vehicle } = useMainStore().order;
const vehicleReqs = !!(
vehicle.year
&& vehicle.make
&& vehicle.model
&& vehicle.style
);
// Damage
const { isRepair, numberOfChips, glassToReplace } =
useMainStore().order.damage;
const damageReqs = !!(
(isRepair && numberOfChips)
|| (!isRepair && glassToReplace?.length)
);
// Service Package
const { lineItems } = useMainStore().order;
const packageReqs = !!(
(isRepair || lineItems.glassParts)
&& lineItems.supportingItems
);
// Service Location
const { serviceLocation } = useMainStore().order;
const mobileReqs = !!(
serviceLocation.address
&& serviceLocation.city
&& serviceLocation.state
&& serviceLocation.zipCode
);
const providerLocation = serviceLocation.provider.address;
const dropOffInshopReqs = !!(
providerLocation.streetAddress
&& providerLocation.city
&& providerLocation.state
&& providerLocation.zipCode
);
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|| serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const serviceLocationReqs =
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
// Schedule
const { date, startTime, endTime, jobMaxMinutes, jobMinMinutes } =
useMainStore().order.schedule;
const scheduleReqs = !!(
date
&& startTime
&& endTime
&& jobMaxMinutes
&& jobMinMinutes
);
// Customer
const { firstName, lastName, emailAddress } = useMainStore().order.customer;
const customerReqs = !!(firstName && lastName && emailAddress);
// Contact Info
const { contactInfo } = useMainStore();
const contactInfoReqs = !!(
contactInfo.firstName
&& contactInfo.lastName
&& contactInfo.servicePhone
&& contactInfo.emailAddress
);
return (
vehicleReqs
&& damageReqs
&& packageReqs
&& serviceLocationReqs
&& scheduleReqs
&& customerReqs
&& contactInfoReqs);
},
initializeComponent(pageData) {
if (pageData) {
this.smsOptIn = pageData.smsOptIn;
}
this.smsPhoneNumber = this.getSMSPhoneFromStore();
if (this.isPayInAdvanceDisabled) {
this.paymentMethod = paymentMethods.PAY_AT_TIME_OF_SERVICE;
}
},
updateFooterButtonText(newValue) {
this.$refs.siteFooter.updateButtonText(newValue);
},
async forwardButtonAction() {
this.handleAnalyticsEventsOnForwardButtonAction();
this.mainStore.savePaymentMethodChoice(this.paymentMethod);
this.savePageDataToStore(issPageValues.PAYMENT_METHOD, { smsOptIn: this.smsOptIn });
if (!this.hideSMSOptIn) {
const requestTextUpdates = this.smsOptIn === 'Yes';
this.mainStore.updateContactInfo({ requestTextUpdates });
if (requestTextUpdates) {
this.mainStore.updatePhoneNumbers({ alternative: this.smsPhoneNumber, service: this.smsPhoneNumber });
}
}
if (this.paymentMethod === paymentMethods.PAY_AT_TIME_OF_SERVICE) {
await submitWorkOrder({ submitType: submitType.SAFELITE });
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
} else {
await saveSession({
createWorkOrderNumberForPIA: true,
shouldAwaitSaveSessionQueue: true,
bailoutOnError: true
});
const scenario = this.isAdyenEnabled
? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN
: this.navigationScenarios.CLICKED_PAY_NOW;
this.$router.navigate(
scenario,
this.$route,
{},
{ [routerParams.SKIP_SAVE_SESSION]: true }
);
}
},
backButtonAction() {
const scenario = this.mainStore.isMobileAppointment
? this.navigationScenarios.CLICKED_BACK_MOBILE
: this.navigationScenarios.CLICKED_BACK_INSHOP;
this.$router.navigateWithSpinner(
scenario,
this.$route
);
},
getSMSPhoneFromStore() {
return this.mainStore.contactInfo.alternativePhone ?? this.mainStore.contactInfo.servicePhone;
},
handleEditClicked(section) {
switch (section) {
case 'location':
this.pushEventToGA('order_summary', 'change', 'location', true);
const scenario = this.mainStore.isMobileAppointment
? this.navigationScenarios.EDIT_SERVICE_LOCATION_MOBILE
: this.navigationScenarios.EDIT_SERVICE_LOCATION_INSHOP;
this.$router.navigateWithSpinner(
scenario,
this.$route
);
break;
case 'schedule':
this.pushEventToGA('order_summary', 'change', 'time', true);
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_SCHEDULE,
this.$route
);
break;
case 'vehicle':
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_VEHICLE,
this.$route
);
break;
case 'customer':
this.openContactDetailsModal();
break;
case 'wipers':
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_WIPERS,
this.$route
);
break;
}
},
handleSwitchToInShop() {
this.mainStore.resetServiceLocationAndDependencies();
this.mainStore.updateAppointmentType(AppointmentTypeStrings.IN_SHOP);
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_SCHEDULE,
this.$route
);
},
openContactDetailsModal() {
this.$refs.contactDetailsDrawer.openModal();
},
refreshContactDetails() {
this.smsPhoneNumber = this.mainStore.contactInfo.servicePhone;
},
closeContactDetailsWithoutSaving() {
this.smsOptIn = 'No';
},
handleAnalyticsEventsOnMounted() {
const mobileOrInshop = this.mainStore.isMobileAppointment ? 'mobile' : 'in_shop';
const verifiedOrNotVerified = this.mainStore.isVerified ? 'verified' : 'not_verified';
const repairOrReplace = this.mainStore.damage.isRepair ? 'repair' : 'replace';
this.pushEventToGA('order_summary', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true);
if (containsRecalParts(this.mainStore.lineItems)) {
let coverageTypeForLabel = '';
if (this.mainStore.isITAC) {
coverageTypeForLabel = 'ITAC';
} else if (this.mainStore.isNoComp) {
coverageTypeForLabel = 'no_comp';
} else if (this.mainStore.isVerified) {
coverageTypeForLabel = 'verified';
} else {
coverageTypeForLabel = 'unverified';
}
const recalibrationType = this.mainStore.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType;
this.pushEventToGA(`recalibration_added_${coverageTypeForLabel}`, this.mainStore.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true);
}
const hasFrontWiper = this.mainStore.lineItems?.vaps?.some((part) => part.partType === partTypeStrings.FRONT_WIPER);
const hasRearWiper = this.mainStore.lineItems?.vaps?.some((part) => part.partType === partTypeStrings.REAR_WIPER);
const wiperEventLabel = `front_${hasFrontWiper ? 'advanced' : 'none'}_rear_${hasRearWiper ? 'advanced' : 'none'}`;
this.pushEventToGA('wipers', 'attached_to_order', wiperEventLabel, true);
},
handleAnalyticsEventsOnForwardButtonAction() {
this.pushEventToGA('order_summary', 'edit', 'contact_details', true);
let action;
let label;
switch (this.mainStore.insuranceCoverage?.coverageType) {
case coverageType.Deductible:
action = 'Covered';
label = this.mainStore.currentDeductible;
break;
case coverageType.ITAC:
action = 'ITAC';
label = this.mainStore.currentDeductible + "_" + getCartTotal(this.mainStore.order);
break;
case coverageType.NO_COMP:
action = 'NOCOMP';
label = 'Unverified';
break;
case coverageType.NONE:
default:
action = 'Unverified';
label = 'Unverified';
break;
}
this.pushEventToGA('Submit Appointment', action, label, true, null, 1);
},
getStateSpecificText(value) {
if (value.includes('not')) {
return !value?.toLowerCase().includes(this.mainStore.order.customer.address.state?.toLowerCase());
}
return this.mainStore.order.customer.address.state?.toLowerCase() === value?.toLowerCase();
}
}
};
</script>
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
#app .iss-heritage-container-width.payment-method-container {
.alert-warning {
margin-top: 1.25rem;
margin-bottom: 0rem;
}
.subheader {
margin-top: 1.25rem;
:deep(strong) {
color: $black;
font-weight: 600;
}
:deep(.subheader-secondary) {
margin-top: .625rem;
p {
margin-bottom: 0;
color: #4d4e53;
}
}
}
.section-divider {
margin-top: 1.5rem;
margin-bottom: 1.875rem;
}
.cart-header {
display: flex;
align-items: center;
gap: .25rem;
margin-bottom: .625rem;
.cart-label {
color: $black;
font-weight: $font-weight-bold;
}
.cart-value {
color: $green;
font-weight: $font-weight-bold;
font-size: 1.625rem;
}
}
.cart-dropdown-container {
margin-bottom: 1.25rem;
}
.pia-divider {
margin-top: .5rem;
margin-bottom: 1.25rem;
}
.sms-divider {
margin-top: 1.75rem;
margin-bottom: 1.875rem;
}
.sms-opt-in-section {
margin-bottom: 1.875rem;
}
.sms-opt-in-header {
color: $black;
font-weight: $font-weight-bold;
margin-bottom: 1.125rem;
}
.sms-opt-in-question {
margin-bottom: 1.25rem;
:deep(.question-text) {
margin-bottom: .625rem;
}
}
.disclaimer {
margin-bottom: 1.875rem;
font-weight: $font-weight-light;
font-size: .8125rem;
line-height: 1.54;
color: #4d4e53;
:deep(a) {
color: $heritage-blue-primary;
text-decoration: none;
font-weight: $font-weight-bold;
&:hover,
&:focus {
color: $heritage-blue-secondary;
text-decoration: underline;
}
}
}
.contact-details-drawer {
:deep(strong) {
color: $black;
font-weight: 600;
}
}
.wiper-offer-panel {
margin-top: 1.25rem;
margin-bottom: 1.375rem;
box-shadow: 0 0 .625rem 1px #cacbcc;
border: 1px solid #ddd;
border-radius: .25rem;
max-height: fit-content;
.wiper-logo {
margin: 1.25rem 2.25rem 0 2.25rem;
img {
max-width: 100%;
height: auto;
}
}
.panel-body {
padding: 0 1rem 1.25rem 1rem;
.wiper-header {
margin: 1.25rem 0 .625rem 0;
color: $black;
font-weight: $font-weight-bold;
}
}
.panel-footer {
padding: 0 1rem 1.25rem 1rem;
.edit-wiper-offer-button {
width: 100%;
}
}
}
}
</style>