From d59f27f829b11eed3b17378967021bef1b3ea1e9 Mon Sep 17 00:00:00 2001 From: Andrii Date: Mon, 23 Sep 2024 16:08:43 +0300 Subject: [PATCH 1/3] feat(payment): PAYPAL-4698 test --- ...ative-methods-button-initialize-options.ts | 7 +++ ...lternative-methods-button-strategy.spec.ts | 36 ++++++-------- ...rce-alternative-methods-button-strategy.ts | 7 +-- ...mmerce-credit-button-initialize-options.ts | 7 +++ ...al-commerce-credit-button-strategy.spec.ts | 48 +++++++++---------- .../paypal-commerce-credit-button-strategy.ts | 5 +- ...ommerce-venmo-button-initialize-options.ts | 8 ++++ ...pal-commerce-venmo-button-strategy.spec.ts | 37 ++++++-------- .../paypal-commerce-venmo-button-strategy.ts | 6 +-- ...ypal-commerce-button-initialize-options.ts | 7 +++ .../paypal-commerce-button-strategy.spec.ts | 33 ++++++++----- .../paypal-commerce-button-strategy.ts | 7 +-- 12 files changed, 117 insertions(+), 91 deletions(-) diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-initialize-options.ts b/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-initialize-options.ts index 914c6f37ca..30408fdc56 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-initialize-options.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-initialize-options.ts @@ -20,6 +20,13 @@ export default interface PayPalCommerceAlternativeMethodsButtonOptions { * A set of styling options for the checkout button. */ style?: PayPalButtonStyleOptions; + + /** + * + * A callback that gets called when PayPal SDK restricts to render PayPal component. + * + */ + onEligibilityFailure?(): void; } export interface WithPayPalCommerceAlternativeMethodsButtonInitializeOptions { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.spec.ts index 1fceec77c3..da041e3303 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.spec.ts @@ -70,6 +70,7 @@ describe('PayPalCommerceAlternativeMethodsButtonStrategy', () => { style: { height: 45, }, + onEligibilityFailure: jest.fn(), }; const initializationOptions: CheckoutButtonInitializeOptions = { @@ -104,14 +105,16 @@ describe('PayPalCommerceAlternativeMethodsButtonStrategy', () => { paymentMethod, ); - jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue(paypalSdk); + jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue( + Promise.resolve(paypalSdk), + ); jest.spyOn(paypalCommerceIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue( paypalSdk, ); jest.spyOn(paypalCommerceIntegrationService, 'createBuyNowCartOrThrow').mockReturnValue( - buyNowCart, + Promise.resolve(buyNowCart), ); - jest.spyOn(paypalCommerceIntegrationService, 'createOrder').mockReturnValue(undefined); + jest.spyOn(paypalCommerceIntegrationService, 'createOrder'); jest.spyOn(paypalCommerceIntegrationService, 'tokenizePayment').mockImplementation( jest.fn(), ); @@ -179,6 +182,7 @@ describe('PayPalCommerceAlternativeMethodsButtonStrategy', () => { return { isEligible: jest.fn(() => true), render: jest.fn(), + close: jest.fn(), }; }, ); @@ -372,6 +376,7 @@ describe('PayPalCommerceAlternativeMethodsButtonStrategy', () => { jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => true), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); @@ -379,33 +384,20 @@ describe('PayPalCommerceAlternativeMethodsButtonStrategy', () => { expect(paypalCommerceSdkRenderMock).toHaveBeenCalled(); }); - it('does not render PayPal APM button if it is not eligible', async () => { + it('calls onEligibilityFailure callback when PayPal APM button is not eligible', async () => { const paypalCommerceSdkRenderMock = jest.fn(); jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => false), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); + expect(paypalCommerceAlternativeMethodsOptions.onEligibilityFailure).toHaveBeenCalled(); expect(paypalCommerceSdkRenderMock).not.toHaveBeenCalled(); }); - - it('removes PayPal APM button container if the button has not rendered', async () => { - const paypalCommerceSdkRenderMock = jest.fn(); - - jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ - isEligible: jest.fn(() => false), - render: paypalCommerceSdkRenderMock, - })); - - await strategy.initialize(initializationOptions); - - expect(paypalCommerceIntegrationService.removeElement).toHaveBeenCalledWith( - defaultButtonContainerId, - ); - }); }); describe('#createOrder', () => { @@ -424,8 +416,10 @@ describe('PayPalCommerceAlternativeMethodsButtonStrategy', () => { describe('#handleClick', () => { beforeEach(() => { - jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue(buyNowCart); - jest.spyOn(paymentIntegrationService, 'loadCheckout').mockReturnValue(true); + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.resolve(buyNowCart), + ); + jest.spyOn(paymentIntegrationService, 'loadCheckout'); }); it('creates buy now cart on button click', async () => { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.ts index fc2cc9cfd6..2c1fe8c865 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-alternative-methods/paypal-commerce-alternative-methods-button-strategy.ts @@ -103,7 +103,8 @@ export default class PayPalCommerceAlternativeMethodsButtonStrategy methodId: string, paypalcommercealternativemethods: PayPalCommerceAlternativeMethodsButtonOptions, ): void { - const { apm, buyNowInitializeOptions, style } = paypalcommercealternativemethods; + const { apm, buyNowInitializeOptions, style, onEligibilityFailure } = + paypalcommercealternativemethods; const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow(); const isAvailableFundingSource = Object.values(paypalSdk.FUNDING).includes(apm); @@ -139,8 +140,8 @@ export default class PayPalCommerceAlternativeMethodsButtonStrategy if (paypalButtonRender.isEligible()) { paypalButtonRender.render(`#${containerId}`); - } else { - this.paypalCommerceIntegrationService.removeElement(containerId); + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + onEligibilityFailure(); } } diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-initialize-options.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-initialize-options.ts index 93a9c7c8cb..6dc9aef90c 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-initialize-options.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-initialize-options.ts @@ -25,6 +25,13 @@ export default interface PayPalCommerceCreditButtonInitializeOptions { * A callback that gets called when payment complete on paypal side. */ onComplete?(): void; + + /** + * + * A callback that gets called when PayPal SDK restricts to render PayPal component. + * + */ + onEligibilityFailure?(): void; } export interface WithPayPalCommerceCreditButtonInitializeOptions { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.spec.ts index 1437ee3d97..406922406b 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.spec.ts @@ -86,6 +86,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { height: 45, }, onComplete: jest.fn(), + onEligibilityFailure: jest.fn(), }; const initializationOptions: CheckoutButtonInitializeOptions = { @@ -159,12 +160,14 @@ describe('PayPalCommerceCreditButtonStrategy', () => { ); jest.spyOn(paymentIntegrationService, 'selectShippingOption').mockImplementation(jest.fn()); - jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue(paypalSdk); + jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue( + Promise.resolve(paypalSdk), + ); jest.spyOn(paypalCommerceIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue( paypalSdk, ); jest.spyOn(paypalCommerceIntegrationService, 'createBuyNowCartOrThrow').mockReturnValue( - buyNowCart, + Promise.resolve(buyNowCart), ); jest.spyOn(paypalCommerceIntegrationService, 'createOrder').mockImplementation(jest.fn()); jest.spyOn(paypalCommerceIntegrationService, 'updateOrder').mockImplementation(jest.fn()); @@ -172,7 +175,6 @@ describe('PayPalCommerceCreditButtonStrategy', () => { jest.fn(), ); jest.spyOn(paypalCommerceIntegrationService, 'submitPayment').mockImplementation(jest.fn()); - jest.spyOn(paypalCommerceIntegrationService, 'removeElement').mockImplementation(jest.fn()); jest.spyOn( paypalCommerceIntegrationService, 'getBillingAddressFromOrderDetails', @@ -184,12 +186,13 @@ describe('PayPalCommerceCreditButtonStrategy', () => { jest.spyOn(paypalCommerceIntegrationService, 'getShippingOptionOrThrow').mockReturnValue( getShippingOption(), ); - jest.spyOn(paypalCommerceSdk, 'getPayPalMessages').mockImplementation( - () => payPalMessagesSdk, + jest.spyOn(paypalCommerceSdk, 'getPayPalMessages').mockImplementation(() => + Promise.resolve(payPalMessagesSdk), ); jest.spyOn(payPalMessagesSdk, 'Messages').mockImplementation(() => ({ render: paypalCommerceSdkRenderMock, })); + jest.spyOn(paymentIntegrationService.getState(), 'getStoreConfig').mockReturnValue({ checkoutSettings: { features: { @@ -288,6 +291,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { return { isEligible: jest.fn(() => true), render: jest.fn(), + close: jest.fn(), }; }, ); @@ -433,6 +437,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { isEligible: jest.fn(() => { return options.fundingSource === paypalSdk.FUNDING.CREDIT; }), + close: jest.fn(), }; }, ); @@ -525,6 +530,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => true), render: renderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); @@ -532,33 +538,20 @@ describe('PayPalCommerceCreditButtonStrategy', () => { expect(renderMock).toHaveBeenCalled(); }); - it('does not render PayPal button if it is not eligible', async () => { + it('calls onEligibilityFailure callback when PayPal button is not eligible', async () => { const renderMock = jest.fn(); jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => false), render: renderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); + expect(paypalCommerceCreditOptions.onEligibilityFailure).toHaveBeenCalled(); expect(renderMock).not.toHaveBeenCalled(); }); - - it('removes PayPal button container if the button is not eligible', async () => { - const renderMock = jest.fn(); - - jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ - isEligible: jest.fn(() => false), - render: renderMock, - })); - - await strategy.initialize(initializationOptions); - - expect(paypalCommerceIntegrationService.removeElement).toHaveBeenCalledWith( - defaultButtonContainerId, - ); - }); }); describe('#createOrder', () => { @@ -577,8 +570,10 @@ describe('PayPalCommerceCreditButtonStrategy', () => { describe('#handleClick', () => { beforeEach(() => { - jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue(buyNowCart); - jest.spyOn(paymentIntegrationService, 'loadCheckout').mockReturnValue(true); + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.resolve(buyNowCart), + ); + jest.spyOn(paymentIntegrationService, 'loadCheckout'); }); it('creates buy now cart on button click', async () => { @@ -626,7 +621,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { { orderID: paypalOrderId }, { order: { - get: jest.fn(() => paypalOrderDetails), + get: () => Promise.resolve(paypalOrderDetails), }, }, ); @@ -636,6 +631,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { return { render: jest.fn(), isEligible: jest.fn(() => true), + close: jest.fn(), }; }, ); @@ -655,7 +651,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { }); it('takes order details data from paypal', async () => { - const getOrderActionMock = jest.fn(() => paypalOrderDetails); + const getOrderActionMock = jest.fn(); jest.spyOn(paypalSdk, 'Buttons').mockImplementation( (options: PayPalCommerceButtonsOptions) => { @@ -675,6 +671,7 @@ describe('PayPalCommerceCreditButtonStrategy', () => { return { render: jest.fn(), isEligible: jest.fn(() => true), + close: jest.fn(), }; }, ); @@ -686,7 +683,6 @@ describe('PayPalCommerceCreditButtonStrategy', () => { await new Promise((resolve) => process.nextTick(resolve)); expect(getOrderActionMock).toHaveBeenCalled(); - expect(getOrderActionMock).toHaveReturnedWith(paypalOrderDetails); }); it('updates billing address with valid customers data', async () => { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts index 2c8f5519d9..12952b1203 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts @@ -121,7 +121,8 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto methodId: string, paypalcommercecredit: PayPalCommerceCreditButtonInitializeOptions, ): void { - const { buyNowInitializeOptions, style, onComplete } = paypalcommercecredit; + const { buyNowInitializeOptions, style, onComplete, onEligibilityFailure } = + paypalcommercecredit; const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow(); const state = this.paymentIntegrationService.getState(); @@ -182,6 +183,8 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto if (paypalButton.isEligible()) { paypalButton.render(`#${containerId}`); hasRenderedSmartButton = true; + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + onEligibilityFailure(); } } }); diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts index 5f269dd03e..ed8b6b0ca2 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts @@ -15,6 +15,14 @@ export default interface PayPalCommerceVenmoButtonInitializeOptions { * The options that required to initialize Buy Now functionality. */ buyNowInitializeOptions?: PayPalBuyNowInitializeOptions; + + /** + * + * A callback that gets called when PayPal SDK restricts to render PayPal component. + * + */ + onEligibilityFailure?(): void; + } export interface WithPayPalCommerceVenmoButtonInitializeOptions { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.spec.ts index db2846f55c..8707e96660 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.spec.ts @@ -67,6 +67,7 @@ describe('PayPalCommerceVenmoButtonStrategy', () => { style: { height: 45, }, + onEligibilityFailure: jest.fn(), }; const initializationOptions: CheckoutButtonInitializeOptions = { @@ -100,18 +101,19 @@ describe('PayPalCommerceVenmoButtonStrategy', () => { paymentMethod, ); - jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue(paypalSdk); + jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue( + Promise.resolve(paypalSdk), + ); jest.spyOn(paypalCommerceIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue( paypalSdk, ); jest.spyOn(paypalCommerceIntegrationService, 'createBuyNowCartOrThrow').mockReturnValue( - buyNowCart, + Promise.resolve(buyNowCart), ); - jest.spyOn(paypalCommerceIntegrationService, 'createOrder').mockReturnValue(undefined); + jest.spyOn(paypalCommerceIntegrationService, 'createOrder'); jest.spyOn(paypalCommerceIntegrationService, 'tokenizePayment').mockImplementation( jest.fn(), ); - jest.spyOn(paypalCommerceIntegrationService, 'removeElement').mockImplementation(jest.fn()); jest.spyOn(paypalSdk, 'Buttons').mockImplementation( (options: PayPalCommerceButtonsOptions) => { @@ -175,6 +177,7 @@ describe('PayPalCommerceVenmoButtonStrategy', () => { return { isEligible: jest.fn(() => true), render: jest.fn(), + close: jest.fn(), }; }, ); @@ -357,6 +360,7 @@ describe('PayPalCommerceVenmoButtonStrategy', () => { jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => true), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); @@ -364,33 +368,20 @@ describe('PayPalCommerceVenmoButtonStrategy', () => { expect(paypalCommerceSdkRenderMock).toHaveBeenCalled(); }); - it('does not render PayPal Venmo button if it is not eligible', async () => { + it('calls onEligibilityFailure when PayPal Venmo button if it is not eligible', async () => { const paypalCommerceSdkRenderMock = jest.fn(); jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => false), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); + expect(paypalCommerceVenmoOptions.onEligibilityFailure).toHaveBeenCalled(); expect(paypalCommerceSdkRenderMock).not.toHaveBeenCalled(); }); - - it('removes Venmo PayPal button container if the button has not rendered', async () => { - const paypalCommerceSdkRenderMock = jest.fn(); - - jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ - isEligible: jest.fn(() => false), - render: paypalCommerceSdkRenderMock, - })); - - await strategy.initialize(initializationOptions); - - expect(paypalCommerceIntegrationService.removeElement).toHaveBeenCalledWith( - defaultButtonContainerId, - ); - }); }); describe('#createOrder', () => { @@ -409,8 +400,10 @@ describe('PayPalCommerceVenmoButtonStrategy', () => { describe('#handleClick', () => { beforeEach(() => { - jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue(buyNowCart); - jest.spyOn(paymentIntegrationService, 'loadCheckout').mockReturnValue(true); + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.resolve(buyNowCart), + ); + jest.spyOn(paymentIntegrationService, 'loadCheckout'); }); it('creates buy now cart on button click', async () => { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.ts index f38b1b6c64..9ad204d2ed 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-strategy.ts @@ -93,7 +93,7 @@ export default class PayPalCommerceVenmoButtonStrategy implements CheckoutButton methodId: string, paypalcommercevenmo: PayPalCommerceVenmoButtonInitializeOptions, ): void { - const { buyNowInitializeOptions, style } = paypalcommercevenmo; + const { buyNowInitializeOptions, style, onEligibilityFailure } = paypalcommercevenmo; const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow(); const fundingSource = paypalSdk.FUNDING.VENMO; @@ -121,8 +121,8 @@ export default class PayPalCommerceVenmoButtonStrategy implements CheckoutButton if (paypalButtonRender.isEligible()) { paypalButtonRender.render(`#${containerId}`); - } else { - this.paypalCommerceIntegrationService.removeElement(containerId); + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + onEligibilityFailure(); } } diff --git a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-initialize-options.ts b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-initialize-options.ts index 5808dab25a..e8ec63a51f 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-initialize-options.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-initialize-options.ts @@ -26,6 +26,13 @@ export default interface PayPalCommerceButtonInitializeOptions { * A callback that gets called when payment complete on paypal side. */ onComplete?(): void; + + /** + * + * A callback that gets called when PayPal SDK restricts to render PayPal component. + * + */ + onEligibilityFailure?(): void; } export interface WithPayPalCommerceButtonInitializeOptions { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts index 1495ad8d69..75a10ee570 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts @@ -60,6 +60,7 @@ describe('PayPalCommerceButtonStrategy', () => { height: 45, }, onComplete: jest.fn(), + onEligibilityFailure: jest.fn(), }; const buyNowInitializationOptions: CheckoutButtonInitializeOptions = { @@ -73,6 +74,7 @@ describe('PayPalCommerceButtonStrategy', () => { height: 45, }, onComplete: jest.fn(), + onEligibilityFailure: jest.fn(), }; const initializationOptions: CheckoutButtonInitializeOptions = { @@ -137,12 +139,14 @@ describe('PayPalCommerceButtonStrategy', () => { ); jest.spyOn(paymentIntegrationService, 'selectShippingOption').mockImplementation(jest.fn()); - jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue(paypalSdk); + jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue( + Promise.resolve(paypalSdk), + ); jest.spyOn(paypalCommerceIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue( paypalSdk, ); jest.spyOn(paypalCommerceIntegrationService, 'createBuyNowCartOrThrow').mockReturnValue( - buyNowCart, + Promise.resolve(buyNowCart), ); jest.spyOn(paypalCommerceIntegrationService, 'createOrder').mockImplementation(jest.fn()); jest.spyOn(paypalCommerceIntegrationService, 'updateOrder').mockImplementation(jest.fn()); @@ -150,7 +154,6 @@ describe('PayPalCommerceButtonStrategy', () => { jest.fn(), ); jest.spyOn(paypalCommerceIntegrationService, 'submitPayment').mockImplementation(jest.fn()); - jest.spyOn(paypalCommerceIntegrationService, 'removeElement').mockImplementation(jest.fn()); jest.spyOn( paypalCommerceIntegrationService, 'getBillingAddressFromOrderDetails', @@ -165,6 +168,7 @@ describe('PayPalCommerceButtonStrategy', () => { jest.spyOn(paymentIntegrationService.getState(), 'getStoreConfig').mockReturnValue({ checkoutSettings: { features: { + // TODO: remove this experiment 'PAYPAL-4387.paypal_shipping_callbacks': true, }, }, @@ -260,6 +264,7 @@ describe('PayPalCommerceButtonStrategy', () => { return { isEligible: jest.fn(() => true), render: jest.fn(), + close: jest.fn(), }; }, ); @@ -475,6 +480,7 @@ describe('PayPalCommerceButtonStrategy', () => { jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => true), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); @@ -488,6 +494,7 @@ describe('PayPalCommerceButtonStrategy', () => { jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => false), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); @@ -495,19 +502,18 @@ describe('PayPalCommerceButtonStrategy', () => { expect(paypalCommerceSdkRenderMock).not.toHaveBeenCalled(); }); - it('removes PayPal button container if the button is not eligible', async () => { + it('calls onEligibilityFailure callback when the PayPal button is not eligible', async () => { const paypalCommerceSdkRenderMock = jest.fn(); jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({ isEligible: jest.fn(() => false), render: paypalCommerceSdkRenderMock, + close: jest.fn(), })); await strategy.initialize(initializationOptions); - expect(paypalCommerceIntegrationService.removeElement).toHaveBeenCalledWith( - defaultButtonContainerId, - ); + expect(paypalCommerceOptions.onEligibilityFailure).toHaveBeenCalled(); }); }); @@ -527,8 +533,10 @@ describe('PayPalCommerceButtonStrategy', () => { describe('#handleClick', () => { beforeEach(() => { - jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue(buyNowCart); - jest.spyOn(paymentIntegrationService, 'loadCheckout').mockReturnValue(true); + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.resolve(buyNowCart), + ); + jest.spyOn(paymentIntegrationService, 'loadCheckout'); }); it('creates buy now cart on button click', async () => { @@ -576,7 +584,7 @@ describe('PayPalCommerceButtonStrategy', () => { { orderID: paypalOrderId }, { order: { - get: jest.fn(() => paypalOrderDetails), + get: () => Promise.resolve(paypalOrderDetails), }, }, ); @@ -586,6 +594,7 @@ describe('PayPalCommerceButtonStrategy', () => { return { render: jest.fn(), isEligible: jest.fn(() => true), + close: jest.fn(), }; }, ); @@ -605,7 +614,7 @@ describe('PayPalCommerceButtonStrategy', () => { }); it('takes order details data from paypal', async () => { - const getOrderActionMock = jest.fn(() => paypalOrderDetails); + const getOrderActionMock = jest.fn(); jest.spyOn(paypalSdk, 'Buttons').mockImplementation( (options: PayPalCommerceButtonsOptions) => { @@ -625,6 +634,7 @@ describe('PayPalCommerceButtonStrategy', () => { return { render: jest.fn(), isEligible: jest.fn(() => true), + close: jest.fn(), }; }, ); @@ -636,7 +646,6 @@ describe('PayPalCommerceButtonStrategy', () => { await new Promise((resolve) => process.nextTick(resolve)); expect(getOrderActionMock).toHaveBeenCalled(); - expect(getOrderActionMock).toHaveReturnedWith(paypalOrderDetails); }); it('updates billing address with valid customers data', async () => { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts index 4aab4d0222..0acaf171c3 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts @@ -96,7 +96,8 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat methodId: string, paypalcommerce: PayPalCommerceButtonInitializeOptions, ): void { - const { buyNowInitializeOptions, style, onComplete } = paypalcommerce; + + const { buyNowInitializeOptions, style, onComplete, onEligibilityFailure } = paypalcommerce; const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow(); const state = this.paymentIntegrationService.getState(); @@ -150,8 +151,8 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat if (paypalButton.isEligible()) { paypalButton.render(`#${containerId}`); - } else { - this.paypalCommerceIntegrationService.removeElement(containerId); + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + onEligibilityFailure(); } } From 0edc72de0606feb926a65c1deeb6f45f887fafad Mon Sep 17 00:00:00 2001 From: Andrii Date: Mon, 23 Sep 2024 16:16:59 +0300 Subject: [PATCH 2/3] feat(payment): PAYPAL-4698 test --- .../braintree-paypal-button-options.ts | 7 +++ .../braintree-paypal-button-strategy.spec.ts | 28 ++++++++++++ .../braintree-paypal-button-strategy.ts | 5 ++- .../braintree-paypal-credit-button-options.ts | 7 +++ ...tree-paypal-credit-button-strategy.spec.ts | 44 +++++++++++++++++-- ...braintree-paypal-credit-button-strategy.ts | 12 ++++- 6 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts index fb0185b4bd..4b9cc07ab6 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts @@ -51,6 +51,13 @@ export interface BraintreePaypalButtonInitializeOptions { */ onError?(error: BraintreeError | StandardError): void; + /** + * + * A callback that gets called when Braintree SDK restricts to render PayPal component. + * + */ + onEligibilityFailure?(): void; + /** * The option that used to initialize a PayPal script with provided currency code. */ diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts index 34f86523dc..8e3cee804f 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts @@ -76,6 +76,7 @@ describe('BraintreePaypalButtonStrategy', () => { onAuthorizeError: jest.fn(), onPaymentError: jest.fn(), onError: jest.fn(), + onEligibilityFailure: jest.fn(), }; const initializationOptions: CheckoutButtonInitializeOptions = { @@ -427,6 +428,33 @@ describe('BraintreePaypalButtonStrategy', () => { }); }); + it('does not render PayPal checkout button and calls onEligibilityFailure callback', async () => { + const renderMock = jest.fn(); + + jest.spyOn(paypalSdkMock, 'Buttons').mockImplementationOnce(() => { + return { + isEligible: jest.fn(() => false), + render: renderMock, + }; + }); + + await strategy.initialize(initializationOptions); + + expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ + createOrder: expect.any(Function), + env: 'sandbox', + fundingSource: paypalSdkMock.FUNDING.PAYPAL, + onApprove: expect.any(Function), + style: { + shape: 'rect', + height: 45, + }, + }); + + expect(braintreePaypalOptions.onEligibilityFailure).toHaveBeenCalled(); + expect(renderMock).not.toHaveBeenCalled(); + }); + it('renders PayPal checkout button in production environment if payment method is in test mode', async () => { paymentMethodMock.config.testMode = false; diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts index 6ab1cafb7d..5adcbaa46c 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts @@ -153,7 +153,8 @@ export default class BraintreePaypalButtonStrategy implements CheckoutButtonStra methodId: string, testMode: boolean, ): void { - const { style, shouldProcessPayment, onAuthorizeError } = braintreepaypal; + const { style, shouldProcessPayment, onAuthorizeError, onEligibilityFailure } = + braintreepaypal; const { paypal } = this._window; const fundingSource = paypal?.FUNDING.PAYPAL; @@ -179,6 +180,8 @@ export default class BraintreePaypalButtonStrategy implements CheckoutButtonStra if (paypalButtonRender.isEligible()) { paypalButtonRender.render(`#${containerId}`); + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + onEligibilityFailure(); } } else { this._removeElement(containerId); diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-options.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-options.ts index 8cf2f6e0a1..d1b341156d 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-options.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-options.ts @@ -46,6 +46,13 @@ export interface BraintreePaypalCreditButtonInitializeOptions { */ onError?(error: BraintreeError | StandardError): void; + /** + * + * A callback that gets called when Braintree SDK restricts to render PayPal component. + * + */ + onEligibilityFailure?(): void; + /** * The option that used to initialize a PayPal script with provided currency code. */ diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.spec.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.spec.ts index 23278e1c7e..62d7acdd77 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.spec.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.spec.ts @@ -76,6 +76,7 @@ describe('BraintreePaypalCreditButtonStrategy', () => { onAuthorizeError: jest.fn(), onPaymentError: jest.fn(), onError: jest.fn(), + onEligibilityFailure: jest.fn(), }; const initializationOptions: CheckoutButtonInitializeOptions = { @@ -418,12 +419,10 @@ describe('BraintreePaypalCreditButtonStrategy', () => { }); it('renders braintree credit button if paylater is not eligible', async () => { - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore jest.spyOn(paypalSdkMock, 'Buttons').mockImplementationOnce(() => { return { isEligible: jest.fn(() => false), + render: jest.fn(), }; }); @@ -455,6 +454,45 @@ describe('BraintreePaypalCreditButtonStrategy', () => { }); }); + it('does not render PayPal checkout button and calls onEligibilityFailure callback', async () => { + const renderMock = jest.fn(); + + jest.spyOn(paypalSdkMock, 'Buttons').mockImplementation(() => { + return { + isEligible: jest.fn(() => false), + render: renderMock, + }; + }); + + await strategy.initialize(initializationOptions); + + expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ + createOrder: expect.any(Function), + env: 'sandbox', + fundingSource: paypalSdkMock.FUNDING.PAYLATER, + onApprove: expect.any(Function), + style: { + shape: 'rect', + height: 45, + }, + }); + + expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ + createOrder: expect.any(Function), + env: 'sandbox', + fundingSource: paypalSdkMock.FUNDING.CREDIT, + onApprove: expect.any(Function), + style: { + shape: 'rect', + height: 45, + label: 'credit', + }, + }); + + expect(braintreePaypalCreditOptions.onEligibilityFailure).toHaveBeenCalled(); + expect(renderMock).not.toHaveBeenCalled(); + }); + it('renders braintree checkout button in production environment if payment method is in test mode', async () => { paymentMethodMock.config.testMode = false; diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.ts index b04c764a99..eaea6bd592 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-credit-button-strategy.ts @@ -134,7 +134,8 @@ export default class BraintreePaypalCreditButtonStrategy implements CheckoutButt methodId: string, testMode: boolean, ): void { - const { style, shouldProcessPayment, onAuthorizeError } = braintreepaypalcredit; + const { style, shouldProcessPayment, onAuthorizeError, onEligibilityFailure } = + braintreepaypalcredit; const { paypal } = this._window; let hasRenderedSmartButton = false; @@ -173,6 +174,15 @@ export default class BraintreePaypalCreditButtonStrategy implements CheckoutButt if (paypalButtonRender.isEligible()) { paypalButtonRender.render(`#${containerId}`); hasRenderedSmartButton = true; + } else if ( + paypal.FUNDING.CREDIT && + onEligibilityFailure && + typeof onEligibilityFailure === 'function' + ) { + // the condition is related to paypal.FUNDING.CREDIT because when paypal.FUNDING.PAYLATER is not eligible then + // CREDIT button should be configured and triggered to render with eligibility check + // and if it is not eligible, then onEligibilityFailure callback should be called + onEligibilityFailure(); } } }); From 73e91949ef0871d3af0df7f9ad8b31be5870a38a Mon Sep 17 00:00:00 2001 From: Andrii Date: Mon, 23 Sep 2024 17:13:03 +0300 Subject: [PATCH 3/3] feat(payment): PAYPAL-4698 test --- .../paypal-commerce-credit-button-strategy.ts | 2 +- .../paypal-commerce-venmo-button-initialize-options.ts | 1 - .../src/paypal-commerce/paypal-commerce-button-strategy.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts index 12952b1203..4c618c073f 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-button-strategy.ts @@ -183,7 +183,7 @@ export default class PayPalCommerceCreditButtonStrategy implements CheckoutButto if (paypalButton.isEligible()) { paypalButton.render(`#${containerId}`); hasRenderedSmartButton = true; - } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { onEligibilityFailure(); } } diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts index ed8b6b0ca2..8eb1857523 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-venmo/paypal-commerce-venmo-button-initialize-options.ts @@ -22,7 +22,6 @@ export default interface PayPalCommerceVenmoButtonInitializeOptions { * */ onEligibilityFailure?(): void; - } export interface WithPayPalCommerceVenmoButtonInitializeOptions { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts index 0acaf171c3..c7b9c89959 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts @@ -96,7 +96,6 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat methodId: string, paypalcommerce: PayPalCommerceButtonInitializeOptions, ): void { - const { buyNowInitializeOptions, style, onComplete, onEligibilityFailure } = paypalcommerce; const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow();