Skip to content

Commit

Permalink
feat(payment): INT-2532 Accept payments through StripeV3 using iDEAL …
Browse files Browse the repository at this point in the history
…and SEPA
  • Loading branch information
Victor Parra committed Jul 22, 2020
1 parent d95aa9b commit 69ac6cd
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 94 deletions.
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"homepage": "https://github.com/bigcommerce/checkout-js#readme",
"dependencies": {
"@bigcommerce/checkout-sdk": "^1.83.0",
"@bigcommerce/checkout-sdk": "^1.84.0",
"@bigcommerce/citadel": "^2.15.1",
"@bigcommerce/form-poster": "^1.2.2",
"@bigcommerce/memoize": "^1.0.0",
Expand Down
10 changes: 6 additions & 4 deletions src/app/payment/Payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,20 +247,22 @@ class Payment extends Component<PaymentProps & WithCheckoutPaymentProps & WithLa
const { selectedMethod = defaultMethod } = this.state;

// TODO: Perhaps there is a better way to handle `adyen`, `afterpay`, `amazon`,
// `checkout.com`, `converge`, `sagepay` and `sezzle`. They require a redirection to another website
// during the payment flow but are not categorised as hosted payment methods.
// `checkout.com`, `converge`, `sagepay`, `stripev3` and `sezzle`. They require
// a redirection to another website during the payment flow but are not
// categorised as hosted payment methods.
if (!isSubmittingOrder ||
!selectedMethod ||
selectedMethod.type === PaymentMethodProviderType.Hosted ||
selectedMethod.id === PaymentMethodId.Amazon ||
selectedMethod.id === PaymentMethodId.AmazonPay ||
selectedMethod.id === PaymentMethodId.Checkoutcom ||
selectedMethod.id === PaymentMethodId.Converge ||
selectedMethod.id === PaymentMethodId.Laybuy ||
selectedMethod.id === PaymentMethodId.SagePay ||
selectedMethod.id === PaymentMethodId.Sezzle ||
selectedMethod.id === PaymentMethodId.Laybuy ||
selectedMethod.gateway === PaymentMethodId.AdyenV2 ||
selectedMethod.gateway === PaymentMethodId.Afterpay) {
selectedMethod.gateway === PaymentMethodId.Afterpay ||
selectedMethod.gateway === PaymentMethodId.StripeV3) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/payment/paymentMethod/PaymentMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const PaymentMethodComponent: FunctionComponent<PaymentMethodProps & WithCheckou
return <SquarePaymentMethod { ...props } />;
}

