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
4 changes: 4 additions & 0 deletions changelog/chore-utils-modularization
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: dev

chore: move ECE utilities into separate files
40 changes: 13 additions & 27 deletions client/express-checkout/event-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
getErrorMessageFromNotice,
getExpressCheckoutData,
updateShippingAddressUI,
createPaymentCredential,
shouldUseConfirmationTokens,
} from './utils';
import {
trackExpressCheckoutButtonClick,
Expand Down Expand Up @@ -141,33 +143,17 @@ export const onConfirmHandler = async (
return abortPayment( submitError.message );
}

const useConfirmationToken =
getExpressCheckoutData( 'flags' )?.isEceUsingConfirmationTokens ?? true;
const useConfirmationTokens = shouldUseConfirmationTokens();

let paymentCredential;
if ( useConfirmationToken ) {
Copy link
Contributor Author

@frosso frosso Feb 24, 2026

Choose a reason for hiding this comment

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

mostly extracted to createPaymentCredential for reuse in the future. Tested in client/express-checkout/utils/__tests__/payment-credentials.test.js

const {
confirmationToken,
error,
} = await stripe.createConfirmationToken( {
elements,
} );

if ( error ) {
return abortPayment( error.message );
}

paymentCredential = confirmationToken;
} else {
const { paymentMethod, error } = await stripe.createPaymentMethod( {
let credentialId;
try {
credentialId = await createPaymentCredential(
stripe,
elements,
} );

if ( error ) {
return abortPayment( error.message );
}

paymentCredential = paymentMethod;
useConfirmationTokens
);
} catch ( credentialError ) {
return abortPayment( credentialError.message );
}

try {
Expand All @@ -177,8 +163,8 @@ export const onConfirmHandler = async (
// so that we make it harder for external plugins to modify or intercept checkout data.
...transformStripePaymentMethodForStoreApi(
event,
paymentCredential.id,
useConfirmationToken,
credentialId,
useConfirmationTokens,
paymentMethodTypes
),
extensions: applyFilters(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { getExpressCheckoutButtonAppearance } from '..';
import { getExpressCheckoutButtonAppearance } from '../button-appearance';

describe( 'getExpressCheckoutButtonAppearance', () => {
afterEach( () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { getExpressCheckoutButtonStyleSettings } from '..';
import { getExpressCheckoutButtonStyleSettings } from '../button-style-settings';

describe( 'getExpressCheckoutButtonStyleSettings', () => {
afterEach( () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Internal dependencies
*/
import { shouldUseConfirmationTokens } from '../confirmation-tokens';

describe( 'shouldUseConfirmationTokens', () => {
afterEach( () => {
delete window.wcpayExpressCheckoutParams;
} );

test( 'returns true when flag is true', () => {
window.wcpayExpressCheckoutParams = {
flags: { isEceUsingConfirmationTokens: true },
};

expect( shouldUseConfirmationTokens() ).toBe( true );
} );

test( 'returns false when flag is false', () => {
window.wcpayExpressCheckoutParams = {
flags: { isEceUsingConfirmationTokens: false },
};

expect( shouldUseConfirmationTokens() ).toBe( false );
} );

test( 'defaults to true when flags are absent', () => {
window.wcpayExpressCheckoutParams = {};

expect( shouldUseConfirmationTokens() ).toBe( true );
} );

test( 'defaults to true when params are not set', () => {
expect( shouldUseConfirmationTokens() ).toBe( true );
} );
} );
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { getErrorMessageFromNotice } from '..';
import { getErrorMessageFromNotice } from '../error-messages';

describe( 'getErrorMessageFromNotice', () => {
test( 'strips formatting', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { getExpressCheckoutData } from '..';
import { getExpressCheckoutData } from '../express-checkout-data';

describe( 'getExpressCheckoutData', () => {
afterEach( () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { displayLoginConfirmation } from '..';
import { displayLoginConfirmation } from '../login-confirmation';

describe( 'displayLoginConfirmation', () => {
let confirmSpy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Internal dependencies
*/
import { createPaymentCredential } from '../payment-credentials';

describe( 'createPaymentCredential', () => {
let stripeMock;
let elementsMock;

beforeEach( () => {
stripeMock = {
createConfirmationToken: jest.fn(),
createPaymentMethod: jest.fn(),
};
elementsMock = {};
} );

describe( 'when using confirmation tokens', () => {
test( 'creates a confirmation token and returns its id', async () => {
stripeMock.createConfirmationToken.mockResolvedValue( {
confirmationToken: { id: 'ctoken_123' },
} );

const result = await createPaymentCredential(
stripeMock,
elementsMock,
true
);

expect( stripeMock.createConfirmationToken ).toHaveBeenCalledWith( {
elements: elementsMock,
} );
expect( result ).toBe( 'ctoken_123' );
} );

test( 'throws on Stripe error', async () => {
const stripeError = { message: 'Token creation failed' };
stripeMock.createConfirmationToken.mockResolvedValue( {
error: stripeError,
} );

await expect(
createPaymentCredential( stripeMock, elementsMock, true )
).rejects.toEqual( stripeError );
} );
} );

describe( 'when using payment methods', () => {
test( 'creates a payment method and returns its id', async () => {
stripeMock.createPaymentMethod.mockResolvedValue( {
paymentMethod: { id: 'pm_456' },
} );

const result = await createPaymentCredential(
stripeMock,
elementsMock,
false
);

expect( stripeMock.createPaymentMethod ).toHaveBeenCalledWith( {
elements: elementsMock,
} );
expect( result ).toBe( 'pm_456' );
} );

test( 'throws on Stripe error', async () => {
const stripeError = { message: 'Payment method failed' };
stripeMock.createPaymentMethod.mockResolvedValue( {
error: stripeError,
} );

await expect(
createPaymentCredential( stripeMock, elementsMock, false )
).rejects.toEqual( stripeError );
} );
} );
} );
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { getStripeElementsMode } from '..';
import { getStripeElementsMode } from '../stripe-mode';

describe( 'getStripeElementsMode', () => {
beforeEach( () => {
Expand Down
35 changes: 35 additions & 0 deletions client/express-checkout/utils/button-appearance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Internal dependencies
*/
import { getDefaultBorderRadius } from 'wcpay/utils/express-checkout';
import { getExpressCheckoutData } from './express-checkout-data';

export type ButtonAttributesType =
| { height: string; borderRadius: string }
| undefined;

/**
* Returns the appearance settings for the Express Checkout buttons.
* Currently only configures border radius for the buttons.
*/
export const getExpressCheckoutButtonAppearance = (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exported from client/express-checkout/utils/index.ts with no changes, tested in client/express-checkout/utils/__tests__/button-appearance.test.js

buttonAttributes: ButtonAttributesType
) => {
let borderRadius = getDefaultBorderRadius();
const buttonSettings = getExpressCheckoutData( 'button' );

// Border radius from WooPayments settings
borderRadius = buttonSettings?.radius ?? borderRadius;

// Border radius from Cart & Checkout blocks attributes
if ( typeof buttonAttributes !== 'undefined' ) {
borderRadius = Number( buttonAttributes?.borderRadius ) ?? borderRadius;
}

return {
variables: {
borderRadius: `${ borderRadius }px`,
spacingUnit: '6px',
},
};
};
79 changes: 79 additions & 0 deletions client/express-checkout/utils/button-style-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Internal dependencies
*/
import { getExpressCheckoutData } from './express-checkout-data';

/**
* Returns the style settings for the Express Checkout buttons.
*/
export const getExpressCheckoutButtonStyleSettings = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exported from client/express-checkout/utils/index.ts with no changes, tested in client/express-checkout/utils/__tests__/button-style-settings.test.js

const buttonSettings = getExpressCheckoutData( 'button' );
const enabledMethods = getExpressCheckoutData( 'enabled_methods' ) ?? [];

const mapWooPaymentsThemeToButtonTheme = (
buttonType: string,
theme: string
) => {
switch ( theme ) {
case 'dark':
return 'black';
case 'light':
return 'white';
case 'light-outline':
if ( buttonType === 'googlePay' ) {
return 'white';
}

return 'white-outline';
default:
return 'black';
}
};

const googlePayType =
buttonSettings?.type === 'default'
? 'plain'
: buttonSettings?.type ?? 'buy';

const applePayType =
buttonSettings?.type === 'default'
? 'plain'
: buttonSettings?.type ?? 'plain';

const isGoogleApplePayEnabled = enabledMethods.includes(
'payment_request'
);

return {
paymentMethods: {
applePay: isGoogleApplePayEnabled ? 'always' : 'never',
googlePay: isGoogleApplePayEnabled ? 'always' : 'never',
amazonPay: enabledMethods.includes( 'amazon_pay' )
? 'auto'
: 'never',
link: 'never',
paypal: 'never',
klarna: 'never',
},
layout: { overflow: 'never' },
buttonTheme: {
googlePay: mapWooPaymentsThemeToButtonTheme(
'googlePay',
buttonSettings?.theme ?? 'black'
),
applePay: mapWooPaymentsThemeToButtonTheme(
'applePay',
buttonSettings?.theme ?? 'black'
),
},
buttonType: {
googlePay: googlePayType,
applePay: applePayType,
},
// Allowed height must be 40px to 55px.
buttonHeight: Math.min(
Math.max( parseInt( buttonSettings?.height ?? '48', 10 ), 40 ),
55
),
};
};
22 changes: 22 additions & 0 deletions client/express-checkout/utils/confirmation-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import { getExpressCheckoutData } from './express-checkout-data';

/**
* Determines whether to use Stripe confirmation tokens or legacy payment methods.
*
* This is a global setting controlled by the backend feature flag
* `isEceUsingConfirmationTokens`. It applies uniformly to ALL express payment
* methods (Apple Pay, Google Pay, Amazon Pay). No per-method overrides needed —
* the PHP backend already enforces that Amazon Pay can only be enabled when
* confirmation tokens are enabled (`is_amazon_pay_enabled()` requires
* `is_ece_confirmation_tokens_enabled()`).
*
* Defaults to true (confirmation tokens enabled) when the flag is absent.
*/
export function shouldUseConfirmationTokens(): boolean {
return (
getExpressCheckoutData( 'flags' )?.isEceUsingConfirmationTokens ?? true
);
}
13 changes: 13 additions & 0 deletions client/express-checkout/utils/error-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Get error messages from WooCommerce notice from server response.
*
* @param notice Error notice.
* @return Error messages.
*/
export const getErrorMessageFromNotice = ( notice: string | undefined ) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exported from client/express-checkout/utils/index.ts with no changes, tested in client/express-checkout/utils/__tests__/error-messages.test.js

if ( ! notice ) return '';

const div = document.createElement( 'div' );
div.innerHTML = notice.trim();
return div.firstChild ? div.firstChild.textContent : '';
};
Loading
Loading