diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 000000000..30e99a02e --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] + } \ No newline at end of file diff --git a/src/assets/img/icons/info-circle-blue.svg b/src/assets/img/icons/info-circle-blue.svg new file mode 100644 index 000000000..fca619fb0 --- /dev/null +++ b/src/assets/img/icons/info-circle-blue.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/constants/analytics.js b/src/constants/analytics.js index e156de11b..51b0c24b6 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -15,6 +15,8 @@ const GaCategories = { APPOINTMENT: "Appointment", SERVICE_LOCATION: "service-location", CONFIRMATION_CLICKED: "confirmation clicked", + INSHOP_CONFIRMATION_CLICKED: "inshop confirmation clicked", + MOBILE_CONFIRMATION_CLICKED: "mobile confirmation clicked", RECAL_DISCLAIMER: "Recalibration disclaimer", }; diff --git a/src/constants/experiments.js b/src/constants/experiments.js index f81484058..f8691fc46 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -5,6 +5,7 @@ const experimentUniverses = { IGQ_SkipQuote: "NextGen_IGQSkipToInsurance", AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay", MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment", + MULTI_LOCATION_POPUP: "MultiLocationPopup", }; const experimentSettings = { @@ -40,7 +41,13 @@ const experimentSettings = { SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR", SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR", OFFER_OEM: "OfferOem", + SHOW_MULTI_LOCATION_APPT: "ShowMultiLocationAppt", + SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION: "Show_NoAvailableDaysMultiLocation", + SHOW_NO_PM_DAYS_MULTI_LOCATION: "Show_NoPMDaysMultiLocation", + SHOW_PM_DAYS_MULTI_LOCATION: "Show_PMDaysMultiLocation", USE_ADYEN_PAYMENT: "UseAdyenPayment", + SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry", + SERVICE_PACKAGE_ORDER: "ServicePackageOrder", }; const experimentTriggers = { diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index 9862633ad..417c88809 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -226,6 +226,7 @@ export default { let classes; switch (this.buttonTypeString) { case "timeSlotModalListButton": + case "multiLocationRadioButton": case "listButton": classes = "w-100"; break; @@ -265,6 +266,8 @@ export default { classes += " radio-button-container"; } else if (this.buttonTypeString == "servicePackageRadio") { classes = "package-wrapper col"; + } else if (this.buttonTypeString == "multiLocationRadioButton") { + classes = "multi-location-wrapper col"; } if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) { diff --git a/src/experiment-components/donation-block.vue b/src/experiment-components/donation-block.vue index f93c96520..e48e66860 100644 --- a/src/experiment-components/donation-block.vue +++ b/src/experiment-components/donation-block.vue @@ -31,23 +31,15 @@
- - - - - + + + + +
@@ -264,17 +256,17 @@ export default { background: $blue; } input { - &#one:checked { + &#zero:checked { ~ .indicator-bar { transform: translateX(2rem); } } - &#three:checked { + &#one:checked { ~ .indicator-bar { transform: translateX(calc(100% + 2rem)); } } - &#five:checked { + &#two:checked { ~ .indicator-bar { transform: translateX(calc(200% + 2rem)); } diff --git a/src/fmg-components/promo-modal-question/promo-modal-question.vue b/src/fmg-components/promo-modal-question/promo-modal-question.vue index 1e87d5bb5..bb3b390a8 100644 --- a/src/fmg-components/promo-modal-question/promo-modal-question.vue +++ b/src/fmg-components/promo-modal-question/promo-modal-question.vue @@ -317,6 +317,10 @@ export default { serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, pricedLineItems: pricedLineItemsToTax, + serviceLocationAddressLine1: + store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: + store.getters.order.serviceLocation?.address2, }, "payment-method", false diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index 1c0922ba0..a3dc363f5 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -20,7 +20,6 @@ export async function loadSessionIfPresent(isConceptInsurance, pageNameToLog) { // Do nothing if there is no cookie or session to use for loading. if ( funnelCookie == null || - funnelCookie.SavedSessionId == null || funnelCookie.ReferralCorrelationId == null || !funnelCookie.ReferralNumber ) { @@ -128,7 +127,7 @@ async function loadSession( const response = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.LOAD_SESSION, { - savedSessionId: savedSessionId?.toString(), + savedSessionId: savedSessionId != null ? savedSessionId.toString() : null, referralNumber, referralDate, referralCorrelationId, diff --git a/src/helpers/vehicle-helper.js b/src/helpers/vehicle-helper.js new file mode 100644 index 000000000..cd94d7bd5 --- /dev/null +++ b/src/helpers/vehicle-helper.js @@ -0,0 +1,11 @@ +import store from "@/store"; + +export function hasVehicleInfo() { + return ( + store.getters.order?.vehicle.year && + store.getters.order?.vehicle?.make && + store.getters.order?.vehicle?.model && + store.getters.order?.vehicle?.style && + store.getters.order?.vehicle?.carId + ); +} diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index a9c964268..fa0e53425 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -470,7 +470,8 @@ export default { } }, donationValues() { - return [1, 3, 5]; + var donationAmounts = this.getCmsContent("DonationAmountsWidget", "Text"); + return donationAmounts.split(",").map(Number); }, lineItemsWithoutDonation() { const lineItemsClone = deepClone(this.lineItems); @@ -531,10 +532,11 @@ export default { window.location.assign(location.protocol + "//" + location.host); }, async addDonationToOrder() { + const selectedItemNumber = this.donationValues.indexOf(Number(this.donationAmount)); const donationResponse = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.SUBMIT_DONATION_PART_TO_SV2, { - donationAmount: parseInt(this.donationAmount), + selectedItemNumber: selectedItemNumber, submittedOrder: this.submittedOrder, }, false diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index 76d4daa1e..1bc123d3f 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -94,6 +94,9 @@ function setupMocks() { vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" }, damage: { isRepair: false }, }, + applicationUser: { + experiments: [], + }, policy: { currentDeductible: 123, }, diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 2a6d1d9ed..d333b4bbf 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -53,6 +53,17 @@ v-bind:isDismissible="false" /> +
+ +
+
@@ -311,6 +322,8 @@ export default { serviceLocationState: store.getters.order.serviceLocation.state, serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, pricedLineItems: lineItemsForCart, + serviceLocationAddressLine1: store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2, }, "payment-method", false @@ -515,6 +528,10 @@ export default { serviceLocationState: this.$store.getters.order.serviceLocation.state, serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode, pricedLineItems: this.lineItems, + serviceLocationAddressLine1: + this.$store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: + this.$store.getters.order.serviceLocation?.address2, }, "payment-method", false @@ -639,6 +656,8 @@ export default { serviceLocationState: store.getters.order.serviceLocation.state, serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, pricedLineItems: this.lineItems, + serviceLocationAddressLine1: store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2, }, "payment-method", false @@ -865,6 +884,13 @@ export default { shouldDisplayPiaAlert() { return getBoolFromString(this.$route?.query?.piaError); }, + shouldDisplayUnverifiedInsuranceAlert() { + const shouldDisplayUnverifiedDeduct = experimentMixin.methods.hasSettingEqualTo( + experimentSettings.SHOW_UNVERIFIED_DEDUCT_ENTRY, + "true" + ); + return shouldDisplayUnverifiedDeduct; + }, // Necessary to make the watcher of lineItems work // JavaScript does not keep a record of the old value, only a reference to it's location in the memory. // This creates a separate location for oldValue/newValue so they can be compared @@ -1033,7 +1059,15 @@ export default { } } } + + .alert.alert-info.widget-name-UnverifiedInsuranceAlertWidget { + :deep(.alert-heading) { + color: $black; + font-weight: 600; + } + } } + :deep(span.label), :deep(span) { font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; diff --git a/src/layouts/quote/service-package-question/service-package-question.spec.js b/src/layouts/quote/service-package-question/service-package-question.spec.js index cfa48c616..b26114308 100644 --- a/src/layouts/quote/service-package-question/service-package-question.spec.js +++ b/src/layouts/quote/service-package-question/service-package-question.spec.js @@ -35,6 +35,11 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({ return true; } }, + hasSetting(settingName) { + if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) { + return false; + } + }, hasSettingEqualTo(settingName, settingValue) { return false; }, diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index 6a2d791b3..6e556d182 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -162,6 +162,16 @@ export default { })); this.$emit("currentNumberOfPackages", modifiedAnswers.length); + + if (this.experimentPackageOrder()) { + const orderedKeys = this.experimentPackageOrder() + .split(",") + .map((key) => key.trim()); + modifiedAnswers.sort( + (a, b) => orderedKeys.indexOf(a.value) - orderedKeys.indexOf(b.value) + ); + } + return modifiedAnswers; }, isDiscountOnOrder() { @@ -234,6 +244,11 @@ export default { ) ); }, + experimentPackageOrder() { + return experimentMixin.methods.hasSetting(experimentSettings.SERVICE_PACKAGE_ORDER) + ? experimentMixin.methods.getSettingValue(experimentSettings.SERVICE_PACKAGE_ORDER) + : false; + }, getButtonTypeObject() { if (this.isAfterpayBreakoutDisplay()) { return this.servicePackageRadioAfterpayExperiment; diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue new file mode 100644 index 000000000..effa3c91b --- /dev/null +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -0,0 +1,345 @@ + + + + diff --git a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js new file mode 100644 index 000000000..f305d4e2e --- /dev/null +++ b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js @@ -0,0 +1,121 @@ +import { mount } from "@vue/test-utils"; +import multiLocationRadio from "./multi-location-radio"; +import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"; + +describe("multi-location-radio.vue", () => { + it("Should include buttonLabel in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"])); + }); + + it("Should include buttonLabelSubCopy in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"])); + }); + it("Should include buttonFooterCopy in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"])); + }); + it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a buttonBodyCopy", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + // Act + const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy); + + // Assert + expect(results.length).toBe(5); + }); + it("Should get strings from getArrayOfListItemsFromRawCmsCopy without