diff --git a/changelog/add-woopay-klaviyo-newsletter-support b/changelog/add-woopay-klaviyo-newsletter-support new file mode 100644 index 00000000000..64e94c6638e --- /dev/null +++ b/changelog/add-woopay-klaviyo-newsletter-support @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add WooPay Klaviyo newsletter integration. diff --git a/changelog/fix-198-mccy-fedex-conversion b/changelog/fix-198-mccy-fedex-conversion new file mode 100644 index 00000000000..7fecbc49b87 --- /dev/null +++ b/changelog/fix-198-mccy-fedex-conversion @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Fix FedEx insurance rates with different currencies. + + diff --git a/changelog/fix-6567-user-set-date-and-time-formatting-arent-respected-in-react-components b/changelog/fix-6567-user-set-date-and-time-formatting-arent-respected-in-react-components new file mode 100644 index 00000000000..5c69920cf26 --- /dev/null +++ b/changelog/fix-6567-user-set-date-and-time-formatting-arent-respected-in-react-components @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Apply User-Defined Date Formatting Settings to WP Admin React Components diff --git a/changelog/fix-8620-fee-types-in-fees-brakedown-tooltip-are-not-internationalised b/changelog/fix-8620-fee-types-in-fees-brakedown-tooltip-are-not-internationalised new file mode 100644 index 00000000000..57528510958 --- /dev/null +++ b/changelog/fix-8620-fee-types-in-fees-brakedown-tooltip-are-not-internationalised @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Use translatable strings on the fee breakdown tooltip of the payment settings screen. diff --git a/changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured b/changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured new file mode 100644 index 00000000000..f524fd812f1 --- /dev/null +++ b/changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Hide transaction fee on admin view order screen when transaction is not captured. diff --git a/changelog/fix-9735-render-transactions-correctly-on-capture b/changelog/fix-9735-render-transactions-correctly-on-capture new file mode 100644 index 00000000000..9ed7f628e4c --- /dev/null +++ b/changelog/fix-9735-render-transactions-correctly-on-capture @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Ensure captured transactions appear in the Transactions tab without requiring a page refresh. diff --git a/changelog/fix-9996-currency-conversion-fee-phrasing b/changelog/fix-9996-currency-conversion-fee-phrasing new file mode 100644 index 00000000000..bdee2cbc00f --- /dev/null +++ b/changelog/fix-9996-currency-conversion-fee-phrasing @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Use "currency conversion fee" instead "foreign exchange fee" in payment timeline and various other places. diff --git a/changelog/fix-ece-button-for-price-including-tax b/changelog/fix-ece-button-for-price-including-tax new file mode 100644 index 00000000000..521ceb2af68 --- /dev/null +++ b/changelog/fix-ece-button-for-price-including-tax @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Show express checkout for products w/o shipping but where tax is included into price. diff --git a/changelog/fix-tokenized-ece-product-bundles-totals b/changelog/fix-tokenized-ece-product-bundles-totals new file mode 100644 index 00000000000..c003feec46a --- /dev/null +++ b/changelog/fix-tokenized-ece-product-bundles-totals @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: fix: tokenized ECE item compatibility w/ product bundles + + diff --git a/changelog/update-s6837-reduce-api-impact-for-test-drive-setup b/changelog/update-s6837-reduce-api-impact-for-test-drive-setup new file mode 100644 index 00000000000..eaa0360fe7a --- /dev/null +++ b/changelog/update-s6837-reduce-api-impact-for-test-drive-setup @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Reduce the maximum number of API calls during the test-drive process and add maximum duration cut off logic. + + diff --git a/client/capital/index.tsx b/client/capital/index.tsx index 81e76ad91b4..469b2d283a8 100644 --- a/client/capital/index.tsx +++ b/client/capital/index.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import { __, _n } from '@wordpress/i18n'; import { TableCard } from '@woocommerce/components'; -import { dateI18n } from '@wordpress/date'; /** * Internal dependencies. @@ -25,6 +24,8 @@ import Chip from 'components/chip'; import { useLoans } from 'wcpay/data'; import { getAdminUrl } from 'wcpay/utils'; import './style.scss'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; +import DateFormatNotice from 'wcpay/components/date-format-notice'; const columns = [ { @@ -80,7 +81,7 @@ const getLoanStatusText = ( loan: CapitalLoan ) => { return loan.fully_paid_at ? __( 'Paid off', 'woocommerce-payments' ) + ': ' + - dateI18n( 'M j, Y', loan.fully_paid_at ) + formatDateTimeFromString( loan.fully_paid_at ) : __( 'Active', 'woocommerce-payments' ); }; @@ -112,7 +113,9 @@ const getRowsData = ( loans: CapitalLoan[] ) => const data = { paid_out_at: { value: loan.paid_out_at, - display: clickable( dateI18n( 'M j, Y', loan.paid_out_at ) ), + display: clickable( + formatDateTimeFromString( loan.paid_out_at ) + ), }, status: { value: getLoanStatusText( loan ), @@ -150,7 +153,7 @@ const getRowsData = ( loans: CapitalLoan[] ) => value: loan.first_paydown_at, display: clickable( loan.first_paydown_at - ? dateI18n( 'M j, Y', loan.first_paydown_at ) + ? formatDateTimeFromString( loan.first_paydown_at ) : '-' ), }, @@ -207,6 +210,7 @@ const CapitalPage = (): JSX.Element => { return ( + { wcpaySettings.accountLoans.has_active_loan && ( diff --git a/client/capital/test/__snapshots__/index.test.tsx.snap b/client/capital/test/__snapshots__/index.test.tsx.snap index 2b146dd5714..9f0e93091cf 100644 --- a/client/capital/test/__snapshots__/index.test.tsx.snap +++ b/client/capital/test/__snapshots__/index.test.tsx.snap @@ -5,6 +5,54 @@ exports[`CapitalPage renders the TableCard component with loan data 1`] = `
+
+ +
+ The date and time formats now match your preferences. You can update them anytime in the + + settings + + . +
+ +
diff --git a/client/capital/test/index.test.tsx b/client/capital/test/index.test.tsx index f9eb43b8a49..41ea917902a 100644 --- a/client/capital/test/index.test.tsx +++ b/client/capital/test/index.test.tsx @@ -25,6 +25,7 @@ declare const global: { accountLoans: { has_active_loan: boolean; }; + dateFormat: string; }; }; @@ -37,6 +38,7 @@ describe( 'CapitalPage', () => { }, accountLoans: { has_active_loan: true }, testMode: true, + dateFormat: 'M j, Y', }; } ); diff --git a/client/components/account-status/account-fees/expiration-description.js b/client/components/account-status/account-fees/expiration-description.js index 6be5b58681c..497f0207ef3 100644 --- a/client/components/account-status/account-fees/expiration-description.js +++ b/client/components/account-status/account-fees/expiration-description.js @@ -4,13 +4,12 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; -import moment from 'moment'; /** * Internal dependencies */ import { formatCurrency } from 'multi-currency/interface/functions'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; const ExpirationDescription = ( { feeData: { volume_allowance: volumeAllowance, end_time: endTime, ...rest }, @@ -26,7 +25,7 @@ const ExpirationDescription = ( { 'woocommerce-payments' ), formatCurrency( volumeAllowance, currencyCode ), - dateI18n( 'F j, Y', moment( endTime ).toISOString() ) + formatDateTimeFromString( endTime ) ); } else if ( volumeAllowance ) { description = sprintf( @@ -44,7 +43,7 @@ const ExpirationDescription = ( { 'Discounted base fee expires on %1$s.', 'woocommerce-payments' ), - dateI18n( 'F j, Y', moment( endTime ).toISOString() ) + formatDateTimeFromString( endTime ) ); } else { return null; diff --git a/client/components/account-status/account-fees/test/index.js b/client/components/account-status/account-fees/test/index.js index 5258af4ffdc..7405b33e371 100644 --- a/client/components/account-status/account-fees/test/index.js +++ b/client/components/account-status/account-fees/test/index.js @@ -46,6 +46,7 @@ describe( 'AccountFees', () => { precision: 2, }, }, + dateFormat: 'F j, Y', }; } ); diff --git a/client/components/active-loan-summary/index.tsx b/client/components/active-loan-summary/index.tsx index 0c5059ef87c..7ae902c590e 100755 --- a/client/components/active-loan-summary/index.tsx +++ b/client/components/active-loan-summary/index.tsx @@ -13,7 +13,6 @@ import { } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; -import { dateI18n } from '@wordpress/date'; /** * Internal dependencies. @@ -24,6 +23,7 @@ import { useActiveLoanSummary } from 'wcpay/data'; import { getAdminUrl } from 'wcpay/utils'; import './style.scss'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; const Block = ( { title, @@ -210,12 +210,8 @@ const ActiveLoanSummary = (): JSX.Element => { 'Repaid this period (until %s)', 'woocommerce-payments' ), - dateI18n( - 'M j, Y', - new Date( - details.current_repayment_interval.due_at * - 1000 - ) + formatDateTimeFromTimestamp( + details.current_repayment_interval.due_at ) ) } > @@ -251,9 +247,8 @@ const ActiveLoanSummary = (): JSX.Element => { - { dateI18n( - 'M j, Y', - new Date( details.advance_paid_out_at * 1000 ) + { formatDateTimeFromTimestamp( + details.advance_paid_out_at ) } { - { dateI18n( - 'M j, Y', - new Date( details.repayments_begin_at * 1000 ) + { formatDateTimeFromTimestamp( + details.repayments_begin_at ) } diff --git a/client/components/active-loan-summary/test/__snapshots__/index.js.snap b/client/components/active-loan-summary/test/__snapshots__/index.js.snap index 4424415245c..4e9dd15ec13 100644 --- a/client/components/active-loan-summary/test/__snapshots__/index.js.snap +++ b/client/components/active-loan-summary/test/__snapshots__/index.js.snap @@ -74,7 +74,7 @@ exports[`Active loan summary renders correctly 1`] = `
- Repaid this period (until Feb 14, 2022) + Repaid this period (until Feb 15, 2022)
( { useActiveLoanSummary: jest.fn(), } ) ); +// Mock dateI18n +jest.mock( '@wordpress/date', () => ( { + dateI18n: jest.fn( ( format, date ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'UTC' ); // Ensure UTC is used + } ), +} ) ); + describe( 'Active loan summary', () => { beforeEach( () => { global.wcpaySettings = { @@ -34,6 +43,7 @@ describe( 'Active loan summary', () => { precision: 2, }, }, + dateFormat: 'M j, Y', }; } ); afterEach( () => { diff --git a/client/components/date-format-notice/index.tsx b/client/components/date-format-notice/index.tsx new file mode 100644 index 00000000000..cdaba000939 --- /dev/null +++ b/client/components/date-format-notice/index.tsx @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import React, { useState } from 'react'; +import { __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import BannerNotice from 'components/banner-notice'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; +import './style.scss'; + +const optionName = 'wcpay_date_format_notice_dismissed'; + +const DateFormatNotice: React.FC = () => { + const { updateOptions } = useDispatch( 'wc/admin/options' ); + const [ isBannerVisible, setIsBannerVisible ] = useState( + ! wcpaySettings.isDateFormatNoticeDismissed + ); + + const handleDismiss = () => { + setIsBannerVisible( false ); + wcpaySettings.isDateFormatNoticeDismissed = true; + updateOptions( { + [ optionName ]: true, + } ); + }; + + const handleSettingsClick = () => { + handleDismiss(); + }; + + if ( ! isBannerVisible ) { + return null; + } + + return ( + + { interpolateComponents( { + components: { + settingsLink: ( + + ), + }, + mixedString: __( + 'The date and time formats now match your preferences. You can update them anytime in the {{settingsLink}}settings{{/settingsLink}}.', + 'woocommerce-payments' + ), + } ) } + + ); +}; + +export default DateFormatNotice; diff --git a/client/components/date-format-notice/style.scss b/client/components/date-format-notice/style.scss new file mode 100644 index 00000000000..86ee0987728 --- /dev/null +++ b/client/components/date-format-notice/style.scss @@ -0,0 +1,5 @@ +.date-format-notice { + .wcpay-banner-notice__content { + align-self: center; // Align the content to the center of the notice and the icon. + } +} diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index edaa068a100..920adf540c7 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -68,6 +68,7 @@ declare const global: { connect: { country: string; }; + dateFormat: string; }; }; @@ -240,6 +241,7 @@ describe( 'Deposits Overview information', () => { precision: 2, }, }, + dateFormat: 'F j, Y', }; mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: false, diff --git a/client/components/disputed-order-notice/index.js b/client/components/disputed-order-notice/index.js index ab51a52d16e..c7d1db9e7d1 100644 --- a/client/components/disputed-order-notice/index.js +++ b/client/components/disputed-order-notice/index.js @@ -1,7 +1,6 @@ import moment from 'moment'; import React, { useEffect } from 'react'; import { __, _n, sprintf } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; import { createInterpolateElement } from '@wordpress/element'; /** @@ -20,6 +19,7 @@ import { import { useCharge } from 'wcpay/data'; import { recordEvent } from 'tracks'; import './style.scss'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; const DisputedOrderNoticeHandler = ( { chargeId, onDisableOrderRefund } ) => { const { data: charge } = useCharge( chargeId ); @@ -84,8 +84,10 @@ const DisputedOrderNoticeHandler = ( { chargeId, onDisableOrderRefund } ) => { return null; } - const now = moment(); - const dueBy = moment.unix( dispute.evidence_details?.due_by ); + // Get current time in UTC for consistent timezone-independent comparison + const now = moment().utc(); + // Parse the Unix timestamp as UTC since it's stored that way in the API + const dueBy = moment.unix( dispute.evidence_details?.due_by ).utc(); // If the dispute is due in the past, don't show notice. if ( ! now.isBefore( dueBy ) ) { @@ -131,7 +133,7 @@ const UrgentDisputeNoticeBody = ( { formatString, formattedAmount, reasons[ disputeReason ].display, - dateI18n( 'M j, Y', dueBy.local().toISOString() ) + formatDateTimeFromString( dueBy.toISOString() ) ); let suffix = sprintf( @@ -182,7 +184,7 @@ const RegularDisputeNoticeBody = ( { const suffix = sprintf( // Translators: %1$s is the dispute due date. __( 'Please respond before %1$s.', 'woocommerce-payments' ), - dateI18n( 'M j, Y', dueBy.local().toISOString() ) + formatDateTimeFromString( dueBy.toISOString() ) ); return ( diff --git a/client/components/disputed-order-notice/test/index.test.js b/client/components/disputed-order-notice/test/index.test.js index 7e44da132e0..784092295f3 100644 --- a/client/components/disputed-order-notice/test/index.test.js +++ b/client/components/disputed-order-notice/test/index.test.js @@ -36,6 +36,7 @@ describe( 'DisputedOrderNoticeHandler', () => { connect: { country: 'US', }, + dateFormat: 'M j, Y', }; useCharge.mockReturnValue( { data: mockCharge } ); } ); diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 2b3f402abcb..42cb5f25f88 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -95,6 +95,16 @@ const ConnectAccountPage: React.FC = () => { const loaderProgressRef = useRef( testDriveLoaderProgress ); loaderProgressRef.current = testDriveLoaderProgress; + // Use a timer to track the elapsed time for the test drive mode setup. + let testDriveSetupStartTime: number; + // The test drive setup will be forced finished after 40 seconds + // (10 seconds for the initial calls plus 30 for checking the account status in a loop). + const testDriveSetupMaxDuration = 40; + + // Helper function to calculate the elapsed time in seconds. + const elapsed = ( time: number ) => + Math.round( ( Date.now() - time ) / 1000 ); + const { connectUrl, connect: { availableCountries, country }, @@ -173,19 +183,20 @@ const ConnectAccountPage: React.FC = () => { method: 'GET', } ).then( ( account ) => { // Simulate the update of the loader progress bar by 4% per check. - // Limit to a maximum of 15 checks or 30 seconds. - updateLoaderProgress( 100, 4 ); + // Limit to a maximum of 10 checks (6% progress per each request starting from 40% = max 10 checks). + updateLoaderProgress( 100, 6 ); - // If the account status is not a pending one or progress percentage is above 95, - // consider our work done and redirect the merchant. - // Otherwise, schedule another check after 2 seconds. + // If the account status is not a pending one, the progress percentage is above 95, + // or we've exceeded the timeout, consider our work done and redirect the merchant. + // Otherwise, schedule another check after a 2.5 seconds wait. if ( ( account && ( account as AccountData ).status && ! ( account as AccountData ).status.includes( 'pending' ) ) || - loaderProgressRef.current > 95 + loaderProgressRef.current > 95 || + elapsed( testDriveSetupStartTime ) > testDriveSetupMaxDuration ) { setTestDriveLoaderProgress( 100 ); const queryArgs = { @@ -203,12 +214,18 @@ const ConnectAccountPage: React.FC = () => { ...extraQueryArgs, } ); } else { - setTimeout( () => checkAccountStatus( extraQueryArgs ), 2000 ); + // Schedule another check after 2.5 seconds. + // 2.5 seconds plus 0.5 seconds for the fetch request is 3 seconds. + // With a maximum of 10 checks, we will wait for 30 seconds before ending the process normally. + setTimeout( () => checkAccountStatus( extraQueryArgs ), 2500 ); } } ); }; const handleSetupTestDriveMode = async () => { + // Record the start time of the test drive setup. + testDriveSetupStartTime = Date.now(); + // Initialize the progress bar. setTestDriveLoaderProgress( 5 ); setTestDriveModeSubmitted( true ); trackConnectAccountClicked( true ); @@ -256,6 +273,7 @@ const ConnectAccountPage: React.FC = () => { } clearInterval( updateProgress ); + // Update the progress bar to 40% since we've finished the initial account setup. setTestDriveLoaderProgress( 40 ); // Check the url for the `wcpay-connection-success` parameter, indicating a successful connection. diff --git a/client/data/authorizations/actions.ts b/client/data/authorizations/actions.ts index 0885da1cbfe..ace7f3c6fed 100644 --- a/client/data/authorizations/actions.ts +++ b/client/data/authorizations/actions.ts @@ -196,6 +196,13 @@ export function* submitCaptureAuthorization( 'getPaymentIntent' ); + // Need to invalidate transactions tab to update newly captured transaction if needed. + yield controls.dispatch( + STORE_NAME, + 'invalidateResolutionForStoreSelector', + 'getTransactions' + ); + // Create success notice. yield controls.dispatch( 'core/notices', diff --git a/client/data/authorizations/test/actions.test.ts b/client/data/authorizations/test/actions.test.ts index 171ef6dd5ad..36527d1836a 100644 --- a/client/data/authorizations/test/actions.test.ts +++ b/client/data/authorizations/test/actions.test.ts @@ -118,6 +118,14 @@ describe( 'Authorizations actions', () => { ) ); + expect( generator.next().value ).toEqual( + controls.dispatch( + 'wc/payments', + 'invalidateResolutionForStoreSelector', + 'getTransactions' + ) + ); + expect( generator.next().value ).toEqual( controls.dispatch( 'core/notices', diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 2176b079377..4e15a8653a8 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -4,9 +4,7 @@ * External dependencies */ import React from 'react'; -import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; -import moment from 'moment'; import { Card, CardBody, @@ -41,6 +39,8 @@ import { import { depositStatusLabels } from '../strings'; import './style.scss'; import { PayoutsRenameNotice } from '../rename-notice'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; +import DateFormatNotice from 'wcpay/components/date-format-notice'; /** * Renders the deposit status indicator UI, re-purposing the OrderStatus component from @woocommerce/components. @@ -135,11 +135,7 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( { key="depositDate" label={ `${ depositDateLabel }: ` + - dateI18n( - 'M j, Y', - moment.utc( deposit.date ).toISOString(), - true // TODO Change call to gmdateI18n and remove this deprecated param once WP 5.4 support ends. - ) + formatDateTimeFromString( deposit.date ) } value={ } detail={ deposit.bankAccount } @@ -248,6 +244,7 @@ export const DepositDetails: React.FC< DepositDetailsProps > = ( { return ( + { isLoading ? ( diff --git a/client/deposits/details/test/index.tsx b/client/deposits/details/test/index.tsx index 5f350daf4d0..fa9a4dbf042 100644 --- a/client/deposits/details/test/index.tsx +++ b/client/deposits/details/test/index.tsx @@ -45,6 +45,7 @@ declare const global: { connect: { country: string; }; + dateFormat: string; }; wcSettings: { countries: Record< string, string > }; }; @@ -67,6 +68,7 @@ describe( 'Deposit overview', () => { precision: 2, }, }, + dateFormat: 'M j, Y', }; } ); diff --git a/client/deposits/index.tsx b/client/deposits/index.tsx index d799ff3d385..229acdbbb09 100644 --- a/client/deposits/index.tsx +++ b/client/deposits/index.tsx @@ -23,6 +23,7 @@ import { useSettings } from 'wcpay/data'; import DepositsList from './list'; import { hasAutomaticScheduledDeposits } from 'wcpay/deposits/utils'; import { recordEvent } from 'wcpay/tracks'; +import DateFormatNotice from 'wcpay/components/date-format-notice'; const useNextDepositNoticeState = () => { const { updateOptions } = useDispatch( 'wc/admin/options' ); @@ -149,6 +150,7 @@ const DepositsPage: React.FC = () => { return ( + diff --git a/client/deposits/list/index.tsx b/client/deposits/list/index.tsx index dc1e555a98c..089e0a59c25 100644 --- a/client/deposits/list/index.tsx +++ b/client/deposits/list/index.tsx @@ -6,9 +6,7 @@ import React, { useState } from 'react'; import { recordEvent } from 'tracks'; import { useMemo } from '@wordpress/element'; -import { dateI18n } from '@wordpress/date'; import { __, _n, sprintf } from '@wordpress/i18n'; -import moment from 'moment'; import { TableCard, Link } from '@woocommerce/components'; import { onQueryChange, getQuery } from '@woocommerce/navigation'; import { @@ -40,6 +38,7 @@ import { applyThousandSeparator } from '../../utils/index.js'; import DepositStatusChip from 'components/deposit-status-chip'; import './style.scss'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; const getColumns = ( sortByDate?: boolean ): DepositsTableHeader[] => [ { @@ -126,11 +125,7 @@ export const DepositsList = (): JSX.Element => { href={ getDetailsURL( deposit.id, 'payouts' ) } onClick={ () => recordEvent( 'wcpay_deposits_row_click' ) } > - { dateI18n( - 'M j, Y', - moment.utc( deposit.date ).toISOString(), - true // TODO Change call to gmdateI18n and remove this deprecated param once WP 5.4 support ends. - ) } + { formatDateTimeFromString( deposit.date ) } ); @@ -317,11 +312,7 @@ export const DepositsList = (): JSX.Element => { row[ 0 ], { ...row[ 1 ], - value: dateI18n( - 'Y-m-d', - moment.utc( row[ 1 ].value ).toISOString(), - true - ), + value: formatDateTimeFromString( row[ 1 ].value as string ), }, ...row.slice( 2 ), ] ); diff --git a/client/deposits/list/test/__snapshots__/index.tsx.snap b/client/deposits/list/test/__snapshots__/index.tsx.snap index 07945cd0c64..c26a364fd04 100644 --- a/client/deposits/list/test/__snapshots__/index.tsx.snap +++ b/client/deposits/list/test/__snapshots__/index.tsx.snap @@ -361,7 +361,7 @@ exports[`Deposits list renders correctly a single deposit 1`] = ` data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fpayouts%2Fdetails&id=po_mock1" > - Jan 2, 2020 + Jan 2 2020 - Jan 3, 2020 + Jan 3 2020 - Jan 4, 2020 + Jan 4 2020 - Jan 2, 2020 + Jan 2 2020 - Jan 3, 2020 + Jan 3 2020 - Jan 4, 2020 + Jan 4 2020 { locale: { code: 'en', }, + dateFormat: 'M j Y', }; } ); @@ -310,7 +312,7 @@ describe( 'Deposits list', () => { // 2. The indexOf check in amount's expect is because the amount in CSV may not contain // trailing zeros as in the display amount. // - expect( formatDate( csvFirstDeposit[ 1 ], 'M j, Y' ) ).toBe( + expect( csvFirstDeposit[ 1 ].replace( /^"|"$/g, '' ) ).toBe( displayFirstDeposit[ 0 ] ); // date expect( csvFirstDeposit[ 2 ] ).toBe( displayFirstDeposit[ 1 ] ); // type diff --git a/client/deposits/utils/index.ts b/client/deposits/utils/index.ts index 3d8fd6276e1..05f65c46bc3 100644 --- a/client/deposits/utils/index.ts +++ b/client/deposits/utils/index.ts @@ -2,21 +2,15 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; import moment from 'moment'; - -const formatDate = ( format: string, date: number | string ) => - dateI18n( - format, - moment.utc( date ).toISOString(), - true // TODO Change call to gmdateI18n and remove this deprecated param once WP 5.4 support ends. - ); +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface DepositObject { date: number | string; } + export const getDepositDate = ( deposit?: DepositObject | null ): string => - deposit ? formatDate( 'F j, Y', deposit?.date ) : '—'; + deposit ? formatDateTimeFromString( deposit?.date as string ) : '—'; interface GetDepositMonthlyAnchorLabelProps { monthlyAnchor: number; diff --git a/client/deposits/utils/test/index.ts b/client/deposits/utils/test/index.ts index d0361137104..e1957ce4564 100644 --- a/client/deposits/utils/test/index.ts +++ b/client/deposits/utils/test/index.ts @@ -8,7 +8,29 @@ import momentLib from 'moment'; */ import { getDepositDate, getDepositMonthlyAnchorLabel } from '../'; +declare const global: { + wcpaySettings: { + dateFormat: string; + }; +}; + +// Mock dateI18n +jest.mock( '@wordpress/date', () => ( { + dateI18n: jest.fn( ( format, date ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'UTC' ); // Ensure UTC is used + } ), +} ) ); + describe( 'Deposits Overview Utils / getDepositDate', () => { + beforeEach( () => { + jest.clearAllMocks(); + global.wcpaySettings = { + dateFormat: 'F j, Y', + }; + } ); + test( 'returns a display value without a deposit', () => { expect( getDepositDate() ).toEqual( '—' ); } ); diff --git a/client/disputes/evidence/test/index.js b/client/disputes/evidence/test/index.js index e9ccdb826cb..142ee738ab8 100644 --- a/client/disputes/evidence/test/index.js +++ b/client/disputes/evidence/test/index.js @@ -96,6 +96,7 @@ describe( 'Dispute evidence form', () => { global.wcpaySettings = { restUrl: 'http://example.com/wp-json/', + dateFormat: 'M j, Y', }; } ); afterEach( () => { @@ -190,6 +191,8 @@ describe( 'Dispute evidence page', () => { precision: 2, }, }, + dateFormat: 'M j, Y', + timeFormat: 'g:iA', }; } ); diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index cdb82bacaaf..ba9d4f21b04 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -5,7 +5,6 @@ */ import React, { useState } from 'react'; import { recordEvent } from 'tracks'; -import { dateI18n } from '@wordpress/date'; import { _n, __, sprintf } from '@wordpress/i18n'; import moment from 'moment'; import { Button } from '@wordpress/components'; @@ -45,8 +44,9 @@ import { getDisputesCSV } from 'wcpay/data/disputes/resolvers'; import { applyThousandSeparator } from 'wcpay/utils'; import { useSettings } from 'wcpay/data'; import { isAwaitingResponse } from 'wcpay/disputes/utils'; - +import DateFormatNotice from 'wcpay/components/date-format-notice'; import './style.scss'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; const getHeaders = ( sortColumn?: string ): DisputesTableHeader[] => [ { @@ -190,10 +190,9 @@ const smartDueDate = ( dispute: CachedDispute ) => { ); } - return dateI18n( - 'M j, Y / g:iA', - moment.utc( dispute.due_by ).local().toISOString() - ); + return formatDateTimeFromString( dispute.due_by, { + includeTime: true, + } ); }; export const DisputesList = (): JSX.Element => { @@ -284,10 +283,9 @@ export const DisputesList = (): JSX.Element => { created: { value: dispute.created, display: clickable( - dateI18n( - 'M j, Y', - moment( dispute.created ).toISOString() - ) + formatDateTimeFromString( dispute.created, { + includeTime: true, + } ) ), }, dueBy: { @@ -464,17 +462,18 @@ export const DisputesList = (): JSX.Element => { { // Disputed On. ...row[ 10 ], - value: dateI18n( - 'Y-m-d', - moment( row[ 10 ].value ).toISOString() + value: formatDateTimeFromString( + row[ 10 ].value as string ), }, { // Respond by. ...row[ 11 ], - value: dateI18n( - 'Y-m-d / g:iA', - moment( row[ 11 ].value ).toISOString() + value: formatDateTimeFromString( + row[ 11 ].value as string, + { + includeTime: true, + } ), }, ]; @@ -522,6 +521,7 @@ export const DisputesList = (): JSX.Element => { return ( + { precision: 2, }, }, + dateFormat: 'M j, Y', + timeFormat: 'g:iA', }; } ); diff --git a/client/disputes/test/__snapshots__/index.tsx.snap b/client/disputes/test/__snapshots__/index.tsx.snap index 06459d236e4..b3fae6b5d47 100644 --- a/client/disputes/test/__snapshots__/index.tsx.snap +++ b/client/disputes/test/__snapshots__/index.tsx.snap @@ -5,6 +5,54 @@ exports[`Disputes list renders correctly 1`] = `
+
+ +
+ The date and time formats now match your preferences. You can update them anytime in the + + settings + + . +
+ +
diff --git a/client/disputes/test/index.tsx b/client/disputes/test/index.tsx index 910977164a8..70fdcd326cc 100644 --- a/client/disputes/test/index.tsx +++ b/client/disputes/test/index.tsx @@ -12,13 +12,14 @@ import os from 'os'; */ import DisputesList from '..'; import { useDisputes, useDisputesSummary, useSettings } from 'data/index'; -import { formatDate, getUnformattedAmount } from 'wcpay/utils/test-utils'; +import { getUnformattedAmount } from 'wcpay/utils/test-utils'; import React from 'react'; import { CachedDispute, DisputeReason, DisputeStatus, } from 'wcpay/types/disputes'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; jest.mock( '@woocommerce/csv-export', () => { const actualModule = jest.requireActual( '@woocommerce/csv-export' ); @@ -90,6 +91,8 @@ declare const global: { locale: { code: string; }; + dateFormat?: string; + timeFormat?: string; }; }; @@ -186,6 +189,8 @@ describe( 'Disputes list', () => { locale: { code: 'en', }, + dateFormat: 'Y-m-d', + timeFormat: 'g:iA', }; } ); @@ -351,8 +356,10 @@ describe( 'Disputes list', () => { `"${ displayFirstDispute[ 5 ] }"` ); // customer - expect( formatDate( csvFirstDispute[ 11 ], 'Y-m-d / g:iA' ) ).toBe( - formatDate( displayFirstDispute[ 6 ], 'Y-m-d / g:iA' ) + expect( csvFirstDispute[ 11 ].replace( /^"|"$/g, '' ) ).toBe( + formatDateTimeFromString( mockDisputes[ 0 ].due_by, { + includeTime: true, + } ) ); // date respond by } ); } ); diff --git a/client/documents/index.tsx b/client/documents/index.tsx index 07f75e99ddf..c95c9d3a6ba 100644 --- a/client/documents/index.tsx +++ b/client/documents/index.tsx @@ -9,10 +9,11 @@ import React from 'react'; import Page from 'components/page'; import DocumentsList from './list'; import { TestModeNotice } from 'components/test-mode-notice'; - +import DateFormatNotice from 'wcpay/components/date-format-notice'; export const DocumentsPage = (): JSX.Element => { return ( + diff --git a/client/documents/list/index.tsx b/client/documents/list/index.tsx index b69e2df54af..3223d45dcec 100644 --- a/client/documents/list/index.tsx +++ b/client/documents/list/index.tsx @@ -4,9 +4,7 @@ * External dependencies */ import React, { useCallback, useEffect, useState } from 'react'; -import { dateI18n } from '@wordpress/date'; import { __, _n, sprintf } from '@wordpress/i18n'; -import moment from 'moment'; import { TableCard, TableCardColumn } from '@woocommerce/components'; import { onQueryChange, getQuery } from '@woocommerce/navigation'; import { Button } from '@wordpress/components'; @@ -21,6 +19,7 @@ import DocumentsFilters from '../filters'; import Page from '../../components/page'; import { getDocumentUrl } from 'wcpay/utils'; import VatFormModal from 'wcpay/vat/form-modal'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface Column extends TableCardColumn { key: 'date' | 'type' | 'description' | 'download'; @@ -68,16 +67,8 @@ const getDocumentDescription = ( document: Document ) => { if ( document.period_from && document.period_to ) { return sprintf( __( 'VAT invoice for %s to %s', 'woocommerce-payments' ), - dateI18n( - 'M j, Y', - moment.utc( document.period_from ).toISOString(), - 'utc' - ), - dateI18n( - 'M j, Y', - moment.utc( document.period_to ).toISOString(), - 'utc' - ) + formatDateTimeFromString( document.period_from ), + formatDateTimeFromString( document.period_to ) ); } return __( @@ -180,10 +171,7 @@ export const DocumentsList = (): JSX.Element => { const data = { date: { value: document.date, - display: dateI18n( - 'M j, Y', - moment.utc( document.date ).local().toISOString() - ), + display: formatDateTimeFromString( document.date ), }, type: { value: documentType, diff --git a/client/documents/list/test/index.tsx b/client/documents/list/test/index.tsx index c54cf7a02c8..0eac7e0bf61 100644 --- a/client/documents/list/test/index.tsx +++ b/client/documents/list/test/index.tsx @@ -36,6 +36,7 @@ declare const global: { accountStatus: { hasSubmittedVatData: boolean; }; + dateFormat: string; }; }; @@ -60,6 +61,11 @@ describe( 'Documents list', () => { let container: Element; let rerender: ( ui: React.ReactElement ) => void; beforeEach( () => { + global.wcpaySettings = { + accountStatus: { hasSubmittedVatData: true }, + dateFormat: 'M j, Y', + }; + mockUseDocuments.mockReturnValue( { documents: getMockDocuments(), isLoading: false, @@ -200,6 +206,7 @@ describe( 'Document download button', () => { beforeEach( () => { global.wcpaySettings = { accountStatus: { hasSubmittedVatData: true }, + dateFormat: 'M j, Y', }; render( ); @@ -223,6 +230,7 @@ describe( 'Document download button', () => { beforeEach( () => { global.wcpaySettings = { accountStatus: { hasSubmittedVatData: false }, + dateFormat: 'M j, Y', }; render( ); @@ -293,6 +301,7 @@ describe( 'Direct document download', () => { global.wcpaySettings = { accountStatus: { hasSubmittedVatData: true }, + dateFormat: 'M j, Y', }; } ); diff --git a/client/globals.d.ts b/client/globals.d.ts index 0c40590fcb5..d7b22d130eb 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -123,6 +123,7 @@ declare global { storeName: string; isNextDepositNoticeDismissed: boolean; isInstantDepositNoticeDismissed: boolean; + isDateFormatNoticeDismissed: boolean; locale: { code: string; english_name: string; @@ -134,6 +135,8 @@ declare global { isOverviewSurveySubmitted: boolean; lifetimeTPV: number; defaultExpressCheckoutBorderRadius: string; + dateFormat: string; + timeFormat: string; }; const wc: { diff --git a/client/overview/index.js b/client/overview/index.js index edb215993c7..5d6d06a52c2 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -33,6 +33,7 @@ import SandboxModeSwitchToLiveNotice from 'wcpay/components/sandbox-mode-switch- import './style.scss'; import BannerNotice from 'wcpay/components/banner-notice'; import { PayoutsRenameNotice } from 'wcpay/deposits/rename-notice'; +import DateFormatNotice from 'wcpay/components/date-format-notice'; const OverviewPageError = () => { const queryParams = getQuery(); @@ -152,6 +153,7 @@ const OverviewPage = () => { + { showLoanOfferError && ( { __( diff --git a/client/overview/modal/update-business-details/index.tsx b/client/overview/modal/update-business-details/index.tsx index 2c654a57f9b..29a649ad561 100644 --- a/client/overview/modal/update-business-details/index.tsx +++ b/client/overview/modal/update-business-details/index.tsx @@ -3,9 +3,7 @@ */ import React, { useState } from 'react'; import { Button, Modal, Notice } from '@wordpress/components'; -import { dateI18n } from '@wordpress/date'; import { sprintf } from '@wordpress/i18n'; -import moment from 'moment'; /** * Internal dependencies @@ -13,6 +11,7 @@ import moment from 'moment'; import strings from './strings'; import './index.scss'; import { recordEvent } from 'wcpay/tracks'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; interface Props { errorMessages: Array< string >; @@ -57,11 +56,11 @@ const UpdateBusinessDetailsModal = ( { currentDeadline ? sprintf( strings.restrictedSoonDescription, - dateI18n( - 'ga M j, Y', - moment( - currentDeadline * 1000 - ).toISOString() + formatDateTimeFromTimestamp( + currentDeadline, + { + customFormat: 'ga M j, Y', + } ) ) : strings.restrictedDescription } diff --git a/client/overview/task-list/tasks/dispute-task.tsx b/client/overview/task-list/tasks/dispute-task.tsx index 235b92696b9..333c15d6709 100644 --- a/client/overview/task-list/tasks/dispute-task.tsx +++ b/client/overview/task-list/tasks/dispute-task.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; import moment from 'moment'; import { getHistory } from '@woocommerce/navigation'; @@ -15,6 +14,7 @@ import { formatCurrency } from 'multi-currency/interface/functions'; import { getAdminUrl } from 'wcpay/utils'; import { recordEvent } from 'tracks'; import { isDueWithin } from 'wcpay/disputes/utils'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; /** * Returns an array of disputes that are due within the specified number of days. @@ -142,10 +142,9 @@ export const getDisputeResolutionTask = ( ? sprintf( __( 'Respond today by %s', 'woocommerce-payments' ), // Show due_by time in local timezone: e.g. "11:59 PM". - dateI18n( - 'g:i A', - moment.utc( dispute.due_by ).local().toISOString() - ) + formatDateTimeFromString( dispute.due_by, { + customFormat: 'g:i A', + } ) ) : sprintf( __( @@ -153,11 +152,8 @@ export const getDisputeResolutionTask = ( 'woocommerce-payments' ), // Show due_by date in local timezone: e.g. "Jan 1, 2021". - dateI18n( - 'M j, Y', - moment.utc( dispute.due_by ).local().toISOString() - ), - moment( dispute.due_by ).fromNow( true ) // E.g. "2 days". + formatDateTimeFromString( dispute.due_by ), + moment.utc( dispute.due_by ).fromNow( true ) // E.g. "2 days". ); return disputeTask; diff --git a/client/overview/task-list/tasks/update-business-details-task.tsx b/client/overview/task-list/tasks/update-business-details-task.tsx index a18e06e9c09..61255a0e413 100644 --- a/client/overview/task-list/tasks/update-business-details-task.tsx +++ b/client/overview/task-list/tasks/update-business-details-task.tsx @@ -11,9 +11,8 @@ import { addQueryArgs } from '@wordpress/url'; */ import type { TaskItemProps } from '../types'; import UpdateBusinessDetailsModal from 'wcpay/overview/modal/update-business-details'; -import { dateI18n } from '@wordpress/date'; -import moment from 'moment'; import { recordEvent } from 'wcpay/tracks'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; export const getUpdateBusinessDetailsTask = ( errorMessages: string[], @@ -46,10 +45,9 @@ export const getUpdateBusinessDetailsTask = ( 'Update by %s to avoid a disruption in payouts.', 'woocommerce-payments' ), - dateI18n( - 'ga M j, Y', - moment( currentDeadline * 1000 ).toISOString() - ) + formatDateTimeFromTimestamp( currentDeadline, { + customFormat: 'ga M j, Y', + } ) ); if ( hasSingleError ) { diff --git a/client/overview/task-list/test/tasks.js b/client/overview/task-list/test/tasks.js index 733d2208b08..9134a1c6842 100644 --- a/client/overview/task-list/test/tasks.js +++ b/client/overview/task-list/test/tasks.js @@ -139,6 +139,7 @@ describe( 'getTasks()', () => { precision: 2, }, }, + dateFormat: 'M j, Y', }; } ); afterEach( () => { diff --git a/client/payment-details/dispute-details/dispute-due-by-date.tsx b/client/payment-details/dispute-details/dispute-due-by-date.tsx index 18993ef2387..91255a4d786 100644 --- a/client/payment-details/dispute-details/dispute-due-by-date.tsx +++ b/client/payment-details/dispute-details/dispute-due-by-date.tsx @@ -2,22 +2,22 @@ * External dependencies */ import React from 'react'; -import { dateI18n } from '@wordpress/date'; import { __, _n, sprintf } from '@wordpress/i18n'; import classNames from 'classnames'; import moment from 'moment'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; const DisputeDueByDate: React.FC< { dueBy: number; showRemainingDays?: boolean; } > = ( { dueBy, showRemainingDays = true } ) => { const daysRemaining = Math.floor( - moment.unix( dueBy ).diff( moment(), 'days', true ) - ); - const respondByDate = dateI18n( - 'M j, Y, g:ia', - moment( dueBy * 1000 ).toISOString() + moment.unix( dueBy ).utc().diff( moment().utc(), 'days', true ) ); + const respondByDate = formatDateTimeFromTimestamp( dueBy, { + separator: ', ', + includeTime: true, + } ); return ( { respondByDate } diff --git a/client/payment-details/dispute-details/dispute-resolution-footer.tsx b/client/payment-details/dispute-details/dispute-resolution-footer.tsx index 15fec759244..bc4b1e94dbd 100644 --- a/client/payment-details/dispute-details/dispute-resolution-footer.tsx +++ b/client/payment-details/dispute-details/dispute-resolution-footer.tsx @@ -2,8 +2,6 @@ * External dependencies */ import React from 'react'; -import moment from 'moment'; -import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; import { Link } from '@woocommerce/components'; import { createInterpolateElement } from '@wordpress/element'; @@ -17,18 +15,15 @@ import { recordEvent } from 'tracks'; import { getAdminUrl } from 'wcpay/utils'; import { getDisputeFeeFormatted } from 'wcpay/disputes/utils'; import './style.scss'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; const DisputeUnderReviewFooter: React.FC< { dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >; } > = ( { dispute } ) => { const submissionDateFormatted = dispute.metadata.__evidence_submitted_at - ? dateI18n( - 'M j, Y', - moment - .unix( - parseInt( dispute.metadata.__evidence_submitted_at, 10 ) - ) - .toISOString() + ? formatDateTimeFromTimestamp( + parseInt( dispute.metadata.__evidence_submitted_at, 10 ), + { includeTime: true } ) : '-'; @@ -93,13 +88,9 @@ const DisputeWonFooter: React.FC< { dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >; } > = ( { dispute } ) => { const closedDateFormatted = dispute.metadata.__dispute_closed_at - ? dateI18n( - 'M j, Y', - moment - .unix( - parseInt( dispute.metadata.__dispute_closed_at, 10 ) - ) - .toISOString() + ? formatDateTimeFromTimestamp( + parseInt( dispute.metadata.__dispute_closed_at, 10 ), + { includeTime: true } ) : '-'; @@ -171,13 +162,8 @@ const DisputeLostFooter: React.FC< { const disputeFeeFormatted = getDisputeFeeFormatted( dispute, true ) ?? '-'; const closedDateFormatted = dispute.metadata.__dispute_closed_at - ? dateI18n( - 'M j, Y', - moment - .unix( - parseInt( dispute.metadata.__dispute_closed_at, 10 ) - ) - .toISOString() + ? formatDateTimeFromTimestamp( + parseInt( dispute.metadata.__dispute_closed_at, 10 ) ) : '-'; @@ -274,13 +260,8 @@ const InquiryUnderReviewFooter: React.FC< { dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >; } > = ( { dispute } ) => { const submissionDateFormatted = dispute.metadata.__evidence_submitted_at - ? dateI18n( - 'M j, Y', - moment - .unix( - parseInt( dispute.metadata.__evidence_submitted_at, 10 ) - ) - .toISOString() + ? formatDateTimeFromTimestamp( + parseInt( dispute.metadata.__evidence_submitted_at, 10 ) ) : '-'; @@ -346,13 +327,8 @@ const InquiryClosedFooter: React.FC< { } > = ( { dispute } ) => { const isSubmitted = !! dispute.metadata.__evidence_submitted_at; const closedDateFormatted = dispute.metadata.__dispute_closed_at - ? dateI18n( - 'M j, Y', - moment - .unix( - parseInt( dispute.metadata.__dispute_closed_at, 10 ) - ) - .toISOString() + ? formatDateTimeFromTimestamp( + parseInt( dispute.metadata.__dispute_closed_at, 10 ) ) : '-'; diff --git a/client/payment-details/dispute-details/dispute-steps.tsx b/client/payment-details/dispute-details/dispute-steps.tsx index 0f90723a8f1..28707dfc6c4 100644 --- a/client/payment-details/dispute-details/dispute-steps.tsx +++ b/client/payment-details/dispute-details/dispute-steps.tsx @@ -7,8 +7,6 @@ import React from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; import { ExternalLink } from '@wordpress/components'; -import { dateI18n } from '@wordpress/date'; -import moment from 'moment'; import HelpOutlineIcon from 'gridicons/dist/help-outline'; /** @@ -20,6 +18,7 @@ import { formatExplicitCurrency } from 'multi-currency/interface/functions'; import { ClickTooltip } from 'wcpay/components/tooltip'; import { getDisputeFeeFormatted } from 'wcpay/disputes/utils'; import DisputeDueByDate from './dispute-due-by-date'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; interface Props { dispute: Dispute; @@ -35,14 +34,8 @@ export const DisputeSteps: React.FC< Props > = ( { } ) => { let emailLink; if ( customer?.email ) { - const chargeDate = dateI18n( - 'Y-m-d', - moment( chargeCreated * 1000 ).toISOString() - ); - const disputeDate = dateI18n( - 'Y-m-d', - moment( dispute.created * 1000 ).toISOString() - ); + const chargeDate = formatDateTimeFromTimestamp( chargeCreated ); + const disputeDate = formatDateTimeFromTimestamp( dispute.created ); const emailSubject = sprintf( // Translators: %1$s is the store name, %2$s is the charge date. __( @@ -175,14 +168,12 @@ export const InquirySteps: React.FC< Props > = ( { } ) => { let emailLink; if ( customer?.email ) { - const chargeDate = dateI18n( - 'Y-m-d', - moment( chargeCreated * 1000 ).toISOString() - ); - const disputeDate = dateI18n( - 'Y-m-d', - moment( dispute.created * 1000 ).toISOString() - ); + const chargeDate = formatDateTimeFromTimestamp( chargeCreated, { + includeTime: true, + } ); + const disputeDate = formatDateTimeFromTimestamp( dispute.created, { + includeTime: true, + } ); const emailSubject = sprintf( // Translators: %1$s is the store name, %2$s is the charge date. __( diff --git a/client/payment-details/dispute-details/dispute-summary-row.tsx b/client/payment-details/dispute-details/dispute-summary-row.tsx index 0a43cb223e0..95119d01f82 100644 --- a/client/payment-details/dispute-details/dispute-summary-row.tsx +++ b/client/payment-details/dispute-details/dispute-summary-row.tsx @@ -4,10 +4,8 @@ * External dependencies */ import React from 'react'; -import moment from 'moment'; import HelpOutlineIcon from 'gridicons/dist/help-outline'; import { __ } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; /** * Internal dependencies @@ -20,6 +18,7 @@ import { formatStringValue } from 'wcpay/utils'; import { ClickTooltip } from 'wcpay/components/tooltip'; import Paragraphs from 'wcpay/components/paragraphs'; import DisputeDueByDate from './dispute-due-by-date'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; interface Props { dispute: Dispute; @@ -39,10 +38,10 @@ const DisputeSummaryRow: React.FC< Props > = ( { dispute } ) => { { title: __( 'Disputed On', 'woocommerce-payments' ), content: dispute.created - ? dateI18n( - 'M j, Y, g:ia', - moment( dispute.created * 1000 ).toISOString() - ) + ? formatDateTimeFromTimestamp( dispute.created, { + separator: ', ', + includeTime: true, + } ) : '–', }, { diff --git a/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap index 7b4a7e3650f..a1af47f03c4 100644 --- a/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap @@ -6,6 +6,54 @@ exports[`Order details page should match the snapshot - Charge without payment i class="wcpay-payment-details woocommerce-payments-page" style="max-width: 1032px;" > +
+ +
+ The date and time formats now match your preferences. You can update them anytime in the + + settings + + . +
+ +
{ featureFlags: { paymentTimeline: true }, zeroDecimalCurrencies: [], connect: { country: 'US' }, + timeFormat: 'g:ia', + dateFormat: 'M j, Y', }; const selectMock = jest.fn( ( storeName ) => diff --git a/client/payment-details/payment-details/index.tsx b/client/payment-details/payment-details/index.tsx index cdf568520f2..a32d0c94193 100644 --- a/client/payment-details/payment-details/index.tsx +++ b/client/payment-details/payment-details/index.tsx @@ -17,7 +17,7 @@ import PaymentDetailsPaymentMethod from '../payment-method'; import { ApiError } from '../../types/errors'; import { Charge } from '../../types/charges'; import { PaymentIntent } from '../../types/payment-intents'; - +import DateFormatNotice from 'wcpay/components/date-format-notice'; interface PaymentDetailsProps { id: string; isLoading: boolean; @@ -56,6 +56,7 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { return ( + { const { readers, chargeError, isLoading } = useCardReaderStats( props.chargeId, @@ -34,6 +34,7 @@ const PaymentCardReaderChargeDetails = ( props ) => { if ( ! isLoading && chargeError instanceof Error ) { return ( + diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index 1e5b6bfcac9..59ae556c785 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -4,7 +4,6 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; import { Card, CardBody, @@ -64,6 +63,10 @@ import DisputeResolutionFooter from '../dispute-details/dispute-resolution-foote import ErrorBoundary from 'components/error-boundary'; import RefundModal from 'wcpay/payment-details/summary/refund-modal'; import CardNotice from 'wcpay/components/card-notice'; +import { + formatDateTimeFromString, + formatDateTimeFromTimestamp, +} from 'wcpay/utils/date-time'; declare const window: any; @@ -110,10 +113,10 @@ const composePaymentSummaryItems = ( { { title: __( 'Date', 'woocommerce-payments' ), content: charge.created - ? dateI18n( - 'M j, Y, g:ia', - moment( charge.created * 1000 ).toISOString() - ) + ? formatDateTimeFromTimestamp( charge.created, { + separator: ', ', + includeTime: true, + } ) : '–', }, { @@ -714,12 +717,13 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { } ) }{ ' ' } diff --git a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap index 083da902f05..6dacc01df87 100644 --- a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap @@ -304,7 +304,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca this charge within the next 7 days @@ -660,7 +660,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th this charge within the next 7 days diff --git a/client/payment-details/summary/test/index.test.tsx b/client/payment-details/summary/test/index.test.tsx index 9055d481dda..8c47e9b9b6a 100755 --- a/client/payment-details/summary/test/index.test.tsx +++ b/client/payment-details/summary/test/index.test.tsx @@ -17,6 +17,15 @@ import PaymentDetailsSummary from '../'; import { useAuthorization } from 'wcpay/data'; import { paymentIntentMock } from 'wcpay/data/payment-intents/test/hooks'; +// Mock dateI18n +jest.mock( '@wordpress/date', () => ( { + dateI18n: jest.fn( ( format, date ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'UTC' ); // Ensure UTC is used + } ), +} ) ); + declare const global: { wcSettings: { locale: { @@ -34,6 +43,8 @@ declare const global: { featureFlags: { isAuthAndCaptureEnabled: boolean; }; + dateFormat: string; + timeFormat: string; }; }; @@ -74,7 +85,7 @@ const getBaseCharge = (): Charge => id: 'ch_38jdHA39KKA', payment_intent: 'pi_abc', /* Stripe data comes in seconds, instead of the default Date milliseconds */ - created: Date.parse( 'Sep 19, 2019, 5:24 pm' ) / 1000, + created: 1568913840, amount: 2000, amount_refunded: 0, application_fee_amount: 70, @@ -203,6 +214,8 @@ describe( 'PaymentDetailsSummary', () => { precision: 0, }, }, + dateFormat: 'M j, Y', + timeFormat: 'g:ia', }; // mock Date.now that moment library uses to get current date for testing purposes @@ -408,7 +421,7 @@ describe( 'PaymentDetailsSummary', () => { ).toHaveTextContent( /\$20.00/ ); expect( screen.getByText( /Disputed On/i ).nextSibling - ).toHaveTextContent( /Aug 30, 2023/ ); + ).toHaveTextContent( /Aug 31, 2023/ ); expect( screen.getByText( /Reason/i ).nextSibling ).toHaveTextContent( /Transaction unauthorized/ ); diff --git a/client/payment-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/test/__snapshots__/index.test.tsx.snap index aa8a34effd0..19af9ff7235 100644 --- a/client/payment-details/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/test/__snapshots__/index.test.tsx.snap @@ -6,6 +6,54 @@ exports[`Payment details page should match the snapshot - Charge query param 1`] class="wcpay-payment-details woocommerce-payments-page" style="max-width: 1032px;" > +
+ +
+ The date and time formats now match your preferences. You can update them anytime in the + + settings + + . +
+ +
+
+ +
+ The date and time formats now match your preferences. You can update them anytime in the + + settings + + . +
+ +
{ diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index b76f659ac63..6e4f593e1f0 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -5,9 +5,7 @@ */ import { flatMap } from 'lodash'; import { __, sprintf } from '@wordpress/i18n'; -import { dateI18n } from '@wordpress/date'; import { addQueryArgs } from '@wordpress/url'; -import moment from 'moment'; import { createInterpolateElement } from '@wordpress/element'; import { Link } from '@woocommerce/components'; import SyncIcon from 'gridicons/dist/sync'; @@ -31,6 +29,7 @@ import { formatFee } from 'utils/fees'; import { getAdminUrl } from 'wcpay/utils'; import { ShieldIcon } from 'wcpay/icons'; import { fraudOutcomeRulesetMapping, paymentFailureMapping } from './mappings'; +import { formatDateTimeFromTimestamp } from 'wcpay/utils/date-time'; /** * Creates a timeline item about a payment status change @@ -84,10 +83,7 @@ const getDepositTimelineItem = ( 'woocommerce-payments' ), formattedAmount, - dateI18n( - 'M j, Y', - moment( event.deposit.arrival_date * 1000 ).toISOString() - ) + formatDateTimeFromTimestamp( event.deposit.arrival_date ) ); const depositUrl = getAdminUrl( { page: 'wc-admin', @@ -143,10 +139,7 @@ const getFinancingPaydownTimelineItem = ( event, formattedAmount, body ) => { 'woocommerce-payments' ), formattedAmount, - dateI18n( - 'M j, Y', - moment( event.deposit.arrival_date * 1000 ).toISOString() - ) + formatDateTimeFromTimestamp( event.deposit.arrival_date ) ); const depositUrl = getAdminUrl( { @@ -410,12 +403,12 @@ export const feeBreakdown = ( event ) => { fixedRate !== 0 ? __( /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ - 'Foreign exchange fee: %1$s%% + %2$s', + 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' ) : __( /* translators: %1$s% is the fee percentage */ - 'Foreign exchange fee: %1$s%%', + 'Currency conversion fee: %1$s%%', 'woocommerce-payments' ), 'additional-wcpay-subscription': diff --git a/client/payment-details/timeline/test/__snapshots__/map-events.js.snap b/client/payment-details/timeline/test/__snapshots__/map-events.js.snap index 4126cb1ff38..b229ba674be 100644 --- a/client/payment-details/timeline/test/__snapshots__/map-events.js.snap +++ b/client/payment-details/timeline/test/__snapshots__/map-events.js.snap @@ -397,7 +397,7 @@ exports[`mapTimelineEvents single currency events formats captured events with f International card fee: 1.5%
  • - Foreign exchange fee: 2% + Currency conversion fee: 2%
  • Discount diff --git a/client/payment-details/timeline/test/index.js b/client/payment-details/timeline/test/index.js index 2529f3d673e..616c780dd69 100644 --- a/client/payment-details/timeline/test/index.js +++ b/client/payment-details/timeline/test/index.js @@ -34,6 +34,7 @@ describe( 'PaymentDetailsTimeline', () => { precision: 2, }, }, + dateFormat: 'M j, Y', }; } ); diff --git a/client/payment-details/timeline/test/map-events.js b/client/payment-details/timeline/test/map-events.js index f1c0588d659..beee5dc959b 100644 --- a/client/payment-details/timeline/test/map-events.js +++ b/client/payment-details/timeline/test/map-events.js @@ -47,6 +47,7 @@ describe( 'mapTimelineEvents', () => { precision: 2, }, }, + dateFormat: 'M j, Y', }; } ); diff --git a/client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js b/client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js new file mode 100644 index 00000000000..91009ab1a25 --- /dev/null +++ b/client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js @@ -0,0 +1,253 @@ +/** + * External dependencies + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import '../wc-product-bundles'; + +describe( 'ECE product bundles compatibility', () => { + it( 'filters out cart items that are bundled by something else', () => { + const cartData = applyFilters( + 'wcpay.express-checkout.map-line-items', + { + items: [ + { + key: 'd179a6924eafc82d7864f1e0caedbe95', + id: 261, + type: 'bundle', + quantity: 1, + item_data: [ + { + key: 'Includes', + value: 'T-Shirt × 1', + }, + { + key: 'Includes', + value: 'T-Shirt with Logo × 2', + }, + { + key: 'Includes', + value: 'V-Neck T-Shirt - Medium × 1', + }, + ], + extensions: { + bundles: { + bundled_items: [ + 'abda15f782e68dc63bd615d6a05fa3d2', + '4d16fa6ebc10a1d66013b0f85640eb2b', + 'ff279cc5574ef1cf45aa76bde0d66baa', + ], + bundle_data: { + configuration: { + '1': { + product_id: 13, + quantity: 1, + discount: 20, + optional_selected: 'yes', + }, + '2': { + product_id: 30, + quantity: 2, + discount: '', + }, + '3': { + product_id: 10, + quantity: 1, + discount: '', + attributes: { + attribute_size: 'Medium', + }, + variation_id: '25', + }, + '4': { + product_id: 10, + quantity: 0, + discount: '', + optional_selected: 'no', + attributes: [], + }, + }, + is_editable: false, + is_price_hidden: false, + is_subtotal_hidden: false, + is_hidden: false, + is_meta_hidden_in_cart: true, + is_meta_hidden_in_summary: false, + }, + }, + }, + }, + { + key: 'abda15f782e68dc63bd615d6a05fa3d2', + id: 13, + type: 'simple', + quantity: 1, + extensions: { + bundles: { + bundled_by: 'd179a6924eafc82d7864f1e0caedbe95', + bundled_item_data: { + bundle_id: 261, + bundled_item_id: 1, + is_removable: true, + is_indented: true, + is_subtotal_aggregated: true, + is_parent_visible: true, + is_last: false, + is_price_hidden: false, + is_subtotal_hidden: false, + is_thumbnail_hidden: false, + is_hidden_in_cart: false, + is_hidden_in_summary: true, + }, + }, + }, + }, + { + key: '4d16fa6ebc10a1d66013b0f85640eb2b', + id: 30, + type: 'simple', + quantity: 2, + extensions: { + bundles: { + bundled_by: 'd179a6924eafc82d7864f1e0caedbe95', + bundled_item_data: { + bundle_id: 261, + bundled_item_id: 2, + is_removable: false, + is_indented: true, + is_subtotal_aggregated: true, + is_parent_visible: true, + is_last: false, + is_price_hidden: true, + is_subtotal_hidden: true, + is_thumbnail_hidden: false, + is_hidden_in_cart: false, + is_hidden_in_summary: true, + }, + }, + }, + }, + { + key: 'ff279cc5574ef1cf45aa76bde0d66baa', + id: 25, + type: 'variation', + quantity: 1, + extensions: { + bundles: { + bundled_by: 'd179a6924eafc82d7864f1e0caedbe95', + bundled_item_data: { + bundle_id: 261, + bundled_item_id: 3, + is_removable: false, + is_indented: true, + is_subtotal_aggregated: true, + is_parent_visible: true, + is_last: true, + is_price_hidden: true, + is_subtotal_hidden: true, + is_thumbnail_hidden: false, + is_hidden_in_cart: false, + is_hidden_in_summary: true, + }, + }, + }, + }, + { + key: 'c51ce410c124a10e0db5e4b97fc2af39', + id: 13, + type: 'simple', + quantity: 1, + extensions: { + bundles: [], + }, + }, + ], + items_count: 2, + } + ); + + expect( cartData ).toStrictEqual( { + items: [ + { + extensions: { + bundles: { + bundle_data: { + configuration: { + '1': { + discount: 20, + optional_selected: 'yes', + product_id: 13, + quantity: 1, + }, + '2': { + discount: '', + product_id: 30, + quantity: 2, + }, + '3': { + attributes: { + attribute_size: 'Medium', + }, + discount: '', + product_id: 10, + quantity: 1, + variation_id: '25', + }, + '4': { + attributes: [], + discount: '', + optional_selected: 'no', + product_id: 10, + quantity: 0, + }, + }, + is_editable: false, + is_hidden: false, + is_meta_hidden_in_cart: true, + is_meta_hidden_in_summary: false, + is_price_hidden: false, + is_subtotal_hidden: false, + }, + bundled_items: [ + 'abda15f782e68dc63bd615d6a05fa3d2', + '4d16fa6ebc10a1d66013b0f85640eb2b', + 'ff279cc5574ef1cf45aa76bde0d66baa', + ], + }, + }, + id: 261, + item_data: [ + { + key: 'Includes', + value: 'T-Shirt × 1', + }, + { + key: 'Includes', + value: 'T-Shirt with Logo × 2', + }, + { + key: 'Includes', + value: 'V-Neck T-Shirt - Medium × 1', + }, + ], + key: 'd179a6924eafc82d7864f1e0caedbe95', + quantity: 1, + type: 'bundle', + }, + { + extensions: { + bundles: [], + }, + id: 13, + key: 'c51ce410c124a10e0db5e4b97fc2af39', + quantity: 1, + type: 'simple', + }, + ], + items_count: 2, + } ); + } ); +} ); diff --git a/client/tokenized-express-checkout/compatibility/wc-product-bundles.js b/client/tokenized-express-checkout/compatibility/wc-product-bundles.js new file mode 100644 index 00000000000..7a3d2a4dc3c --- /dev/null +++ b/client/tokenized-express-checkout/compatibility/wc-product-bundles.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +addFilter( + 'wcpay.express-checkout.map-line-items', + 'automattic/wcpay/express-checkout', + ( cartData ) => { + return { + ...cartData, + // ensuring that the items that are bundled by another don't appear in the summary. + // otherwise they might be contributing to the wrong order total, creating errors. + items: cartData.items.filter( + ( item ) => ! item.extensions?.bundles?.bundled_by + ), + }; + } +); diff --git a/client/tokenized-express-checkout/index.js b/client/tokenized-express-checkout/index.js index 3b2eb1a8b46..e6c24d02e91 100644 --- a/client/tokenized-express-checkout/index.js +++ b/client/tokenized-express-checkout/index.js @@ -13,6 +13,7 @@ import '../checkout/express-checkout-buttons.scss'; import './compatibility/wc-deposits'; import './compatibility/wc-order-attribution'; import './compatibility/wc-product-page'; +import './compatibility/wc-product-bundles'; import { getExpressCheckoutButtonAppearance, getExpressCheckoutButtonStyleSettings, diff --git a/client/tokenized-express-checkout/transformers/wc-to-stripe.js b/client/tokenized-express-checkout/transformers/wc-to-stripe.js index 6d9b39035ce..794fc5309b1 100644 --- a/client/tokenized-express-checkout/transformers/wc-to-stripe.js +++ b/client/tokenized-express-checkout/transformers/wc-to-stripe.js @@ -8,6 +8,7 @@ import { decodeEntities } from '@wordpress/html-entities'; * Internal dependencies */ import { getExpressCheckoutData } from '../utils'; +import { applyFilters } from '@wordpress/hooks'; /** * GooglePay/ApplePay expect the prices to be formatted in cents. @@ -34,10 +35,16 @@ export const transformPrice = ( price, priceObject ) => { * - https://docs.stripe.com/js/elements_object/express_checkout_element_shippingaddresschange_event * - https://docs.stripe.com/js/elements_object/express_checkout_element_shippingratechange_event * - * @param {Object} cartData Store API Cart response object. + * @param {Object} rawCartData Store API Cart response object. * @return {{pending: boolean, name: string, amount: integer}} `displayItems` for Stripe. */ -export const transformCartDataForDisplayItems = ( cartData ) => { +export const transformCartDataForDisplayItems = ( rawCartData ) => { + // allowing extensions to manipulate the individual items returned by the backend. + const cartData = applyFilters( + 'wcpay.express-checkout.map-line-items', + rawCartData + ); + const displayItems = cartData.items.map( ( item ) => ( { amount: transformPrice( parseInt( item.totals?.line_subtotal || item.prices.price, 10 ), diff --git a/client/transactions/blocked/columns.tsx b/client/transactions/blocked/columns.tsx index 1d75e407cea..8e4f410ff32 100644 --- a/client/transactions/blocked/columns.tsx +++ b/client/transactions/blocked/columns.tsx @@ -2,9 +2,7 @@ * External dependencies */ import React from 'react'; -import { dateI18n } from '@wordpress/date'; import { __ } from '@wordpress/i18n'; -import moment from 'moment'; import { TableCardColumn, TableCardBodyColumn } from '@woocommerce/components'; /** @@ -15,6 +13,7 @@ import TransactionStatusPill from 'wcpay/components/transaction-status-pill'; import { FraudOutcomeTransaction } from '../../data'; import { getDetailsURL } from '../../components/details-link'; import ClickableCell from '../../components/clickable-cell'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface Column extends TableCardColumn { key: 'created' | 'amount' | 'customer' | 'status'; @@ -70,10 +69,9 @@ export const getBlockedListRowContent = ( data.payment_intent.id || data.order_id.toString(), 'transactions' ); - const formattedCreatedDate = dateI18n( - 'M j, Y / g:iA', - moment.utc( data.created ).local().toISOString() - ); + const formattedCreatedDate = formatDateTimeFromString( data.created, { + includeTime: true, + } ); const clickable = ( children: JSX.Element | string ) => ( { children } diff --git a/client/transactions/blocked/test/columns.test.tsx b/client/transactions/blocked/test/columns.test.tsx index 7ca2c3d4895..b65e2d10c12 100644 --- a/client/transactions/blocked/test/columns.test.tsx +++ b/client/transactions/blocked/test/columns.test.tsx @@ -15,6 +15,8 @@ declare const global: { connect: { country: string; }; + dateFormat: string; + timeFormat: string; }; }; const mockWcPaySettings = { @@ -23,6 +25,8 @@ const mockWcPaySettings = { connect: { country: 'US', }, + dateFormat: 'M j, Y', + timeFormat: 'g:iA', }; describe( 'Blocked fraud outcome transactions columns', () => { diff --git a/client/transactions/index.tsx b/client/transactions/index.tsx index 507a972cd5a..f8139f16797 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 DateFormatNotice from 'components/date-format-notice'; declare const window: any; @@ -106,6 +107,7 @@ export const TransactionsPage: React.FC = () => { return ( + = ( { depositId, dateAvailable } ) => { id: depositId, } ); - const formattedDateAvailable = dateI18n( - 'M j, Y', - moment.utc( dateAvailable ).toISOString(), - true // TODO Change call to gmdateI18n and remove this deprecated param once WP 5.4 support ends. + const formattedDateAvailable = formatDateTimeFromString( + dateAvailable ); - return { formattedDateAvailable }; } diff --git a/client/transactions/list/index.tsx b/client/transactions/list/index.tsx index f56b7d0f084..3437a7bf84b 100644 --- a/client/transactions/list/index.tsx +++ b/client/transactions/list/index.tsx @@ -7,9 +7,7 @@ import React, { Fragment, useState } from 'react'; import { uniq } from 'lodash'; import { useDispatch } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; -import { dateI18n } from '@wordpress/date'; import { __, _n, sprintf } from '@wordpress/i18n'; -import moment from 'moment'; import { TableCard, Search, @@ -58,6 +56,7 @@ import { getTransactionsCSV } from '../../data/transactions/resolvers'; import p24BankList from '../../payment-details/payment-method/p24/bank-list'; import { HoverTooltip } from 'components/tooltip'; import { PAYMENT_METHOD_TITLES } from 'wcpay/constants/payment-method'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface TransactionsListProps { depositId?: string; @@ -448,10 +447,9 @@ export const TransactionsList = ( date: { value: txn.date, display: clickable( - dateI18n( - 'M j, Y / g:iA', - moment.utc( txn.date ).local().toISOString() - ) + formatDateTimeFromString( txn.date, { + includeTime: true, + } ) ), }, channel: { diff --git a/client/transactions/list/test/__snapshots__/index.tsx.snap b/client/transactions/list/test/__snapshots__/index.tsx.snap index 7a4b1eb3392..c432444b3ea 100644 --- a/client/transactions/list/test/__snapshots__/index.tsx.snap +++ b/client/transactions/list/test/__snapshots__/index.tsx.snap @@ -493,7 +493,7 @@ exports[`Transactions list renders correctly when can filter by several currenci href="admin.php?page=wc-admin&path=%2Fpayments%2Ftransactions%2Fdetails&id=pi_mock&transaction_id=txn_j23jda9JJa&transaction_type=refund" tabindex="-1" > - Jan 2, 2020 / 12:46PM + Jan 2, 2020 / 5:46PM - Jan 4, 2020 / 11:22PM + Jan 5, 2020 / 4:22AM - Jan 2, 2020 / 2:55PM + Jan 2, 2020 / 7:55PM - Jan 2, 2020 / 12:46PM + Jan 2, 2020 / 5:46PM - Jan 4, 2020 / 11:22PM + Jan 5, 2020 / 4:22AM - Jan 2, 2020 / 2:55PM + Jan 2, 2020 / 7:55PM - Jan 4, 2020 / 11:22PM + Jan 5, 2020 / 4:22AM - Jan 2, 2020 / 12:46PM + Jan 2, 2020 / 5:46PM - Jan 4, 2020 / 11:22PM + Jan 5, 2020 / 4:22AM - Jan 2, 2020 / 2:55PM + Jan 2, 2020 / 7:55PM - Jan 2, 2020 / 12:46PM + Jan 2, 2020 / 5:46PM - Jan 4, 2020 / 11:22PM + Jan 5, 2020 / 4:22AM - Jan 2, 2020 / 2:55PM + Jan 2, 2020 / 7:55PM - Jan 2, 2020 / 12:46PM + Jan 2, 2020 / 5:46PM - Jan 4, 2020 / 11:22PM + Jan 5, 2020 / 4:22AM - Jan 2, 2020 / 2:55PM + Jan 2, 2020 / 7:55PM ( { + dateI18n: jest.fn( ( format, date ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'UTC' ); // Ensure UTC is used + } ), +} ) ); + describe( 'Deposit', () => { + beforeEach( () => { + // Mock the window.wcpaySettings property + window.wcpaySettings.dateFormat = 'M j, Y'; + window.wcpaySettings.timeFormat = 'g:i a'; + } ); + + afterEach( () => { + // Reset the mock + jest.clearAllMocks(); + } ); + test( 'renders with date and payout available', () => { const { container: link } = render( diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx index 024dcae07db..ce8ff812523 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -53,6 +53,15 @@ jest.mock( 'data/index', () => ( { useTransactionsSummary: jest.fn(), } ) ); +// Mock dateI18n +jest.mock( '@wordpress/date', () => ( { + dateI18n: jest.fn( ( format, date ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'UTC' ); // Ensure UTC is used + } ), +} ) ); + const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction< typeof downloadCSVFile >; @@ -233,6 +242,8 @@ describe( 'Transactions list', () => { code: 'en', }, }; + window.wcpaySettings.dateFormat = 'M j, Y'; + window.wcpaySettings.timeFormat = 'g:iA'; } ); test( 'renders correctly when filtered by payout', () => { diff --git a/client/transactions/risk-review/columns.tsx b/client/transactions/risk-review/columns.tsx index d7f5de95111..c1e24c4d428 100644 --- a/client/transactions/risk-review/columns.tsx +++ b/client/transactions/risk-review/columns.tsx @@ -2,9 +2,7 @@ * External dependencies */ import React from 'react'; -import { dateI18n } from '@wordpress/date'; import { __ } from '@wordpress/i18n'; -import moment from 'moment'; import { TableCardColumn, TableCardBodyColumn } from '@woocommerce/components'; import { Button } from '@wordpress/components'; @@ -17,6 +15,7 @@ import { formatExplicitCurrency } from 'multi-currency/interface/functions'; import { recordEvent } from 'tracks'; import TransactionStatusPill from 'wcpay/components/transaction-status-pill'; import { FraudOutcomeTransaction } from '../../data'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface Column extends TableCardColumn { key: 'created' | 'amount' | 'customer' | 'status'; @@ -76,10 +75,9 @@ export const getRiskReviewListRowContent = ( data: FraudOutcomeTransaction ): Record< string, TableCardBodyColumn > => { const detailsURL = getDetailsURL( data.payment_intent.id, 'transactions' ); - const formattedCreatedDate = dateI18n( - 'M j, Y / g:iA', - moment.utc( data.created ).local().toISOString() - ); + const formattedCreatedDate = formatDateTimeFromString( data.created, { + includeTime: true, + } ); const clickable = ( children: JSX.Element | string ) => ( { children } diff --git a/client/transactions/risk-review/test/columns.test.tsx b/client/transactions/risk-review/test/columns.test.tsx index 033f9eb7aa2..54de107e0e4 100644 --- a/client/transactions/risk-review/test/columns.test.tsx +++ b/client/transactions/risk-review/test/columns.test.tsx @@ -15,6 +15,8 @@ declare const global: { connect: { country: string; }; + dateFormat: string; + timeFormat: string; }; }; const mockWcPaySettings = { @@ -23,6 +25,8 @@ const mockWcPaySettings = { connect: { country: 'US', }, + dateFormat: 'M j, Y', + timeFormat: 'g:iA', }; describe( 'Review fraud outcome transactions columns', () => { diff --git a/client/transactions/uncaptured/index.tsx b/client/transactions/uncaptured/index.tsx index 17058760c19..42d6d69542b 100644 --- a/client/transactions/uncaptured/index.tsx +++ b/client/transactions/uncaptured/index.tsx @@ -7,7 +7,6 @@ import React, { useEffect } from 'react'; import { __ } from '@wordpress/i18n'; import { TableCard, TableCardColumn } from '@woocommerce/components'; import { onQueryChange, getQuery } from '@woocommerce/navigation'; -import { dateI18n } from '@wordpress/date'; import moment from 'moment'; /** @@ -21,6 +20,7 @@ import { formatExplicitCurrency } from 'multi-currency/interface/functions'; import RiskLevel, { calculateRiskMapping } from 'components/risk-level'; import { recordEvent } from 'tracks'; import CaptureAuthorizationButton from 'wcpay/components/capture-authorization-button'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface Column extends TableCardColumn { key: @@ -130,35 +130,25 @@ export const AuthorizationsList = (): JSX.Element => { display: auth.payment_intent_id, }, created: { - value: dateI18n( - 'M j, Y / g:iA', - moment.utc( auth.created ).local().toISOString() - ), + value: formatDateTimeFromString( auth.created, { + includeTime: true, + } ), display: clickable( - dateI18n( - 'M j, Y / g:iA', - moment.utc( auth.created ).local().toISOString() - ) + formatDateTimeFromString( auth.created, { + includeTime: true, + } ) ), }, // Payments are authorized for a maximum of 7 days capture_by: { - value: dateI18n( - 'M j, Y / g:iA', - moment - .utc( auth.created ) - .add( 7, 'd' ) - .local() - .toISOString() + value: formatDateTimeFromString( + moment.utc( auth.created ).add( 7, 'd' ).toISOString(), + { includeTime: true } ), display: clickable( - dateI18n( - 'M j, Y / g:iA', - moment - .utc( auth.created ) - .add( 7, 'd' ) - .local() - .toISOString() + formatDateTimeFromString( + moment.utc( auth.created ).add( 7, 'd' ).toISOString(), + { includeTime: true } ) ), }, diff --git a/client/transactions/uncaptured/test/index.test.tsx b/client/transactions/uncaptured/test/index.test.tsx index 52f76dcc882..fc21a532b67 100644 --- a/client/transactions/uncaptured/test/index.test.tsx +++ b/client/transactions/uncaptured/test/index.test.tsx @@ -67,6 +67,8 @@ declare const global: { precision: number; }; }; + dateFormat: string; + timeFormat: string; }; }; @@ -126,6 +128,8 @@ describe( 'Authorizations list', () => { precision: 2, }, }, + dateFormat: 'M j, Y', + timeFormat: 'g:iA', }; } ); diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 711d3d337ed..791b727b164 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -143,7 +143,7 @@ export const formatMethodFeesTooltip = ( return (
    -
    Base fee
    +
    { __( 'Base fee', 'woocommerce-payments' ) }
    { getFeeDescriptionString( accountFees.base, @@ -153,7 +153,12 @@ export const formatMethodFeesTooltip = (
    { hasFees( accountFees.additional ) ? (
    -
    International payment method fee
    +
    + { __( + 'International payment method fee', + 'woocommerce-payments' + ) } +
    { getFeeDescriptionString( accountFees.additional, @@ -166,14 +171,21 @@ export const formatMethodFeesTooltip = ( ) } { hasFees( accountFees.fx ) ? (
    -
    Foreign exchange fee
    +
    + { __( + 'Currency conversion fee', + 'woocommerce-payments' + ) } +
    { getFeeDescriptionString( accountFees.fx ) }
    ) : ( '' ) }
    -
    Total per transaction
    +
    + { __( 'Total per transaction', 'woocommerce-payments' ) } +
    { getFeeDescriptionString( total ) }
    diff --git a/client/utils/date-time.ts b/client/utils/date-time.ts new file mode 100644 index 00000000000..83e4c2c2257 --- /dev/null +++ b/client/utils/date-time.ts @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import { dateI18n } from '@wordpress/date'; +import moment from 'moment'; + +type DateTimeFormat = string | null; + +interface FormatDateTimeOptions { + /** Whether to include time in the formatted string (defaults to true) */ + includeTime?: boolean; + /** Separator between date and time (defaults to ' / ') */ + separator?: string; + /** Custom format to use instead of WordPress settings */ + customFormat?: DateTimeFormat; + /** Timezone string (e.g., 'UTC', 'America/New_York'). If undefined, uses site default */ + timezone?: string; +} + +/** + * Formats a date/time string in YYYY-MM-DD HH:MM:SS format according to WordPress settings. + * The input date string is converted to UTC for consistent handling across timezones. + * + * @param dateTimeStr - Date time string in YYYY-MM-DD HH:MM:SS format + * @param options - Formatting options + */ +export function formatDateTimeFromString( + dateTimeStr: string, + options: FormatDateTimeOptions = {} +): string { + const { + customFormat = null, + includeTime = false, + separator = ' / ', + timezone = undefined, + } = options; + + // Convert to UTC ISO string for consistent handling + const utcDateTime = moment.utc( dateTimeStr ).toISOString(); + + const format = + customFormat || + `${ window.wcpaySettings.dateFormat }${ + includeTime + ? `${ separator }${ window.wcpaySettings.timeFormat }` + : '' + }`; + + return dateI18n( format, utcDateTime, timezone ); +} + +/** + * Formats a Unix timestamp according to WordPress settings. + * The input timestamp is converted to UTC for consistent handling across timezones. + * + * @param timestamp - Unix timestamp (seconds since epoch) + * @param options - Formatting options + */ +export function formatDateTimeFromTimestamp( + timestamp: number, + options: FormatDateTimeOptions = {} +): string { + const { + customFormat = null, + includeTime = false, + separator = ' / ', + timezone = undefined, + } = options; + + // Convert to UTC ISO string for consistent handling + const utcDateTime = moment.unix( timestamp ).utc().toISOString(); + + const format = + customFormat || + `${ window.wcpaySettings.dateFormat }${ + includeTime + ? `${ separator }${ window.wcpaySettings.timeFormat }` + : '' + }`; + + return dateI18n( format, utcDateTime, timezone ); +} diff --git a/client/utils/test/__snapshots__/account-fees.tsx.snap b/client/utils/test/__snapshots__/account-fees.tsx.snap index 89321bc7582..d92aa6ae54e 100644 --- a/client/utils/test/__snapshots__/account-fees.tsx.snap +++ b/client/utils/test/__snapshots__/account-fees.tsx.snap @@ -23,7 +23,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base
    - Foreign exchange fee + Currency conversion fee
    1% @@ -102,7 +102,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base
    - Foreign exchange fee + Currency conversion fee
    1% diff --git a/client/utils/test/date-time.test.ts b/client/utils/test/date-time.test.ts new file mode 100644 index 00000000000..798c95d7755 --- /dev/null +++ b/client/utils/test/date-time.test.ts @@ -0,0 +1,181 @@ +/** + * Internal dependencies + */ +import { + formatDateTimeFromString, + formatDateTimeFromTimestamp, +} from 'wcpay/utils/date-time'; + +// Mock dateI18n +jest.mock( '@wordpress/date', () => ( { + dateI18n: jest.fn( ( format, date, timezone ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, timezone || 'UTC' ); // Use provided timezone or fallback to UTC + } ), +} ) ); + +describe( 'Date/Time Formatting', () => { + const originalWcpaySettings = window.wcpaySettings; + const mockWcpaySettings = { + dateFormat: 'Y-m-d', + timeFormat: 'H:i', + }; + + beforeEach( () => { + jest.clearAllMocks(); + window.wcpaySettings = mockWcpaySettings as typeof wcpaySettings; + } ); + + afterEach( () => { + window.wcpaySettings = originalWcpaySettings; + } ); + + describe( 'formatDateTimeFromString', () => { + it( 'should format using default WordPress settings', () => { + const dateTime = '2024-10-23 15:28:26'; + const formatted = formatDateTimeFromString( dateTime, { + includeTime: true, + } ); + + expect( formatted ).toBe( '2024-10-23 / 15:28' ); + } ); + + it( 'should use custom format if provided', () => { + const dateTime = '2024-10-23 15:28:26'; + const options = { customFormat: 'd-m-Y H:i:s' }; + const formatted = formatDateTimeFromString( dateTime, options ); + + expect( formatted ).toBe( '23-10-2024 15:28:26' ); + } ); + + it( 'should exclude time if includeTime is set to false', () => { + const dateTime = '2024-10-23 15:28:26'; + const formatted = formatDateTimeFromString( dateTime ); + + expect( formatted ).toBe( '2024-10-23' ); + } ); + + it( 'should use custom separator when provided', () => { + const dateTime = '2024-10-23 15:28:26'; + const options = { separator: ' - ', includeTime: true }; + const formatted = formatDateTimeFromString( dateTime, options ); + + expect( formatted ).toBe( '2024-10-23 - 15:28' ); + } ); + + it( 'should handle different timezones correctly', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const dateI18n = require( '@wordpress/date' ).dateI18n; + // Temporarily modify the mock to use a different timezone: America/New_York + dateI18n.mockImplementationOnce( + ( format: string, date: string | number ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'America/New_York' ); + } + ); + + const dateTime = '2024-10-23 15:28:26'; + const formatted = formatDateTimeFromString( dateTime, { + includeTime: true, + } ); + + expect( formatted ).toBe( '2024-10-23 / 11:28' ); + } ); + + it( 'should respect explicitly provided timezone', () => { + const dateTime = '2024-10-23 15:28:26'; + + // Test with UTC timezone + const formattedUTC = formatDateTimeFromString( dateTime, { + includeTime: true, + timezone: 'UTC', + } ); + expect( formattedUTC ).toBe( '2024-10-23 / 15:28' ); + + // Test with New York timezone + const formattedNY = formatDateTimeFromString( dateTime, { + includeTime: true, + timezone: 'America/New_York', + } ); + expect( formattedNY ).toBe( '2024-10-23 / 11:28' ); + } ); + } ); + + describe( 'formatDateTimeFromTimestamp', () => { + it( 'should format using default WordPress settings', () => { + const timestamp = 1729766906; // 2024-10-23 10:48:26 UTC + const formatted = formatDateTimeFromTimestamp( timestamp, { + includeTime: true, + } ); + + expect( formatted ).toBe( '2024-10-24 / 10:48' ); + } ); + + it( 'should use custom format if provided', () => { + const timestamp = 1729766906; // 2024-10-23 10:48:26 UTC + const options = { customFormat: 'd-m-Y H:i:s' }; + const formatted = formatDateTimeFromTimestamp( timestamp, options ); + + expect( formatted ).toBe( '24-10-2024 10:48:26' ); + } ); + + it( 'should exclude time if includeTime is set to false', () => { + const timestamp = 1729766906; // 2024-10-23 10:48:26 UTC + const formatted = formatDateTimeFromTimestamp( timestamp ); + + expect( formatted ).toBe( '2024-10-24' ); + } ); + + it( 'should use custom separator when provided', () => { + const timestamp = 1729766906; // 2024-10-23 10:48:26 UTC + const options = { + separator: ' - ', + includeTime: true, + }; + const formatted = formatDateTimeFromTimestamp( timestamp, options ); + + expect( formatted ).toBe( '2024-10-24 - 10:48' ); + } ); + + it( 'should handle different timezones correctly', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const dateI18n = require( '@wordpress/date' ).dateI18n; + // Temporarily modify the mock to use a different timezone: America/New_York + dateI18n.mockImplementationOnce( + ( format: string, date: string | number ) => { + return jest + .requireActual( '@wordpress/date' ) + .dateI18n( format, date, 'America/New_York' ); + } + ); + + const timestamp = 1729766906; // 2024-10-24 10:48:26 UTC + const formatted = formatDateTimeFromTimestamp( timestamp, { + includeTime: true, + } ); + + // In New York (EDT), this should be 4 hours behind UTC + expect( formatted ).toBe( '2024-10-24 / 06:48' ); + } ); + + it( 'should respect explicitly provided timezone', () => { + const timestamp = 1729766906; // 2024-10-24 10:48:26 UTC + + // Test with UTC timezone + const formattedUTC = formatDateTimeFromTimestamp( timestamp, { + includeTime: true, + timezone: 'UTC', + } ); + expect( formattedUTC ).toBe( '2024-10-24 / 10:48' ); + + // Test with New York timezone + const formattedNY = formatDateTimeFromTimestamp( timestamp, { + includeTime: true, + timezone: 'America/New_York', + } ); + expect( formattedNY ).toBe( '2024-10-24 / 06:48' ); + } ); + } ); +} ); diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index f3d711094af..531fa432fb8 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -6,6 +6,7 @@ */ use Automattic\Jetpack\Identity_Crisis as Jetpack_Identity_Crisis; +use WCPay\Constants\Intent_Status; use WCPay\Core\Server\Request; use WCPay\Database_Cache; use WCPay\Logger; @@ -972,6 +973,7 @@ private function get_js_settings(): array { 'storeName' => get_bloginfo( 'name' ), 'isNextDepositNoticeDismissed' => WC_Payments_Features::is_next_deposit_notice_dismissed(), 'isInstantDepositNoticeDismissed' => get_option( 'wcpay_instant_deposit_notice_dismissed', false ), + 'isDateFormatNoticeDismissed' => get_option( 'wcpay_date_format_notice_dismissed', false ), 'dismissedDuplicateNotices' => get_option( 'wcpay_duplicate_payment_method_notices_dismissed', [] ), 'locale' => WC_Payments_Utils::get_language_data( get_locale() ), 'isOverviewSurveySubmitted' => get_option( 'wcpay_survey_payment_overview_submitted', false ), @@ -979,6 +981,8 @@ private function get_js_settings(): array { 'lifetimeTPV' => $this->account->get_lifetime_total_payment_volume(), 'defaultExpressCheckoutBorderRadius' => WC_Payments_Express_Checkout_Button_Handler::DEFAULT_BORDER_RADIUS_IN_PX, 'isWooPayGlobalThemeSupportEligible' => WC_Payments_Features::is_woopay_global_theme_support_eligible(), + 'dateFormat' => wc_date_format(), + 'timeFormat' => get_option( 'time_format' ), ]; return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); @@ -1250,7 +1254,7 @@ public function show_woopay_payment_method_name_admin( $order_id ) { */ public function display_wcpay_transaction_fee( $order_id ) { $order = wc_get_order( $order_id ); - if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) ) { + if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) || Intent_Status::REQUIRES_CAPTURE === $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ) ) { return; } ?> diff --git a/includes/class-wc-payments-captured-event-note.php b/includes/class-wc-payments-captured-event-note.php index 10c48567952..07e902d8632 100644 --- a/includes/class-wc-payments-captured-event-note.php +++ b/includes/class-wc-payments-captured-event-note.php @@ -327,9 +327,9 @@ private function fee_label_mapping( int $fixed_rate, bool $is_capped ) { $res['additional-fx'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ - ? __( 'Foreign exchange fee: %1$s%% + %2$s', 'woocommerce-payments' ) + ? __( 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ - : __( 'Foreign exchange fee: %1$s%%', 'woocommerce-payments' ); + : __( 'Currency conversion fee: %1$s%%', 'woocommerce-payments' ); $res['additional-wcpay-subscription'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 4ad2d32625e..7b4ae0750c2 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -1930,6 +1930,7 @@ public static function add_wcpay_options_to_woocommerce_permissions_list( $permi 'wcpay_duplicate_payment_method_notices_dismissed', 'wcpay_exit_survey_dismissed', 'wcpay_instant_deposit_notice_dismissed', + 'wcpay_date_format_notice_dismissed', ], true ); diff --git a/includes/compat/blocks/class-blocks-data-extractor.php b/includes/compat/blocks/class-blocks-data-extractor.php index 673cae7f352..becc393a5da 100644 --- a/includes/compat/blocks/class-blocks-data-extractor.php +++ b/includes/compat/blocks/class-blocks-data-extractor.php @@ -59,6 +59,15 @@ private function get_available_blocks() { $blocks[] = new \Mailchimp_Woocommerce_Newsletter_Blocks_Integration(); } + if ( class_exists( '\WCK\Blocks\CheckoutIntegration' ) ) { + // phpcs:ignore + /** + * @psalm-suppress UndefinedClass + * @phpstan-ignore-next-line + */ + $blocks[] = new \WCK\Blocks\CheckoutIntegration(); + } + return $blocks; } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 672f2584c67..86d1a82c54d 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -415,7 +415,7 @@ public function should_show_express_checkout_button() { return true; } - // Non-shipping product and billing is calculated based on shopper billing addres. Excludes Pay for Order page. + // Non-shipping product and tax is calculated based on shopper billing address. Excludes Pay for Order page. if ( // If the product doesn't needs shipping. ( @@ -426,8 +426,10 @@ public function should_show_express_checkout_button() { ( ( $this->is_cart() || $this->is_checkout() ) && ! WC()->cart->needs_shipping() ) ) - // ...and billing is calculated based on billing address. - && wc_tax_enabled() && 'billing' === get_option( 'woocommerce_tax_based_on' ) + // ...and tax is calculated based on billing address. + && wc_tax_enabled() + && 'billing' === get_option( 'woocommerce_tax_based_on' ) + && 'yes' !== get_option( 'woocommerce_prices_include_tax' ) ) { return false; } diff --git a/includes/multi-currency/Compatibility/WooCommerceFedEx.php b/includes/multi-currency/Compatibility/WooCommerceFedEx.php index 8a38d058e40..15c25b4ba27 100644 --- a/includes/multi-currency/Compatibility/WooCommerceFedEx.php +++ b/includes/multi-currency/Compatibility/WooCommerceFedEx.php @@ -8,13 +8,25 @@ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\MultiCurrency; -use WCPay\MultiCurrency\Utils; /** * Class that controls Multi Currency Compatibility with WooCommerce FedEx Plugin. */ class WooCommerceFedEx extends BaseCompatibility { + /** + * Calls to look for in the backtrace when determining whether + * to return store currency or skip converting product prices. + */ + private const WC_SHIPPING_FEDEX_CALLS = [ + 'WC_Shipping_Fedex->set_settings', + 'WC_Shipping_Fedex->per_item_shipping', + 'WC_Shipping_Fedex->box_shipping', + 'WC_Shipping_Fedex->get_fedex_api_request', + 'WC_Shipping_Fedex->get_fedex_requests', + 'WC_Shipping_Fedex->process_result', + ]; + /** * Init the class. * @@ -23,10 +35,31 @@ class WooCommerceFedEx extends BaseCompatibility { public function init() { // Add needed actions and filters if FedEx is active. if ( class_exists( 'WC_Shipping_Fedex_Init' ) ) { + add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ] ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] ); } } + /** + * Checks to see if the product's price should be converted. + * + * @param bool $return Whether to convert the product's price or not. Default is true. + * + * @return bool True if it should be converted. + */ + public function should_convert_product_price( bool $return ): bool { + // If it's already false, return it. + if ( ! $return ) { + return $return; + } + + if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) { + return false; + } + + return $return; + } + /** * Determine whether to return the store currency or not. * @@ -40,15 +73,7 @@ public function should_return_store_currency( bool $return ): bool { return $return; } - $calls = [ - 'WC_Shipping_Fedex->set_settings', - 'WC_Shipping_Fedex->per_item_shipping', - 'WC_Shipping_Fedex->box_shipping', - 'WC_Shipping_Fedex->get_fedex_api_request', - 'WC_Shipping_Fedex->get_fedex_requests', - 'WC_Shipping_Fedex->process_result', - ]; - if ( $this->utils->is_call_in_backtrace( $calls ) ) { + if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) { return true; } diff --git a/tests/fixtures/captured-payments/discount.json b/tests/fixtures/captured-payments/discount.json index 2fa6a911d74..5bf6f936c45 100644 --- a/tests/fixtures/captured-payments/discount.json +++ b/tests/fixtures/captured-payments/discount.json @@ -60,7 +60,7 @@ "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", "additional-international": "International card fee: 1%", - "additional-fx": "Foreign exchange fee: 1%", + "additional-fx": "Currency conversion fee: 1%", "discount": { "label": "Discount", "variable": "Variable fee: -4.9%", diff --git a/tests/fixtures/captured-payments/foreign-card.json b/tests/fixtures/captured-payments/foreign-card.json index 234878b2372..df45c326d62 100644 --- a/tests/fixtures/captured-payments/foreign-card.json +++ b/tests/fixtures/captured-payments/foreign-card.json @@ -53,7 +53,7 @@ "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", "additional-international": "International card fee: 1%", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $95.47 USD" } diff --git a/tests/fixtures/captured-payments/fx-decimal.json b/tests/fixtures/captured-payments/fx-decimal.json index b95e9318c84..2f065036122 100644 --- a/tests/fixtures/captured-payments/fx-decimal.json +++ b/tests/fixtures/captured-payments/fx-decimal.json @@ -45,7 +45,7 @@ "feeString": "Fee (3.9% + $0.30): -$4.39", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $100.65 USD" } diff --git a/tests/fixtures/captured-payments/fx-partial-capture.json b/tests/fixtures/captured-payments/fx-partial-capture.json index f10ff7aa9e9..691390d4852 100644 --- a/tests/fixtures/captured-payments/fx-partial-capture.json +++ b/tests/fixtures/captured-payments/fx-partial-capture.json @@ -57,7 +57,7 @@ "feeString": "Fee (3.51% + £0.21): -$0.88", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%", + "additional-fx": "Currency conversion fee: 1%", "discount": { "label": "Discount", "variable": "Variable fee: -0.39%", diff --git a/tests/fixtures/captured-payments/fx-with-capped-fee.json b/tests/fixtures/captured-payments/fx-with-capped-fee.json index 8c1b602a3eb..4c31a8435d7 100644 --- a/tests/fixtures/captured-payments/fx-with-capped-fee.json +++ b/tests/fixtures/captured-payments/fx-with-capped-fee.json @@ -55,7 +55,7 @@ "feeBreakdown": { "base": "Base fee: capped at $6.00", "additional-international": "International card fee: 1.5%", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $971.04 USD" } diff --git a/tests/fixtures/captured-payments/fx.json b/tests/fixtures/captured-payments/fx.json index 8ceee7b7438..f18ca9297ab 100644 --- a/tests/fixtures/captured-payments/fx.json +++ b/tests/fixtures/captured-payments/fx.json @@ -46,7 +46,7 @@ "feeString": "Fee (3.9% + $0.30): -$4.20", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $95.84 USD" } diff --git a/tests/fixtures/captured-payments/jpy-payment.json b/tests/fixtures/captured-payments/jpy-payment.json index 6c7a6b3ee05..4b4c6c152c9 100644 --- a/tests/fixtures/captured-payments/jpy-payment.json +++ b/tests/fixtures/captured-payments/jpy-payment.json @@ -57,7 +57,7 @@ "feeBreakdown": { "base": "Base fee: 3.6%", "additional-international": "International card fee: 2%", - "additional-fx": "Foreign exchange fee: 2%" + "additional-fx": "Currency conversion fee: 2%" }, "netString": "Net payout: ¥4,507 JPY" } diff --git a/tests/fixtures/captured-payments/subscription.json b/tests/fixtures/captured-payments/subscription.json index b7312ea0c02..d0e1fe705e4 100644 --- a/tests/fixtures/captured-payments/subscription.json +++ b/tests/fixtures/captured-payments/subscription.json @@ -53,7 +53,7 @@ "feeString": "Fee (4.9% + $0.30): -$3.04", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%", + "additional-fx": "Currency conversion fee: 1%", "additional-wcpay-subscription": "Subscription transaction fee: 1%" }, "netString": "Net payout: $52.87 USD" diff --git a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-handler.php b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-handler.php new file mode 100644 index 00000000000..0b10752c0f5 --- /dev/null +++ b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-handler.php @@ -0,0 +1,136 @@ +shipping()->unregister_shipping_methods(); + + $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $this->mock_wcpay_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class ); + $this->mock_ece_button_helper = $this->createMock( WC_Payments_Express_Checkout_Button_Helper::class ); + $this->mock_express_checkout_ajax_handler = $this->createMock( WC_Payments_Express_Checkout_Ajax_Handler::class ); + + $this->system_under_test = new WC_Payments_Express_Checkout_Button_Handler( + $this->mock_wcpay_account, + $this->mock_wcpay_gateway, + $this->mock_ece_button_helper, + $this->mock_express_checkout_ajax_handler + ); + + // Set up shipping zones and methods. + $this->zone = new WC_Shipping_Zone(); + $this->zone->set_zone_name( 'Worldwide' ); + $this->zone->set_zone_order( 1 ); + $this->zone->save(); + + $flat_rate = $this->zone->add_shipping_method( 'flat_rate' ); + $this->flat_rate_id = $flat_rate; + + $local_pickup = $this->zone->add_shipping_method( 'local_pickup' ); + $this->local_pickup_id = $local_pickup; + } + + public function tear_down() { + parent::tear_down(); + + // Clean up shipping zones and methods. + $this->zone->delete(); + } + + public function test_filter_cart_needs_shipping_address_regular_products() { + $this->assertEquals( + true, + $this->system_under_test->filter_cart_needs_shipping_address( true ), + 'Should not modify shipping address requirement for regular products' + ); + } + + + public function test_filter_cart_needs_shipping_address_subscription_products() { + WC_Subscriptions_Cart::set_cart_contains_subscription( true ); + $this->mock_ece_button_helper->method( 'is_checkout' )->willReturn( true ); + + $this->zone->delete_shipping_method( $this->flat_rate_id ); + $this->zone->delete_shipping_method( $this->local_pickup_id ); + + $this->assertFalse( + $this->system_under_test->filter_cart_needs_shipping_address( true ), + 'Should not require shipping address for subscription without shipping methods' + ); + + remove_filter( 'woocommerce_shipping_method_count', '__return_zero' ); + WC_Subscriptions_Cart::set_cart_contains_subscription( false ); + } +} diff --git a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php index 2432c61172c..8006faac78f 100644 --- a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php +++ b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php @@ -28,13 +28,6 @@ class WC_Payments_Express_Checkout_Button_Helper_Test extends WCPAY_UnitTestCase */ private $mock_wcpay_account; - /** - * Express Checkout Helper instance. - * - * @var WC_Payments_Express_Checkout_Button_Helper - */ - private $express_checkout_helper; - /** * Test shipping zone. * @@ -61,21 +54,7 @@ class WC_Payments_Express_Checkout_Button_Helper_Test extends WCPAY_UnitTestCase * * @var WC_Payments_Express_Checkout_Button_Helper */ - private $mock_express_checkout_helper; - - /** - * Express Checkout Ajax Handler instance. - * - * @var WC_Payments_Express_Checkout_Ajax_Handler - */ - private $mock_express_checkout_ajax_handler; - - /** - * Express Checkout ECE Button Handler instance. - * - * @var WC_Payments_Express_Checkout_Button_Handler - */ - private $mock_express_checkout_ece_button_handler; + private $system_under_test; /** * Test product to add to the cart @@ -92,23 +71,7 @@ public function set_up() { $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); $this->mock_wcpay_gateway = $this->make_wcpay_gateway(); - $this->mock_express_checkout_helper = new WC_Payments_Express_Checkout_Button_Helper( $this->mock_wcpay_gateway, $this->mock_wcpay_account ); - $this->mock_express_checkout_ajax_handler = $this->getMockBuilder( WC_Payments_Express_Checkout_Ajax_Handler::class ) - ->setConstructorArgs( - [ - $this->mock_express_checkout_helper, - ] - ) - ->getMock(); - - $this->mock_ece_button_helper = $this->getMockBuilder( WC_Payments_Express_Checkout_Button_Helper::class ) - ->setConstructorArgs( - [ - $this->mock_wcpay_gateway, - $this->mock_wcpay_account, - ] - ) - ->getMock(); + $this->system_under_test = new WC_Payments_Express_Checkout_Button_Helper( $this->mock_wcpay_gateway, $this->mock_wcpay_account ); WC_Helper_Shipping::delete_simple_flat_rate(); $zone = new WC_Shipping_Zone(); @@ -128,7 +91,7 @@ public function set_up() { WC()->session->init(); WC()->cart->add_to_cart( $this->simple_product->get_id(), 1 ); - $this->mock_express_checkout_helper->update_shipping_method( [ self::get_shipping_option_rate_id( $this->flat_rate_id ) ] ); + $this->system_under_test->update_shipping_method( [ self::get_shipping_option_rate_id( $this->flat_rate_id ) ] ); WC()->cart->calculate_totals(); } @@ -195,34 +158,34 @@ public function test_common_get_button_settings() { 'height' => '48', 'radius' => '', ], - $this->mock_express_checkout_helper->get_common_button_settings() + $this->system_under_test->get_common_button_settings() ); } public function test_cart_prices_include_tax_with_tax_disabled() { add_filter( 'wc_tax_enabled', '__return_false' ); - $this->assertTrue( $this->mock_express_checkout_helper->cart_prices_include_tax() ); + $this->assertTrue( $this->system_under_test->cart_prices_include_tax() ); } public function test_cart_prices_include_tax_with_tax_enabled_and_display_incl() { add_filter( 'wc_tax_enabled', '__return_true' ); // reset in tear_down. add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] ); // reset in tear_down. - $this->assertTrue( $this->mock_express_checkout_helper->cart_prices_include_tax() ); + $this->assertTrue( $this->system_under_test->cart_prices_include_tax() ); } public function test_cart_prices_include_tax_with_tax_enabled_and_display_excl() { add_filter( 'wc_tax_enabled', '__return_true' ); // reset in tear_down. add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_excl' ] ); // reset in tear_down. - $this->assertFalse( $this->mock_express_checkout_helper->cart_prices_include_tax() ); + $this->assertFalse( $this->system_under_test->cart_prices_include_tax() ); } public function test_get_total_label() { $this->mock_wcpay_account->method( 'get_statement_descriptor' ) ->willReturn( 'Google Pay' ); - $result = $this->mock_express_checkout_helper->get_total_label(); + $result = $this->system_under_test->get_total_label(); $this->assertEquals( 'Google Pay (via WooCommerce)', $result ); } @@ -238,49 +201,54 @@ function () { } ); - $result = $this->mock_express_checkout_helper->get_total_label(); + $result = $this->system_under_test->get_total_label(); $this->assertEquals( 'Google Pay (via WooPayments)', $result ); remove_all_filters( 'wcpay_payment_request_total_label_suffix' ); } - public function test_filter_cart_needs_shipping_address_returns_false() { - sleep( 1 ); - $this->zone->delete_shipping_method( $this->flat_rate_id ); - $this->zone->delete_shipping_method( $this->local_pickup_id ); + public function test_should_show_express_checkout_button_for_non_shipping_but_price_includes_tax() { + $this->mock_wcpay_account + ->method( 'is_stripe_connected' ) + ->willReturn( true ); - WC_Subscriptions_Cart::set_cart_contains_subscription( true ); + WC_Payments::mode()->dev(); - $this->mock_ece_button_helper - ->method( 'is_product' ) - ->willReturn( true ); + add_filter( 'woocommerce_is_checkout', '__return_true' ); + add_filter( 'wc_shipping_enabled', '__return_false' ); + add_filter( 'wc_tax_enabled', '__return_true' ); - $this->mock_express_checkout_ece_button_handler = new WC_Payments_Express_Checkout_Button_Handler( - $this->mock_wcpay_account, - $this->mock_wcpay_gateway, - $this->mock_ece_button_helper, - $this->mock_express_checkout_ajax_handler - ); + update_option( 'woocommerce_tax_based_on', 'billing' ); + update_option( 'woocommerce_prices_include_tax', 'yes' ); - $this->assertFalse( $this->mock_express_checkout_ece_button_handler->filter_cart_needs_shipping_address( true ) ); + $this->assertTrue( $this->system_under_test->should_show_express_checkout_button() ); + + remove_filter( 'woocommerce_is_checkout', '__return_true' ); + remove_filter( 'wc_tax_enabled', '__return_true' ); + remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] ); } - public function test_filter_cart_needs_shipping_address_returns_true() { - WC_Subscriptions_Cart::set_cart_contains_subscription( true ); - $this->mock_ece_button_helper - ->method( 'is_product' ) + public function test_should_not_show_express_checkout_button_for_non_shipping_but_price_does_not_include_tax() { + $this->mock_wcpay_account + ->method( 'is_stripe_connected' ) ->willReturn( true ); - $this->mock_express_checkout_ece_button_handler = new WC_Payments_Express_Checkout_Button_Handler( - $this->mock_wcpay_account, - $this->mock_wcpay_gateway, - $this->mock_ece_button_helper, - $this->mock_express_checkout_ajax_handler - ); + WC_Payments::mode()->dev(); + + add_filter( 'woocommerce_is_checkout', '__return_true' ); + add_filter( 'wc_shipping_enabled', '__return_false' ); + add_filter( 'wc_tax_enabled', '__return_true' ); + + update_option( 'woocommerce_tax_based_on', 'billing' ); + update_option( 'woocommerce_prices_include_tax', 'no' ); - $this->assertTrue( $this->mock_express_checkout_ece_button_handler->filter_cart_needs_shipping_address( true ) ); + $this->assertFalse( $this->system_under_test->should_show_express_checkout_button() ); + + remove_filter( 'woocommerce_is_checkout', '__return_true' ); + remove_filter( 'wc_tax_enabled', '__return_true' ); + remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] ); } /** diff --git a/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php b/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php index 60e130390fd..e52927230ca 100644 --- a/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php +++ b/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php @@ -35,6 +35,20 @@ class WCPay_Multi_Currency_WooCommerceFedEx_Tests extends WCPAY_UnitTestCase { */ private $woocommerce_fedex; + /** + * Calls to check in the backtrace. + * + * @var array + */ + private $woocommerce_fedex_calls = [ + 'WC_Shipping_Fedex->set_settings', + 'WC_Shipping_Fedex->per_item_shipping', + 'WC_Shipping_Fedex->box_shipping', + 'WC_Shipping_Fedex->get_fedex_api_request', + 'WC_Shipping_Fedex->get_fedex_requests', + 'WC_Shipping_Fedex->process_result', + ]; + /** * Pre-test setup */ @@ -54,37 +68,45 @@ public function test_should_return_store_currency_returns_true_if_true_passed() // If the calls are found, it should return true. public function test_should_return_store_currency_returns_true_if_calls_found() { - $calls = [ - 'WC_Shipping_Fedex->set_settings', - 'WC_Shipping_Fedex->per_item_shipping', - 'WC_Shipping_Fedex->box_shipping', - 'WC_Shipping_Fedex->get_fedex_api_request', - 'WC_Shipping_Fedex->get_fedex_requests', - 'WC_Shipping_Fedex->process_result', - ]; $this->mock_utils ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) - ->with( $calls ) + ->with( $this->woocommerce_fedex_calls ) ->willReturn( true ); + $this->assertTrue( $this->woocommerce_fedex->should_return_store_currency( false ) ); } - // If the calls are found, it should return true. + // If the calls are not found, it should return false. public function test_should_return_store_currency_returns_false_if_no_calls_found() { - $calls = [ - 'WC_Shipping_Fedex->set_settings', - 'WC_Shipping_Fedex->per_item_shipping', - 'WC_Shipping_Fedex->box_shipping', - 'WC_Shipping_Fedex->get_fedex_api_request', - 'WC_Shipping_Fedex->get_fedex_requests', - 'WC_Shipping_Fedex->process_result', - ]; $this->mock_utils ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) - ->with( $calls ) + ->with( $this->woocommerce_fedex_calls ) ->willReturn( false ); + $this->assertFalse( $this->woocommerce_fedex->should_return_store_currency( false ) ); } + + // If true is passed to should_convert_product_price and no calls are found, it should return true. + public function test_should_convert_product_price_returns_true_if_true_passed_and_no_calls_found() { + $this->mock_utils + ->expects( $this->once() ) + ->method( 'is_call_in_backtrace' ) + ->with( $this->woocommerce_fedex_calls ) + ->willReturn( false ); + + $this->assertTrue( $this->woocommerce_fedex->should_convert_product_price( true ) ); + } + + // If calls are found, should_convert_product_price should return false even if true was passed. + public function test_should_convert_product_price_returns_false_if_calls_found() { + $this->mock_utils + ->expects( $this->once() ) + ->method( 'is_call_in_backtrace' ) + ->with( $this->woocommerce_fedex_calls ) + ->willReturn( true ); + + $this->assertFalse( $this->woocommerce_fedex->should_convert_product_price( true ) ); + } }