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/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 index e38578e54..a2d7651a2 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -47,6 +47,8 @@ import { storeActions } from "@/constants/store-actions"; import { getAmountDue } from "@/helpers/pricing-helper.js"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; import { paymentMethods } from "@/constants/payment-method-constants"; +import { createAdyenCheckout } from "@/helpers/adyen-helper"; +import { Dropin } from "@adyen/adyen-web/auto"; export default { name: "payment-adyen", @@ -127,45 +129,31 @@ export default { this.sessionId = responseJson.sessionId; - const configuration = { + const checkout = await createAdyenCheckout({ session: session, - clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", - environment: "test", - amount: { - value: this.adyenPriceTotal, - currency: "USD", - }, - locale: "en_US", - countryCode: "US", - showPayButton: true, - translations: { - en_US: { - "creditCard.securityCode.label": "CVV/CVC", + handlers: { + onPaymentCompleted: (result, component) => { + console.log(`Payment completed from Adyen.`); + this.handleCompletedPayment(result); + }, + onPaymentFailed: (result, component) => { + console.log(`Payment failed from Adyen`); + this.handleFailedPayment(result); + }, + onError: (error, component) => { + console.log(`Error from Adyen`); + this.handleError(error); }, }, - - onPaymentCompleted: (result, component) => { - console.log(`Payment completed from Adyen.`); - this.handleCompletedPayment(result); + options: { + amount: this.adyenPriceTotal, }, - onPaymentFailed: (result, component) => { - console.log(`Payment failed from Adyen`); - this.handleFailedPayment(result); - }, - onError: (error, component) => { - console.log(`Error from Adyen`); - this.handleError(error); - }, - }; + }); - console.log(`Configuration =`); - console.log(configuration); + console.log(checkout); - // eslint-disable-next-line - const checkOut = await AdyenWeb.AdyenCheckout(configuration); - window.checkout = checkOut; - // eslint-disable-next-line - const dropin = new AdyenWeb.Dropin(checkOut, { + window.checkout = checkout; + const dropin = new Dropin(checkout, { paymentMethodsConfiguration: { ideal: { showImage: true, @@ -239,7 +227,11 @@ export default { // Save payment type. // For now, CC. // Need to determine payment type from Adyen response - await this.dispatchStoreAction(storeActions.SAVE_PAYMENT_METHOD_CHOICE, paymentMethods.CREDIT_CARD, false); + await this.dispatchStoreAction( + storeActions.SAVE_PAYMENT_METHOD_CHOICE, + paymentMethods.CREDIT_CARD, + false + ); await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); await baseMixin.methods.dispatchStoreAction( @@ -279,7 +271,6 @@ export default { } catch (error) { console.log(`Error getting session info after failure.`); } - }, handleError(error) {}, @@ -312,18 +303,18 @@ export default { referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, workOrderNumber: this.workOrderNumberLastSixDigits, zipCodeCtu: this.locationInfo.zipCodeCtu, - amount: this.adyenPriceTotal, // TODO + amount: this.adyenPriceTotal, city: this.locationInfo.city, houseNumberOrName: this.splitStreetAddress.number, street: this.splitStreetAddress.street, zipCode: this.locationInfo.zipCode, stateOrProvince: this.locationInfo.state, - returnUrl: "http://localhost:8080/fmg/confirmation", + returnUrl: "http://localhost:8080/fmg/virtual/payment/return", email: this.$store.getters.order.customer.emailAddress, IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: crypto.randomUUID(), // `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, + idempotencyKey: `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, }; }, diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index 4b51ff857..5090df2b3 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -93,6 +93,11 @@ export const routeData = { 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/methods/route-logic/adyen-return.js b/src/router/methods/route-logic/adyen-return.js new file mode 100644 index 000000000..9bf1940ba --- /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 cf0392ccf..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. @@ -48,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), ];