Skip to content

Commit

Permalink
Upgrade Stripe to support Bancontact (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
vstudenichnik-insoft authored Jun 26, 2024
1 parent 45656d2 commit e8d042e
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 61 deletions.
48 changes: 40 additions & 8 deletions src/payment/bancontact/stripe.js
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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();
}
}
68 changes: 15 additions & 53 deletions src/utils/stripe.js
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -341,7 +303,7 @@ export {
createIDealPaymentMethod,
getKlarnaIntentDetails,
getKlarnaConfirmationDetails,
createBancontactSource,
getBancontactConfirmationDetails,
getPaymentRequestData,
stripeAmountByCurrency,
isStripeChargeableAmount,
Expand Down
52 changes: 52 additions & 0 deletions src/utils/stripe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createIDealPaymentMethod,
getKlarnaIntentDetails,
getKlarnaConfirmationDetails,
getBancontactConfirmationDetails,
getPaymentRequestData,
} from './stripe';

Expand Down Expand Up @@ -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 = {};
Expand Down
113 changes: 113 additions & 0 deletions test/payment/bancontact-stripe.test.js
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
},
);
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export namespace payment {
card?: InputPaymentRedirect;
paysafecard?: InputPaymentRedirect;
klarna?: InputPaymentRedirect;
bancontact?: InputPaymentRedirect;
}): Promise<void>;

export function authenticate(id: string): Promise<object | { error: Error }>;
Expand Down

0 comments on commit e8d042e

Please sign in to comment.