if (method.id === PaymentMethodId.StripeV3) {
if (method.gateway === PaymentMethodId.StripeV3) {
return <StripePaymentMethod { ...props } />;
}

Expand Down
174 changes: 118 additions & 56 deletions src/app/payment/paymentMethod/StripePaymentMethod.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,11 @@ import React, { FunctionComponent } from 'react';
import { CheckoutProvider } from '../../checkout';
import { getStoreConfig } from '../../config/config.mock';
import { createLocaleContext, LocaleContext, LocaleContextType } from '../../locale';
import { getCreditCardInputStyles } from '../creditCard';
import { getPaymentMethod } from '../payment-methods.mock';

import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod';
import { default as PaymentMethodComponent, PaymentMethodProps } from './PaymentMethod';

jest.mock('../creditCard', () => ({
...jest.requireActual('../creditCard'),
getCreditCardInputStyles: jest.fn<ReturnType<typeof getCreditCardInputStyles>, Parameters<typeof getCreditCardInputStyles>>(
(_containerId, _fieldType) => {
return Promise.resolve({ color: 'rgb(255, 0, 0)', fontWeight: '500', fontFamily: 'Montserrat, Arial, Helvetica, sans-serif', fontSize: '14px', fontSmoothing: 'auto'});
}
),
}));

describe('when using Stripe payment', () => {
let method: PaymentMethod;
let checkoutService: CheckoutService;
Expand All @@ -39,7 +29,6 @@ describe('when using Stripe payment', () => {
checkoutService = createCheckoutService();
checkoutState = checkoutService.getState();
localeContext = createLocaleContext(getStoreConfig());
method = { ...getPaymentMethod(), id: 'stripev3' };

jest.spyOn(checkoutState.data, 'getConfig')
.mockReturnValue(getStoreConfig());
Expand All @@ -64,61 +53,134 @@ describe('when using Stripe payment', () => {
);
});

it('renders as hosted widget method', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

expect(component.props())
.toEqual(expect.objectContaining({
containerId: 'stripe-card-field',
deinitializePayment: expect.any(Function),
initializePayment: expect.any(Function),
additionalContainerClassName: 'optimizedCheckout-form-input',
method,
}));
});
describe('when using card component', () => {
beforeEach(() => {
method = { ...getPaymentMethod(), id: 'card', gateway: 'stripev3', method: 'card'};
});

it('renders as hosted widget method', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

expect(component.props())
.toEqual(expect.objectContaining({
containerId: `stripe-card-component-field`,
deinitializePayment: expect.any(Function),
initializePayment: expect.any(Function),
additionalContainerClassName: 'optimizedCheckout-form-input widget--stripev3',
method,
}));
});

it('initializes method with required config', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

it('initializes method with required config', async () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);
component.prop('initializePayment')({
methodId: method.id,
gatewayId: method.gateway,
});

expect(checkoutService.initializePayment)
.toHaveBeenCalledWith(expect.objectContaining({
methodId: method.id,
stripev3: {
containerId: 'stripe-card-component-field',
options: {
classes: {
base: 'form-input optimizedCheckout-form-input',
},
},
},
}));
});
});

component.prop('initializePayment')({
methodId: method.id,
gatewayId: method.gateway,
describe('when using ideal component', () => {
beforeEach(() => {
method = { ...getPaymentMethod(), id: 'idealBank', gateway: 'stripev3', method: 'idealBank'};
});

expect(getCreditCardInputStyles)
.toHaveBeenCalledWith('stripe-card-field', ['color', 'fontFamily', 'fontWeight', 'fontSmoothing']);
it('renders as hosted widget method', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

expect(component.props())
.toEqual(expect.objectContaining({
containerId: `stripe-idealBank-component-field`,
deinitializePayment: expect.any(Function),
initializePayment: expect.any(Function),
additionalContainerClassName: 'optimizedCheckout-form-input widget--stripev3',
method,
}));
});

await new Promise(resolve => process.nextTick(resolve));
it('initializes method with required config', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

expect(checkoutService.initializePayment)
.toHaveBeenCalledWith(expect.objectContaining({
component.prop('initializePayment')({
methodId: method.id,
gatewayId: method.gateway,
[method.id]: {
containerId: 'stripe-card-field',
style: {
base: {
color: 'rgb(255, 0, 0)',
fontWeight: '500',
fontFamily: 'Montserrat, Arial, Helvetica, sans-serif',
fontSize: '14px',
fontSmoothing: 'auto',
'::placeholder': {
color: '#E1E1E1',
},
});

expect(checkoutService.initializePayment)
.toHaveBeenCalledWith(expect.objectContaining({
methodId: method.id,
stripev3: {
containerId: 'stripe-idealBank-component-field',
options: {
classes: {
base: 'form-input optimizedCheckout-form-input',
},
},
},
invalid: {
color: 'rgb(255, 0, 0)',
fontWeight: '500',
fontFamily: 'Montserrat, Arial, Helvetica, sans-serif',
fontSize: '14px',
fontSmoothing: 'auto',
iconColor: 'rgb(255, 0, 0)',
})
);
});
});

describe('when using iban component', () => {
beforeEach(() => {
method = { ...getPaymentMethod(), id: 'iban', gateway: 'stripev3', method: 'iban'};
});

it('renders as hosted widget method', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

expect(component.props())
.toEqual(expect.objectContaining({
containerId: `stripe-iban-component-field`,
deinitializePayment: expect.any(Function),
initializePayment: expect.any(Function),
additionalContainerClassName: 'optimizedCheckout-form-input widget--stripev3',
method,
}));
});

it('initializes method with required config', () => {
const container = mount(<PaymentMethodTest { ...defaultProps } method={ method } />);
const component: ReactWrapper<HostedWidgetPaymentMethodProps> = container.find(HostedWidgetPaymentMethod);

component.prop('initializePayment')({
methodId: method.id,
gatewayId: method.gateway,
});

expect(checkoutService.initializePayment)
.toHaveBeenCalledWith(expect.objectContaining({
methodId: method.id,
stripev3: {
containerId: 'stripe-iban-component-field',
options: {
classes: {
base: 'form-input optimizedCheckout-form-input',
},
supportedCountries: ['SEPA'],
},
},
},
}));
}));
});
});
});
64 changes: 41 additions & 23 deletions src/app/payment/paymentMethod/StripePaymentMethod.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,65 @@
import { PaymentInitializeOptions } from '@bigcommerce/checkout-sdk';
import { PaymentInitializeOptions, StripeElementOptions } from '@bigcommerce/checkout-sdk';
import React, { useCallback, FunctionComponent } from 'react';
import { Omit } from 'utility-types';

import { getCreditCardInputStyles, CreditCardInputStylesType } from '../creditCard';

import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod';

export type SquarePaymentMethodProps = Omit<HostedWidgetPaymentMethodProps, 'containerId' | 'hideContentWhenSignedOut'>;
export type StripePaymentMethodProps = Omit<HostedWidgetPaymentMethodProps, 'containerId'>;

export interface StripeOptions {
card: StripeElementOptions;
iban: StripeElementOptions;
idealBank: StripeElementOptions;
}

const StripePaymentMethod: FunctionComponent<SquarePaymentMethodProps> = ({
export enum StripeV3PaymentMethodType {
card = 'card',
iban = 'iban',
idealBank = 'idealBank',
}

const StripePaymentMethod: FunctionComponent<StripePaymentMethodProps> = ({
initializePayment,
method,
...rest
}) => {
const paymentMethodType = method.id as StripeV3PaymentMethodType;
const containerId = `stripe-${paymentMethodType}-component-field`;

const initializeStripePayment = useCallback(async (options: PaymentInitializeOptions) => {
const creditCardInputStyles = await getCreditCardInputStyles('stripe-card-field', ['color', 'fontFamily', 'fontWeight', 'fontSmoothing']);
const creditCardInputErrorStyles = await getCreditCardInputStyles('stripe-card-field', ['color'], CreditCardInputStylesType.Error);
const classes = {
base: 'form-input optimizedCheckout-form-input',
};

const stripeOptions: StripeOptions = {
[StripeV3PaymentMethodType.card]: {
classes,
},
[StripeV3PaymentMethodType.iban]: {
...{ classes },
supportedCountries: ['SEPA'],
},
[StripeV3PaymentMethodType.idealBank]: {
classes,
},
};

return initializePayment({
...options,
stripev3: {
containerId: 'stripe-card-field',
style: {
base: {
...creditCardInputStyles,
'::placeholder': {
color: '#E1E1E1',
},
},
invalid: {
...creditCardInputErrorStyles,
iconColor: creditCardInputErrorStyles.color,
},
},
containerId,
options: stripeOptions[paymentMethodType],
},
});
}, [initializePayment]);
}, [initializePayment, containerId, paymentMethodType]);

return <HostedWidgetPaymentMethod
{ ...rest }
additionalContainerClassName="optimizedCheckout-form-input"
containerId="stripe-card-field"
additionalContainerClassName="optimizedCheckout-form-input widget--stripev3"
containerId={ containerId }
hideContentWhenSignedOut
initializePayment={ initializeStripePayment }
method={ method }
/>;
};

Expand Down
Loading

0 comments on commit 69ac6cd

Please sign in to comment.