Skip to content

Commit a6d4f1c

Browse files
feat(payment): PAYPAL-6352 implemented server side shipping callbacks logic
1 parent ef35c12 commit a6d4f1c

7 files changed

Lines changed: 285 additions & 219 deletions

packages/bigcommerce-payments-integration/src/bigcommerce-payments-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ export interface BigCommercePaymentsInitializationData {
265265
shouldRenderFields?: boolean;
266266
shouldRunAcceleratedCheckout?: boolean;
267267
paymentButtonStyles?: Record<string, PayPalButtonStyleOptions>;
268-
isAppSwitchEnabled?: boolean;
269268
paypalBNPLConfiguration?: PayPalBNPLConfigurationItem[];
269+
isServerSideShippingCallbacksEnabled?: boolean;
270270
}
271271

272272
/**

packages/bigcommerce-payments-integration/src/bigcommerce-payments/bigcommerce-payments-button-strategy.spec.ts

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -441,57 +441,6 @@ describe('BigCommercePaymentsButtonStrategy', () => {
441441
expect(bigCommercePaymentsSdkRenderMock).toHaveBeenCalled();
442442
});
443443

444-
it('render button with appSwitch flag', async () => {
445-
jest.spyOn(
446-
paymentIntegrationService.getState(),
447-
'getPaymentMethodOrThrow',
448-
).mockReturnValue({
449-
...paymentMethod,
450-
initializationData: {
451-
...paymentMethod.initializationData,
452-
isAppSwitchEnabled: true,
453-
},
454-
});
455-
456-
await strategy.initialize(initializationOptions);
457-
458-
expect(paypalSdk.Buttons).toHaveBeenCalledWith({
459-
appSwitchWhenAvailable: true,
460-
fundingSource: paypalSdk.FUNDING.PAYPAL,
461-
style: bigCommercePaymentsOptions.style,
462-
createOrder: expect.any(Function),
463-
onApprove: expect.any(Function),
464-
});
465-
});
466-
467-
it('calls resume when appSwitch enabled and returned from app', async () => {
468-
jest.spyOn(
469-
paymentIntegrationService.getState(),
470-
'getPaymentMethodOrThrow',
471-
).mockReturnValue({
472-
...paymentMethod,
473-
initializationData: {
474-
...paymentMethod.initializationData,
475-
isAppSwitchEnabled: true,
476-
},
477-
});
478-
479-
const resumeMock = jest.fn();
480-
const bigCommercePaymentsSdkRenderMock = jest.fn();
481-
482-
jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({
483-
close: jest.fn(),
484-
isEligible: jest.fn(() => true),
485-
render: bigCommercePaymentsSdkRenderMock,
486-
hasReturned: jest.fn(() => true),
487-
resume: resumeMock,
488-
}));
489-
490-
await strategy.initialize(initializationOptions);
491-
492-
expect(resumeMock).toHaveBeenCalled();
493-
});
494-
495444
it('does not render PayPal button if it is not eligible', async () => {
496445
const bigCommercePaymentsSdkRenderMock = jest.fn();
497446

@@ -586,6 +535,14 @@ describe('BigCommercePaymentsButtonStrategy', () => {
586535

587536
describe('#onApprove button callback', () => {
588537
describe('default flow', () => {
538+
beforeEach(() => {
539+
const consignment = getConsignment();
540+
jest.spyOn(
541+
paymentIntegrationService.getState(),
542+
'getConsignmentsOrThrow',
543+
).mockReturnValue([consignment]);
544+
});
545+
589546
it('tokenizes payment on paypal approve', async () => {
590547
await strategy.initialize(initializationOptions);
591548

@@ -598,6 +555,94 @@ describe('BigCommercePaymentsButtonStrategy', () => {
598555
paypalOrderId,
599556
);
600557
});
558+
559+
it('call getConsignmentOrThrow when server side shipping callbacks is on', async () => {
560+
jest.spyOn(
561+
paymentIntegrationService.getState(),
562+
'getPaymentMethodOrThrow',
563+
).mockReturnValue({
564+
...paymentMethod,
565+
initializationData: {
566+
...paymentMethod.initializationData,
567+
isHostedCheckoutEnabled: true,
568+
isServerSideShippingCallbacksEnabled: true,
569+
},
570+
});
571+
await strategy.initialize(initializationOptions);
572+
573+
eventEmitter.emit('onApprove');
574+
await new Promise((resolve) => process.nextTick(resolve));
575+
576+
expect(
577+
paymentIntegrationService.getState().getConsignmentsOrThrow,
578+
).toHaveBeenCalled();
579+
});
580+
581+
it('selects shipping option when server side shipping callbacks is on', async () => {
582+
jest.spyOn(
583+
paymentIntegrationService.getState(),
584+
'getPaymentMethodOrThrow',
585+
).mockReturnValue({
586+
...paymentMethod,
587+
initializationData: {
588+
...paymentMethod.initializationData,
589+
isHostedCheckoutEnabled: true,
590+
isServerSideShippingCallbacksEnabled: true,
591+
},
592+
});
593+
594+
await strategy.initialize(initializationOptions);
595+
596+
eventEmitter.emit('onApprove');
597+
await new Promise((resolve) => process.nextTick(resolve));
598+
599+
expect(paymentIntegrationService.selectShippingOption).toHaveBeenCalled();
600+
});
601+
602+
it('updates address with merged object when server side shipping callbacks is on', async () => {
603+
const consignment = getConsignment();
604+
jest.spyOn(
605+
paymentIntegrationService.getState(),
606+
'getPaymentMethodOrThrow',
607+
).mockReturnValue({
608+
...paymentMethod,
609+
initializationData: {
610+
...paymentMethod.initializationData,
611+
isHostedCheckoutEnabled: true,
612+
isServerSideShippingCallbacksEnabled: true,
613+
},
614+
});
615+
616+
await strategy.initialize(initializationOptions);
617+
618+
const orderDetails = {
619+
firstName: 'Full',
620+
lastName: 'Name',
621+
email: 'john@doe.com',
622+
phone: '',
623+
company: '',
624+
address1: '2 E 61st St',
625+
address2: 'Apt.1',
626+
city: 'New York',
627+
countryCode: 'US',
628+
postalCode: '10065',
629+
stateOrProvince: '',
630+
stateOrProvinceCode: 'NY',
631+
customFields: [],
632+
};
633+
634+
const shippingAddress = {
635+
...consignment.shippingAddress,
636+
...orderDetails,
637+
};
638+
639+
eventEmitter.emit('onApprove');
640+
await new Promise((resolve) => process.nextTick(resolve));
641+
642+
expect(paymentIntegrationService.updateShippingAddress).toHaveBeenCalledWith(
643+
shippingAddress,
644+
);
645+
});
601646
});
602647

603648
describe('shipping options feature flow', () => {

packages/bigcommerce-payments-integration/src/bigcommerce-payments/bigcommerce-payments-button-strategy.ts

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import BigCommercePaymentsButtonInitializeOptions, {
2323
} from './bigcommerce-payments-button-initialize-options';
2424

2525
export default class BigCommercePaymentsButtonStrategy implements CheckoutButtonStrategy {
26+
private buyNowCartId?: string;
27+
2628
constructor(
2729
private paymentIntegrationService: PaymentIntegrationService,
2830
private bigCommercePaymentsIntegrationService: BigCommercePaymentsIntegrationService,
@@ -88,18 +90,18 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
8890
false,
8991
);
9092

91-
this.renderButton(containerId, methodId, bigcommerce_payments, isBuyNowFlow);
93+
this.renderButton(containerId, methodId, bigcommerce_payments);
9294
}
9395

9496
deinitialize(): Promise<void> {
97+
this.buyNowCartId = undefined;
9598
return Promise.resolve();
9699
}
97100

98101
private renderButton(
99102
containerId: string,
100103
methodId: string,
101104
bigcommerce_payments: BigCommercePaymentsButtonInitializeOptions,
102-
isBuyNowFlow?: boolean,
103105
): void {
104106
const { buyNowInitializeOptions, style, onComplete, onEligibilityFailure } =
105107
bigcommerce_payments;
@@ -108,14 +110,10 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
108110
const state = this.paymentIntegrationService.getState();
109111
const paymentMethod =
110112
state.getPaymentMethodOrThrow<BigCommercePaymentsInitializationData>(methodId);
111-
const { isHostedCheckoutEnabled, isAppSwitchEnabled } =
113+
const { isHostedCheckoutEnabled, isServerSideShippingCallbacksEnabled } =
112114
paymentMethod.initializationData || {};
113115

114116
const defaultCallbacks = {
115-
...(!isBuyNowFlow &&
116-
this.isPaypalCommerceAppSwitchEnabled(methodId) && {
117-
appSwitchWhenAvailable: true,
118-
}),
119117
createOrder: () =>
120118
this.bigCommercePaymentsIntegrationService.createOrder('bigcommerce_payments'),
121119
onApprove: ({ orderID }: ApproveCallbackPayload) =>
@@ -128,14 +126,20 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
128126
};
129127

130128
const hostedCheckoutCallbacks = {
131-
...(!isAppSwitchEnabled && {
129+
...(!isServerSideShippingCallbacksEnabled && {
132130
onShippingAddressChange: (data: ShippingAddressChangeCallbackPayload) =>
133131
this.onShippingAddressChange(data),
134132
onShippingOptionsChange: (data: ShippingOptionChangeCallbackPayload) =>
135133
this.onShippingOptionsChange(data),
136134
}),
137135
onApprove: (data: ApproveCallbackPayload, actions: ApproveCallbackActions) =>
138-
this.onHostedCheckoutApprove(data, actions, methodId, onComplete),
136+
this.onHostedCheckoutApprove(
137+
data,
138+
actions,
139+
methodId,
140+
onComplete,
141+
isServerSideShippingCallbacksEnabled,
142+
),
139143
};
140144

141145
const buttonRenderOptions: BigCommercePaymentsButtonsOptions = {
@@ -149,11 +153,7 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
149153
const paypalButton = paypalSdk.Buttons(buttonRenderOptions);
150154

151155
if (paypalButton.isEligible()) {
152-
if (paypalButton.hasReturned?.() && this.isPaypalCommerceAppSwitchEnabled(methodId)) {
153-
paypalButton.resume?.();
154-
} else {
155-
paypalButton.render(`#${containerId}`);
156-
}
156+
paypalButton.render(`#${containerId}`);
157157
} else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') {
158158
onEligibilityFailure();
159159
} else {
@@ -169,6 +169,7 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
169169
await this.bigCommercePaymentsIntegrationService.createBuyNowCartOrThrow(
170170
buyNowInitializeOptions,
171171
);
172+
this.buyNowCartId = buyNowCart.id;
172173

173174
await this.paymentIntegrationService.loadCheckout(buyNowCart.id);
174175
}
@@ -179,6 +180,7 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
179180
actions: ApproveCallbackActions,
180181
methodId: string,
181182
onComplete?: () => void,
183+
isServerSideShippingCallbacksEnabled?: boolean,
182184
): Promise<boolean> {
183185
if (!data.orderID) {
184186
throw new MissingDataError(MissingDataErrorType.MissingOrderId);
@@ -187,6 +189,8 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
187189
const state = this.paymentIntegrationService.getState();
188190
const cart = state.getCartOrThrow();
189191
const orderDetails = await actions.order.get();
192+
let shippingAddress;
193+
let selectedShippingOptionId;
190194

191195
try {
192196
const billingAddress =
@@ -197,12 +201,33 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
197201
await this.paymentIntegrationService.updateBillingAddress(billingAddress);
198202

199203
if (cart.lineItems.physicalItems.length > 0) {
200-
const shippingAddress =
201-
this.bigCommercePaymentsIntegrationService.getShippingAddressFromOrderDetails(
202-
orderDetails,
203-
);
204+
if (isServerSideShippingCallbacksEnabled) {
205+
await this.paymentIntegrationService.loadCheckout(this.buyNowCartId || cart.id);
206+
const refreshedState = this.paymentIntegrationService.getState();
207+
const consignment = refreshedState.getConsignmentsOrThrow()[0];
208+
selectedShippingOptionId = consignment.selectedShippingOption?.id;
209+
const quoteShippingAddress = consignment.shippingAddress;
210+
shippingAddress = {
211+
...quoteShippingAddress,
212+
...this.bigCommercePaymentsIntegrationService.getShippingAddressFromOrderDetails(
213+
orderDetails,
214+
),
215+
};
216+
} else {
217+
shippingAddress =
218+
this.bigCommercePaymentsIntegrationService.getShippingAddressFromOrderDetails(
219+
orderDetails,
220+
);
221+
}
204222

205223
await this.paymentIntegrationService.updateShippingAddress(shippingAddress);
224+
225+
if (selectedShippingOptionId) {
226+
await this.paymentIntegrationService.selectShippingOption(
227+
selectedShippingOptionId,
228+
);
229+
}
230+
206231
await this.bigCommercePaymentsIntegrationService.updateOrder();
207232
}
208233

@@ -271,17 +296,4 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
271296
throw error;
272297
}
273298
}
274-
275-
/**
276-
*
277-
* PayPal AppSwitch enabling handling
278-
*
279-
*/
280-
private isPaypalCommerceAppSwitchEnabled(methodId: string): boolean {
281-
const state = this.paymentIntegrationService.getState();
282-
const paymentMethod =
283-
state.getPaymentMethodOrThrow<BigCommercePaymentsInitializationData>(methodId);
284-
285-
return paymentMethod.initializationData?.isAppSwitchEnabled ?? false;
286-
}
287299
}

0 commit comments

Comments
 (0)