diff --git a/enabler/decs.d.ts b/enabler/decs.d.ts index d5cf927..0f8afe3 100644 --- a/enabler/decs.d.ts +++ b/enabler/decs.d.ts @@ -1 +1,8 @@ declare module '*.scss'; +declare module "@adyen/adyen-web" +declare module "@adyen/adyen-web/dist/types/components/ApplePay" +declare module "@adyen/adyen-web/dist/types/components/GooglePay" +declare module "@adyen/adyen-web/dist/types/core" +declare module "@adyen/adyen-web/dist/types/core/core" +declare module "@adyen/adyen-web/dist/types/components/Redirect/Redirect" +declare module "@adyen/adyen-web/dist/types/core/types" diff --git a/enabler/package-lock.json b/enabler/package-lock.json index d646c2b..d8d6823 100644 --- a/enabler/package-lock.json +++ b/enabler/package-lock.json @@ -8,6 +8,7 @@ "name": "enabler", "version": "1.0.0", "dependencies": { + "@adyen/adyen-web": "5.51.0", "serve": "14.2.1" }, "devDependencies": { @@ -20,6 +21,20 @@ "vite-plugin-css-injected-by-js": "3.4.0" } }, + "node_modules/@adyen/adyen-web": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-5.51.0.tgz", + "integrity": "sha512-jH3Us9k57AhdOrwBL444EQdQ+xTgg4BTD/9/RSOmldw9EM1LzX1+6AKkX/7QioJoYO6L7gI5DUu5iq915ju0IA==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@babel/runtime-corejs3": "^7.20.1", + "@types/applepayjs": "^3.0.4", + "@types/googlepay": "^0.7.0", + "classnames": "^2.3.1", + "core-js-pure": "^3.25.3", + "preact": "10.13.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -600,6 +615,29 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.9.tgz", + "integrity": "sha512-oeOFTrYWdWXCvXGB5orvMTJ6gCZ9I6FBjR+M38iKNXCsPxr4xT0RTdg5uz1H7QP8pp74IzPtwritEr+JscqHXQ==", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", @@ -1676,6 +1714,11 @@ "optional": true, "peer": true }, + "node_modules/@types/applepayjs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-3.0.4.tgz", + "integrity": "sha512-RqaVZWy1Kj4e1PoUoOI8uA+4UuuLpicQFxfU9Y/xWJFZFT6mFB4PiiY911iDxFk7pdvaj5HKH7VsWRisRca1Rg==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1723,6 +1766,11 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/googlepay": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.5.tgz", + "integrity": "sha512-158egcRaqkMSpW6unyGV4uG4FpoCklRf3J5emCzOXSRVAohMfIuZ481JNvp4X6+KxoNjxWiGtMx5vb1YfQADPw==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2353,6 +2401,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "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==" + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -2544,6 +2597,16 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-js-pure": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz", + "integrity": "sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -4411,6 +4474,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.13.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz", + "integrity": "sha512-q44QFLhOhty2Bd0Y46fnYW0gD/cbVM9dUVtNTDKPcdXSMA7jfY+Jpd6rk3GB0lcQss0z5s/6CmVP0Z/hV+g6pw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4511,6 +4583,11 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/registry-auth-token": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", diff --git a/enabler/package.json b/enabler/package.json index 95cf241..dc1ef6f 100644 --- a/enabler/package.json +++ b/enabler/package.json @@ -12,6 +12,7 @@ "serve": "npm run build && serve dist -l 3000 --cors" }, "dependencies": { + "@adyen/adyen-web": "5.51.0", "serve": "14.2.1" }, "devDependencies": { diff --git a/enabler/src/components/base.ts b/enabler/src/components/base.ts index c3c3fbf..b3814b7 100644 --- a/enabler/src/components/base.ts +++ b/enabler/src/components/base.ts @@ -1,53 +1,50 @@ -import { FakeSdk } from '../fake-sdk'; -import { ComponentOptions, PaymentComponent, PaymentMethod, PaymentResult } from '../payment-enabler/payment-enabler'; - -export type ElementOptions = { - paymentMethod: PaymentMethod; -}; +import Core from '@adyen/adyen-web/dist/types/core/core'; +import { ComponentOptions, PaymentComponent, PaymentMethod } from '../payment-enabler/payment-enabler'; +import ApplePay from '@adyen/adyen-web/dist/types/components/ApplePay'; +import GooglePay from '@adyen/adyen-web/dist/types/components/GooglePay'; +import RedirectElement from '@adyen/adyen-web/dist/types/components/Redirect/Redirect'; export type BaseOptions = { - sdk: FakeSdk; - processorUrl: string; - sessionId: string; - environment: string; - config: { - showPayButton?: boolean; - }; - onComplete: (result: PaymentResult) => void; - onError: (error?: any) => void; + adyenCheckout: typeof Core; } /** * Base Web Component */ export abstract class BaseComponent implements PaymentComponent { - protected paymentMethod: ElementOptions['paymentMethod']; - protected sdk: FakeSdk; - protected processorUrl: BaseOptions['processorUrl']; - protected sessionId: BaseOptions['sessionId']; - protected environment: BaseOptions['environment']; - protected config: BaseOptions['config']; - protected showPayButton: boolean; - protected onComplete: (result: PaymentResult) => void; - protected onError: (error?: any) => void; + protected paymentMethod: PaymentMethod; + protected adyenCheckout: typeof Core; + protected config: ComponentOptions['config']; + protected component: typeof ApplePay | typeof GooglePay | typeof RedirectElement; - constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { - this.sdk = baseOptions.sdk; - this.processorUrl = baseOptions.processorUrl; - this.sessionId = baseOptions.sessionId; - this.environment = baseOptions.environment; - this.config = baseOptions.config; - this.onComplete = baseOptions.onComplete; - this.onError = baseOptions.onError; - this.showPayButton = - 'showPayButton' in componentOptions.config ? !!componentOptions.config.showPayButton : - 'showPayButton' in baseOptions.config ? !!baseOptions.config.showPayButton : - true; + constructor(paymentMethod: PaymentMethod, baseOptions: BaseOptions, componentOptions: ComponentOptions) { + this.paymentMethod = paymentMethod; + this.adyenCheckout = baseOptions.adyenCheckout; + this.config = componentOptions.config; + this.component = this._create(); } - abstract submit(): void; + protected _create(): typeof ApplePay | typeof GooglePay | typeof RedirectElement { + return this.adyenCheckout.create(this.paymentMethod, this.config); + } - abstract mount(selector: string): void ; + submit() { + this.component.submit(); + }; + + mount(selector: string) { + if ('isAvailable' in this.component) { + this.component.isAvailable() + .then(() => { + this.component.mount(selector); + }) + .catch((e: unknown) => { + console.log(`${this.paymentMethod } is not available`, e); + }); + } else { + this.component.mount(selector); + } + } showValidation?(): void; isValid?(): boolean; diff --git a/enabler/src/components/payment-methods/applepay.ts b/enabler/src/components/payment-methods/applepay.ts new file mode 100644 index 0000000..1234c84 --- /dev/null +++ b/enabler/src/components/payment-methods/applepay.ts @@ -0,0 +1,14 @@ +import { BaseComponent, BaseOptions } from '../base'; +import { ComponentOptions, PaymentMethod } from '../../payment-enabler/payment-enabler'; + +/** + * Apple pay component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/apple-pay/web-component/ + */ +export class Applepay extends BaseComponent { + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + super(PaymentMethod.applepay, baseOptions, componentOptions); + } +} diff --git a/enabler/src/components/payment-methods/card.ts b/enabler/src/components/payment-methods/card.ts new file mode 100644 index 0000000..157447f --- /dev/null +++ b/enabler/src/components/payment-methods/card.ts @@ -0,0 +1,50 @@ +import { ComponentOptions, PaymentMethod } from '../../payment-enabler/payment-enabler'; +import { BaseComponent, BaseOptions } from '../base'; + +/** + * Credit card component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/cards/web-component/ + */ + + +export class Card extends BaseComponent { + private endDigits: string; + + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + super(PaymentMethod.card, baseOptions, componentOptions); + } + + protected _create() { + const that = this; + return this.adyenCheckout.create(this.paymentMethod, { + onFieldValid : function(data) { + const { endDigits, fieldType } = data; + if (endDigits && fieldType === 'encryptedCardNumber') { + that.endDigits = endDigits; + } + }, + ...this.config, + }); + } + + + showValidation() { + this.component.showValidation(); + } + + isValid() { + return this.component.isValid; + } + + getState() { + return { + card: { + endDigits: this.endDigits, + brand: this.component.state.selectedBrandValue, + } + }; + } + +} diff --git a/enabler/src/components/payment-methods/card/card.ts b/enabler/src/components/payment-methods/card/card.ts deleted file mode 100644 index 870816d..0000000 --- a/enabler/src/components/payment-methods/card/card.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ComponentOptions } from '../../../payment-enabler/payment-enabler'; -import buttonStyles from '../../../style/button.module.scss'; -import inputFieldStyles from '../../../style/inputField.module.scss'; -import styles from '../../../style/style.module.scss'; -import { BaseComponent, BaseOptions } from '../../base'; -import { addFormFieldsEventListeners, fieldIds, getCardBrand, getInput, validateAllFields } from './utils'; - -export class Card extends BaseComponent { - constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { - super(baseOptions, componentOptions); - this.paymentMethod = 'card'; - } - - mount(selector: string) { - document.querySelector(selector).insertAdjacentHTML("afterbegin", this._getTemplate()); - if (this.showPayButton) { - document.querySelector('#creditCardForm-paymentButton').addEventListener('click', (e) => { - e.preventDefault(); - this.submit(); - }); - } - - addFormFieldsEventListeners(); - } - - async submit() { - // here we would call the SDK to submit the payment - this.sdk.init({ environment: this.environment }); - const isFormValid = validateAllFields(); - if (!isFormValid) { - return; - } - try { - const requestData = { - paymentMethod: { - type: this.paymentMethod, - cardNumber: getInput(fieldIds.cardNumber).value.replace(/\s/g, ''), - expiryMonth: getInput(fieldIds.expiryDate).value.split('/')[0], - expiryYear: getInput(fieldIds.expiryDate).value.split('/')[1], - cvc: getInput(fieldIds.cvv).value, - holderName: getInput(fieldIds.holderName).value, - } - }; - const response = await fetch(this.processorUrl + '/payments', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-Session-Id': this.sessionId }, - body: JSON.stringify(requestData), - }); - const data = await response.json(); - - if (data.outcome === 'Authorized' && data.paymentReference) { - this.onComplete && this.onComplete({ isSuccess: true, paymentReference: data.paymentReference }); - } else { - this.onComplete && this.onComplete({ isSuccess: false }); - } - } catch(e) { - this.onError('Some error occurred. Please try again.'); - } - } - - private _getTemplate() { - const payButton = this.showPayButton ? `` : ''; - return ` -
-
-
- - - Invalid card number -
- - - - -
-
-
-
- - - Invalid expiry date -
-
- - - Invalid CVV -
-
-
- - - This field is required -
- ${payButton} -
-
- ` - } - - showValidation() { - validateAllFields(); - } - - isValid() { - return validateAllFields(); - } - - getState() { - return { - card: { - endDigits: getInput(fieldIds.cardNumber).value.slice(-4), - brand: getCardBrand(getInput(fieldIds.cardNumber).value), - expiryDate: getInput(fieldIds.expiryDate).value, - }, - }; - } -} diff --git a/enabler/src/components/payment-methods/card/utils.ts b/enabler/src/components/payment-methods/card/utils.ts deleted file mode 100644 index 1211f26..0000000 --- a/enabler/src/components/payment-methods/card/utils.ts +++ /dev/null @@ -1,180 +0,0 @@ -import styles from '../../../style/style.module.scss'; -import inputFieldStyles from '../../../style/inputField.module.scss'; - -export const fieldIds = { - cardNumber: 'creditCardForm-cardNumber', - expiryDate: 'creditCardForm-expiryDate', - cvv: 'creditCardForm-cvv', - holderName: 'creditCardForm-holderNameLabel', -}; - -export const getInput = (field: string) => (document.querySelector(`#${field}`) as HTMLInputElement); - -const showErrorIfInvalid = (field: string) => { - if (!isFieldValid(field)) { - const input = getInput(field); - input.parentElement.classList.add(inputFieldStyles.error); - input.parentElement.querySelector(`#${field} + .${inputFieldStyles.errorField}`).classList.remove(styles.hidden); - } -} - -const hideErrorIfValid = (field: string) => { - if (isFieldValid(field)) { - const input = getInput(field); - input.parentElement.classList.remove(inputFieldStyles.error); - input.parentElement.querySelector(`#${field} + .${inputFieldStyles.errorField}`).classList.add(styles.hidden); - } -} - -export const validateAllFields = () => { - let isValid = true; - Object.values(fieldIds).forEach((field) => { - if (!isFieldValid(field)) { - isValid = false; - showErrorIfInvalid(field); - } - }); - return isValid; -} - -const handleFieldValidation = (field: string) => { - const input = getInput(field); - input.addEventListener('input', () => { - hideErrorIfValid(field); - }); - input.addEventListener('focusout', () => { - showErrorIfInvalid(field); - input.value.length > 0 ? input.parentElement.classList.add(inputFieldStyles.containValue) : input.parentElement.classList.remove(inputFieldStyles.containValue); - }); -} - -const isFieldValid = (field: string) => { - const input = getInput(field); - switch (field) { - case 'creditCardForm-cardNumber': - return input.value.replace(/\s/g, '').length === 16; - case 'creditCardForm-expiryDate': - return input.value.length === 5; - case 'creditCardForm-cvv': - return input.value.length === 3; - case 'creditCardForm-holderNameLabel': - return input.value.length > 0; - default: - return false; - } -} - -export const getCardBrand = (cardNumber: string) => { - if (cardNumber.startsWith('4')) { - return 'visa'; - } - if (cardNumber.startsWith('5')) { - return 'mastercard'; - } - if (cardNumber.startsWith('6')) { - return 'maestro'; - } - if (cardNumber.startsWith('3')) { - return 'amex'; - } - return 'unknown'; -} - -const dateFormatter = (): ((inputValue: string) => string) => { - let previousValue = ''; - return (inputValue: string): string => { - let output = inputValue; - let isInvalidValue = false; - const pattern = /[0-9/]/; - const lastCharacter = inputValue.slice(-1); - const inputLength = inputValue.length; - /*** - * should not allow any character other than 0-9 and / - * should convert eg: 1/ => 01/ - * eg: 12/3/ =>12/3 - * eg: 18 =>01/8 - * eg: 123 =>12/3 - * eg: 12(previous value = 1) =>12/ - * eg: 12(previous value = 12/) =>1 (delete behaviour) - * eg: 12/345 =>12/34 - */ - if (!pattern.test(lastCharacter)) { - isInvalidValue = true; - } else if (lastCharacter === '/') { - if (inputLength === 2) { - output = `0${inputValue}`; - } else if (inputLength !== 3) { - isInvalidValue = true; - } - } else if (inputLength === 2) { - if (previousValue !== `${output}/`) { - if (Number(inputValue) > 12) { - output = `0${inputValue[0]}/${inputValue[1]}`; - } else { - output = `${output}/`; - } - } else { - output = output[0]; - } - } else if (inputLength === 3 && lastCharacter != '/') { - output = `${inputValue[0]}${inputValue[1]}/${inputValue[2]}`; - } else if (inputLength > 5) { - isInvalidValue = true; - } - - if (isInvalidValue) { - output = inputValue.substring(0, inputLength - 1); - } - previousValue = output; - return output; - }; -}; - -const addCardNumberEventListeners = () => { - const cardNumber = getInput(fieldIds.cardNumber); - cardNumber.addEventListener('input', () => { - cardNumber.value = cardNumber.value.replace(/\D/g,'').replace(/(\d{4})/g, '$1 ').trim(); - const brand = getCardBrand(cardNumber.value); - const cardIcons = document.querySelectorAll(`.${styles.cardIcon}`); - cardIcons.forEach((icon) => { - icon.classList.add(styles.hidden); - }); - const cardIcon = document.querySelector(`#creditCardForm-${brand}`); - if (cardIcon) { - cardIcon.classList.remove(styles.hidden); - } - }); - handleFieldValidation(fieldIds.cardNumber); -} - -const addDateEventListeners = () => { - const expiryDate = getInput(fieldIds.expiryDate); - expiryDate.addEventListener('input', () => { - expiryDate.value = dateFormatter()(expiryDate.value); - }); - handleFieldValidation(fieldIds.expiryDate); -} - -const addCvvEventListeners = () => { - const cvv = getInput(fieldIds.cvv); - cvv.addEventListener('input', () => { - if (isNaN(Number(cvv.value))) { - cvv.value = cvv.value.slice(0, -1); - } - if (cvv.value.length > 3) { - cvv.value = cvv.value.slice(0, 3); - } - }); - handleFieldValidation(fieldIds.cvv); -} - -const addHolderNameEventListeners = () => { - handleFieldValidation(fieldIds.holderName); -} - -export const addFormFieldsEventListeners = () => { - addCardNumberEventListeners(); - addDateEventListeners(); - addCvvEventListeners(); - addHolderNameEventListeners(); -} diff --git a/enabler/src/components/payment-methods/googlepay.ts b/enabler/src/components/payment-methods/googlepay.ts new file mode 100644 index 0000000..f0d02af --- /dev/null +++ b/enabler/src/components/payment-methods/googlepay.ts @@ -0,0 +1,14 @@ +import { BaseComponent, BaseOptions } from '../base'; +import { ComponentOptions, PaymentMethod } from '../../payment-enabler/payment-enabler'; + +/** + * Google pay component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/google-pay/web-component/ + */ +export class Googlepay extends BaseComponent { + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + super(PaymentMethod.googlepay, baseOptions, componentOptions); + } +} diff --git a/enabler/src/components/payment-methods/ideal.ts b/enabler/src/components/payment-methods/ideal.ts new file mode 100644 index 0000000..d6301dc --- /dev/null +++ b/enabler/src/components/payment-methods/ideal.ts @@ -0,0 +1,21 @@ +import { ComponentOptions, PaymentMethod } from '../../payment-enabler/payment-enabler'; +import { BaseComponent, BaseOptions } from '../base'; +/** + * Ideal component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/ideal/web-component/ + */ +export class Ideal extends BaseComponent { + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + super(PaymentMethod.ideal, baseOptions, componentOptions); + } + + showValidation() { + this.component.showValidation(); + } + + isValid() { + return this.component.isValid; + } +} diff --git a/enabler/src/components/payment-methods/klarna.ts b/enabler/src/components/payment-methods/klarna.ts new file mode 100644 index 0000000..25a9d56 --- /dev/null +++ b/enabler/src/components/payment-methods/klarna.ts @@ -0,0 +1,17 @@ +import { BaseComponent, BaseOptions } from '../base'; +import { ComponentOptions, PaymentMethod } from '../../payment-enabler/payment-enabler'; + +/** + * Klarna component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/klarna/web-component/ + */ +export class Klarna extends BaseComponent { + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + /* todo: + pass locale + */ + super(PaymentMethod.klarna, baseOptions, componentOptions); + } +} diff --git a/enabler/src/components/payment-methods/paypal.ts b/enabler/src/components/payment-methods/paypal.ts new file mode 100644 index 0000000..2b794c0 --- /dev/null +++ b/enabler/src/components/payment-methods/paypal.ts @@ -0,0 +1,27 @@ +import { BaseComponent, BaseOptions } from '../base'; +import { ComponentOptions, PaymentMethod } from '../../payment-enabler/payment-enabler'; + +/** + * Paypal component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/paypal/web-component/ + */ +export class Paypal extends BaseComponent { + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + // TODO: + + /* + + Hide Venmo + + If you and your shopper are both located in the US, Venmo is shown in the PayPal Component by default. To hide Venmo in the PayPal Component, set blockPayPalVenmoButton to true. + + Use the create method of your AdyenCheckout instance, in this case checkout, to create an instance of the Component. Add the configuration object if you created one. + + + const paypalComponent = checkout.create('paypal', paypalConfiguration).mount('#paypal-container'); + */ + super(PaymentMethod.paypal, baseOptions, componentOptions); + } +} diff --git a/enabler/src/fake-sdk.ts b/enabler/src/fake-sdk.ts deleted file mode 100644 index b1be1b9..0000000 --- a/enabler/src/fake-sdk.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class FakeSdk { - private environment: string; - constructor({ environment }) { - this.environment = environment; - console.log('FakeSdk constructor', this.environment); - } - init(opts: any) { - console.log('FakeSdk init', opts); - } -} diff --git a/enabler/src/main.ts b/enabler/src/main.ts index 5eef9db..6f02271 100644 --- a/enabler/src/main.ts +++ b/enabler/src/main.ts @@ -1,3 +1,3 @@ -import { MockPaymentEnabler } from './payment-enabler/payment-enabler-mock'; +import { AdyenPaymentEnabler } from './payment-enabler/adyen-payment-enabler'; -export { MockPaymentEnabler as Enabler }; +export { AdyenPaymentEnabler as Enabler }; diff --git a/enabler/src/payment-enabler/adyen-payment-enabler.ts b/enabler/src/payment-enabler/adyen-payment-enabler.ts new file mode 100644 index 0000000..b092ebe --- /dev/null +++ b/enabler/src/payment-enabler/adyen-payment-enabler.ts @@ -0,0 +1,143 @@ +import '@adyen/adyen-web/dist/adyen.css'; +import AdyenCheckout from '@adyen/adyen-web'; +import { CoreOptions } from '@adyen/adyen-web/dist/types/core/types'; +import { BaseOptions } from '../components/base'; + +import { ComponentOptions, PaymentEnabler, EnablerOptions } from './payment-enabler'; +import { Card } from '../components/payment-methods/card'; +import { Ideal } from '../components/payment-methods/ideal'; +import { Googlepay } from '../components/payment-methods/googlepay'; +import { Applepay } from '../components/payment-methods/applepay'; +import { Klarna } from '../components/payment-methods/klarna'; + +type AdyenEnablerOptions = EnablerOptions & { + config: Omit; + onActionRequired?: (action: any) => Promise; +}; + +export class AdyenPaymentEnabler implements PaymentEnabler { + setupData: Promise<{ baseOptions: BaseOptions }>; + + constructor(options: AdyenEnablerOptions) { + this.setupData = AdyenPaymentEnabler._Setup(options); + } + + private static _Setup = async (options: AdyenEnablerOptions): Promise<{ baseOptions: BaseOptions }> => { + const [sessionResponse, configResponse] = await Promise.all([ + fetch(options.processorUrl + '/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Session-Id': options.sessionId }, + body: JSON.stringify({}), + }), + fetch(options.processorUrl + '/operations/config', { + method: 'GET', + headers: { 'Content-Type': 'application/json', 'X-Session-Id': options.sessionId }, + }), + ]); + + const [sessionJson, configJson] = await Promise.all([sessionResponse.json(), configResponse.json()]); + + const { sessionData: data, paymentReference } = sessionJson; + + const adyenCheckout = await AdyenCheckout({ + onPaymentCompleted: (result, component) => { + debugger; + console.info(result, component); + window.location.href = options.processorUrl + '/confirm'; + }, + onError: (error, component) => { + console.error(error.name, error.message, error.stack, component); + }, + onSubmit: async (state, component) => { + try { + const reqData = { + ...state.data, + channel: 'Web', + paymentReference, + }; + const response = await fetch(options.processorUrl + '/payments', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Session-Id': options.sessionId }, + body: JSON.stringify(reqData), + }); + const data = await response.json(); + console.log('onSubmit test', state, component, data) + if (data.action) { + options.onActionRequired && options.onActionRequired({ + offsite: data.action.type === 'redirect', + }); + component.handleAction(data.action); + } else { + if (data.resultCode === 'Authorised') { + component.setStatus('success'); + options.onComplete && options.onComplete({ isSuccess: true, paymentReference }); + } else { + options.onComplete && options.onComplete({ isSuccess: false }); + component.setStatus('error'); + } + } + } catch (e) { + console.log('Payment aborted by client'); + component.setStatus('ready'); + } + }, + onAdditionalDetails: async (state, component) => { + console.log('onAdditionalDetails', state, component); + const requestData = { + ...state.data, + paymentReference, + }; + const url = options.processorUrl.endsWith('/') + ? `${options.processorUrl}payment/details` + : `${options.processorUrl}/payment/details`; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Session-Id': options.sessionId }, + body: JSON.stringify(requestData), + }); + const data = await response.json(); + if (data.resultCode === 'Authorised') { + component.setStatus('success'); + options.onComplete && options.onComplete({ isSuccess: true, paymentReference }); + } else { + options.onComplete && options.onComplete({ isSuccess: false }); + component.setStatus('error'); + } + }, + analytics: { + enabled: true, + }, + + ...(options.config?.locale ? { locale: options.config.locale } : {}), + ...(options.config?.showPayButton ? { showPayButton: options.config.showPayButton } : {}), + + environment: configJson.environment, + clientKey: configJson.clientKey, + session: { + id: data.id, + sessionData: data.sessionData, + }, + }); + + return { + baseOptions: { + adyenCheckout: adyenCheckout + } + }; + } + + async createComponent(type: string, componentOptions: ComponentOptions) { + const { baseOptions } = await this.setupData; + const supportedMethods = { + applepay: Applepay, + card: Card, + googlepay: Googlepay, + ideal: Ideal, + klarna: Klarna, + } + if (!Object.keys(supportedMethods).includes(type)) { + throw new Error(`Component type not supported: ${type}. Supported types: ${Object.keys(supportedMethods).join(', ')}`); + } + return new supportedMethods[type](baseOptions, componentOptions); + } +} diff --git a/enabler/src/payment-enabler/payment-enabler-mock.ts b/enabler/src/payment-enabler/payment-enabler-mock.ts deleted file mode 100644 index f161abd..0000000 --- a/enabler/src/payment-enabler/payment-enabler-mock.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BaseOptions } from '../components/base'; -import { Card } from '../components/payment-methods/card/card'; -import { FakeSdk } from '../fake-sdk'; -import { ComponentOptions, EnablerOptions, PaymentEnabler, PaymentMethod } from './payment-enabler'; - -const SupportedMethods: PaymentMethod[] = ['card']; - -declare global { - interface ImportMeta { - env: any; - } -} - -export class MockPaymentEnabler implements PaymentEnabler { - setupData: Promise<{ baseOptions: BaseOptions }>; - - private constructor(options: EnablerOptions) { - this.setupData = MockPaymentEnabler._Setup(options); - } - - private static _Setup = async (options: EnablerOptions): Promise<{ baseOptions: BaseOptions }> => { - // Fetch SDK config from processor if needed, for example: - - // const configResponse = await fetch(instance.processorUrl + '/config', { - // method: 'GET', - // headers: { 'Content-Type': 'application/json' }, - // }); - - // const configJson = await configResponse.json(); - - const sdkOptions = { - // environment: configJson.environment, - environment: 'test' - } - - return Promise.resolve({ baseOptions: { - sdk: new FakeSdk(sdkOptions), - processorUrl: options.processorUrl, - sessionId: options.sessionId, - environment: sdkOptions.environment, - config: options.config || {}, - onComplete: options.onComplete || (() => {}), - onError: options.onError || (() => {}), - } - }); - } - - async createComponent(type: string, componentOptions: ComponentOptions) { - const { baseOptions } = await this.setupData; - - switch (type) { - case 'card': - return new Card(baseOptions, componentOptions); - } - throw new Error(`Payment method not supported: ${type}. Supported methods: ${Object.keys(SupportedMethods).join(', ')}`); - } -} diff --git a/enabler/src/payment-enabler/payment-enabler.ts b/enabler/src/payment-enabler/payment-enabler.ts index 5a9e6e0..75cd131 100644 --- a/enabler/src/payment-enabler/payment-enabler.ts +++ b/enabler/src/payment-enabler/payment-enabler.ts @@ -15,12 +15,25 @@ export interface PaymentComponent { export type EnablerOptions = { processorUrl: string; sessionId: string; - config?: { showPayButton?: boolean }; + config?: { + locale?: string; + showPayButton?: boolean; + }; + onActionRequired?: () => Promise; onComplete?: (result: PaymentResult) => void; onError?: (error: any) => void; }; -export type PaymentMethod = 'card'; + +export enum PaymentMethod { + applepay = "applepay", + card = "card", + dropin = "dropin", + googlepay = "googlepay", + ideal = "ideal", + klarna = "klarna", + paypal = "paypal", +} export type PaymentResult = { isSuccess: true; @@ -37,5 +50,5 @@ export interface PaymentEnabler { /** * @throws {Error} */ - createComponent: (type: string, opts: ComponentOptions, sessionId?: string) => Promise + createComponent: (type: string, opts: ComponentOptions) => Promise }