diff --git a/enabler/dev-utils/session.js b/enabler/dev-utils/session.js index 119978f..0d7f0dd 100644 --- a/enabler/dev-utils/session.js +++ b/enabler/dev-utils/session.js @@ -58,7 +58,8 @@ const getSessionId = async (cartId, isDropin = false) => { "bancontactmobile", "twint", "sepadirectdebit", - "klarna_billie" + "klarna_billie", + "amazonpay" ], // add here your allowed methods for development purposes }), }; diff --git a/enabler/src/components/base.ts b/enabler/src/components/base.ts index ae7c59a..6818f72 100644 --- a/enabler/src/components/base.ts +++ b/enabler/src/components/base.ts @@ -8,7 +8,8 @@ import { Klarna, EPS, Twint, - SepaDirectDebit + SepaDirectDebit, + AmazonPay } from "@adyen/adyen-web"; import { ComponentOptions, @@ -27,7 +28,8 @@ type AdyenComponent = | EPS | Twint | Redirect - | SepaDirectDebit; + | SepaDirectDebit + | AmazonPay; /** * Base Web Component diff --git a/enabler/src/components/payment-methods/amazonpay.ts b/enabler/src/components/payment-methods/amazonpay.ts new file mode 100644 index 0000000..3a166ec --- /dev/null +++ b/enabler/src/components/payment-methods/amazonpay.ts @@ -0,0 +1,69 @@ +import { + ComponentOptions, + PaymentMethod, +} from "../../payment-enabler/payment-enabler"; +import { AdyenBaseComponentBuilder, DefaultAdyenComponent } from "../base"; +import { BaseOptions } from "../../payment-enabler/adyen-payment-enabler"; +import { AmazonPay, ICore } from "@adyen/adyen-web"; + +/** + * Amazon pay component + * + * Configuration options: + * https://docs.adyen.com/payment-methods/amazon-pay/web-component/ + */ +export class AmazonpayBuilder extends AdyenBaseComponentBuilder { + public componentHasSubmit = false; + + constructor(baseOptions: BaseOptions) { + super(PaymentMethod.amazonpay, baseOptions); + } + + build(config: ComponentOptions): AmazonPayComponent { + const amazonPayComponent = new AmazonPayComponent({ + paymentMethod: this.paymentMethod, + adyenCheckout: this.adyenCheckout, + componentOptions: config, + sessionId: this.sessionId, + processorUrl: this.processorUrl, + }); + amazonPayComponent.init(); + + return amazonPayComponent; + } +} + +export class AmazonPayComponent extends DefaultAdyenComponent { + constructor(opts: { + paymentMethod: PaymentMethod; + adyenCheckout: ICore; + componentOptions: ComponentOptions; + sessionId: string; + processorUrl: string; + usesOwnCertificate?: boolean; + }) { + super(opts); + } + + init(): void { + const returnUrl = this.processorUrl.endsWith("/") + ? `${this.processorUrl}payments/amazonpay?step=review&sessionId=${this.sessionId}` + : `${this.processorUrl}/payments/amazonpay?step=review&sessionId=${this.sessionId}`; + + this.component = new AmazonPay(this.adyenCheckout, { + showPayButton: this.componentOptions.showPayButton, + productType: 'PayOnly', + // environment: 'test', // we can add an additional environment variable for the enabler for this, or add it to the baseOptions to be passed by the user of the enabler + returnUrl, + onClick: (resolve, reject) => { + if (this.componentOptions.onPayButtonClick) { + return this.componentOptions + .onPayButtonClick() + .then(() => resolve()) + .catch((error) => reject(error)); + } + return resolve(); + }, + }); + } +} diff --git a/enabler/src/payment-enabler/adyen-payment-enabler.ts b/enabler/src/payment-enabler/adyen-payment-enabler.ts index db280ca..d3380c1 100644 --- a/enabler/src/payment-enabler/adyen-payment-enabler.ts +++ b/enabler/src/payment-enabler/adyen-payment-enabler.ts @@ -33,6 +33,7 @@ import { DropinEmbeddedBuilder } from "../dropin/dropin-embedded"; import { SepaBuilder } from "../components/payment-methods/sepadirectdebit"; import { BancontactMobileBuilder } from "../components/payment-methods/bancontactcard-mobile"; import { KlarnaBillieBuilder } from "../components/payment-methods/klarna-billie"; +import { AmazonpayBuilder } from "../components/payment-methods/amazonpay"; class AdyenInitError extends Error { sessionId: string; @@ -231,6 +232,7 @@ export class AdyenPaymentEnabler implements PaymentEnabler { twint: TwintBuilder, sepadirectdebit: SepaBuilder, klarna_billie: KlarnaBillieBuilder, + amazonpay: AmazonpayBuilder, }; if (!Object.keys(supportedMethods).includes(type)) { throw new Error( diff --git a/enabler/src/payment-enabler/payment-enabler.ts b/enabler/src/payment-enabler/payment-enabler.ts index 433790d..dc63140 100644 --- a/enabler/src/payment-enabler/payment-enabler.ts +++ b/enabler/src/payment-enabler/payment-enabler.ts @@ -43,7 +43,8 @@ export enum PaymentMethod { bancontactmobile = "bcmc_mobile", // Bancontact mobile twint = "twint", sepadirectdebit = "sepadirectdebit", - klarna_billie = "klarna_b2b" // Billie + klarna_billie = "klarna_b2b", // Billie + amazonpay = "amazonpay" } export type PaymentResult = diff --git a/processor/package.json b/processor/package.json index df5ff70..cebe996 100644 --- a/processor/package.json +++ b/processor/package.json @@ -10,7 +10,7 @@ "lint:fix": "prettier --write \"**/**/*.{ts,js,json}\" && eslint --fix --ext .ts src", "build": "rm -rf /dist && tsc", "dev": "ts-node src/main.ts", - "watch": "nodemon --watch \"src/**\" --ext \"ts,json\" --ignore \"src/**/*.spec.ts\" --exec \"ts-node src/main.ts\"", + "watch": "nodemon --watch \"src/**\" --ext \"ts,json,html\" --ignore \"src/**/*.spec.ts\" --exec \"ts-node src/main.ts\"", "test": "jest --detectOpenHandles", "test:watch": "jest --watch --detectOpenHandles", "test:coverage": "jest --detectOpenHandles --coverage", diff --git a/processor/src/public/checkout.html b/processor/src/public/checkout.html new file mode 100644 index 0000000..7d7e0be --- /dev/null +++ b/processor/src/public/checkout.html @@ -0,0 +1,184 @@ + + + + + + Amazon Pay Checkout + + + +
+ + + + + + + \ No newline at end of file diff --git a/processor/src/routes/adyen-payment.route.ts b/processor/src/routes/adyen-payment.route.ts index 64dd68d..e858156 100644 --- a/processor/src/routes/adyen-payment.route.ts +++ b/processor/src/routes/adyen-payment.route.ts @@ -19,6 +19,11 @@ import { } from '../dtos/adyen-payment.dto'; import { AdyenPaymentService } from '../services/adyen-payment.service'; import { HmacAuthHook } from '../libs/fastify/hooks/hmac-auth.hook'; +import path from 'node:path'; +import fastifyStatic from '@fastify/static'; +import { getConfig } from '../config/config'; +import { promisify } from 'node:util'; +import { readFile } from 'fs'; type PaymentRoutesOptions = { paymentService: AdyenPaymentService; @@ -31,6 +36,14 @@ export const adyenPaymentRoutes = async ( fastify: FastifyInstance, opts: FastifyPluginOptions & PaymentRoutesOptions, ) => { + // Serve static files (HTML, CSS, JS) + fastify.register(fastifyStatic, { + root: path.join(__dirname, 'public'), + prefix: '/public/', + }); + + const readFileAsync = promisify(readFile); + fastify.post<{ Body: PaymentMethodsRequestDTO; Reply: PaymentMethodsResponseDTO }>( '/payment-methods', { @@ -90,6 +103,39 @@ export const adyenPaymentRoutes = async ( }, ); + fastify.get<{ + Reply: string; + Querystring: { + paymentReference: string; + redirectResult?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + }; + }>( + '/payments/amazonpay', + { + preHandler: [], + }, + async (request, reply) => { + //HINT: add check here for amazon session ID and return a html here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const queryParams = request.query as any; + + if (queryParams.amazonCheckoutSessionId) { + const filePath = path.join(__dirname, '../public/checkout.html'); + const fileContent = await readFileAsync(filePath, 'utf8'); + + // Inject the environment variable into the HTML content + const htmlWithEnv = fileContent + .replace('{{ADYEN_CLIENT_KEY}}', getConfig().adyenClientKey) + .replace('{{ENVIRONMENT}}', getConfig().adyenEnvironment); + return reply.type('text/html').send(htmlWithEnv); + } + + return reply.send('missing query parameters'); + }, + ); + fastify.get<{ Reply: ConfirmPaymentResponseDTO; Querystring: { @@ -104,8 +150,10 @@ export const adyenPaymentRoutes = async ( preHandler: [opts.sessionQueryParamAuthHook.authenticate()], }, async (request, reply) => { + //HINT: add check here for amazon session ID and return a html here // eslint-disable-next-line @typescript-eslint/no-explicit-any const queryParams = request.query as any; + const res = await opts.paymentService.confirmPayment({ data: { details: { diff --git a/processor/src/services/converters/capture-payment.converter.ts b/processor/src/services/converters/capture-payment.converter.ts index ac3a835..ca03973 100644 --- a/processor/src/services/converters/capture-payment.converter.ts +++ b/processor/src/services/converters/capture-payment.converter.ts @@ -13,7 +13,7 @@ import { /** * These payment methods require line items to be send to Adyen for capturing payments */ -export const METHODS_REQUIRE_LINE_ITEMS = ['klarna', 'klarna_account', 'klarna_paynow', 'klarna_b2b']; +export const METHODS_REQUIRE_LINE_ITEMS = ['klarna', 'klarna_account', 'klarna_paynow', 'klarna_b2b', 'amazonpay']; export class CapturePaymentConverter { private ctCartService: CommercetoolsCartService; diff --git a/processor/src/services/converters/create-payment.converter.ts b/processor/src/services/converters/create-payment.converter.ts index 3f7e93f..d866608 100644 --- a/processor/src/services/converters/create-payment.converter.ts +++ b/processor/src/services/converters/create-payment.converter.ts @@ -37,6 +37,7 @@ export class CreatePaymentConverter { case 'klarna': case 'klarna_paynow': case 'klarna_b2b': + case 'amazonpay': case 'klarna_account': { return { lineItems: mapCoCoCartItemsToAdyenLineItems(cart), diff --git a/processor/src/services/converters/payment-components.converter.ts b/processor/src/services/converters/payment-components.converter.ts index 2290f28..7d71c31 100644 --- a/processor/src/services/converters/payment-components.converter.ts +++ b/processor/src/services/converters/payment-components.converter.ts @@ -51,6 +51,9 @@ export class PaymentComponentsConverter { { type: 'klarna_billie', // klarna_b2b }, + { + type: 'amazonpay', + }, ], }; } diff --git a/processor/test/services/adyen-payment.service.spec.ts b/processor/test/services/adyen-payment.service.spec.ts index df016a9..a81f654 100644 --- a/processor/test/services/adyen-payment.service.spec.ts +++ b/processor/test/services/adyen-payment.service.spec.ts @@ -98,7 +98,7 @@ describe('adyen-payment.service', () => { test('getSupportedPaymentComponents', async () => { const result: SupportedPaymentComponentsSchemaDTO = await paymentService.getSupportedPaymentComponents(); - expect(result?.components).toHaveLength(14); + expect(result?.components).toHaveLength(15); expect(result?.components[0]?.type).toStrictEqual('card'); expect(result?.components[1]?.type).toStrictEqual('ideal'); expect(result?.components[2]?.type).toStrictEqual('paypal'); @@ -113,6 +113,7 @@ describe('adyen-payment.service', () => { expect(result?.components[11]?.type).toStrictEqual('twint'); expect(result?.components[12]?.type).toStrictEqual('sepadirectdebit'); expect(result?.components[13]?.type).toStrictEqual('klarna_billie'); + expect(result?.components[14]?.type).toStrictEqual('amazonpay'); }); test('getStatus', async () => { diff --git a/processor/test/services/converters/capture.converter.spec.ts b/processor/test/services/converters/capture.converter.spec.ts index 69835b7..6cb8821 100644 --- a/processor/test/services/converters/capture.converter.spec.ts +++ b/processor/test/services/converters/capture.converter.spec.ts @@ -3,7 +3,7 @@ import { METHODS_REQUIRE_LINE_ITEMS } from '../../../src/services/converters/cap describe('capture.converter', () => { test('METHODS_REQUIRE_LINE_ITEMS', () => { - const expected = ['klarna', 'klarna_account', 'klarna_paynow', 'klarna_b2b']; + const expected = ['klarna', 'klarna_account', 'klarna_paynow', 'klarna_b2b', 'amazonpay']; expect(METHODS_REQUIRE_LINE_ITEMS).toEqual(expected); }); });