Skip to content

Commit 898381c

Browse files
author
FranciscoOnt
committed
feat(checkout): INT-775 Implement Masterpass button in customer section
1 parent ad7c5ff commit 898381c

12 files changed

+499
-7
lines changed

src/customer/create-customer-strategy-registry.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PaymentMethodActionCreator, PaymentMethodRequestSender } from '../payme
99
import { AmazonPayScriptLoader } from '../payment/strategies/amazon-pay';
1010
import { createBraintreeVisaCheckoutPaymentProcessor, VisaCheckoutScriptLoader } from '../payment/strategies/braintree';
1111
import { ChasePayScriptLoader } from '../payment/strategies/chasepay';
12+
import { MasterpassScriptLoader } from '../payment/strategies/masterpass';
1213
import { RemoteCheckoutActionCreator, RemoteCheckoutRequestSender } from '../remote-checkout';
1314

1415
import CustomerActionCreator from './customer-action-creator';
@@ -20,6 +21,7 @@ import {
2021
ChasePayCustomerStrategy,
2122
CustomerStrategy,
2223
DefaultCustomerStrategy,
24+
MasterpassCustomerStrategy,
2325
} from './strategies';
2426
import SquareCustomerStrategy from './strategies/square-customer-strategy';
2527

@@ -35,14 +37,15 @@ export default function createCustomerStrategyRegistry(
3537
const paymentMethodActionCreator = new PaymentMethodActionCreator(new PaymentMethodRequestSender(requestSender));
3638
const remoteCheckoutRequestSender = new RemoteCheckoutRequestSender(requestSender);
3739
const remoteCheckoutActionCreator = new RemoteCheckoutActionCreator(remoteCheckoutRequestSender);
40+
const scriptLoader = getScriptLoader();
3841

3942
registry.register('amazon', () =>
4043
new AmazonPayCustomerStrategy(
4144
store,
4245
paymentMethodActionCreator,
4346
remoteCheckoutActionCreator,
4447
remoteCheckoutRequestSender,
45-
new AmazonPayScriptLoader(getScriptLoader())
48+
new AmazonPayScriptLoader(scriptLoader)
4649
)
4750
);
4851

@@ -53,8 +56,8 @@ export default function createCustomerStrategyRegistry(
5356
paymentMethodActionCreator,
5457
new CustomerStrategyActionCreator(registry),
5558
remoteCheckoutActionCreator,
56-
createBraintreeVisaCheckoutPaymentProcessor(getScriptLoader(), requestSender),
57-
new VisaCheckoutScriptLoader(getScriptLoader())
59+
createBraintreeVisaCheckoutPaymentProcessor(scriptLoader, requestSender),
60+
new VisaCheckoutScriptLoader(scriptLoader)
5861
)
5962
);
6063

@@ -63,7 +66,7 @@ export default function createCustomerStrategyRegistry(
6366
store,
6467
paymentMethodActionCreator,
6568
remoteCheckoutActionCreator,
66-
new ChasePayScriptLoader(getScriptLoader()),
69+
new ChasePayScriptLoader(scriptLoader),
6770
requestSender,
6871
createFormPoster()
6972
)
@@ -73,8 +76,17 @@ export default function createCustomerStrategyRegistry(
7376
new SquareCustomerStrategy(
7477
store,
7578
new RemoteCheckoutActionCreator(remoteCheckoutRequestSender)
76-
)
77-
);
79+
)
80+
);
81+
82+
registry.register('masterpass', () =>
83+
new MasterpassCustomerStrategy(
84+
store,
85+
paymentMethodActionCreator,
86+
remoteCheckoutActionCreator,
87+
new MasterpassScriptLoader(scriptLoader)
88+
)
89+
);
7890

7991
registry.register('default', () =>
8092
new DefaultCustomerStrategy(

src/customer/customer-request-options.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { RequestOptions } from '../common/http-request';
22

3-
import { AmazonPayCustomerInitializeOptions, BraintreeVisaCheckoutCustomerInitializeOptions, ChasePayCustomerInitializeOptions } from './strategies';
3+
import {
4+
AmazonPayCustomerInitializeOptions,
5+
BraintreeVisaCheckoutCustomerInitializeOptions,
6+
ChasePayCustomerInitializeOptions,
7+
MasterpassCustomerInitializeOptions
8+
} from './strategies';
49

510
/**
611
* A set of options for configuring any requests related to the customer step of
@@ -41,4 +46,5 @@ export interface CustomerInitializeOptions extends CustomerRequestOptions {
4146
* They can be omitted unless you need to support Chasepay.
4247
*/
4348
chasepay?: ChasePayCustomerInitializeOptions;
49+
masterpass?: MasterpassCustomerInitializeOptions;
4450
}

src/customer/strategies/chasepay-customer-strategy.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,34 @@ describe('ChasePayCustomerStrategy', () => {
146146
expect(JPMC.ChasePay.configure).toHaveBeenCalled();
147147
});
148148

149+
it('fails to initialize the strategy if no methodid is supplied', async () => {
150+
chasePayOptions = { methodId: undefined, chasepay: { container: 'login' } };
151+
try {
152+
await strategy.initialize(chasePayOptions);
153+
} catch (e) {
154+
expect(e).toBeInstanceOf(InvalidArgumentError);
155+
}
156+
});
157+
158+
it('fails to initialize the strategy if no cart is supplied', async () => {
159+
jest.spyOn(store.getState().cart, 'getCart')
160+
.mockReturnValue(undefined);
161+
try {
162+
await strategy.initialize(chasePayOptions);
163+
} catch (e) {
164+
expect(e).toBeInstanceOf(MissingDataError);
165+
}
166+
});
167+
168+
it('fails to initialize the strategy if no digitalSessionID is supplied', async () => {
169+
paymentMethodMock.initializationData.digitalSessionId = undefined;
170+
try {
171+
await strategy.initialize(chasePayOptions);
172+
} catch (e) {
173+
expect(e).toBeInstanceOf(NotInitializedError);
174+
}
175+
});
176+
149177
it('registers the start and complete callbacks', async () => {
150178
JPMC.ChasePay.on = jest.fn((type, callback) => callback);
151179

src/customer/strategies/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export { default as DefaultCustomerStrategy } from './default-customer-strategy'
44
export { default as BraintreeVisaCheckoutCustomerStrategy, BraintreeVisaCheckoutCustomerInitializeOptions } from './braintree-visacheckout-customer-strategy';
55
export { default as ChasePayCustomerStrategy, ChasePayCustomerInitializeOptions } from './chasepay-customer-strategy';
66
export { default as SquareCustomerStrategy } from './square-customer-strategy';
7+
export { default as MasterpassCustomerInitializeOptions} from './masterpass-customer-initialize-options';
8+
export { default as MasterpassCustomerStrategy } from './masterpass-customer-strategy';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default interface MasterpassCustomerInitializeOptions {
2+
/**
3+
* The ID of a container which the checkout button should be inserted into.
4+
*/
5+
container: string;
6+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { createRequestSender, RequestSender } from '@bigcommerce/request-sender';
2+
import { createScriptLoader } from '@bigcommerce/script-loader';
3+
4+
import { getCartState } from '../../cart/carts.mock';
5+
import { createCheckoutStore, CheckoutStore } from '../../checkout';
6+
import { getCheckoutState } from '../../checkout/checkouts.mock';
7+
import { InvalidArgumentError, MissingDataError } from '../../common/error/errors';
8+
import { getConfigState } from '../../config/configs.mock';
9+
import { PaymentMethod, PaymentMethodActionCreator, PaymentMethodRequestSender } from '../../payment';
10+
import { getMasterpass, getPaymentMethodsState } from '../../payment/payment-methods.mock';
11+
import { Masterpass, MasterpassScriptLoader } from '../../payment/strategies/masterpass';
12+
import { getMasterpassScriptMock } from '../../payment/strategies/masterpass/masterpass.mock';
13+
import { RemoteCheckoutActionCreator, RemoteCheckoutRequestSender } from '../../remote-checkout';
14+
import { CustomerInitializeOptions } from '../customer-request-options';
15+
import { getCustomerState } from '../customers.mock';
16+
17+
import { CustomerStrategy, MasterpassCustomerStrategy } from './';
18+
19+
describe('MasterpassCustomerStrategy', () => {
20+
let container: HTMLDivElement;
21+
let masterpass: Masterpass;
22+
let masterpassScriptLoader: MasterpassScriptLoader;
23+
let paymentMethodActionCreator: PaymentMethodActionCreator;
24+
let paymentMethodMock: PaymentMethod;
25+
let remoteCheckoutActionCreator: RemoteCheckoutActionCreator;
26+
let requestSender: RequestSender;
27+
let store: CheckoutStore;
28+
let strategy: CustomerStrategy;
29+
30+
beforeEach(() => {
31+
paymentMethodMock = {
32+
...getMasterpass(),
33+
initializationData: {
34+
checkoutId: 'checkoutId',
35+
allowedCardTypes: ['visa', 'amex', 'mastercard'],
36+
},
37+
};
38+
39+
store = createCheckoutStore({
40+
checkout: getCheckoutState(),
41+
customer: getCustomerState(),
42+
config: getConfigState(),
43+
cart: getCartState(),
44+
paymentMethods: getPaymentMethodsState(),
45+
});
46+
47+
jest.spyOn(store, 'dispatch')
48+
.mockReturnValue(Promise.resolve(store.getState()));
49+
50+
jest.spyOn(store.getState().paymentMethods, 'getPaymentMethod')
51+
.mockReturnValue(paymentMethodMock);
52+
53+
requestSender = createRequestSender();
54+
55+
remoteCheckoutActionCreator = new RemoteCheckoutActionCreator(
56+
new RemoteCheckoutRequestSender(requestSender)
57+
);
58+
59+
masterpass = getMasterpassScriptMock();
60+
61+
masterpassScriptLoader = new MasterpassScriptLoader(createScriptLoader());
62+
63+
jest.spyOn(masterpassScriptLoader, 'load')
64+
.mockReturnValue(Promise.resolve(masterpass));
65+
66+
paymentMethodActionCreator = new PaymentMethodActionCreator(
67+
new PaymentMethodRequestSender(requestSender)
68+
);
69+
strategy = new MasterpassCustomerStrategy(
70+
store,
71+
paymentMethodActionCreator,
72+
remoteCheckoutActionCreator,
73+
masterpassScriptLoader
74+
);
75+
76+
container = document.createElement('div');
77+
container.setAttribute('id', 'login');
78+
document.body.appendChild(container);
79+
});
80+
81+
afterEach(() => {
82+
document.body.removeChild(container);
83+
});
84+
85+
it('creates an instance of MasterpassCustomerStrategy', () => {
86+
expect(strategy).toBeInstanceOf(MasterpassCustomerStrategy);
87+
});
88+
89+
describe('#initialize()', () => {
90+
let masterpassOptions: CustomerInitializeOptions;
91+
92+
beforeEach(() => {
93+
masterpassOptions = { methodId: 'masterpass', masterpass: { container: 'login' } };
94+
});
95+
96+
it('loads masterpass script in test mode if enabled', async () => {
97+
paymentMethodMock.config.testMode = true;
98+
99+
await strategy.initialize(masterpassOptions);
100+
101+
expect(masterpassScriptLoader.load).toHaveBeenLastCalledWith(true);
102+
});
103+
104+
it('loads masterpass without test mode if disabled', async () => {
105+
paymentMethodMock.config.testMode = false;
106+
107+
await strategy.initialize(masterpassOptions);
108+
109+
expect(masterpassScriptLoader.load).toHaveBeenLastCalledWith(false);
110+
});
111+
112+
it('fails to initialize the strategy if no methodid is supplied', async () => {
113+
masterpassOptions = { methodId: undefined, masterpass: { container: 'login' } };
114+
try {
115+
await strategy.initialize(masterpassOptions);
116+
} catch (e) {
117+
expect(e).toBeInstanceOf(InvalidArgumentError);
118+
}
119+
});
120+
121+
it('fails to initialize the strategy if no cart is supplied', async () => {
122+
jest.spyOn(store.getState().cart, 'getCart')
123+
.mockReturnValue(undefined);
124+
try {
125+
await strategy.initialize(masterpassOptions);
126+
} catch (e) {
127+
expect(e).toBeInstanceOf(MissingDataError);
128+
}
129+
});
130+
131+
it('fails to initialize the strategy if no checkoutId is supplied', async () => {
132+
paymentMethodMock.initializationData.checkoutId = undefined;
133+
try {
134+
await strategy.initialize(masterpassOptions);
135+
} catch (e) {
136+
expect(e).toBeInstanceOf(MissingDataError);
137+
}
138+
});
139+
140+
it('proceeds to checkout if masterpass button is clicked', async () => {
141+
jest.spyOn(masterpass, 'checkout');
142+
await strategy.initialize(masterpassOptions);
143+
if (masterpassOptions.masterpass) {
144+
const masterpassButton = document.getElementById(masterpassOptions.masterpass.container);
145+
if (masterpassButton) {
146+
const btn = masterpassButton.firstChild as HTMLElement;
147+
if (btn) {
148+
btn.click();
149+
expect(masterpass.checkout).toHaveBeenCalled();
150+
}
151+
}
152+
}
153+
});
154+
});
155+
156+
describe('#deinitialize()', () => {
157+
let masterpassOptions: CustomerInitializeOptions;
158+
159+
beforeEach(() => {
160+
masterpassOptions = { methodId: 'masterpass', masterpass: { container: 'login' } };
161+
});
162+
163+
it('succesfully deinitializes the strategy', async () => {
164+
jest.spyOn(masterpass, 'checkout');
165+
await strategy.initialize(masterpassOptions);
166+
strategy.deinitialize();
167+
if (masterpassOptions.masterpass) {
168+
const masterpassButton = document.getElementById(masterpassOptions.masterpass.container);
169+
if (masterpassButton) {
170+
expect(masterpassButton.firstChild).toBe(null);
171+
}
172+
}
173+
// Prevent "After Each" failure
174+
container = document.createElement('div');
175+
document.body.appendChild(container);
176+
});
177+
});
178+
179+
describe('#signIn()', () => {
180+
beforeEach(async () => {
181+
await strategy.initialize({ methodId: 'masterpass', masterpass: { container: 'login' } });
182+
});
183+
184+
it('throws error if trying to sign in programmatically', async () => {
185+
expect(() => strategy.signIn({ email: 'foo@bar.com', password: 'foobar' })).toThrowError();
186+
});
187+
});
188+
189+
describe('#signOut()', () => {
190+
beforeEach(async () => {
191+
const paymentId = {
192+
providerId: 'masterpass',
193+
};
194+
195+
jest.spyOn(store.getState().payment, 'getPaymentId')
196+
.mockReturnValue(paymentId);
197+
198+
jest.spyOn(remoteCheckoutActionCreator, 'signOut')
199+
.mockReturnValue('data');
200+
201+
await strategy.initialize({ methodId: 'masterpass', masterpass: { container: 'login' } });
202+
});
203+
204+
it('throws error if trying to sign out programmatically', async () => {
205+
const options = {
206+
methodId: 'masterpass',
207+
};
208+
209+
await strategy.signOut(options);
210+
211+
expect(remoteCheckoutActionCreator.signOut).toHaveBeenCalledWith('masterpass', options);
212+
expect(store.dispatch).toHaveBeenCalled();
213+
});
214+
});
215+
});

0 commit comments

Comments
 (0)