- Channel
+ Sales channel
- Channel
+ Sales channel
- Online
+ Online store
diff --git a/client/payment-details/summary/test/index.test.tsx b/client/payment-details/summary/__tests__/index.test.js
similarity index 90%
rename from client/payment-details/summary/test/index.test.tsx
rename to client/payment-details/summary/__tests__/index.test.js
index 8c47e9b9b6a..8daed75e2ab 100755
--- a/client/payment-details/summary/test/index.test.tsx
+++ b/client/payment-details/summary/__tests__/index.test.js
@@ -11,11 +11,10 @@ import '@wordpress/jest-console';
/**
* Internal dependencies
*/
-import type { Charge } from 'wcpay/types/charges';
-import type { Dispute } from 'wcpay/types/disputes';
import PaymentDetailsSummary from '../';
import { useAuthorization } from 'wcpay/data';
import { paymentIntentMock } from 'wcpay/data/payment-intents/test/hooks';
+import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context';
// Mock dateI18n
jest.mock( '@wordpress/date', () => ( {
@@ -26,28 +25,6 @@ jest.mock( '@wordpress/date', () => ( {
} ),
} ) );
-declare const global: {
- wcSettings: {
- locale: {
- siteLocale: string;
- };
- };
- wcpaySettings: {
- isSubscriptionsActive: boolean;
- shouldUseExplicitPrice: boolean;
- zeroDecimalCurrencies: string[];
- currencyData: Record< string, any >;
- connect: {
- country: string;
- };
- featureFlags: {
- isAuthAndCaptureEnabled: boolean;
- };
- dateFormat: string;
- timeFormat: string;
- };
-};
-
const mockDisputeDoAccept = jest.fn();
jest.mock( 'wcpay/data', () => ( {
@@ -76,88 +53,84 @@ jest.mock( '@wordpress/data', () => ( {
withSelect: jest.fn( () => jest.fn() ),
} ) );
-const mockUseAuthorization = useAuthorization as jest.MockedFunction<
- typeof useAuthorization
->;
-
-const getBaseCharge = (): Charge =>
- ( {
- id: 'ch_38jdHA39KKA',
- payment_intent: 'pi_abc',
- /* Stripe data comes in seconds, instead of the default Date milliseconds */
- created: 1568913840,
+const mockUseAuthorization = useAuthorization;
+
+const getBaseCharge = () => ( {
+ id: 'ch_38jdHA39KKA',
+ payment_intent: 'pi_abc',
+ /* Stripe data comes in seconds, instead of the default Date milliseconds */
+ created: 1568913840,
+ amount: 2000,
+ amount_refunded: 0,
+ application_fee_amount: 70,
+ disputed: false,
+ dispute: null,
+ currency: 'usd',
+ type: 'charge',
+ status: 'succeeded',
+ paid: true,
+ captured: true,
+ balance_transaction: {
amount: 2000,
- amount_refunded: 0,
- application_fee_amount: 70,
- disputed: false,
- dispute: null,
currency: 'usd',
- type: 'charge',
- status: 'succeeded',
- paid: true,
- captured: true,
- balance_transaction: {
- amount: 2000,
- currency: 'usd',
- fee: 70,
- },
- refunds: {
- data: [],
- },
- order: {
- number: 45981,
- url: 'https://somerandomorderurl.com/?edit_order=45981',
+ fee: 70,
+ },
+ refunds: {
+ data: [],
+ },
+ order: {
+ number: 45981,
+ url: 'https://somerandomorderurl.com/?edit_order=45981',
+ },
+ billing_details: {
+ name: 'Customer name',
+ email: 'mock@example.com',
+ },
+ payment_method_details: {
+ card: {
+ brand: 'visa',
+ last4: '4242',
},
- billing_details: {
- name: 'Customer name',
- email: 'mock@example.com',
- },
- payment_method_details: {
- card: {
- brand: 'visa',
- last4: '4242',
- },
- type: 'card',
- },
- outcome: {
- risk_level: 'normal',
- },
- } as any );
+ type: 'card',
+ },
+ outcome: {
+ risk_level: 'normal',
+ },
+} );
-const getBaseDispute = (): Dispute =>
- ( {
- id: 'dp_1',
- amount: 2000,
- charge: 'ch_38jdHA39KKA',
- order: null,
- balance_transactions: [
- {
- amount: -2000,
- currency: 'usd',
- fee: 1500,
- reporting_category: 'dispute',
- },
- ],
- created: 1693453017,
- currency: 'usd',
- evidence: {
- billing_address: '123 test address',
- customer_email_address: 'test@email.com',
- customer_name: 'Test customer',
- shipping_address: '123 test address',
- },
- evidence_details: {
- due_by: 1694303999,
- has_evidence: false,
- past_due: false,
- submission_count: 0,
+const getBaseDispute = () => ( {
+ id: 'dp_1',
+ amount: 2000,
+ charge: 'ch_38jdHA39KKA',
+ order: null,
+ balance_transactions: [
+ {
+ amount: -2000,
+ currency: 'usd',
+ fee: 1500,
+ reporting_category: 'dispute',
},
- issuer_evidence: null,
- metadata: {},
- payment_intent: 'pi_1',
- reason: 'fraudulent',
- status: 'needs_response',
- } as Dispute );
+ ],
+ created: 1693453017,
+ currency: 'usd',
+ evidence: {
+ billing_address: '123 test address',
+ customer_email_address: 'test@email.com',
+ customer_name: 'Test customer',
+ shipping_address: '123 test address',
+ },
+ evidence_details: {
+ due_by: 1694303999,
+ has_evidence: false,
+ past_due: false,
+ submission_count: 0,
+ },
+ issuer_evidence: null,
+ metadata: {},
+ payment_intent: 'pi_1',
+ reason: 'fraudulent',
+ status: 'needs_response',
+} );
const getBaseMetadata = () => ( {
platform: 'ios',
@@ -165,19 +138,16 @@ const getBaseMetadata = () => ( {
reader_model: 'COTS_DEVICE',
} );
-function renderCharge(
- charge: Charge,
- metadata = {},
- isLoading = false,
- props = {}
-) {
+function renderCharge( charge, metadata = {}, isLoading = false, props = {} ) {
const { container } = render(
-
+
+
+
);
return container;
}
@@ -233,6 +203,7 @@ describe( 'PaymentDetailsSummary', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect( console ).toHaveWarnedWith(
+ // eslint-disable-next-line max-len
'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.'
);
} );
@@ -251,7 +222,7 @@ describe( 'PaymentDetailsSummary', () => {
balance_transaction: {
amount: -charge.amount_refunded,
currency: 'usd',
- } as any,
+ },
} );
expect( renderCharge( charge ) ).toMatchSnapshot();
@@ -265,7 +236,7 @@ describe( 'PaymentDetailsSummary', () => {
balance_transaction: {
amount: -charge.amount_refunded,
currency: 'usd',
- } as any,
+ },
} );
const container = renderCharge( charge );
@@ -287,7 +258,7 @@ describe( 'PaymentDetailsSummary', () => {
if ( charge.order ) {
charge.order.subscriptions = [
{
- number: 246,
+ number: 'custom-246',
url: 'https://example.com/subscription/246',
},
];
@@ -297,7 +268,7 @@ describe( 'PaymentDetailsSummary', () => {
} );
test( 'renders loading state', () => {
- expect( renderCharge( {} as any, true ) ).toMatchSnapshot();
+ expect( renderCharge( {}, true ) ).toMatchSnapshot();
} );
describe( 'capture notification and fraud buttons', () => {
@@ -545,7 +516,7 @@ describe( 'PaymentDetailsSummary', () => {
reporting_category: 'dispute',
},
],
- } as Dispute,
+ },
};
renderCharge( charge );
diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx
index 59ae556c785..4b2844f519a 100644
--- a/client/payment-details/summary/index.tsx
+++ b/client/payment-details/summary/index.tsx
@@ -120,7 +120,7 @@ const composePaymentSummaryItems = ( {
: '–',
},
{
- title: __( 'Channel', 'woocommerce-payments' ),
+ title: __( 'Sales channel', 'woocommerce-payments' ),
content: (
{ isTapToPay( metadata?.reader_model )
@@ -199,7 +199,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
const { authorization } = useAuthorization(
charge.payment_intent as string,
- charge.order?.number as number,
+ charge.order?.id as number,
shouldFetchAuthorization
);
@@ -458,7 +458,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
{ ! isLoading && isFraudOutcomeReview && (
= ( {
= ( {
charge.payment_intent,
order_id:
charge.order
- ?.number,
+ ?.id,
}
);
window.location =
@@ -679,7 +679,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
actions={
! isFraudOutcomeReview ? (
- Channel
+ Sales channel
- Online
+ Online store
diff --git a/client/payment-details/test/index.test.tsx b/client/payment-details/test/index.test.tsx
index c79874a2e48..ffb842aa84b 100644
--- a/client/payment-details/test/index.test.tsx
+++ b/client/payment-details/test/index.test.tsx
@@ -56,6 +56,13 @@ jest.mock( '@woocommerce/navigation', () => ( {
addHistoryListener: jest.fn(),
} ) );
+jest.mock( '@woocommerce/data', () => ( {
+ useUserPreferences: jest.fn( () => ( {
+ updateUserPreferences: jest.fn(),
+ wc_payments_wporg_review_2025_prompt_dismissed: false,
+ } ) ),
+} ) );
+
const chargeMock = {
id: 'ch_mock',
amount: 1500,
diff --git a/client/payment-methods-icons.tsx b/client/payment-methods-icons.tsx
index 42484747eeb..46eb21e10b2 100644
--- a/client/payment-methods-icons.tsx
+++ b/client/payment-methods-icons.tsx
@@ -8,7 +8,6 @@ import classNames from 'classnames';
/**
* Internal dependencies
*/
-import AlipayAsset from 'assets/images/payment-methods/alipay-logo.svg';
import BancontactAsset from 'assets/images/payment-methods/bancontact.svg?asset';
import EpsAsset from 'assets/images/payment-methods/eps.svg?asset';
import GiropayAsset from 'assets/images/payment-methods/giropay.svg?asset';
@@ -25,6 +24,7 @@ import KlarnaAsset from 'assets/images/payment-methods/klarna.svg?asset';
import GrabPayAsset from 'assets/images/payment-methods/grabpay.svg?asset';
import VisaAsset from 'assets/images/cards/visa.svg?asset';
import MasterCardAsset from 'assets/images/cards/mastercard.svg?asset';
+import MultibancoAsset from 'assets/images/payment-methods/multibanco.svg?asset';
import AmexAsset from 'assets/images/cards/amex.svg?asset';
import WooAsset from 'assets/images/payment-methods/woo.svg?asset';
import WooAssetShort from 'assets/images/payment-methods/woo-short.svg?asset';
@@ -76,10 +76,6 @@ export const ApplePayIcon = iconComponent(
ApplePayAsset,
__( 'Apple Pay', 'woocommerce-payments' )
);
-export const AlipayIcon = iconComponent(
- AlipayAsset,
- __( 'Alipay', 'woocommerce-payments' )
-);
export const BancontactIcon = iconComponent(
BancontactAsset,
__( 'Bancontact', 'woocommerce-payments' )
@@ -137,6 +133,10 @@ export const MastercardIcon = iconComponent(
MasterCardAsset,
__( 'Mastercard', 'woocommerce-payments' )
);
+export const MultibancoIcon = iconComponent(
+ MultibancoAsset,
+ __( 'Multibanco', 'woocommerce-payments' )
+);
export const P24Icon = iconComponent(
P24Asset,
__( 'Przelewy24 (P24)', 'woocommerce-payments' )
diff --git a/client/payment-methods-map.tsx b/client/payment-methods-map.tsx
index 9dc5a685940..07206384353 100644
--- a/client/payment-methods-map.tsx
+++ b/client/payment-methods-map.tsx
@@ -1,14 +1,15 @@
/**
* External dependencies
*/
+import React from 'react';
import { __ } from '@wordpress/i18n';
+import classNames from 'classnames';
/**
* Internal dependencies
*/
import {
- AlipayIcon,
AffirmIcon,
AfterpayIcon,
ClearpayIcon,
@@ -23,23 +24,48 @@ import {
P24Icon,
SepaIcon,
SofortIcon,
+ MultibancoIcon,
GrabPayIcon,
WeChatPayIcon,
} from 'wcpay/payment-methods-icons';
const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US';
-export interface PaymentMethodMapEntry {
- id: string;
- label: string;
- description: string;
- icon: ReactImgFuncComponent;
- currencies: string[];
- stripe_key: string;
- allows_manual_capture: boolean;
- allows_pay_later: boolean;
- accepts_only_domestic_payment: boolean;
-}
+import type { PaymentMethodMapEntry } from './types/payment-methods';
+
+// Get any payment method definitions from the client.
+const PaymentMethodDefinitions =
+ typeof wooPaymentsPaymentMethodDefinitions !== 'undefined'
+ ? wooPaymentsPaymentMethodDefinitions
+ : {};
+
+const convertedPaymentMethodDefinitions = Object.fromEntries<
+ PaymentMethodMapEntry
+>(
+ Object.entries( PaymentMethodDefinitions ).map( ( [ key, value ] ) => [
+ key,
+ {
+ id: value.id,
+ label: value.title,
+ description: value.description,
+ icon: ( { className } ) => (
+
+ ),
+ currencies: value.currencies,
+ stripe_key: value.stripe_key,
+ allows_manual_capture: value.allows_manual_capture,
+ allows_pay_later: value.allows_pay_later,
+ accepts_only_domestic_payment: value.accepts_only_domestic_payment,
+ },
+ ] )
+);
const PaymentMethodInformationObject: Record<
string,
@@ -59,20 +85,6 @@ const PaymentMethodInformationObject: Record<
allows_pay_later: false,
accepts_only_domestic_payment: false,
},
- alipay: {
- id: 'alipay',
- label: __( 'Alipay', 'woocommerce-payments' ),
- description: __(
- 'Alipay is a popular wallet in China, operated by Ant Financial Services Group, a financial services provider affiliated with Alibaba.',
- 'woocommerce-payments'
- ),
- icon: AlipayIcon,
- currencies: [],
- stripe_key: 'alipay_payments',
- allows_manual_capture: false,
- allows_pay_later: false,
- accepts_only_domestic_payment: false,
- },
au_becs_debit: {
id: 'au_becs_debit',
label: __( 'BECS Direct Debit', 'woocommerce-payments' ),
@@ -185,6 +197,20 @@ const PaymentMethodInformationObject: Record<
allows_pay_later: false,
accepts_only_domestic_payment: false,
},
+ multibanco: {
+ id: 'multibanco',
+ label: __( 'Multibanco', 'woocommerce-payments' ),
+ description: __(
+ 'A voucher based payment method for your customers in Portugal.',
+ 'woocommerce-payments'
+ ),
+ icon: MultibancoIcon,
+ currencies: [ 'EUR' ],
+ stripe_key: 'multibanco_payments',
+ allows_manual_capture: false,
+ allows_pay_later: false,
+ accepts_only_domestic_payment: false,
+ },
affirm: {
id: 'affirm',
label: __( 'Affirm', 'woocommerce-payments' ),
@@ -292,6 +318,7 @@ const PaymentMethodInformationObject: Record<
allows_pay_later: false,
accepts_only_domestic_payment: false,
},
+ ...convertedPaymentMethodDefinitions,
};
export default PaymentMethodInformationObject;
diff --git a/client/plugins-page/deactivation-survey/index.js b/client/plugins-page/deactivation-survey/index.tsx
similarity index 79%
rename from client/plugins-page/deactivation-survey/index.js
rename to client/plugins-page/deactivation-survey/index.tsx
index faa4eaf2228..794fa25850f 100644
--- a/client/plugins-page/deactivation-survey/index.js
+++ b/client/plugins-page/deactivation-survey/index.tsx
@@ -12,11 +12,20 @@ import './style.scss';
import Loadable from 'wcpay/components/loadable';
import WooPaymentsIcon from 'assets/images/woopayments.svg?asset';
-const PluginDisableSurvey = ( { onRequestClose } ) => {
+interface PluginDisableSurveyProps {
+ /**
+ * Callback to close the modal.
+ */
+ onRequestClose: () => void;
+}
+const PluginDisableSurvey = ( {
+ onRequestClose,
+}: PluginDisableSurveyProps ) => {
const [ isLoading, setIsLoading ] = useState( true );
return (
( {
+ useTestMode: jest.fn(),
+ usePaymentRequestEnabledSettings: jest.fn(),
+} ) );
+
+describe( 'GooglePayTestModeCompatibilityNotice', () => {
+ beforeEach( () => {
+ jest.resetAllMocks();
+ } );
+
+ it( 'does not render when the account is not live', () => {
+ useTestMode.mockReturnValue( [ true ] );
+ usePaymentRequestEnabledSettings.mockReturnValue( [ true ] );
+
+ const { container } = render(
+
+
+
+ );
+
+ expect( container.firstChild ).toBeNull();
+ } );
+
+ it( 'does not render when test mode is not active', () => {
+ useTestMode.mockReturnValue( [ false ] );
+ usePaymentRequestEnabledSettings.mockReturnValue( [ true ] );
+
+ const { container } = render(
+
+
+
+ );
+
+ expect( container.firstChild ).toBeNull();
+ } );
+
+ it( 'does not render when Google Pay is not enabled', () => {
+ useTestMode.mockReturnValue( [ true ] );
+ usePaymentRequestEnabledSettings.mockReturnValue( [ false ] );
+
+ const { container } = render(
+
+
+
+ );
+
+ expect( container.firstChild ).toBeNull();
+ } );
+
+ it( 'renders the notice when the requirements are met', () => {
+ useTestMode.mockReturnValue( [ true ] );
+ usePaymentRequestEnabledSettings.mockReturnValue( [ true ] );
+
+ render(
+
+
+
+ );
+
+ expect(
+ screen.getByText( /Google Pay is incompatible with test mode/, {
+ ignore: '.a11y-speak-region',
+ } )
+ ).toBeInTheDocument();
+ } );
+} );
diff --git a/client/settings/test/settings-section.tsx b/client/settings/__tests__/settings-section.tsx
similarity index 100%
rename from client/settings/test/settings-section.tsx
rename to client/settings/__tests__/settings-section.tsx
diff --git a/client/settings/advanced-settings/multi-currency-toggle.js b/client/settings/advanced-settings/multi-currency-toggle.js
index 5a285c7c37a..e5033e7e5d5 100644
--- a/client/settings/advanced-settings/multi-currency-toggle.js
+++ b/client/settings/advanced-settings/multi-currency-toggle.js
@@ -3,7 +3,6 @@
*/
import { CheckboxControl, ExternalLink } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
@@ -17,14 +16,6 @@ const MultiCurrencyToggle = () => {
updateIsMultiCurrencyEnabled,
] = useMultiCurrency();
- const headingRef = useRef( null );
-
- useEffect( () => {
- if ( ! headingRef.current ) return;
-
- headingRef.current.focus();
- }, [] );
-
const handleMultiCurrencyStatusChange = ( value ) => {
updateIsMultiCurrencyEnabled( value );
};
diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js
index c2b23f5968e..edc2339e506 100644
--- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js
+++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js
@@ -3,7 +3,6 @@
*/
import { CheckboxControl, ExternalLink } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
-import { useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
@@ -18,14 +17,6 @@ const WCPaySubscriptionsToggle = () => {
updateIsWCPaySubscriptionsEnabled,
] = useWCPaySubscriptions();
- const headingRef = useRef( null );
-
- useEffect( () => {
- if ( ! headingRef.current ) return;
-
- headingRef.current.focus();
- }, [] );
-
const handleWCPaySubscriptionsStatusChange = ( value ) => {
updateIsWCPaySubscriptionsEnabled( value );
};
diff --git a/client/settings/buy-now-pay-later-section/__tests__/buy-now-pay-later-section.test.js b/client/settings/buy-now-pay-later-section/__tests__/buy-now-pay-later-section.test.js
index 4b800dc5ea3..fa801af11e7 100644
--- a/client/settings/buy-now-pay-later-section/__tests__/buy-now-pay-later-section.test.js
+++ b/client/settings/buy-now-pay-later-section/__tests__/buy-now-pay-later-section.test.js
@@ -45,9 +45,7 @@ jest.mock( 'wcpay/data', () => ( {
} ) );
jest.mock( '@wordpress/data', () => ( {
- useDispatch: jest
- .fn()
- .mockReturnValue( { updateAvailablePaymentMethodIds: jest.fn() } ),
+ useDispatch: jest.fn().mockReturnValue( {} ),
select: jest.fn(),
} ) );
diff --git a/client/settings/express-checkout-settings/test/file-upload.test.js b/client/settings/express-checkout-settings/__tests__/file-upload.test.js
similarity index 100%
rename from client/settings/express-checkout-settings/test/file-upload.test.js
rename to client/settings/express-checkout-settings/__tests__/file-upload.test.js
diff --git a/client/settings/express-checkout-settings/test/index.js b/client/settings/express-checkout-settings/__tests__/index.test.js
similarity index 80%
rename from client/settings/express-checkout-settings/test/index.js
rename to client/settings/express-checkout-settings/__tests__/index.test.js
index 899d5d782d5..c3377aa447c 100644
--- a/client/settings/express-checkout-settings/test/index.js
+++ b/client/settings/express-checkout-settings/__tests__/index.test.js
@@ -10,8 +10,10 @@ import { render, screen, within } from '@testing-library/react';
*/
import ExpressCheckoutSettings from '..';
import PaymentRequestButtonPreview from '../payment-request-button-preview';
+import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context';
jest.mock( '../../../data', () => ( {
+ useTestMode: jest.fn().mockReturnValue( [] ),
useGetSettings: jest.fn().mockReturnValue( {} ),
useSettings: jest.fn().mockReturnValue( {} ),
usePaymentRequestEnabledSettings: jest
@@ -66,9 +68,17 @@ jest.mock( '@woocommerce/components', () => ( {
) ),
} ) );
+const renderWithSettingsProvider = ( ui ) =>
+ render(
+
+ { ui }
+
+ );
+
describe( 'ExpressCheckoutSettings', () => {
beforeEach( () => {
global.wcpaySettings = {
+ accountStatus: {},
restUrl: 'http://example.com/wp-json/',
featureFlags: {
woopayExpressCheckout: true,
@@ -77,7 +87,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders error message for invalid method IDs', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
const errorMessage = screen.queryByText(
'Invalid express checkout method ID specified.'
@@ -86,7 +98,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders payment request breadcrumbs', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
const linkToPayments = screen.getByRole( 'link', {
name: 'WooPayments',
@@ -100,7 +114,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders payment request title and description', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
const heading = screen.queryByRole( 'heading', {
name: 'Settings',
@@ -109,7 +125,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders payment request enable setting and confirm its checkbox label', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
const label = screen.getByRole( 'checkbox', {
name: 'Enable Apple Pay / Google Pay',
@@ -118,7 +136,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders payment request general setting and confirm its first heading', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
expect(
screen.queryByRole( 'heading', {
@@ -128,7 +148,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders woopay breadcrumbs', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
const linkToPayments = screen.getByRole( 'link', {
name: 'WooPayments',
@@ -140,7 +162,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders woopay settings and confirm its checkbox label', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
const label = screen.getByRole( 'checkbox', {
name: 'Enable WooPay',
@@ -149,7 +173,9 @@ describe( 'ExpressCheckoutSettings', () => {
} );
test( 'renders WooPay express button appearance settings if feature flag is enabled and confirm its first heading', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
expect(
screen.queryByRole( 'heading', {
@@ -161,7 +187,9 @@ describe( 'ExpressCheckoutSettings', () => {
test( 'does not render WooPay express button appearance settings if feature flag is disabled', () => {
global.wcpaySettings.featureFlags.woopayExpressCheckout = false;
- render( );
+ renderWithSettingsProvider(
+
+ );
expect(
screen.queryByRole( 'heading', {
diff --git a/client/settings/express-checkout-settings/test/payment-request-button-preview.test.js b/client/settings/express-checkout-settings/__tests__/payment-request-button-preview.test.js
similarity index 100%
rename from client/settings/express-checkout-settings/test/payment-request-button-preview.test.js
rename to client/settings/express-checkout-settings/__tests__/payment-request-button-preview.test.js
diff --git a/client/settings/express-checkout-settings/test/payment-request-settings.test.js b/client/settings/express-checkout-settings/__tests__/payment-request-settings.test.js
similarity index 89%
rename from client/settings/express-checkout-settings/test/payment-request-settings.test.js
rename to client/settings/express-checkout-settings/__tests__/payment-request-settings.test.js
index 47e5a61de2b..459ebf99a42 100644
--- a/client/settings/express-checkout-settings/test/payment-request-settings.test.js
+++ b/client/settings/express-checkout-settings/__tests__/payment-request-settings.test.js
@@ -19,10 +19,12 @@ import {
usePaymentRequestButtonTheme,
useWooPayEnabledSettings,
} from '../../../data';
+import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context';
jest.mock( '../../../data', () => ( {
usePaymentRequestEnabledSettings: jest.fn(),
usePaymentRequestLocations: jest.fn(),
+ useTestMode: jest.fn().mockReturnValue( [ false ] ),
usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'default' ] ),
usePaymentRequestButtonBorderRadius: jest.fn().mockReturnValue( [ 4 ] ),
usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ),
@@ -63,6 +65,15 @@ const getMockPaymentRequestLocations = (
updatePaymentRequestLocationsHandler,
];
+const renderWithSettingsProvider = ( ui ) =>
+ render(
+
+ { ui }
+
+ );
+
describe( 'PaymentRequestSettings', () => {
beforeEach( () => {
usePaymentRequestEnabledSettings.mockReturnValue(
@@ -79,7 +90,9 @@ describe( 'PaymentRequestSettings', () => {
} );
it( 'renders enable settings with defaults', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
// confirm there is a heading
expect(
@@ -104,7 +117,9 @@ describe( 'PaymentRequestSettings', () => {
)
);
- render( );
+ renderWithSettingsProvider(
+
+ );
expect( updateIsPaymentRequestEnabledHandler ).not.toHaveBeenCalled();
@@ -119,7 +134,9 @@ describe( 'PaymentRequestSettings', () => {
} );
it( 'renders general settings with defaults', () => {
- render( );
+ renderWithSettingsProvider(
+
+ );
// confirm settings headings
expect(
@@ -163,7 +180,9 @@ describe( 'PaymentRequestSettings', () => {
updatePaymentRequestLocationsHandler
)
);
- render( );
+ renderWithSettingsProvider(
+
+ );
expect( updatePaymentRequestLocationsHandler ).not.toHaveBeenCalled();
@@ -201,7 +220,9 @@ describe( 'PaymentRequestSettings', () => {
setButtonThemeMock,
] );
- render( );
+ renderWithSettingsProvider(
+
+ );
expect( setButtonTypeMock ).not.toHaveBeenCalled();
expect( setButtonSizeMock ).not.toHaveBeenCalled();
@@ -237,7 +258,9 @@ describe( 'PaymentRequestSettings', () => {
)
);
- render( );
+ renderWithSettingsProvider(
+
+ );
// Uncheck each checkbox, and verify them what kind of action should have been called
userEvent.click( screen.getByText( 'Product Page' ) );
diff --git a/client/settings/express-checkout-settings/test/woopay-settings.test.js b/client/settings/express-checkout-settings/__tests__/woopay-settings.test.js
similarity index 100%
rename from client/settings/express-checkout-settings/test/woopay-settings.test.js
rename to client/settings/express-checkout-settings/__tests__/woopay-settings.test.js
diff --git a/client/settings/express-checkout-settings/payment-request-settings.js b/client/settings/express-checkout-settings/payment-request-settings.js
index a676eba6bbe..eb473f0626b 100644
--- a/client/settings/express-checkout-settings/payment-request-settings.js
+++ b/client/settings/express-checkout-settings/payment-request-settings.js
@@ -15,6 +15,7 @@ import {
usePaymentRequestEnabledSettings,
usePaymentRequestLocations,
} from 'wcpay/data';
+import GooglePayTestModeCompatibilityNotice from '../google-pay-test-mode-compatibility-notice';
const PaymentRequestSettings = ( { section } ) => {
const [
@@ -44,6 +45,7 @@ const PaymentRequestSettings = ( { section } ) => {
{ section === 'enable' && (
+
{
const id = 'apple_pay_google_pay';
@@ -171,6 +172,7 @@ const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => {
+
{ isDuplicate && (
( {
+ useTestMode: jest.fn().mockReturnValue( [] ),
usePaymentRequestEnabledSettings: jest.fn(),
useWooPayEnabledSettings: jest.fn(),
useEnabledPaymentMethodIds: jest.fn(),
@@ -70,7 +71,7 @@ describe( 'ExpressCheckout', () => {
)
);
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
render(
@@ -84,7 +85,7 @@ describe( 'ExpressCheckout', () => {
} );
it( 'has the correct href links to the express checkout settings pages', async () => {
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] );
@@ -112,7 +113,7 @@ describe( 'ExpressCheckout', () => {
} );
it( 'hide link payment if card payment method is inactive', async () => {
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'link' ] ] );
@@ -126,7 +127,7 @@ describe( 'ExpressCheckout', () => {
} );
it( 'show link payment if card payment method is active', async () => {
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] );
@@ -140,7 +141,7 @@ describe( 'ExpressCheckout', () => {
} );
it( 'test stripe link checkbox checked', async () => {
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] );
@@ -154,7 +155,7 @@ describe( 'ExpressCheckout', () => {
} );
it( 'test stripe link checkbox not checked', async () => {
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card' ] ] );
@@ -172,7 +173,7 @@ describe( 'ExpressCheckout', () => {
useWooPayEnabledSettings.mockReturnValue(
getMockWooPayEnabledSettings( false, updateIsWooPayEnabledHandler )
);
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] );
@@ -200,7 +201,7 @@ describe( 'ExpressCheckout', () => {
useWooPayEnabledSettings.mockReturnValue(
getMockWooPayEnabledSettings( false, updateIsWooPayEnabledHandler )
);
- const context = { featureFlags: { woopay: true } };
+ const context = { accountStatus: {}, featureFlags: { woopay: true } };
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] );
useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] );
diff --git a/client/settings/fraud-protection/style.scss b/client/settings/fraud-protection/style.scss
index 7fdc76021c7..88ac2ca4292 100644
--- a/client/settings/fraud-protection/style.scss
+++ b/client/settings/fraud-protection/style.scss
@@ -251,52 +251,54 @@
}
}
-.components-modal__header {
- border-bottom: 1px solid #ddd;
-}
+.fraud-protection-level-modal {
+ .components-modal__header {
+ border-bottom: 1px solid #ddd;
+ }
-.components-modal__body--fraud-protection {
- padding-top: 24px;
+ .components-modal__body--fraud-protection {
+ padding-top: 24px;
- ul {
- list-style-type: disc;
- margin-left: 24px;
+ ul {
+ list-style-type: disc;
+ margin-left: 24px;
+ }
}
-}
-.component-modal__text {
- &--blocked {
- color: #b32d2e;
- }
- &--review {
- color: #996800;
+ .component-modal__text {
+ &--blocked {
+ color: #b32d2e;
+ }
+ &--review {
+ color: #996800;
+ }
}
-}
-.component-modal__button {
- &--confirm {
- display: block;
- margin-left: auto;
+ .component-modal__button {
+ &--confirm {
+ display: block;
+ margin-left: auto;
+ }
}
-}
-.component-notice--is-info {
- margin-left: 0;
- margin-right: 0;
- margin-bottom: 1em;
- background-color: #f0f6fc;
- border-left: none;
-}
+ .component-notice--is-info {
+ margin-left: 0;
+ margin-right: 0;
+ margin-bottom: 1em;
+ background-color: #f0f6fc;
+ border-left: none;
+ }
-.component-notice__icon {
- margin-right: 10px;
- align-self: center;
-}
+ .component-notice__icon {
+ margin-right: 10px;
+ align-self: center;
+ }
-.component-notice__content--flex {
- display: flex;
- flex-direction: row;
- align-items: flex-start;
+ .component-notice__content--flex {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ }
}
#fraud-protection-welcome-tour-first-step {
diff --git a/client/settings/google-pay-test-mode-compatibility-notice.tsx b/client/settings/google-pay-test-mode-compatibility-notice.tsx
new file mode 100644
index 00000000000..e5d81824e8b
--- /dev/null
+++ b/client/settings/google-pay-test-mode-compatibility-notice.tsx
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { ExternalLink } from '@wordpress/components';
+import interpolateComponents from '@automattic/interpolate-components';
+import React, { useContext } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { usePaymentRequestEnabledSettings, useTestMode } from 'wcpay/data';
+import InlineNotice from 'wcpay/components/inline-notice';
+import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context';
+
+const GooglePayTestModeCompatibilityNotice = () => {
+ const [ isTestModeEnabled ] = useTestMode();
+ const [ isPaymentRequestEnabled ] = usePaymentRequestEnabledSettings();
+ const {
+ accountStatus: { isLive: isLiveAccount },
+ } = useContext( WCPaySettingsContext );
+
+ if ( ! isLiveAccount ) {
+ return null;
+ }
+
+ if ( ! isTestModeEnabled ) {
+ return null;
+ }
+
+ if ( ! isPaymentRequestEnabled ) {
+ return null;
+ }
+
+ return (
+
+ { interpolateComponents( {
+ mixedString: __(
+ 'Google Pay is incompatible with test mode. {{learnMore}}Learn more{{/learnMore}}.',
+ 'woocommerce-payments'
+ ),
+ components: {
+ learnMore: (
+
+ ),
+ },
+ } ) }
+
+ );
+};
+
+export default GooglePayTestModeCompatibilityNotice;
diff --git a/client/settings/payment-methods-list/index.js b/client/settings/payment-methods-list/index.js
index 5a82a36a453..997c2bb9874 100644
--- a/client/settings/payment-methods-list/index.js
+++ b/client/settings/payment-methods-list/index.js
@@ -115,7 +115,6 @@ const PaymentMethodsList = ( { methodIds } ) => {
allows_manual_capture: isAllowingManualCapture,
setup_required: isSetupRequired,
setup_tooltip: setupTooltip,
- // TODO : fix in https://github.com/Automattic/woocommerce-payments/issues/10182 to remove duplicated logic
currencies,
} ) => {
if (
diff --git a/client/settings/payment-methods-list/payment-method.tsx b/client/settings/payment-methods-list/payment-method.tsx
index 18f978a6caa..d6611a77ed9 100644
--- a/client/settings/payment-methods-list/payment-method.tsx
+++ b/client/settings/payment-methods-list/payment-method.tsx
@@ -130,7 +130,7 @@ const PaymentMethod = ( {
upeCapabilityStatuses.INACTIVE === status || isPoInProgress;
const {
accountFees,
- }: { accountFees: Record< string, FeeStructure > } = useContext(
+ }: { accountFees?: Record< string, FeeStructure > } = useContext(
WCPaySettingsContext
);
const [ isManualCaptureEnabled ] = useManualCapture();
diff --git a/client/settings/payment-methods-section/__tests__/payment-methods-section.test.js b/client/settings/payment-methods-section/__tests__/payment-methods-section.test.js
index 4fa6f05218d..66f4de17da6 100644
--- a/client/settings/payment-methods-section/__tests__/payment-methods-section.test.js
+++ b/client/settings/payment-methods-section/__tests__/payment-methods-section.test.js
@@ -54,9 +54,7 @@ jest.mock( 'multi-currency/interface/data', () => ( {
} ) );
jest.mock( '@wordpress/data', () => ( {
- useDispatch: jest
- .fn()
- .mockReturnValue( { updateAvailablePaymentMethodIds: jest.fn() } ),
+ useDispatch: jest.fn().mockReturnValue( {} ),
select: jest.fn(),
} ) );
diff --git a/client/settings/phone-input/index.tsx b/client/settings/phone-input/index.tsx
index 4697d3ece62..621ad6f93b7 100644
--- a/client/settings/phone-input/index.tsx
+++ b/client/settings/phone-input/index.tsx
@@ -44,10 +44,32 @@ const PhoneNumberInput = ( {
] = useState< intlTelInput.Plugin | null >( null );
const inputRef = useRef< HTMLInputElement >( null );
+ // in some special cases, the phone number is valid but the library doesn't recognize it as such
+ const isValidNumber = ( instance: intlTelInput.Plugin ): boolean => {
+ // Special case for Singapore: some numbers are valid but the library doesn't recognize them
+ if (
+ '65' === instance.getSelectedCountryData().dialCode &&
+ ! instance.isValidNumber()
+ ) {
+ if ( 11 !== instance.getNumber().length ) {
+ return false;
+ }
+
+ if (
+ [ '800', '805', '806', '807', '808', '809' ].includes(
+ instance.getNumber().substr( 3, 3 )
+ )
+ ) {
+ return true;
+ }
+ }
+ return instance.isValidNumber();
+ };
+
const handlePhoneNumberInputChange = () => {
if ( inputInstance ) {
onValueChange( inputInstance.getNumber() );
- onValidationChange( inputInstance.isValidNumber() );
+ onValidationChange( isValidNumber( inputInstance ) );
}
};
@@ -69,7 +91,7 @@ const PhoneNumberInput = ( {
const handleCountryChange = () => {
if ( iti && ( focusLost || iti.getNumber() ) ) {
onValueChange( iti.getNumber() );
- onValidationChange( iti.isValidNumber() );
+ onValidationChange( isValidNumber( iti ) );
}
};
@@ -151,7 +173,7 @@ const PhoneNumberInput = ( {
( focusLost || inputInstance.getNumber() )
) {
inputInstance.setNumber( value );
- onValidationChange( inputInstance.isValidNumber() );
+ onValidationChange( isValidNumber( inputInstance ) );
}
}, [ value, inputInstance, inputRef, onValidationChange, focusLost ] );
@@ -178,7 +200,7 @@ const PhoneNumberInput = ( {
}
name={ inputProps.name }
className={
- inputInstance && ! inputInstance.isValidNumber()
+ inputInstance && ! isValidNumber( inputInstance )
? 'phone-input input-text has-error'
: 'phone-input input-text'
}
diff --git a/client/settings/support-phone-input/test/support-phone-input.test.js b/client/settings/support-phone-input/test/support-phone-input.test.js
index 227f02f52d7..10e17455d07 100644
--- a/client/settings/support-phone-input/test/support-phone-input.test.js
+++ b/client/settings/support-phone-input/test/support-phone-input.test.js
@@ -98,6 +98,75 @@ describe( 'SupportPhoneInput', () => {
).toEqual( 'Please enter a valid phone number.' );
} );
+ it( 'Singapore phone number validation special cases - starting with 800, 805, 806, 807, 808 or 809', async () => {
+ useAccountBusinessSupportPhone.mockReturnValue( [
+ '+6580600000', // test phone number.
+ jest.fn(),
+ ] );
+ useTestModeOnboarding.mockReturnValue( true );
+
+ const { container } = render( );
+ expect(
+ container.querySelector( '.components-notice.is-error' )
+ ).toBeNull();
+
+ fireEvent.change(
+ screen.getByLabelText(
+ 'Support phone number (+1 0000000000 can be used in sandbox mode)'
+ ),
+ {
+ target: { value: '+6580000000' },
+ }
+ );
+ expect(
+ container.querySelector( '.components-notice.is-error' )
+ ).toBeNull();
+ fireEvent.change(
+ screen.getByLabelText(
+ 'Support phone number (+1 0000000000 can be used in sandbox mode)'
+ ),
+ {
+ target: { value: '+6580500000' },
+ }
+ );
+ expect(
+ container.querySelector( '.components-notice.is-error' )
+ ).toBeNull();
+ fireEvent.change(
+ screen.getByLabelText(
+ 'Support phone number (+1 0000000000 can be used in sandbox mode)'
+ ),
+ {
+ target: { value: '+6580700000' },
+ }
+ );
+ expect(
+ container.querySelector( '.components-notice.is-error' )
+ ).toBeNull();
+ fireEvent.change(
+ screen.getByLabelText(
+ 'Support phone number (+1 0000000000 can be used in sandbox mode)'
+ ),
+ {
+ target: { value: '+6580800000' },
+ }
+ );
+ expect(
+ container.querySelector( '.components-notice.is-error' )
+ ).toBeNull();
+ fireEvent.change(
+ screen.getByLabelText(
+ 'Support phone number (+1 0000000000 can be used in sandbox mode)'
+ ),
+ {
+ target: { value: '+6580900000' },
+ }
+ );
+ expect(
+ container.querySelector( '.components-notice.is-error' )
+ ).toBeNull();
+ } );
+
it( 'in sandbox mode, allow all 0s number', async () => {
useAccountBusinessSupportPhone.mockReturnValue( [
'+10000000000', // test phone number.
diff --git a/client/settings/wcpay-settings-context.js b/client/settings/wcpay-settings-context.js
index 531e806ff27..92a92427908 100644
--- a/client/settings/wcpay-settings-context.js
+++ b/client/settings/wcpay-settings-context.js
@@ -3,15 +3,6 @@
*/
import { createContext } from 'react';
-const WCPaySettingsContext = createContext( {
- accountFees: {},
- accountLoans: {},
- accountStatus: {},
- featureFlags: {
- isAuthAndCaptureEnabled: false,
- isDisputeIssuerEvidenceEnabled: false,
- woopay: false,
- },
-} );
+const WCPaySettingsContext = createContext( window.wcpaySettings );
export default WCPaySettingsContext;
diff --git a/client/style.scss b/client/style.scss
index f44cc0284ca..ccbcc9ce04f 100644
--- a/client/style.scss
+++ b/client/style.scss
@@ -10,13 +10,15 @@
}
}
-.components-card__body {
- > *:first-child {
- margin-top: 0;
- }
+.woocommerce-payments-page {
+ .components-card__body {
+ > *:first-child {
+ margin-top: 0;
+ }
- > *:last-child {
- margin-bottom: 0;
+ > *:last-child {
+ margin-bottom: 0;
+ }
}
}
diff --git a/client/success/index.js b/client/success/index.js
new file mode 100644
index 00000000000..d3dd21d996e
--- /dev/null
+++ b/client/success/index.js
@@ -0,0 +1,106 @@
+/**
+ * External dependencies
+ */
+import tinycolor from 'tinycolor2';
+
+document.addEventListener( 'DOMContentLoaded', () => {
+ const multibancoInstructionsContainer = document.getElementById(
+ 'wc-payment-gateway-multibanco-instructions-container'
+ );
+
+ if ( multibancoInstructionsContainer ) {
+ // Get the computed text color
+ const computedTextColor = window.getComputedStyle(
+ multibancoInstructionsContainer
+ ).color;
+
+ // Get the parent's background color, accounting for transparency
+ const getEffectiveBackgroundColor = ( element ) => {
+ let currentElement = element;
+ let backgroundColor = window.getComputedStyle( currentElement )
+ .backgroundColor;
+
+ // Keep going up the DOM tree until we find a non-transparent background
+ while ( currentElement ) {
+ const color = tinycolor( backgroundColor );
+ // Skip colors with 0 alpha
+ if ( color.getAlpha() > 0 ) {
+ return backgroundColor;
+ }
+ currentElement = currentElement.parentElement;
+ if ( ! currentElement ) {
+ return 'rgb(255, 255, 255)'; // Default to white if we reach the root
+ }
+ backgroundColor = window.getComputedStyle( currentElement )
+ .backgroundColor;
+ }
+ return 'rgb(255, 255, 255)'; // Default to white as fallback
+ };
+
+ const parentBgColor = getEffectiveBackgroundColor(
+ multibancoInstructionsContainer.parentElement
+ );
+
+ // Convert RGB color to RGBA with different opacities
+ const convertRgbToRgba = ( rgbColor, opacity ) => {
+ const color = tinycolor( rgbColor );
+ color.setAlpha( opacity );
+ return color.toRgbString();
+ };
+
+ // Set the CSS variables on the container
+ multibancoInstructionsContainer.style.setProperty(
+ '--woopayments-multibanco-text-color',
+ computedTextColor
+ );
+ multibancoInstructionsContainer.style.setProperty(
+ '--woopayments-multibanco-bg-color',
+ convertRgbToRgba( computedTextColor, 0.06 )
+ );
+ multibancoInstructionsContainer.style.setProperty(
+ '--woopayments-multibanco-border-color',
+ convertRgbToRgba( computedTextColor, 0.16 )
+ );
+ multibancoInstructionsContainer.style.setProperty(
+ '--woopayments-multibanco-card-bg-color',
+ parentBgColor
+ );
+
+ // Add click handlers for copy buttons
+ const copyButtons = multibancoInstructionsContainer.querySelectorAll(
+ '.copy-btn'
+ );
+ copyButtons.forEach( ( button ) => {
+ button.addEventListener( 'click', () => {
+ const textToCopy = button.dataset.copyValue;
+ if ( textToCopy ) {
+ navigator.clipboard
+ .writeText( textToCopy )
+ .then( () => {
+ button.classList.add( 'copied' );
+ setTimeout( () => {
+ button.classList.remove( 'copied' );
+ }, 2000 );
+ } )
+ .catch( () => {
+ // show a prompt with the data-copy-value selected in a field and tell the user to copy it
+ prompt(
+ `Failed to copy text. Please copy it manually:`,
+ textToCopy
+ );
+ } );
+ }
+ } );
+ } );
+
+ // Add click handler for print button
+ const printButton = multibancoInstructionsContainer.querySelector(
+ '.print-btn'
+ );
+ if ( printButton ) {
+ printButton.addEventListener( 'click', () => {
+ window.print();
+ } );
+ }
+ }
+} );
diff --git a/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js b/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js
new file mode 100644
index 00000000000..f7103875833
--- /dev/null
+++ b/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js
@@ -0,0 +1,137 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useRef } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { getExpressCheckoutButtonAppearance } from '../../utils';
+
+export const SUPPORTED_PREVIEW_PAYMENT_METHODS = [ 'googlePay', 'applePay' ];
+
+const GooglePayButtonPreview = ( { options, buttonAttributes, theme } ) => {
+ const googlePlayContainerRef = useRef( null );
+ const hasStartedLoadingGooglePlayButton = useRef( null );
+ const appearance = getExpressCheckoutButtonAppearance( buttonAttributes );
+ const borderRadius = appearance.variables.borderRadius;
+
+ useEffect( () => {
+ if (
+ googlePlayContainerRef.current &&
+ ! hasStartedLoadingGooglePlayButton.current
+ ) {
+ hasStartedLoadingGooglePlayButton.current = true;
+ ( async () => {
+ // The container may be inside an iframe, so we need to retrieve a reference to the document and window objects.
+ const targetDocument =
+ googlePlayContainerRef.current.ownerDocument;
+ const targetWindow = targetDocument.defaultView;
+ if ( ! targetWindow.google?.payments?.api?.PaymentsClient ) {
+ await new Promise( ( resolve ) => {
+ const script = document.createElement( 'script' );
+ script.src = 'https://pay.google.com/gp/p/js/pay.js';
+ script.onload = resolve;
+ targetDocument.head.appendChild( script );
+ } );
+ }
+
+ const googlePayClient = new targetWindow.google.payments.api.PaymentsClient(
+ {
+ environment: 'TEST',
+ }
+ );
+
+ const buttonColor = theme === 'black' ? 'black' : 'white'; // There is no 'outline' theme in Google Pay.
+
+ const button = googlePayClient.createButton( {
+ buttonType: options.buttonType.googlePay,
+ buttonColor,
+ buttonRadius: parseFloat( borderRadius ),
+ buttonSizeMode: 'fill',
+ onClick: () => {},
+ } );
+ googlePlayContainerRef.current.appendChild( button );
+ } )();
+ }
+ }, [ theme, borderRadius, options.buttonType.googlePay ] );
+
+ useEffect( () => {
+ googlePlayContainerRef.current
+ ?.querySelector( 'button' )
+ ?.style?.setProperty( 'border-radius', borderRadius );
+ }, [ borderRadius ] );
+
+ return (
+
+ );
+};
+
+const ApplePayButtonPreview = ( { options, buttonAttributes, theme } ) => {
+ const appearance = getExpressCheckoutButtonAppearance( buttonAttributes );
+ const borderRadius = appearance.variables.borderRadius;
+
+ const buttonStyle = {
+ height: `${ options.buttonHeight }px`,
+ borderRadius,
+ ApplePayButtonType: options.buttonType.applePay,
+ WebkitAppearance: '-apple-pay-button',
+ width: '100%',
+ };
+
+ if ( [ 'black', 'white', 'white-outline' ].includes( theme ) ) {
+ buttonStyle.ApplePayButtonStyle = theme;
+ } else {
+ buttonStyle.ApplePayButtonStyle = 'white';
+ }
+
+ return (
+
+
+
+ );
+};
+
+const ExpressCheckoutButtonPreview = ( {
+ expressPaymentMethod,
+ options,
+ buttonAttributes,
+} ) => {
+ const theme = options.buttonTheme[ expressPaymentMethod ];
+
+ if ( expressPaymentMethod === 'googlePay' ) {
+ return (
+
+ );
+ }
+
+ if ( expressPaymentMethod === 'applePay' ) {
+ return (
+
+ );
+ }
+
+ return null;
+};
+
+export default ExpressCheckoutButtonPreview;
diff --git a/client/tokenized-express-checkout/blocks/components/express-checkout-component.js b/client/tokenized-express-checkout/blocks/components/express-checkout-component.js
index 9975b56d406..b794b083e0b 100644
--- a/client/tokenized-express-checkout/blocks/components/express-checkout-component.js
+++ b/client/tokenized-express-checkout/blocks/components/express-checkout-component.js
@@ -11,6 +11,9 @@ import {
} from '../../event-handlers';
import { useExpressCheckout } from '../hooks/use-express-checkout';
import { PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT } from 'wcpay/checkout/constants';
+import ExpressCheckoutButtonPreview, {
+ SUPPORTED_PREVIEW_PAYMENT_METHODS,
+} from './express-checkout-button-preview';
const getPaymentMethodsOverride = ( enabledPaymentMethod ) => {
const allDisabled = {
@@ -131,16 +134,28 @@ const ExpressCheckoutComponent = ( {
};
};
+ const checkoutElementOptions = {
+ ...withBlockOverride(),
+ ...adjustButtonHeights( withBlockOverride(), expressPaymentMethod ),
+ ...getPaymentMethodsOverride( expressPaymentMethod ),
+ };
+
+ if (
+ isPreview &&
+ SUPPORTED_PREVIEW_PAYMENT_METHODS.includes( expressPaymentMethod )
+ ) {
+ return (
+
+ );
+ }
+
return (
{
+ shippingRates = shippingData.shippingRates[ 0 ].shipping_rates
+ .map( ( rate ) => {
return {
id: rate.rate_id,
amount: parseInt( rate.price, 10 ),
displayName: rate.name,
};
- }
- );
+ } )
+ .slice( 0, SHIPPING_RATES_UPPER_LIMIT_COUNT );
} else {
shippingRates = [
{
diff --git a/client/tokenized-express-checkout/blocks/index.js b/client/tokenized-express-checkout/blocks/index.js
index d2cefd85eb7..b8b8a6f9529 100644
--- a/client/tokenized-express-checkout/blocks/index.js
+++ b/client/tokenized-express-checkout/blocks/index.js
@@ -39,9 +39,7 @@ export const tokenizedExpressCheckoutElementApplePay = ( api ) => ( {
return false;
}
- return new Promise( ( resolve ) => {
- checkPaymentMethodIsAvailable( 'applePay', cart, resolve );
- } );
+ return checkPaymentMethodIsAvailable( 'applePay', cart );
},
} );
@@ -77,9 +75,7 @@ export const tokenizedExpressCheckoutElementGooglePay = ( api ) => {
return false;
}
- return new Promise( ( resolve ) => {
- checkPaymentMethodIsAvailable( 'googlePay', cart, resolve );
- } );
+ return checkPaymentMethodIsAvailable( 'googlePay', cart );
},
};
};
diff --git a/client/tokenized-express-checkout/compatibility/wc-product-page.js b/client/tokenized-express-checkout/compatibility/wc-product-page.js
index 001b233da30..2caa11ba641 100644
--- a/client/tokenized-express-checkout/compatibility/wc-product-page.js
+++ b/client/tokenized-express-checkout/compatibility/wc-product-page.js
@@ -9,6 +9,7 @@ import debounce from '../debounce';
* External dependencies
*/
import { addFilter, doAction } from '@wordpress/hooks';
+import { getExpressCheckoutData } from 'wcpay/tokenized-express-checkout/utils';
jQuery( ( $ ) => {
$( document.body ).on( 'woocommerce_variation_has_changed', async () => {
@@ -19,6 +20,10 @@ jQuery( ( $ ) => {
// Block the payment request button as soon as an "input" event is fired, to avoid sync issues
// when the customer clicks on the button before the debounced event is processed.
jQuery( ( $ ) => {
+ if ( getExpressCheckoutData( 'button_context' ) !== 'product' ) {
+ return;
+ }
+
const $quantityInput = $( '.quantity' );
const handleQuantityChange = () => {
expressCheckoutButtonUi.blockButton();
diff --git a/client/tokenized-express-checkout/constants.js b/client/tokenized-express-checkout/constants.js
new file mode 100644
index 00000000000..6c58c715386
--- /dev/null
+++ b/client/tokenized-express-checkout/constants.js
@@ -0,0 +1,4 @@
+// This const defines the max number of shipping options that can be handled by the ECE.
+// More than 9 options will prevent the UI from behaving correctly.
+// See https://github.com/Automattic/woocommerce-payments/issues/10490 .
+export const SHIPPING_RATES_UPPER_LIMIT_COUNT = 9;
diff --git a/client/tokenized-express-checkout/event-handlers.js b/client/tokenized-express-checkout/event-handlers.js
index 1ef908bfc79..84fcdd77b6d 100644
--- a/client/tokenized-express-checkout/event-handlers.js
+++ b/client/tokenized-express-checkout/event-handlers.js
@@ -65,7 +65,7 @@ export const shippingAddressChangeHandler = async ( event, elements ) => {
} );
event.resolve( {
- shippingRates: transformCartDataForShippingRates( cartData ),
+ shippingRates,
lineItems: transformCartDataForDisplayItems( cartData ),
} );
} catch ( error ) {
diff --git a/client/tokenized-express-checkout/index.js b/client/tokenized-express-checkout/index.js
index 3c34201daa7..10d175b6ba1 100644
--- a/client/tokenized-express-checkout/index.js
+++ b/client/tokenized-express-checkout/index.js
@@ -64,25 +64,60 @@ const fetchNewCartData = async () => {
return cartData;
};
-const getServerSideExpressCheckoutProductData = () => {
- const displayItems = (
- getExpressCheckoutData( 'product' )?.displayItems ?? []
- ).map( ( { label, amount } ) => ( {
- name: label,
- amount,
- } ) );
-
- return {
- total: getExpressCheckoutData( 'product' )?.total.amount,
- currency: getExpressCheckoutData( 'product' )?.currency,
- requestShipping:
- getExpressCheckoutData( 'product' )?.needs_shipping ?? false,
- requestPhone:
- getExpressCheckoutData( 'checkout' )?.needs_payer_phone ?? false,
- displayItems,
- };
+const getTotalAmount = () => {
+ if ( cachedCartData ) {
+ return transformPrice(
+ parseInt( cachedCartData.totals.total_price, 10 ) -
+ parseInt( cachedCartData.totals.total_refund || 0, 10 ),
+ cachedCartData.totals
+ );
+ }
+
+ if (
+ getExpressCheckoutData( 'button_context' ) === 'product' &&
+ getExpressCheckoutData( 'product' )
+ ) {
+ return getExpressCheckoutData( 'product' )?.total.amount;
+ }
};
+const getOnClickOptions = () => {
+ if ( cachedCartData ) {
+ return {
+ // pay-for-order should never display the shipping selection.
+ shippingAddressRequired:
+ getExpressCheckoutData( 'button_context' ) !==
+ 'pay_for_order' && cachedCartData.needs_shipping,
+ shippingRates: transformCartDataForShippingRates( cachedCartData ),
+ phoneNumberRequired:
+ getExpressCheckoutData( 'checkout' )?.needs_payer_phone ??
+ false,
+ lineItems: transformCartDataForDisplayItems( cachedCartData ),
+ };
+ }
+
+ if (
+ getExpressCheckoutData( 'button_context' ) === 'product' &&
+ getExpressCheckoutData( 'product' )
+ ) {
+ return {
+ shippingAddressRequired:
+ getExpressCheckoutData( 'product' )?.needs_shipping ?? false,
+ phoneNumberRequired:
+ getExpressCheckoutData( 'checkout' )?.needs_payer_phone ??
+ false,
+ lineItems: (
+ getExpressCheckoutData( 'product' )?.displayItems ?? []
+ ).map( ( { label, amount } ) => ( {
+ name: label,
+ amount,
+ } ) ),
+ };
+ }
+};
+
+let elements;
+
jQuery( ( $ ) => {
// Don't load if blocks checkout is being loaded.
if (
@@ -173,15 +208,16 @@ jQuery( ( $ ) => {
/**
* Starts the Express Checkout Element
*
- * @param {Object} options ECE options.
+ * @param {Object} creationOptions ECE initialization options.
*/
- startExpressCheckoutElement: async ( options ) => {
+ startExpressCheckoutElement: async ( creationOptions ) => {
let addToCartPromise = Promise.resolve();
const stripe = await api.getStripe();
- const elements = stripe.elements( {
+ // https://docs.stripe.com/js/elements_object/create_without_intent
+ elements = stripe.elements( {
mode: 'payment',
- amount: options.total,
- currency: options.currency,
+ amount: creationOptions.total,
+ currency: creationOptions.currency,
paymentMethodCreation: 'manual',
appearance: getExpressCheckoutButtonAppearance(),
locale: getExpressCheckoutData( 'stripe' )?.locale ?? 'en',
@@ -247,9 +283,12 @@ jQuery( ( $ ) => {
} );
}
+ const options = getOnClickOptions();
const shippingOptionsWithFallback =
- ! options.shippingRates || // server-side data on the product page initialization doesn't provide any shipping rates.
- options.shippingRates.length === 0 // but it can also happen that there are no rates in the array.
+ // server-side data on the product page initialization doesn't provide any shipping rates.
+ ! options.shippingRates ||
+ // but it can also happen that there are no rates in the array.
+ options.shippingRates.length === 0
? [
// fallback for initialization (and initialization _only_), before an address is provided by the ECE.
{
@@ -263,8 +302,9 @@ jQuery( ( $ ) => {
]
: options.shippingRates;
- const clickOptions = {
- // `options.displayItems`, `options.requestShipping`, `options.requestPhone`, `options.shippingRates`,
+ onClickHandler( event );
+ event.resolve( {
+ // `options.displayItems`, `options.shippingAddressRequired`, `options.requestPhone`, `options.shippingRates`,
// are all coming from prior of the initialization.
// The "real" values will be updated once the button loads.
// They are preemptively initialized because the `event.resolve({})`
@@ -272,20 +312,15 @@ jQuery( ( $ ) => {
business: {
name: getExpressCheckoutData( 'store_name' ),
},
- lineItems: options.displayItems,
emailRequired: true,
- shippingAddressRequired: options.requestShipping,
- phoneNumberRequired: options.requestPhone,
- shippingRates: options.requestShipping
+ ...options,
+ shippingRates: options.shippingAddressRequired
? shippingOptionsWithFallback
: undefined,
allowedShippingCountries: getExpressCheckoutData(
'checkout'
).allowed_shipping_countries,
- };
-
- onClickHandler( event );
- event.resolve( clickOptions );
+ } );
} );
eceButton.on( 'shippingaddresschange', async ( event ) => {
@@ -333,54 +368,17 @@ jQuery( ( $ ) => {
expressCheckoutButtonUi.getButtonSeparator().show();
}
} );
-
- removeAction(
- 'wcpay.express-checkout.update-button-data',
- 'automattic/wcpay/express-checkout'
- );
- addAction(
- 'wcpay.express-checkout.update-button-data',
- 'automattic/wcpay/express-checkout',
- async () => {
- // if the product cannot be added to cart (because of missing variation selection, etc),
- // don't try to add it to the cart to get new data - the call will likely fail.
- if (
- getExpressCheckoutData( 'button_context' ) === 'product'
- ) {
- const addToCartButton = $(
- '.single_add_to_cart_button'
- );
-
- // First check if product can be added to cart.
- if ( addToCartButton.is( '.disabled' ) ) {
- return;
- }
- }
-
- try {
- expressCheckoutButtonUi.blockButton();
-
- cachedCartData = await fetchNewCartData();
-
- // We need to re init the payment request button to ensure the shipping options & taxes are re-fetched.
- // The cachedCartData from the Store API will be used from now on,
- // instead of the `product` attributes.
- wcpayExpressCheckoutParams.product = null;
-
- await wcpayECE.init();
-
- expressCheckoutButtonUi.unblockButton();
- } catch ( e ) {
- expressCheckoutButtonUi.hideContainer();
- }
- }
- );
},
/**
* Initialize event handlers and UI state
*/
init: async () => {
+ removeAction(
+ 'wcpay.express-checkout.update-button-data',
+ 'automattic/wcpay/express-checkout'
+ );
+
// on product pages, we should be able to have `getExpressCheckoutData( 'product' )` from the backend,
// which saves us some AJAX calls.
if (
@@ -394,9 +392,7 @@ jQuery( ( $ ) => {
if ( ! getExpressCheckoutData( 'product' ) && ! cachedCartData ) {
try {
cachedCartData = await fetchNewCartData();
- } catch ( e ) {
- // if something fails here, we can likely fall back on `getExpressCheckoutData( 'product' )`.
- }
+ } catch ( e ) {}
}
// once (and if) cart data has been fetched, we can safely clear product data from the backend.
@@ -411,48 +407,84 @@ jQuery( ( $ ) => {
getCartApiHandler().useSeparateCart();
}
- if ( cachedCartData ) {
+ const total = getTotalAmount();
+ if ( total === 0 ) {
+ expressCheckoutButtonUi.hideContainer();
+ expressCheckoutButtonUi.getButtonSeparator().hide();
+ } else if ( cachedCartData ) {
// If this is the cart page, or checkout page, or pay-for-order page, we need to request the cart details.
// but if the data is not available, we can't render the button.
- const total = transformPrice(
- parseInt( cachedCartData.totals.total_price, 10 ) -
- parseInt( cachedCartData.totals.total_refund || 0, 10 ),
- cachedCartData.totals
- );
- if ( total === 0 ) {
- expressCheckoutButtonUi.hideContainer();
- expressCheckoutButtonUi.getButtonSeparator().hide();
- } else {
- await wcpayECE.startExpressCheckoutElement( {
- total,
- currency: cachedCartData.totals.currency_code.toLowerCase(),
- // pay-for-order should never display the shipping selection.
- requestShipping:
- getExpressCheckoutData( 'button_context' ) !==
- 'pay_for_order' &&
- cachedCartData.needs_shipping,
- shippingRates: transformCartDataForShippingRates(
- cachedCartData
- ),
- requestPhone:
- getExpressCheckoutData( 'checkout' )
- ?.needs_payer_phone ?? false,
- displayItems: transformCartDataForDisplayItems(
- cachedCartData
- ),
- } );
- }
+ await wcpayECE.startExpressCheckoutElement( {
+ total,
+ currency: cachedCartData.totals.currency_code.toLowerCase(),
+ } );
} else if (
getExpressCheckoutData( 'button_context' ) === 'product' &&
getExpressCheckoutData( 'product' )
) {
- await wcpayECE.startExpressCheckoutElement(
- getServerSideExpressCheckoutProductData()
- );
+ await wcpayECE.startExpressCheckoutElement( {
+ total,
+ currency: getExpressCheckoutData( 'product' )?.currency,
+ } );
} else {
expressCheckoutButtonUi.hideContainer();
expressCheckoutButtonUi.getButtonSeparator().hide();
}
+
+ addAction(
+ 'wcpay.express-checkout.update-button-data',
+ 'automattic/wcpay/express-checkout',
+ async () => {
+ // if the product cannot be added to cart (because of missing variation selection, etc),
+ // don't try to add it to the cart to get new data - the call will likely fail.
+ if (
+ getExpressCheckoutData( 'button_context' ) === 'product'
+ ) {
+ const addToCartButton = $(
+ '.single_add_to_cart_button'
+ );
+
+ // First check if product can be added to cart.
+ if ( addToCartButton.is( '.disabled' ) ) {
+ return;
+ }
+ }
+
+ try {
+ expressCheckoutButtonUi.blockButton();
+
+ const prevTotal = getTotalAmount();
+
+ cachedCartData = await fetchNewCartData();
+
+ // We need to re init the payment request button to ensure the shipping options & taxes are re-fetched.
+ // The cachedCartData from the Store API will be used from now on,
+ // instead of the `product` attributes.
+ wcpayExpressCheckoutParams.product = null;
+
+ expressCheckoutButtonUi.unblockButton();
+
+ // since the "total" is part of the initialization of the Stripe elements (and not part of the ECE button),
+ // if the totals change, we might need to update it on the element itself.
+ const newTotal = getTotalAmount();
+ if ( ! elements ) {
+ wcpayECE.init();
+ } else if ( newTotal !== prevTotal && newTotal > 0 ) {
+ elements.update( { amount: newTotal } );
+ }
+
+ if ( newTotal === 0 ) {
+ expressCheckoutButtonUi.hideContainer();
+ expressCheckoutButtonUi.getButtonSeparator().hide();
+ } else {
+ expressCheckoutButtonUi.showContainer();
+ expressCheckoutButtonUi.getButtonSeparator().show();
+ }
+ } catch ( e ) {
+ expressCheckoutButtonUi.hideContainer();
+ }
+ }
+ );
},
};
diff --git a/client/tokenized-express-checkout/transformers/__tests__/wc-to-stripe.test.js b/client/tokenized-express-checkout/transformers/__tests__/wc-to-stripe.test.js
index 4af29a32003..53ca554711c 100644
--- a/client/tokenized-express-checkout/transformers/__tests__/wc-to-stripe.test.js
+++ b/client/tokenized-express-checkout/transformers/__tests__/wc-to-stripe.test.js
@@ -582,6 +582,318 @@ describe( 'wc-to-stripe transformers', () => {
] );
} );
+ it( 'does not return more than 9 items', () => {
+ const rates = transformCartDataForShippingRates( {
+ shipping_rates: [
+ {
+ package_id: 0,
+ name: 'Shipment 1',
+ destination: {},
+ items: [
+ {
+ key: 'aab3238922bcc25a6f606eb525ffdc56',
+ name: 'Beanie',
+ quantity: 1,
+ },
+ ],
+ shipping_rates: [
+ {
+ rate_id: 'flat_rate:5',
+ name: 'Express shipping',
+ description: '',
+ delivery_time: '',
+ price: '1170',
+ taxes: '97',
+ instance_id: 5,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:10',
+ name: 'Flat rate 1',
+ description: '',
+ delivery_time: '',
+ price: '100',
+ taxes: '8',
+ instance_id: 10,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:11',
+ name: 'Flat rate 2',
+ description: '',
+ delivery_time: '',
+ price: '200',
+ taxes: '17',
+ instance_id: 11,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:12',
+ name: 'Flat rate 3',
+ description: '',
+ delivery_time: '',
+ price: '300',
+ taxes: '25',
+ instance_id: 12,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:13',
+ name: 'Flat rate 4',
+ description: '',
+ delivery_time: '',
+ price: '400',
+ taxes: '33',
+ instance_id: 13,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:14',
+ name: 'Flat rate 5',
+ description: '',
+ delivery_time: '',
+ price: '500',
+ taxes: '41',
+ instance_id: 14,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:15',
+ name: 'Flat rate 6',
+ description: '',
+ delivery_time: '',
+ price: '600',
+ taxes: '50',
+ instance_id: 15,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:16',
+ name: 'Flat rate 7',
+ description: '',
+ delivery_time: '',
+ price: '700',
+ taxes: '58',
+ instance_id: 16,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:17',
+ name: 'Flat rate 8',
+ description: '',
+ delivery_time: '',
+ price: '800',
+ taxes: '66',
+ instance_id: 17,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'flat_rate:18',
+ name: 'Flat rate 9',
+ description: '',
+ delivery_time: '',
+ price: '900',
+ taxes: '74',
+ instance_id: 18,
+ method_id: 'flat_rate',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'pickup_location:0',
+ name: '(Palo Alto, CA)',
+ description: '',
+ delivery_time: '',
+ price: '100',
+ taxes: '8',
+ instance_id: 0,
+ method_id: 'pickup_location',
+ selected: false,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ {
+ rate_id: 'free_shipping:1',
+ name: 'Free shipping',
+ description: '',
+ delivery_time: '',
+ price: '0',
+ taxes: '0',
+ instance_id: 1,
+ method_id: 'free_shipping',
+ meta_data: [
+ {
+ key: 'Items',
+ value: 'Beanie × 1',
+ },
+ ],
+ selected: true,
+ currency_code: 'USD',
+ currency_symbol: '$',
+ currency_minor_unit: 2,
+ currency_decimal_separator: '.',
+ currency_thousand_separator: ',',
+ currency_prefix: '$',
+ currency_suffix: '',
+ },
+ ],
+ },
+ ],
+ } );
+
+ expect( rates ).toHaveLength( 9 );
+ // let's also ensure it contains the "selected" rate.
+ expect( rates ).toContainEqual( {
+ amount: 0,
+ deliveryEstimate: '',
+ id: 'free_shipping:1',
+ displayName: 'Free shipping',
+ } );
+ } );
+
it( 'transforms shipping options for local pickup', () => {
expect(
transformCartDataForShippingRates( {
diff --git a/client/tokenized-express-checkout/transformers/wc-to-stripe.js b/client/tokenized-express-checkout/transformers/wc-to-stripe.js
index b46b766fd2f..a748a99d628 100644
--- a/client/tokenized-express-checkout/transformers/wc-to-stripe.js
+++ b/client/tokenized-express-checkout/transformers/wc-to-stripe.js
@@ -9,6 +9,7 @@ import { decodeEntities } from '@wordpress/html-entities';
*/
import { getExpressCheckoutData } from '../utils';
import { applyFilters } from '@wordpress/hooks';
+import { SHIPPING_RATES_UPPER_LIMIT_COUNT } from 'wcpay/tokenized-express-checkout/constants';
/**
* GooglePay/ApplePay expect the prices to be formatted in cents.
@@ -140,6 +141,7 @@ export const transformCartDataForShippingRates = ( cartData ) =>
return rateA.selected ? -1 : 1; // Objects with 'selected: true' come first
} )
+ .slice( 0, SHIPPING_RATES_UPPER_LIMIT_COUNT )
.map( ( rate ) => ( {
id: rate.rate_id,
displayName: decodeEntities( rate.name ),
diff --git a/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js b/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js
index b592169da22..5beb7e32942 100644
--- a/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js
+++ b/client/tokenized-express-checkout/utils/checkPaymentMethodIsAvailable.js
@@ -14,71 +14,75 @@ import WCPayAPI from 'wcpay/checkout/api';
import { getUPEConfig } from 'wcpay/utils/checkout';
export const checkPaymentMethodIsAvailable = memoize(
- ( paymentMethod, cart, resolve ) => {
- // Create the DIV container on the fly
- const containerEl = document.createElement( 'div' );
+ ( paymentMethod, cart ) => {
+ return new Promise( ( resolve ) => {
+ // Create the DIV container on the fly
+ const containerEl = document.createElement( 'div' );
- // Ensure the element is hidden and doesn’t interfere with the page layout.
- containerEl.style.display = 'none';
+ // Ensure the element is hidden and doesn’t interfere with the page layout.
+ containerEl.style.display = 'none';
- document.querySelector( 'body' ).appendChild( containerEl );
+ document.querySelector( 'body' ).appendChild( containerEl );
- const root = ReactDOM.createRoot( containerEl );
+ const root = ReactDOM.createRoot( containerEl );
- const api = new WCPayAPI(
- {
- publishableKey: getUPEConfig( 'publishableKey' ),
- accountId: getUPEConfig( 'accountId' ),
- forceNetworkSavedCards: getUPEConfig(
- 'forceNetworkSavedCards'
- ),
- locale: getUPEConfig( 'locale' ),
- isStripeLinkEnabled: isLinkEnabled(
- getUPEConfig( 'paymentMethodsConfig' )
- ),
- },
- request
- );
+ const api = new WCPayAPI(
+ {
+ publishableKey: getUPEConfig( 'publishableKey' ),
+ accountId: getUPEConfig( 'accountId' ),
+ forceNetworkSavedCards: getUPEConfig(
+ 'forceNetworkSavedCards'
+ ),
+ locale: getUPEConfig( 'locale' ),
+ isStripeLinkEnabled: isLinkEnabled(
+ getUPEConfig( 'paymentMethodsConfig' )
+ ),
+ },
+ request
+ );
- root.render(
-
- resolve( false ) }
+ root.render(
+ {
- let canMakePayment = false;
- if ( event.availablePaymentMethods ) {
- canMakePayment =
- event.availablePaymentMethods[ paymentMethod ];
- }
- resolve( canMakePayment );
- root.unmount();
- containerEl.remove();
- } }
- />
-
- );
+ >
+ resolve( false ) }
+ options={ {
+ paymentMethods: {
+ amazonPay: 'never',
+ applePay:
+ paymentMethod === 'applePay'
+ ? 'always'
+ : 'never',
+ googlePay:
+ paymentMethod === 'googlePay'
+ ? 'always'
+ : 'never',
+ link: 'never',
+ paypal: 'never',
+ },
+ } }
+ onReady={ ( event ) => {
+ let canMakePayment = false;
+ if ( event.availablePaymentMethods ) {
+ canMakePayment =
+ event.availablePaymentMethods[
+ paymentMethod
+ ];
+ }
+ resolve( canMakePayment );
+ root.unmount();
+ containerEl.remove();
+ } }
+ />
+
+ );
+ } );
}
);
diff --git a/client/tracks/event.d.ts b/client/tracks/event.d.ts
index 39673fef900..aed5c150717 100644
--- a/client/tracks/event.d.ts
+++ b/client/tracks/event.d.ts
@@ -51,6 +51,17 @@ export type Event =
| 'wcpay_inbox_action_dismissed'
| 'wcpay_inbox_action_click'
| 'wcpay_inbox_note_view'
+ | 'wcpay_merchant_feedback_prompt_view'
+ | 'wcpay_merchant_feedback_prompt_dismiss'
+ | 'wcpay_merchant_feedback_prompt_yes_click'
+ | 'wcpay_merchant_feedback_prompt_no_click'
+ | 'wcpay_merchant_feedback_prompt_positive_modal_view'
+ | 'wcpay_merchant_feedback_prompt_positive_modal_leave_review_click'
+ | 'wcpay_merchant_feedback_prompt_positive_modal_close_click'
+ | 'wcpay_merchant_feedback_prompt_negative_modal_view'
+ | 'wcpay_merchant_feedback_prompt_negative_feedback'
+ | 'wcpay_merchant_feedback_prompt_negative_modal_contact_support_click'
+ | 'wcpay_merchant_feedback_prompt_negative_modal_close_click'
| 'wcpay_onboarding_flow_started'
| 'wcpay_onboarding_flow_step_completed'
| 'wcpay_onboarding_flow_hidden'
diff --git a/client/transactions/filters/config.ts b/client/transactions/filters/config.ts
index 8485dbe771d..45a53ca2859 100644
--- a/client/transactions/filters/config.ts
+++ b/client/transactions/filters/config.ts
@@ -442,43 +442,46 @@ export const getAdvancedFilters = (
},
channel: {
labels: {
- add: __( 'Channel', 'woocommerce-payments' ),
+ add: __( 'Sales channel', 'woocommerce-payments' ),
remove: __(
- 'Remove transaction channel filter',
+ 'Remove transaction sales channel filter',
'woocommerce-payments'
),
rule: __(
- 'Select a transaction channel filter match',
+ 'Select a transaction sales channel filter match',
'woocommerce-payments'
),
- /* translators: A sentence describing a Transaction Channel filter. */
title:
wooCommerceVersion < 7.8
? __(
- '{{title}}Channel{{/title}} {{rule /}} {{filter /}}',
+ '{{title}}Sales channel{{/title}} {{rule /}} {{filter /}}',
'woocommerce-payments'
)
: __(
- 'Channel ',
+ 'Sales channel ',
'woocommerce-payments'
),
filter: __(
- 'Select a transaction channel',
+ 'Select a transaction sales channel',
'woocommerce-payments'
),
},
rules: [
{
value: 'is',
- /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction channel type. */
- label: _x( 'Is', 'Channel', 'woocommerce-payments' ),
+ /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction sales channel type. */
+ label: _x(
+ 'Is',
+ 'Sales channel',
+ 'woocommerce-payments'
+ ),
},
{
value: 'is_not',
- /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction channel type. */
+ /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction sales channel type. */
label: _x(
'Is not',
- 'Channel',
+ 'Sales channel',
'woocommerce-payments'
),
},
diff --git a/client/transactions/filters/test/__snapshots__/index.tsx.snap b/client/transactions/filters/test/__snapshots__/index.tsx.snap
index 7feb3c5bb55..61210139d09 100644
--- a/client/transactions/filters/test/__snapshots__/index.tsx.snap
+++ b/client/transactions/filters/test/__snapshots__/index.tsx.snap
@@ -5,13 +5,18 @@ HTMLOptionsCollection [
,
,
+ ,
]
`;
diff --git a/client/transactions/filters/test/index.tsx b/client/transactions/filters/test/index.tsx
index a958629edca..cd5779f3744 100644
--- a/client/transactions/filters/test/index.tsx
+++ b/client/transactions/filters/test/index.tsx
@@ -356,15 +356,15 @@ describe( 'Transactions filters', () => {
let ruleSelector: HTMLElement;
beforeEach( () => {
- addAdvancedFilter( 'Channel' );
+ addAdvancedFilter( 'Sales channel' );
ruleSelector = screen.getByRole( 'combobox', {
- name: /transaction channel filter/i,
+ name: /transaction sales channel filter/i,
} );
} );
test( 'should render all types', () => {
const typeSelect = screen.getByRole( 'combobox', {
- name: /transaction channel$/i,
+ name: /transaction sales channel$/i,
} ) as HTMLSelectElement;
expect( typeSelect.options ).toMatchSnapshot();
} );
@@ -372,10 +372,9 @@ describe( 'Transactions filters', () => {
test( 'should filter by is', () => {
user.selectOptions( ruleSelector, 'is' );
- // need to include $ in name, otherwise "Select a transaction type filter" is also matched.
user.selectOptions(
screen.getByRole( 'combobox', {
- name: /transaction channel$/i,
+ name: /transaction sales channel$/i,
} ),
'online'
);
@@ -390,7 +389,7 @@ describe( 'Transactions filters', () => {
// need to include $ in name, otherwise "Select a transaction type filter" is also matched.
user.selectOptions(
screen.getByRole( 'combobox', {
- name: /transaction channel$/i,
+ name: /transaction sales channel$/i,
} ),
'in_person'
);
diff --git a/client/transactions/index.tsx b/client/transactions/index.tsx
index 507a972cd5a..eb8ff012336 100644
--- a/client/transactions/index.tsx
+++ b/client/transactions/index.tsx
@@ -23,6 +23,7 @@ import {
} from 'wcpay/data';
import WCPaySettingsContext from '../settings/wcpay-settings-context';
import BlockedList from './blocked';
+import { MaybeShowMerchantFeedbackPrompt } from 'wcpay/merchant-feedback-prompt';
declare const window: any;
@@ -106,6 +107,7 @@ export const TransactionsPage: React.FC = () => {
return (
+
- Channel
+ Sales channel
- Channel
+ Sales channel
- Online
+ Online store
|
- 123
+ custom-123
|
- Online
+ Online store
|
- 125
+ custom-125
|
- 335
+ custom-335
|
- Channel
+ Sales channel
- Channel
+ Sales channel
|
- Online
+ Online store
|
- 123
+ custom-123
|
- Online
+ Online store
|
- 125
+ custom-125
|
- 335
+ custom-335
|
- Channel
+ Sales channel
- Channel
+ Sales channel
|
- Online
+ Online store
|
- 125
+ custom-125
|
- Channel
+ Sales channel
- Channel
+ Sales channel
|
- Online
+ Online store
|
- 123
+ custom-123
|
- 246
+ custom-246
|
- Online
+ Online store
|
- 125
+ custom-125
|
- 335
+ custom-335
|
- Channel
+ Sales channel
- Channel
+ Sales channel
|
- Online
+ Online store
|
- 123
+ custom-123
|
- Online
+ Online store
|
- 125
+ custom-125
|
- 335
+ custom-335
|
- Channel
+ Sales channel
- Channel
+ Sales channel
|
- Online
+ Online store
|
- 123
+ custom-123
|
- Online
+ Online store
|
- 125
+ custom-125
|
- 335
+ custom-335
|
Transaction[] = () => [
type: 'refund',
source: 'visa',
order: {
- number: 123,
+ id: 123,
+ number: 'custom-123',
url: 'https://example.com/order/123',
// eslint-disable-next-line camelcase
customer_url: 'https://example.com/customer/my-name',
@@ -139,7 +140,8 @@ const getMockTransactions: () => Transaction[] = () => [
type: 'charge',
source: 'mastercard',
order: {
- number: 125,
+ id: 123,
+ number: 'custom-125',
url: 'https://example.com/order/125',
// eslint-disable-next-line camelcase
customer_url: 'https://example.com/customer/my-name',
@@ -170,7 +172,8 @@ const getMockTransactions: () => Transaction[] = () => [
type: 'charge',
source: 'visa',
order: {
- number: 335,
+ id: 123,
+ number: 'custom-335',
url: 'https://example.com/order/335',
// eslint-disable-next-line camelcase
customer_url: 'https://example.com/customer/my-name',
@@ -424,7 +427,7 @@ describe( 'Transactions list', () => {
const mockTransactions = getMockTransactions();
mockTransactions[ 0 ].order.subscriptions = [
{
- number: 246,
+ number: 'custom-246',
url: 'https://example.com/subscription/246',
},
];
diff --git a/client/transactions/strings.ts b/client/transactions/strings.ts
index ac90fb7a9ca..f1ad67e21c7 100644
--- a/client/transactions/strings.ts
+++ b/client/transactions/strings.ts
@@ -31,8 +31,9 @@ export const sourceDevice = {
// Mapping of transaction channel type string.
export const channel = {
- online: __( 'Online', 'woocommerce-payments' ),
+ online: __( 'Online store', 'woocommerce-payments' ),
in_person: __( 'In-Person', 'woocommerce-payments' ),
+ in_person_pos: __( 'In-Person (POS)', 'woocommerce-payments' ),
};
// Mapping of transaction risk level string.
diff --git a/client/types/deposits.d.ts b/client/types/deposits.d.ts
index 9bc5ed4a8f5..6c41a561d87 100644
--- a/client/types/deposits.d.ts
+++ b/client/types/deposits.d.ts
@@ -32,6 +32,8 @@ export interface CachedDeposit {
bankAccount: string;
automatic: boolean;
bank_reference_key: string;
+ failure_code: PayoutFailureCode;
+ failure_message: string;
}
export interface DepositsSummaryCache {
@@ -52,3 +54,32 @@ export type DepositStatus =
| 'in_transit'
| 'canceled'
| 'failed';
+
+export type PayoutFailureCode =
+ | 'insufficient_funds'
+ | 'bank_account_restricted'
+ | 'debit_not_authorized'
+ | 'invalid_card'
+ | 'declined'
+ | 'invalid_transaction'
+ | 'refer_to_card_issuer'
+ | 'unsupported_card'
+ | 'lost_or_stolen_card'
+ | 'invalid_issuer'
+ | 'expired_card'
+ | 'could_not_process'
+ | 'invalid_account_number'
+ | 'incorrect_account_holder_name'
+ | 'account_closed'
+ | 'no_account'
+ | 'exceeds_amount_limit'
+ | 'account_frozen'
+ | 'issuer_unavailable'
+ | 'invalid_currency'
+ | 'incorrect_account_type'
+ | 'incorrect_account_holder_details'
+ | 'bank_ownership_changed'
+ | 'exceeds_count_limit'
+ | 'incorrect_account_holder_address'
+ | 'incorrect_account_holder_tax_id'
+ | 'invalid_account_number_length';
diff --git a/client/types/orders.d.ts b/client/types/orders.d.ts
index 1865fd23c5a..bb67aa4efe9 100644
--- a/client/types/orders.d.ts
+++ b/client/types/orders.d.ts
@@ -7,12 +7,18 @@
*/
interface SubscriptionDetails {
- number: number;
+ number: string; // Comment for OderDetails.number below applies here as well.
url: string;
}
interface OrderDetails {
- number: number;
+ id: number;
+ /**
+ * The order number for display.
+ * By default, it's order ID but a plugin can customize it.
+ * See PHP method WC_Order::get_order_number().
+ */
+ number: string;
url: string;
customer_url: null | string;
customer_email: null | string;
diff --git a/client/types/payment-methods.d.ts b/client/types/payment-methods.d.ts
index 064fc1a431b..350659ee2f5 100644
--- a/client/types/payment-methods.d.ts
+++ b/client/types/payment-methods.d.ts
@@ -20,6 +20,16 @@ export type PaymentMethod =
| 'ideal'
| 'p24'
| 'sepa_debit'
- | 'sofort'
- | 'affirm'
- | 'afterpay_clearpay';
+ | 'sofort';
+
+export interface PaymentMethodMapEntry {
+ id: string;
+ label: string;
+ description: string;
+ icon: ReactImgFuncComponent;
+ currencies: string[];
+ stripe_key: string;
+ allows_manual_capture: boolean;
+ allows_pay_later: boolean;
+ accepts_only_domestic_payment: boolean;
+}
diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx
index 5bbe260b5f3..7346b408e1f 100644
--- a/client/utils/account-fees.tsx
+++ b/client/utils/account-fees.tsx
@@ -114,7 +114,7 @@ export const getCurrentBaseFee = (
};
export const formatMethodFeesTooltip = (
- accountFees: FeeStructure
+ accountFees?: FeeStructure
): JSX.Element => {
if ( ! accountFees ) return <>>;
diff --git a/client/utils/charge/index.ts b/client/utils/charge/index.ts
index 0169a45b4e0..ee87ac6428f 100755
--- a/client/utils/charge/index.ts
+++ b/client/utils/charge/index.ts
@@ -173,14 +173,14 @@ export const getChargeAmounts = ( charge: Charge ): ChargeAmounts => {
};
/**
- * Displays the transaction's channel: Online | In-Person | In-Person (POS).
+ * Displays the transaction's sales channel: Online store | In-Person | In-Person (POS).
* This method is called on the list of transactions page.
*
* In the list of transactions, the type holds the brand of the payment method, so we aren't passing it.
* Instead, we pass the transaction.channel directly, which might be in_person|in_person_pos|online.
*
* @param {string} channel The transaction channel.
- * @return {string} Online, In-Person, or In-Person (POS).
+ * @return {string} Online store, In-Person, or In-Person (POS).
*/
export const getTransactionChannel = ( channel: string ): string => {
switch ( channel ) {
@@ -189,12 +189,12 @@ export const getTransactionChannel = ( channel: string ): string => {
case 'in_person_pos':
return __( 'In-Person (POS)', 'woocommerce-payments' );
default:
- return __( 'Online', 'woocommerce-payments' );
+ return __( 'Online store', 'woocommerce-payments' );
}
};
/**
- * Displays the channel based on the charge data from Stripe and metadata for a transaction: Online | In-Person | In-Person (POS).
+ * Displays the sales channel based on the charge data from Stripe and metadata for a transaction: Online store | In-Person | In-Person (POS).
* This method is called in the individual transaction page.
*
* In the individual transaction page, we are getting the data from Stripe, so we pass the charge.type
@@ -204,8 +204,7 @@ export const getTransactionChannel = ( channel: string ): string => {
*
* @param {string} type The transaction charge type, which can be card_present or interac_present for In-Person payments.
* @param {Record} metadata The transaction metadata, which may include ipp_channel indicating the channel source.
- * @return {string} Returns 'Online', 'In-Person', or 'In-Person (POS)' based on the transaction type and metadata.
- *
+ * @return {string} Returns 'Online store', 'In-Person', or 'In-Person (POS)' based on the transaction type and metadata.
*/
export const getChargeChannel = (
type: string,
@@ -218,5 +217,5 @@ export const getChargeChannel = (
return __( 'In-Person', 'woocommerce-payments' );
}
- return __( 'Online', 'woocommerce-payments' );
+ return __( 'Online store', 'woocommerce-payments' );
};
diff --git a/client/utils/charge/test/index.js b/client/utils/charge/test/index.js
index 07b7f64f312..a0fbd6b2913 100755
--- a/client/utils/charge/test/index.js
+++ b/client/utils/charge/test/index.js
@@ -499,14 +499,14 @@ describe( 'Charge utilities / get channel string', () => {
expect( result ).toBe( 'In-Person' );
} );
- test( 'should return "Online" for online channel', () => {
+ test( 'should return "Online store" for online channel', () => {
const result = utils.getTransactionChannel( 'online' );
- expect( result ).toBe( 'Online' );
+ expect( result ).toBe( 'Online store' );
} );
- test( 'should return "Online" for null channel', () => {
+ test( 'should return "Online store" for null channel', () => {
const result = utils.getTransactionChannel( null );
- expect( result ).toBe( 'Online' );
+ expect( result ).toBe( 'Online store' );
} );
} );
@@ -530,11 +530,11 @@ describe( 'Charge utilities / get channel string', () => {
expect( result ).toBe( 'In-Person' );
} );
- test( 'should return "Online" for online type', () => {
+ test( 'should return "Online store" for online type', () => {
const result = utils.getChargeChannel( 'online', {
ipp_channel: 'mobile_pos',
} );
- expect( result ).toBe( 'Online' );
+ expect( result ).toBe( 'Online store' );
} );
} );
} );
diff --git a/client/utils/embedded-components/account-session.ts b/client/utils/embedded-components/account-session.ts
deleted file mode 100644
index d5ffbcd99e1..00000000000
--- a/client/utils/embedded-components/account-session.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * External dependencies
- */
-import { useState, useEffect } from 'react';
-import { __ } from '@wordpress/i18n';
-import {
- loadConnectAndInitialize,
- StripeConnectInstance,
-} from '@stripe/connect-js/pure';
-
-/**
- * Internal dependencies
- */
-import { createAccountSession } from 'wcpay/utils/embedded-components/utils';
-
-interface UseAccountSessionProps {
- /**
- * Function to set the load error message.
- */
- setLoadErrorMessage: ( message: string ) => void;
- /**
- * The appearance object.
- */
- appearance: {
- variables: Record< string, any >;
- };
-}
-
-/**
- * This is a custom hook that retrieve the account session data.
- * It returns the StripeConnectInstance object that is used to render the embedded components.
- *
- * If the account session data is not available, it returns null.
- *
- * @return StripeConnectInstance|null
- */
-const useAccountSession = ( {
- setLoadErrorMessage,
- appearance,
-}: UseAccountSessionProps ): StripeConnectInstance | null => {
- const [
- stripeConnectInstance,
- setStripeConnectInstance,
- ] = useState< StripeConnectInstance | null >( null );
-
- useEffect( () => {
- const initializeStripe = async () => {
- try {
- // Fetch account session
- const accountSession = await createAccountSession();
-
- if ( ! accountSession?.clientSecret ) {
- setLoadErrorMessage(
- __(
- "Failed to create account session. Please check that you're using the latest version of WooPayments.",
- 'woocommerce-payments'
- )
- );
- return;
- }
-
- // Initialize Stripe Connect
- const stripeInstance = loadConnectAndInitialize( {
- publishableKey: accountSession.publishableKey,
- fetchClientSecret: async () => accountSession.clientSecret,
- appearance: {
- overlays: 'drawer',
- variables: appearance.variables,
- },
- locale: accountSession.locale.replace( '_', '-' ),
- } );
-
- setStripeConnectInstance( stripeInstance );
- } catch ( error ) {
- setLoadErrorMessage(
- __(
- 'Failed to retrieve account session. Please try again later.',
- 'woocommerce-payments'
- )
- );
- }
- };
-
- initializeStripe();
- }, [ appearance, setLoadErrorMessage ] );
-
- return stripeConnectInstance;
-};
-
-export default useAccountSession;
diff --git a/client/utils/embedded-components/kyc-account-session.ts b/client/utils/embedded-components/kyc-account-session.ts
deleted file mode 100644
index 4c81841c226..00000000000
--- a/client/utils/embedded-components/kyc-account-session.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * External dependencies
- */
-import { useState, useEffect } from 'react';
-import { __ } from '@wordpress/i18n';
-import {
- loadConnectAndInitialize,
- StripeConnectInstance,
-} from '@stripe/connect-js/pure';
-
-/**
- * Internal dependencies
- */
-import { createKycAccountSession, isPoEligible } from 'wcpay/onboarding/utils';
-import { trackRedirected } from 'wcpay/onboarding/tracking';
-
-interface UseKycAccountSessionProps {
- /**
- * The data object from the onboarding context.
- */
- data: Record< string, any >;
- /**
- * Set it to true whenever KYC needs to be resumed.
- */
- continueKyc: boolean;
- /**
- * Function to set the load error message.
- */
- setLoadErrorMessage: ( message: string ) => void;
- /**
- * The appearance object.
- */
- appearance: {
- variables: Record< string, any >;
- };
-}
-
-/**
- * This is a custom hook that retrieve the account session data.
- * It returns the StripeConnectInstance object that is used to render the embedded components.
- *
- * If the account session data is not available, it returns null.
- *
- * @return StripeConnectInstance|null
- */
-const useKycAccountSession = ( {
- data,
- continueKyc,
- setLoadErrorMessage,
- appearance,
-}: UseKycAccountSessionProps ): StripeConnectInstance | null => {
- const [
- stripeConnectInstance,
- setStripeConnectInstance,
- ] = useState< StripeConnectInstance | null >( null );
-
- useEffect( () => {
- const initializeStripe = async () => {
- try {
- // Fetch account session
- const isEligible =
- ! continueKyc && ( await isPoEligible( data ) );
- const accountSession = await createKycAccountSession(
- data,
- isEligible
- );
-
- if ( ! accountSession?.clientSecret ) {
- setLoadErrorMessage(
- __(
- "Failed to create account session. Please check that you're using the latest version of WooPayments.",
- 'woocommerce-payments'
- )
- );
- return;
- }
-
- trackRedirected( isEligible, true );
-
- // Initialize Stripe Connect
- const stripeInstance = loadConnectAndInitialize( {
- publishableKey: accountSession.publishableKey,
- fetchClientSecret: async () => accountSession.clientSecret,
- appearance: {
- overlays: 'drawer',
- variables: appearance.variables,
- },
- locale: accountSession.locale.replace( '_', '-' ),
- } );
-
- setStripeConnectInstance( stripeInstance );
- } catch ( error ) {
- setLoadErrorMessage(
- __(
- 'Failed to retrieve account session. Please try again later.',
- 'woocommerce-payments'
- )
- );
- }
- };
-
- initializeStripe();
- }, [ data, continueKyc, appearance, setLoadErrorMessage ] );
-
- return stripeConnectInstance;
-};
-
-export default useKycAccountSession;
diff --git a/client/utils/embedded-components/utils.ts b/client/utils/embedded-components/utils.ts
deleted file mode 100644
index af87f26f5e5..00000000000
--- a/client/utils/embedded-components/utils.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * External dependencies
- */
-import { addQueryArgs } from '@wordpress/url';
-import apiFetch from '@wordpress/api-fetch';
-
-/**
- * Internal dependencies
- */
-import { NAMESPACE } from 'data/constants';
-import { AccountKycSession } from '../../onboarding/types';
-
-/**
- * Make an API request to create an account session.
- */
-export const createAccountSession = async (): Promise< AccountKycSession > => {
- return await apiFetch< AccountKycSession >( {
- path: addQueryArgs( `${ NAMESPACE }/accounts/session`, {} ),
- method: 'GET',
- } );
-};
diff --git a/client/utils/format-ssr.js b/client/utils/format-ssr.js
deleted file mode 100644
index 47e0bf90d6f..00000000000
--- a/client/utils/format-ssr.js
+++ /dev/null
@@ -1,334 +0,0 @@
-/** @format */
-
-/**
- * External dependencies
- */
-import { dateI18n } from '@wordpress/date';
-import { decodeEntities } from '@wordpress/html-entities';
-
-const CHECK_MARK = String.fromCharCode( 10004 ); // ✔
-const CROSS_MARK = String.fromCharCode( 10060 ); // ❌
-const DASH_MARK = '–';
-
-export const formatSsr = ( systemStatus, wcPayData ) => {
- const ssr = `### WordPress Environment ###
-
-WordPress address (URL): ${ systemStatus.environment.site_url }
-Site address (URL): ${ systemStatus.environment.home_url }
-WC Version: ${ systemStatus.environment.version }
-Log Directory Writable: ${
- systemStatus.environment.log_directory_writable
- ? CHECK_MARK
- : CROSS_MARK
- }
-WP Version: ${ systemStatus.environment.wp_version }
-WP Multisite: ${
- systemStatus.environment.wp_multisite ? CHECK_MARK : DASH_MARK
- }
-WP Memory Limit: ${ formatSize( systemStatus.environment.wp_memory_limit ) }
-WP Debug Mode: ${
- systemStatus.environment.wp_debug_mode ? CHECK_MARK : DASH_MARK
- }
-WP Cron: ${ systemStatus.environment.wp_cron ? CHECK_MARK : DASH_MARK }
-Language: ${ systemStatus.environment.language }
-External object cache: ${
- systemStatus.environment.external_object_cache ? CHECK_MARK : DASH_MARK
- }
-
-### Server Environment ###
-
-Server Info: ${ systemStatus.environment.server_info }
-PHP Version: ${ systemStatus.environment.php_version }
-PHP Post Max Size: ${ formatSize( systemStatus.environment.php_post_max_size ) }
-PHP Time Limit: ${ systemStatus.environment.php_max_execution_time }
-PHP Max Input Vars: ${ systemStatus.environment.php_max_input_vars }
-cURL Version: ${ systemStatus.environment.curl_version }
-
-SUHOSIN Installed: ${
- systemStatus.environment.suhosin_installed ? CHECK_MARK : DASH_MARK
- }
-MySQL Version: ${ systemStatus.environment.mysql_version_string }
-Max Upload Size: ${ formatSize( systemStatus.environment.max_upload_size ) }
-Default Timezone is UTC: ${
- systemStatus.environment.default_timezone === 'UTC'
- ? CHECK_MARK
- : CROSS_MARK
- }
-fsockopen/cURL: ${
- systemStatus.environment.fsockopen_or_curl_enabled
- ? CHECK_MARK
- : CROSS_MARK
- }
-SoapClient: ${
- systemStatus.environment.soapclient_enabled ? CHECK_MARK : CROSS_MARK
- }
-DOMDocument: ${
- systemStatus.environment.domdocument_enabled ? CHECK_MARK : CROSS_MARK
- }
-GZip: ${ systemStatus.environment.gzip_enabled ? CHECK_MARK : CROSS_MARK }
-Multibyte String: ${
- systemStatus.environment.mbstring_enabled ? CHECK_MARK : CROSS_MARK
- }
-Remote Post: ${
- systemStatus.environment.remote_post_successful
- ? CHECK_MARK
- : CROSS_MARK
- }
-Remote Get: ${
- systemStatus.environment.remote_get_successful ? CHECK_MARK : CROSS_MARK
- }
-
-### Database ###
-
-WC Database Version: ${ systemStatus.database.wc_database_version }
-WC Database Prefix: ${ systemStatus.database.database_prefix }
-${ printDatabaseDetails( systemStatus.database ) }
-${ printPostTypeCounts( systemStatus.post_type_counts ) }
-
-### Security ###
-
-Secure connection (HTTPS): ${
- systemStatus.security.secure_connection
- ? CHECK_MARK
- : CROSS_MARK + '\nYour store is not using HTTPS.'
- }
-Hide errors from visitors: ${
- systemStatus.security.hide_errors
- ? CHECK_MARK
- : CROSS_MARK + 'Error messages should not be shown to visitors.'
- }
-
-### Active Plugins (${ systemStatus.active_plugins.length }) ###
-
-${ printPlugins( systemStatus.active_plugins, null ) }
-### Inactive Plugins (${ systemStatus.inactive_plugins.length }) ###
-
-${ printPlugins( systemStatus.inactive_plugins, null ) }${ printPlugins(
- systemStatus.dropins_mu_plugins.dropins,
- 'Dropin Plugins'
- ) }${ printPlugins(
- systemStatus.dropins_mu_plugins.mu_plugins,
- 'Must Use Plugins'
- ) }
-### Settings ###
-
-API Enabled: ${ systemStatus.settings.api_enabled ? CHECK_MARK : DASH_MARK }
-Force SSL: ${ systemStatus.settings.force_ssl ? CHECK_MARK : DASH_MARK }
-Currency: ${ systemStatus.settings.currency } (${ decodeEntities(
- systemStatus.settings.currency_symbol
- ) })
-Currency Position: ${ systemStatus.settings.currency_position }
-Thousand Separator: ${ systemStatus.settings.thousand_separator }
-Decimal Separator: ${ systemStatus.settings.decimal_separator }
-Number of Decimals: ${ systemStatus.settings.number_of_decimals }
-Taxonomies: Product Types: ${ printTerms( systemStatus.settings.taxonomies ) }
-Taxonomies: Product Visibility: ${ printTerms(
- systemStatus.settings.product_visibility_terms
- ) }
-Connected to WooCommerce.com: ${
- systemStatus.settings.woocommerce_com_connected === 'yes'
- ? CHECK_MARK
- : DASH_MARK
- }
-
-### WC Pages ###
-
-${ printPages( systemStatus.pages ) }
-### Theme ###
-
-Name: ${ systemStatus.theme.name }
-Version: ${
- systemStatus.theme.version_latest
- ? `${ systemStatus.theme.version } (update to version ${ systemStatus.theme.version_latest } is available)`
- : systemStatus.theme.version
- }
-Author URL: ${ systemStatus.theme.author_url }
-Child Theme: ${
- systemStatus.theme.is_child_theme
- ? CHECK_MARK
- : CROSS_MARK +
- ' - If you are modifying WooCommerce on a parent theme that you did not build personally we recommend using a child theme.'
- }
-WooCommerce Support: ${
- systemStatus.theme.has_woocommerce_support ? CHECK_MARK : CROSS_MARK
- }
-
-### Templates ###
-
-${
- systemStatus.theme.has_woocommerce_file
- ? 'Archive Template: ' +
- 'Your theme has a woocommerce.php file, you will not be able to override the woocommerce/archive-product.php custom template.'
- : ''
-}
-Overrides: ${
- systemStatus.theme.overrides.length > 0
- ? systemStatus.theme.overrides
- .map( ( override ) => {
- return override.file;
- } )
- .join( ', ' )
- : DASH_MARK
- }
-${
- systemStatus.theme.has_outdated_templates
- ? 'Outdated Templates: ' + CROSS_MARK
- : ''
-}
-
-### WooCommerce Payments ###
-
-Connected to WPCOM: ${
- wcPayData.status === 'NOACCOUNT' ||
- wcPayData.status === 'ONBOARDING_DISABLED'
- ? 'No'
- : 'Yes'
- }
-Account ID: ${ wcPayData.account_id }
-
-### Status report information ###
-
-Generated at: ${ dateI18n( 'Y-m-d H:i:s P' ) }
-`;
- return ssr;
-};
-
-function printDatabaseDetails( database ) {
- const dbSize = database.database_size;
- const dbTables = database.database_tables;
- let result = '';
- if ( dbSize && dbTables ) {
- result =
- 'Total Database Size: ' +
- ( dbSize.data + dbSize.index ).toFixed( 2 ) +
- 'MB\n' +
- 'Database Data Size: ' +
- dbSize.data.toFixed( 2 ) +
- 'MB\n' +
- 'Database Index Size: ' +
- dbSize.index.toFixed( 2 ) +
- 'MB\n';
- result += printTables( dbTables.woocommerce );
- result += printTables( dbTables.other );
- } else {
- result = 'Unable to retrieve database information.';
- }
- return result;
-}
-
-function printTables( tables ) {
- let result = '';
- for ( const [ tableName, tableDetail ] of Object.entries( tables ) ) {
- result += tableName;
- if ( tableDetail ) {
- result += `: Data: ${ tableDetail.data }MB + Index: ${ tableDetail.index }MB + Engine ${ tableDetail.engine }`;
- }
- result += '\n';
- }
- return result;
-}
-
-function printPostTypeCounts( postTypeCounts ) {
- let result = '';
- if ( postTypeCounts ) {
- result = '### Post Type Counts ###\n';
- postTypeCounts.forEach( ( postType ) => {
- result += `\n${ postType.type }: ${ postType.count }`;
- } );
- }
- return result;
-}
-
-function printPlugins( plugins, header ) {
- let result = '';
- if ( header && plugins.length > 0 ) {
- result = '\n### ' + header + ' (' + plugins.length + ')\n\n';
- }
- plugins.forEach( ( plugin ) => {
- const currentVersion = plugin.version;
- result += `${ plugin.name }: by ${ plugin.author_name } - ${ currentVersion }`;
- const latestVersion = plugin.version_latest;
- if (
- currentVersion &&
- latestVersion &&
- currentVersion !== latestVersion
- ) {
- result += ` (update to version ${ latestVersion } is available)`;
- }
- result += '\n';
- } );
- return result;
-}
-
-function printPages( pages ) {
- let result = '';
- pages.forEach( ( page ) => {
- result += page.page_name + ': ';
- let foundError = false;
- if ( ! page.page_set ) {
- result += CROSS_MARK + ' Page not set';
- foundError = true;
- } else if ( ! page.page_exists ) {
- result +=
- CROSS_MARK + ' Page ID is set, but the page does not exist';
- foundError = true;
- } else if ( ! page.page_visible ) {
- result += CROSS_MARK + ' Page visibility should be public';
- foundError = true;
- } else if ( page.shortcode_required || page.block_required ) {
- if ( ! page.shortcode_present && ! page.block_present ) {
- result +=
- CROSS_MARK +
- ` Page does not contain the ${ page.shortcode } shortcode or the ${ page.block } block`;
- foundError = true;
- }
- }
- if ( ! foundError ) {
- result += 'Page ID #' + page.page_id;
- }
- result += '\n';
- } );
- return result;
-}
-
-function printTerms( arr ) {
- let result = '';
- Object.entries( arr ).forEach( ( [ key, value ] ) => {
- result += value.toLowerCase() + ' (' + key + ')\n';
- } );
- return result;
-}
-
-/**
- * Convert bytes into human-readable format. Resemble from PHP function size_format in WordPress core.
- *
- * @param {number} bytes Amount of bytes to be converted.
- * @param {number} decimals Number of digits after the decimal. Default 0.
- * @return {string} Human-readable string.
- */
-function formatSize( bytes, decimals = 0 ) {
- if ( bytes === 0 ) {
- return '0 B';
- }
-
- const KB_IN_BYTES = 1024;
- const MB_IN_BYTES = KB_IN_BYTES * 1024;
- const GB_IN_BYTES = MB_IN_BYTES * 1024;
- const TB_IN_BYTES = GB_IN_BYTES * 1024;
-
- const unitPerBytesMapping = [
- [ 'TB', TB_IN_BYTES ],
- [ 'GB', GB_IN_BYTES ],
- [ 'MB', MB_IN_BYTES ],
- [ 'KB', KB_IN_BYTES ],
- [ 'B', 1 ],
- ];
-
- for ( const [ unit, bytesPerUnit ] of unitPerBytesMapping ) {
- if ( bytes >= bytesPerUnit ) {
- return ( bytes / bytesPerUnit ).toFixed( decimals ) + ' ' + unit;
- }
- }
-
- return 'N/A';
-}
diff --git a/composer.json b/composer.json
index 035d7b76b1b..e8ea75f318a 100644
--- a/composer.json
+++ b/composer.json
@@ -22,10 +22,10 @@
"require": {
"php": ">=7.3",
"ext-json": "*",
- "automattic/jetpack-connection": "6.2.0",
- "automattic/jetpack-config": "3.0.0",
- "automattic/jetpack-autoloader": "5.0.0",
- "automattic/jetpack-sync": "4.1.0",
+ "automattic/jetpack-connection": "6.7.3",
+ "automattic/jetpack-config": "3.0.1",
+ "automattic/jetpack-autoloader": "5.0.2",
+ "automattic/jetpack-sync": "4.8.3",
"woocommerce/subscriptions-core": "6.7.1"
},
"require-dev": {
@@ -90,6 +90,7 @@
"autoload": {
"psr-4": {
"WCPay\\MultiCurrency\\": "includes/multi-currency",
+ "WCPay\\PaymentMethods\\Configs\\": "includes/payment-methods/Configs",
"WCPay\\Vendor\\": "lib/packages",
"WCPay\\": "src"
},
diff --git a/composer.lock b/composer.lock
index c7dad55dc73..553997998ff 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,27 +4,27 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6265907401ba377e5857d8960a5ceb1e",
+ "content-hash": "82ae2b86cd431fd736751ad4c4460abb",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
- "version": "v3.0.0",
+ "version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git",
- "reference": "d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6"
+ "reference": "de63ac4ce117542765900c02555edb19c38e0322"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6",
- "reference": "d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6",
+ "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/de63ac4ce117542765900c02555edb19c38e0322",
+ "reference": "de63ac4ce117542765900c02555edb19c38e0322",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.0.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -52,31 +52,31 @@
],
"description": "Used to record internal usage stats for Automattic. Not visible to site owners.",
"support": {
- "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v3.0.0"
+ "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v3.0.2"
},
- "time": "2024-11-14T20:12:50+00:00"
+ "time": "2025-03-05T12:24:54+00:00"
},
{
"name": "automattic/jetpack-admin-ui",
- "version": "v0.5.1",
+ "version": "v0.5.4",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-admin-ui.git",
- "reference": "a0894d34333451089add7b20f70e73b6509d6b6d"
+ "reference": "a8be8e2b920ea4c9423a2f8d541973afa08a2e54"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/a0894d34333451089add7b20f70e73b6509d6b6d",
- "reference": "a0894d34333451089add7b20f70e73b6509d6b6d",
+ "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/a8be8e2b920ea4c9423a2f8d541973afa08a2e54",
+ "reference": "a8be8e2b920ea4c9423a2f8d541973afa08a2e54",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
- "automattic/jetpack-logo": "^3.0.0",
- "automattic/wordbless": "^0.4.2",
+ "automattic/jetpack-changelogger": "^6.0.0",
+ "automattic/jetpack-logo": "^3.0.2",
+ "automattic/jetpack-test-environment": "@dev",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -108,30 +108,30 @@
],
"description": "Generic Jetpack wp-admin UI elements",
"support": {
- "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.5.1"
+ "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.5.4"
},
- "time": "2024-11-25T16:33:45+00:00"
+ "time": "2025-03-05T12:25:10+00:00"
},
{
"name": "automattic/jetpack-assets",
- "version": "v4.0.2",
+ "version": "v4.0.10",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-assets.git",
- "reference": "b718bf1d687adbf60d3eab8b5c80038c48ef112c"
+ "reference": "67ffb8b5e55496efdf336ea6ad7960e7d0f02340"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/b718bf1d687adbf60d3eab8b5c80038c48ef112c",
- "reference": "b718bf1d687adbf60d3eab8b5c80038c48ef112c",
+ "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/67ffb8b5e55496efdf336ea6ad7960e7d0f02340",
+ "reference": "67ffb8b5e55496efdf336ea6ad7960e7d0f02340",
"shasum": ""
},
"require": {
- "automattic/jetpack-constants": "^3.0.1",
+ "automattic/jetpack-constants": "^3.0.3",
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"brain/monkey": "^2.6.2",
"wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0",
"yoast/phpunit-polyfills": "^1.1.1"
@@ -165,22 +165,22 @@
],
"description": "Asset management utilities for Jetpack ecosystem packages",
"support": {
- "source": "https://github.com/Automattic/jetpack-assets/tree/v4.0.2"
+ "source": "https://github.com/Automattic/jetpack-assets/tree/v4.0.10"
},
- "time": "2024-12-16T13:08:13+00:00"
+ "time": "2025-03-05T12:25:29+00:00"
},
{
"name": "automattic/jetpack-autoloader",
- "version": "v5.0.0",
+ "version": "v5.0.2",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
- "reference": "eb6331a5c50a03afd9896ce012e66858de9c49c5"
+ "reference": "495ec34a198ffc160d77e9393a6bdc461c4f06f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/eb6331a5c50a03afd9896ce012e66858de9c49c5",
- "reference": "eb6331a5c50a03afd9896ce012e66858de9c49c5",
+ "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/495ec34a198ffc160d77e9393a6bdc461c4f06f1",
+ "reference": "495ec34a198ffc160d77e9393a6bdc461c4f06f1",
"shasum": ""
},
"require": {
@@ -188,7 +188,7 @@
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"composer/composer": "^2.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -229,29 +229,29 @@
"wordpress"
],
"support": {
- "source": "https://github.com/Automattic/jetpack-autoloader/tree/v5.0.0"
+ "source": "https://github.com/Automattic/jetpack-autoloader/tree/v5.0.2"
},
- "time": "2024-11-25T16:33:57+00:00"
+ "time": "2025-02-24T17:06:04+00:00"
},
{
"name": "automattic/jetpack-config",
- "version": "v3.0.0",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-config.git",
- "reference": "fc719eff5073634b0c62793b05be913ca634e192"
+ "reference": "13f26ed2830d9043d351e49c5ab4e2b6ec21e9d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/fc719eff5073634b0c62793b05be913ca634e192",
- "reference": "fc719eff5073634b0c62793b05be913ca634e192",
+ "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/13f26ed2830d9043d351e49c5ab4e2b6ec21e9d2",
+ "reference": "13f26ed2830d9043d351e49c5ab4e2b6ec21e9d2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.0.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"automattic/jetpack-connection": "@dev",
"automattic/jetpack-import": "@dev",
"automattic/jetpack-jitm": "@dev",
@@ -309,37 +309,37 @@
],
"description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.",
"support": {
- "source": "https://github.com/Automattic/jetpack-config/tree/v3.0.0"
+ "source": "https://github.com/Automattic/jetpack-config/tree/v3.0.1"
},
- "time": "2024-11-14T20:12:40+00:00"
+ "time": "2025-02-24T17:05:29+00:00"
},
{
"name": "automattic/jetpack-connection",
- "version": "v6.2.0",
+ "version": "v6.7.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-connection.git",
- "reference": "52cd2ba7d845eb516d505959bd9a5e94d1bf4203"
+ "reference": "18babb05f11da2cb7212f32cb83f74b1ded24fa8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/52cd2ba7d845eb516d505959bd9a5e94d1bf4203",
- "reference": "52cd2ba7d845eb516d505959bd9a5e94d1bf4203",
+ "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/18babb05f11da2cb7212f32cb83f74b1ded24fa8",
+ "reference": "18babb05f11da2cb7212f32cb83f74b1ded24fa8",
"shasum": ""
},
"require": {
- "automattic/jetpack-a8c-mc-stats": "^3.0.0",
- "automattic/jetpack-admin-ui": "^0.5.1",
- "automattic/jetpack-assets": "^4.0.1",
- "automattic/jetpack-constants": "^3.0.1",
- "automattic/jetpack-redirect": "^3.0.1",
- "automattic/jetpack-roles": "^3.0.1",
- "automattic/jetpack-status": "^5.0.1",
+ "automattic/jetpack-a8c-mc-stats": "^3.0.2",
+ "automattic/jetpack-admin-ui": "^0.5.4",
+ "automattic/jetpack-assets": "^4.0.10",
+ "automattic/jetpack-constants": "^3.0.3",
+ "automattic/jetpack-redirect": "^3.0.3",
+ "automattic/jetpack-roles": "^3.0.3",
+ "automattic/jetpack-status": "^5.0.6",
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
- "automattic/wordbless": "^0.4.2",
+ "automattic/jetpack-changelogger": "^6.0.0",
+ "automattic/jetpack-test-environment": "@dev",
"brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -352,7 +352,7 @@
"textdomain": "jetpack-connection",
"mirror-repo": "Automattic/jetpack-connection",
"branch-alias": {
- "dev-trunk": "6.2.x-dev"
+ "dev-trunk": "6.7.x-dev"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}"
@@ -384,29 +384,29 @@
],
"description": "Everything needed to connect to the Jetpack infrastructure",
"support": {
- "source": "https://github.com/Automattic/jetpack-connection/tree/v6.2.0"
+ "source": "https://github.com/Automattic/jetpack-connection/tree/v6.7.3"
},
- "time": "2024-12-09T15:47:56+00:00"
+ "time": "2025-03-10T15:59:13+00:00"
},
{
"name": "automattic/jetpack-constants",
- "version": "v3.0.1",
+ "version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-constants.git",
- "reference": "d4b7820defcdb40c1add88d5ebd722e4ba80a873"
+ "reference": "4eac57a30282d67589fdad81034d11ac7b7c4941"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/d4b7820defcdb40c1add88d5ebd722e4ba80a873",
- "reference": "d4b7820defcdb40c1add88d5ebd722e4ba80a873",
+ "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/4eac57a30282d67589fdad81034d11ac7b7c4941",
+ "reference": "4eac57a30282d67589fdad81034d11ac7b7c4941",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -435,29 +435,29 @@
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
- "source": "https://github.com/Automattic/jetpack-constants/tree/v3.0.1"
+ "source": "https://github.com/Automattic/jetpack-constants/tree/v3.0.3"
},
- "time": "2024-11-25T16:33:27+00:00"
+ "time": "2025-03-05T12:24:54+00:00"
},
{
"name": "automattic/jetpack-ip",
- "version": "v0.4.1",
+ "version": "v0.4.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-ip.git",
- "reference": "04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a"
+ "reference": "05711f48000853583240626a3bd26d73e790eab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a",
- "reference": "04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a",
+ "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/05711f48000853583240626a3bd26d73e790eab3",
+ "reference": "05711f48000853583240626a3bd26d73e790eab3",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -490,30 +490,30 @@
],
"description": "Utilities for working with IP addresses.",
"support": {
- "source": "https://github.com/Automattic/jetpack-ip/tree/v0.4.1"
+ "source": "https://github.com/Automattic/jetpack-ip/tree/v0.4.3"
},
- "time": "2024-11-25T16:33:22+00:00"
+ "time": "2025-03-05T12:24:52+00:00"
},
{
"name": "automattic/jetpack-password-checker",
- "version": "v0.4.1",
+ "version": "v0.4.4",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-password-checker.git",
- "reference": "e721e7659cc7a6a37152a4e96485e6c139f02d5f"
+ "reference": "72dbcc59df904651ac61329b2619e44036da361f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/e721e7659cc7a6a37152a4e96485e6c139f02d5f",
- "reference": "e721e7659cc7a6a37152a4e96485e6c139f02d5f",
+ "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/72dbcc59df904651ac61329b2619e44036da361f",
+ "reference": "72dbcc59df904651ac61329b2619e44036da361f",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
- "automattic/wordbless": "^0.4.2",
+ "automattic/jetpack-changelogger": "^6.0.0",
+ "automattic/jetpack-test-environment": "@dev",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -542,30 +542,30 @@
],
"description": "Password Checker.",
"support": {
- "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.4.1"
+ "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.4.4"
},
- "time": "2024-11-25T16:33:31+00:00"
+ "time": "2025-03-05T12:25:00+00:00"
},
{
"name": "automattic/jetpack-redirect",
- "version": "v3.0.1",
+ "version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-redirect.git",
- "reference": "89732a3ba1c5eba8cfd948b7567823cd884102d5"
+ "reference": "b2f9a1f3a050f8b752c41dd297078b6a145b1317"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/89732a3ba1c5eba8cfd948b7567823cd884102d5",
- "reference": "89732a3ba1c5eba8cfd948b7567823cd884102d5",
+ "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/b2f9a1f3a050f8b752c41dd297078b6a145b1317",
+ "reference": "b2f9a1f3a050f8b752c41dd297078b6a145b1317",
"shasum": ""
},
"require": {
- "automattic/jetpack-status": "^5.0.1",
+ "automattic/jetpack-status": "^5.0.6",
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -594,29 +594,29 @@
],
"description": "Utilities to build URLs to the jetpack.com/redirect/ service",
"support": {
- "source": "https://github.com/Automattic/jetpack-redirect/tree/v3.0.1"
+ "source": "https://github.com/Automattic/jetpack-redirect/tree/v3.0.3"
},
- "time": "2024-11-25T16:34:01+00:00"
+ "time": "2025-03-05T12:25:15+00:00"
},
{
"name": "automattic/jetpack-roles",
- "version": "v3.0.1",
+ "version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-roles.git",
- "reference": "fe5f2a45901ea14be00728119d097619615fb031"
+ "reference": "c691c5c9ede877b4d67636febec609e263ad9567"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/fe5f2a45901ea14be00728119d097619615fb031",
- "reference": "fe5f2a45901ea14be00728119d097619615fb031",
+ "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/c691c5c9ede877b4d67636febec609e263ad9567",
+ "reference": "c691c5c9ede877b4d67636febec609e263ad9567",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -645,32 +645,32 @@
],
"description": "Utilities, related with user roles and capabilities.",
"support": {
- "source": "https://github.com/Automattic/jetpack-roles/tree/v3.0.1"
+ "source": "https://github.com/Automattic/jetpack-roles/tree/v3.0.3"
},
- "time": "2024-11-25T16:33:29+00:00"
+ "time": "2025-03-05T12:24:56+00:00"
},
{
"name": "automattic/jetpack-status",
- "version": "v5.0.1",
+ "version": "v5.0.6",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-status.git",
- "reference": "769f55b6327187a85c14ed21943eea430f63220d"
+ "reference": "4ed0546871baa1ab8a019ab779033abe71ddb6fb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/769f55b6327187a85c14ed21943eea430f63220d",
- "reference": "769f55b6327187a85c14ed21943eea430f63220d",
+ "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/4ed0546871baa1ab8a019ab779033abe71ddb6fb",
+ "reference": "4ed0546871baa1ab8a019ab779033abe71ddb6fb",
"shasum": ""
},
"require": {
- "automattic/jetpack-constants": "^3.0.1",
+ "automattic/jetpack-constants": "^3.0.3",
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"automattic/jetpack-connection": "@dev",
- "automattic/jetpack-ip": "^0.4.1",
+ "automattic/jetpack-ip": "^0.4.3",
"automattic/jetpack-plans": "@dev",
"brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
@@ -706,38 +706,38 @@
],
"description": "Used to retrieve information about the current status of Jetpack and the site overall.",
"support": {
- "source": "https://github.com/Automattic/jetpack-status/tree/v5.0.1"
+ "source": "https://github.com/Automattic/jetpack-status/tree/v5.0.6"
},
- "time": "2024-11-25T16:33:53+00:00"
+ "time": "2025-03-05T12:25:13+00:00"
},
{
"name": "automattic/jetpack-sync",
- "version": "v4.1.0",
+ "version": "v4.8.3",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-sync.git",
- "reference": "5747f144575b9474622692f2bc8e4315363ea44d"
+ "reference": "4170bcda7bf9925dc83b4b8485c3182e23c328e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/5747f144575b9474622692f2bc8e4315363ea44d",
- "reference": "5747f144575b9474622692f2bc8e4315363ea44d",
+ "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/4170bcda7bf9925dc83b4b8485c3182e23c328e0",
+ "reference": "4170bcda7bf9925dc83b4b8485c3182e23c328e0",
"shasum": ""
},
"require": {
- "automattic/jetpack-connection": "^6.2.0",
- "automattic/jetpack-constants": "^3.0.1",
- "automattic/jetpack-ip": "^0.4.1",
- "automattic/jetpack-password-checker": "^0.4.1",
- "automattic/jetpack-roles": "^3.0.1",
- "automattic/jetpack-status": "^5.0.1",
+ "automattic/jetpack-connection": "^6.7.1",
+ "automattic/jetpack-constants": "^3.0.3",
+ "automattic/jetpack-ip": "^0.4.3",
+ "automattic/jetpack-password-checker": "^0.4.4",
+ "automattic/jetpack-roles": "^3.0.3",
+ "automattic/jetpack-status": "^5.0.6",
"php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-changelogger": "^6.0.0",
"automattic/jetpack-search": "@dev",
- "automattic/jetpack-waf": "^0.23.1",
- "automattic/wordbless": "^0.4.2",
+ "automattic/jetpack-test-environment": "@dev",
+ "automattic/jetpack-waf": "@dev",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -749,7 +749,7 @@
"textdomain": "jetpack-sync",
"mirror-repo": "Automattic/jetpack-sync",
"branch-alias": {
- "dev-trunk": "4.1.x-dev"
+ "dev-trunk": "4.8.x-dev"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-sync/compare/v${old}...v${new}"
@@ -775,9 +775,9 @@
],
"description": "Everything needed to allow syncing to the WP.com infrastructure.",
"support": {
- "source": "https://github.com/Automattic/jetpack-sync/tree/v4.1.0"
+ "source": "https://github.com/Automattic/jetpack-sync/tree/v4.8.3"
},
- "time": "2024-12-09T15:48:10+00:00"
+ "time": "2025-03-05T12:25:45+00:00"
},
{
"name": "composer/installers",
@@ -2311,16 +2311,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.12.1",
+ "version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
+ "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
- "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
+ "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"shasum": ""
},
"require": {
@@ -2359,7 +2359,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
},
"funding": [
{
@@ -2367,7 +2367,7 @@
"type": "tidelift"
}
],
- "time": "2024-11-08T17:47:46+00:00"
+ "time": "2025-02-12T12:17:51+00:00"
},
{
"name": "netresearch/jsonmapper",
@@ -2742,12 +2742,12 @@
"source": {
"type": "git",
"url": "https://github.com/PHPCompatibility/PHPCompatibility.git",
- "reference": "02835e45d27f5d0ed1642d5c8a5e8bfd2c7d7796"
+ "reference": "9013cd039fe5740953f9fdeebd19d901b80e26f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/02835e45d27f5d0ed1642d5c8a5e8bfd2c7d7796",
- "reference": "02835e45d27f5d0ed1642d5c8a5e8bfd2c7d7796",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9013cd039fe5740953f9fdeebd19d901b80e26f2",
+ "reference": "9013cd039fe5740953f9fdeebd19d901b80e26f2",
"shasum": ""
},
"require": {
@@ -2822,9 +2822,13 @@
{
"url": "https://opencollective.com/php_codesniffer",
"type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcompatibility",
+ "type": "thanks_dev"
}
],
- "time": "2024-11-30T16:25:00+00:00"
+ "time": "2025-01-20T20:06:48+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-paragonie",
@@ -2900,16 +2904,16 @@
},
{
"name": "phpcompatibility/phpcompatibility-wp",
- "version": "2.1.5",
+ "version": "2.1.6",
"source": {
"type": "git",
"url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git",
- "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082"
+ "reference": "80ccb1a7640995edf1b87a4409fa584cd5869469"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082",
- "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/80ccb1a7640995edf1b87a4409fa584cd5869469",
+ "reference": "80ccb1a7640995edf1b87a4409fa584cd5869469",
"shasum": ""
},
"require": {
@@ -2966,7 +2970,7 @@
"type": "open_collective"
}
],
- "time": "2024-04-24T21:37:59+00:00"
+ "time": "2025-01-16T22:34:19+00:00"
},
{
"name": "phpcsstandards/phpcsextra",
@@ -5100,16 +5104,16 @@
},
{
"name": "sirbrillig/phpcs-variable-analysis",
- "version": "v2.11.21",
+ "version": "v2.11.22",
"source": {
"type": "git",
"url": "https://github.com/sirbrillig/phpcs-variable-analysis.git",
- "reference": "eb2b351927098c24860daa7484e290d3eed693be"
+ "reference": "ffb6f16c6033ec61ed84446b479a31d6529f0eb7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/eb2b351927098c24860daa7484e290d3eed693be",
- "reference": "eb2b351927098c24860daa7484e290d3eed693be",
+ "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/ffb6f16c6033ec61ed84446b479a31d6529f0eb7",
+ "reference": "ffb6f16c6033ec61ed84446b479a31d6529f0eb7",
"shasum": ""
},
"require": {
@@ -5121,7 +5125,6 @@
"phpcsstandards/phpcsdevcs": "^1.1",
"phpstan/phpstan": "^1.7",
"phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3",
- "sirbrillig/phpcs-import-detection": "^1.1",
"vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0"
},
"type": "phpcodesniffer-standard",
@@ -5154,7 +5157,7 @@
"source": "https://github.com/sirbrillig/phpcs-variable-analysis",
"wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki"
},
- "time": "2024-12-02T16:37:49+00:00"
+ "time": "2025-01-06T17:54:24+00:00"
},
{
"name": "slevomat/coding-standard",
@@ -5287,16 +5290,16 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.11.2",
+ "version": "3.11.3",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079"
+ "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079",
- "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10",
+ "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10",
"shasum": ""
},
"require": {
@@ -5361,9 +5364,13 @@
{
"url": "https://opencollective.com/php_codesniffer",
"type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/phpcsstandards",
+ "type": "thanks_dev"
}
],
- "time": "2024-12-11T16:04:26+00:00"
+ "time": "2025-01-23T17:04:15+00:00"
},
{
"name": "symfony/console",
@@ -7012,9 +7019,9 @@
"php": ">=7.3",
"ext-json": "*"
},
- "platform-dev": {},
+ "platform-dev": [],
"platform-overrides": {
"php": "7.3"
},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.3.0"
}
diff --git a/docker/wc-payments-php.ini b/docker/wc-payments-php.ini
index a5614e7449b..664510331f5 100644
--- a/docker/wc-payments-php.ini
+++ b/docker/wc-payments-php.ini
@@ -1 +1,2 @@
upload_max_filesize = 20M
+memory_limit = 256M
diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php
index ad4cc0857c9..02643e043ff 100644
--- a/includes/admin/class-wc-payments-admin.php
+++ b/includes/admin/class-wc-payments-admin.php
@@ -6,9 +6,11 @@
*/
use Automattic\Jetpack\Identity_Crisis as Jetpack_Identity_Crisis;
+use Automattic\WooCommerce\Admin\Features\Features;
use WCPay\Constants\Intent_Status;
use WCPay\Core\Server\Request;
use WCPay\Database_Cache;
+use WCPay\Inline_Script_Payloads\Woo_Payments_Payment_Method_Definitions;
use WCPay\Logger;
use WCPay\WooPay\WooPay_Utilities;
@@ -168,6 +170,39 @@ public function init_hooks() {
add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'show_woopay_payment_method_name_admin' ] );
add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'display_wcpay_transaction_fee' ] );
add_action( 'admin_init', [ $this, 'redirect_deposits_to_payouts' ] );
+ add_action( 'woocommerce_update_options_site-visibility', [ $this, 'inform_stripe_when_store_goes_live' ] );
+ }
+
+ /**
+ * When a store transitions to live mode, we need to notify Stripe to trigger necessary verification checks.
+ *
+ * @return void
+ */
+ public function inform_stripe_when_store_goes_live() {
+
+ $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
+ // New Settings API uses wp_rest nonce.
+ $nonce_string = Features::is_enabled( 'settings' ) ? 'wp_rest' : 'woocommerce-settings';
+ if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $nonce_string ) ) {
+ return;
+ }
+
+ // If an account is not connected, we can skip this.
+ if ( ! $this->account->is_stripe_connected() ) {
+ return;
+ }
+
+ $coming_soon_value = get_option( 'woocommerce_coming_soon' );
+ $coming_soon_new_value = sanitize_text_field( wp_unslash( $_POST['woocommerce_coming_soon'] ) );
+
+ // If the store is transitioning from coming soon to live, Stripe should be notified.
+ // This is triggered by updating the account business URL.
+ if ( 'no' === $coming_soon_new_value && $coming_soon_value !== $coming_soon_new_value ) {
+ $response = $this->wcpay_gateway->update_account_settings( [ 'account_business_url' => $this->account->get_business_url() ] );
+ if ( is_wp_error( $response ) ) {
+ Logger::error( 'Failed to update account business URL.' );
+ }
+ }
}
/**
@@ -564,6 +599,11 @@ public function register_payments_scripts() {
}
WC_Payments::register_script_with_dependencies( 'WCPAY_DASH_APP', 'dist/index', [ 'wp-api-request' ] );
+ wp_add_inline_script(
+ 'WCPAY_DASH_APP',
+ new Woo_Payments_Payment_Method_Definitions(),
+ 'before'
+ );
wp_set_script_translations( 'WCPAY_DASH_APP', 'woocommerce-payments' );
@@ -597,6 +637,11 @@ public function register_payments_scripts() {
);
WC_Payments::register_script_with_dependencies( 'WCPAY_ADMIN_SETTINGS', 'dist/settings' );
+ wp_add_inline_script(
+ 'WCPAY_ADMIN_SETTINGS',
+ new Woo_Payments_Payment_Method_Definitions(),
+ 'before'
+ );
wp_localize_script(
'WCPAY_ADMIN_SETTINGS',
@@ -621,6 +666,11 @@ public function register_payments_scripts() {
);
WC_Payments::register_script_with_dependencies( 'WCPAY_PAYMENT_GATEWAYS_PAGE', 'dist/payment-gateways' );
+ wp_add_inline_script(
+ 'WCPAY_PAYMENT_GATEWAYS_PAGE',
+ new Woo_Payments_Payment_Method_Definitions(),
+ 'before'
+ );
WC_Payments_Utils::register_style(
'WCPAY_PAYMENT_GATEWAYS_PAGE',
diff --git a/includes/admin/class-wc-rest-payments-reader-controller.php b/includes/admin/class-wc-rest-payments-reader-controller.php
index 33c8eaec2dc..69a1b22ed96 100644
--- a/includes/admin/class-wc-rest-payments-reader-controller.php
+++ b/includes/admin/class-wc-rest-payments-reader-controller.php
@@ -149,7 +149,6 @@ public function register_routes() {
* @return WP_REST_Response|WP_Error
*/
public function get_summary( $request ) {
-
$transaction_id = $request->get_param( 'transaction_id' );
try {
@@ -159,7 +158,10 @@ public function get_summary( $request ) {
if ( empty( $transaction ) ) {
return rest_ensure_response( [] );
}
- $summary = $this->api_client->get_readers_charge_summary( gmdate( 'Y-m-d', $transaction['created'] ) );
+ $summary = $this->api_client->get_readers_charge_summary(
+ gmdate( 'Y-m-d', $transaction['created'] ),
+ $transaction_id
+ );
} catch ( API_Exception $e ) {
return rest_ensure_response( new WP_Error( 'wcpay_get_summary', $e->getMessage() ) );
}
diff --git a/includes/class-duplicates-detection-service.php b/includes/class-duplicates-detection-service.php
index 82159057bd9..afa8124f3d1 100644
--- a/includes/class-duplicates-detection-service.php
+++ b/includes/class-duplicates-detection-service.php
@@ -14,7 +14,6 @@
use WC_Payments;
use WCPay\Payment_Methods\Affirm_Payment_Method;
use WCPay\Payment_Methods\Afterpay_Payment_Method;
-use WCPay\Payment_Methods\Alipay_Payment_Method;
use WCPay\Payment_Methods\Bancontact_Payment_Method;
use WCPay\Payment_Methods\Becs_Payment_Method;
use WCPay\Payment_Methods\CC_Payment_Method;
@@ -25,6 +24,7 @@
use WCPay\Payment_Methods\Sepa_Payment_Method;
use WCPay\Payment_Methods\Grabpay_Payment_Method;
use WCPay\Payment_Methods\Wechatpay_Payment_Method;
+use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry;
/**
* Class handling detection of payment methods enabled by multiple plugins simultaneously.
@@ -94,10 +94,11 @@ private function search_for_cc() {
* @return Duplicates_Detection_Service
*/
private function search_for_additional_payment_methods() {
+ // Get all payment method definitions.
+
$keywords = [
'bancontact' => Bancontact_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
'sepa' => Sepa_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
- 'alipay' => Alipay_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
'p24' => P24_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
'przelewy24' => P24_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
'ideal' => Ideal_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
@@ -111,6 +112,16 @@ private function search_for_additional_payment_methods() {
'wechatpay' => Wechatpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
];
+ $payment_method_definitions = PaymentMethodDefinitionRegistry::instance()->get_all_payment_method_definitions();
+
+ // This gets all the registered payment method definitions. As new payment methods are converted from the legacy style, they need to be removed from the list above.
+ foreach ( $payment_method_definitions as $definition_class ) {
+ $definition_keywords = $definition_class::get_keywords();
+ foreach ( $definition_keywords as $keyword ) {
+ $keywords[ $keyword ] = $definition_class::get_id();
+ }
+ }
+
foreach ( $this->get_enabled_gateways() as $gateway ) {
foreach ( $keywords as $keyword => $payment_method ) {
if ( strpos( $gateway->id, $keyword ) !== false ) {
diff --git a/includes/class-payment-information.php b/includes/class-payment-information.php
index 4b4f8a13f04..8548c671443 100644
--- a/includes/class-payment-information.php
+++ b/includes/class-payment-information.php
@@ -11,6 +11,7 @@
exit; // Exit if accessed directly.
}
+use WCPay\Constants\Payment_Method;
use WCPay\Constants\Payment_Type;
use WCPay\Constants\Payment_Initiated_By;
use WCPay\Constants\Payment_Capture_Type;
@@ -498,4 +499,13 @@ public function set_error( \WP_Error $error ) {
public function get_error() {
return $this->error;
}
+
+ /**
+ * Returns true if the payment method is an offline payment method, false otherwise.
+ *
+ * @return bool True if the payment method is an offline payment method, false otherwise.
+ */
+ public function is_offline_payment_method(): bool {
+ return in_array( $this->payment_method_stripe_id, Payment_Method::OFFLINE_PAYMENT_METHODS, true );
+ }
}
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 357c6323d51..761dcae591a 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -61,14 +61,15 @@
use WCPay\Payment_Methods\Becs_Payment_Method;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Payment_Methods\Eps_Payment_Method;
-use WCPay\Payment_Methods\Alipay_Payment_Method;
use WCPay\Payment_Methods\Ideal_Payment_Method;
use WCPay\Payment_Methods\Klarna_Payment_Method;
use WCPay\Payment_Methods\P24_Payment_Method;
use WCPay\Payment_Methods\Sepa_Payment_Method;
use WCPay\Payment_Methods\UPE_Payment_Method;
+use WCPay\Payment_Methods\Multibanco_Payment_Method;
use WCPay\Payment_Methods\Grabpay_Payment_Method;
use WCPay\Payment_Methods\Wechatpay_Payment_Method;
+use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry;
/**
* Gateway class for WooPayments
@@ -349,6 +350,7 @@ public function __construct(
'klarna' => 'klarna_payments',
'grabpay' => 'grabpay_payments',
'jcb' => 'jcb_payments',
+ 'multibanco' => 'multibanco_payments',
'wechat_pay' => 'wechat_pay_payments',
];
@@ -1626,6 +1628,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$processing = [];
}
+ $is_offline_payment_method = $payment_information->is_offline_payment_method();
if ( ! empty( $intent ) ) {
if ( ! $intent->is_authorized() ) {
$intent_failed = true;
@@ -1676,11 +1679,20 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
}
if ( Intent_Status::REQUIRES_ACTION === $status ) {
- if ( isset( $next_action['type'] ) && 'redirect_to_url' === $next_action['type'] && ! empty( $next_action['redirect_to_url']['url'] ) ) {
+ $next_action_type = $next_action['type'] ?? null;
+ if ( 'redirect_to_url' === $next_action_type && ! empty( $next_action[ $next_action_type ]['url'] ) ) {
$response = [
'result' => 'success',
- 'redirect' => $next_action['redirect_to_url']['url'],
+ 'redirect' => $next_action[ $next_action_type ]['url'],
];
+ } elseif ( 'multibanco_display_details' === $next_action_type ) {
+ $this->order_service->attach_multibanco_info_to_order(
+ $order,
+ $next_action[ $next_action_type ]['reference'],
+ $next_action[ $next_action_type ]['entity'],
+ $next_action[ $next_action_type ]['hosted_voucher_url'],
+ $next_action[ $next_action_type ]['expires_at']
+ );
} else {
$response = [
'result' => 'success',
@@ -1691,18 +1703,28 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$payment_needed ? 'pi' : 'si',
$order_id,
$client_secret,
- wp_create_nonce( 'wcpay_update_order_status_nonce' )
+ wp_create_nonce( 'wcpay_update_order_status_nonce' ),
),
// Include the payment method ID so the Blocks integration can save cards.
'payment_method' => $payment_information->get_payment_method(),
];
}
+ } elseif ( $this->is_changing_payment_method_for_subscription() ) {
+ // Only attempt to use WC_Subscriptions_Change_Payment_Gateway if it exists.
+ if ( class_exists( 'WC_Subscriptions_Change_Payment_Gateway' ) ) {
+ // Update the payment method for subscription if the payment intent is not requiring action.
+ WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $order, $payment_information->get_payment_method() );
+ }
+
+ // Because this new payment does not require action/confirmation, remove this filter so that WC_Subscriptions_Change_Payment_Gateway proceeds to update all subscriptions if flagged.
+ remove_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_method_for_subscriptions' ], 10 );
}
}
- $this->order_service->attach_intent_info_to_order( $order, $intent );
+ $allow_update_on_success = $this->is_changing_payment_method_for_subscription() || $this->is_subscription_item_in_cart();
+ $this->order_service->attach_intent_info_to_order( $order, $intent, $allow_update_on_success );
$this->attach_exchange_info_to_order( $order, $charge_id );
- if ( Intent_Status::SUCCEEDED === $status ) {
+ if ( Intent_Status::SUCCEEDED === $status || ( Intent_Status::REQUIRES_ACTION === $status && $is_offline_payment_method ) ) {
$this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() );
}
$this->order_service->update_order_status_from_intent( $order, $intent );
@@ -1710,7 +1732,18 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$this->maybe_add_customer_notification_note( $order, $processing );
- if ( isset( $response ) ) {
+ if ( isset( $status ) && Intent_Status::REQUIRES_ACTION === $status && $this->is_changing_payment_method_for_subscription() ) {
+ // Because we're filtering woocommerce_subscriptions_update_payment_via_pay_shortcode, we need to manually set this delayed update all flag here.
+ if ( isset( $_POST['update_all_subscriptions_payment_method'] ) && wc_clean( wp_unslash( $_POST['update_all_subscriptions_payment_method'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $order->update_meta_data( '_delayed_update_payment_method_all', wc_clean( wp_unslash( $_POST['payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $order->save();
+ }
+
+ wp_safe_redirect( $response['redirect'] );
+ exit;
+ }
+
+ if ( isset( $response ) && ! $is_offline_payment_method ) {
return $response;
}
@@ -2252,19 +2285,22 @@ static function ( $refund ) use ( $refund_amount ) {
$currency = strtoupper( $refund['currency'] );
Tracker::track_admin( 'wcpay_edit_order_refund_success' );
} catch ( Exception $e ) {
+ if ( $e instanceof API_Exception && 'insufficient_balance_for_refund' === $e->get_error_code() ) {
+ // Handle insufficient_balance_for_refund error.
+ $this->order_service->handle_insufficient_balance_for_refund( $order, WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) );
+ } else {
+ $note = sprintf(
+ /* translators: %1: the successfully charged amount, %2: error message */
+ __( 'A refund of %1$s failed to complete: %2$s', 'woocommerce-payments' ),
+ WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $amount, [ 'currency' => $currency ] ), $order ),
+ $e->getMessage()
+ );
- $note = sprintf(
- /* translators: %1: the successfully charged amount, %2: error message */
- __( 'A refund of %1$s failed to complete: %2$s', 'woocommerce-payments' ),
- WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $amount, [ 'currency' => $currency ] ), $order ),
- $e->getMessage()
- );
+ Logger::log( $note );
+ $order->add_order_note( $note );
+ }
- Logger::log( $note );
- $order->add_order_note( $note );
$this->order_service->set_wcpay_refund_status_for_order( $order, 'failed' );
- $order->save();
-
Tracker::track_admin( 'wcpay_edit_order_refund_failure', [ 'reason' => $note ] );
return new WP_Error( 'wcpay_edit_order_refund_failure', $e->getMessage() );
}
@@ -3437,8 +3473,9 @@ public function update_order_status() {
$amount = $order->get_total();
$payment_method_details = false;
+ $is_changing_payment = isset( $_POST['is_changing_payment'] ) && filter_var( wp_unslash( $_POST['is_changing_payment'] ), FILTER_VALIDATE_BOOLEAN );
- if ( $amount > 0 ) {
+ if ( $amount > 0 && ! $is_changing_payment ) {
// An exception is thrown if an intent can't be found for the given intent ID.
$request = Get_Intention::create( $intent_id );
$request->set_hook_args( $order );
@@ -3488,10 +3525,27 @@ public function update_order_status() {
}
}
+ $return_url = $this->get_return_url( $order );
+
+ if ( $is_changing_payment ) {
+ $payment_token = $this->get_payment_token( $order );
+ if ( class_exists( 'WC_Subscriptions_Change_Payment_Gateway' ) ) {
+ WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $order, $payment_token->get_gateway_id() );
+ $notice = __( 'Payment method updated.', 'woocommerce-payments' );
+
+ if ( WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $order ) && WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $order, $token->get_gateway_id() ) ) {
+ $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-payments' );
+ }
+
+ wc_add_notice( $notice );
+ }
+ $return_url = method_exists( $order, 'get_view_order_url' ) ? $order->get_view_order_url() : $this->get_return_url( $order );
+ }
+
// Send back redirect URL in the successful case.
echo wp_json_encode(
[
- 'return_url' => $this->get_return_url( $order ),
+ 'return_url' => $return_url,
]
);
wp_die();
@@ -3891,6 +3945,20 @@ public function get_selected_stripe_payment_type_id() {
return $this->stripe_id;
}
+ /**
+ * Returns the list of enabled payment method types for UPE that are available based on the manual capture setting.
+ *
+ * @return string[]
+ */
+ public function get_upe_enabled_payment_method_ids_based_on_manual_capture() {
+ $automatic_capture = empty( $this->get_option( 'manual_capture' ) ) || $this->get_option( 'manual_capture' ) === 'no';
+ if ( $automatic_capture ) {
+ return $this->get_upe_enabled_payment_method_ids();
+ }
+
+ return array_intersect( $this->get_upe_enabled_payment_method_ids(), [ Payment_Method::CARD, Payment_Method::LINK ] );
+ }
+
/**
* Returns the list of enabled payment method types that will function with the current checkout.
*
@@ -3900,12 +3968,7 @@ public function get_selected_stripe_payment_type_id() {
* @return string[]
*/
public function get_payment_method_ids_enabled_at_checkout( $order_id = null, $force_currency_check = false ) {
- $automatic_capture = empty( $this->get_option( 'manual_capture' ) ) || $this->get_option( 'manual_capture' ) === 'no';
- if ( $automatic_capture ) {
- $upe_enabled_payment_methods = $this->get_upe_enabled_payment_method_ids();
- } else {
- $upe_enabled_payment_methods = array_intersect( $this->get_upe_enabled_payment_method_ids(), [ Payment_Method::CARD, Payment_Method::LINK ] );
- }
+ $upe_enabled_payment_methods = $this->get_upe_enabled_payment_method_ids_based_on_manual_capture();
if ( is_wc_endpoint_url( 'order-pay' ) ) {
$force_currency_check = true;
}
@@ -3920,14 +3983,16 @@ public function get_payment_method_ids_enabled_at_checkout( $order_id = null, $f
// with the payment methods which are sent with the payment intent request, otherwise
// Stripe returns an error.
- // force_currency_check = 0 is_admin = 0 currency_is_checked = 1.
- // force_currency_check = 0 is_admin = 1 currency_is_checked = 0.
- // force_currency_check = 1 is_admin = 0 currency_is_checked = 1.
- // force_currency_check = 1 is_admin = 1 currency_is_checked = 1.
+ // In order to allow payment methods to be displayed in admin pages (e.g. blocks editor),
+ // we need to skip the currency check (unless force_currency_check is true).
+ // force_currency_check = 0 is_admin = 0 -> skip_currency_check = 0.
+ // force_currency_check = 0 is_admin = 1 -> skip_currency_check = 1.
+ // force_currency_check = 1 is_admin = 0 -> skip_currency_check = 0.
+ // force_currency_check = 1 is_admin = 1 -> skip_currency_check = 0.
$skip_currency_check = ! $force_currency_check && is_admin();
$processing_payment_method = $this->payment_methods[ $payment_method_id ];
- if ( $processing_payment_method->is_enabled_at_checkout( $this->get_account_country() ) && ( $skip_currency_check || $processing_payment_method->is_currency_valid( $this->get_account_domestic_currency(), $order_id ) ) ) {
+ if ( $processing_payment_method->is_enabled_at_checkout( $this->get_account_country(), $skip_currency_check ) && ( $skip_currency_check || $processing_payment_method->is_currency_valid( $this->get_account_domestic_currency(), $order_id ) ) ) {
$status = $active_payment_methods[ $payment_method_capability_key ]['status'] ?? null;
if ( 'active' === $status ) {
$enabled_payment_methods[] = $payment_method_id;
@@ -3975,7 +4040,6 @@ public function get_upe_available_payment_methods() {
$available_methods = [ 'card' ];
$available_methods[] = Becs_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
- $available_methods[] = Alipay_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Bancontact_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Eps_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Ideal_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
@@ -3985,9 +4049,17 @@ public function get_upe_available_payment_methods() {
$available_methods[] = Affirm_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Afterpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
+ $available_methods[] = Multibanco_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Grabpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Wechatpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
+ // This gets all the registered payment method definitions. As new payment methods are converted from the legacy style, they need to be removed from the list above.
+ $payment_method_definitions = PaymentMethodDefinitionRegistry::instance()->get_all_payment_method_definitions();
+
+ foreach ( $payment_method_definitions as $definition_class ) {
+ $available_methods[] = $definition_class::get_id();
+ }
+
$available_methods = array_values(
apply_filters(
'wcpay_upe_available_payment_methods',
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index 518d189addd..8272d979bb5 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -351,6 +351,7 @@ public function get_account_status_data(): array {
'status' => $account['status'],
'created' => $account['created'] ?? '',
'testDrive' => $account['is_test_drive'] ?? false,
+ 'isLive' => $account['is_live'] ?? false,
'paymentsEnabled' => $account['payments_enabled'],
'detailsSubmitted' => $account['details_submitted'] ?? true,
'deposits' => $account['deposits'] ?? [],
@@ -372,6 +373,11 @@ public function get_account_status_data(): array {
'declineOnAVSFailure' => $account['fraud_mitigation_settings']['avs_check_enabled'] ?? null,
'declineOnCVCFailure' => $account['fraud_mitigation_settings']['cvc_check_enabled'] ?? null,
],
+ // Campaigns are temporary flags that are used to enable/disable features for a limited time.
+ 'campaigns' => [
+ // The flag for the WordPress.org merchant review campaign in 2025. Eligibility is determined per-account on transact-platform-server.
+ 'wporgReview2025' => $account['eligibility_wporg_review_campaign_2025'] ?? false,
+ ],
];
}
diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php
index d80ec898145..3ca93c093cd 100644
--- a/includes/class-wc-payments-checkout.php
+++ b/includes/class-wc-payments-checkout.php
@@ -309,7 +309,7 @@ public function get_payment_fields_js_config() {
*/
public function get_enabled_payment_method_config() {
$settings = [];
- $enabled_payment_methods = $this->gateway->get_payment_method_ids_enabled_at_checkout();
+ $enabled_payment_methods = $this->gateway->get_upe_enabled_payment_method_ids_based_on_manual_capture();
foreach ( $enabled_payment_methods as $payment_method_id ) {
// Link by Stripe should be validated with available fees.
diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php
index 395b430fb43..1a2dd532c63 100644
--- a/includes/class-wc-payments-order-service.php
+++ b/includes/class-wc-payments-order-service.php
@@ -8,6 +8,7 @@
use WCPay\Constants\Fraud_Meta_Box_Type;
use WCPay\Constants\Order_Status;
use WCPay\Constants\Intent_Status;
+use WCPay\Constants\Payment_Method;
use WCPay\Exceptions\Order_Not_Found_Exception;
use WCPay\Fraud_Prevention\Models\Rule;
use WCPay\Logger;
@@ -130,6 +131,34 @@ class WC_Payments_Order_Service {
*/
const WCPAY_PAYMENT_TRANSACTION_ID_META_KEY = '_wcpay_payment_transaction_id';
+ /**
+ * Meta key used to store the Multibanco entity.
+ *
+ * @const string
+ */
+ const WCPAY_MULTIBANCO_ENTITY_META_KEY = '_wcpay_multibanco_entity';
+
+ /**
+ * Meta key used to store the Multibanco reference.
+ *
+ * @const string
+ */
+ const WCPAY_MULTIBANCO_REFERENCE_META_KEY = '_wcpay_multibanco_reference';
+
+ /**
+ * Meta key used to store the Multibanco expiry.
+ *
+ * @const string
+ */
+ const WCPAY_MULTIBANCO_EXPIRY_META_KEY = '_wcpay_multibanco_expiry';
+
+ /**
+ * Meta key used to store the Multibanco URL.
+ *
+ * @const string
+ */
+ const WCPAY_MULTIBANCO_URL_META_KEY = '_wcpay_multibanco_url';
+
/**
* Client for making requests to the WooCommerce Payments API
*
@@ -180,7 +209,14 @@ public function update_order_status_from_intent( $order, $intent ) {
break;
case Intent_Status::REQUIRES_ACTION:
case Intent_Status::REQUIRES_PAYMENT_METHOD:
- $this->mark_payment_started( $order, $intent_data );
+ if ( ! empty( $intent_data['error'] ) ) {
+ $this->unlock_order_payment( $order );
+ $this->mark_payment_failed( $order, $intent_data['intent_id'], $intent_data['intent_status'], $intent_data['charge_id'], $intent_data['error']['message'] );
+ } elseif ( in_array( $intent->get_payment_method_type(), Payment_Method::OFFLINE_PAYMENT_METHODS, true ) ) {
+ $this->mark_payment_on_hold( $order, $intent_data );
+ } else {
+ $this->mark_payment_started( $order, $intent_data );
+ }
break;
default:
Logger::error( 'Uncaught payment intent status of ' . $intent_data['intent_status'] . ' passed for order id: ' . $order->get_id() );
@@ -901,12 +937,13 @@ public function get_fraud_meta_box_type_for_order( $order ): string {
*
* @param WC_Order $order The order.
* @param WC_Payments_API_Payment_Intention|WC_Payments_API_Setup_Intention $intent The payment or setup intention object.
+ * @param bool $allow_update_on_success Whether the payment is being changed for a subscription.
*
* @throws Order_Not_Found_Exception
*/
- public function attach_intent_info_to_order( WC_Order $order, $intent ) {
- // We don't want to allow metadata for a successful payment to be disrupted.
- if ( Intent_Status::SUCCEEDED === $this->get_intention_status_for_order( $order ) ) {
+ public function attach_intent_info_to_order( WC_Order $order, $intent, $allow_update_on_success = false ) {
+ // We don't want to allow metadata for a successful payment to be disrupted (except for when changing payment method for subscription or renewing subscription).
+ if ( Intent_Status::SUCCEEDED === $this->get_intention_status_for_order( $order ) && ! $allow_update_on_success ) {
return;
}
// first, let's prepare all the metadata needed for refunds, required for status change etc.
@@ -1149,6 +1186,28 @@ private function mark_payment_authorized( $order, $intent_data ) {
$this->set_intention_status_for_order( $order, $intent_data['intent_status'] );
}
+ /**
+ * Updates an order to on-hold status, while adding a note with a link to the transaction.
+ *
+ * @param WC_Order $order Order object.
+ * @param array $intent_data The intent data associated with this order.
+ *
+ * @return void
+ */
+ private function mark_payment_on_hold( $order, $intent_data ) {
+ $note = $this->generate_payment_started_note( $order, $intent_data['intent_id'] );
+ if ( $this->order_note_exists( $order, $note ) ) {
+ return;
+ }
+
+ $fraud_meta_box_type = $this->intent_has_card_payment_type( $intent_data ) ? Fraud_Meta_Box_Type::PAYMENT_STARTED : Fraud_Meta_Box_Type::NOT_CARD;
+ $this->set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type );
+
+ $this->update_order_status( $order, Order_Status::ON_HOLD );
+ $order->add_order_note( $note );
+ $this->set_intention_status_for_order( $order, $intent_data['intent_status'] );
+ }
+
/**
* Updates an order to processing/completed status, while adding a note with a link to the transaction.
*
@@ -2056,6 +2115,7 @@ private function get_intent_data( WC_Payments_API_Abstract_Intention $intent ):
if ( $intent instanceof WC_Payments_API_Payment_Intention ) {
$charge = $intent->get_charge();
$intent_data['charge_id'] = $charge ? $charge->get_id() : null;
+ $intent_data['error'] = $intent->get_last_payment_error();
}
return $intent_data;
@@ -2139,13 +2199,13 @@ private function intent_has_card_payment_type( $intent_data ): bool {
* Handle insufficient balance for refund.
*
* @param WC_Order $order The order being refunded.
- * @param int $amount The refund amount.
+ * @param int $stripe_amount The refund amount.
*/
- public function handle_insufficient_balance_for_refund( WC_Order $order, $amount ) {
+ public function handle_insufficient_balance_for_refund( WC_Order $order, int $stripe_amount ) {
$account_country = WC_Payments::get_account_service()->get_account_country();
$formatted_amount = wc_price(
- WC_Payments_Utils::interpret_stripe_amount( $amount, $order->get_currency() ),
+ WC_Payments_Utils::interpret_stripe_amount( $stripe_amount, $order->get_currency() ),
[ 'currency' => $order->get_currency() ]
);
@@ -2156,6 +2216,37 @@ public function handle_insufficient_balance_for_refund( WC_Order $order, $amount
}
}
+ /**
+ * Attach Multibanco information to the order.
+ *
+ * @param WC_Order $order The order being paid.
+ * @param string $reference The Multibanco reference.
+ * @param string $entity The Multibanco entity.
+ * @param string $url The Multibanco URL.
+ * @param int $expiry The Multibanco expiry.
+ */
+ public function attach_multibanco_info_to_order( WC_Order $order, string $reference, string $entity, string $url, int $expiry ): void {
+ $order->update_meta_data( self::WCPAY_MULTIBANCO_REFERENCE_META_KEY, $reference );
+ $order->update_meta_data( self::WCPAY_MULTIBANCO_ENTITY_META_KEY, $entity );
+ $order->update_meta_data( self::WCPAY_MULTIBANCO_URL_META_KEY, $url );
+ $order->update_meta_data( self::WCPAY_MULTIBANCO_EXPIRY_META_KEY, $expiry );
+ }
+
+ /**
+ * Get Multibanco information from the order.
+ *
+ * @param WC_Order $order The order.
+ * @return array
+ */
+ public function get_multibanco_info_from_order( WC_Order $order ): array {
+ return [
+ 'reference' => $order->get_meta( self::WCPAY_MULTIBANCO_REFERENCE_META_KEY ),
+ 'entity' => $order->get_meta( self::WCPAY_MULTIBANCO_ENTITY_META_KEY ),
+ 'url' => $order->get_meta( self::WCPAY_MULTIBANCO_URL_META_KEY ),
+ 'expiry' => $order->get_meta( self::WCPAY_MULTIBANCO_EXPIRY_META_KEY ),
+ ];
+ }
+
/**
* Check if FROD is supported for the given country.
*
diff --git a/includes/class-wc-payments-order-success-page.php b/includes/class-wc-payments-order-success-page.php
index a892771e456..f93081cc955 100644
--- a/includes/class-wc-payments-order-success-page.php
+++ b/includes/class-wc-payments-order-success-page.php
@@ -7,22 +7,38 @@
use WCPay\Constants\Payment_Method;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Core\Server\Request\Get_Intention;
+use WCPay\Constants\Intent_Status;
+use WCPay\Constants\Order_Status;
/**
* Class handling order success page.
*/
class WC_Payments_Order_Success_Page {
+
+ /**
+ * Whether to hide the blocks status description.
+ *
+ * @var bool
+ */
+ private $should_hide_status_description = false;
+
/**
* Constructor.
*/
public function __construct() {
add_filter( 'woocommerce_order_received_verify_known_shoppers', [ $this, 'determine_woopay_order_received_verify_known_shoppers' ], 11 );
add_action( 'woocommerce_before_thankyou', [ $this, 'register_payment_method_override' ] );
+ add_action( 'woocommerce_before_thankyou', [ $this, 'maybe_render_multibanco_payment_instructions' ] );
add_action( 'woocommerce_order_details_before_order_table', [ $this, 'unregister_payment_method_override' ] );
+ add_action( 'woocommerce_order_details_before_order_table', [ $this, 'maybe_render_multibanco_payment_instructions' ] );
add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_paid_order' ], 11 );
add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_successful_intent' ], 11 );
+ add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'replace_order_received_text_for_failed_orders' ], 11 );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
+ add_action( 'woocommerce_email_order_details', [ $this, 'add_multibanco_payment_instructions_to_order_on_hold_email' ], 10, 4 );
+ add_action( 'wp_footer', [ $this, 'output_footer_scripts' ] );
}
/**
@@ -33,6 +49,99 @@ public function register_payment_method_override() {
add_filter( 'woocommerce_order_get_payment_method_title', [ $this, 'show_woocommerce_payments_payment_method_name' ], 10, 2 );
}
+ /**
+ * Maybe render the payment instructions for Multibanco payment method.
+ *
+ * @param int $order_id The order ID.
+ */
+ public function maybe_render_multibanco_payment_instructions( $order_id ) {
+ if ( is_order_received_page() && current_filter() === 'woocommerce_order_details_before_order_table' ) {
+ // Prevent rendering twice on order received page.
+ return;
+ }
+
+ $order = wc_get_order( $order_id );
+
+ if ( ! $order || $order->get_payment_method() !== 'woocommerce_payments_' . Payment_Method::MULTIBANCO || 'on-hold' !== $order->get_status() ) {
+ return;
+ }
+
+ $order_service = WC_Payments::get_order_service();
+ $multibanco_info = $order_service->get_multibanco_info_from_order( $order );
+ $unix_expiry = $multibanco_info['expiry'];
+ $expiry_date = date_i18n( wc_date_format() . ' ' . wc_time_format(), $unix_expiry );
+ $days_remaining = max( 0, floor( ( $unix_expiry - time() ) / DAY_IN_SECONDS ) );
+ $formatted_order_total = $order->get_formatted_order_total();
+ wc_print_notice(
+ __( 'Your order is on hold until payment is received. Please follow the payment instructions by the expiry date.', 'woocommerce-payments' ),
+ 'notice'
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ' )
+ if (
+ version_compare( WC_VERSION, '8.0', '>' )
&& version_compare( WC_VERSION, '8.3', '<' )
) {
echo "
@@ -255,11 +364,86 @@ private function format_addtional_thankyou_order_received_text( string $addition
return sprintf( '%s ', $additional_text );
}
+ /**
+ * Replace the order received text with a failure message when the order status is 'failed'.
+ *
+ * @param string $text The original thank you text.
+ * @return string
+ */
+ public function replace_order_received_text_for_failed_orders( $text ) {
+ global $wp;
+
+ $order_id = absint( $wp->query_vars['order-received'] );
+ $order = wc_get_order( $order_id );
+
+ if ( ! $order ||
+ ! $order->needs_payment() ||
+ 0 !== strpos( $order->get_payment_method(), WC_Payment_Gateway_WCPay::GATEWAY_ID )
+ ) {
+ return $text;
+ }
+
+ $intent_id = $order->get_meta( '_intent_id', true );
+ $payment_method = $order->get_payment_method();
+
+ // Strip the gateway ID prefix from the payment method.
+ $payment_method_type = str_replace( WC_Payment_Gateway_WCPay::GATEWAY_ID . '_', '', $payment_method );
+
+ $should_show_failure = false;
+
+ // Check order status first to avoid unnecessary API calls.
+ if ( $order->has_status( Order_Status::FAILED ) ) {
+ $should_show_failure = true;
+ } elseif ( ! empty( $intent_id ) && ! empty( $payment_method_type ) && in_array( $payment_method_type, Payment_Method::REDIRECT_PAYMENT_METHODS, true ) ) {
+ // For redirect-based payment methods that haven't been marked as failed yet, check the intent status.
+ // Add a small delay to allow the intent to be updated.
+ sleep( 1 );
+
+ $intent = Get_Intention::create( $intent_id );
+ $intent = $intent->send();
+ $intent_status = $intent->get_status();
+
+ if ( Intent_Status::REQUIRES_PAYMENT_METHOD === $intent_status && $intent->get_last_payment_error() ) {
+ $should_show_failure = true;
+ }
+ }
+
+ if ( $should_show_failure ) {
+ // Store the failure state to use in wp_footer.
+ $this->should_hide_status_description = true;
+
+ $checkout_url = wc_get_checkout_url();
+ return sprintf(
+ /* translators: %s: checkout URL */
+ __( 'Unfortunately, your order has failed. Please try checking out again.', 'woocommerce-payments' ),
+ esc_url( $checkout_url )
+ );
+ }
+
+ return $text;
+ }
+
+ /**
+ * Output any necessary footer scripts
+ */
+ public function output_footer_scripts() {
+ if ( ! empty( $this->should_hide_status_description ) ) {
+ echo "
+
+ ";
+ }
+ }
+
/**
* Enqueue style to the order success page
*/
public function enqueue_scripts() {
- if ( ! is_order_received_page() ) {
+ if ( ! is_order_received_page() && ! is_view_order_page() ) {
return;
}
@@ -270,6 +454,10 @@ public function enqueue_scripts() {
WC_Payments::get_file_version( 'assets/css/success.css' ),
'all',
);
+
+ WC_Payments::register_script_with_dependencies( 'WCPAY_SUCCESS_PAGE', 'dist/success', [] );
+ wp_set_script_translations( 'WCPAY_SUCCESS_PAGE', 'woocommerce-payments' );
+ wp_enqueue_script( 'WCPAY_SUCCESS_PAGE' );
}
/**
@@ -300,4 +488,113 @@ public function determine_woopay_order_received_verify_known_shoppers( $value )
return ! $is_within_grace_period;
}
+
+ /**
+ * Add Multibanco payment instructions to the order on-hold email.
+ *
+ * @param WC_Order|mixed $order The order object.
+ * @param bool|mixed $sent_to_admin Whether the email is being sent to the admin.
+ * @param bool|mixed $plain_text Whether the email is plain text.
+ * @param WC_Email|string $email The email object.
+ */
+ public function add_multibanco_payment_instructions_to_order_on_hold_email( $order, $sent_to_admin = false, $plain_text = false, $email = '' ): void {
+ if ( ! $email instanceof WC_Email_Customer_On_Hold_Order || ! $order || $order->get_payment_method() !== 'woocommerce_payments_' . Payment_Method::MULTIBANCO ) {
+ return;
+ }
+
+ $order_service = WC_Payments::get_order_service();
+ $multibanco_info = $order_service->get_multibanco_info_from_order( $order );
+ $unix_expiry = $multibanco_info['expiry'];
+ $expiry_date = date_i18n( wc_date_format() . ' ' . wc_time_format(), $unix_expiry );
+ $formatted_order_total = $order->get_formatted_order_total();
+
+ if ( $plain_text ) {
+ echo "----------------------------------------\n";
+ echo __( 'Multibanco Payment instructions', 'woocommerce-payments' ) . "\n\n";
+ printf(
+ /* translators: %s: expiry date */
+ __( 'Expires %s', 'woocommerce-payments' ) . "\n\n",
+ $expiry_date
+ );
+ echo '1. ' . __( 'In your online bank account or from an ATM, choose "Payment and other services".', 'woocommerce-payments' ) . "\n";
+ echo '2. ' . __( 'Click "Payments of services/shopping".', 'woocommerce-payments' ) . "\n";
+ echo '3. ' . __( 'Enter the entity number, reference number, and amount.', 'woocommerce-payments' ) . "\n\n";
+ echo __( 'Entity', 'woocommerce-payments' ) . ': ' . $multibanco_info['entity'] . "\n";
+ echo __( 'Reference', 'woocommerce-payments' ) . ': ' . $multibanco_info['reference'] . "\n";
+ echo __( 'Amount', 'woocommerce-payments' ) . ': ' . wp_strip_all_tags( $formatted_order_total ) . "\n";
+ echo "----------------------------------------\n\n";
+ } else {
+ ?>
+
+
+
+
+
+
+
+
+  ); ?>)
+
+ |
+
+ get_order_number() );
+ ?>
+ |
+
+
+ |
+ %s', 'woocommerce-payments' ),
+ [
+ 'strong' => '',
+ ]
+ ),
+ $expiry_date
+ );
+ ?>
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+
+ |
+
+
+
+
+
+ init_session_cookie();
if ( $this->_customer_id !== $this->_data['token_customer_id'] ) {
+ \WCPay\Logger::error(
+ sprintf(
+ 'Tokenized ECE cookie and session customer mismatch - customer: %s (%s) , session: %s (%s)',
+ var_export( $this->_customer_id, true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- should only be triggered when logging is enabled.
+ gettype( $this->_customer_id ),
+ var_export( $this->_data['token_customer_id'], true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- should only be triggered when logging is enabled.
+ gettype( $this->_data['token_customer_id'] )
+ )
+ );
+
+ // throwing an exception here to prevent further processing of the request.
throw new Exception( __( 'Invalid token: cookie and session customer mismatch', 'woocommerce-payments' ) );
}
diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php
index 86b37b914c2..dd38b8d2c09 100644
--- a/includes/class-wc-payments-utils.php
+++ b/includes/class-wc-payments-utils.php
@@ -58,19 +58,19 @@ class WC_Payments_Utils {
* Mirrors JS's createInterpolateElement functionality.
* Returns a string where angle brackets expressions are replaced with unescaped html while the rest is escaped.
*
- * @param string $string string to process.
+ * @param string $text string to process.
* @param array $element_map map of elements to not escape.
*
* @return string String where all of the html was escaped, except for the tags specified in element map.
*/
- public static function esc_interpolated_html( $string, $element_map ) {
+ public static function esc_interpolated_html( $text, $element_map ) {
// Regex to match string expressions wrapped in angle brackets.
$tokenizer = '/<(\/)?(\w+)\s*(\/)?>/';
$string_queue = [];
$token_queue = [];
$last_mapped = true;
// Start with a copy of the string.
- $processed = $string;
+ $processed = $text;
// Match every angle bracket expression.
while ( preg_match( $tokenizer, $processed, $matches ) ) {
@@ -97,7 +97,7 @@ public static function esc_interpolated_html( $string, $element_map ) {
$map_matched = preg_match( '/^<(\w+)(\s.+?)?\/?>$/', $element_map[ $token ], $map_matches );
if ( ! $map_matches ) {
// Should not happen with the properly formatted html as map value. Return the whole string escaped.
- return esc_html( $string );
+ return esc_html( $text );
}
// Add the matched token and its attributes into the token queue. It will not be escaped when constructing the final string.
$tag = $map_matches[1];
@@ -123,7 +123,7 @@ public static function esc_interpolated_html( $string, $element_map ) {
// No mapped tokens were found in the string, or token and string queues are not of equal length.
// The latter should not happen - token queue and string queue should be the same length.
if ( empty( $token_queue ) || count( $token_queue ) !== count( $string_queue ) ) {
- return esc_html( $string );
+ return esc_html( $text );
}
// Construct the final string by escaping the string queue values and not escaping the token queue.
@@ -361,20 +361,20 @@ public static function map_search_orders_to_charge_ids( $search ) {
/**
* Redacts the provided array, removing the sensitive information, and limits its depth to LOG_MAX_RECURSION.
*
- * @param object|array $array The array to redact.
+ * @param object|array $input The array to redact.
* @param array $keys_to_redact The keys whose values need to be redacted.
* @param integer $level The current recursion level.
*
* @return string|array The redacted array.
*/
- public static function redact_array( $array, array $keys_to_redact, int $level = 0 ) {
- if ( is_object( $array ) ) {
+ public static function redact_array( $input, array $keys_to_redact, int $level = 0 ) {
+ if ( is_object( $input ) ) {
// TODO: if we ever want to log objects, they could implement a method returning an array or a string.
- return get_class( $array ) . '()';
+ return get_class( $input ) . '()';
}
- if ( ! is_array( $array ) ) {
- return $array;
+ if ( ! is_array( $input ) ) {
+ return $input;
}
if ( $level >= self::MAX_ARRAY_DEPTH ) {
@@ -383,7 +383,7 @@ public static function redact_array( $array, array $keys_to_redact, int $level =
$result = [];
- foreach ( $array as $key => $value ) {
+ foreach ( $input as $key => $value ) {
if ( in_array( $key, $keys_to_redact, true ) ) {
$result[ $key ] = '(redacted)';
continue;
@@ -398,23 +398,23 @@ public static function redact_array( $array, array $keys_to_redact, int $level =
/**
* Apply a callback on every value in an array, regardless of the number of array dimensions.
*
- * @param array $array The array to map.
+ * @param array $input The array to map.
* @param callable $callback The callback to apply.
*
* @return array The mapped array.
*/
- public static function array_map_recursive( array $array, callable $callback ): array {
- foreach ( $array as $key => $value ) {
+ public static function array_map_recursive( array $input, callable $callback ): array {
+ foreach ( $input as $key => $value ) {
if ( \is_array( $value ) ) {
$value = self::array_map_recursive( $value, $callback );
} else {
- $value = $callback( $value, $key, $array );
+ $value = $callback( $value, $key, $input );
}
- $array[ $key ] = $value;
+ $input[ $key ] = $value;
}
- return $array;
+ return $input;
}
/**
@@ -424,31 +424,31 @@ public static function array_map_recursive( array $array, callable $callback ):
*
* @see https://www.php.net/manual/en/function.array-filter.php
*
- * @param array $array The array to filter.
+ * @param array $input The array to filter.
* @param callable|null $callback Optional. The callback to apply.
* The callback should return true to keep the value, false otherwise.
* If no callback is provided, all non-truthy values will be removed.
*
* @return array The filtered array.
*/
- public static function array_filter_recursive( array $array, ?callable $callback = null ): array {
- foreach ( $array as $key => &$value ) { // Mind the use of a reference.
+ public static function array_filter_recursive( array $input, ?callable $callback = null ): array {
+ foreach ( $input as $key => &$value ) { // Mind the use of a reference.
if ( \is_array( $value ) ) {
$value = self::array_filter_recursive( $value, $callback );
if ( ! $value ) {
- unset( $array[ $key ] );
+ unset( $input[ $key ] );
}
} elseif ( ! is_null( $callback ) ) {
if ( ! $callback( $value ) ) {
- unset( $array[ $key ] );
+ unset( $input[ $key ] );
}
} elseif ( ! $value ) {
- unset( $array[ $key ] );
+ unset( $input[ $key ] );
}
}
unset( $value ); // Kill the reference to avoid memory leaks.
- return $array;
+ return $input;
}
/**
diff --git a/includes/class-wc-payments-webhook-processing-service.php b/includes/class-wc-payments-webhook-processing-service.php
index 492c4da79be..4a4268b9cfc 100644
--- a/includes/class-wc-payments-webhook-processing-service.php
+++ b/includes/class-wc-payments-webhook-processing-service.php
@@ -426,6 +426,7 @@ private function process_webhook_payment_intent_failed( $event_body ) {
Payment_Method::CARD_PRESENT,
Payment_Method::US_BANK_ACCOUNT,
Payment_Method::BECS,
+ Payment_Method::WECHAT_PAY,
];
if ( empty( $payment_method_type ) || ! in_array( $payment_method_type, $actionable_methods, true ) ) {
@@ -689,14 +690,14 @@ private function process_wcpay_notification( array $event_body ) {
/**
* Safely get a value from the webhook event body array.
*
- * @param array $array Array to read from.
+ * @param array $items Array to read from.
* @param string $key ID to fetch on.
*
* @return string|array|int|bool
* @throws Invalid_Webhook_Data_Exception Thrown if ID not set.
*/
- private function read_webhook_property( $array, $key ) {
- if ( ! isset( $array[ $key ] ) ) {
+ private function read_webhook_property( $items, $key ) {
+ if ( ! isset( $items[ $key ] ) ) {
throw new Invalid_Webhook_Data_Exception(
sprintf(
/* translators: %1: ID being fetched */
@@ -705,19 +706,19 @@ private function read_webhook_property( $array, $key ) {
)
);
}
- return $array[ $key ];
+ return $items[ $key ];
}
/**
* Safely check whether a webhook contains a property.
*
- * @param array $array Array to read from.
+ * @param array $items Array to read from.
* @param string $key ID to fetch on.
*
* @return bool
*/
- private function has_webhook_property( $array, $key ) {
- return isset( $array[ $key ] );
+ private function has_webhook_property( $items, $key ) {
+ return isset( $items[ $key ] );
}
/**
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 4149785dede..41759213ddf 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -13,7 +13,6 @@
use WCPay\Core\Server\Request;
use WCPay\Migrations\Allowed_Payment_Request_Button_Types_Update;
use WCPay\Payment_Methods\CC_Payment_Method;
-use WCPay\Payment_Methods\Alipay_Payment_Method;
use WCPay\Payment_Methods\Bancontact_Payment_Method;
use WCPay\Payment_Methods\Becs_Payment_Method;
use WCPay\Payment_Methods\Giropay_Payment_Method;
@@ -25,6 +24,7 @@
use WCPay\Payment_Methods\Eps_Payment_Method;
use WCPay\Payment_Methods\Wechatpay_Payment_Method;
use WCPay\Payment_Methods\UPE_Payment_Method;
+use WCPay\Payment_Methods\Multibanco_Payment_Method;
use WCPay\WooPay_Tracker;
use WCPay\WooPay\WooPay_Utilities;
use WCPay\WooPay\WooPay_Order_Status_Sync;
@@ -46,6 +46,7 @@
use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\Grabpay_Payment_Method;
use WCPay\WC_Payments_Currency_Manager;
+use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry;
/**
* Main class for the WooPayments extension. Its responsibility is to initialize the extension.
@@ -348,6 +349,7 @@ public static function init() {
}
add_action( 'admin_init', [ __CLASS__, 'add_woo_admin_notes' ] );
+ add_action( 'admin_init', [ __CLASS__, 'remove_deprecated_notes' ] );
add_action( 'init', [ __CLASS__, 'install_actions' ] );
add_action( 'woocommerce_blocks_payment_method_type_registration', [ __CLASS__, 'register_checkout_gateway' ] );
@@ -428,7 +430,6 @@ public static function init() {
include_once __DIR__ . '/payment-methods/class-cc-payment-gateway.php';
include_once __DIR__ . '/payment-methods/class-upe-payment-method.php';
include_once __DIR__ . '/payment-methods/class-cc-payment-method.php';
- include_once __DIR__ . '/payment-methods/class-alipay-payment-method.php';
include_once __DIR__ . '/payment-methods/class-bancontact-payment-method.php';
include_once __DIR__ . '/payment-methods/class-sepa-payment-method.php';
include_once __DIR__ . '/payment-methods/class-giropay-payment-method.php';
@@ -441,6 +442,7 @@ public static function init() {
include_once __DIR__ . '/payment-methods/class-affirm-payment-method.php';
include_once __DIR__ . '/payment-methods/class-afterpay-payment-method.php';
include_once __DIR__ . '/payment-methods/class-klarna-payment-method.php';
+ include_once __DIR__ . '/payment-methods/class-multibanco-payment-method.php';
include_once __DIR__ . '/payment-methods/class-grabpay-payment-method.php';
include_once __DIR__ . '/payment-methods/class-wechatpay-payment-method.php';
include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-helper.php';
@@ -568,7 +570,6 @@ public static function init() {
$payment_method_classes = [
CC_Payment_Method::class,
- Alipay_Payment_Method::class,
Bancontact_Payment_Method::class,
Sepa_Payment_Method::class,
Giropay_Payment_Method::class,
@@ -581,15 +582,30 @@ public static function init() {
Affirm_Payment_Method::class,
Afterpay_Payment_Method::class,
Klarna_Payment_Method::class,
+ Multibanco_Payment_Method::class,
Grabpay_Payment_Method::class,
Wechatpay_Payment_Method::class,
];
$payment_methods = [];
+ // Initialize legacy payment methods.
foreach ( $payment_method_classes as $payment_method_class ) {
$payment_method = new $payment_method_class( self::$token_service );
$payment_methods[ $payment_method->get_id() ] = $payment_method;
}
+
+ // Initialize definition-based payment methods.
+ // Initialize and get payment method classes from the registry for those that have been converted.
+ $registry = PaymentMethodDefinitionRegistry::instance();
+ $registry->init();
+
+ $payment_method_definitions = $registry->get_all_payment_method_definitions();
+
+ foreach ( $payment_method_definitions as $definition_class ) {
+ $payment_method = new UPE_Payment_Method( self::$token_service, $definition_class );
+ $payment_methods[ $payment_method->get_id() ] = $payment_method;
+ }
+
foreach ( $payment_methods as $payment_method ) {
self::$payment_method_map[ $payment_method->get_id() ] = $payment_method;
@@ -658,9 +674,8 @@ function () {
}
add_filter( 'woocommerce_payment_gateways', [ __CLASS__, 'register_gateway' ] );
- add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ], 2 );
- add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ], 3 );
- add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'replace_wcpay_gateway_with_payment_methods' ], 4 );
+ add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'order_woopayments_gateways' ], 2 );
+ add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'order_woopayments_gateways' ], 3 );
add_filter( 'woocommerce_rest_api_option_permissions', [ __CLASS__, 'add_wcpay_options_to_woocommerce_permissions_list' ], 5 );
add_filter( 'woocommerce_admin_get_user_data_fields', [ __CLASS__, 'add_user_data_fields' ] );
@@ -704,6 +719,7 @@ function () {
// Add admin screens.
if ( is_admin() ) {
+ include_once WCPAY_ABSPATH . 'includes/inline-script-payloads/class-woo-payments-payment-method-definitions.php';
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-payments-admin.php';
}
@@ -738,10 +754,8 @@ function () {
}
// Load Stripe Billing subscription integration.
- if ( WC_Payments_Features::is_stripe_billing_eligible() ) {
- include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions.php';
- WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account, self::$token_service );
- }
+ include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions.php';
+ WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account, self::$token_service );
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.9.0', '<' ) ) {
add_action( 'woocommerce_onboarding_profile_data_updated', 'WC_Payments_Features::maybe_enable_wcpay_subscriptions_after_onboarding', 10, 2 );
@@ -866,54 +880,52 @@ public static function hide_gateways_on_settings_page() {
}
/**
- * By default, new payment gateways are put at the bottom of the list on the admin "Payments" settings screen.
- * For visibility, we want WooPayments to be at the top of the list.
+ * Sets WooPayments gateways at the beginning if not already in the ordering.
+ * Sets WooPayments gateways after the main gateway if already in the ordering.
*
* @param array $ordering Existing ordering of the payment gateways.
*
* @return array Modified ordering.
*/
- public static function set_gateway_top_of_list( $ordering ) {
- $ordering = (array) $ordering;
- $id = self::get_gateway()->id;
- // Only tweak the ordering if the list hasn't been reordered with WooPayments in it already.
- if ( ! isset( $ordering[ $id ] ) || ! is_numeric( $ordering[ $id ] ) ) {
- $ordering[ $id ] = empty( $ordering ) ? 0 : ( min( $ordering ) - 1 );
- }
- return $ordering;
- }
+ public static function order_woopayments_gateways( $ordering ) {
+ try {
+ $ordering = (array) $ordering;
- /**
- * Replace the main WCPay gateway with all WCPay payment methods
- * when retrieving the "woocommerce_gateway_order" option.
- *
- * @param array $ordering Gateway order.
- *
- * @return array
- */
- public static function replace_wcpay_gateway_with_payment_methods( $ordering ) {
- $ordering = (array) $ordering;
- $wcpay_index = array_search(
- self::get_gateway()->id,
- array_keys( $ordering ),
- true
- );
+ $woopayments_payment_methods = array_flip( self::get_woopayments_gateway_ids() );
+ $main_gateway_id = self::get_gateway()->id;
+ $main_gateway_position = $ordering[ $main_gateway_id ] ?? null;
- if ( false === $wcpay_index ) {
- // The main WCPay gateway isn't on the list.
- return $ordering;
- }
+ $before = [];
+ $after = [];
- $method_order = self::get_gateway()->get_option( 'payment_method_order', [] );
+ foreach ( $ordering as $gateway_id => $position ) {
+ if ( null === $main_gateway_position || $position < $main_gateway_position ) {
+ $before[ $gateway_id ] = null; // `null` for now, the position will be set later.
+ } elseif ( $position > $main_gateway_position && ! isset( $woopayments_payment_methods[ $gateway_id ] ) ) {
+ $after[ $gateway_id ] = null; // `null` for now, the position will be set later.
+ }
+ }
- if ( empty( $method_order ) ) {
- return $ordering;
- }
+ $new_ordering = [];
+ if ( null === $main_gateway_position ) {
+ $new_ordering = array_merge( $woopayments_payment_methods, $before, $after );
+ } else {
+ $new_ordering = array_merge( $before, $woopayments_payment_methods, $after );
+ }
- $ordering = array_keys( $ordering );
+ $index = 0;
+ foreach ( array_keys( $new_ordering ) as $gateway_id ) {
+ $new_ordering[ $gateway_id ] = $index++;
+ }
- array_splice( $ordering, $wcpay_index, 1, $method_order );
- return array_flip( $ordering );
+ return $new_ordering;
+ } catch ( Exception $e ) {
+ if ( function_exists( 'wc_get_logger' ) ) {
+ $logger = wc_get_logger();
+ $logger->warning( 'Failed to order gateways: ' . $e->getMessage(), [ 'source' => 'woopayments' ] );
+ }
+ return $ordering;
+ }
}
/**
@@ -939,6 +951,9 @@ public static function add_user_data_fields( $user_data_fields ) {
'wc_payments_payouts_hidden_columns',
'wc_payments_disputes_hidden_columns',
'wc_payments_documents_hidden_columns',
+
+ // WPORG 2025 merchant feedback prompt user dismissed state.
+ 'wc_payments_wporg_review_2025_prompt_dismissed',
]
);
}
@@ -1244,6 +1259,19 @@ public static function get_payment_gateway_by_id( $payment_method_id ) {
return self::$payment_gateway_map[ $payment_method_id ];
}
+ /**
+ * Returns the WooPayments gateway IDs.
+ *
+ * @return array
+ */
+ public static function get_woopayments_gateway_ids() {
+ $wcpay_gateway_ids = [];
+ foreach ( self::get_payment_gateway_map() as $gateway ) {
+ $wcpay_gateway_ids[] = $gateway->id;
+ }
+ return $wcpay_gateway_ids;
+ }
+
/**
* Returns Payment Method map.
*
@@ -1501,9 +1529,6 @@ public static function add_woo_admin_notes() {
}
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) {
- require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-qualitative-feedback.php';
- WC_Payments_Notes_Qualitative_Feedback::possibly_add_note();
-
require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-set-https-for-checkout.php';
WC_Payments_Notes_Set_Https_For_Checkout::possibly_add_note();
@@ -1519,6 +1544,18 @@ public static function add_woo_admin_notes() {
add_filter( 'admin_notices', [ __CLASS__, 'wcpay_show_old_woocommerce_for_hungary_sweden_and_czech_republic' ] );
}
+ /**
+ * Removes deprecated notes i.e. no longer required notes.
+ *
+ * @return void
+ */
+ public static function remove_deprecated_notes() {
+ if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) {
+ require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-qualitative-feedback.php';
+ WC_Payments_Notes_Qualitative_Feedback::possibly_delete_note();
+ }
+ }
+
/**
* Shows an alert notice for Norwegian merchants on WooCommerce 7.4 and below
*/
diff --git a/includes/compat/multi-currency/class-wc-payments-currency-manager.php b/includes/compat/multi-currency/class-wc-payments-currency-manager.php
index c00da0d1a69..b20c7bc18de 100644
--- a/includes/compat/multi-currency/class-wc-payments-currency-manager.php
+++ b/includes/compat/multi-currency/class-wc-payments-currency-manager.php
@@ -8,7 +8,6 @@
namespace WCPay;
use WC_Payment_Gateway_WCPay;
-use WCPay\Constants\Payment_Method;
defined( 'ABSPATH' ) || exit;
@@ -66,25 +65,17 @@ public function get_multi_currency_instance() {
* @return array The currencies keyed with the related payment method
*/
public function get_enabled_payment_method_currencies() {
- $enabled_payment_method_ids = $this->gateway->get_upe_enabled_payment_method_ids();
+ $enabled_payment_method_ids = $this->gateway->get_upe_enabled_payment_method_ids();
+ // getting all the payment methods that are also present in `$enabled_payment_method_ids`.
+ $enabled_payment_methods = array_values( array_intersect_key( $this->gateway->wc_payments_get_payment_method_map(), array_flip( $enabled_payment_method_ids ) ) );
$account_currency = $this->gateway->get_account_domestic_currency();
$payment_methods_needing_currency = array_reduce(
- $enabled_payment_method_ids,
- function ( $result, $method ) use ( $account_currency ) {
- if ( in_array( $method, [ 'card', 'card_present' ], true ) ) {
+ $enabled_payment_methods,
+ function ( $result, $payment_method_instance ) use ( $account_currency ) {
+ $method = $payment_method_instance->get_id();
+ if ( in_array( $method, [ 'card', 'card_present', 'link' ], true ) ) {
return $result;
}
- try {
- $method_key = Payment_Method::search( $method );
- } catch ( \InvalidArgumentException $e ) {
- return $result;
- }
- $class_key = ucfirst( strtolower( $method_key ? $method_key : $method ) );
- $class_name = "\\WCPay\\Payment_Methods\\{$class_key}_Payment_Method";
- if ( ! class_exists( $class_name ) ) {
- return $result;
- }
- $payment_method_instance = new $class_name( null );
$result[ $method ] = [
'currencies' => $payment_method_instance->has_domestic_transactions_restrictions() ? [ $account_currency ] : $payment_method_instance->get_currencies(),
diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
index 53d6cb43200..ca9449c071c 100644
--- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
+++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
@@ -225,6 +225,39 @@ public function maybe_init_subscriptions_hooks() {
// Update subscriptions token when user sets a default payment method.
add_filter( 'woocommerce_subscriptions_update_subscription_token', [ $this, 'update_subscription_token' ], 10, 3 );
+ add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_method_for_subscriptions' ], 10, 3 );
+ }
+
+ /**
+ * Stops WC Subscriptions from updating the payment method for subscriptions.
+ *
+ * @param bool $update_payment_method Whether to update the payment method.
+ * @param string $new_payment_method The new payment method.
+ * @param WC_Subscription $subscription The subscription.
+ * @return bool
+ */
+ public function update_payment_method_for_subscriptions( $update_payment_method, $new_payment_method, $subscription ) {
+ // Skip if the change payment method request was not made yet.
+ if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wcsnonce'] ), 'wcs_change_payment_method' ) ) {
+ return $update_payment_method;
+ }
+
+ // Avoid interfering with use-cases not related to updating payment method for subscriptions.
+ if ( ! $this->is_changing_payment_method_for_subscription() ) {
+ return $update_payment_method;
+ }
+
+ // Avoid interfering with other payment gateways' operations.
+ if ( $new_payment_method !== $this->id ) {
+ return $update_payment_method;
+ }
+
+ // If the payment method is a saved payment method, we don't need to stop WC Subscriptions from updating it.
+ if ( ( isset( $_POST[ 'wc-' . $new_payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $new_payment_method . '-payment-token' ] ) ) {
+ return $update_payment_method;
+ }
+
+ return false;
}
/**
@@ -326,6 +359,7 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) {
$token = $this->get_payment_token( $renewal_order );
if ( is_null( $token ) && ! WC_Payments::is_network_saved_cards_enabled() ) {
+ $renewal_order->add_order_note( 'Subscription renewal failed: No saved payment method found.' );
Logger::error( 'There is no saved payment token for order #' . $renewal_order->get_id() );
// TODO: Update to use Order_Service->mark_payment_failed.
$renewal_order->update_status( 'failed' );
@@ -380,6 +414,7 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) {
public function update_failing_payment_method( $subscription, $renewal_order ) {
$renewal_token = $this->get_payment_token( $renewal_order );
if ( is_null( $renewal_token ) ) {
+ $renewal_order->add_order_note( 'Unable to update subscription payment method: No valid payment token or method found.' );
Logger::error( 'Failing subscription could not be updated: there is no saved payment token for order #' . $renewal_order->get_id() );
return;
}
diff --git a/includes/constants/class-payment-method.php b/includes/constants/class-payment-method.php
index 013e3b6c9fa..24c5cb8b831 100644
--- a/includes/constants/class-payment-method.php
+++ b/includes/constants/class-payment-method.php
@@ -36,6 +36,7 @@ class Payment_Method extends Base_Constant {
const AFFIRM = 'affirm';
const AFTERPAY = 'afterpay_clearpay';
const KLARNA = 'klarna';
+ const MULTIBANCO = 'multibanco';
const GRABPAY = 'grabpay';
const WECHAT_PAY = 'wechat_pay';
@@ -49,4 +50,12 @@ class Payment_Method extends Base_Constant {
self::AFTERPAY,
self::KLARNA,
];
+
+ const OFFLINE_PAYMENT_METHODS = [
+ self::MULTIBANCO,
+ ];
+
+ const REDIRECT_PAYMENT_METHODS = [
+ self::WECHAT_PAY,
+ ];
}
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
index 413b51aa064..073e09b0ad9 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
@@ -307,6 +307,7 @@ public function scripts() {
}
wp_localize_script( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'wcpayExpressCheckoutParams', $express_checkout_params );
+ wp_localize_script( 'WCPAY_BLOCKS_CHECKOUT', 'wcpayExpressCheckoutParams', $express_checkout_params );
wp_set_script_translations( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'woocommerce-payments' );
diff --git a/includes/inline-script-payloads/class-woo-payments-payment-method-definitions.php b/includes/inline-script-payloads/class-woo-payments-payment-method-definitions.php
new file mode 100644
index 00000000000..c50046abe79
--- /dev/null
+++ b/includes/inline-script-payloads/class-woo-payments-payment-method-definitions.php
@@ -0,0 +1,33 @@
+
get_cached_account_data();
+ $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : '';
+
+ if ( Country_Code::AUSTRALIA === $account_country ) {
+ return [ Currency_Code::AUSTRALIAN_DOLLAR ];
+ }
+
+ if ( Country_Code::CANADA === $account_country ) {
+ return [ Currency_Code::CANADIAN_DOLLAR ];
+ }
+
+ if ( Country_Code::UNITED_KINGDOM === $account_country ) {
+ return [ Currency_Code::POUND_STERLING ];
+ }
+
+ if ( Country_Code::HONG_KONG === $account_country ) {
+ return [ Currency_Code::HONG_KONG_DOLLAR ];
+ }
+
+ if ( Country_Code::JAPAN === $account_country ) {
+ return [ Currency_Code::JAPANESE_YEN ];
+ }
+
+ if ( Country_Code::NEW_ZEALAND === $account_country ) {
+ return [ Currency_Code::NEW_ZEALAND_DOLLAR ];
+ }
+
+ if ( Country_Code::SINGAPORE === $account_country ) {
+ return [ Currency_Code::SINGAPORE_DOLLAR ];
+ }
+
+ if ( Country_Code::UNITED_STATES === $account_country ) {
+ return [ Currency_Code::UNITED_STATES_DOLLAR ];
+ }
+
+ if ( Country_Code::HUNGARY === $account_country ) {
+ return [ Currency_Code::HUNGARIAN_FORINT ];
+ }
+
+ if ( in_array(
+ $account_country,
+ [
+ Country_Code::AUSTRIA,
+ Country_Code::BELGIUM,
+ Country_Code::BULGARIA,
+ Country_Code::CYPRUS,
+ Country_Code::CZECHIA,
+ Country_Code::DENMARK,
+ Country_Code::ESTONIA,
+ Country_Code::FINLAND,
+ Country_Code::FRANCE,
+ Country_Code::GERMANY,
+ Country_Code::GREECE,
+ Country_Code::IRELAND,
+ Country_Code::ITALY,
+ Country_Code::LATVIA,
+ Country_Code::LITHUANIA,
+ Country_Code::LUXEMBOURG,
+ Country_Code::MALTA,
+ Country_Code::NETHERLANDS,
+ Country_Code::NORWAY,
+ Country_Code::PORTUGAL,
+ Country_Code::ROMANIA,
+ Country_Code::SLOVAKIA,
+ Country_Code::SLOVENIA,
+ Country_Code::SPAIN,
+ Country_Code::SWEDEN,
+ Country_Code::SWITZERLAND,
+ Country_Code::CROATIA,
+ ],
+ true
+ ) ) {
+ return [ Currency_Code::EURO ];
+ }
+
+ // fallback currency code, just in case.
+ return [ Currency_Code::CHINESE_YUAN ];
+ }
+
+ /**
+ * Get the list of supported countries
+ *
+ * @return string[] Array of country codes
+ */
+ public static function get_supported_countries(): array {
+ return [];
+ }
+
+ /**
+ * Get the payment method capabilities
+ *
+ * @return string[]
+ */
+ public static function get_capabilities(): array {
+ return [
+ PaymentMethodCapability::REFUNDS,
+ PaymentMethodCapability::MULTI_CURRENCY,
+ ];
+ }
+
+ /**
+ * Get the URL for the payment method's icon
+ *
+ * @param string|null $account_country Optional. The merchant's account country.
+ *
+ * @return string
+ */
+ public static function get_icon_url( ?string $account_country = null ): string {
+ return plugins_url( 'assets/images/payment-methods/alipay-logo.svg', WCPAY_PLUGIN_FILE );
+ }
+
+ /**
+ * Get the URL for the payment method's dark mode icon
+ *
+ * @param string|null $account_country Optional. The merchant's account country.
+ *
+ * @return string Returns regular icon URL if no dark mode icon exists
+ */
+ public static function get_dark_icon_url( ?string $account_country = null ): string {
+ return self::get_icon_url( $account_country );
+ }
+
+ /**
+ * Get the URL for the payment method's settings icon
+ *
+ * @return string
+ */
+ public static function get_settings_icon_url(): string {
+ return self::get_icon_url();
+ }
+
+ /**
+ * Get the testing instructions for the payment method
+ *
+ * @param string $account_country The merchant's account country.
+ * @return string HTML string containing testing instructions
+ */
+ public static function get_testing_instructions( string $account_country ): string {
+ return '';
+ }
+
+ /**
+ * Get the currency limits for the payment method
+ *
+ * @return array>
+ */
+ public static function get_limits_per_currency(): array {
+ return [];
+ }
+
+ /**
+ * Whether this payment method is available for the given currency and country
+ *
+ * @param string $currency The currency code to check.
+ * @param string $account_country The merchant's account country.
+ *
+ * @return bool
+ */
+ public static function is_available_for( string $currency, string $account_country ): bool {
+ if ( ! PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries(), $currency, $account_country ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether this payment method should be enabled by default
+ *
+ * @return bool
+ */
+ public static function is_enabled_by_default(): bool {
+ return false;
+ }
+
+ /**
+ * Get the minimum amount for this payment method for a given currency and country
+ *
+ * @param string $currency The currency code.
+ * @param string $country The country code.
+ *
+ * @return int|null The minimum amount or null if no minimum.
+ */
+ public static function get_minimum_amount( string $currency, string $country ): ?int {
+ return null;
+ }
+
+ /**
+ * Get the maximum amount for this payment method for a given currency and country
+ *
+ * @param string $currency The currency code.
+ * @param string $country The country code.
+ *
+ * @return int|null The maximum amount or null if no maximum.
+ */
+ public static function get_maximum_amount( string $currency, string $country ): ?int {
+ return null;
+ }
+}
diff --git a/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php b/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php
new file mode 100644
index 00000000000..234a825ab48
--- /dev/null
+++ b/includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php
@@ -0,0 +1,175 @@
+>
+ */
+ public static function get_limits_per_currency(): array;
+
+ /**
+ * Get minimum amount for a currency and country
+ *
+ * @param string $currency The currency code.
+ * @param string $country The country code.
+ * @return int|null Returns null if no limit is set
+ */
+ public static function get_minimum_amount( string $currency, string $country ): ?int;
+
+ /**
+ * Get maximum amount for a currency and country
+ *
+ * @param string $currency The currency code.
+ * @param string $country The country code.
+ * @return int|null Returns null if no limit is set
+ */
+ public static function get_maximum_amount( string $currency, string $country ): ?int;
+}
diff --git a/includes/payment-methods/Configs/Registry/PaymentMethodDefinitionRegistry.php b/includes/payment-methods/Configs/Registry/PaymentMethodDefinitionRegistry.php
new file mode 100644
index 00000000000..da70308275e
--- /dev/null
+++ b/includes/payment-methods/Configs/Registry/PaymentMethodDefinitionRegistry.php
@@ -0,0 +1,139 @@
+>
+ */
+ private $available_definitions = [
+ // Add new payment method definitions here.
+ AlipayDefinition::class,
+ ];
+
+ /**
+ * Payment method definitions that have been registered for use.
+ *
+ * @var array>
+ */
+ private $payment_methods = [];
+
+ /**
+ * Constructor is private to enforce singleton pattern.
+ */
+ private function __construct() {}
+
+ /**
+ * Get the singleton instance.
+ *
+ * @return PaymentMethodDefinitionRegistry
+ */
+ public static function instance(): self {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Initialize the registry by registering all available payment method definitions.
+ *
+ * @return void
+ */
+ public function init(): void {
+ foreach ( $this->available_definitions as $definition ) {
+ $this->register_payment_method( $definition );
+ }
+ }
+
+ /**
+ * Get all available payment method definitions.
+ *
+ * @return array> Array of payment method definition class names.
+ */
+ public function get_available_definitions(): array {
+ return $this->available_definitions;
+ }
+
+ /**
+ * Register a payment method definition.
+ *
+ * @param string $definition_class The payment method definition class to register.
+ * @psalm-param class-string $definition_class
+ * @throws \InvalidArgumentException If the class does not exist or does not implement PaymentMethodDefinitionInterface.
+ */
+ public function register_payment_method( string $definition_class ): void {
+ if ( ! class_exists( $definition_class ) ) {
+ throw new \InvalidArgumentException(
+ sprintf(
+ 'Payment method definition class "%s" does not exist.',
+ $definition_class
+ )
+ );
+ }
+
+ $interfaces = class_implements( $definition_class );
+ if ( ! isset( $interfaces[ PaymentMethodDefinitionInterface::class ] ) ) {
+ throw new \InvalidArgumentException(
+ sprintf(
+ 'Payment method definition class "%s" must implement %s.',
+ $definition_class,
+ PaymentMethodDefinitionInterface::class
+ )
+ );
+ }
+
+ /**
+ * Ensure type safety for the payment method definition class.
+ *
+ * @var class-string $definition_class
+ */
+ $this->payment_methods[ $definition_class::get_id() ] = $definition_class;
+ }
+
+ /**
+ * Get all registered payment method definitions.
+ *
+ * @return class-string[] All registered payment method definition classes.
+ */
+ public function get_all_payment_method_definitions(): array {
+ return $this->payment_methods;
+ }
+
+ /**
+ * Get all available payment method definitions for a given account and currency.
+ *
+ * @param string $account_country The account country.
+ * @param string $currency The currency.
+ * @return string[] All available payment method definition classes.
+ */
+ public function get_available_payment_method_definitions( string $account_country, string $currency ): array {
+ return array_filter(
+ $this->payment_methods,
+ function ( $definition_class ) use ( $account_country, $currency ) {
+ return $definition_class::is_available_for( $currency, $account_country );
+ }
+ );
+ }
+}
diff --git a/includes/payment-methods/Configs/Utils/PaymentMethodUtils.php b/includes/payment-methods/Configs/Utils/PaymentMethodUtils.php
new file mode 100644
index 00000000000..4f3faa8ec90
--- /dev/null
+++ b/includes/payment-methods/Configs/Utils/PaymentMethodUtils.php
@@ -0,0 +1,136 @@
+ $supported_currencies The list of supported currencies.
+ * @param array $supported_countries The list of supported countries.
+ * @param string $currency The currency code to check.
+ * @param string $account_country The merchant's account country.
+ * @return bool
+ */
+ public static function is_available_for( array $supported_currencies, array $supported_countries, string $currency, string $account_country ): bool {
+ // Check if currency is supported.
+ if ( ! empty( $supported_currencies ) && ! in_array( $currency, $supported_currencies, true ) ) {
+ return false;
+ }
+
+ // Check if country is supported.
+ if ( ! empty( $supported_countries ) && ! in_array( $account_country, $supported_countries, true ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Is the payment method a BNPL (Buy Now Pay Later) payment method?
+ *
+ * @param array $capabilities The payment method capabilities.
+ * @return boolean
+ */
+ public static function is_bnpl( array $capabilities ): bool {
+ return in_array( PaymentMethodCapability::BUY_NOW_PAY_LATER, $capabilities, true );
+ }
+
+ /**
+ * Is the payment method a reusable payment method?
+ *
+ * @param array $capabilities The payment method capabilities.
+ * @return boolean
+ */
+ public static function is_reusable( array $capabilities ): bool {
+ return in_array( PaymentMethodCapability::TOKENIZATION, $capabilities, true );
+ }
+
+ /**
+ * Does the payment method accept only domestic payments?
+ *
+ * @param array $capabilities The payment method capabilities.
+ * @return boolean
+ */
+ public static function accepts_only_domestic_payments( array $capabilities ): bool {
+ return in_array( PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY, $capabilities, true );
+ }
+
+ /**
+ * Does the payment method allow manual capture?
+ *
+ * @param array $capabilities The payment method capabilities.
+ * @return boolean
+ */
+ public static function allows_manual_capture( array $capabilities ): bool {
+ return in_array( PaymentMethodCapability::CAPTURE_LATER, $capabilities, true );
+ }
+
+ /**
+ * Checks if a currency is domestic for a given country.
+ *
+ * @param string $currency The currency code to check.
+ * @param string $country The country code to check against.
+ * @return bool True if the currency is domestic for the country
+ */
+ public static function is_domestic_currency_for_country( string $currency, string $country ): bool {
+ // Get the locale info which contains country->currency mapping.
+ $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php';
+
+ // If country doesn't exist in our locale info, we can't validate.
+ if ( ! isset( $locale_info[ $country ] ) ) {
+ return false;
+ }
+
+ return $locale_info[ $country ]['currency_code'] === $currency;
+ }
+
+ /**
+ * Get the payment method definitions as a JSON string.
+ *
+ * @return string
+ */
+ public static function get_payment_method_definitions_json() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+ $payment_method_definitions = [];
+
+ foreach ( $registry->get_available_definitions() as $payment_method_definition ) {
+ $payment_method_definitions[ $payment_method_definition::get_id() ] = [
+ 'id' => $payment_method_definition::get_id(),
+ 'stripe_key' => $payment_method_definition::get_stripe_id(),
+ 'title' => $payment_method_definition::get_title(),
+ 'description' => $payment_method_definition::get_description(),
+ 'settings_icon_url' => $payment_method_definition::get_settings_icon_url(),
+ 'currencies' => $payment_method_definition::get_supported_currencies(),
+ 'allows_manual_capture' => $payment_method_definition::allows_manual_capture(),
+ 'allows_pay_later' => $payment_method_definition::is_bnpl(),
+ 'accepts_only_domestic_payment' => $payment_method_definition::accepts_only_domestic_payments(),
+ ];
+ }
+
+ $encoded_response = wp_json_encode( $payment_method_definitions );
+ return false === $encoded_response ? '' : $encoded_response;
+ }
+}
diff --git a/includes/payment-methods/class-alipay-payment-method.php b/includes/payment-methods/class-alipay-payment-method.php
deleted file mode 100644
index 82cb7b926fe..00000000000
--- a/includes/payment-methods/class-alipay-payment-method.php
+++ /dev/null
@@ -1,154 +0,0 @@
-stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->is_reusable = false;
- $this->currencies = [
- Currency_Code::AUSTRALIAN_DOLLAR,
- Currency_Code::CANADIAN_DOLLAR,
- Currency_Code::POUND_STERLING,
- Currency_Code::HONG_KONG_DOLLAR,
- Currency_Code::JAPANESE_YEN,
- Currency_Code::NEW_ZEALAND_DOLLAR,
- Currency_Code::SINGAPORE_DOLLAR,
- Currency_Code::UNITED_STATES_DOLLAR,
- Currency_Code::HUNGARIAN_FORINT,
- Currency_Code::EURO,
- Currency_Code::CHINESE_YUAN,
- ];
- $this->icon_url = plugins_url( 'assets/images/payment-methods/alipay-logo.svg', WCPAY_PLUGIN_FILE );
- $this->countries = [];
- }
-
- /**
- * Returns payment method title.
- *
- * @param string|null $account_country Country of merchants account.
- * @param array|false $payment_details Optional payment details from charge object.
- *
- * @return string
- */
- public function get_title( ?string $account_country = null, $payment_details = false ) {
- return __( 'Alipay', 'woocommerce-payments' );
- }
-
- /**
- * Returns testing credentials to be printed at checkout in test mode.
- *
- * @param string $account_country The country of the account.
- * @return string
- */
- public function get_testing_instructions( string $account_country ) {
- return '';
- }
-
- /**
- * Returns payment method supported countries for the merchant's account
- * (ensuring it's part of the contracted Alipay countries).
- *
- * @return array
- */
- public function get_currencies() {
- $account = \WC_Payments::get_account_service()->get_cached_account_data();
- $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : '';
-
- if ( Country_Code::AUSTRALIA === $account_country ) {
- return [ Currency_Code::AUSTRALIAN_DOLLAR ];
- }
-
- if ( Country_Code::CANADA === $account_country ) {
- return [ Currency_Code::CANADIAN_DOLLAR ];
- }
-
- if ( Country_Code::UNITED_KINGDOM === $account_country ) {
- return [ Currency_Code::POUND_STERLING ];
- }
-
- if ( Country_Code::HONG_KONG === $account_country ) {
- return [ Currency_Code::HONG_KONG_DOLLAR ];
- }
-
- if ( Country_Code::JAPAN === $account_country ) {
- return [ Currency_Code::JAPANESE_YEN ];
- }
-
- if ( Country_Code::NEW_ZEALAND === $account_country ) {
- return [ Currency_Code::NEW_ZEALAND_DOLLAR ];
- }
-
- if ( Country_Code::SINGAPORE === $account_country ) {
- return [ Currency_Code::SINGAPORE_DOLLAR ];
- }
-
- if ( Country_Code::UNITED_STATES === $account_country ) {
- return [ Currency_Code::UNITED_STATES_DOLLAR ];
- }
-
- if ( Country_Code::HUNGARY === $account_country ) {
- return [ Currency_Code::HUNGARIAN_FORINT ];
- }
-
- if ( in_array(
- $account_country,
- [
- Country_Code::AUSTRIA,
- Country_Code::BELGIUM,
- Country_Code::BULGARIA,
- Country_Code::CYPRUS,
- Country_Code::CZECHIA,
- Country_Code::DENMARK,
- Country_Code::ESTONIA,
- Country_Code::FINLAND,
- Country_Code::FRANCE,
- Country_Code::GERMANY,
- Country_Code::GREECE,
- Country_Code::IRELAND,
- Country_Code::ITALY,
- Country_Code::LATVIA,
- Country_Code::LITHUANIA,
- Country_Code::LUXEMBOURG,
- Country_Code::MALTA,
- Country_Code::NETHERLANDS,
- Country_Code::NORWAY,
- Country_Code::PORTUGAL,
- Country_Code::ROMANIA,
- Country_Code::SLOVAKIA,
- Country_Code::SLOVENIA,
- Country_Code::SPAIN,
- Country_Code::SWEDEN,
- Country_Code::SWITZERLAND,
- Country_Code::CROATIA,
- ],
- true
- ) ) {
- return [ Currency_Code::EURO ];
- }
-
- // fallback currency code, just in case.
- return [ Currency_Code::CHINESE_YUAN ];
- }
-}
diff --git a/includes/payment-methods/class-klarna-payment-method.php b/includes/payment-methods/class-klarna-payment-method.php
index 27495db4b02..1f40314d44f 100644
--- a/includes/payment-methods/class-klarna-payment-method.php
+++ b/includes/payment-methods/class-klarna-payment-method.php
@@ -1,6 +1,6 @@
stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
+ $this->title = 'Multibanco';
+ $this->is_reusable = false;
+ $this->icon_url = plugins_url( 'assets/images/payment-methods/multibanco-logo.svg', WCPAY_PLUGIN_FILE );
+ $this->dark_icon_url = plugins_url( 'assets/images/payment-methods/multibanco-logo-dark.svg', WCPAY_PLUGIN_FILE );
+ $this->currencies = [ Currency_Code::EURO ];
+ $this->countries = [ Country_Code::PORTUGAL ];
+ }
+
+ /**
+ * Returns payment method title
+ *
+ * @param string|null $account_country Country of merchants account.
+ * @param array|false $payment_details Optional payment details from charge object.
+ *
+ * @return string
+ */
+ public function get_title( ?string $account_country = null, $payment_details = false ) {
+ return __( 'Multibanco', 'woocommerce-payments' );
+ }
+
+ /**
+ * Returns testing credentials to be printed at checkout in test mode.
+ *
+ * @param string $account_country The country of the account.
+ * @return string
+ */
+ public function get_testing_instructions( string $account_country ) {
+ return '';
+ }
+}
diff --git a/includes/payment-methods/class-upe-payment-method.php b/includes/payment-methods/class-upe-payment-method.php
index 4065c1b27e0..005f8701d6e 100644
--- a/includes/payment-methods/class-upe-payment-method.php
+++ b/includes/payment-methods/class-upe-payment-method.php
@@ -1,6 +1,6 @@
|null
+ */
+ protected $definition;
+
/**
* Stripe key name
*
@@ -109,9 +116,22 @@ abstract class UPE_Payment_Method {
* Create instance of payment method
*
* @param WC_Payments_Token_Service $token_service Instance of WC_Payments_Token_Service.
+ * @param class-string|null $definition Optional payment method definition class name.
*/
- public function __construct( $token_service ) {
+ public function __construct( $token_service, ?string $definition = null ) {
$this->token_service = $token_service;
+ $this->definition = $definition;
+
+ if ( null !== $this->definition ) {
+ // Cache values that don't require context.
+ $this->stripe_id = $this->definition::get_stripe_id();
+ $this->is_reusable = $this->definition::is_reusable();
+ $this->currencies = $this->definition::get_supported_currencies();
+ $this->accept_only_domestic_payment = $this->definition::accepts_only_domestic_payments();
+ $this->limits_per_currency = $this->definition::get_limits_per_currency();
+ $this->is_bnpl = $this->definition::is_bnpl();
+ $this->countries = $this->definition::get_supported_countries();
+ }
}
/**
@@ -120,6 +140,9 @@ public function __construct( $token_service ) {
* @return string
*/
public function get_id() {
+ if ( null !== $this->definition ) {
+ return $this->definition::get_id();
+ }
return $this->stripe_id;
}
@@ -134,6 +157,9 @@ public function get_id() {
* @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_title( ?string $account_country = null, $payment_details = false ) {
+ if ( null !== $this->definition ) {
+ return $this->definition::get_title( $account_country );
+ }
return $this->title;
}
@@ -161,16 +187,17 @@ public function has_domestic_transactions_restrictions() {
* can be used at checkout
*
* @param string $account_country Country of merchants account.
+ * @param bool $skip_limits_per_currency_check Whether to skip limits per currency check.
*
* @return bool
*/
- public function is_enabled_at_checkout( string $account_country ) {
+ public function is_enabled_at_checkout( string $account_country, bool $skip_limits_per_currency_check = false ) {
if ( $this->is_subscription_item_in_cart() || $this->is_changing_payment_method_for_subscription() ) {
return $this->is_reusable();
}
// This part ensures that when payment limits for the currency declared, those will be respected (e.g. BNPLs).
- if ( [] !== $this->limits_per_currency ) {
+ if ( [] !== $this->limits_per_currency && ! $skip_limits_per_currency_check ) {
$order = null;
if ( is_wc_endpoint_url( 'order-pay' ) ) {
$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
@@ -275,7 +302,12 @@ public function get_payment_token_for_user( $user, $payment_method_id ) {
* @param string $account_country The country of the account.
* @return string
*/
- abstract public function get_testing_instructions( string $account_country );
+ public function get_testing_instructions( string $account_country ) {
+ if ( null !== $this->definition ) {
+ return $this->definition::get_testing_instructions( $account_country );
+ }
+ return '';
+ }
/**
* Returns the payment method icon URL or an empty string.
@@ -286,6 +318,9 @@ abstract public function get_testing_instructions( string $account_country );
* @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_icon( ?string $account_country = null ) {
+ if ( null !== $this->definition ) {
+ return $this->definition::get_icon_url( $account_country );
+ }
return isset( $this->icon_url ) ? $this->icon_url : '';
}
@@ -296,6 +331,9 @@ public function get_icon( ?string $account_country = null ) {
* @return string
*/
public function get_dark_icon( ?string $account_country = null ) {
+ if ( null !== $this->definition ) {
+ return $this->definition::get_dark_icon_url( $account_country );
+ }
return isset( $this->dark_icon_url ) ? $this->dark_icon_url : $this->get_icon( $account_country );
}
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index f56443e682f..0dc37ef7482 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -2404,7 +2404,7 @@ protected function check_response_for_errors( $response ) {
$response_code
);
} elseif ( isset( $response_body['error'] ) ) {
- $response_body_error_code = $response_body['error']['code'] ?? null;
+ $response_body_error_code = $response_body['error']['code'] ?? $response_body['error']['message_code'] ?? null;
$payment_intent_status = $response_body['error']['payment_intent']['status'] ?? null;
// We redact the API error message to prevent prompting the merchant to contact Stripe support
@@ -2559,35 +2559,35 @@ private function get_order_info_from_intention_object( $intention_id ) {
* Adds order information to the charge object.
*
* @param string $charge_id Charge ID.
- * @param array $object Object to add order information.
+ * @param array $entity Object to add order information.
*
* @return array
*/
- private function add_order_info_to_charge_object( $charge_id, $object ) {
+ private function add_order_info_to_charge_object( $charge_id, $entity ) {
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
- $object = $this->add_order_info_to_object( $order, $object );
+ $entity = $this->add_order_info_to_object( $order, $entity );
- return $object;
+ return $entity;
}
/**
* Returns a transaction with order information when it exists.
*
* @param bool|\WC_Order|\WC_Order_Refund $order Order object.
- * @param array $object Object to add order information.
+ * @param array $entity Object to add order information.
*
* @return array new object with order information.
*/
- private function add_order_info_to_object( $order, $object ) {
+ private function add_order_info_to_object( $order, $entity ) {
// Add order information to the `$transaction`.
// If the order couldn't be retrieved, return an empty order.
- $object['order'] = [];
+ $entity['order'] = [];
if ( $order ) {
- $object['order'] = $this->build_order_info( $order );
+ $entity['order'] = $this->build_order_info( $order );
}
- return $object;
+ return $entity;
}
/**
@@ -2598,6 +2598,7 @@ private function add_order_info_to_object( $order, $object ) {
*/
public function build_order_info( WC_Order $order ): array {
$order_info = [
+ 'id' => $order->get_id(),
'number' => $order->get_order_number(),
'url' => $order->get_edit_order_url(),
'customer_url' => $this->get_customer_url( $order ),
@@ -2823,12 +2824,17 @@ private function uuid() {
/**
* Fetch readers charge summary.
*
- * @param string $charge_date Charge date for readers.
+ * @param string $charge_date Charge date for readers.
+ * @param string|null $transaction_id Optional transaction ID to filter results.
*
* @return array reader objects.
*/
- public function get_readers_charge_summary( string $charge_date ): array {
- return $this->request( [ 'charge_date' => $charge_date ], self::READERS_CHARGE_SUMMARY, self::GET );
+ public function get_readers_charge_summary( string $charge_date, ?string $transaction_id = null ): array {
+ $params = [ 'charge_date' => $charge_date ];
+ if ( $transaction_id ) {
+ $params['transaction_id'] = $transaction_id;
+ }
+ return $this->request( $params, self::READERS_CHARGE_SUMMARY, self::GET );
}
/**
diff --git a/package-lock.json b/package-lock.json
index 2733be620df..ee2f86d529e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "woocommerce-payments",
- "version": "9.0.0",
+ "version": "9.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "woocommerce-payments",
- "version": "9.0.0",
+ "version": "9.1.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@automattic/interpolate-components": "1.2.1",
"@fingerprintjs/fingerprintjs": "3.4.1",
"@stripe/connect-js": "3.3.16",
- "@stripe/react-connect-js": "3.3.17",
+ "@stripe/react-connect-js": "3.3.21",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/stripe-js": "1.15.1",
"@woocommerce/explat": "2.3.0",
@@ -73,7 +73,7 @@
"@wordpress/hooks": "3.6.1",
"@wordpress/html-entities": "3.6.1",
"@wordpress/i18n": "4.6.1",
- "@wordpress/icons": "8.2.3",
+ "@wordpress/icons": "10.19.0",
"@wordpress/jest-preset-default": "8.1.2",
"@wordpress/plugins": "4.4.3",
"@wordpress/primitives": "3.30.0",
@@ -3424,9 +3424,9 @@
"dev": true
},
"node_modules/@babel/runtime": {
- "version": "7.24.8",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
- "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -5539,11 +5539,11 @@
"integrity": "sha512-lMUKJJaDl6qzjp+czNn+N6wMwFXwLawmB2jNNgds8SeR+bXCVCXevzJ8dfF92KfmexKg++hBYagF9e99sEMBJQ=="
},
"node_modules/@stripe/react-connect-js": {
- "version": "3.3.17",
- "resolved": "https://registry.npmjs.org/@stripe/react-connect-js/-/react-connect-js-3.3.17.tgz",
- "integrity": "sha512-uxCh6ACcSWS/t0kBeqvvRieBI9pRxh2rPxt6NpjrTg3Ft1ZDleUfg9OAjkfoOT3Ta+FTomouA19l2ju7If2h5A==",
+ "version": "3.3.21",
+ "resolved": "https://registry.npmjs.org/@stripe/react-connect-js/-/react-connect-js-3.3.21.tgz",
+ "integrity": "sha512-b2M9S9QhdIR7suF0VYWwG0+H7SV+X7Y4y/mfJcsVUU/oyrUPZ1V6HIP9z07C33JDcR5I5ULIiwIBRytgPhr09g==",
"peerDependencies": {
- "@stripe/connect-js": ">=3.3.16",
+ "@stripe/connect-js": ">=3.3.20",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
@@ -9348,6 +9348,16 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.122.tgz",
"integrity": "sha512-rF6rUBS80n4oK16EW8nE75U+9fw0SSUgoPtWSvHhPXdT7itbvmS7UjB/jyM8i3AkvI6yeSM5qCwo+xN0npGDHg=="
},
+ "node_modules/@woocommerce/onboarding/node_modules/@types/react": {
+ "version": "17.0.83",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.83.tgz",
+ "integrity": "sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "^0.16",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/@woocommerce/onboarding/node_modules/@woocommerce/csv-export": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.9.0.tgz",
@@ -9439,6 +9449,37 @@
"react-dom": "^17.0.0"
}
},
+ "node_modules/@woocommerce/onboarding/node_modules/@wordpress/element": {
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.20.0.tgz",
+ "integrity": "sha512-Ou7EoGtGe4FUL6fKALINXJLKoSfyWTBJzkJfN2HzSgM1wira9EuWahl8MQN0HAUaWeOoDqMKPvnglfS+kC8JLA==",
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@types/react": "^17.0.37",
+ "@types/react-dom": "^17.0.11",
+ "@wordpress/escape-html": "^2.22.0",
+ "change-case": "^4.1.2",
+ "is-plain-object": "^5.0.0",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@woocommerce/onboarding/node_modules/@wordpress/icons": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-8.4.0.tgz",
+ "integrity": "sha512-N/bzt5z534JyAWdTyDdsu9G+6NQ5FvykmNbKZrZuUHTuEI8KbxYaN/5lT6W6Lkwq32D/B6ibpt1LpSQJ37IZWw==",
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/element": "^4.6.0",
+ "@wordpress/primitives": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@woocommerce/onboarding/node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -9781,6 +9822,20 @@
"integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==",
"dev": true
},
+ "node_modules/@wordpress/block-editor/node_modules/@wordpress/icons": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-8.4.0.tgz",
+ "integrity": "sha512-N/bzt5z534JyAWdTyDdsu9G+6NQ5FvykmNbKZrZuUHTuEI8KbxYaN/5lT6W6Lkwq32D/B6ibpt1LpSQJ37IZWw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/element": "^4.6.0",
+ "@wordpress/primitives": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@wordpress/block-editor/node_modules/@wordpress/notices": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-3.31.0.tgz",
@@ -11169,20 +11224,6 @@
"npm": ">=8.19.2"
}
},
- "node_modules/@wordpress/commands/node_modules/@wordpress/icons": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.3.0.tgz",
- "integrity": "sha512-d2jOFfLIAxPr+/Bzg7wnRDEDPogMpVefe4cHgIx9kXp/+7DG4KWNJm842qVithjs1wVgSzavzWbFrTtPEZ+Uhw==",
- "dependencies": {
- "@babel/runtime": "^7.16.0",
- "@wordpress/element": "^6.3.0",
- "@wordpress/primitives": "^4.3.0"
- },
- "engines": {
- "node": ">=18.12.0",
- "npm": ">=8.19.2"
- }
- },
"node_modules/@wordpress/commands/node_modules/@wordpress/is-shallow-equal": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.3.0.tgz",
@@ -11498,6 +11539,19 @@
"resolved": "https://registry.npmjs.org/memize/-/memize-2.1.0.tgz",
"integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg=="
},
+ "node_modules/@wordpress/components/node_modules/@wordpress/icons": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-8.4.0.tgz",
+ "integrity": "sha512-N/bzt5z534JyAWdTyDdsu9G+6NQ5FvykmNbKZrZuUHTuEI8KbxYaN/5lT6W6Lkwq32D/B6ibpt1LpSQJ37IZWw==",
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/element": "^4.6.0",
+ "@wordpress/primitives": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@wordpress/components/node_modules/@wordpress/rich-text": {
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-5.20.0.tgz",
@@ -12107,20 +12161,6 @@
"npm": ">=8.19.2"
}
},
- "node_modules/@wordpress/core-data/node_modules/@wordpress/icons": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.3.0.tgz",
- "integrity": "sha512-d2jOFfLIAxPr+/Bzg7wnRDEDPogMpVefe4cHgIx9kXp/+7DG4KWNJm842qVithjs1wVgSzavzWbFrTtPEZ+Uhw==",
- "dependencies": {
- "@babel/runtime": "^7.16.0",
- "@wordpress/element": "^6.3.0",
- "@wordpress/primitives": "^4.3.0"
- },
- "engines": {
- "node": ">=18.12.0",
- "npm": ">=8.19.2"
- }
- },
"node_modules/@wordpress/core-data/node_modules/@wordpress/is-shallow-equal": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.3.0.tgz",
@@ -12708,16 +12748,113 @@
}
},
"node_modules/@wordpress/icons": {
- "version": "8.2.3",
- "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-8.2.3.tgz",
- "integrity": "sha512-e73iDdlo+c6h8Rhm7mKg+CX7s8cSlGVqtKQooeM3RRo54UaN2hh4Va/zjXZj45+AYG3gx75PPSKFElhHt3LW4Q==",
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.19.0.tgz",
+ "integrity": "sha512-bYIzWgK3pLI/ShAzkhtzesy/f77WdC7CUdY6kbyic6Q706E3NOqHPeEyvecyOXJn9LKjwtes9jcnjOehNIyuxw==",
"dependencies": {
- "@babel/runtime": "^7.16.0",
- "@wordpress/element": "^4.4.1",
- "@wordpress/primitives": "^3.4.1"
+ "@babel/runtime": "7.25.7",
+ "@wordpress/element": "^6.19.0",
+ "@wordpress/primitives": "^4.19.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/@types/react": {
+ "version": "18.3.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
+ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/@types/react-dom": {
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
+ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/@wordpress/element": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.19.0.tgz",
+ "integrity": "sha512-11kWRNiHbDkm5uXxEQiVVcEmdUHzBUjzsgp7Ui1iT8yDp0Taf8F30GzqGlWiu0B1K9VxUYLgVCqXamNqo64Ahg==",
+ "dependencies": {
+ "@babel/runtime": "7.25.7",
+ "@types/react": "^18.2.79",
+ "@types/react-dom": "^18.2.25",
+ "@wordpress/escape-html": "^3.19.0",
+ "change-case": "^4.1.2",
+ "is-plain-object": "^5.0.0",
+ "react": "^18.3.0",
+ "react-dom": "^18.3.0"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/@wordpress/escape-html": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.19.0.tgz",
+ "integrity": "sha512-vG2h1e/+MmLupGzseeoveB+48wz+ZhB9FhJ+yl0B19H/n4PfcSBl3XD0EPw9iAM6y6KMST/2qqkdFGNwohdnmA==",
+ "dependencies": {
+ "@babel/runtime": "7.25.7"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/@wordpress/primitives": {
+ "version": "4.19.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.19.0.tgz",
+ "integrity": "sha512-HX7lvE6R/u3iJI8sbf85/7k3Vasdco4EWmwT1JTWpRVMl1KcphfmaYs7/nTDqrkbOo19VHOZQhnJCjUPd/O5QA==",
+ "dependencies": {
+ "@babel/runtime": "7.25.7",
+ "@wordpress/element": "^6.19.0",
+ "clsx": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
}
},
"node_modules/@wordpress/is-shallow-equal": {
@@ -13236,6 +13373,50 @@
"react": "^17.0.0"
}
},
+ "node_modules/@wordpress/plugins/node_modules/@types/react": {
+ "version": "17.0.83",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.83.tgz",
+ "integrity": "sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw==",
+ "dev": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "^0.16",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@wordpress/plugins/node_modules/@wordpress/element": {
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.20.0.tgz",
+ "integrity": "sha512-Ou7EoGtGe4FUL6fKALINXJLKoSfyWTBJzkJfN2HzSgM1wira9EuWahl8MQN0HAUaWeOoDqMKPvnglfS+kC8JLA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@types/react": "^17.0.37",
+ "@types/react-dom": "^17.0.11",
+ "@wordpress/escape-html": "^2.22.0",
+ "change-case": "^4.1.2",
+ "is-plain-object": "^5.0.0",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@wordpress/plugins/node_modules/@wordpress/icons": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-8.4.0.tgz",
+ "integrity": "sha512-N/bzt5z534JyAWdTyDdsu9G+6NQ5FvykmNbKZrZuUHTuEI8KbxYaN/5lT6W6Lkwq32D/B6ibpt1LpSQJ37IZWw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/element": "^4.6.0",
+ "@wordpress/primitives": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@wordpress/postcss-plugins-preset": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.3.0.tgz",
@@ -13566,20 +13747,6 @@
"npm": ">=8.19.2"
}
},
- "node_modules/@wordpress/preferences/node_modules/@wordpress/icons": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.3.0.tgz",
- "integrity": "sha512-d2jOFfLIAxPr+/Bzg7wnRDEDPogMpVefe4cHgIx9kXp/+7DG4KWNJm842qVithjs1wVgSzavzWbFrTtPEZ+Uhw==",
- "dependencies": {
- "@babel/runtime": "^7.16.0",
- "@wordpress/element": "^6.3.0",
- "@wordpress/primitives": "^4.3.0"
- },
- "engines": {
- "node": ">=18.12.0",
- "npm": ">=8.19.2"
- }
- },
"node_modules/@wordpress/preferences/node_modules/@wordpress/is-shallow-equal": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.3.0.tgz",
diff --git a/package.json b/package.json
index 2566e769c7f..dc97829c118 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-payments",
- "version": "9.0.0",
+ "version": "9.1.0",
"main": "webpack.config.js",
"author": "Automattic",
"license": "GPL-3.0-or-later",
@@ -78,7 +78,7 @@
"@automattic/interpolate-components": "1.2.1",
"@fingerprintjs/fingerprintjs": "3.4.1",
"@stripe/connect-js": "3.3.16",
- "@stripe/react-connect-js": "3.3.17",
+ "@stripe/react-connect-js": "3.3.21",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/stripe-js": "1.15.1",
"@woocommerce/explat": "2.3.0",
@@ -138,7 +138,7 @@
"@wordpress/hooks": "3.6.1",
"@wordpress/html-entities": "3.6.1",
"@wordpress/i18n": "4.6.1",
- "@wordpress/icons": "8.2.3",
+ "@wordpress/icons": "10.19.0",
"@wordpress/jest-preset-default": "8.1.2",
"@wordpress/plugins": "4.4.3",
"@wordpress/primitives": "3.30.0",
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index c3547cfc17b..46c8c4d4bf5 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -81,6 +81,16 @@
+
+
+ includes/payment-methods/Configs/
+
+
+
+ includes/payment-methods/Configs/
+
+
+
./tests/*
diff --git a/readme.txt b/readme.txt
index fd7dbd443ce..dd56c5b82bf 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment
Requires at least: 6.0
Tested up to: 6.7
Requires PHP: 7.3
-Stable tag: 9.0.0
+Stable tag: 9.1.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -87,6 +87,49 @@ You can read our Terms of Service and other policies [here](https://woocommerce.
== Changelog ==
+= 9.1.0 - 2025-03-19 =
+* Add - Add a prompt to encourage merchants to provide feedback or leave a WordPress.org review for WooPayments
+* Add - Add order failure message on Order Received page.
+* Add - Add support to change payment method to 3DS card for subscriptions.
+* Add - Add WeChat Pay response handling.
+* Add - Implement Multibanco payment method
+* Add - Implement specific handling for insufficient_balance_for_refund when refunding the admin order management
+* Add - Inform Stripe when a store switch from coming soon to live mode
+* Add - Show failure reason in details page for failed payout. This will help merchants get better support, or understand the next steps needed to fix the failing payouts.
+* Fix - Add an order note when a recurring payment fails or when updating the payment method fails due to a missing or invalid payment token.
+* Fix - Bypass enabled at checkout checks for rendering payment methods configs.
+* Fix - Correct the dispute notice for Klarna Inquiries
+* Fix - fix: GooglePay/ApplePay fail when there are more than 9 shipping options.
+* Fix - fix: GooglePay/ApplePay script dependencies with WooCommerce 9.7
+* Fix - Fix issue where survey modal is not scrollable on smaller screen sizes.
+* Fix - Fix validation for support phone numbers for Singapore
+* Fix - Improve payout failure messages for better clarity and accuracy
+* Fix - Inconsitent Safe Mode notice with the latest Jetpack version
+* Fix - Init PMME container in cart block so that it can be dynamically rendered once the requirements are met.
+* Fix - Make sure that WooPayments gateways follow the main WooPayments card gateway in gateway ordering on the page.
+* Fix - Manual capture fails in the transaction detail screen with a customized order number
+* Fix - Properly extract styles when using the site editor.
+* Fix - Renamed function parameters to avoid reserved keyword conflicts
+* Fix - Resolved an issue on stores that had the Stripe Billing feature enabled (US-only) and then changed their store location to an ineligible country.
+* Fix - Scoped CSS selectors for WP components to prevent unintended styling on other pages
+* Fix - Show Express Checkout button previews in template editor
+* Fix - Skip email input search in pay for order flow and use email provided in order data for WooPay iframe.
+* Fix - Skip limits per currency check on admin pages
+* Fix - Tests: Suppressed unexpected JSON output in maybe_handle_onboarding test by wrapping execution with ob_start() and ob_end_clean()
+* Update - Better handling of HTTPs errors in embedded components.
+* Update - Change wording for Sales Channel, Online Store, In-Person, and In-Person (POS)
+* Update - Enhancements to country select field in onboarding.
+* Update - feat: add compatibility notice for Google Pay with live mode accounts.
+* Update - Jetpack packages in composer
+* Update - Remove the 60 day survey admin note, since it will be redundant after we add the reviews prompt.
+* Update - Track action complete event in Stripe Notification embedded component.
+* Update - update: tokenize ECE initialization and update flow on pricing change.
+* Dev - Add centralized payment method definitions to streamline implementation and maintenance of payment methods.
+* Dev - Exclude playwright-report from eslint.
+* Dev - Include transaction ID when requesting card reader fee charges summary.
+* Dev - Refactors to the embedded compoennts logic.
+* Dev - Update @wordpress/icons version to latest version 10.19.0
+
= 9.0.0 - 2025-02-26 =
* Add - Add E2E tests for currency switching at checkout.
* Add - Add GrabPay payment method details to the View Transaction page.
diff --git a/src/Internal/Service/SessionService.php b/src/Internal/Service/SessionService.php
index 51904c3b148..b233f4c2050 100644
--- a/src/Internal/Service/SessionService.php
+++ b/src/Internal/Service/SessionService.php
@@ -34,15 +34,15 @@ public function __construct( LegacyProxy $legacy_proxy ) {
* Getter.
*
* @param string $key Session key.
- * @param mixed|null $default Default value to return if key is not set.
+ * @param mixed|null $initial Default value to return if key is not set.
*
* @return mixed
*/
- public function get( string $key, $default = null ) {
+ public function get( string $key, $initial = null ) {
if ( $this->has_wc_session() ) {
- return $this->get_wc_session()->get( $key, $default );
+ return $this->get_wc_session()->get( $key, $initial );
} else {
- return $default;
+ return $initial;
}
}
diff --git a/tests/js/jest.config.js b/tests/js/jest.config.js
index 7665524e08d..f6afe00fa8d 100644
--- a/tests/js/jest.config.js
+++ b/tests/js/jest.config.js
@@ -45,6 +45,14 @@ module.exports = {
'/docker/',
'/tests/e2e',
],
+ watchPathIgnorePatterns: [
+ '/node_modules/',
+ '/vendor/',
+ '/.*/build/',
+ '/.*/build-module/',
+ '/docker/',
+ '/tests/e2e',
+ ],
transform: {
...tsjPreset.transform,
'^.+\\.(jpg|svg|png|gif)(\\?.*)?$': '/tests/js/fileMock.js',
diff --git a/tests/unit/admin/test-class-wc-rest-payments-readers-controller.php b/tests/unit/admin/test-class-wc-rest-payments-readers-controller.php
index a168d5a1279..a6f8138bf0e 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-readers-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-readers-controller.php
@@ -136,19 +136,29 @@ public function test_get_summary() {
],
];
+ $transaction_id = uniqid( 'trx_' );
+
$this->mock_api_client
->expects( $this->once() )
->method( 'get_transaction' )
- ->willReturn( [ 'created' => 1634291278 ] );
+ ->with( $transaction_id )
+ ->willReturn(
+ [
+ 'created' => 1634291278,
+ 'id' => $transaction_id,
+ ]
+ );
$this->mock_api_client
->expects( $this->once() )
->method( 'get_readers_charge_summary' )
- ->with( gmdate( 'Y-m-d', 1634291278 ) )
+ ->with( gmdate( 'Y-m-d', 1634291278 ), $transaction_id )
->willReturn( $readers );
$request = new WP_REST_Request( 'GET' );
- $request->set_param( 'transaction_id', 1 );
+ $request->set_param( 'transaction_id', $transaction_id );
+ $request->set_param( 'charge_date', gmdate( 'Y-m-d', 1634291278 ) );
+
$response = $this->controller->get_summary( $request );
$this->assertSame( $readers, $response->get_data() );
}
diff --git a/tests/unit/helpers/class-wc-mock-wc-data-store.php b/tests/unit/helpers/class-wc-mock-wc-data-store.php
index c305ac04485..61649bd8ab5 100644
--- a/tests/unit/helpers/class-wc-mock-wc-data-store.php
+++ b/tests/unit/helpers/class-wc-mock-wc-data-store.php
@@ -57,25 +57,25 @@ public function set_object_id_field( $object_id_field ) {
$this->object_id_field_for_meta = $object_id_field;
}
- public function create( &$object ) {
+ public function create( &$entity ) {
if ( 'user' === $this->meta_type ) {
- $content_id = wc_create_new_customer( $object->get_content(), 'username-' . time(), 'hunter2' );
+ $content_id = wc_create_new_customer( $entity->get_content(), 'username-' . time(), 'hunter2' );
} else {
- $content_id = wp_insert_post( [ 'post_title' => $object->get_content() ] );
+ $content_id = wp_insert_post( [ 'post_title' => $entity->get_content() ] );
}
if ( $content_id ) {
- $object->set_id( $content_id );
+ $entity->set_id( $content_id );
}
- $object->apply_changes();
+ $entity->apply_changes();
}
/**
* Simple read.
*/
- public function read( &$object ) {
- $object->set_defaults();
- $id = $object->get_id();
+ public function read( &$entity ) {
+ $entity->set_defaults();
+ $id = $entity->get_id();
if ( empty( $id ) ) {
return;
@@ -86,38 +86,38 @@ public function read( &$object ) {
if ( ! $user_object ) {
return;
}
- $object->set_content( $user_object->user_email );
+ $entity->set_content( $user_object->user_email );
} else {
$post_object = get_post( $id );
if ( ! $post_object ) {
return;
}
- $object->set_content( $post_object->post_title );
+ $entity->set_content( $post_object->post_title );
}
- $object->read_meta_data();
- $object->set_object_read( true );
+ $entity->read_meta_data();
+ $entity->set_object_read( true );
}
/**
* Simple update.
*/
- public function update( &$object ) {
+ public function update( &$entity ) {
global $wpdb;
- $content_id = $object->get_id();
+ $content_id = $entity->get_id();
if ( 'user' === $this->meta_type ) {
wp_update_user(
[
'ID' => $content_id,
- 'user_email' => $object->get_content(),
+ 'user_email' => $entity->get_content(),
]
);
} else {
wp_update_post(
[
'ID' => $content_id,
- 'post_title' => $object->get_content(),
+ 'post_title' => $entity->get_content(),
]
);
}
@@ -126,13 +126,13 @@ public function update( &$object ) {
/**
* Simple delete.
*/
- public function delete( &$object, $args = [] ) {
+ public function delete( &$entity, $args = [] ) {
if ( 'user' === $this->meta_type ) {
- wp_delete_user( $object->get_id() );
+ wp_delete_user( $entity->get_id() );
} else {
- wp_delete_post( $object->get_id() );
+ wp_delete_post( $entity->get_id() );
}
- $object->set_id( 0 );
+ $entity->set_id( 0 );
}
}
diff --git a/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-invalid.php b/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-invalid.php
new file mode 100644
index 00000000000..79485b2729e
--- /dev/null
+++ b/tests/unit/payment-methods/Configs/mocks/class-mock-payment-method-definition-invalid.php
@@ -0,0 +1,17 @@
+getProperty( 'instance' );
+ $instance_property->setAccessible( true );
+ $instance_property->setValue( null, null );
+ $instance_property->setAccessible( false );
+
+ // Create a new instance.
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Reset available definitions.
+ $available_definitions = $reflection->getProperty( 'available_definitions' );
+ $available_definitions->setAccessible( true );
+ $available_definitions->setValue( $registry, [] );
+ $available_definitions->setAccessible( false );
+
+ // Reset payment methods.
+ $payment_methods = $reflection->getProperty( 'payment_methods' );
+ $payment_methods->setAccessible( true );
+ $payment_methods->setValue( $registry, [] );
+ $payment_methods->setAccessible( false );
+ }
+
+ /**
+ * Test that the singleton pattern works correctly.
+ */
+ public function test_singleton_pattern() {
+ // Get two instances of the registry.
+ $instance1 = PaymentMethodDefinitionRegistry::instance();
+ $instance2 = PaymentMethodDefinitionRegistry::instance();
+
+ // Test that both instances are the same object.
+ $this->assertSame( $instance1, $instance2 );
+
+ // Test that we cannot instantiate the class directly.
+ $reflection = new ReflectionClass( PaymentMethodDefinitionRegistry::class );
+ $constructor = $reflection->getConstructor();
+ $this->assertTrue( $constructor->isPrivate(), 'Constructor should be private' );
+ }
+
+ /**
+ * Test that valid payment method definitions can be registered.
+ */
+ public function test_valid_payment_method_registration() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+
+ $methods = $registry->get_all_payment_method_definitions();
+ $this->assertArrayHasKey( 'mock_method', $methods );
+ $this->assertEquals( MockPaymentMethodDefinition::class, $methods['mock_method'] );
+ }
+
+ /**
+ * Test that invalid payment method classes throw appropriate exceptions.
+ */
+ public function test_invalid_payment_method_registration() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Test non-existent class.
+ $this->expectException( \InvalidArgumentException::class );
+ $this->expectExceptionMessage( 'Payment method definition class "NonExistentClass" does not exist.' );
+ $registry->register_payment_method( 'NonExistentClass' );
+
+ // Test class that doesn't implement interface.
+ $this->expectException( \InvalidArgumentException::class );
+ $this->expectExceptionMessage( 'Payment method definition class "' . InvalidMockPaymentMethod::class . '" must implement ' . PaymentMethodDefinitionInterface::class );
+ $registry->register_payment_method( InvalidMockPaymentMethod::class );
+ }
+
+ /**
+ * Test that the same payment method cannot be registered twice.
+ */
+ public function test_duplicate_payment_method_registration() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Register the payment method first time.
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+
+ // Attempt to register the same payment method again.
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+
+ // Verify that the payment method is only registered once.
+ $methods = $registry->get_all_payment_method_definitions();
+ $count = array_filter(
+ $methods,
+ function ( $method ) {
+ return MockPaymentMethodDefinition::class === $method;
+ }
+ );
+ $this->assertCount( 1, $count );
+ }
+
+ /**
+ * Test that get_all_payment_method_definitions() returns the correct list.
+ */
+ public function test_get_all_payment_method_definitions() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Register multiple payment methods.
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+ $registry->register_payment_method( SecondMockPaymentMethodDefinition::class );
+
+ // Get all registered methods.
+ $methods = $registry->get_all_payment_method_definitions();
+
+ // Verify that both methods are in the list.
+ $this->assertCount( 2, $methods );
+ $this->assertArrayHasKey( 'mock_method', $methods );
+ $this->assertArrayHasKey( 'second_mock_method', $methods );
+ $this->assertEquals( MockPaymentMethodDefinition::class, $methods['mock_method'] );
+ $this->assertEquals( SecondMockPaymentMethodDefinition::class, $methods['second_mock_method'] );
+ }
+
+ /**
+ * Test that an empty registry returns an empty array.
+ */
+ public function test_empty_registry_returns_empty_array() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+ $methods = $registry->get_all_payment_method_definitions();
+
+ $this->assertIsArray( $methods );
+ $this->assertEmpty( $methods );
+ }
+
+ /**
+ * Test that init() registers all available definitions.
+ */
+ public function test_init_registers_all_available_definitions() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Register multiple payment methods.
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+ $registry->register_payment_method( SecondMockPaymentMethodDefinition::class );
+
+ // Initialize the registry.
+ $registry->init();
+
+ // Verify that all registered methods are available.
+ $methods = $registry->get_all_payment_method_definitions();
+ $this->assertCount( 2, $methods );
+ $this->assertArrayHasKey( 'mock_method', $methods );
+ $this->assertArrayHasKey( 'second_mock_method', $methods );
+ $this->assertEquals( MockPaymentMethodDefinition::class, $methods['mock_method'] );
+ $this->assertEquals( SecondMockPaymentMethodDefinition::class, $methods['second_mock_method'] );
+ }
+
+ /**
+ * Test that get_available_definitions() returns the expected list of payment method definitions.
+ */
+ public function test_available_definitions_match_expected_list() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Set up some available definitions.
+ $reflection = new ReflectionClass( PaymentMethodDefinitionRegistry::class );
+ $available_definitions_prop = $reflection->getProperty( 'available_definitions' );
+ $available_definitions_prop->setAccessible( true );
+ $available_definitions_prop->setValue(
+ $registry,
+ [
+ MockPaymentMethodDefinition::class,
+ SecondMockPaymentMethodDefinition::class,
+ ]
+ );
+ $available_definitions_prop->setAccessible( false );
+
+ // Get the available definitions.
+ $available_definitions = $registry->get_available_definitions();
+
+ // Verify we get back exactly what we set up.
+ $this->assertCount( 2, $available_definitions );
+ $this->assertContains( MockPaymentMethodDefinition::class, $available_definitions );
+ $this->assertContains( SecondMockPaymentMethodDefinition::class, $available_definitions );
+
+ // Verify each definition is a valid payment method class.
+ foreach ( $available_definitions as $definition ) {
+ $this->assertTrue( class_exists( $definition ), "Definition class $definition should exist." );
+ $this->assertTrue(
+ is_subclass_of( $definition, PaymentMethodDefinitionInterface::class ),
+ "Definition class $definition should implement PaymentMethodDefinitionInterface."
+ );
+ }
+ }
+
+ /**
+ * Test that get_available_payment_method_definitions() filters by currency and country.
+ */
+ public function test_get_available_payment_method_definitions_filters_correctly() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Register our test payment methods.
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+ $registry->register_payment_method( SecondMockPaymentMethodDefinition::class );
+
+ // Test filtering by currency.
+ $methods = $registry->get_available_payment_method_definitions( 'US', 'USD' );
+ $this->assertCount( 2, $methods, 'Both methods should be available for USD in US.' );
+ $this->assertArrayHasKey( 'mock_method', $methods );
+ $this->assertArrayHasKey( 'second_mock_method', $methods );
+
+ // Test filtering by unsupported currency.
+ $methods = $registry->get_available_payment_method_definitions( 'US', 'EUR' );
+ $this->assertCount( 1, $methods, 'Only one method should support EUR.' );
+ $this->assertArrayHasKey( 'second_mock_method', $methods );
+ $this->assertArrayNotHasKey( 'mock_method', $methods );
+
+ // Test filtering by country.
+ $methods = $registry->get_available_payment_method_definitions( 'CA', 'USD' );
+ $this->assertCount( 1, $methods, 'Only one method should be available in Canada.' );
+ $this->assertArrayHasKey( 'mock_method', $methods );
+ $this->assertArrayNotHasKey( 'second_mock_method', $methods );
+ }
+
+ /**
+ * Test that get_available_payment_method_definitions() returns empty array when no methods match.
+ */
+ public function test_get_available_payment_method_definitions_empty_when_no_matches() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+
+ // Register our test payment methods.
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+ $registry->register_payment_method( SecondMockPaymentMethodDefinition::class );
+
+ // Test with unsupported currency and country combination.
+ $methods = $registry->get_available_payment_method_definitions( 'GB', 'EUR' );
+ $this->assertIsArray( $methods );
+ $this->assertEmpty( $methods );
+ }
+}
diff --git a/tests/unit/payment-methods/Configs/test-class-payment-method-utils.php b/tests/unit/payment-methods/Configs/test-class-payment-method-utils.php
new file mode 100644
index 00000000000..ce0e5d57381
--- /dev/null
+++ b/tests/unit/payment-methods/Configs/test-class-payment-method-utils.php
@@ -0,0 +1,264 @@
+assertEquals( 'test_payments', PaymentMethodUtils::get_stripe_id( 'test' ) );
+ }
+
+ /**
+ * Test that is_available_for() works correctly with supported currency and country.
+ */
+ public function test_is_available_for_with_supported_currency_and_country() {
+ $supported_currencies = [ 'USD', 'CAD' ];
+ $supported_countries = [ 'US', 'CA' ];
+
+ // Test with supported currency and country.
+ $this->assertTrue(
+ PaymentMethodUtils::is_available_for( $supported_currencies, $supported_countries, 'USD', 'US' ),
+ 'Should be available for USD in US'
+ );
+
+ $this->assertTrue(
+ PaymentMethodUtils::is_available_for( $supported_currencies, $supported_countries, 'CAD', 'CA' ),
+ 'Should be available for CAD in CA'
+ );
+ }
+
+ /**
+ * Test that is_available_for() works correctly with unsupported currency.
+ */
+ public function test_is_available_for_with_unsupported_currency() {
+ $supported_currencies = [ 'USD', 'CAD' ];
+ $supported_countries = [ 'US', 'CA' ];
+
+ $this->assertFalse(
+ PaymentMethodUtils::is_available_for( $supported_currencies, $supported_countries, 'EUR', 'US' ),
+ 'Should not be available for EUR in US'
+ );
+ }
+
+ /**
+ * Test that is_available_for() works correctly with unsupported country.
+ */
+ public function test_is_available_for_with_unsupported_country() {
+ $supported_currencies = [ 'USD', 'CAD' ];
+ $supported_countries = [ 'US', 'CA' ];
+
+ $this->assertFalse(
+ PaymentMethodUtils::is_available_for( $supported_currencies, $supported_countries, 'USD', 'GB' ),
+ 'Should not be available for USD in GB'
+ );
+ }
+
+ /**
+ * Test that is_available_for() works correctly with empty support arrays.
+ */
+ public function test_is_available_for_with_empty_support_arrays() {
+ // Empty arrays should allow all currencies and countries.
+ $this->assertTrue(
+ PaymentMethodUtils::is_available_for( [], [], 'USD', 'US' ),
+ 'Empty arrays should allow all currencies and countries'
+ );
+
+ $this->assertTrue(
+ PaymentMethodUtils::is_available_for( [], [], 'EUR', 'GB' ),
+ 'Empty arrays should allow all currencies and countries'
+ );
+ }
+
+ /**
+ * Test that is_bnpl() correctly identifies BNPL methods.
+ */
+ public function test_is_bnpl() {
+ $capabilities = [ PaymentMethodCapability::BUY_NOW_PAY_LATER ];
+ $this->assertTrue(
+ PaymentMethodUtils::is_bnpl( $capabilities ),
+ 'Should identify BNPL capability'
+ );
+
+ $capabilities = [ PaymentMethodCapability::TOKENIZATION ];
+ $this->assertFalse(
+ PaymentMethodUtils::is_bnpl( $capabilities ),
+ 'Should not identify non-BNPL capability as BNPL'
+ );
+
+ $capabilities = [];
+ $this->assertFalse(
+ PaymentMethodUtils::is_bnpl( $capabilities ),
+ 'Empty capabilities should not be identified as BNPL'
+ );
+ }
+
+ /**
+ * Test that is_reusable() identifies tokenizable methods.
+ */
+ public function test_is_reusable() {
+ $capabilities = [ PaymentMethodCapability::TOKENIZATION ];
+ $this->assertTrue(
+ PaymentMethodUtils::is_reusable( $capabilities ),
+ 'Should identify tokenizable capability'
+ );
+
+ $capabilities = [ PaymentMethodCapability::BUY_NOW_PAY_LATER ];
+ $this->assertFalse(
+ PaymentMethodUtils::is_reusable( $capabilities ),
+ 'Should not identify non-tokenizable capability as reusable'
+ );
+
+ $capabilities = [];
+ $this->assertFalse(
+ PaymentMethodUtils::is_reusable( $capabilities ),
+ 'Empty capabilities should not be identified as reusable'
+ );
+ }
+
+ /**
+ * Test that accepts_only_domestic_payments() identifies domestic-only methods.
+ */
+ public function test_accepts_only_domestic_payments() {
+ $capabilities = [ PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY ];
+ $this->assertTrue(
+ PaymentMethodUtils::accepts_only_domestic_payments( $capabilities ),
+ 'Should identify domestic-only capability'
+ );
+
+ $capabilities = [ PaymentMethodCapability::TOKENIZATION ];
+ $this->assertFalse(
+ PaymentMethodUtils::accepts_only_domestic_payments( $capabilities ),
+ 'Should not identify non-domestic-only capability as domestic-only'
+ );
+
+ $capabilities = [];
+ $this->assertFalse(
+ PaymentMethodUtils::accepts_only_domestic_payments( $capabilities ),
+ 'Empty capabilities should not be identified as domestic-only'
+ );
+ }
+
+ /**
+ * Test that allows_manual_capture() identifies manual capture support.
+ */
+ public function test_allows_manual_capture() {
+ $capabilities = [ PaymentMethodCapability::CAPTURE_LATER ];
+ $this->assertTrue(
+ PaymentMethodUtils::allows_manual_capture( $capabilities ),
+ 'Should identify manual capture capability'
+ );
+
+ $capabilities = [ PaymentMethodCapability::TOKENIZATION ];
+ $this->assertFalse(
+ PaymentMethodUtils::allows_manual_capture( $capabilities ),
+ 'Should not identify non-manual-capture capability as supporting manual capture'
+ );
+
+ $capabilities = [];
+ $this->assertFalse(
+ PaymentMethodUtils::allows_manual_capture( $capabilities ),
+ 'Empty capabilities should not be identified as supporting manual capture'
+ );
+ }
+
+ /**
+ * Test that is_domestic_currency_for_country() works with valid combinations.
+ */
+ public function test_is_domestic_currency_for_country_with_valid_combinations() {
+ // Test some known valid combinations.
+ $this->assertTrue(
+ PaymentMethodUtils::is_domestic_currency_for_country( 'USD', 'US' ),
+ 'USD should be domestic for US'
+ );
+
+ $this->assertTrue(
+ PaymentMethodUtils::is_domestic_currency_for_country( 'EUR', 'DE' ),
+ 'EUR should be domestic for DE'
+ );
+
+ $this->assertTrue(
+ PaymentMethodUtils::is_domestic_currency_for_country( 'GBP', 'GB' ),
+ 'GBP should be domestic for GB'
+ );
+ }
+
+ /**
+ * Test that is_domestic_currency_for_country() works with invalid combinations.
+ */
+ public function test_is_domestic_currency_for_country_with_invalid_combinations() {
+ $this->assertFalse(
+ PaymentMethodUtils::is_domestic_currency_for_country( 'EUR', 'US' ),
+ 'EUR should not be domestic for US'
+ );
+
+ $this->assertFalse(
+ PaymentMethodUtils::is_domestic_currency_for_country( 'USD', 'GB' ),
+ 'USD should not be domestic for GB'
+ );
+
+ // Test with invalid country code.
+ $this->assertFalse(
+ PaymentMethodUtils::is_domestic_currency_for_country( 'USD', 'XX' ),
+ 'Should return false for invalid country code'
+ );
+ }
+
+ /**
+ * Test that get_payment_method_definitions_json() returns valid JSON.
+ */
+ public function test_get_payment_method_definitions_json_returns_valid_json() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+ $registry->register_payment_method( MockPaymentMethodDefinition::class );
+ $registry->register_payment_method( SecondMockPaymentMethodDefinition::class );
+
+ $json = PaymentMethodUtils::get_payment_method_definitions_json();
+
+ // Verify it's valid JSON.
+ $decoded = json_decode( $json, true );
+ $this->assertNotNull( $decoded, 'Should return valid JSON' );
+ $this->assertIsArray( $decoded, 'Decoded JSON should be an array' );
+
+ // Verify required fields are present.
+ foreach ( $decoded as $method ) {
+ $this->assertArrayHasKey( 'id', $method );
+ $this->assertArrayHasKey( 'title', $method );
+ $this->assertArrayHasKey( 'description', $method );
+ $this->assertArrayHasKey( 'icon', $method );
+ $this->assertArrayHasKey( 'currencies', $method );
+ $this->assertArrayHasKey( 'allows_manual_capture', $method );
+ $this->assertArrayHasKey( 'allows_pay_later', $method );
+ $this->assertArrayHasKey( 'accepts_only_domestic_payment', $method );
+ }
+ }
+
+ /**
+ * Test that get_payment_method_definitions_json() returns empty string for empty registry.
+ */
+ public function test_get_payment_method_definitions_json_empty_registry() {
+ $registry = PaymentMethodDefinitionRegistry::instance();
+ $json = PaymentMethodUtils::get_payment_method_definitions_json();
+
+ $this->assertJson( $json );
+ $decoded = json_decode( $json, true );
+ $this->assertIsArray( $decoded );
+ $this->assertEmpty( $decoded );
+ }
+}
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
index 39e94cba660..db88e07dcc0 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
@@ -975,6 +975,119 @@ public function test_intent_status_requires_action() {
);
}
+ /**
+ * Test processing offline payment with the status "requires_action".
+ * This is the status returned when the shopper needs to complete
+ * the payment offsite.
+ */
+ public function test_intent_status_requires_action_offine_payment() {
+ // Arrange: Reusable data.
+ $intent_id = 'pi_mock';
+ $charge_id = 'ch_mock';
+ $customer_id = 'cus_mock';
+ $status = Intent_Status::REQUIRES_ACTION;
+ $secret = 'cs_mock';
+ $order_id = 123;
+ $total = 12.23;
+
+ // Arrange: Create an order to test with.
+ $mock_order = $this->createMock( 'WC_Order' );
+
+ // Arrange: Set a good return value for order ID.
+ $mock_order
+ ->method( 'get_id' )
+ ->willReturn( $order_id );
+
+ // Arrange: Set a good return value for order total.
+ $mock_order
+ ->method( 'get_total' )
+ ->willReturn( $total );
+
+ // Arrange: Set a WP_User object as a return value of order's get_user.
+ $mock_order
+ ->method( 'get_user' )
+ ->willReturn( wp_get_current_user() );
+
+ // Arrange: Set a good return value for customer ID.
+ $this->mock_customer_service->expects( $this->once() )
+ ->method( 'create_customer_for_user' )
+ ->willReturn( $customer_id );
+
+ // Arrange: Create a mock cart.
+ $mock_cart = $this->createMock( 'WC_Cart' );
+
+ // Arrange: Return a 'requires_action' response from create_and_confirm_intention().
+ $intent = WC_Helper_Intention::create_intention( [ 'status' => $status ] );
+
+ $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $intent );
+
+ // Assert: Order has correct charge id meta data.
+ // Assert: Order has correct intention status meta data.
+ // Assert: Order has correct intent ID.
+ // This test is a little brittle because we don't really care about the order
+ // in which the different calls are made, but it's not possible to write it
+ // otherwise for now.
+ // There's an issue open for that here:
+ // https://github.com/sebastianbergmann/phpunit/issues/4026.
+ $mock_order
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'update_meta_data' )
+ ->withConsecutive(
+ [ '_wcpay_mode', WC_Payments::mode()->is_test() ? 'test' : 'prod' ],
+ [ '_wcpay_multi_currency_stripe_exchange_rate', 0.86 ]
+ );
+
+ // Assert: The Order_Service is called correctly.
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'set_customer_id_for_order' )
+ ->with( $mock_order, $customer_id );
+
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'set_payment_method_id_for_order' )
+ ->with( $mock_order, 'pm_mock' );
+
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with( $mock_order, $intent );
+
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'update_order_status_from_intent' )
+ ->with( $mock_order, $intent );
+
+ // Assert: empty_cart() was called (just like status success).
+ $mock_cart
+ ->expects( $this->once() )
+ ->method( 'empty_cart' );
+
+ $charge_request = $this->mock_wcpay_request( Get_Charge::class, 1, 'ch_mock' );
+
+ $charge_request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] );
+
+ // Act: process payment.
+ $payment_information_mock = $this->getMockBuilder( 'WCPay\Payment_Information' )
+ ->setConstructorArgs( [ 'pm_mock', $mock_order, null, null, null, null, null, '', 'card' ] )
+ ->setMethods( [ 'is_offline_payment_method' ] )
+ ->getMock();
+ $payment_information_mock->method( 'is_offline_payment_method' )
+ ->willReturn( true );
+
+ $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information_mock );
+
+ // Assert: Returning correct array.
+ $this->assertEquals( 'success', $result['result'] );
+ $this->assertEquals( $this->return_url, $result['redirect'] );
+ }
+
/**
* Test processing free order with the status "requires_action".
* This is the status returned when the saved card setup requires
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
index fe96d9d9834..c2b4b661150 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
@@ -265,7 +265,15 @@ public function test_update_failing_payment_method_copies_last_method_from_renew
public function test_update_failing_payment_method_does_not_copy_method_if_renewal_has_no_method() {
$subscription = WC_Helper_Order::create_order( self::USER_ID );
- $renewal_order = WC_Helper_Order::create_order( self::USER_ID );
+ $renewal_order = $this->createMock( WC_Order::class );
+
+ $renewal_order->expects( $this->once() )
+ ->method( 'get_payment_tokens' )
+ ->willReturn( [] );
+
+ $renewal_order->expects( $this->once() )
+ ->method( 'add_order_note' )
+ ->with( 'Unable to update subscription payment method: No valid payment token or method found.' );
$this->wcpay_gateway->update_failing_payment_method( $subscription, $renewal_order );
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php
index e62e4591896..ef0bc69c0cc 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php
@@ -747,13 +747,24 @@ public function test_payment_methods_enabled_based_on_currency_limits() {
WC()->session->init();
WC()->cart->empty_cart();
- // Total is 100 USD, which is above both payment methods (Affirm and AfterPay) minimums.
- WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 10 );
+ // Total is 10 USD, which is below Affirm minimum but above AfterPay minimum.
+ WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 1 );
WC()->cart->calculate_totals();
$affirm_method = $this->payment_methods['affirm'];
$afterpay_method = $this->payment_methods['afterpay_clearpay'];
+ $this->assertFalse( $affirm_method->is_enabled_at_checkout( 'US' ) ); // Affirm minimum is 50 USD.
+ $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) ); // AfterPay minimum is 1 USD.
+
+ // Currency Limits check for affirm can be skipped by passing a second parameter (this is a workaround for the blocks editor).
+ $this->assertTrue( $affirm_method->is_enabled_at_checkout( 'US', true ) );
+
+ WC()->cart->empty_cart();
+ // Total is 100 USD, which is above both payment methods (Affirm and AfterPay) minimums.
+ WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 10 );
+ WC()->cart->calculate_totals();
+
$this->assertTrue( $affirm_method->is_enabled_at_checkout( 'US' ) );
$this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) );
}
@@ -3757,6 +3768,92 @@ public function test_get_recommended_payment_method_no_country_code_provided( $i
remove_filter( 'woocommerce_countries_base_country', $filter_callback );
}
+ public function test_updating_subscription_for_non_3ds_cards_removes_hook() {
+ $_GET['change_payment_method'] = 10;
+ WC_Subscriptions::set_wcs_is_subscription(
+ function ( $order ) {
+ return true;
+ }
+ );
+
+ $pi = new Payment_Information( 'pm_test', WC_Helper_Order::create_order(), null, new WC_Payment_Token_CC(), null, null, null, '', 'card' );
+
+ $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
+ $request->expects( $this->once() )
+ ->method( 'set_payment_methods' )
+ ->with( [ 'card' ] );
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) );
+
+ add_filter(
+ 'woocommerce_subscriptions_update_payment_via_pay_shortcode',
+ [ $this->card_gateway, 'update_payment_method_for_subscriptions' ],
+ 10,
+ 3
+ );
+
+ $this->assertEquals(
+ 10,
+ has_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this->card_gateway, 'update_payment_method_for_subscriptions' ] ),
+ 'Hook should be registered before payment processing'
+ );
+
+ $this->card_gateway->process_payment_for_order( WC()->cart, $pi );
+
+ $this->assertFalse(
+ has_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this->card_gateway, 'update_payment_method_for_subscriptions' ] ),
+ 'Hook should be removed after processing payment for subscription with non-3DS card'
+ );
+ }
+
+ public function test_updating_subscription_for_3ds_cards_sets_delayed_update_payment_method_all() {
+ $_GET['change_payment_method'] = 10;
+ WC_Subscriptions::set_wcs_is_subscription(
+ function ( $order ) {
+ return true;
+ }
+ );
+
+ $order = WC_Helper_Order::create_order();
+
+ // Set up POST data including update_all_subscriptions_payment_method.
+ $_POST = [
+ 'payment_method' => 'woocommerce_payments',
+ 'update_all_subscriptions_payment_method' => '1',
+ ];
+
+ $pi = new Payment_Information( 'pm_test', $order, null, new WC_Payment_Token_CC(), null, null, null, '', 'card' );
+
+ $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
+ $request->expects( $this->once() )
+ ->method( 'set_payment_methods' )
+ ->with( [ 'card' ] );
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn(
+ WC_Helper_Intention::create_intention(
+ [
+ 'status' => 'requires_action',
+ 'next_action' => [
+ 'type' => 'use_stripe_sdk',
+ ],
+ ]
+ )
+ );
+
+ try {
+ // The test exits early so we need to handle the exception.
+ $this->card_gateway->process_payment_for_order( WC()->cart, $pi );
+ } catch ( Exception $e ) {
+ $this->assertEquals(
+ 'woocommerce_payments',
+ $order->get_meta( '_delayed_update_payment_method_all' ),
+ 'Order metadata for delayed payment method update was not set correctly'
+ );
+ }
+ }
+
/**
* Sets up the expectation for a certain factor for the new payment
* process to be either set or unset.
diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php
index d46a32722af..2ce778d4bdc 100644
--- a/tests/unit/test-class-wc-payments-account.php
+++ b/tests/unit/test-class-wc-payments-account.php
@@ -428,7 +428,10 @@ function () {
);
// Act.
+ // Some code paths in the onboarding output JSON, so we use output buffering to suppress it while testing.
+ ob_start();
$wcpay_account->maybe_handle_onboarding();
+ ob_end_clean();
remove_all_filters( 'wp_doing_ajax' );
remove_all_filters( 'wp_die_ajax_handler' );
diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php
index c6ba78a24fc..b2ee7a0260c 100644
--- a/tests/unit/test-class-wc-payments-checkout.php
+++ b/tests/unit/test-class-wc-payments-checkout.php
@@ -85,7 +85,22 @@ public function set_up() {
// Setup the gateway mock.
$this->mock_wcpay_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class )
- ->onlyMethods( [ 'get_account_domestic_currency', 'get_payment_method_ids_enabled_at_checkout', 'should_use_stripe_platform_on_checkout_page', 'should_support_saved_payments', 'is_saved_cards_enabled', 'save_payment_method_checkbox', 'get_account_statement_descriptor', 'get_icon_url', 'get_payment_method_ids_enabled_at_checkout_filtered_by_fees', 'is_subscription_item_in_cart', 'wc_payments_get_payment_method_by_id', 'display_gateway_html' ] )
+ ->onlyMethods(
+ [
+ 'get_account_domestic_currency',
+ 'should_use_stripe_platform_on_checkout_page',
+ 'should_support_saved_payments',
+ 'is_saved_cards_enabled',
+ 'save_payment_method_checkbox',
+ 'get_account_statement_descriptor',
+ 'get_icon_url',
+ 'get_upe_enabled_payment_method_ids_based_on_manual_capture',
+ 'get_payment_method_ids_enabled_at_checkout_filtered_by_fees',
+ 'is_subscription_item_in_cart',
+ 'wc_payments_get_payment_method_by_id',
+ 'display_gateway_html',
+ ]
+ )
->disableOriginalConstructor()
->getMock();
$this->mock_wcpay_gateway->id = 'woocommerce_payments';
@@ -138,7 +153,7 @@ public function test_save_payment_method_checkbox_not_called_when_saved_cards_di
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -159,7 +174,7 @@ public function test_save_payment_method_checkbox_not_called_for_non_logged_in_u
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -185,7 +200,7 @@ public function test_save_payment_method_checkbox_called() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -208,7 +223,7 @@ public function test_save_payment_method_checkbox_called() {
public function test_display_gateway_html_called() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -221,7 +236,7 @@ public function test_display_gateway_html_called() {
public function test_is_woopay_enabled_when_should_enable_woopay_and_enable_it_on_cart_or_checkout() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_woopay_utilities->method( 'should_enable_woopay' )->willReturn( true );
@@ -234,7 +249,7 @@ public function test_is_woopay_enabled_when_should_enable_woopay_and_enable_it_o
public function test_is_woopay_enabled_false_when_should_not_enable_woopay() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_woopay_utilities->method( 'should_enable_woopay' )->willReturn( false );
@@ -247,7 +262,7 @@ public function test_is_woopay_enabled_false_when_should_not_enable_woopay() {
public function test_is_woopay_enabled_false_when_should_enable_woopay_but_not_enable_it_on_cart_or_checkout() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_woopay_utilities->method( 'should_enable_woopay' )->willReturn( true );
@@ -260,7 +275,7 @@ public function test_is_woopay_enabled_false_when_should_enable_woopay_but_not_e
public function test_return_icon_url() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -276,7 +291,7 @@ public function test_return_icon_url() {
public function test_force_network_saved_cards_enabled_when_should_use_stripe_platform() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -290,7 +305,7 @@ public function test_force_network_saved_cards_enabled_when_should_use_stripe_pl
public function test_force_network_saved_cards_disabled_when_should_not_use_stripe_platform() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [] );
$this->mock_wcpay_gateway
@@ -309,7 +324,7 @@ public function test_link_payment_method_provided_when_card_enabled() {
$dark_icon_url = 'test-dark-icon-url';
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [ 'card', 'link' ] );
$this->mock_wcpay_gateway
@@ -406,7 +421,7 @@ public function test_no_save_option_for_non_reusable_payment_method( $payment_me
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn(
[
$payment_method_id,
@@ -445,7 +460,7 @@ public function test_no_save_option_for_reusable_payment_payment_with_subscripti
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn(
[
Payment_Method::CARD,
@@ -474,7 +489,7 @@ public function test_no_save_option_for_reusable_payment_payment_but_with_saved_
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn(
[
Payment_Method::CARD,
@@ -503,7 +518,7 @@ public function test_save_option_for_reusable_payment_payment() {
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn(
[
Payment_Method::CARD,
@@ -524,7 +539,7 @@ public function test_upe_appearance_transients() {
->willReturn( 'US' );
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn(
[
Payment_Method::CARD,
@@ -583,7 +598,7 @@ public function test_credit_card_testing_instructions_by_country( string $countr
->willReturn( $country );
$this->mock_wcpay_gateway
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->method( 'get_upe_enabled_payment_method_ids_based_on_manual_capture' )
->willReturn( [ 'card' ] );
$this->mock_wcpay_gateway
diff --git a/tests/unit/test-class-wc-payments-currency-manager.php b/tests/unit/test-class-wc-payments-currency-manager.php
index 6f13664ec75..ce4d32a610b 100644
--- a/tests/unit/test-class-wc-payments-currency-manager.php
+++ b/tests/unit/test-class-wc-payments-currency-manager.php
@@ -59,6 +59,7 @@ public function set_up() {
->disableOriginalConstructor()
->setMethods(
[
+ 'wc_payments_get_payment_method_map',
'get_upe_enabled_payment_method_ids',
'get_account_country',
'get_account_domestic_currency',
@@ -83,13 +84,16 @@ public function set_up() {
public function test_it_should_not_update_available_currencies_when_enabled_payment_methods_do_not_need_it() {
$this->multi_currency_mock->expects( $this->never() )->method( $this->anything() );
+ $this->gateway_mock->expects( $this->atLeastOnce() )->method( 'wc_payments_get_payment_method_map' )->willReturn( $this->get_mocked_payment_methods_map() );
$this->gateway_mock->expects( $this->atLeastOnce() )->method( 'get_upe_enabled_payment_method_ids' )->willReturn( [ 'card' ] );
$this->gateway_mock->expects( $this->atLeastOnce() )->method( 'get_account_domestic_currency' )->willReturn( 'USD' );
+ $this->multi_currency_mock->expects( $this->never() )->method( 'set_enabled_currencies' );
$this->currency_manager->maybe_add_missing_currencies();
}
public function test_it_should_not_update_available_currencies_when_not_needed() {
+ $this->gateway_mock->expects( $this->atLeastOnce() )->method( 'wc_payments_get_payment_method_map' )->willReturn( $this->get_mocked_payment_methods_map() );
$this->gateway_mock->expects( $this->atLeastOnce() )->method( 'get_upe_enabled_payment_method_ids' )->willReturn(
[
'card',
@@ -118,6 +122,7 @@ public function test_it_should_not_update_available_currencies_when_not_needed()
}
public function test_it_should_update_available_currencies_when_needed() {
+ $this->gateway_mock->expects( $this->atLeastOnce() )->method( 'wc_payments_get_payment_method_map' )->willReturn( $this->get_mocked_payment_methods_map() );
$this->gateway_mock->expects( $this->atLeastOnce() )->method( 'get_upe_enabled_payment_method_ids' )->willReturn(
[
'card',
@@ -158,6 +163,7 @@ public function test_it_should_update_available_currencies_when_needed() {
}
public function test_it_should_not_update_available_currencies_with_bnpl_methods() {
+ $this->gateway_mock->expects( $this->atLeastOnce() )->method( 'wc_payments_get_payment_method_map' )->willReturn( $this->get_mocked_payment_methods_map() );
$this->gateway_mock->expects( $this->atLeastOnce() )->method( 'get_upe_enabled_payment_method_ids' )->willReturn(
[
'card',
@@ -184,6 +190,7 @@ public function test_it_should_not_update_available_currencies_with_bnpl_methods
}
public function test_it_should_update_available_currencies_with_bnpl_methods() {
+ $this->gateway_mock->expects( $this->atLeastOnce() )->method( 'wc_payments_get_payment_method_map' )->willReturn( $this->get_mocked_payment_methods_map() );
$this->gateway_mock->expects( $this->atLeastOnce() )->method( 'get_upe_enabled_payment_method_ids' )->willReturn(
[
'card',
@@ -216,4 +223,65 @@ public function test_it_should_update_available_currencies_with_bnpl_methods() {
$this->currency_manager->maybe_add_missing_currencies();
}
+
+ private function get_mocked_payment_methods_map() {
+ $card_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $card_payment_method->method( 'get_currencies' )->willReturn( [] );
+ $card_payment_method->method( 'get_title' )->willReturn( 'Card Payment Method' );
+ $card_payment_method->method( 'get_id' )->willReturn( 'card' );
+ $card_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ $becs_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $becs_payment_method->method( 'get_currencies' )->willReturn( [ 'AUD' ] );
+ $becs_payment_method->method( 'get_title' )->willReturn( 'au_becs_debit Payment Method' );
+ $becs_payment_method->method( 'get_id' )->willReturn( 'au_becs_debit' );
+ $becs_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ $bancontact_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $bancontact_payment_method->method( 'get_currencies' )->willReturn( [ 'EUR' ] );
+ $bancontact_payment_method->method( 'get_title' )->willReturn( 'bancontact Payment Method' );
+ $bancontact_payment_method->method( 'get_id' )->willReturn( 'bancontact' );
+ $bancontact_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ $eps_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $eps_payment_method->method( 'get_currencies' )->willReturn( [ 'EUR' ] );
+ $eps_payment_method->method( 'get_title' )->willReturn( 'eps Payment Method' );
+ $eps_payment_method->method( 'get_id' )->willReturn( 'eps' );
+ $eps_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ $giropay_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $giropay_payment_method->method( 'get_currencies' )->willReturn( [ 'EUR' ] );
+ $giropay_payment_method->method( 'get_title' )->willReturn( 'giropay Payment Method' );
+ $giropay_payment_method->method( 'get_id' )->willReturn( 'giropay' );
+ $giropay_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ $sepa_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $sepa_payment_method->method( 'get_currencies' )->willReturn( [ 'EUR' ] );
+ $sepa_payment_method->method( 'get_title' )->willReturn( 'sepa_debit Payment Method' );
+ $sepa_payment_method->method( 'get_id' )->willReturn( 'sepa_debit' );
+ $sepa_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ $klarna_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $klarna_payment_method->method( 'get_currencies' )->willReturn( [ 'USD', 'GBP', 'EUR', 'DKK', 'NOK', 'SEK' ] );
+ $klarna_payment_method->method( 'get_title' )->willReturn( 'klarna Payment Method' );
+ $klarna_payment_method->method( 'get_id' )->willReturn( 'klarna' );
+ $klarna_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( true );
+
+ $link_payment_method = $this->createMock( \WCPay\Payment_Methods\UPE_Payment_Method::class );
+ $link_payment_method->method( 'get_currencies' )->willReturn( [ 'USD' ] );
+ $link_payment_method->method( 'get_title' )->willReturn( 'link Payment Method' );
+ $link_payment_method->method( 'get_id' )->willReturn( 'link' );
+ $link_payment_method->method( 'has_domestic_transactions_restrictions' )->willReturn( false );
+
+ return [
+ 'card' => $card_payment_method,
+ 'au_becs_debit' => $becs_payment_method,
+ 'bancontact' => $bancontact_payment_method,
+ 'eps' => $eps_payment_method,
+ 'giropay' => $giropay_payment_method,
+ 'sepa_debit' => $sepa_payment_method,
+ 'klarna' => $klarna_payment_method,
+ 'link' => $link_payment_method,
+ ];
+ }
}
diff --git a/tests/unit/test-class-wc-payments-incentives-service.php b/tests/unit/test-class-wc-payments-incentives-service.php
index 9c8921289ab..9b12a949853 100644
--- a/tests/unit/test-class-wc-payments-incentives-service.php
+++ b/tests/unit/test-class-wc-payments-incentives-service.php
@@ -406,7 +406,9 @@ function ( $value, $expiration ) {
$this->assertSame( 'yes', $value );
// Ensure the cache is set to expire in 90 days - 30 days = 60 days.
- $this->assertSame( 60 * DAY_IN_SECONDS, $expiration );
+ $expected_expiration = 60 * DAY_IN_SECONDS;
+ // Allowing 5-second difference to avoid flaky tests due to time() precision.
+ $this->assertLessThanOrEqual( 5, abs( $expected_expiration - $expiration ), 'Expiration time should be within 5 second of expected value' );
return $value;
},
diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php
index d3fef27ea1f..1ab8593f879 100644
--- a/tests/unit/test-class-wc-payments-order-service.php
+++ b/tests/unit/test-class-wc-payments-order-service.php
@@ -8,6 +8,7 @@
use WCPay\Constants\Fraud_Meta_Box_Type;
use WCPay\Constants\Order_Status;
use WCPay\Constants\Intent_Status;
+use WCPay\Constants\Payment_Method;
use WCPay\Fraud_Prevention\Models\Rule;
/**
@@ -476,6 +477,47 @@ public function mark_payment_started_provider() {
];
}
+ /**
+ * Tests if the order is marked with the payment on hold for offline payments.
+ * Public method update_order_status_from_intent calls private method mark_payment_on_hold.
+ */
+ public function test_mark_payment_on_hold() {
+ // Arrange: Create intention with provided args.
+ $intent = WC_Helper_Intention::create_intention(
+ [
+ 'status' => Intent_Status::REQUIRES_ACTION,
+ 'payment_method_types' => [ 'offline_test_payment_method' ],
+ 'payment_method_options' => [ Payment_Method::OFFLINE_PAYMENT_METHODS[0] => [] ],
+ ]
+ );
+
+ // Act: Attempt to mark the payment on hold.
+ $this->order_service->update_order_status_from_intent( $this->order, $intent );
+
+ // Assert: Check to make sure the intent_status meta was set.
+ $this->assertEquals( $intent->get_status(), $this->order_service->get_intention_status_for_order( $this->order ) );
+
+ // Assert: Confirm that the fraud outcome status and fraud meta box type meta were not set/set correctly.
+ $this->assertEquals( false, $this->order_service->get_fraud_outcome_status_for_order( $this->order ) );
+ $this->assertEquals( Fraud_Meta_Box_Type::NOT_CARD, $this->order_service->get_fraud_meta_box_type_for_order( $this->order ) );
+
+ // Assert: Check that the order status was updated to on hold.
+ $this->assertTrue( $this->order->has_status( Order_Status::ON_HOLD ) );
+
+ // Assert: Check that the notes were updated.
+ $notes = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
+ $this->assertStringContainsString( 'started using WooPayments', $notes[0]->content );
+ $this->assertStringContainsString( 'Payments (pi_mock)', $notes[0]->content );
+
+ // Assert: Check that the order was unlocked.
+ $this->assertFalse( get_transient( 'wcpay_processing_intent_' . $this->order->get_id() ) );
+
+ // Assert: Applying the same data multiple times does not cause duplicate actions.
+ $this->order_service->update_order_status_from_intent( $this->order, $intent );
+ $notes_2 = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
+ $this->assertEquals( count( $notes ), count( $notes_2 ) );
+ }
+
/**
* Tests if mark_payment_started exits if the order status is not Peding.
* Public method update_order_status_from_intent calls private method mark_payment_started.
@@ -1276,6 +1318,26 @@ public function test_attach_intent_info_to_order() {
$this->assertEquals( $intent_id, $this->order->get_meta( '_intent_id', true ) );
}
+ public function test_attach_intent_order_with_allow_update_on_success() {
+ $intent = WC_Helper_Intention::create_intention(
+ [
+ 'id' => 'pi_mock',
+ 'status' => Intent_Status::SUCCEEDED,
+ ]
+ );
+ $this->order_service->attach_intent_info_to_order( $this->order, $intent );
+
+ $another_intent = WC_Helper_Intention::create_intention(
+ [
+ 'id' => 'pi_mock_2',
+ 'status' => Intent_Status::CANCELED,
+ ]
+ );
+ $this->order_service->attach_intent_info_to_order( $this->order, $another_intent, true );
+
+ $this->assertEquals( Intent_Status::CANCELED, $this->order->get_meta( '_intention_status', true ) );
+ }
+
public function test_attach_intent_info_to_order_after_successful_payment() {
$intent = WC_Helper_Intention::create_intention(
[
diff --git a/tests/unit/test-class-wc-payments-order-success-page.php b/tests/unit/test-class-wc-payments-order-success-page.php
index 4fc35ce02d1..7bdbbd1ab88 100644
--- a/tests/unit/test-class-wc-payments-order-success-page.php
+++ b/tests/unit/test-class-wc-payments-order-success-page.php
@@ -6,6 +6,8 @@
*/
use WCPay\Payment_Methods\UPE_Payment_Method;
+use WCPay\Core\Server\Request\Get_Intention;
+use WCPay\Constants\Payment_Method;
/**
* WC_Payments_Order_Success_Page unit tests.
@@ -125,4 +127,80 @@ public function test_show_lpm_payment_method_name_icon_not_found() {
$this->assertFalse( $result );
}
+
+ public function test_replace_order_received_text_for_failed_orders_with_failed_status() {
+ $order = WC_Helper_Order::create_order();
+ $order->set_status( 'failed' );
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->set_total( 50 ); // Ensure order needs payment.
+ $order->save();
+
+ // Set up global wp query vars.
+ global $wp;
+ $wp->query_vars['order-received'] = $order->get_id();
+
+ $original_text = 'Thank you. Your order has been received.';
+ $result = $this->payments_order_success_page->replace_order_received_text_for_failed_orders( $original_text );
+
+ $this->assertStringContainsString( 'Unfortunately, your order has failed', $result );
+ $this->assertStringContainsString( wc_get_checkout_url(), $result );
+ }
+
+ public function test_replace_order_received_text_for_failed_orders_with_redirect_payment_failed_intent() {
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments_wechat_pay' );
+ $order->set_total( 50 ); // Ensure order needs payment.
+ $order->add_meta_data( '_intent_id', 'pi_123' );
+ $order->save();
+
+ // Set up global wp query vars.
+ global $wp;
+ $wp->query_vars['order-received'] = $order->get_id();
+
+ // Mock the Get_Intention request.
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'id' => 'pi_123',
+ 'status' => 'requires_payment_method',
+ 'last_payment_error' => [ 'message' => 'Payment failed' ],
+ ]
+ );
+
+ $this->mock_wcpay_request( Get_Intention::class, 1, 'pi_123' )
+ ->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $original_text = 'Thank you. Your order has been received.';
+ $result = $this->payments_order_success_page->replace_order_received_text_for_failed_orders( $original_text );
+
+ $this->assertStringContainsString( 'Unfortunately, your order has failed', $result );
+ $this->assertStringContainsString( wc_get_checkout_url(), $result );
+ }
+
+ public function test_replace_order_received_text_for_non_failed_order() {
+ $order = WC_Helper_Order::create_order();
+ $order->set_status( 'processing' );
+ $order->save();
+
+ // Set up global wp query vars.
+ global $wp;
+ $wp->query_vars['order-received'] = $order->get_id();
+
+ $original_text = 'Thank you. Your order has been received.';
+ $result = $this->payments_order_success_page->replace_order_received_text_for_failed_orders( $original_text );
+
+ $this->assertEquals( $original_text, $result );
+ }
+
+ public function test_replace_order_received_text_for_invalid_order() {
+ // Set up global wp query vars with invalid order ID.
+ global $wp;
+ $wp->query_vars['order-received'] = 999999;
+
+ $original_text = 'Thank you. Your order has been received.';
+ $result = $this->payments_order_success_page->replace_order_received_text_for_failed_orders( $original_text );
+
+ $this->assertEquals( $original_text, $result );
+ }
}
diff --git a/tests/unit/test-class-wc-payments.php b/tests/unit/test-class-wc-payments.php
index 430ff7a1c96..9f0e9145842 100644
--- a/tests/unit/test-class-wc-payments.php
+++ b/tests/unit/test-class-wc-payments.php
@@ -143,4 +143,85 @@ private function set_woopay_enabled( $is_enabled ) {
// Trigger the addition of the disable nonce filter when appropriate.
apply_filters( 'rest_request_before_callbacks', [], [], new WP_REST_Request() );
}
+
+ public function test_set_woopayments_gateways_before_other_gateways_when_not_in_ordering() {
+ $woopayments_gateway_ids = WC_Payments::get_woopayments_gateway_ids();
+ $main_gateway_id = WC_Payments::get_gateway()->id;
+
+ $initial_ordering = [
+ 'other_gateway_1' => 0,
+ 'other_gateway_2' => 1,
+ ];
+
+ $result = WC_Payments::order_woopayments_gateways( $initial_ordering );
+
+ $this->assertContainsAllGateways( $woopayments_gateway_ids, $result );
+ $this->assertSequentialOrdering( $woopayments_gateway_ids, $result );
+ $this->assertLessThan( $result['other_gateway_1'], $result[ $main_gateway_id ] );
+ }
+
+ public function test_set_woopayments_gateways_after_main_gateway() {
+ $woopayments_gateway_ids = WC_Payments::get_woopayments_gateway_ids();
+ $main_gateway_id = WC_Payments::get_gateway()->id;
+
+ $initial_ordering = [
+ 'other_gateway_1' => 0,
+ $main_gateway_id => 1,
+ 'other_gateway_2' => 2,
+ ];
+
+ $result = WC_Payments::order_woopayments_gateways( $initial_ordering );
+
+ $this->assertContainsAllGateways( $woopayments_gateway_ids, $result );
+ $this->assertSequentialOrdering( $woopayments_gateway_ids, $result );
+ $this->assertLessThan( $result[ $main_gateway_id ], $result['other_gateway_1'] );
+ $this->assertLessThan( $result['other_gateway_2'], $result[ $main_gateway_id ] );
+ }
+
+ public function test_set_woopayments_gateways_at_beginning_when_ordering_is_empty() {
+ $woopayments_gateway_ids = WC_Payments::get_woopayments_gateway_ids();
+ $initial_ordering = [];
+
+ $result = WC_Payments::order_woopayments_gateways( $initial_ordering );
+
+ $this->assertContainsAllGateways( $woopayments_gateway_ids, $result );
+ $this->assertSequentialOrdering( $woopayments_gateway_ids, $result );
+ $this->assertEquals( count( $woopayments_gateway_ids ), count( $result ) );
+ }
+
+ /**
+ * Assert that all WooPayments gateways are in the result
+ *
+ * @param array $gateways Expected gateway IDs
+ * @param array $result Result from order_woopayments_gateways
+ */
+ private function assertContainsAllGateways( $gateways, $result ) {
+ foreach ( $gateways as $gateway_id ) {
+ $this->assertArrayHasKey( $gateway_id, $result );
+ }
+ }
+
+ /**
+ * Assert that the WooPayments gateways are in sequential order
+ *
+ * @param array $gateways Expected gateway IDs
+ * @param array $result Result from order_woopayments_gateways
+ */
+ private function assertSequentialOrdering( $gateways, $result ) {
+ $positions = [];
+ foreach ( $gateways as $gateway_id ) {
+ $positions[ $gateway_id ] = $result[ $gateway_id ];
+ }
+
+ asort( $positions );
+
+ // Check that positions are sequential.
+ $previous_position = null;
+ foreach ( $positions as $position ) {
+ if ( null !== $previous_position ) {
+ $this->assertEquals( $previous_position + 1, $position, 'Gateway positions should be sequential' );
+ }
+ $previous_position = $position;
+ }
+ }
}
diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
index 2fbd9c027d6..65fb97086b8 100644
--- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
+++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
@@ -794,8 +794,8 @@ function ( $message ) {
* Data provider for test_redacting_params
*/
public function redacting_params_data() {
- $string_should_not_include_secret = function ( $string ) {
- return false === strpos( $string, 'some-secret' );
+ $string_should_not_include_secret = function ( $input ) {
+ return false === strpos( $input, 'some-secret' );
};
return [
@@ -1156,6 +1156,42 @@ public function test_update_compatibility_data() {
$this->assertSame( 'success', $result['result'] );
}
+ public function test_get_readers_charge_summary() {
+ $transaction_id = uniqid( 'trx_' );
+ $charge_date = gmdate( 'Y-m-d', 1634291278 );
+ $this->mock_http_client
+ ->expects( $this->once() )
+ ->method( 'remote_request' )
+ ->willReturn(
+ [
+ 'body' => wp_json_encode(
+ [
+ 'result' => 'success',
+ 'data' => [
+ (object) [
+ 'reader_id' => 'reader_1',
+ 'count' => 1,
+ 'status' => 'active',
+ 'fee' => [
+ 'amount' => 100,
+ 'currency' => 'USD',
+ ],
+ ],
+ ],
+ ]
+ ),
+ 'response' => [
+ 'code' => 200,
+ 'message' => 'OK',
+ ],
+ ]
+ );
+
+ $result = $this->payments_api_client->get_readers_charge_summary( '2024-01-01', $transaction_id );
+ $this->assertSame( 1, $result['data'][0]['count'] );
+ }
+
+
public function test_get_tracking_info() {
$expect = [ 'hosting-provider' => 'test' ];
diff --git a/webpack/shared.js b/webpack/shared.js
index 2dce99ca3ec..e407e1532de 100644
--- a/webpack/shared.js
+++ b/webpack/shared.js
@@ -41,6 +41,7 @@ module.exports = {
'cart-block': './client/cart/blocks/index.js',
'plugins-page': './client/plugins-page/index.js',
'frontend-tracks': './client/frontend-tracks/index.js',
+ success: './client/success/index.js',
},
// Override webpack public path dynamically on every entry.
// Required for chunks loading to work on sites with JS concatenation.
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index b12bccf14c7..16eee4a1da9 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -11,7 +11,7 @@
* WC tested up to: 9.6.0
* Requires at least: 6.0
* Requires PHP: 7.3
- * Version: 9.0.0
+ * Version: 9.1.0
* Requires Plugins: woocommerce
*
* @package WooCommerce\Payments
@@ -335,7 +335,7 @@ function wcpay_get_jetpack_idc_custom_content(): array {
'nonAdminTitle' => __( 'Safe Mode activated', 'woocommerce-payments' ),
'nonAdminBodyText' => sprintf(
/* translators: %s: WooPayments. */
- __( 'We’ve detected that you have duplicate sites connected to %s. When Safe Mode is active, payments will not be interrupted. However, some features may not be available until you’ve resolved this issue below. Safe Mode is most frequently activated when you’re transferring your site from one domain to another, or creating a staging site for testing. A site adminstrator can resolve this issue. Learn more', 'woocommerce-payments' ),
+ __( 'We’ve detected that you have duplicate sites connected to %s. When Safe Mode is active, payments will not be interrupted. However, some features may not be available until you’ve resolved this issue below. Safe Mode is most frequently activated when you’re transferring your site from one domain to another, or creating a staging site for testing. A site administrator can resolve this issue. Learn more', 'woocommerce-payments' ),
'WooPayments'
),
'supportURL' => 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/safe-mode/',
@@ -344,6 +344,7 @@ function wcpay_get_jetpack_idc_custom_content(): array {
__( '%s Safe Mode', 'woocommerce-payments' ),
'WooPayments'
),
+ 'stayInSafeModeButtonLabel' => __( 'Stay in Safe Mode', 'woocommerce-payments' ),
'dynamicSiteUrlText' => sprintf(
/* translators: %s: WooPayments. */
__( "Notice: It appears that your 'wp-config.php' file might be using dynamic site URL values. Dynamic site URLs could cause %s to enter Safe Mode. Learn how to set a static site URL.", 'woocommerce-payments' ),
@@ -355,16 +356,7 @@ function wcpay_get_jetpack_idc_custom_content(): array {
$urls = Automattic\Jetpack\Identity_Crisis::get_mismatched_urls();
if ( false !== $urls ) {
$current_url = untrailingslashit( $urls['current_url'] );
- /**
- * Undo the reverse the Jetpack IDC library is doing since we want to display the URL.
- *
- * @see https://github.com/Automattic/jetpack-identity-crisis/blob/trunk/src/class-identity-crisis.php#L471
- */
- $idc_sync_error = Automattic\Jetpack\Identity_Crisis::check_identity_crisis();
- if ( is_array( $idc_sync_error ) && ! empty( $idc_sync_error['reversed_url'] ) ) {
- $urls['wpcom_url'] = strrev( $urls['wpcom_url'] );
- }
- $wpcom_url = untrailingslashit( $urls['wpcom_url'] );
+ $wpcom_url = untrailingslashit( $urls['wpcom_url'] );
$custom_content['migrateCardBodyText'] = sprintf(
/* translators: %1$s: The current site domain name. %2$s: The original site domain name. Please keep hostname tags in your translation so that they can be formatted properly. %3$s: WooPayments. */
@@ -377,6 +369,7 @@ function wcpay_get_jetpack_idc_custom_content(): array {
'WooPayments'
);
+ // Regular "Start Fresh" card body text - used for non-development sites.
$custom_content['startFreshCardBodyText'] = sprintf(
/* translators: %1$s: The current site domain name. %2$s: The original site domain name. Please keep hostname tags in your translation so that they can be formatted properly. %3$s: WooPayments. */
__(
@@ -387,6 +380,38 @@ function wcpay_get_jetpack_idc_custom_content(): array {
$wpcom_url,
'WooPayments'
);
+
+ // Start Fresh card body text when in the development mode.
+ $custom_content['startFreshCardBodyTextDev'] = sprintf(
+ /* translators: %1$s: The current site domain name. %2$s: The original site domain name. %3$s: WooPayments. */
+ __(
+ 'Recommended for - development sites
- sites that need access to all %3$s features
Please note that creating a fresh connection for %1$s would require restoring the connection on %2$s if that site is cloned back to production. Learn more. ',
+ 'woocommerce-payments'
+ ),
+ $current_url,
+ $wpcom_url,
+ 'WooPayments'
+ );
+
+ // Safe Mode card body text when in the development mode.
+ $custom_content['safeModeCardBodyText'] = sprintf(
+ /* translators: %s: WooPayments. */
+ __(
+ 'Recommended for - short-lived test sites
- sites that will be cloned back to production after testing
Please note that staying in Safe Mode will cause issues for some %s features such as dispute and refund updates, payment confirmations for local payment methods. Learn more. ',
+ 'woocommerce-payments'
+ ),
+ 'WooPayments'
+ );
+
+ $custom_content['mainBodyTextDev'] = sprintf(
+ /* translators: %1$s: The current site domain name. %2$s: The original site domain name. */
+ __(
+ 'Your site is in Safe Mode because %1$s appears to be a staging or development copy of %2$s. Two sites that are telling WooPayments they’re the same site. Learn more about Safe Mode issues.',
+ 'woocommerce-payments'
+ ),
+ $current_url,
+ $wpcom_url,
+ );
}
return $custom_content;
|