From e8d042efa1cba238cc3b6832c3c4c1f0db9b0bbc Mon Sep 17 00:00:00 2001 From: vstudenichnik-insoft <56135641+vstudenichnik-insoft@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:38:28 +0200 Subject: [PATCH] Upgrade Stripe to support Bancontact (#177) --- src/payment/bancontact/stripe.js | 48 +++++++++-- src/utils/stripe.js | 68 ++++----------- src/utils/stripe.test.js | 52 ++++++++++++ test/payment/bancontact-stripe.test.js | 113 +++++++++++++++++++++++++ types/index.d.ts | 1 + 5 files changed, 221 insertions(+), 61 deletions(-) create mode 100644 test/payment/bancontact-stripe.test.js diff --git a/src/payment/bancontact/stripe.js b/src/payment/bancontact/stripe.js index feb9563..29aee49 100644 --- a/src/payment/bancontact/stripe.js +++ b/src/payment/bancontact/stripe.js @@ -1,8 +1,9 @@ import Payment from '../payment'; -import { createBancontactSource } from '../../utils/stripe'; +import { getBancontactConfirmationDetails } from '../../utils/stripe'; import { PaymentMethodDisabledError, LibraryNotLoadedError, + UnableAuthenticatePaymentMethodError, } from '../../utils/errors'; /** @typedef {import('@stripe/stripe-js').Stripe} Stripe */ @@ -46,24 +47,55 @@ export default class StripeBancontactPayment extends Payment { } async tokenize() { + const cart = await this.getCart(); + const intent = await this.createIntent({ + gateway: 'stripe', + action: 'setup', + account_id: cart.account_id, + intent: { + payment_method_types: ['bancontact'], + usage: 'off_session', + }, + }); + await this.loadScripts(this.scripts); - const cart = await this.getCart(); - const { source, error: sourceError } = await createBancontactSource( - this.stripe, - cart, + const { error } = await this.stripe.confirmBancontactSetup( + intent.client_secret, + getBancontactConfirmationDetails(cart), + ); + + if (error) { + throw new Error(error.message); + } + } + + async handleRedirect(queryParams) { + const { redirect_status, setup_intent_client_secret } = queryParams; + + if (redirect_status !== 'succeeded') { + throw new UnableAuthenticatePaymentMethodError(); + } + + await this.loadScripts(this.scripts); + + const { setupIntent, error } = await this.stripe.retrieveSetupIntent( + setup_intent_client_secret, ); - if (sourceError) { - throw new Error(sourceError.message); + if (error) { + throw new Error(error.message); } await this.updateCart({ billing: { method: 'bancontact', + bancontact: { + token: setupIntent.id, + }, }, }); - window.location.replace(source.redirect.url); + this.onSuccess(); } } diff --git a/src/utils/stripe.js b/src/utils/stripe.js index 4fe2251..9ab69de 100644 --- a/src/utils/stripe.js +++ b/src/utils/stripe.js @@ -1,4 +1,4 @@ -import { get, reduce, toLower, isEmpty } from './index'; +import { get, isEmpty } from './index'; /** @typedef {import('@stripe/stripe-js').Stripe} Stripe */ /** @typedef {import('@stripe/stripe-js').StripeCardElement} StripeCardElement */ @@ -76,44 +76,6 @@ function getBillingDetails(cart) { return details; } -/** - * @param {CreateSourceData} source - * @param {object} data - */ -function setBancontactOwner(source, data) { - const fillValues = (fieldsMap, data) => - reduce( - fieldsMap, - (acc, srcKey, destKey) => { - const value = data[srcKey]; - if (value) { - acc[destKey] = value; - } - return acc; - }, - {}, - ); - const { account = {}, billing, shipping } = data; - const billingData = { - ...account.shipping, - ...account.billing, - ...shipping, - ...billing, - }; - const billingAddress = fillValues(addressFieldsMap, billingData); - - source.owner = { - email: account.email, - name: billingData.name || account.name, - ...(billingData.phone - ? { phone: billingData.phone } - : account.phone - ? { phone: account.phone } - : undefined), - ...(!isEmpty(billingAddress) ? { address: billingAddress } : undefined), - }; -} - /** * @param {string} type * @param {import('@stripe/stripe-js').StripeElements} elements @@ -218,23 +180,23 @@ function getKlarnaConfirmationDetails(cart) { } /** - * @param {Stripe} stripe + * Returns Bancontact Setup Intent confirmation details. + * * @param {object} cart + * @returns {import('@stripe/stripe-js').ConfirmBancontactPaymentData} */ -async function createBancontactSource(stripe, cart) { - /** @type {CreateSourceData} */ - const sourceObject = { - type: 'bancontact', - amount: Math.round(get(cart, 'grand_total', 0) * 100), - currency: toLower(get(cart, 'currency', 'eur')), - redirect: { - return_url: window.location.href, +function getBancontactConfirmationDetails(cart) { + const billingDetails = getBillingDetails(cart); + const returnUrl = `${ + window.location.origin + window.location.pathname + }?gateway=stripe`; + + return { + payment_method: { + billing_details: billingDetails, }, + return_url: returnUrl, }; - - setBancontactOwner(sourceObject, cart); - - return stripe.createSource(sourceObject); } /** @@ -341,7 +303,7 @@ export { createIDealPaymentMethod, getKlarnaIntentDetails, getKlarnaConfirmationDetails, - createBancontactSource, + getBancontactConfirmationDetails, getPaymentRequestData, stripeAmountByCurrency, isStripeChargeableAmount, diff --git a/src/utils/stripe.test.js b/src/utils/stripe.test.js index e565673..fcc25ad 100644 --- a/src/utils/stripe.test.js +++ b/src/utils/stripe.test.js @@ -3,6 +3,7 @@ import { createIDealPaymentMethod, getKlarnaIntentDetails, getKlarnaConfirmationDetails, + getBancontactConfirmationDetails, getPaymentRequestData, } from './stripe'; @@ -294,6 +295,57 @@ describe('utils/stripe', () => { }); }); + describe('#getBancontactConfirmationDetails', () => { + beforeEach(() => { + global.window = { + location: { + origin: 'http://test.swell.test', + pathname: '/checkout', + }, + }; + }); + + afterEach(() => { + global.window = undefined; + }); + + it('should return confirmation details', () => { + const cart = { + account: { email: 'test@swell.is' }, + billing: { + name: 'Test Person-us', + phone: '3106683312', + city: 'Beverly Hills', + country: 'US', + address1: 'Lombard St 10', + address2: 'Apt 214', + zip: '90210', + state: 'CA', + }, + }; + const result = getBancontactConfirmationDetails(cart); + + expect(result).toEqual({ + payment_method: { + billing_details: { + address: { + city: 'Beverly Hills', + country: 'US', + line1: 'Lombard St 10', + line2: 'Apt 214', + postal_code: '90210', + state: 'CA', + }, + email: 'test@swell.is', + name: 'Test Person-us', + phone: '3106683312', + }, + }, + return_url: 'http://test.swell.test/checkout?gateway=stripe', + }); + }); + }); + describe('#getPaymentRequestData', () => { let cart = {}; let params = {}; diff --git a/test/payment/bancontact-stripe.test.js b/test/payment/bancontact-stripe.test.js new file mode 100644 index 0000000..c067163 --- /dev/null +++ b/test/payment/bancontact-stripe.test.js @@ -0,0 +1,113 @@ +import { describePayment } from './utils'; +import { UnableAuthenticatePaymentMethodError } from '../../src/utils/errors'; +import StripeBancontactPayment from '../../src/payment/bancontact/stripe'; + +jest.mock('../../src/utils/stripe', () => ({ + getBancontactConfirmationDetails: () => 'Test confirmation details', +})); + +describePayment( + 'payment/bancontact/stripe', + (request, options, paymentMock) => { + let params; + let methods; + + const confirmBancontactSetup = jest.fn(() => ({})); + const retrieveSetupIntent = jest.fn(() => ({ + setupIntent: { + id: 'seti_bancontact_test', + }, + })); + + beforeEach(() => { + params = {}; + methods = { + card: { + publishable_key: 'test_stripe_publishable_key', + }, + bancontact: {}, + }; + + global.window.Stripe = jest.fn(() => ({ + confirmBancontactSetup: confirmBancontactSetup, + retrieveSetupIntent: retrieveSetupIntent, + })); + }); + + describe('#tokenize', () => { + it('should create and confirm Stripe Bancontact Setup Intent', async () => { + paymentMock.getCart.mockImplementationOnce(() => + Promise.resolve({ + account_id: 'test_account_id', + }), + ); + + const payment = new StripeBancontactPayment( + request, + options, + params, + methods, + ); + + await payment.tokenize(); + + expect(global.window.Stripe).toHaveBeenCalledWith( + 'test_stripe_publishable_key', + ); + expect(paymentMock.createIntent).toHaveBeenCalledWith({ + account_id: 'test_account_id', + gateway: 'stripe', + action: 'setup', + intent: { + payment_method_types: ['bancontact'], + usage: 'off_session', + }, + }); + expect(confirmBancontactSetup).toHaveBeenCalledWith( + 'test_stripe_client_secret', + 'Test confirmation details', + ); + }); + }); + + describe('#handleRedirect', () => { + it('should handle redirect with succeeded redirect status', async () => { + const queryParams = { + redirect_status: 'succeeded', + payment_intent_client_secret: 'test_intent_client_secret', + }; + const payment = new StripeBancontactPayment( + request, + options, + params, + methods, + ); + + await payment.handleRedirect(queryParams); + + expect(paymentMock.updateCart).toHaveBeenCalledWith({ + billing: { + method: 'bancontact', + bancontact: { token: 'seti_bancontact_test' }, + }, + }); + }); + + it('should throw an error when the redirect status is not "succeeded"', async () => { + const queryParams = { + redirect_status: 'failed', + }; + const payment = new StripeBancontactPayment( + request, + options, + params, + methods, + ); + + await expect(payment.handleRedirect(queryParams)).rejects.toThrow( + UnableAuthenticatePaymentMethodError, + ); + }); + }); + }, +); diff --git a/types/index.d.ts b/types/index.d.ts index 206d46c..7e03c83 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -304,6 +304,7 @@ export namespace payment { card?: InputPaymentRedirect; paysafecard?: InputPaymentRedirect; klarna?: InputPaymentRedirect; + bancontact?: InputPaymentRedirect; }): Promise; export function authenticate(id: string): Promise;