diff --git a/package-lock.json b/package-lock.json index 7d44b181a..2e45411d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "Safelite", "version": "0.1.0", "dependencies": { + "@adyen/adyen-web": "^6.27.0", "@iframe-resizer/child": "^5.3.3", "axios": "^0.30.2", "bootstrap": "^5.3.3", @@ -59,6 +60,18 @@ "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22" } }, + "node_modules/@adyen/adyen-web": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.27.0.tgz", + "integrity": "sha512-6E6hqEkMkoY/FevjO53zhgeCWMt+i/PDc11MX6eVM7Fvd+Sdgc9cIwTBz+NKIXs4ZerG6n2gd+ZIZEywqF/aBQ==", + "license": "MIT", + "dependencies": { + "@types/applepayjs": "14.0.9", + "@types/googlepay": "0.7.8", + "classnames": "2.5.1", + "preact": "10.22.1" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3173,6 +3186,12 @@ "node": ">=10.13.0" } }, + "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/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3328,6 +3347,12 @@ "@types/send": "*" } }, + "node_modules/@types/googlepay": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.8.tgz", + "integrity": "sha512-EHl7G7jIeit/Px/fi/Du/19SZDvqrRhy0DjJX40Vzs/97m3sro9mzVyAYNC0pfzpWGDY+zPjtTLEfhV+OYWUcQ==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -5789,6 +5814,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.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -13681,6 +13712,16 @@ "dev": true, "license": "MIT" }, + "node_modules/preact": { + "version": "10.22.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz", + "integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 7d06ad112..c55259920 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@adyen/adyen-web": "^6.27.0", "@iframe-resizer/child": "^5.3.3", "axios": "^0.30.2", "bootstrap": "^5.3.3", diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 93a6f70d8..b739d9ff9 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -22,6 +22,7 @@ const applicationConfig = { PIA_ERROR_URL: location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel", CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/confirmation", + PIA_ADYEN_RETURN_URL: location.protocol + "//" + location.host + "/fmg/virtual/payment/return", GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE", YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60", OUTLOOK_CALENDAR: diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index c1771b3f5..e40fce79a 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -216,6 +216,14 @@ const endpoints = { url: "/order/api/v1/order/add-donation-part", method: "POST", }, + InitializeAdyenPayment: { + url: "/payment/api/v1/payment/payment/session/initialize-session", + method: "POST", + }, + GetAdyenSessionResult: { + url: "/payment/api/v1/payment/payment/session/session-result", + method: "POST", + }, }; export { endpoints }; diff --git a/src/constants/experiments.js b/src/constants/experiments.js index f358fc89f..0ca3987f0 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -33,6 +33,7 @@ const experimentSettings = { SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays", SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays", SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays", + USE_ADYEN_PAYMENT: "UseAdyenPayment", }; const experimentTriggers = { diff --git a/src/helpers/adyen-helper.js b/src/helpers/adyen-helper.js new file mode 100644 index 000000000..93bd53bc8 --- /dev/null +++ b/src/helpers/adyen-helper.js @@ -0,0 +1,116 @@ +import { AdyenCheckout } from "@adyen/adyen-web/auto"; +import store from "@/store"; +import globalMethods from "@/global-methods"; +import { endpoints } from "@/constants/endpoints"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; + +export async function getNewSession(sessionConfig) {} + +export async function getSessionInfo(sessionId, encryptedSessionResult, pageNameToLog = "") { + const order = store.getters.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, + pageNameToLog: pageNameToLog, + }); + + 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: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", + environment: "test", + 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", + }; + } + + console.log(`Creating checkout with configuration:`); + console.log(config); + + return await AdyenCheckout(config); +} + +function getLocationInfo(order) { + const serviceLocation = order.serviceLocation; + const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; + 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 "FMG-2.0"; +} diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue new file mode 100644 index 000000000..5e74692d5 --- /dev/null +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -0,0 +1,410 @@ + + diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 955d106b5..0fa02e80a 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -170,6 +170,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas import { debugLog } from "@/helpers/debug-log-helper"; import { ErrorMessage } from "vee-validate"; import { Field } from "vee-validate"; +import experimentMixin from "../../mixins/experiment-mixin"; defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); @@ -609,10 +610,17 @@ export default { } this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - this.$router.navigateWithoutSaving( - this.navigationScenarios.CLICKED_PAY_NOW, - this.pageName + + const shouldUseAdyen = experimentMixin.methods.hasSettingEqualTo( + experimentSettings.USE_ADYEN_PAYMENT, + "true" ); + + const scenarioName = shouldUseAdyen + ? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN + : this.navigationScenarios.CLICKED_PAY_NOW; + + this.$router.navigateWithoutSaving(scenarioName, this.pageName); }, async evaluatePromosAndTaxItemsOnOrder() { //Revalidate promos if there are any inactive, or active promos diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index b1ec2b120..c905f3d78 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -156,6 +156,9 @@ export default { lastFour: lastFour, }; + console.log(`CCToken =`); + console.log(ccToken); + baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); baseMixin.methods.dispatchStoreAction( storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index b660afca3..c2247d59b 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -61,6 +61,7 @@ const navigationScenarios = { // Payment CLICKED_INSURANCE: "CLICKED_INSURANCE", CLICKED_PAY_NOW: "CLICKED_PAY_NOW", + CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN", CLICKED_PAY_LATER: "CLICKED_PAY_LATER", PIA_ERROR: "PIA_ERROR", PIA_CC_ERROR: "PIA_CC_ERROR", diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index 98d6c503a..5090df2b3 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -89,6 +89,15 @@ export const routeData = { name: "payment-pia-return", path: "/payment-pia-return", }, + PAYMENT_ADYEN: { + name: "payment-adyen", + path: "/payment-adyen", + }, + PAYMENT_ADYEN_RETURN: { + name: "adyen-return", + path: "/virtual/payment/return", + virtual: true, + }, CONFIRMATION: { name: "confirmation", path: "/confirmation", diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 02a0d9384..192e1572e 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -537,6 +537,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_PAY_NOW, destinationPageData: routeData.PAYMENT, }, + { + scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN, + destinationPageData: routeData.PAYMENT_ADYEN, + }, { scenario: navigationScenarios.CLICKED_INSURANCE, destinationPageData: routeData.INSURANCE_COMPANY, @@ -563,6 +567,19 @@ const routingTable = function () { }, ], }, + { + pageName: routeData.PAYMENT_ADYEN.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.PAYMENT_METHOD, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.CONFIRMATION, + }, + ], + }, { pageName: routeData.PAYMENT_PIA_RETURN.name, maps: [ diff --git a/src/router/methods/route-logic/adyen-return.js b/src/router/methods/route-logic/adyen-return.js new file mode 100644 index 000000000..a516518ac --- /dev/null +++ b/src/router/methods/route-logic/adyen-return.js @@ -0,0 +1,73 @@ +import { AdyenCheckout } from "@adyen/adyen-web"; +import { routeData } from "@/router/constants/routes"; +import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper"; + +export async function adyenReturnBeforeEnter(to, from) { + console.log(`In adyen return.`); + console.log(JSON.parse(JSON.stringify(to))); + + const sessionId = to?.query?.sessionId; + const redirectResult = to?.query?.redirectResult; + + if (sessionId && redirectResult) { + const result = await finalizeAdyenPayment(sessionId, redirectResult); + console.log(`Out of promise`); + console.log(result); + + const sessionInfo = await getSessionInfo(sessionId, result.sessionResult); + + console.log(`Got session info:`); + console.log(sessionInfo); + + // TODO once afterpay info is returned, create cc token and submit order. + + return { + name: routeData.CONFIRMATION.name, + replace: true, + }; + } + + return { + name: routeData.ERROR.name, + replace: true, + }; +} + +function finalizeAdyenPayment(sessionId, redirectResult) { + return new Promise((resolve, reject) => { + createAdyenCheckout({ + session: { + id: sessionId, + }, + handlers: { + onPaymentCompleted: (result, component) => { + console.log(`Payment successful`); + console.log(result); + resolve(result); + }, + onPaymentFailed: (result, component) => { + console.log(`Payment failed`); + console.log(result); + reject("Payment failed from Adyen"); + }, + onError: (error, component) => { + console.log(`Error occured`); + console.log(error); + reject("Error from Adyen"); + }, + }, + options: {}, + }).then( + (checkout) => { + checkout.submitDetails({ + details: { + redirectResult: redirectResult, + }, + }); + }, + (error) => { + reject(error); + } + ); + }); +} diff --git a/src/router/methods/routes.js b/src/router/methods/routes.js index 97af04068..ec6077270 100644 --- a/src/router/methods/routes.js +++ b/src/router/methods/routes.js @@ -13,6 +13,7 @@ import { restartBeforeEnter } from "@/router/methods/route-logic/restart"; import { paymentMethodBeforeEnter } from "@/router/methods/route-logic/payment-method"; import { paymentBeforeEnter } from "@/router/methods/route-logic/payment"; import { serviceLocationBeforeEnter } from "@/router/methods/route-logic/service-location"; +import { adyenReturnBeforeEnter } from "@/router/methods/route-logic/adyen-return"; export const routes = [ // Non-virtual pages. @@ -36,6 +37,7 @@ export const routes = [ createRoute(routeData.PAYMENT_METHOD, paymentMethodBeforeEnter), createRoute(routeData.PAYMENT, paymentBeforeEnter), createRoute(routeData.PAYMENT_PIA_RETURN), + createRoute(routeData.PAYMENT_ADYEN), createRoute(routeData.CONFIRMATION), createRoute(routeData.RETURN_USER), createRoute(routeData.MOBILE_DETAILS), @@ -47,4 +49,5 @@ export const routes = [ createVirtualRoute(routeData.AUTO_ROUTE, autoRouteBeforeEnter), createVirtualRoute(routeData.ERROR, errorBeforeEnter), createVirtualRoute(routeData.RESTART, restartBeforeEnter), + createVirtualRoute(routeData.PAYMENT_ADYEN_RETURN, adyenReturnBeforeEnter), ];