Merge pull request #2938 from Safelite/feature/CASH-1757

Feature/cash-1757
This commit is contained in:
CarlNation 2025-11-14 04:20:31 -05:00 committed by GitHub
commit 308610187f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 195 additions and 58 deletions

View file

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

View file

@ -1,10 +1,5 @@
<template> <template>
<div id="afterpay-banner" role="alert"> <div class="afterpay-modal-banner" role="alert">
<component
:is="'script'"
src="https://js.squarecdn.com/square-marketplace.js"
async></component>
<div> <div>
<span <span
class="callout" class="callout"
@ -22,22 +17,52 @@
<span v-else v-html="token"></span> <span v-else v-html="token"></span>
<span class="nbsp">&nbsp;</span> <span class="nbsp">&nbsp;</span>
</span> </span>
<a <textLink
id="afterpay-learnmore" linkType="text"
href="#" :text="bannerCta"
data-afterpay-modal="en_US" href="javascript:void(0)"
data-bind="click:afterpayLearnMore"> @click-event="openModal" />
{{ modalCopy }}
</a>
</div> </div>
</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> </template>
<script> <script>
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper"; import {
import baseMixin from "@/mixins/base-mixin.js"; doesCopyContainTextLink,
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper"; splitCopyOnCMSPlaceHolder,
import { getArrayOfAllLineItemsAndChildParts } from "@/store"; 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 INLINE_IMAGE_TOKEN = "custom:inlineImage";
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice"; const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
@ -48,20 +73,16 @@ function isInlineImageToken(token) {
function isAfterpayPriceToken(token) { function isAfterpayPriceToken(token) {
return token.includes(AFTERPAY_PRICE_TOKEN); return token.includes(AFTERPAY_PRICE_TOKEN);
} }
function getInlineAltText(token) { function getInlineAltText(token) {
const innerTokens = token.split(","); const innerTokens = token.split(",");
return innerTokens[1] ?? ""; return innerTokens[1] ?? "";
} }
export default { export default {
name: "afterpay-modal-banner", name: "afterpay-banner",
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
lineItems: Array, modalWidgetName: String,
isInsuranceSelected: Boolean,
afterpayExtendedPayOptionThreshold: Number,
}, },
data() { data() {
return {}; return {};
@ -70,6 +91,17 @@ export default {
isInlineImageToken, isInlineImageToken,
isAfterpayPriceToken, isAfterpayPriceToken,
getInlineAltText, getInlineAltText,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
openModal() {
this.modal?.openModal();
},
closeModal() {
this.modal?.closeModal();
},
}, },
computed: { computed: {
imageUrl() { imageUrl() {
@ -84,39 +116,40 @@ export default {
afterpayCopyTokens() { afterpayCopyTokens() {
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText")); return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
}, },
modalCopy() { bannerCta() {
return this.getCmsContent(this.cmsWidgetName, "FooterText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
getTierOnePackagePrice() { modalHeaderText() {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); return this.getCmsContent(this.modalWidgetName, "HeaderText");
if (this.lineItems.promos) { },
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT"); modalDisclaimerText() {
} return this.getCmsContent(this.modalWidgetName, "BodyText2");
},
let price = baseMixin.methods.getTierOnePackagePrice( modalName() {
baseMixin.methods.filterOutFees(allLineItems) return this.modalWidgetName;
); },
modal() {
if (this.lineItems.promos) { return this.$refs[this.modalName];
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); },
const promos = getPromosThatMatchLineItemsOnOrder( afterpaySectionsContent() {
this.lineItems.promos, const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
allLineItems if (!sections) return [];
); sections.forEach((section) => {
promos.forEach((promo) => { const sectionContent = this.getCmsContent(section.Name, "BodyText");
price += baseMixin.methods.getTotalLineItemPrice(promo); section["content"] = sectionContent;
}); });
} return sections;
return price;
}, },
}, },
components: {}, components: {
textLink,
modal,
},
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
#afterpay-banner { .afterpay-modal-banner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: $blue-150; background-color: $blue-150;
@ -166,12 +199,116 @@ export default {
max-width: 32rem; max-width: 32rem;
} }
} }
& .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> </style>

View file

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