Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export interface BigCommercePaymentsInitializationData {
shouldRenderFields?: boolean;
shouldRunAcceleratedCheckout?: boolean;
paymentButtonStyles?: Record<string, PayPalButtonStyleOptions>;
isAppSwitchEnabled?: boolean;
}

/**
Expand Down Expand Up @@ -357,6 +358,8 @@ export interface BigCommercePaymentsButtons {
render(id: string): void;
close(): void;
isEligible(): boolean;
hasReturned?(): boolean;
resume?(): void;
}

export interface BigCommercePaymentsButtonsOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,57 @@ describe('BigCommercePaymentsButtonStrategy', () => {
expect(bigCommercePaymentsSdkRenderMock).toHaveBeenCalled();
});

it('render button with appSwitch flag', async () => {
jest.spyOn(
paymentIntegrationService.getState(),
'getPaymentMethodOrThrow',
).mockReturnValue({
...paymentMethod,
initializationData: {
...paymentMethod.initializationData,
isAppSwitchEnabled: true,
},
});

await strategy.initialize(initializationOptions);

expect(paypalSdk.Buttons).toHaveBeenCalledWith({
appSwitchWhenAvailable: true,
fundingSource: paypalSdk.FUNDING.PAYPAL,
style: bigCommercePaymentsOptions.style,
createOrder: expect.any(Function),
onApprove: expect.any(Function),
});
});

it('calls resume when appSwitch enabled and returned from app', async () => {
jest.spyOn(
paymentIntegrationService.getState(),
'getPaymentMethodOrThrow',
).mockReturnValue({
...paymentMethod,
initializationData: {
...paymentMethod.initializationData,
isAppSwitchEnabled: true,
},
});

const resumeMock = jest.fn();
const bigCommercePaymentsSdkRenderMock = jest.fn();

jest.spyOn(paypalSdk, 'Buttons').mockImplementation(() => ({
close: jest.fn(),
isEligible: jest.fn(() => true),
render: bigCommercePaymentsSdkRenderMock,
hasReturned: jest.fn(() => true),
resume: resumeMock,
}));

await strategy.initialize(initializationOptions);

expect(resumeMock).toHaveBeenCalled();
});

it('does not render PayPal button if it is not eligible', async () => {
const bigCommercePaymentsSdkRenderMock = jest.fn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
false,
);

this.renderButton(containerId, methodId, bigcommerce_payments);
this.renderButton(containerId, methodId, bigcommerce_payments, isBuyNowFlow);
}

deinitialize(): Promise<void> {
Expand All @@ -99,6 +99,7 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
containerId: string,
methodId: string,
bigcommerce_payments: BigCommercePaymentsButtonInitializeOptions,
isBuyNowFlow?: boolean,
): void {
const { buyNowInitializeOptions, style, onComplete, onEligibilityFailure } =
bigcommerce_payments;
Comment on lines 99 to 105
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The renderButton call at line 96 omits isBuyNowFlow, incorrectly enabling appSwitchWhenAvailable in buy-now flows.
Severity: CRITICAL | Confidence: 0.95

🔍 Detailed Analysis

The renderButton method is invoked at line 96 without providing the isBuyNowFlow parameter. This results in isBuyNowFlow being undefined within the method's scope. Consequently, the condition !isBuyNowFlow evaluates to true, causing appSwitchWhenAvailable: true to be incorrectly included in the defaultCallbacks object (lines 115-118) when this.isPaypalCommerceAppSwitchEnabled(methodId) is true, even if the current flow is a buy-now flow. This behavior deviates from the intended logic of enabling app switch only when not in a buy-now flow.

💡 Suggested Fix

Pass the isBuyNowFlow parameter to the renderButton call at line 96. This ensures isBuyNowFlow is correctly evaluated within the method, preventing appSwitchWhenAvailable from being enabled in buy-now flows.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
packages/bigcommerce-payments-integration/src/bigcommerce-payments/bigcommerce-payments-button-strategy.ts#L99-L105

Potential issue: The `renderButton` method is invoked at line 96 without providing the
`isBuyNowFlow` parameter. This results in `isBuyNowFlow` being `undefined` within the
method's scope. Consequently, the condition `!isBuyNowFlow` evaluates to `true`, causing
`appSwitchWhenAvailable: true` to be incorrectly included in the `defaultCallbacks`
object (lines 115-118) when `this.isPaypalCommerceAppSwitchEnabled(methodId)` is true,
even if the current flow is a buy-now flow. This behavior deviates from the intended
logic of enabling app switch only when not in a buy-now flow.

Did we get this right? 👍 / 👎 to inform future reviews.

Expand All @@ -107,9 +108,14 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
const state = this.paymentIntegrationService.getState();
const paymentMethod =
state.getPaymentMethodOrThrow<BigCommercePaymentsInitializationData>(methodId);
const { isHostedCheckoutEnabled } = paymentMethod.initializationData || {};
const { isHostedCheckoutEnabled, isAppSwitchEnabled } =
paymentMethod.initializationData || {};

const defaultCallbacks = {
...(!isBuyNowFlow &&
this.isPaypalCommerceAppSwitchEnabled(methodId) && {
appSwitchWhenAvailable: true,
}),
createOrder: () =>
this.bigCommercePaymentsIntegrationService.createOrder('bigcommerce_payments'),
onApprove: ({ orderID }: ApproveCallbackPayload) =>
Expand All @@ -122,10 +128,12 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
};

const hostedCheckoutCallbacks = {
onShippingAddressChange: (data: ShippingAddressChangeCallbackPayload) =>
this.onShippingAddressChange(data),
onShippingOptionsChange: (data: ShippingOptionChangeCallbackPayload) =>
this.onShippingOptionsChange(data),
...(!isAppSwitchEnabled && {
onShippingAddressChange: (data: ShippingAddressChangeCallbackPayload) =>
this.onShippingAddressChange(data),
onShippingOptionsChange: (data: ShippingOptionChangeCallbackPayload) =>
this.onShippingOptionsChange(data),
}),
onApprove: (data: ApproveCallbackPayload, actions: ApproveCallbackActions) =>
this.onHostedCheckoutApprove(data, actions, methodId, onComplete),
};
Expand All @@ -141,7 +149,11 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
const paypalButton = paypalSdk.Buttons(buttonRenderOptions);

if (paypalButton.isEligible()) {
paypalButton.render(`#${containerId}`);
if (paypalButton.hasReturned?.() && this.isPaypalCommerceAppSwitchEnabled(methodId)) {
paypalButton.resume?.();
} else {
paypalButton.render(`#${containerId}`);
}
} else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') {
onEligibilityFailure();
} else {
Expand Down Expand Up @@ -259,4 +271,17 @@ export default class BigCommercePaymentsButtonStrategy implements CheckoutButton
throw error;
}
}

/**
*
* PayPal AppSwitch enabling handling
*
*/
private isPaypalCommerceAppSwitchEnabled(methodId: string): boolean {
const state = this.paymentIntegrationService.getState();
const paymentMethod =
state.getPaymentMethodOrThrow<BigCommercePaymentsInitializationData>(methodId);

return paymentMethod.initializationData?.isAppSwitchEnabled ?? false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default function getBigCommercePaymentsPaymentMethod(): PaymentMethod {
},
},
],
isAppSwitchEnabled: false,
},
skipRedirectConfirmationAlert: false,
type: 'PAYMENT_TYPE_API',
Expand Down