Skip to content

Commit eabb2f3

Browse files
authored
feat(payment): STRIPE-200 add stripe phone field (#1681)
1 parent 42df8d3 commit eabb2f3

File tree

5 files changed

+330
-22
lines changed

5 files changed

+330
-22
lines changed

packages/core/src/payment/strategies/stripe-upe/stripe-upe-script-loader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default class StripeUPEScriptLoader {
3030
'alipay_pm_beta_1',
3131
'link_default_integration_beta_1',
3232
'shipping_address_element_beta_1',
33+
'address_element_beta_1',
3334
],
3435
apiVersion: '2020-03-02;alipay_beta=v1;link_beta=v1',
3536
});

packages/core/src/payment/strategies/stripe-upe/stripe-upe.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface StripeCustomerEvent extends StripeEvent {
6060

6161
export interface StripeShippingEvent extends StripeEvent {
6262
isNewAddress?: boolean;
63+
phoneFieldRequired: boolean;
6364
value: {
6465
address: {
6566
city: string;
@@ -70,6 +71,7 @@ export interface StripeShippingEvent extends StripeEvent {
7071
state: string;
7172
};
7273
name: string;
74+
phone: string;
7375
};
7476
}
7577

@@ -166,6 +168,7 @@ export interface StripeConfirmPaymentData {
166168

167169
export interface FieldsOptions {
168170
billingDetails?: AutoOrNever | BillingDetailsProperties;
171+
phone?: string;
169172
}
170173

171174
export interface WalletOptions {
@@ -177,14 +180,25 @@ export interface WalletOptions {
177180
* All available options are here https://stripe.com/docs/js/elements_object/create_payment_element
178181
*/
179182
export interface StripeElementsCreateOptions {
183+
mode?: string;
180184
fields?: FieldsOptions;
181185
wallets?: WalletOptions;
182186
allowedCountries?: string[];
183187
defaultValues?: ShippingDefaultValues | CustomerDefaultValues;
188+
validation?: validationElement;
189+
}
190+
191+
interface validationElement {
192+
phone?: validationRequiredElement;
193+
}
194+
195+
interface validationRequiredElement {
196+
required?: string;
184197
}
185198

186199
interface ShippingDefaultValues {
187200
name: string;
201+
phone: string;
188202
address: {
189203
line1: string;
190204
line2: string;
@@ -356,5 +370,5 @@ export enum StripeStringConstants {
356370
export enum StripeElementType {
357371
PAYMENT = 'payment',
358372
AUTHENTICATION = 'linkAuthentication',
359-
SHIPPING = 'shippingAddress',
373+
SHIPPING = 'address',
360374
}

packages/core/src/shipping/strategies/stripe-upe/stripe-upe-shipping-strategy.spec.ts

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import { createScriptLoader } from '@bigcommerce/script-loader';
44
import { Observable, of } from 'rxjs';
55

66
import { CheckoutRequestSender, CheckoutStore, createCheckoutStore } from '../../../checkout';
7-
import { InvalidArgumentError, MissingDataError } from '../../../common/error/errors';
7+
import {
8+
InvalidArgumentError,
9+
MissingDataError,
10+
NotInitializedError,
11+
} from '../../../common/error/errors';
812
import { getGuestCustomer } from '../../../customer/customers.mock';
13+
import { getAddressFormFields } from '../../../form/form.mock';
914
import {
1015
LoadPaymentMethodAction,
1116
PaymentMethod,
@@ -15,13 +20,17 @@ import {
1520
} from '../../../payment';
1621
import { getStripeUPE } from '../../../payment/payment-methods.mock';
1722
import {
23+
StripeElement,
1824
StripeHostWindow,
1925
StripeScriptLoader,
26+
StripeShippingEvent,
2027
StripeUPEClient,
2128
} from '../../../payment/strategies/stripe-upe';
2229
import {
2330
getShippingStripeUPEJsMock,
2431
getShippingStripeUPEJsMockWithAnElementCreated,
32+
getShippingStripeUPEJsOnMock,
33+
getStripeUPEInitializeOptionsMockWithStyles,
2534
getStripeUPEShippingInitializeOptionsMock,
2635
} from '../../../shipping/strategies/stripe-upe/stripe-upe-shipping.mock';
2736
import ConsignmentActionCreator from '../../consignment-action-creator';
@@ -45,6 +54,28 @@ describe('StripeUPEShippingStrategy', () => {
4554
let paymentMethodMock: PaymentMethod;
4655
let paymentMethodActionCreator: PaymentMethodActionCreator;
4756

57+
const stripeShippingEvent = (complete = true): StripeShippingEvent => {
58+
return {
59+
complete,
60+
elementType: '',
61+
empty: false,
62+
isNewAddress: true,
63+
phoneFieldRequired: true,
64+
value: {
65+
address: {
66+
city: 'Lorem',
67+
country: 'US',
68+
line1: 'ok',
69+
line2: 'ok',
70+
postal_code: '44910',
71+
state: 'TX',
72+
},
73+
name: 'Alan',
74+
phone: '+523333333333',
75+
},
76+
};
77+
};
78+
4879
beforeEach(() => {
4980
store = createCheckoutStore();
5081
paymentMethodMock = { ...getStripeUPE(), clientToken: 'myToken' };
@@ -137,6 +168,94 @@ describe('StripeUPEShippingStrategy', () => {
137168
expect(stripeUPEJsMock.elements).toHaveBeenCalledTimes(1);
138169
});
139170

171+
it('does not load stripeUPE if initialization options are not provided', () => {
172+
const promise = strategy.initialize({ methodId: 'stripeupe' });
173+
174+
expect(promise).rejects.toThrow(NotInitializedError);
175+
});
176+
177+
it('does not load stripeUPE if UPE options are not provided', () => {
178+
const promise = strategy.initialize({
179+
methodId: 'stripeupe',
180+
stripeupe: {
181+
methodId: '',
182+
gatewayId: '',
183+
onChangeShipping: jest.fn(),
184+
availableCountries: 'US,MX',
185+
getStripeState: jest.fn(),
186+
},
187+
});
188+
189+
expect(promise).rejects.toThrow(InvalidArgumentError);
190+
});
191+
192+
it('does not load stripeUPE when styles is provided', async () => {
193+
const testColor = '#123456';
194+
const style = {
195+
labelText: testColor,
196+
fieldText: testColor,
197+
fieldPlaceholderText: testColor,
198+
fieldErrorText: testColor,
199+
fieldBackground: testColor,
200+
fieldInnerShadow: testColor,
201+
fieldBorder: testColor,
202+
};
203+
204+
await expect(
205+
strategy.initialize(getStripeUPEInitializeOptionsMockWithStyles(style)),
206+
).resolves.toBe(store.getState());
207+
expect(stripeUPEJsMock.elements).toHaveBeenNthCalledWith(1, {
208+
clientSecret: 'clientToken',
209+
appearance: {
210+
rules: {
211+
'.Input': {
212+
borderColor: testColor,
213+
boxShadow: testColor,
214+
color: testColor,
215+
},
216+
},
217+
variables: {
218+
borderRadius: '4px',
219+
colorBackground: testColor,
220+
colorDanger: testColor,
221+
colorPrimary: testColor,
222+
colorText: testColor,
223+
colorTextPlaceholder: testColor,
224+
colorTextSecondary: testColor,
225+
spacingUnit: '4px',
226+
},
227+
},
228+
});
229+
});
230+
231+
it('loads a single instance of StripeUPEClient without last name and phone fields', async () => {
232+
jest.spyOn(store.getState().shippingAddress, 'getShippingAddress').mockReturnValue(
233+
getShippingAddress(),
234+
);
235+
236+
jest.spyOn(store.getState().form, 'getShippingAddressFields').mockReturnValue([
237+
{
238+
id: 'field_7',
239+
name: 'phone',
240+
custom: false,
241+
label: 'Phone Number',
242+
required: false,
243+
default: '',
244+
},
245+
]);
246+
jest.spyOn(store.getState().shippingAddress, 'getShippingAddress').mockReturnValue({
247+
...getShippingAddress(),
248+
lastName: '',
249+
});
250+
251+
await expect(strategy.initialize(shippingInitialization)).resolves.toBe(
252+
store.getState(),
253+
);
254+
255+
expect(stripeScriptLoader.getStripeClient).toHaveBeenCalledTimes(1);
256+
expect(stripeUPEJsMock.elements).toHaveBeenCalledTimes(1);
257+
});
258+
140259
it('returns an error when methodId is not present', () => {
141260
const promise = strategy.initialize({
142261
...getStripeUPEShippingInitializeOptionsMock(),
@@ -156,6 +275,105 @@ describe('StripeUPEShippingStrategy', () => {
156275

157276
expect(promise).rejects.toBeInstanceOf(MissingDataError);
158277
});
278+
279+
it('triggers onChange event callback and mounts component', async () => {
280+
const stripeMockElement: StripeElement = {
281+
destroy: jest.fn(),
282+
mount: jest.fn(),
283+
unmount: jest.fn(),
284+
on: jest.fn((_, callback) => callback(stripeShippingEvent(true))),
285+
};
286+
const stripeUPEJsMockWithElement = getShippingStripeUPEJsOnMock(stripeMockElement);
287+
288+
jest.spyOn(store.getState().shippingAddress, 'getShippingAddress').mockReturnValue(
289+
getShippingAddress(),
290+
);
291+
jest.spyOn(stripeScriptLoader, 'getStripeClient').mockResolvedValueOnce(
292+
stripeUPEJsMockWithElement,
293+
);
294+
jest.useFakeTimers();
295+
296+
await expect(strategy.initialize(shippingInitialization)).resolves.toBe(
297+
store.getState(),
298+
);
299+
300+
jest.runAllTimers();
301+
302+
expect(stripeScriptLoader.getStripeClient).toHaveBeenCalledTimes(1);
303+
expect(stripeMockElement.on).toHaveBeenCalledTimes(1);
304+
expect(stripeMockElement.mount).toHaveBeenCalledWith(expect.any(String));
305+
expect(shippingInitialization.stripeupe?.onChangeShipping).toHaveBeenCalledTimes(1);
306+
});
307+
308+
it('triggers onChange event callback and mounts component when event is not completed', async () => {
309+
const stripeMockElement: StripeElement = {
310+
destroy: jest.fn(),
311+
mount: jest.fn(),
312+
unmount: jest.fn(),
313+
on: jest.fn((_, callback) => callback(stripeShippingEvent(false))),
314+
};
315+
const stripeUPEJsMockWithElement = getShippingStripeUPEJsOnMock(stripeMockElement);
316+
317+
jest.spyOn(store.getState().form, 'getShippingAddressFields').mockReturnValue(
318+
getAddressFormFields(),
319+
);
320+
jest.spyOn(store.getState().shippingAddress, 'getShippingAddress').mockReturnValue({
321+
...getShippingAddress(),
322+
countryCode: '',
323+
});
324+
jest.spyOn(stripeScriptLoader, 'getStripeClient').mockResolvedValueOnce(
325+
stripeUPEJsMockWithElement,
326+
);
327+
jest.useFakeTimers();
328+
329+
await expect(strategy.initialize(shippingInitialization)).resolves.toBe(
330+
store.getState(),
331+
);
332+
333+
jest.runAllTimers();
334+
335+
expect(stripeScriptLoader.getStripeClient).toHaveBeenCalledTimes(1);
336+
expect(stripeMockElement.on).toHaveBeenCalledTimes(1);
337+
expect(stripeMockElement.mount).toHaveBeenCalledWith(expect.any(String));
338+
expect(shippingInitialization.stripeupe?.onChangeShipping).toHaveBeenCalledTimes(1);
339+
});
340+
341+
it('triggers onChange event callback and throws error if event data is missing', async () => {
342+
const missingShippingEvent = (): StripeShippingEvent => {
343+
return {
344+
complete: false,
345+
elementType: '',
346+
empty: false,
347+
phoneFieldRequired: false,
348+
value: {
349+
address: {
350+
city: '',
351+
country: '',
352+
line1: '',
353+
line2: '',
354+
postal_code: '',
355+
state: '',
356+
},
357+
name: '',
358+
phone: '',
359+
},
360+
};
361+
};
362+
const stripeMockElement: StripeElement = {
363+
destroy: jest.fn(),
364+
mount: jest.fn(),
365+
unmount: jest.fn(),
366+
on: jest.fn((_, callback) => callback(missingShippingEvent)),
367+
};
368+
369+
jest.spyOn(stripeScriptLoader, 'getStripeClient').mockResolvedValueOnce(
370+
getShippingStripeUPEJsOnMock(stripeMockElement),
371+
);
372+
373+
const promise = strategy.initialize(shippingInitialization);
374+
375+
await expect(promise).rejects.toBeInstanceOf(MissingDataError);
376+
});
159377
});
160378

161379
describe('#deinitialize()', () => {

0 commit comments

Comments
 (0)