diff --git a/package-lock.json b/package-lock.json index c2c89abc..c8ac4360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "digitalconsumer.iss", "version": "0.1.0", "dependencies": { + "@adyen/adyen-web": "^6.31.1", "axios": "^1.13.5", "axios-retry": "^3.5.0", "bootstrap": "^5.3", @@ -93,6 +94,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@adyen/adyen-web": { + "version": "6.31.1", + "resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.31.1.tgz", + "integrity": "sha512-vxeJW1bTvjXxYf6CkPQV8gbYIhm/hupcjjeoKkkBdxk7geW3qy18Jlco4/MuSeb88ctTRM3gebwFXpqF2NjBLQ==", + "license": "MIT", + "dependencies": { + "@types/applepayjs": "14.0.9", + "@types/googlepay": "0.7.10", + "classnames": "2.5.1", + "preact": "10.28.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "dev": true, @@ -4125,6 +4138,12 @@ "node": ">= 10" } }, + "node_modules/@types/applepayjs": { + "version": "14.0.9", + "resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.9.tgz", + "integrity": "sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==", + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.1", "dev": true, @@ -4283,6 +4302,12 @@ "@types/node": "*" } }, + "node_modules/@types/googlepay": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.10.tgz", + "integrity": "sha512-ByXjDfxEp87zsjw5cVsNKEeA+AIJYcWwJvRqPu85jO0Lp/FeG67csnf4mqbPaIxmdI6+Khp3v5I0yyjq9MrGhw==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "dev": true, @@ -7371,6 +7396,12 @@ "dev": true, "license": "MIT" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.1", "dev": true, @@ -17407,6 +17438,16 @@ "dev": true, "license": "MIT" }, + "node_modules/preact": { + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "dev": true, diff --git a/package.json b/package.json index 32e0fad3..c60c9e10 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "lint:inspect": "eslint --inspect-config" }, "dependencies": { + "@adyen/adyen-web": "^6.31.1", "axios": "^1.13.5", "axios-retry": "^3.5.0", "bootstrap": "^5.3", diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 1e5ee6d8..c8231ecb 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -26,7 +26,9 @@ const applicationConfig = Object.freeze({ FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging", BAILOUT_ON_APPLICATION_ERROR: true, BAILOUT_ON_API_ERROR: true, - BAILOUT_ON_ROUTER_ERROR: true + BAILOUT_ON_ROUTER_ERROR: true, + ADYEN_CLIENT_KEY: process.env.VUE_APP_ADYEN_CLIENT_KEY, + ADYEN_ENVIRONMENT: process.env.VUE_APP_ADYEN_ENVIRONMENT }); export default applicationConfig; diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 057ef9e4..e322bb48 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -10,6 +10,7 @@ const PARTS_BASE_URL = '/parts/api/v1/parts'; const PRICE_BASE_URL = '/price/api/v1/price'; const SCHEDULE_BASE_URL = '/schedule/api/v1/schedule'; const VEHICLE_BASE_URL = '/vehicle/api/v1/vehicle'; +const PAYMENT_BASE_URL = '/payment/api/v1/payment'; const endpoints = Object.freeze({ GetRouteInfo: { @@ -239,6 +240,14 @@ const endpoints = Object.freeze({ GetPaymentSignature: { url: `${ORDER_BASE_URL}/sign`, method: 'POST' + }, + InitializeAdyenPayment: { + url: `${PAYMENT_BASE_URL}/payment/session/initialize-session`, + method: "POST", + }, + GetAdyenSessionResult: { + url: `${PAYMENT_BASE_URL}/payment/session/session-result`, + method: "POST", } }); diff --git a/src/constants/experiments.js b/src/constants/experiments.js index f1d52ede..9e959331 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -1,7 +1,8 @@ const experimentUniverses = Object.freeze({ ISS_FUNNEL: 'ISSFunnel', ISS_FEATURETOGGLE_AREFEES_HIDDEN: 'NextGenISS_FeatureToggle_AreFeesHidden', - ISS_FEATURETOGGLE_AREFEES_OVERRIDDEN: 'NextGenISS_FeatureToggle_AreFeesOverridden' + ISS_FEATURETOGGLE_AREFEES_OVERRIDDEN: 'NextGenISS_FeatureToggle_AreFeesOverridden', + ADYEN_PAYMENT_TEST: 'NextGenAdyenPaymentTest' }); const experimentSettings = Object.freeze({ @@ -10,7 +11,8 @@ const experimentSettings = Object.freeze({ ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN: 'HideMobileFee', ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_OVERRIDDEN: 'OverrideMobileFee', ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_HIDDEN: 'HideRecycleFee', - ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_OVERRIDDEN: 'OverrideRecycleFee' + ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_OVERRIDDEN: 'OverrideRecycleFee', + ISS_ENABLE_ADYEN_V1: 'ISS_Enable_Adyen_V1' }); const experimentTriggers = Object.freeze({ diff --git a/src/constants/progress-bar-mapper.js b/src/constants/progress-bar-mapper.js index c47c738a..f9b6b7e0 100644 --- a/src/constants/progress-bar-mapper.js +++ b/src/constants/progress-bar-mapper.js @@ -66,6 +66,14 @@ export const pageProgressMapper = { // No progress bar on this page percent: 0 }, + 'payment-page-adyen': { + // No progress bar on this page + percent: 0 + }, + 'payment-return-adyen': { + // No progress bar on this page + percent: 0 + }, 'tpa-search': { percent: 80 }, diff --git a/src/helpers/adyen-helper.js b/src/helpers/adyen-helper.js new file mode 100644 index 00000000..d1b5b571 --- /dev/null +++ b/src/helpers/adyen-helper.js @@ -0,0 +1,164 @@ +import { AdyenCheckout } from "@adyen/adyen-web/auto"; +import { useMainStore } from "@/store"; +import globalMethods from "@/global-methods"; +import endpoints from "@/constants/endpoints"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; +import { paymentMethods } from "@/constants/payment-method-constants"; +import applicationConfig from "@/constants/application-config"; + +export async function getSessionInfo(sessionId, encryptedSessionResult) { + const order = useMainStore().order; + const location = getLocationInfo(order); + const workOrder = getWorkOrderLastSixDigits(order); + const system = getSourceSystem(); + + const request = { + sessionId: sessionId, + sessionResult: encryptedSessionResult, + zipCodeCtu: location.zipCodeCtu, + workOrderNumber: workOrder, + sourceSystem: system, + }; + + const response = await globalMethods.callHttpClient({ + method: endpoints.GetAdyenSessionResult.method, + endpoint: endpoints.GetAdyenSessionResult.url, + payload: request, + logApiCall: true + }); + + return response?.data; +} + +export async function createAdyenCheckout({ + session: { id, sessionData }, + handlers: { onPaymentCompleted, onPaymentFailed, onError }, + options: { showPayButton = true, amount }, +}) { + const config = { + session: { + id: id, + sessionData: sessionData, + }, + clientKey: applicationConfig.ADYEN_CLIENT_KEY, + environment: applicationConfig.ADYEN_ENVIRONMENT, + locale: "en_US", + countryCode: "US", + showPayButton: showPayButton, + translations: { + en_US: { + "creditCard.securityCode.label": "CVV/CVC", + }, + }, + onPaymentCompleted: onPaymentCompleted, + onPaymentFailed: onPaymentFailed, + onError: onError, + }; + + if (amount !== undefined && amount !== null) { + config.amount = { + value: amount, + currency: "USD", + }; + } + + return await AdyenCheckout(config); +} + +export function mapAdyenToIssPaymentMethod(adyenMethod) { + const paymentMethodMap = { + ["scheme"]: paymentMethods.CREDIT_CARD, + ["afterpaytouch_US"]: paymentMethods.AFTERPAY, + ["paypal"]: paymentMethods.PAYPAL, + ["applepay"]: paymentMethods.APPLEPAY, + }; + + const match = paymentMethodMap[adyenMethod]; + + return match ?? paymentMethods.CREDIT_CARD; +} + +export function mapIssToAdyenPaymentMethod(issMethod) { + const paymentMethodMap = { + [paymentMethods.CREDIT_CARD]: "scheme", + [paymentMethods.AFTERPAY]: "afterpaytouch_US", + [paymentMethods.PAYPAL]: "paypal", + [paymentMethods.APPLEPAY]: "applepay", + [paymentMethods.PayNow]: null, + [paymentMethods.PAY_AT_TIME_OF_SERVICE]: null, + }; + + const match = paymentMethodMap[issMethod]; + + return match; +} + +export function generateCcToken(adyenSessionInfo) { + const zipCode = useMainStore().order.customer.address.zipCode + ?? useMainStore().order.serviceLocation.zipCode + ?? useMainStore().order.serviceLocation.provider.address.zipCode + + const ccToken = { + subscriptionId: adyenSessionInfo?.storedToken, + expMonth: adyenSessionInfo?.cardExpiryMonth, + expYear: adyenSessionInfo?.cardExpiryYear, + cardType: adyenSessionInfo?.cardType, + billToPostalCode: zipCode, + billToFirstName: useMainStore().contactInfo.firstName, + billToLastName: useMainStore().contactInfo.lastName, + referenceNumber: adyenSessionInfo?.workOrderNumber, + authCode: adyenSessionInfo?.authCode, + transactionId: adyenSessionInfo?.transactionReference, + transReferenceNumber: adyenSessionInfo?.transactionReference, + lastFour: adyenSessionInfo?.last4DigitsOfCard, + }; + + return ccToken; +} + +function getLocationInfo(order) { + const serviceLocation = order.serviceLocation; + const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE || + serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP; + if (isMobile) { + return { + address: serviceLocation.address, + address2: serviceLocation.address2, + city: serviceLocation.city, + state: serviceLocation.state, + zipCode: serviceLocation.zipCode, + zipCodeCtu: serviceLocation.zipCodeCtu, + }; + } else { + const providerInfo = serviceLocation.provider.address; + return { + address: providerInfo.streetAddress, + address2: "", + city: providerInfo.city, + state: providerInfo.state, + zipCode: providerInfo.zipCode, + zipCodeCtu: providerInfo.zipCodeCtu, + }; + } +} + +function getWorkOrderLastSixDigits(order) { + const workOrderNumber = order.workOrderNumber ?? ""; + const tokens = workOrderNumber.split("-"); + const lastSegment = tokens[tokens.length - 1] ?? ""; + const numDigits = lastSegment.length; + const numZeroes = 6 - numDigits; + + let result = ""; + for (let i = 0; i < numZeroes; i++) { + result += "0"; + } + + result += lastSegment; + + return result; +} + +function getSourceSystem() { + return "ISS-NextGen"; +} diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 41eee5ee..0c3d0c7d 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -239,8 +239,23 @@ export default { return !isEnabled || useMainStore().isUnverified || (useMainStore().isDeductible && isDeductibleTotalEqualToZero); }, + isAdyenEnabled() { + const adyenEnabled = this.getSettingValue(experimentSettings.ISS_ENABLE_ADYEN_V1); + return adyenEnabled === 'true'; + }, paymentMethodWidgetName() { - return supportsApplePay() ? this.widget.paymentMethodApplePay : this.widget.paymentMethod; + 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 @@ -418,8 +433,12 @@ export default { bailoutOnError: true }); + const scenario = this.isAdyenEnabled + ? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN + : this.navigationScenarios.CLICKED_PAY_NOW; + this.$router.navigate( - this.navigationScenarios.CLICKED_PAY_NOW, + scenario, this.$route, {}, { [routerParams.SKIP_SAVE_SESSION]: true } diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue new file mode 100644 index 00000000..91462f51 --- /dev/null +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -0,0 +1,623 @@ + + + diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue new file mode 100644 index 00000000..d306857d --- /dev/null +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -0,0 +1,160 @@ + + + diff --git a/src/router/router-constants/issPage-values.js b/src/router/router-constants/issPage-values.js index c683e971..4d36c097 100644 --- a/src/router/router-constants/issPage-values.js +++ b/src/router/router-constants/issPage-values.js @@ -19,6 +19,8 @@ const issPageValues = Object.freeze({ PAYMENT_METHOD: 'payment-method', PAYMENT_PAGE: 'payment-page', PAYMENT_RETURN: 'payment-return', + PAYMENT_PAGE_ADYEN: 'payment-page-adyen', + PAYMENT_RETURN_ADYEN: 'payment-return-adyen', PART_QUESTIONS: 'part-questions', POLICY_HOLDER_DETAILS: 'policy-holder-details', PROVIDER_PREFERENCE: 'provider-preference', diff --git a/src/router/router-constants/navigation-scenarios.js b/src/router/router-constants/navigation-scenarios.js index 2c9768bf..b0b5bea9 100644 --- a/src/router/router-constants/navigation-scenarios.js +++ b/src/router/router-constants/navigation-scenarios.js @@ -102,6 +102,8 @@ const navigationScenarios = Object.freeze({ CLICKED_BACK_INSHOP: 'CLICKED_BACK_INSHOP', CLICKED_BACK_MOBILE: 'CLICKED_BACK_MOBILE', CLICKED_PAY_NOW: 'CLICKED_PAY_NOW', + CLICKED_PAY_NOW_ADYEN: 'CLICKED_PAY_NOW_ADYEN', + PAY_IN_ADVANCE_CANCEL: 'PAY_IN_ADVANCE_CANCEL', PAY_IN_ADVANCE_ERROR: 'PAY_IN_ADVANCE_ERROR', PAY_IN_ADVANCE_CREDIT_CARD_ERROR: 'PAY_IN_ADVANCE_CREDIT_CARD_ERROR', PAY_IN_ADVANCE_SUCCESS: 'PAY_IN_ADVANCE_SUCCESS', diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js index aa686acd..f461bf59 100644 --- a/src/router/router-constants/routing-table.js +++ b/src/router/router-constants/routing-table.js @@ -641,6 +641,10 @@ const routingTable = () => [ scenario: navigationScenarios.CLICKED_PAY_NOW, destinationIssPageValue: issPageValues.PAYMENT_PAGE }, + { + scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN, + destinationIssPageValue: issPageValues.PAYMENT_PAGE_ADYEN + }, { scenario: navigationScenarios.EDIT_SERVICE_LOCATION_INSHOP, destinationIssPageValue: issPageValues.SCHEDULE_PAGE @@ -676,6 +680,23 @@ const routingTable = () => [ } ] }, + { + issPageValue: issPageValues.PAYMENT_PAGE_ADYEN, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationIssPageValue: issPageValues.PAYMENT_METHOD + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationIssPageValue: issPageValues.ORDER_CONFIRMATION + }, + { + scenario: navigationScenarios.PAY_IN_ADVANCE_SUCCESS, + destinationIssPageValue: issPageValues.ORDER_CONFIRMATION + } + ] + }, { issPageValue: issPageValues.PAYMENT_RETURN, maps: [ @@ -693,6 +714,23 @@ const routingTable = () => [ } ] }, + { + issPageValue: issPageValues.PAYMENT_RETURN_ADYEN, + maps: [ + { + scenario: navigationScenarios.PAY_IN_ADVANCE_CANCEL, + destinationIssPageValue: issPageValues.PAYMENT_PAGE_ADYEN + }, + { + scenario: navigationScenarios.PAY_IN_ADVANCE_ERROR, + destinationIssPageValue: issPageValues.PAYMENT_PAGE_ADYEN + }, + { + scenario: navigationScenarios.PAY_IN_ADVANCE_SUCCESS, + destinationIssPageValue: issPageValues.ORDER_CONFIRMATION + } + ] + }, { issPageValue: issPageValues.TPA_CONFIRMATION, maps: [ diff --git a/src/store/index.js b/src/store/index.js index cf2e9904..4321a8f4 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1778,6 +1778,9 @@ export const useMainStore = defineStore({ updatePaypalToken(token) { this.order.payment.paypalToken = token; }, + updateNextGenSettledAmount(amount) { + this.order.payment.nextGenSettledAmount = amount; + }, setSaveSessionPromise(promise) { this.applicationUser.saveSessionPromise = promise; }, @@ -2896,6 +2899,24 @@ export const useMainStore = defineStore({ endpoint: endpoints.GetPaymentSignature.url }); }, + async initializeAdyenPayment(requestBody) { + const response = await globalMethods.callHttpClient({ + method: endpoints.InitializeAdyenPayment.method, + endpoint: endpoints.InitializeAdyenPayment.url, + payload: requestBody, + logApiCall: true + }); + return response.data; + }, + async getAdyenSessionResult(paymentSessionRequest) { + const response = await globalMethods.callHttpClient({ + method: endpoints.GetAdyenSessionResult.method, + endpoint: endpoints.GetAdyenSessionResult.url, + payload: paymentSessionRequest, + logApiCall: true + }); + return response.data; + }, hasSubmittedOrder() { return window.sessionStorage.getItem(webStorageConstants.SUBMITTED_ORDER) !== null; diff --git a/vue.config.js b/vue.config.js index 3e5f6ffa..d9d97c60 100644 --- a/vue.config.js +++ b/vue.config.js @@ -3,6 +3,8 @@ process.env.VUE_APP_CURRENT_ENVIRONMENT = 'Localhost'; process.env.VUE_APP_CUSTOMER_PORTAL_URL = 'https://myaccountdev.safelite.com/'; process.env.VUE_APP_GOOGLE_PLACES_API_KEY = 'AIzaSyCuLhQcDdZTTb4JzpUFms1OCch2dk5lHF0'; process.env.VUE_APP_SAFELITE_HOP = 'https://iss-hop-dev.glassclaim.com/fmgCheckoutShared.aspx'; +process.env.VUE_APP_ADYEN_ENVIRONMENT = 'test'; +process.env.VUE_APP_ADYEN_CLIENT_KEY = 'test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD'; // GA & GTM // NOTE: Using the old ISS site GTM Cotnainer ID for now, will create a new one soon.