Skip to content

Commit 5501017

Browse files
feat(payment): PAYPAL-6433 implemented server side shipping callbacks logic
1 parent 3df9ed8 commit 5501017

14 files changed

Lines changed: 432 additions & 182 deletions

packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.spec.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => {
453453
initializationData: {
454454
...paymentMethod.initializationData,
455455
isHostedCheckoutEnabled: true,
456-
isAppSwitchEnabled: true,
456+
isServerSideShippingCallbacksEnabled: true,
457457
},
458458
};
459459

@@ -558,19 +558,109 @@ describe('PayPalCommerceCreditButtonStrategy', () => {
558558
});
559559

560560
describe('#onApprove button callback', () => {
561+
beforeEach(() => {
562+
const paymentMethodWithShippingOptionsFeature = {
563+
...paymentMethod,
564+
initializationData: {
565+
...paymentMethod.initializationData,
566+
isHostedCheckoutEnabled: true,
567+
isServerSideShippingCallbacksEnabled: true,
568+
},
569+
};
570+
571+
jest.spyOn(
572+
paymentIntegrationService.getState(),
573+
'getPaymentMethodOrThrow',
574+
).mockReturnValue(paymentMethodWithShippingOptionsFeature);
575+
});
561576
describe('default flow', () => {
562577
it('tokenizes payment on paypal approve', async () => {
578+
const paymentMethodObject = {
579+
...paymentMethod,
580+
initializationData: {
581+
...paymentMethod.initializationData,
582+
isHostedCheckoutEnabled: false,
583+
},
584+
};
585+
jest.spyOn(
586+
paymentIntegrationService.getState(),
587+
'getPaymentMethodOrThrow',
588+
).mockReturnValue(paymentMethodObject);
589+
563590
await strategy.initialize(initializationOptions);
564591

565592
eventEmitter.emit('onApprove');
566-
567593
await new Promise((resolve) => process.nextTick(resolve));
568594

569595
expect(paypalCommerceIntegrationService.tokenizePayment).toHaveBeenCalledWith(
570596
defaultMethodId,
571597
paypalOrderId,
572598
);
573599
});
600+
601+
it('call getConsignmentOrThrow when server side shipping callbacks is on', async () => {
602+
await strategy.initialize(initializationOptions);
603+
604+
eventEmitter.emit('onApprove');
605+
await new Promise((resolve) => process.nextTick(resolve));
606+
607+
expect(
608+
paymentIntegrationService.getState().getConsignmentsOrThrow,
609+
).toHaveBeenCalled();
610+
});
611+
612+
it('selects shipping option when server side shipping callbacks is on', async () => {
613+
const consignment = getConsignment();
614+
jest.spyOn(
615+
paymentIntegrationService.getState(),
616+
'getConsignmentsOrThrow',
617+
).mockReturnValue([consignment]);
618+
619+
await strategy.initialize(initializationOptions);
620+
621+
eventEmitter.emit('onApprove');
622+
await new Promise((resolve) => process.nextTick(resolve));
623+
624+
expect(paymentIntegrationService.selectShippingOption).toHaveBeenCalled();
625+
});
626+
627+
it('updates address with merged object when server side shipping callbacks is on', async () => {
628+
const consignment = getConsignment();
629+
jest.spyOn(
630+
paymentIntegrationService.getState(),
631+
'getConsignmentsOrThrow',
632+
).mockReturnValue([consignment]);
633+
634+
await strategy.initialize(initializationOptions);
635+
636+
const orderDetails = {
637+
firstName: 'Full',
638+
lastName: 'Name',
639+
email: 'john@doe.com',
640+
phone: '',
641+
company: '',
642+
address1: '2 E 61st St',
643+
address2: 'Apt.1',
644+
city: 'New York',
645+
countryCode: 'US',
646+
postalCode: '10065',
647+
stateOrProvince: '',
648+
stateOrProvinceCode: 'NY',
649+
customFields: [],
650+
};
651+
652+
const shippingAddress = {
653+
...consignment.shippingAddress,
654+
...orderDetails,
655+
};
656+
657+
eventEmitter.emit('onApprove');
658+
await new Promise((resolve) => process.nextTick(resolve));
659+
660+
expect(paymentIntegrationService.updateShippingAddress).toHaveBeenCalledWith(
661+
shippingAddress,
662+
);
663+
});
574664
});
575665

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

packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import PayPalCommerceCreditButtonInitializeOptions, {
2222
} from './paypal-commerce-credit-button-initialize-options';
2323

2424
export default class PayPalCommerceCreditButtonStrategy implements CheckoutButtonStrategy {
25+
private buyNowCartId?: string;
26+
2527
constructor(
2628
private paymentIntegrationService: PaymentIntegrationService,
2729
private paypalIntegrationService: PayPalIntegrationService,
@@ -83,7 +85,6 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
8385
const currencyCode = isBuyNowFlow
8486
? providedCurrencyCode
8587
: state.getCartOrThrow().currency.code;
86-
8788
await this.paypalIntegrationService.loadPayPalSdk(methodId, currencyCode, false);
8889

8990
this.renderButton(containerId, methodId, paypalcommercecredit);
@@ -104,7 +105,7 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
104105
const paypalSdk = this.paypalIntegrationService.getPayPalSdkOrThrow();
105106
const state = this.paymentIntegrationService.getState();
106107
const paymentMethod = state.getPaymentMethodOrThrow<PayPalInitializationData>(methodId);
107-
const { isHostedCheckoutEnabled, isAppSwitchEnabled } =
108+
const { isHostedCheckoutEnabled, isServerSideShippingCallbacksEnabled } =
108109
paymentMethod.initializationData || {};
109110

110111
const defaultCallbacks = {
@@ -119,14 +120,20 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
119120
};
120121

121122
const hostedCheckoutCallbacks = {
122-
...(!isAppSwitchEnabled && {
123+
...(!isServerSideShippingCallbacksEnabled && {
123124
onShippingAddressChange: (data: ShippingAddressChangeCallbackPayload) =>
124125
this.onShippingAddressChange(data),
125126
onShippingOptionsChange: (data: ShippingOptionChangeCallbackPayload) =>
126127
this.onShippingOptionsChange(data),
127128
}),
128129
onApprove: (data: ApproveCallbackPayload, actions: ApproveCallbackActions) =>
129-
this.onHostedCheckoutApprove(data, actions, methodId, onComplete),
130+
this.onHostedCheckoutApprove(
131+
data,
132+
actions,
133+
methodId,
134+
onComplete,
135+
isServerSideShippingCallbacksEnabled,
136+
),
130137
};
131138

132139
const fundingSources = [paypalSdk.FUNDING.PAYLATER, paypalSdk.FUNDING.CREDIT];
@@ -166,6 +173,8 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
166173
buyNowInitializeOptions,
167174
);
168175

176+
this.buyNowCartId = buyNowCart.id;
177+
169178
await this.paymentIntegrationService.loadCheckout(buyNowCart.id);
170179
}
171180
}
@@ -175,6 +184,7 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
175184
actions: ApproveCallbackActions,
176185
methodId: string,
177186
onComplete?: () => void,
187+
isServerSideShippingCallbacksEnabled?: boolean,
178188
): Promise<boolean> {
179189
if (!data.orderID) {
180190
throw new MissingDataError(MissingDataErrorType.MissingOrderId);
@@ -183,6 +193,7 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
183193
const state = this.paymentIntegrationService.getState();
184194
const cart = state.getCartOrThrow();
185195
const orderDetails = await actions.order.get();
196+
let shippingAddress;
186197

187198
try {
188199
const billingAddress =
@@ -191,8 +202,30 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto
191202
await this.paymentIntegrationService.updateBillingAddress(billingAddress);
192203

193204
if (cart.lineItems.physicalItems.length > 0) {
194-
const shippingAddress =
195-
this.paypalIntegrationService.getShippingAddressFromOrderDetails(orderDetails);
205+
if (isServerSideShippingCallbacksEnabled) {
206+
await this.paymentIntegrationService.loadCheckout(this.buyNowCartId || cart.id);
207+
const refreshedState = this.paymentIntegrationService.getState();
208+
const consignment = refreshedState.getConsignmentsOrThrow()[0];
209+
const selectedShippingOptionId = consignment.selectedShippingOption?.id;
210+
const quoteShippingAddress = consignment.shippingAddress;
211+
shippingAddress = {
212+
...quoteShippingAddress,
213+
...this.paypalIntegrationService.getShippingAddressFromOrderDetails(
214+
orderDetails,
215+
),
216+
};
217+
218+
if (selectedShippingOptionId) {
219+
await this.paymentIntegrationService.selectShippingOption(
220+
selectedShippingOptionId,
221+
);
222+
}
223+
} else {
224+
shippingAddress =
225+
this.paypalIntegrationService.getShippingAddressFromOrderDetails(
226+
orderDetails,
227+
);
228+
}
196229

197230
await this.paymentIntegrationService.updateShippingAddress(shippingAddress);
198231
await this.paypalIntegrationService.updateOrder('paypalcommerce');

packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-customer-strategy.spec.ts

Lines changed: 96 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -335,34 +335,6 @@ describe('PayPalCommerceCreditCustomerStrategy', () => {
335335
});
336336
});
337337

338-
it('initializes paypal buttons without shipping callbacks when appSwitch enabled', async () => {
339-
jest.spyOn(
340-
paymentIntegrationService.getState(),
341-
'getPaymentMethodOrThrow',
342-
).mockReturnValue({
343-
...paymentMethod,
344-
initializationData: {
345-
...paymentMethod.initializationData,
346-
isHostedCheckoutEnabled: true,
347-
isAppSwitchEnabled: true,
348-
},
349-
});
350-
351-
await strategy.initialize(initializationOptions);
352-
353-
expect(paypalSdk.Buttons).toHaveBeenCalledWith({
354-
fundingSource: paypalSdk.FUNDING.PAYLATER,
355-
style: {
356-
height: DefaultCheckoutButtonHeight,
357-
color: StyleButtonColor.silver,
358-
label: 'checkout',
359-
},
360-
createOrder: expect.any(Function),
361-
onApprove: expect.any(Function),
362-
onClick: expect.any(Function),
363-
});
364-
});
365-
366338
it('renders PayPal PayLater button if it is eligible', async () => {
367339
const paypalCommerceSdkRenderMock = jest.fn();
368340

@@ -454,6 +426,14 @@ describe('PayPalCommerceCreditCustomerStrategy', () => {
454426
});
455427

456428
describe('#onApprove button callback', () => {
429+
beforeEach(() => {
430+
const consignment = getConsignment();
431+
jest.spyOn(
432+
paymentIntegrationService.getState(),
433+
'getConsignmentsOrThrow',
434+
).mockReturnValue([consignment]);
435+
});
436+
457437
describe('default flow', () => {
458438
it('tokenizes payment on paypal approve', async () => {
459439
await strategy.initialize(initializationOptions);
@@ -467,6 +447,94 @@ describe('PayPalCommerceCreditCustomerStrategy', () => {
467447
approveDataOrderId,
468448
);
469449
});
450+
451+
it('call getConsignmentOrThrow when server side shipping callbacks is on', async () => {
452+
jest.spyOn(
453+
paymentIntegrationService.getState(),
454+
'getPaymentMethodOrThrow',
455+
).mockReturnValue({
456+
...paymentMethod,
457+
initializationData: {
458+
...paymentMethod.initializationData,
459+
isHostedCheckoutEnabled: true,
460+
isServerSideShippingCallbacksEnabled: true,
461+
},
462+
});
463+
await strategy.initialize(initializationOptions);
464+
465+
eventEmitter.emit('onApprove');
466+
await new Promise((resolve) => process.nextTick(resolve));
467+
468+
expect(
469+
paymentIntegrationService.getState().getConsignmentsOrThrow,
470+
).toHaveBeenCalled();
471+
});
472+
473+
it('selects shipping option when server side shipping callbacks is on', async () => {
474+
jest.spyOn(
475+
paymentIntegrationService.getState(),
476+
'getPaymentMethodOrThrow',
477+
).mockReturnValue({
478+
...paymentMethod,
479+
initializationData: {
480+
...paymentMethod.initializationData,
481+
isHostedCheckoutEnabled: true,
482+
isServerSideShippingCallbacksEnabled: true,
483+
},
484+
});
485+
486+
await strategy.initialize(initializationOptions);
487+
488+
eventEmitter.emit('onApprove');
489+
await new Promise((resolve) => process.nextTick(resolve));
490+
491+
expect(paymentIntegrationService.selectShippingOption).toHaveBeenCalled();
492+
});
493+
494+
it('updates address with merged object when server side shipping callbacks is on', async () => {
495+
const consignment = getConsignment();
496+
jest.spyOn(
497+
paymentIntegrationService.getState(),
498+
'getPaymentMethodOrThrow',
499+
).mockReturnValue({
500+
...paymentMethod,
501+
initializationData: {
502+
...paymentMethod.initializationData,
503+
isHostedCheckoutEnabled: true,
504+
isServerSideShippingCallbacksEnabled: true,
505+
},
506+
});
507+
508+
await strategy.initialize(initializationOptions);
509+
510+
const orderDetails = {
511+
firstName: 'Full',
512+
lastName: 'Name',
513+
email: 'john@doe.com',
514+
phone: '',
515+
company: '',
516+
address1: '2 E 61st St',
517+
address2: 'Apt.1',
518+
city: 'New York',
519+
countryCode: 'US',
520+
postalCode: '10065',
521+
stateOrProvince: '',
522+
stateOrProvinceCode: 'NY',
523+
customFields: [],
524+
};
525+
526+
const shippingAddress = {
527+
...consignment.shippingAddress,
528+
...orderDetails,
529+
};
530+
531+
eventEmitter.emit('onApprove');
532+
await new Promise((resolve) => process.nextTick(resolve));
533+
534+
expect(paymentIntegrationService.updateShippingAddress).toHaveBeenCalledWith(
535+
shippingAddress,
536+
);
537+
});
470538
});
471539

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

0 commit comments

Comments
 (0)