From 779ef47509e3907aeab9e2a0a7f693e1a9d3d5d7 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 20 Dec 2024 10:21:49 +0100 Subject: [PATCH 1/4] Translate hard coded strings in the fees tooltip breakdown (#10004) --- ...rakedown-tooltip-are-not-internationalised | 4 ++++ client/utils/account-fees.tsx | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-8620-fee-types-in-fees-brakedown-tooltip-are-not-internationalised 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/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index a1c8056f78d..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 ) ? (
-
Currency conversion fee
+
+ { __( + 'Currency conversion fee', + 'woocommerce-payments' + ) } +
{ getFeeDescriptionString( accountFees.fx ) }
) : ( '' ) }
-
Total per transaction
+
+ { __( 'Total per transaction', 'woocommerce-payments' ) } +
{ getFeeDescriptionString( total ) }
From 3c7b1155c4e3db7e8dbe5cb70cccf19a1fc40ae9 Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Fri, 20 Dec 2024 11:00:47 +0100 Subject: [PATCH 2/4] Invalidate transactions store when transaction is captured (#10015) --- .../fix-9735-render-transactions-correctly-on-capture | 4 ++++ client/data/authorizations/actions.ts | 7 +++++++ client/data/authorizations/test/actions.test.ts | 8 ++++++++ 3 files changed, 19 insertions(+) create mode 100644 changelog/fix-9735-render-transactions-correctly-on-capture 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/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', From f0ebc14465fa5f66268b22b358ab6dcde4085a58 Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Fri, 20 Dec 2024 12:48:43 +0200 Subject: [PATCH 3/4] Reduce API impact from test drive setup (#10014) Co-authored-by: Dan Paun <82826872+dpaun1985@users.noreply.github.com> --- ...837-reduce-api-impact-for-test-drive-setup | 5 +++ client/connect-account-page/index.tsx | 32 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 changelog/update-s6837-reduce-api-impact-for-test-drive-setup 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/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. From 3794508c29cdc11e37eae148ee12d58252311782 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 20 Dec 2024 12:14:15 +0100 Subject: [PATCH 4/4] Apply User-Defined Date Formatting Settings to WP Admin React Components (#9635) Co-authored-by: deepakpathania <68396823+deepakpathania@users.noreply.github.com> --- ...atting-arent-respected-in-react-components | 4 + client/capital/index.tsx | 12 +- .../test/__snapshots__/index.test.tsx.snap | 48 +++++ client/capital/test/index.test.tsx | 2 + .../account-fees/expiration-description.js | 7 +- .../account-status/account-fees/test/index.js | 1 + .../components/active-loan-summary/index.tsx | 20 +- .../test/__snapshots__/index.js.snap | 2 +- .../active-loan-summary/test/index.js | 10 + .../components/date-format-notice/index.tsx | 67 +++++++ .../components/date-format-notice/style.scss | 5 + .../deposits-overview/test/index.tsx | 2 + .../components/disputed-order-notice/index.js | 12 +- .../disputed-order-notice/test/index.test.js | 1 + client/deposits/details/index.tsx | 11 +- client/deposits/details/test/index.tsx | 2 + client/deposits/index.tsx | 2 + client/deposits/list/index.tsx | 15 +- .../list/test/__snapshots__/index.tsx.snap | 12 +- client/deposits/list/test/index.tsx | 6 +- client/deposits/utils/index.ts | 12 +- client/deposits/utils/test/index.ts | 22 +++ client/disputes/evidence/test/index.js | 3 + client/disputes/index.tsx | 32 ++-- client/disputes/info/index.tsx | 16 +- client/disputes/info/test/index.tsx | 4 + .../test/__snapshots__/index.tsx.snap | 48 +++++ client/disputes/test/index.tsx | 13 +- client/documents/index.tsx | 3 +- client/documents/list/index.tsx | 20 +- client/documents/list/test/index.tsx | 9 + client/globals.d.ts | 3 + client/overview/index.js | 2 + .../modal/update-business-details/index.tsx | 13 +- .../overview/task-list/tasks/dispute-task.tsx | 16 +- .../tasks/update-business-details-task.tsx | 10 +- client/overview/task-list/test/tasks.js | 1 + .../dispute-details/dispute-due-by-date.tsx | 12 +- .../dispute-resolution-footer.tsx | 50 ++--- .../dispute-details/dispute-steps.tsx | 27 +-- .../dispute-details/dispute-summary-row.tsx | 11 +- .../test/__snapshots__/index.test.tsx.snap | 48 +++++ .../order-details/test/index.test.tsx | 4 + .../payment-details/payment-details/index.tsx | 3 +- client/payment-details/readers/index.js | 3 +- client/payment-details/summary/index.tsx | 22 ++- .../test/__snapshots__/index.test.tsx.snap | 4 +- .../summary/test/index.test.tsx | 17 +- .../test/__snapshots__/index.test.tsx.snap | 96 ++++++++++ client/payment-details/test/index.test.tsx | 4 + client/payment-details/timeline/map-events.js | 13 +- client/payment-details/timeline/test/index.js | 1 + .../timeline/test/map-events.js | 1 + client/transactions/blocked/columns.tsx | 10 +- .../blocked/test/columns.test.tsx | 4 + client/transactions/index.tsx | 2 + client/transactions/list/deposit.tsx | 10 +- client/transactions/list/index.tsx | 10 +- .../list/test/__snapshots__/index.tsx.snap | 32 ++-- client/transactions/list/test/deposit.tsx | 20 ++ client/transactions/list/test/index.tsx | 11 ++ client/transactions/risk-review/columns.tsx | 10 +- .../risk-review/test/columns.test.tsx | 4 + client/transactions/uncaptured/index.tsx | 36 ++-- .../uncaptured/test/index.test.tsx | 4 + client/utils/date-time.ts | 82 ++++++++ client/utils/test/date-time.test.ts | 181 ++++++++++++++++++ includes/admin/class-wc-payments-admin.php | 3 + includes/class-wc-payments.php | 1 + 69 files changed, 915 insertions(+), 289 deletions(-) create mode 100644 changelog/fix-6567-user-set-date-and-time-formatting-arent-respected-in-react-components create mode 100644 client/components/date-format-notice/index.tsx create mode 100644 client/components/date-format-notice/style.scss create mode 100644 client/utils/date-time.ts create mode 100644 client/utils/test/date-time.test.ts 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/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/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 1ac643d31dc..dc3e20ac02a 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 { @@ -48,6 +46,7 @@ import CSVExportModal from 'components/csv-export-modal'; import { ReportingExportLanguageHook } from 'wcpay/settings/reporting-settings/interfaces'; import './style.scss'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; const getColumns = ( sortByDate?: boolean ): DepositsTableHeader[] => [ { @@ -140,11 +139,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 ) } ); @@ -335,11 +330,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 { reporting: { exportModalDismissed: true, }, + dateFormat: 'M j Y', }; } ); @@ -321,7 +323,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 cdb85131f5d..6e8c4d2d61b 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'; @@ -56,8 +55,9 @@ import { useSettings } from 'wcpay/data'; import { isAwaitingResponse } from 'wcpay/disputes/utils'; import CSVExportModal from 'components/csv-export-modal'; import { ReportingExportLanguageHook } from 'wcpay/settings/reporting-settings/interfaces'; - +import DateFormatNotice from 'wcpay/components/date-format-notice'; import './style.scss'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; const getHeaders = ( sortColumn?: string ): DisputesTableHeader[] => [ { @@ -201,10 +201,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 => { @@ -301,10 +300,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: { @@ -485,17 +483,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, + } ), }, ]; @@ -553,6 +552,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 1409bfc852d..37bbd2e93af 100644 --- a/client/disputes/test/index.tsx +++ b/client/disputes/test/index.tsx @@ -17,13 +17,14 @@ import { useReportingExportLanguage, 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' ); @@ -100,6 +101,8 @@ declare const global: { reporting?: { exportModalDismissed: boolean; }; + dateFormat?: string; + timeFormat?: string; }; }; @@ -198,6 +201,8 @@ describe( 'Disputes list', () => { reporting: { exportModalDismissed: true, }, + dateFormat: 'Y-m-d', + timeFormat: 'g:iA', }; } ); @@ -363,8 +368,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 0d10d7de86b..8b91ee4b05f 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; reporting: { exportModalDismissed?: boolean; }; @@ -137,6 +138,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 7e5570ce8ce..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( { 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/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 a5206ae0e5e..a4d9abc852a 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, @@ -70,6 +68,7 @@ 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 { ReportingExportLanguageHook } from 'wcpay/settings/reporting-settings/interfaces'; +import { formatDateTimeFromString } from 'wcpay/utils/date-time'; interface TransactionsListProps { depositId?: string; @@ -466,10 +465,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 b233b4d5477..66db0268a34 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -58,6 +58,15 @@ jest.mock( 'data/index', () => ( { useReportingExportLanguage: jest.fn( () => [ 'en', 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 >; @@ -244,6 +253,8 @@ describe( 'Transactions list', () => { exportModalDismissed: true, }, }; + 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/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/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 d78671d1298..392dec5c611 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -973,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 ), 'reporting' => [ 'exportModalDismissed' => get_option( 'wcpay_reporting_export_modal_dismissed', false ), ], @@ -983,6 +984,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 ); 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 );