Skip to content

Commit

Permalink
feat(customer): CHECKOUT-4223 Add customer step component
Browse files Browse the repository at this point in the history
  • Loading branch information
davidchin committed Aug 8, 2019
1 parent f0fe892 commit 70cb6c6
Show file tree
Hide file tree
Showing 12 changed files with 1,225 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/app/common/request/responses.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Response } from '@bigcommerce/request-sender';

export function getResponse<T>(body: T, headers = {}, status = 200, statusText = 'OK'): Response<T> {
return {
body,
status,
statusText,
headers: {
'content-type': 'application/json',
...headers,
},
};
}
51 changes: 51 additions & 0 deletions src/app/customer/CheckoutButton.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { mount } from 'enzyme';
import { noop } from 'lodash';
import React from 'react';

import CheckoutButton from './CheckoutButton';

describe('CheckoutButton', () => {
it('initializes button when component is mounted', () => {
const initialize = jest.fn();
const onError = jest.fn();

mount(
<CheckoutButton
containerId="foobarContainer"
methodId="foobar"
deinitialize={ noop }
initialize={ initialize }
onError={ onError }
/>
);

expect(initialize)
.toHaveBeenCalledWith({
methodId: 'foobar',
foobar: {
container: 'foobarContainer',
onError,
},
});
});

it('deinitializes button when component unmounts', () => {
const deinitialize = jest.fn();
const onError = jest.fn();

const component = mount(
<CheckoutButton
containerId="foobarContainer"
methodId="foobar"
deinitialize={ deinitialize }
initialize={ noop }
onError={ onError }
/>
);

component.unmount();

expect(deinitialize)
.toHaveBeenCalled();
});
});
46 changes: 46 additions & 0 deletions src/app/customer/CheckoutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CustomerInitializeOptions, CustomerRequestOptions } from '@bigcommerce/checkout-sdk';
import React, { Component } from 'react';

export interface CheckoutButtonProps {
containerId: string;
methodId: string;
deinitialize(options: CustomerRequestOptions): void;
initialize(options: CustomerInitializeOptions): void;
onError?(error: Error): void;
}

export default class CheckoutButton extends Component<CheckoutButtonProps> {
componentDidMount() {
const {
containerId,
initialize,
methodId,
onError,
} = this.props;

initialize({
methodId,
[methodId]: {
container: containerId,
onError,
},
});
}

componentWillUnmount() {
const {
deinitialize,
methodId,
} = this.props;

deinitialize({ methodId });
}

render() {
const { containerId } = this.props;

return (
<div id={ containerId } />
);
}
}
108 changes: 108 additions & 0 deletions src/app/customer/CheckoutButtonList.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mount, render } from 'enzyme';
import { noop } from 'lodash';
import React from 'react';

import { getStoreConfig } from '../config/config.mock';
import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale';

import CheckoutButton from './CheckoutButton';
import CheckoutButtonList from './CheckoutButtonList';

describe('CheckoutButtonList', () => {
let localeContext: LocaleContextType;

beforeEach(() => {
localeContext = createLocaleContext(getStoreConfig());
});

it('matches snapshot', () => {
const component = render(
<LocaleContext.Provider value={ localeContext }>
<CheckoutButtonList
methodIds={ ['amazon', 'braintreevisacheckout'] }
deinitialize={ noop }
initialize={ noop }
/>
</LocaleContext.Provider>
);

expect(component)
.toMatchSnapshot();
});

it('filters out unsupported methods', () => {
const component = mount(
<LocaleContext.Provider value={ localeContext }>
<CheckoutButtonList
methodIds={ ['amazon', 'braintreevisacheckout', 'foobar'] }
deinitialize={ noop }
initialize={ noop }
/>
</LocaleContext.Provider>
);

expect(component.find(CheckoutButton))
.toHaveLength(2);
});

it('does not render if there are no supported methods', () => {
const component = mount(
<LocaleContext.Provider value={ localeContext }>
<CheckoutButtonList
methodIds={ ['foobar'] }
deinitialize={ noop }
initialize={ noop }
/>
</LocaleContext.Provider>
);

expect(component.html())
.toEqual(null);
});

it('passes data to every checkout button', () => {
const deinitialize = jest.fn();
const initialize = jest.fn();
const component = mount(
<LocaleContext.Provider value={ localeContext }>
<CheckoutButtonList
methodIds={ ['amazon', 'braintreevisacheckout'] }
deinitialize={ deinitialize }
initialize={ initialize }
/>
</LocaleContext.Provider>
);

expect(component.find(CheckoutButton).at(0).props())
.toEqual({
containerId: 'amazonCheckoutButton',
methodId: 'amazon',
deinitialize,
initialize,
});
});

it('notifies parent if methods are incompatible with Embedded Checkout', () => {
const methodIds = ['amazon', 'braintreevisacheckout'];
const onError = jest.fn();
const checkEmbeddedSupport = jest.fn(() => { throw new Error(); });

render(
<LocaleContext.Provider value={ localeContext }>
<CheckoutButtonList
methodIds={ methodIds }
checkEmbeddedSupport={ checkEmbeddedSupport }
deinitialize={ noop }
initialize={ noop }
onError={ onError }
/>
</LocaleContext.Provider>
);

expect(checkEmbeddedSupport)
.toHaveBeenCalledWith(methodIds);

expect(onError)
.toHaveBeenCalledWith(expect.any(Error));
});
});
72 changes: 72 additions & 0 deletions src/app/customer/CheckoutButtonList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { CustomerInitializeOptions, CustomerRequestOptions } from '@bigcommerce/checkout-sdk';
import React, { Fragment, FunctionComponent } from 'react';

import { TranslatedString } from '../language';

import CheckoutButton from './CheckoutButton';

// TODO: The API should tell UI which payment method offers its own checkout button
export const SUPPORTED_METHODS: string[] = [
'amazon',
'braintreevisacheckout',
'chasepay',
'masterpass',
'googlepaybraintree',
'googlepaystripe',
];

export interface CheckoutButtonListProps {
methodIds: string[];
checkEmbeddedSupport?(methodIds: string[]): void;
deinitialize(options: CustomerRequestOptions): void;
initialize(options: CustomerInitializeOptions): void;
onError?(error: Error): void;
}

const CheckoutButtonList: FunctionComponent<CheckoutButtonListProps> = ({
checkEmbeddedSupport,
onError,
methodIds,
...rest
}) => {
const supportedMethodIds = methodIds
.filter(methodId => SUPPORTED_METHODS.indexOf(methodId) !== -1);

if (supportedMethodIds.length === 0) {
return null;
}

if (checkEmbeddedSupport) {
try {
checkEmbeddedSupport(supportedMethodIds);
} catch (error) {
if (onError) {
onError(error);
} else {
throw error;
}

return null;
}
}

return (
<Fragment>
<p><TranslatedString id="remote.continue_with_text" /></p>

<div className="checkoutRemote">
{ supportedMethodIds.map(methodId =>
<CheckoutButton
containerId={ `${methodId}CheckoutButton` }
key={ methodId }
methodId={ methodId }
onError={ onError }
{ ...rest }
/>
) }
</div>
</Fragment>
);
};

export default CheckoutButtonList;
Loading

0 comments on commit 70cb6c6

Please sign in to comment.