From f6fad596b1fd6eecf6fbd5f3731dbc10ae388bd3 Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Mon, 28 Aug 2023 13:04:04 +1200 Subject: [PATCH 01/84] Disputes: Add dispute notice to transactions page (#6998) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> Co-authored-by: Shendy Kurnia Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> --- changelog/add-6923-dispute-details-notice | 3 + .../components/dispute-status-chip/index.tsx | 5 +- client/disputes/index.tsx | 11 +-- client/disputes/strings.ts | 25 +++++++ client/disputes/utils.ts | 8 +++ client/order/index.js | 5 +- .../dispute-details/dispute-notice.tsx | 71 +++++++++++++++++++ .../payment-details/dispute-details/index.tsx | 16 ++++- .../dispute-details/style.scss | 7 ++ 9 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 changelog/add-6923-dispute-details-notice create mode 100644 client/payment-details/dispute-details/dispute-notice.tsx diff --git a/changelog/add-6923-dispute-details-notice b/changelog/add-6923-dispute-details-notice new file mode 100644 index 00000000000..027a1fa2448 --- /dev/null +++ b/changelog/add-6923-dispute-details-notice @@ -0,0 +1,3 @@ +Significance: patch +Type: add +Comment: Dispute notice added to transactions screen behind a feature flag. diff --git a/client/components/dispute-status-chip/index.tsx b/client/components/dispute-status-chip/index.tsx index df3a60e888c..ccb9e11a553 100644 --- a/client/components/dispute-status-chip/index.tsx +++ b/client/components/dispute-status-chip/index.tsx @@ -11,8 +11,7 @@ import React from 'react'; import Chip from '../chip'; import displayStatus from './mappings'; import { formatStringValue } from 'utils'; -import { isDueWithin } from 'wcpay/disputes/utils'; -import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; +import { isAwaitingResponse, isDueWithin } from 'wcpay/disputes/utils'; import type { CachedDispute, DisputeStatus, @@ -27,7 +26,7 @@ const DisputeStatusChip: React.FC< Props > = ( { status, dueBy } ) => { const mapping = displayStatus[ status ] || {}; const message = mapping.message || formatStringValue( status ); - const needsResponse = disputeAwaitingResponseStatuses.includes( status ); + const needsResponse = isAwaitingResponse( status ); const isUrgent = needsResponse && dueBy && isDueWithin( { dueBy, days: 3 } ); diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index f28732ffd1b..3ba2782f82f 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -35,12 +35,12 @@ import { reasons } from './strings'; import { formatStringValue } from 'utils'; import { formatExplicitCurrency } from 'utils/currency'; import DisputesFilters from './filters'; -import { disputeAwaitingResponseStatuses } from './filters/config'; import DownloadButton from 'components/download-button'; import disputeStatusMapping from 'components/dispute-status-chip/mappings'; import { CachedDispute, DisputesTableHeader } from 'wcpay/types/disputes'; import { getDisputesCSV } from 'wcpay/data/disputes/resolvers'; import { applyThousandSeparator } from 'wcpay/utils'; +import { isAwaitingResponse } from 'wcpay/disputes/utils'; import './style.scss'; @@ -154,10 +154,7 @@ const getHeaders = ( sortColumn?: string ): DisputesTableHeader[] => [ */ const smartDueDate = ( dispute: CachedDispute ) => { // if dispute is not awaiting response, return an empty string. - if ( - dispute.due_by === '' || - ! disputeAwaitingResponseStatuses.includes( dispute.status ) - ) { + if ( dispute.due_by === '' || ! isAwaitingResponse( dispute.status ) ) { return ''; } // Get current time in UTC. @@ -224,9 +221,7 @@ export const DisputesList = (): JSX.Element => { const reasonDisplay = reasonMapping ? reasonMapping.display : formatStringValue( dispute.reason ); - const needsResponse = disputeAwaitingResponseStatuses.includes( - dispute.status - ); + const needsResponse = isAwaitingResponse( dispute.status ); const data: { [ key: string ]: { value: number | string; diff --git a/client/disputes/strings.ts b/client/disputes/strings.ts index e1cf611033a..319e086e005 100644 --- a/client/disputes/strings.ts +++ b/client/disputes/strings.ts @@ -16,6 +16,7 @@ export const reasons: Record< summary?: string[]; required?: string[]; respond?: string[]; + claim?: string; } > = { bank_cannot_process: { @@ -58,6 +59,10 @@ export const reasons: Record< 'woocommerce-payments' ), ], + claim: __( + 'The cardholder claims a credit was not processed.', + 'woocommerce-payments' + ), }, customer_initiated: { display: __( 'Customer initiated', 'woocommerce-payments' ), @@ -107,6 +112,10 @@ export const reasons: Record< 'woocommerce-payments' ), ], + claim: __( + 'The cardholder claims this is a duplicate transaction.', + 'woocommerce-payments' + ), }, fraudulent: { display: __( 'Transaction unauthorized', 'woocommerce-payments' ), @@ -146,6 +155,10 @@ export const reasons: Record< 'woocommerce-payments' ), ], + claim: __( + 'The cardholder claims this is an unauthorized transaction.', + 'woocommerce-payments' + ), }, general: { display: __( 'General', 'woocommerce-payments' ), @@ -202,6 +215,10 @@ export const reasons: Record< 'woocommerce-payments' ), ], + claim: __( + 'The cardholder claims the product was not received.', + 'woocommerce-payments' + ), }, product_unacceptable: { display: __( 'Product unacceptable', 'woocommerce-payments' ), @@ -249,6 +266,10 @@ export const reasons: Record< 'woocommerce-payments' ), ], + claim: __( + 'The cardholder claims the product was unacceptable.', + 'woocommerce-payments' + ), }, subscription_canceled: { display: __( 'Subscription canceled', 'woocommerce-payments' ), @@ -288,6 +309,10 @@ export const reasons: Record< 'woocommerce-payments' ), ], + claim: __( + 'The cardholder claims a subscription was canceled.', + 'woocommerce-payments' + ), }, unrecognized: { display: __( 'Unrecognized', 'woocommerce-payments' ), diff --git a/client/disputes/utils.ts b/client/disputes/utils.ts index 205d9ea05a0..95f59b190d9 100644 --- a/client/disputes/utils.ts +++ b/client/disputes/utils.ts @@ -10,8 +10,10 @@ import moment from 'moment'; import type { CachedDispute, Dispute, + DisputeStatus, EvidenceDetails, } from 'wcpay/types/disputes'; +import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; interface IsDueWithinProps { dueBy: CachedDispute[ 'due_by' ] | EvidenceDetails[ 'due_by' ]; @@ -50,6 +52,12 @@ export const isDueWithin = ( { dueBy, days }: IsDueWithinProps ): boolean => { return isWithinDays && ! isPastDue; }; +export const isAwaitingResponse = ( + status: DisputeStatus | string +): boolean => { + return disputeAwaitingResponseStatuses.includes( status ); +}; + export const isInquiry = ( dispute: Dispute | CachedDispute ): boolean => { // Inquiry dispute statuses are one of `warning_needs_response`, `warning_under_review` or `warning_closed`. return dispute.status.startsWith( 'warning' ); diff --git a/client/order/index.js b/client/order/index.js index 82c37fb75b9..7e07716f6d1 100644 --- a/client/order/index.js +++ b/client/order/index.js @@ -16,8 +16,7 @@ import BannerNotice from 'wcpay/components/banner-notice'; import { formatExplicitCurrency } from 'utils/currency'; import { reasons } from 'wcpay/disputes/strings'; import { getDetailsURL } from 'wcpay/components/details-link'; -import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; -import { isInquiry } from 'wcpay/disputes/utils'; +import { isAwaitingResponse, isInquiry } from 'wcpay/disputes/utils'; import { useCharge } from 'wcpay/data'; import wcpayTracks from 'tracks'; import './style.scss'; @@ -134,7 +133,7 @@ const DisputeNotice = ( { chargeId } ) => { ! charge?.dispute || ! charge?.dispute?.evidence_details?.due_by || // Only show the notice if the dispute is awaiting a response. - ! disputeAwaitingResponseStatuses.includes( charge?.dispute?.status ) + ! isAwaitingResponse( charge.dispute.status ) ) { return null; } diff --git a/client/payment-details/dispute-details/dispute-notice.tsx b/client/payment-details/dispute-details/dispute-notice.tsx new file mode 100644 index 00000000000..747f5343c96 --- /dev/null +++ b/client/payment-details/dispute-details/dispute-notice.tsx @@ -0,0 +1,71 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import { createInterpolateElement } from '@wordpress/element'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +/** + * Internal dependencies + */ +import './style.scss'; +import BannerNotice from 'components/banner-notice'; +import { reasons } from 'wcpay/disputes/strings'; +import { Dispute } from 'wcpay/types/disputes'; +import { isInquiry } from 'wcpay/disputes/utils'; + +interface DisputeNoticeProps { + dispute: Dispute; + urgent: boolean; +} + +const DisputeNotice: React.FC< DisputeNoticeProps > = ( { + dispute, + urgent, +} ) => { + const clientClaim = + reasons[ dispute.reason ]?.claim ?? + __( + 'The cardholder claims this is an unrecognized charge.', + 'woocommerce-payments' + ); + + const noticeText = isInquiry( dispute ) + ? /* translators: link to dispute documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */ + __( + // eslint-disable-next-line max-len + '%s You can challenge their claim if you believe it’s invalid. Not responding will result in an automatic loss. Learn more', + 'woocommerce-payments' + ) + : /* translators: link to dispute documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */ + __( + // eslint-disable-next-line max-len + '%s Challenge the dispute if you believe the claim is invalid, or accept to forfeit the funds and pay the dispute fee. Non-response will result in an automatic loss. Learn more about responding to disputes', + 'woocommerce-payments' + ); + + return ( + } + className="dispute-notice" + isDismissible={ false } + > + { createInterpolateElement( sprintf( noticeText, clientClaim ), { + a: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + strong: , + } ) } + + ); +}; + +export default DisputeNotice; diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx index 9b710d7050d..6f8d5e7b803 100644 --- a/client/payment-details/dispute-details/index.tsx +++ b/client/payment-details/dispute-details/index.tsx @@ -4,22 +4,36 @@ * External dependencies */ import React from 'react'; +import moment from 'moment'; /** * Internal dependencies */ import type { Dispute } from 'wcpay/types/disputes'; import { Card, CardBody } from '@wordpress/components'; import './style.scss'; +import DisputeNotice from './dispute-notice'; +import { isAwaitingResponse } from 'wcpay/disputes/utils'; interface DisputeDetailsProps { dispute: Dispute; } const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { + const now = moment(); + const dueBy = moment.unix( dispute.evidence_details?.due_by ?? 0 ); + const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) ); + return (
- + + { isAwaitingResponse( dispute.status ) && + countdownDays >= 0 && ( + + ) }
diff --git a/client/payment-details/dispute-details/style.scss b/client/payment-details/dispute-details/style.scss index 8a4ec037835..f17f454a3b1 100644 --- a/client/payment-details/dispute-details/style.scss +++ b/client/payment-details/dispute-details/style.scss @@ -4,4 +4,11 @@ padding-left: 24px; padding-right: 24px; padding-bottom: 5px; + + .transaction-details-dispute-details-body { + padding: $grid-unit-20; + } +} +.wcpay-banner-notice.dispute-notice { + margin: 0; } From 98b6f79aabd2d8bdebbed2a029ca748b18b65d72 Mon Sep 17 00:00:00 2001 From: Eduardo Pieretti Umpierre Date: Mon, 28 Aug 2023 17:44:14 -0300 Subject: [PATCH 02/84] Update multi currency documentation links (#7072) --- changelog/update-6186-mc-settings-links | 4 ++++ .../multi-currency-settings/enabled-currencies-list/index.js | 2 +- .../enabled-currencies-list/test/__snapshots__/index.js.snap | 2 +- .../multi-currency-settings/store-settings/index.js | 2 +- .../store-settings/test/__snapshots__/index.test.js.snap | 2 +- client/multi-currency/single-currency-settings/index.js | 4 ++-- includes/multi-currency/SettingsOnboardCta.php | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelog/update-6186-mc-settings-links diff --git a/changelog/update-6186-mc-settings-links b/changelog/update-6186-mc-settings-links new file mode 100644 index 00000000000..3bff5d53ef6 --- /dev/null +++ b/changelog/update-6186-mc-settings-links @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Multi-currency documentation links. diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js b/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js index d66585a26ee..8d08b90ef11 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/index.js @@ -25,7 +25,7 @@ import SettingsSection from 'wcpay/settings/settings-section'; const EnabledCurrenciesSettingsDescription = () => { const LEARN_MORE_URL = - 'https://woocommerce.com/document/woocommerce-payments/currencies/multi-currency-setup/'; + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#enabled-currencies'; return ( <> diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap b/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap index ffc13bc4aba..f4a0e2195bc 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap @@ -14,7 +14,7 @@ exports[`Multi-Currency enabled currencies list Enabled currencies list renders

Accept payments in multiple currencies. Prices are converted based on exchange rates and rounding rules. diff --git a/client/multi-currency/multi-currency-settings/store-settings/index.js b/client/multi-currency/multi-currency-settings/store-settings/index.js index f5f23d82054..ccb8b8f4818 100644 --- a/client/multi-currency/multi-currency-settings/store-settings/index.js +++ b/client/multi-currency/multi-currency-settings/store-settings/index.js @@ -18,7 +18,7 @@ import PreviewModal from 'wcpay/multi-currency/preview-modal'; const StoreSettingsDescription = () => { const LEARN_MORE_URL = - 'https://woocommerce.com/document/woocommerce-payments/currencies/multi-currency-setup/'; + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#store-settings'; return ( <> diff --git a/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap b/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap index 74491d62932..b0ddd6f2af1 100644 --- a/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap +++ b/client/multi-currency/multi-currency-settings/store-settings/test/__snapshots__/index.test.js.snap @@ -14,7 +14,7 @@ exports[`Multi-Currency store settings store settings task renders correctly: sn

Store settings allow your customers to choose which currency they would like to use when shopping at your store. diff --git a/client/multi-currency/single-currency-settings/index.js b/client/multi-currency/single-currency-settings/index.js index 39c3b94ef0e..d637df3789b 100644 --- a/client/multi-currency/single-currency-settings/index.js +++ b/client/multi-currency/single-currency-settings/index.js @@ -412,7 +412,7 @@ const SingleCurrencySettings = () => { onClick={ () => { open( /* eslint-disable-next-line max-len */ - 'https://woocommerce.com/document/woocommerce-payments/currencies/multi-currency-setup/#price-rounding', + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#price-rounding', '_blank' ); } } @@ -477,7 +477,7 @@ const SingleCurrencySettings = () => { onClick={ () => { open( /* eslint-disable-next-line max-len */ - 'https://woocommerce.com/document/woocommerce-payments/currencies/multi-currency-setup/#charm-pricing', + 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/#charm-pricing', '_blank' ); } } diff --git a/includes/multi-currency/SettingsOnboardCta.php b/includes/multi-currency/SettingsOnboardCta.php index 83ba7f12a40..dbe7fa15fb7 100644 --- a/includes/multi-currency/SettingsOnboardCta.php +++ b/includes/multi-currency/SettingsOnboardCta.php @@ -18,7 +18,7 @@ class SettingsOnboardCta extends \WC_Settings_Page { * * @var string */ - const LEARN_MORE_URL = 'https://woocommerce.com/document/woocommerce-payments/currencies/multi-currency-setup/'; + const LEARN_MORE_URL = 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/'; /** * MultiCurrency instance. From e9baed092efb354e0280a9a34fc4dc0e7566cd07 Mon Sep 17 00:00:00 2001 From: Eduardo Pieretti Umpierre Date: Mon, 28 Aug 2023 17:49:21 -0300 Subject: [PATCH 03/84] Fix Currency Switcher Block flag rendering on Windows platform (#6832) --- .../fix-6192-currency-switcher-block-on-windows | 4 ++++ client/multi-currency/blocks/currency-switcher.js | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-6192-currency-switcher-block-on-windows diff --git a/changelog/fix-6192-currency-switcher-block-on-windows b/changelog/fix-6192-currency-switcher-block-on-windows new file mode 100644 index 00000000000..ba18cbf9042 --- /dev/null +++ b/changelog/fix-6192-currency-switcher-block-on-windows @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Currency Switcher Block flag rendering on Windows platform. diff --git a/client/multi-currency/blocks/currency-switcher.js b/client/multi-currency/blocks/currency-switcher.js index 02a23a68f36..5dee2d130be 100644 --- a/client/multi-currency/blocks/currency-switcher.js +++ b/client/multi-currency/blocks/currency-switcher.js @@ -130,6 +130,16 @@ registerBlockType( 'woocommerce-payments/multi-currency-switcher', { // eslint-disable-next-line react-hooks/rules-of-hooks const blockProps = useBlockProps(); + /** + * WP Emoji replaces the flag emoji with an image if it's not natively + * supported by the browser. This behavior is problematic on Windows + * because it renders an tag inside the - { action.label } - - ); - } - - return ( - - ); - } ); - - // We'll render the actions ourselves so we need to remove them from the props sent to the notice component. - delete noticeProps.actions; - } - - return ( - - - { icon && ( - - - - ) } - - { noticeProps.children } - { actions && ( - - { actions } - - ) } - - - - ); -} - -export default BannerNotice; diff --git a/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap b/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap deleted file mode 100644 index c7b2599f363..00000000000 --- a/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,198 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Info BannerNotices renders with dismiss 1`] = ` -

-
-
-
-
- Test notice content -
-
-
-
- -
-
-`; - -exports[`Info BannerNotices renders with dismiss and icon 1`] = ` -
-
-
-
-
- -
-
- Test notice content -
-
-
-
- -
-
-`; - -exports[`Info BannerNotices renders with dismiss and icon and actions 1`] = ` -
-
-
-
-
- Test notice content -
- - - URL - -
-
-
-
-
- -
-
-`; - -exports[`Info BannerNotices renders without dismiss and icon 1`] = ` -
-
-
-
-
- Test notice content -
-
-
-
-
-
-`; diff --git a/client/components/currency-information-for-methods/index.js b/client/components/currency-information-for-methods/index.js index 801af83e6db..38ca0b133cf 100644 --- a/client/components/currency-information-for-methods/index.js +++ b/client/components/currency-information-for-methods/index.js @@ -11,7 +11,7 @@ import interpolateComponents from '@automattic/interpolate-components'; */ import { useCurrencies, useEnabledCurrencies } from '../../data'; import WCPaySettingsContext from '../../settings/wcpay-settings-context'; -import InlineNotice from '../inline-notice'; +import InlineNotice from 'components/inline-notice'; import PaymentMethodsMap from '../../payment-methods-map'; const ListToCommaSeparatedSentencePartConverter = ( items ) => { @@ -90,7 +90,7 @@ const CurrencyInformationForMethods = ( { selectedMethods } ) => { if ( missingCurrencyLabels.length > 0 ) { return ( - + { interpolateComponents( { mixedString: sprintf( __( diff --git a/client/components/deposits-overview/next-deposit.tsx b/client/components/deposits-overview/next-deposit.tsx index 4c17b018e93..39a84131231 100644 --- a/client/components/deposits-overview/next-deposit.tsx +++ b/client/components/deposits-overview/next-deposit.tsx @@ -10,7 +10,6 @@ import { Icon, } from '@wordpress/components'; import { calendar } from '@wordpress/icons'; -import InfoOutlineIcon from 'gridicons/dist/info-outline'; import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; import interpolateComponents from '@automattic/interpolate-components'; import { __, sprintf } from '@wordpress/i18n'; @@ -23,7 +22,7 @@ import { getNextDeposit } from './utils'; import DepositStatusChip from 'components/deposit-status-chip'; import { getDepositDate } from 'deposits/utils'; import { useAllDepositsOverviews, useDepositIncludesLoan } from 'wcpay/data'; -import BannerNotice from 'wcpay/components/banner-notice'; +import InlineNotice from 'components/inline-notice'; import { useSelectedCurrency } from 'wcpay/overview/hooks'; import type * as AccountOverview from 'wcpay/types/account-overview'; @@ -33,11 +32,7 @@ type NextDepositProps = { }; const DepositIncludesLoanPayoutNotice = () => ( - } - isDismissible={ false } - > + { interpolateComponents( { mixedString: __( 'This deposit will include funds from your WooCommerce Capital loan. {{learnMoreLink}}Learn more{{/learnMoreLink}}', @@ -57,13 +52,13 @@ const DepositIncludesLoanPayoutNotice = () => ( ), }, } ) } - + ); const NewAccountWaitingPeriodNotice = () => ( - } + icon className="new-account-waiting-period-notice" isDismissible={ false } > @@ -84,13 +79,13 @@ const NewAccountWaitingPeriodNotice = () => ( ), }, } ) } - + ); const NegativeBalanceDepositsPausedNotice = () => ( - } + icon className="negative-balance-deposits-paused-notice" isDismissible={ false } > @@ -115,7 +110,7 @@ const NegativeBalanceDepositsPausedNotice = () => ( ), }, } ) } - + ); /** diff --git a/client/components/deposits-overview/recent-deposits-list.tsx b/client/components/deposits-overview/recent-deposits-list.tsx index 53811b8fcb1..a0cbfea5fc2 100644 --- a/client/components/deposits-overview/recent-deposits-list.tsx +++ b/client/components/deposits-overview/recent-deposits-list.tsx @@ -11,7 +11,6 @@ import { } from '@wordpress/components'; import { calendar } from '@wordpress/icons'; import { Link } from '@woocommerce/components'; -import InfoOutlineIcon from 'gridicons/dist/info-outline'; import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -24,7 +23,7 @@ import { getDepositDate } from 'deposits/utils'; import { CachedDeposit } from 'wcpay/types/deposits'; import { formatCurrency } from 'wcpay/utils/currency'; import { getDetailsURL } from 'wcpay/components/details-link'; -import BannerNotice from '../banner-notice'; +import InlineNotice from '../inline-notice'; interface DepositRowProps { deposit: CachedDeposit; @@ -89,10 +88,10 @@ const RecentDepositsList: React.FC< RecentDepositsProps > = ( { { deposit.id === oldestPendingDepositId && ( - } + icon children={ 'Deposits pending or in-transit may take 1-2 business days to appear in your bank account once dispatched' } diff --git a/client/components/deposits-overview/style.scss b/client/components/deposits-overview/style.scss index 7be1679ced9..9b2c500af31 100644 --- a/client/components/deposits-overview/style.scss +++ b/client/components/deposits-overview/style.scss @@ -19,7 +19,7 @@ } } } - .wcpay-banner-notice.components-notice { + .wcpay-inline-notice.components-notice { margin: 0; } @@ -27,7 +27,7 @@ // in the notices container and to the business delay // notice if it's the last child of the Deposit history table. &__notices__container - > .wcpay-banner-notice.components-notice:not( :last-child ), + > .wcpay-inline-notice.components-notice:not( :last-child ), .wcpay-deposits-overview__business-day-delay-notice:last-child { margin-bottom: 16px; } diff --git a/client/components/deposits-overview/suspended-deposit-notice.tsx b/client/components/deposits-overview/suspended-deposit-notice.tsx index 1de4f17af92..9ba9c489593 100644 --- a/client/components/deposits-overview/suspended-deposit-notice.tsx +++ b/client/components/deposits-overview/suspended-deposit-notice.tsx @@ -9,8 +9,7 @@ import { Link } from '@woocommerce/components'; /** * Internal dependencies */ -import BannerNotice from 'components/banner-notice'; -import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import InlineNotice from 'components/inline-notice'; /** * Renders a notice informing the user that their deposits are suspended. @@ -19,9 +18,9 @@ import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; */ function SuspendedDepositNotice(): JSX.Element { return ( - } + icon isDismissible={ false } status="warning" > @@ -42,7 +41,7 @@ function SuspendedDepositNotice(): JSX.Element { ), }, } ) } - + ); } diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index f042f06fa24..fa49730a55e 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -325,7 +325,7 @@ exports[`Deposits Overview information Component Renders 1`] = `
@@ -355,7 +355,7 @@ exports[`Deposits Overview information Component Renders 1`] = `
@@ -412,7 +412,7 @@ exports[`Deposits Overview information Component Renders 1`] = ` exports[`Suspended Deposit Notice Renders Component Renders 1`] = `
@@ -442,7 +442,7 @@ exports[`Suspended Deposit Notice Renders Component Renders 1`] = `
diff --git a/client/components/error-boundary/index.js b/client/components/error-boundary/index.js index cc618fa5b91..6fba86a54d9 100644 --- a/client/components/error-boundary/index.js +++ b/client/components/error-boundary/index.js @@ -3,7 +3,7 @@ */ import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import InlineNotice from '../inline-notice'; +import InlineNotice from 'components/inline-notice'; class ErrorBoundary extends Component { constructor() { @@ -30,7 +30,7 @@ class ErrorBoundary extends Component { } return ( - + { __( 'There was an error rendering this view. Please contact support for assistance if the problem persists.', 'woocommerce-payments' diff --git a/client/components/inline-notice/index.tsx b/client/components/inline-notice/index.tsx index af55ee519f8..d0c59812e96 100644 --- a/client/components/inline-notice/index.tsx +++ b/client/components/inline-notice/index.tsx @@ -1,23 +1,111 @@ /** * External dependencies */ -import React from 'react'; -import { Notice } from '@wordpress/components'; +import * as React from 'react'; +import { Flex, FlexItem, Icon, Notice, Button } from '@wordpress/components'; import classNames from 'classnames'; +import CheckmarkIcon from 'gridicons/dist/checkmark'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import InfoOutlineIcon from 'gridicons/dist/info-outline'; /** - * Internal dependencies + * Internal dependencies. */ -import './style.scss'; - -const InlineNotice: React.FunctionComponent< Notice.Props > = ( { - className, - ...restProps -} ) => ( - -); +import './styles.scss'; + +interface InlineNoticeProps extends Notice.Props { + /** + * Whether to display the default icon based on status prop or the icon to display. + * Supported values are: boolean, JSX.Element and `undefined`. + * + * @default undefined + */ + icon?: boolean | JSX.Element; +} + +/** + * Renders a banner notice. + */ +function InlineNotice( props: InlineNoticeProps ): JSX.Element { + const { icon, actions, children, ...noticeProps } = props; + + // Add the default class name to the notice. + noticeProps.className = classNames( + 'wcpay-inline-notice', + `wcpay-inline-${ noticeProps.status }-notice`, + noticeProps.className + ); + + // Use default icon based on status if icon === true. + let iconToDisplay = icon; + if ( iconToDisplay === true ) { + switch ( noticeProps.status ) { + case 'success': + iconToDisplay = ; + break; + case 'error': + case 'warning': + iconToDisplay = ; + break; + case 'info': + default: + iconToDisplay = ; + break; + } + } + + // Convert the notice actions to buttons or link elements. + const actionClass = 'wcpay-inline-notice__action'; + const mappedActions = actions?.map( ( action, index ) => { + // Actions that contain a URL will be rendered as a link. + // This matches WP Notice component behavior. + if ( 'url' in action ) { + return ( + + { action.label } + + ); + } + + return ( + + ); + } ); + + return ( + + + { iconToDisplay && ( + + + + ) } + + { children } + { mappedActions && ( + + { mappedActions } + + ) } + + + + ); +} export default InlineNotice; diff --git a/client/components/inline-notice/style.scss b/client/components/inline-notice/style.scss deleted file mode 100644 index 5f2f855d2cc..00000000000 --- a/client/components/inline-notice/style.scss +++ /dev/null @@ -1,11 +0,0 @@ -.wcpay-inline-notice { - // increasing the specificity of the styles to override the Gutenberg ones - &#{&} { - margin: 0; - margin-bottom: $grid-unit-30; - } - - &.is-info { - background: #def1f7; - } -} diff --git a/client/components/banner-notice/styles.scss b/client/components/inline-notice/styles.scss similarity index 71% rename from client/components/banner-notice/styles.scss rename to client/components/inline-notice/styles.scss index fd39111e0c8..32c77b618d4 100644 --- a/client/components/banner-notice/styles.scss +++ b/client/components/inline-notice/styles.scss @@ -7,14 +7,25 @@ $is-error-hover: #b30f0f; $is-success: #00a32a; $is-success-hover: #00982a; -.wcpay-banner-notice.components-notice { +.wcpay-inline-notice.components-notice { + margin: $gap-large 0; padding: 11px 0 11px 17px; border-left: none; border-radius: 2px; justify-content: flex-start; + /* Margin exceptions */ + @at-root .components-modal__header + &, + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + /* Shared styles for all variants */ - .wcpay-banner-notice__icon { + .wcpay-inline-notice__icon { display: flex; align-items: center; align-self: flex-start; @@ -30,10 +41,10 @@ $is-success-hover: #00982a; margin-top: 2px; margin-bottom: 2px; } - .wcpay-banner-notice__content__actions { + .wcpay-inline-notice__content__actions { padding-top: 12px; } - a.wcpay-banner-notice__action { + a.wcpay-inline-notice__action { text-decoration: none; } &.is-dismissible { @@ -52,16 +63,16 @@ $is-success-hover: #00982a; /* Specific styles for each variant */ &.is-info { background-color: #f0f6fc; - .wcpay-banner-notice__icon svg { + .wcpay-inline-notice__icon svg { fill: $is-info; } - button.wcpay-banner-notice__action { + button.wcpay-inline-notice__action { box-shadow: inset 0 0 0 1px $is-info; &:hover { box-shadow: inset 0 0 0 1px $is-info-hover; } } - .wcpay-banner-notice__action { + .wcpay-inline-notice__action { color: $is-info; &:hover { color: $is-info-hover; @@ -70,16 +81,16 @@ $is-success-hover: #00982a; } &.is-warning { background-color: #fcf9e8; - .wcpay-banner-notice__icon svg { + .wcpay-inline-notice__icon svg { fill: $is-warning; } - button.wcpay-banner-notice__action { + button.wcpay-inline-notice__action { box-shadow: inset 0 0 0 1px $is-warning; &:hover { box-shadow: inset 0 0 0 1px $is-warning-hover; } } - .wcpay-banner-notice__action { + .wcpay-inline-notice__action { color: $is-warning; &:hover { color: $is-warning-hover; @@ -88,16 +99,16 @@ $is-success-hover: #00982a; } &.is-error { background-color: #fcf0f1; - .wcpay-banner-notice__icon svg { + .wcpay-inline-notice__icon svg { fill: $is-error; } - button.wcpay-banner-notice__action { + button.wcpay-inline-notice__action { box-shadow: inset 0 0 0 1px $is-error; &:hover { box-shadow: inset 0 0 0 1px $is-error-hover; } } - .wcpay-banner-notice__action { + .wcpay-inline-notice__action { color: $is-error; &:hover { color: $is-error-hover; @@ -106,16 +117,16 @@ $is-success-hover: #00982a; } &.is-success { background-color: #edfaef; - .wcpay-banner-notice__icon svg { + .wcpay-inline-notice__icon svg { fill: $is-success; } - button.wcpay-banner-notice__action { + button.wcpay-inline-notice__action { box-shadow: inset 0 0 0 1px $is-success; &:hover { box-shadow: inset 0 0 0 1px $is-success-hover; } } - .wcpay-banner-notice__action { + .wcpay-inline-notice__action { color: $is-success; &:hover { color: $is-success-hover; diff --git a/client/components/inline-notice/tests/__snapshots__/index.test.tsx.snap b/client/components/inline-notice/tests/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..ed2038c16b5 --- /dev/null +++ b/client/components/inline-notice/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,293 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Info InlineNotices renders with dismiss 1`] = ` +
+
+
+
+
+ Test notice content +
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders with dismiss and icon 1`] = ` +
+
+
+
+
+ + + + + +
+
+ Test notice content +
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders with dismiss and icon and actions 1`] = ` +
+
+
+
+
+ + + + + +
+
+ Test notice content +
+ + + URL + +
+
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders with no status and custom icon 1`] = ` +
+
+
+
+
+ + + + + +
+
+ Test notice content +
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders without dismiss and icon 1`] = ` +
+
+
+
+
+ Test notice content +
+
+
+
+
+
+`; diff --git a/client/components/banner-notice/tests/index.test.tsx b/client/components/inline-notice/tests/index.test.tsx similarity index 86% rename from client/components/banner-notice/tests/index.test.tsx rename to client/components/inline-notice/tests/index.test.tsx index 3a98e3363b3..23c26860cc8 100644 --- a/client/components/banner-notice/tests/index.test.tsx +++ b/client/components/inline-notice/tests/index.test.tsx @@ -3,16 +3,17 @@ */ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; +import AddIcon from 'gridicons/dist/add'; /** * Internal dependencies */ -import BannerNotice from '../'; +import InlineNotice from '..'; -describe( 'Info BannerNotices renders', () => { +describe( 'Info InlineNotices renders', () => { test( 'with dismiss', () => { const { container } = render( - { test( 'with dismiss and icon', () => { const { container } = render( - @@ -36,8 +37,9 @@ describe( 'Info BannerNotices renders', () => { test( 'with dismiss and icon and actions', () => { const { container } = render( - { test( 'without dismiss and icon', () => { const { container } = render( - { ); expect( container ).toMatchSnapshot(); } ); + + test( 'with no status and custom icon', () => { + const { container } = render( + } + children={ 'Test notice content' } + /> + ); + expect( container ).toMatchSnapshot(); + } ); } ); describe( 'Action click triggers callback', () => { test( 'with dismiss and icon and actions', () => { const onClickMock = jest.fn(); const { getByText } = render( - { const onButtonClickOne = jest.fn(); const onButtonClickTwo = jest.fn(); const { getByText } = render( - { test( 'with dismiss and icon and actions', () => { const onDismissMock = jest.fn(); const { getByLabelText } = render( - { const [ hidden, setHidden ] = React.useState( false ); if ( hidden || ! wcpaySettings.onboardingFlowState ) return null; return ( - setHidden( true ) } > { strings.restoredState } - + ); }; diff --git a/client/onboarding/steps/personal-details.tsx b/client/onboarding/steps/personal-details.tsx index 292f68a0c71..0192306ef02 100644 --- a/client/onboarding/steps/personal-details.tsx +++ b/client/onboarding/steps/personal-details.tsx @@ -3,14 +3,13 @@ */ import React from 'react'; import { Flex, FlexBlock } from '@wordpress/components'; -import { info } from '@wordpress/icons'; /** * Internal dependencies */ import strings from '../strings'; import { OnboardingTextField, OnboardingPhoneNumberField } from '../form'; -import BannerNotice from 'components/banner-notice'; +import InlineNotice from 'components/inline-notice'; const PersonalDetails: React.FC = () => { return ( @@ -25,9 +24,9 @@ const PersonalDetails: React.FC = () => { - + { strings.steps.personal.notice } - + ); }; diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 68879da2039..89908c25d93 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -106,7 +106,7 @@ body.wcpay-onboarding__body { padding: $gap-small $gap; } - .wcpay-banner-notice { + .wcpay-inline-notice { margin: 0; } diff --git a/client/order/index.js b/client/order/index.js index 7e07716f6d1..fd3ed472481 100644 --- a/client/order/index.js +++ b/client/order/index.js @@ -12,7 +12,7 @@ import moment from 'moment'; import { getConfig } from 'utils/order'; import RefundConfirmationModal from './refund-confirm-modal'; import CancelConfirmationModal from './cancel-confirm-modal'; -import BannerNotice from 'wcpay/components/banner-notice'; +import InlineNotice from 'components/inline-notice'; import { formatExplicitCurrency } from 'utils/currency'; import { reasons } from 'wcpay/disputes/strings'; import { getDetailsURL } from 'wcpay/components/details-link'; @@ -221,7 +221,7 @@ const DisputeNotice = ( { chargeId } ) => { suffix = __( '(Last day today)', 'woocommerce-payments' ); } return ( - { { title } { suffix } - + ); }; diff --git a/client/order/style.scss b/client/order/style.scss index 665554de744..d67a64db9ad 100644 --- a/client/order/style.scss +++ b/client/order/style.scss @@ -1,4 +1,4 @@ -/* Overrides for dispute BannerNotice used in order edit screen. */ -#wcpay-order-payment-details-container .wcpay-banner-notice { +/* Overrides for dispute InlineNotice used in order edit screen. */ +#wcpay-order-payment-details-container .wcpay-inline-notice { margin: 24px 0 6px 0; } diff --git a/client/payment-details/dispute-details/dispute-notice.tsx b/client/payment-details/dispute-details/dispute-notice.tsx index 747f5343c96..44c3286704b 100644 --- a/client/payment-details/dispute-details/dispute-notice.tsx +++ b/client/payment-details/dispute-details/dispute-notice.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; -import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; + /** * Internal dependencies */ import './style.scss'; -import BannerNotice from 'components/banner-notice'; +import InlineNotice from 'components/inline-notice'; import { reasons } from 'wcpay/disputes/strings'; import { Dispute } from 'wcpay/types/disputes'; import { isInquiry } from 'wcpay/disputes/utils'; @@ -47,9 +47,9 @@ const DisputeNotice: React.FC< DisputeNoticeProps > = ( { ); return ( - } className="dispute-notice" isDismissible={ false } > @@ -64,7 +64,7 @@ const DisputeNotice: React.FC< DisputeNoticeProps > = ( { ), strong: , } ) } - + ); }; diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx index 6f8d5e7b803..6872a10ed11 100644 --- a/client/payment-details/dispute-details/index.tsx +++ b/client/payment-details/dispute-details/index.tsx @@ -34,7 +34,6 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { urgent={ countdownDays <= 2 } /> ) } -
diff --git a/client/payment-details/dispute-details/style.scss b/client/payment-details/dispute-details/style.scss index f17f454a3b1..c9676e19bc2 100644 --- a/client/payment-details/dispute-details/style.scss +++ b/client/payment-details/dispute-details/style.scss @@ -9,6 +9,3 @@ padding: $grid-unit-20; } } -.wcpay-banner-notice.dispute-notice { - margin: 0; -} diff --git a/client/settings/disable-upe-modal/index.js b/client/settings/disable-upe-modal/index.js index 8b5992f771b..c41ebc55218 100644 --- a/client/settings/disable-upe-modal/index.js +++ b/client/settings/disable-upe-modal/index.js @@ -14,13 +14,13 @@ import './style.scss'; import ConfirmationModal from 'components/confirmation-modal'; import useIsUpeEnabled from 'settings/wcpay-upe-toggle/hook'; import WcPayUpeContext from 'settings/wcpay-upe-toggle/context'; -import InlineNotice from '../../components/inline-notice'; +import InlineNotice from 'components/inline-notice'; import { useEnabledPaymentMethodIds } from '../../data'; import PaymentMethodIcon from '../payment-method-icon'; const NeedHelpBarSection = () => { return ( - + { interpolateComponents( { mixedString: __( 'Need help? Visit {{ docsLink /}} or {{supportLink /}}.', diff --git a/client/settings/express-checkout-settings/payment-request-button-preview.js b/client/settings/express-checkout-settings/payment-request-button-preview.js index 61472d48c91..ed5a865fee0 100644 --- a/client/settings/express-checkout-settings/payment-request-button-preview.js +++ b/client/settings/express-checkout-settings/payment-request-button-preview.js @@ -152,7 +152,7 @@ const PaymentRequestButtonPreview = () => {
) } { ! isWooPayEnabled && ! isPaymentRequestEnabled && ( - + { __( 'To preview the express checkout buttons, ' + 'activate at least one express checkout.', @@ -161,7 +161,7 @@ const PaymentRequestButtonPreview = () => { ) } { isPaymentRequestEnabled && ! isLoading && ! paymentRequest && ( - + { __( 'To preview the Apple Pay and Google Pay buttons, ' + 'ensure your device is configured to accept Apple Pay or Google Pay, ' + diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap index 492f9cde67e..3f68a763b88 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap @@ -46,7 +46,7 @@ exports[`CVC verification card renders correctly when CVC check is disabled 1`]

@@ -76,7 +76,7 @@ exports[`CVC verification card renders correctly when CVC check is disabled 1`]
@@ -152,7 +152,7 @@ exports[`CVC verification card renders correctly when CVC check is enabled 1`] =

@@ -182,7 +182,7 @@ exports[`CVC verification card renders correctly when CVC check is enabled 1`] =
diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap index 5347955901d..55d7b04c106 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap @@ -103,7 +103,7 @@ exports[`International IP address card renders correctly 1`] = `

@@ -133,7 +133,7 @@ exports[`International IP address card renders correctly 1`] = `
@@ -267,7 +267,7 @@ exports[`International IP address card renders correctly when enabled 1`] = `

@@ -297,7 +297,7 @@ exports[`International IP address card renders correctly when enabled 1`] = `
@@ -431,7 +431,7 @@ exports[`International IP address card renders correctly when enabled and checke

@@ -461,7 +461,7 @@ exports[`International IP address card renders correctly when enabled and checke
@@ -594,7 +594,7 @@ exports[`International IP address card renders like disabled when checked, but n

@@ -624,7 +624,7 @@ exports[`International IP address card renders like disabled when checked, but n
diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap index 000b975c4a2..05d4acfd78d 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap @@ -263,7 +263,7 @@ exports[`Order items threshold card renders correctly when enabled 1`] = `

@@ -293,7 +293,7 @@ exports[`Order items threshold card renders correctly when enabled 1`] = `
@@ -494,7 +494,7 @@ exports[`Order items threshold card renders correctly when enabled and checked 1

@@ -524,7 +524,7 @@ exports[`Order items threshold card renders correctly when enabled and checked 1
diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap index c30d4bd6e8e..b702ef97cc5 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap @@ -265,7 +265,7 @@ exports[`Purchase price threshold card renders correctly when enabled 1`] = `

@@ -295,7 +295,7 @@ exports[`Purchase price threshold card renders correctly when enabled 1`] = `
@@ -498,7 +498,7 @@ exports[`Purchase price threshold card renders correctly when enabled and checke

@@ -528,7 +528,7 @@ exports[`Purchase price threshold card renders correctly when enabled and checke
diff --git a/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx b/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx index 2c523993c28..30d2f6db72b 100644 --- a/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx +++ b/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx @@ -8,7 +8,7 @@ import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; * Internal dependencies */ import './../style.scss'; -import BannerNotice from 'wcpay/components/banner-notice'; +import InlineNotice from 'components/inline-notice'; import { TipIcon } from 'wcpay/icons'; const supportedTypes = [ 'error', 'warning', 'info' ] as const; @@ -31,7 +31,7 @@ const FraudProtectionRuleCardNotice: React.FC< FraudProtectionRuleCardNoticeProp const icon = 'info' === type ? : ; return ( -
@@ -61,7 +61,7 @@ Object {
@@ -77,7 +77,7 @@ Object { , "container":
@@ -107,7 +107,7 @@ Object {
@@ -205,7 +205,7 @@ Object {
@@ -234,7 +234,7 @@ Object {
@@ -253,7 +253,7 @@ Object { , "container":
@@ -282,7 +282,7 @@ Object {
@@ -383,7 +383,7 @@ Object {
@@ -412,7 +412,7 @@ Object {
@@ -431,7 +431,7 @@ Object { , "container":
@@ -460,7 +460,7 @@ Object {
@@ -561,7 +561,7 @@ Object {
@@ -590,7 +590,7 @@ Object {
@@ -609,7 +609,7 @@ Object { , "container":
@@ -638,7 +638,7 @@ Object {
@@ -739,7 +739,7 @@ Object {
@@ -768,7 +768,7 @@ Object {
@@ -787,7 +787,7 @@ Object { , "container":
@@ -816,7 +816,7 @@ Object {
diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap index 94fbd23bc0b..be659d3bd3f 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap @@ -302,7 +302,7 @@ Object {

@@ -332,7 +332,7 @@ Object {
@@ -726,7 +726,7 @@ Object {

@@ -756,7 +756,7 @@ Object {
@@ -942,7 +942,7 @@ Object {

@@ -972,7 +972,7 @@ Object {
@@ -1300,7 +1300,7 @@ Object {

@@ -1330,7 +1330,7 @@ Object {
@@ -1724,7 +1724,7 @@ Object {

@@ -1754,7 +1754,7 @@ Object {
@@ -1940,7 +1940,7 @@ Object {

@@ -1970,7 +1970,7 @@ Object {
@@ -2363,7 +2363,7 @@ Object {

@@ -2393,7 +2393,7 @@ Object {
@@ -2876,7 +2876,7 @@ Object {

@@ -2906,7 +2906,7 @@ Object {
@@ -3218,7 +3218,7 @@ Object {

@@ -3248,7 +3248,7 @@ Object {
@@ -3731,7 +3731,7 @@ Object {

@@ -3761,7 +3761,7 @@ Object {
@@ -4120,7 +4120,7 @@ Object {

@@ -4150,7 +4150,7 @@ Object {
@@ -4633,7 +4633,7 @@ Object {

@@ -4663,7 +4663,7 @@ Object {
@@ -4938,7 +4938,7 @@ Object {

@@ -4968,7 +4968,7 @@ Object {
@@ -5451,7 +5451,7 @@ Object {

@@ -5481,7 +5481,7 @@ Object {
@@ -5856,7 +5856,7 @@ Object {

@@ -5886,7 +5886,7 @@ Object {
@@ -6450,7 +6450,7 @@ Object {

@@ -6480,7 +6480,7 @@ Object {
@@ -6774,7 +6774,7 @@ Object {

@@ -6804,7 +6804,7 @@ Object {
@@ -7368,7 +7368,7 @@ Object {

@@ -7398,7 +7398,7 @@ Object {
diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap index a95aa63bc28..eebe1c2ea74 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap @@ -31,7 +31,7 @@ Object { />
@@ -61,7 +61,7 @@ Object {
@@ -77,7 +77,7 @@ Object { , "container":
@@ -107,7 +107,7 @@ Object {
@@ -205,7 +205,7 @@ Object {
@@ -234,7 +234,7 @@ Object {
@@ -250,7 +250,7 @@ Object { , "container":
@@ -279,7 +279,7 @@ Object {
@@ -377,7 +377,7 @@ Object {
@@ -407,7 +407,7 @@ Object {
@@ -423,7 +423,7 @@ Object { , "container":
@@ -453,7 +453,7 @@ Object {
diff --git a/client/settings/fraud-protection/components/protection-levels/index.tsx b/client/settings/fraud-protection/components/protection-levels/index.tsx index 0eb2e86f74a..b8597a68832 100644 --- a/client/settings/fraud-protection/components/protection-levels/index.tsx +++ b/client/settings/fraud-protection/components/protection-levels/index.tsx @@ -17,7 +17,7 @@ import { import { FraudProtectionHelpText, BasicFraudProtectionModal } from '../index'; import { getAdminUrl } from 'wcpay/utils'; import { ProtectionLevel } from '../../advanced-settings/constants'; -import InlineNotice from '../../../../components/inline-notice'; +import InlineNotice from 'components/inline-notice'; import wcpayTracks from 'tracks'; import { CurrentProtectionLevelHook } from '../../interfaces'; @@ -57,6 +57,7 @@ const ProtectionLevels: React.FC = () => { <> { 'error' === advancedFraudProtectionSettings && (
- There was an error retrieving your fraud protection settings. Please refresh the page to try again. +
+
+ + + + + +
+
+ There was an error retrieving your fraud protection settings. Please refresh the page to try again. +
+
diff --git a/client/settings/fraud-protection/style.scss b/client/settings/fraud-protection/style.scss index 614267a6050..14be0e0899b 100644 --- a/client/settings/fraud-protection/style.scss +++ b/client/settings/fraud-protection/style.scss @@ -249,10 +249,6 @@ border-bottom: 1px solid #e0e0e0; border-top: 0; } - .wcpay-banner-notice.fraud-protection-rule-card-notice { - margin-left: 0; - margin-right: 0; - } } &__help-icon { diff --git a/client/settings/survey-modal/index.js b/client/settings/survey-modal/index.js index 4b9d673ad73..45bfe42194a 100644 --- a/client/settings/survey-modal/index.js +++ b/client/settings/survey-modal/index.js @@ -14,8 +14,8 @@ import ConfirmationModal from 'components/confirmation-modal'; import useIsUpeEnabled from 'settings/wcpay-upe-toggle/hook'; import { wcPaySurveys } from './questions'; import WcPaySurveyContext from './context'; -import InlineNotice from '../../components/inline-notice'; -import { LoadableBlock } from '../../components/loadable'; +import InlineNotice from 'components/inline-notice'; +import { LoadableBlock } from 'components/loadable'; const SurveyModalBody = ( { options, surveyQuestion } ) => { const [ isUpeEnabled ] = useIsUpeEnabled(); @@ -26,7 +26,7 @@ const SurveyModalBody = ( { options, surveyQuestion } ) => { return ( <> { ! isUpeEnabled && ( - + { __( "You've disabled the new payments experience in your store.", 'woocommerce-payments' From 900146c4875f415e2a4c7f4916f83df17c880d35 Mon Sep 17 00:00:00 2001 From: Eduardo Pieretti Umpierre Date: Tue, 29 Aug 2023 09:13:54 -0300 Subject: [PATCH 07/84] Fix Multi-currency exchange rate date formatting when using custom date or time settings (#7084) --- changelog/fix-6183-exchange-date | 4 ++++ .../single-currency-settings/index.js | 16 ++++++++-------- .../test/__snapshots__/index.test.js.snap | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 changelog/fix-6183-exchange-date diff --git a/changelog/fix-6183-exchange-date b/changelog/fix-6183-exchange-date new file mode 100644 index 00000000000..240f51b7e1a --- /dev/null +++ b/changelog/fix-6183-exchange-date @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Multi-currency exchange rate date format when using custom date or time settings. diff --git a/client/multi-currency/single-currency-settings/index.js b/client/multi-currency/single-currency-settings/index.js index d637df3789b..ae28c53bea4 100644 --- a/client/multi-currency/single-currency-settings/index.js +++ b/client/multi-currency/single-currency-settings/index.js @@ -3,6 +3,7 @@ * External dependencies */ import React, { useContext, useEffect, useState } from 'react'; +import { dateI18n } from '@wordpress/date'; import { sprintf, __ } from '@wordpress/i18n'; import SettingsLayout from 'wcpay/settings/settings-layout'; import SettingsSection from 'wcpay/settings/settings-section'; @@ -20,7 +21,6 @@ import { decimalCurrencyRoundingOptions, zeroDecimalCurrencyCharmOptions, zeroDecimalCurrencyRoundingOptions, - toMoment, } from './constants'; import { useCurrencies, @@ -101,14 +101,14 @@ const SingleCurrencySettings = () => { } }, [ currencySettings, currency, initialPriceRoundingType ] ); + const dateFormat = storeSettings.date_format ?? 'M j, Y'; + const timeFormat = storeSettings.time_format ?? 'g:iA'; + const formattedLastUpdatedDateTime = targetCurrency - ? moment - .unix( targetCurrency.last_updated ) - .format( - toMoment( storeSettings.date_format ?? 'F j, Y' ) + - ' ' + - toMoment( storeSettings.time_format ?? 'HH:mm' ) - ) + ? dateI18n( + `${ dateFormat } ${ timeFormat }`, + moment.unix( targetCurrency.last_updated ).toISOString() + ) : ''; const CurrencySettingsDescription = () => ( diff --git a/client/multi-currency/single-currency-settings/test/__snapshots__/index.test.js.snap b/client/multi-currency/single-currency-settings/test/__snapshots__/index.test.js.snap index 867b5783771..69b7ed61b74 100644 --- a/client/multi-currency/single-currency-settings/test/__snapshots__/index.test.js.snap +++ b/client/multi-currency/single-currency-settings/test/__snapshots__/index.test.js.snap @@ -82,7 +82,7 @@ exports[`Single currency settings screen Page renders correctly 1`] = `

- Current rate: 1 USD = 0.826381 EUR (Last updated: September 24, 2021 01:14) + Current rate: 1 USD = 0.826381 EUR (Last updated: Sep 24, 2021 1:14AM)

From 605e329ef4cfe8270c2d9a9ebf13ebf6cd0baf38 Mon Sep 17 00:00:00 2001 From: Naman Malhotra Date: Wed, 30 Aug 2023 00:04:44 +0300 Subject: [PATCH 08/84] Migrate link-item and woopay-item to typescript (#7080) --- changelog/imp-7052-migrate-to-ts | 4 ++++ changelog/imp-7052-migrate-to-ts-woopay | 4 ++++ .../settings/express-checkout/interfaces.ts | 7 ++++++ .../{link-item.js => link-item.tsx} | 22 ++++++++----------- client/settings/express-checkout/style.scss | 6 +++++ .../{woopay-item.js => woopay-item.tsx} | 22 +++++++++---------- client/settings/wcpay-settings-context.js | 1 + tsconfig.json | 2 +- 8 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 changelog/imp-7052-migrate-to-ts create mode 100644 changelog/imp-7052-migrate-to-ts-woopay rename client/settings/express-checkout/{link-item.js => link-item.tsx} (93%) rename client/settings/express-checkout/{woopay-item.js => woopay-item.tsx} (93%) diff --git a/changelog/imp-7052-migrate-to-ts b/changelog/imp-7052-migrate-to-ts new file mode 100644 index 00000000000..1f1fa0afad0 --- /dev/null +++ b/changelog/imp-7052-migrate-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate link-item.js to typescript diff --git a/changelog/imp-7052-migrate-to-ts-woopay b/changelog/imp-7052-migrate-to-ts-woopay new file mode 100644 index 00000000000..f866f25573f --- /dev/null +++ b/changelog/imp-7052-migrate-to-ts-woopay @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate woopay-item to typescript diff --git a/client/settings/express-checkout/interfaces.ts b/client/settings/express-checkout/interfaces.ts index e76832a470e..bf32c983829 100644 --- a/client/settings/express-checkout/interfaces.ts +++ b/client/settings/express-checkout/interfaces.ts @@ -6,3 +6,10 @@ export type PaymentRequestEnabledSettingsHook = [ boolean, ( value: boolean ) => void ]; + +export type EnabledMethodIdsHook = [ + Array< string >, + ( value: Array< string > ) => void +]; + +export type WooPayEnabledSettingsHook = [ boolean, ( value: boolean ) => void ]; diff --git a/client/settings/express-checkout/link-item.js b/client/settings/express-checkout/link-item.tsx similarity index 93% rename from client/settings/express-checkout/link-item.js rename to client/settings/express-checkout/link-item.tsx index 19304e668c0..b1117db68b3 100644 --- a/client/settings/express-checkout/link-item.js +++ b/client/settings/express-checkout/link-item.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import React from 'react'; import { __ } from '@wordpress/i18n'; import { CheckboxControl, VisuallyHidden } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; @@ -17,18 +18,21 @@ import './style.scss'; import { HoverTooltip } from 'components/tooltip'; import LinkIcon from 'assets/images/payment-methods/link.svg?asset'; import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import { EnabledMethodIdsHook } from './interfaces'; -const LinkExpressCheckoutItem = () => { - const availablePaymentMethodIds = useGetAvailablePaymentMethodIds(); +const LinkExpressCheckoutItem = (): React.ReactElement => { + const availablePaymentMethodIds = useGetAvailablePaymentMethodIds() as Array< + string + >; const [ isWooPayEnabled ] = useWooPayEnabledSettings(); const [ enabledMethodIds, updateEnabledMethodIds, - ] = useEnabledPaymentMethodIds(); + ] = useEnabledPaymentMethodIds() as EnabledMethodIdsHook; - const updateStripeLinkCheckout = ( isEnabled ) => { + const updateStripeLinkCheckout = ( isEnabled: boolean ) => { //this handles the link payment method checkbox. If it's enable we should add link to the rest of the //enabled payment method. // If false - we should remove link payment method from the enabled payment methods @@ -62,15 +66,7 @@ const LinkExpressCheckoutItem = () => { ) } >
- +
{ - const [ enabledMethodIds ] = useEnabledPaymentMethodIds(); +import { WooPayEnabledSettingsHook } from './interfaces'; + +const WooPayExpressCheckoutItem = (): React.ReactElement => { + const [ enabledMethodIds ] = useEnabledPaymentMethodIds() as Array< + string + >; const [ isWooPayEnabled, updateIsWooPayEnabled, - ] = useWooPayEnabledSettings(); + ] = useWooPayEnabledSettings() as WooPayEnabledSettingsHook; const showIncompatibilityNotice = useWooPayShowIncompatibilityNotice(); @@ -51,15 +57,7 @@ const WooPayExpressCheckoutItem = () => { ) } >
- +
Date: Wed, 30 Aug 2023 12:34:04 +0300 Subject: [PATCH 09/84] Fix the way request constants are traversed (#7095) Co-authored-by: Zvonimir Maglica --- changelog/fix-request-constant-traversing | 4 +++ includes/core/server/class-request.php | 2 +- .../request/test-class-core-request.php | 34 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-request-constant-traversing create mode 100644 tests/unit/core/server/request/test-class-core-request.php diff --git a/changelog/fix-request-constant-traversing b/changelog/fix-request-constant-traversing new file mode 100644 index 00000000000..0449e00a842 --- /dev/null +++ b/changelog/fix-request-constant-traversing @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix the way request params are loaded between parent and child classes. diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index 249ae5387ac..97e2162ab51 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -544,7 +544,7 @@ public static function traverse_class_constants( string $constant_name, bool $un $constant = "$class_name::$constant_name"; if ( defined( $constant ) ) { - $keys = array_merge( $keys, constant( $constant ) ); + $keys = array_merge( constant( $constant ), $keys ); } $class_name = get_parent_class( $class_name ); diff --git a/tests/unit/core/server/request/test-class-core-request.php b/tests/unit/core/server/request/test-class-core-request.php new file mode 100644 index 00000000000..d35e182584b --- /dev/null +++ b/tests/unit/core/server/request/test-class-core-request.php @@ -0,0 +1,34 @@ +assertSame( $expected, $result ); + } +} From 71b94238c47b913cb3491624f37cb3a927305afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Qui=C3=B1ones?= Date: Wed, 30 Aug 2023 13:54:38 +0200 Subject: [PATCH 10/84] Modify title in task to continue with onboarding (#7101) --- changelog/fix-title-task-continue-onboarding | 4 ++++ .../overview/task-list/tasks/update-business-details-task.tsx | 2 +- client/overview/task-list/test/tasks.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-title-task-continue-onboarding diff --git a/changelog/fix-title-task-continue-onboarding b/changelog/fix-title-task-continue-onboarding new file mode 100644 index 00000000000..84241736c04 --- /dev/null +++ b/changelog/fix-title-task-continue-onboarding @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Modify title in task to continue with onboarding 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 84574d3433f..5f6542c67ff 100644 --- a/client/overview/task-list/tasks/update-business-details-task.tsx +++ b/client/overview/task-list/tasks/update-business-details-task.tsx @@ -123,7 +123,7 @@ export const getUpdateBusinessDetailsTask = ( title: ! detailsSubmitted ? sprintf( /* translators: %s: WooPayments */ - __( 'Set up %s', 'woocommerce-payments' ), + __( 'Finish setting up %s', 'woocommerce-payments' ), 'WooPayments' ) : sprintf( diff --git a/client/overview/task-list/test/tasks.js b/client/overview/task-list/test/tasks.js index 3d54ba80834..44b5de4e9f7 100644 --- a/client/overview/task-list/test/tasks.js +++ b/client/overview/task-list/test/tasks.js @@ -173,7 +173,7 @@ describe( 'getTasks()', () => { expect.objectContaining( { key: 'complete-setup', completed: false, - title: 'Set up WooPayments', + title: 'Finish setting up WooPayments', actionLabel: 'Finish setup', } ), ] ) From 4e3735254a7fd3bc9337125f818f5449bbc4dfdd Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Wed, 30 Aug 2023 14:07:22 +0100 Subject: [PATCH 11/84] Remove reference to the v1 experiment from the code. (#7096) --- changelog/dev-remove-v1-experiment | 4 ++++ includes/class-wc-payments-utils.php | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelog/dev-remove-v1-experiment diff --git a/changelog/dev-remove-v1-experiment b/changelog/dev-remove-v1-experiment new file mode 100644 index 00000000000..f4d0231167e --- /dev/null +++ b/changelog/dev-remove-v1-experiment @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Remove reference to old experiment. diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index e3e6365f941..39071b7fe67 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -727,8 +727,7 @@ public static function is_in_progressive_onboarding_treatment_mode(): bool { 'yes' === get_option( 'woocommerce_allow_tracking' ) ); - return 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v1' ) - || 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v2' ); + return 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v2' ); } /** From e6e7c3be3d06153233bd14159bb2c4f0f1e120b4 Mon Sep 17 00:00:00 2001 From: Allie Mims <60988591+allie500@users.noreply.github.com> Date: Wed, 30 Aug 2023 14:13:43 -0400 Subject: [PATCH 12/84] Fix AED and SAR currencies format (#7083) --- changelog/fix-6633-sar-aed-currencies-formatting | 4 ++++ i18n/currency-info.php | 8 ++++---- i18n/locale-info.php | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 changelog/fix-6633-sar-aed-currencies-formatting diff --git a/changelog/fix-6633-sar-aed-currencies-formatting b/changelog/fix-6633-sar-aed-currencies-formatting new file mode 100644 index 00000000000..160ef981508 --- /dev/null +++ b/changelog/fix-6633-sar-aed-currencies-formatting @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixes the currency formatting for AED and SAR currencies. diff --git a/i18n/currency-info.php b/i18n/currency-info.php index aa30c275923..6cfe8d46ed6 100644 --- a/i18n/currency-info.php +++ b/i18n/currency-info.php @@ -127,8 +127,8 @@ return [ 'AED' => [ - 'ar_AE' => $global_formats['rs_comma_dot_rtl'], - 'default' => $global_formats['rs_comma_dot_rtl'], + 'ar_AE' => $global_formats['rs_dot_comma_rtl'], + 'default' => $global_formats['rs_dot_comma_rtl'], ], 'AFN' => [ 'fa_AF' => $global_formats['ls_comma_dot_rtl'], @@ -723,8 +723,8 @@ 'rw_RW' => $global_formats['ls_comma_dot_ltr'], ], 'SAR' => [ - 'ar_SA' => $global_formats['rs_comma_dot_rtl'], - 'default' => $global_formats['rs_comma_dot_rtl'], + 'ar_SA' => $global_formats['rs_dot_comma_rtl'], + 'default' => $global_formats['rs_dot_comma_rtl'], ], 'SBD' => [ 'en_SB' => $global_formats['lx_dot_comma_ltr'], diff --git a/i18n/locale-info.php b/i18n/locale-info.php index 0c8f4150da7..59c234ae970 100644 --- a/i18n/locale-info.php +++ b/i18n/locale-info.php @@ -30,8 +30,8 @@ 'AE' => [ 'currency_code' => 'AED', 'currency_pos' => 'right_space', - 'thousand_sep' => '.', - 'decimal_sep' => ',', + 'thousand_sep' => ',', + 'decimal_sep' => '.', 'num_decimals' => 2, 'weight_unit' => 'kg', 'dimension_unit' => 'cm', @@ -3070,8 +3070,8 @@ 'SA' => [ 'currency_code' => 'SAR', 'currency_pos' => 'right_space', - 'thousand_sep' => '.', - 'decimal_sep' => ',', + 'thousand_sep' => ',', + 'decimal_sep' => '.', 'num_decimals' => 2, 'weight_unit' => 'kg', 'dimension_unit' => 'cm', From b5bfc76ee6e22b061ed00321cee25b8c124f8241 Mon Sep 17 00:00:00 2001 From: Dan Paun <82826872+dpaun1985@users.noreply.github.com> Date: Thu, 31 Aug 2023 18:36:50 +0300 Subject: [PATCH 13/84] Add support for kanji and kana statement descriptors (#7051) Co-authored-by: Dan Paun Co-authored-by: Ahmed --- changelog/add-6874-add-kanji-kana | 4 + client/data/settings/actions.js | 16 +++ client/data/settings/hooks.js | 32 ++++++ client/data/settings/selectors.js | 8 ++ client/settings/transactions/index.js | 108 +++++++++++++++++- client/settings/transactions/style.scss | 9 +- .../settings/transactions/test/index.test.js | 39 +++++++ ...s-wc-rest-payments-settings-controller.php | 2 + includes/class-wc-payment-gateway-wcpay.php | 69 ++++++++--- includes/class-wc-payments-account.php | 20 ++++ .../server/request/class-update-account.php | 22 ++++ 11 files changed, 308 insertions(+), 21 deletions(-) create mode 100644 changelog/add-6874-add-kanji-kana diff --git a/changelog/add-6874-add-kanji-kana b/changelog/add-6874-add-kanji-kana new file mode 100644 index 00000000000..ecd1574347c --- /dev/null +++ b/changelog/add-6874-add-kanji-kana @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Support kanji and kana statement descriptors for Japanese merchants diff --git a/client/data/settings/actions.js b/client/data/settings/actions.js index 59a73f7c225..1d5f231d5d8 100644 --- a/client/data/settings/actions.js +++ b/client/data/settings/actions.js @@ -123,6 +123,22 @@ export function updateAccountStatementDescriptor( accountStatementDescriptor ) { } ); } +export function updateAccountStatementDescriptorKanji( + accountStatementDescriptorKanji +) { + return updateSettingsValues( { + account_statement_descriptor_kanji: accountStatementDescriptorKanji, + } ); +} + +export function updateAccountStatementDescriptorKana( + accountStatementDescriptorKana +) { + return updateSettingsValues( { + account_statement_descriptor_kana: accountStatementDescriptorKana, + } ); +} + export function updateAccountBusinessName( accountBusinessName ) { return updateSettingsValues( { account_business_name: accountBusinessName, diff --git a/client/data/settings/hooks.js b/client/data/settings/hooks.js index 9bea7a71c5e..3fbd2d375b1 100644 --- a/client/data/settings/hooks.js +++ b/client/data/settings/hooks.js @@ -188,6 +188,38 @@ export const useAccountStatementDescriptor = () => { ); }; +export const useAccountStatementDescriptorKanji = () => { + const { updateAccountStatementDescriptorKanji } = useDispatch( STORE_NAME ); + + return useSelect( + ( select ) => { + const { getAccountStatementDescriptorKanji } = select( STORE_NAME ); + + return [ + getAccountStatementDescriptorKanji(), + updateAccountStatementDescriptorKanji, + ]; + }, + [ updateAccountStatementDescriptorKanji ] + ); +}; + +export const useAccountStatementDescriptorKana = () => { + const { updateAccountStatementDescriptorKana } = useDispatch( STORE_NAME ); + + return useSelect( + ( select ) => { + const { getAccountStatementDescriptorKana } = select( STORE_NAME ); + + return [ + getAccountStatementDescriptorKana(), + updateAccountStatementDescriptorKana, + ]; + }, + [ updateAccountStatementDescriptorKana ] + ); +}; + export const useAccountBusinessName = () => { const { updateAccountBusinessName } = useDispatch( STORE_NAME ); diff --git a/client/data/settings/selectors.js b/client/data/settings/selectors.js index e922a4a944d..dfdbadfdd2d 100644 --- a/client/data/settings/selectors.js +++ b/client/data/settings/selectors.js @@ -52,6 +52,14 @@ export const getAccountStatementDescriptor = ( state ) => { return getSettings( state ).account_statement_descriptor || ''; }; +export const getAccountStatementDescriptorKanji = ( state ) => { + return getSettings( state ).account_statement_descriptor_kanji || ''; +}; + +export const getAccountStatementDescriptorKana = ( state ) => { + return getSettings( state ).account_statement_descriptor_kana || ''; +}; + export const getAccountBusinessName = ( state ) => { return getSettings( state ).account_business_name || ''; }; diff --git a/client/settings/transactions/index.js b/client/settings/transactions/index.js index eb929c8e2a9..e4b11b66b21 100644 --- a/client/settings/transactions/index.js +++ b/client/settings/transactions/index.js @@ -15,6 +15,8 @@ import { import CardBody from '../card-body'; import { useAccountStatementDescriptor, + useAccountStatementDescriptorKanji, + useAccountStatementDescriptorKana, useGetSavingError, useSavedCards, } from '../../data'; @@ -23,8 +25,12 @@ import ManualCaptureControl from 'wcpay/settings/transactions/manual-capture-con import SupportPhoneInput from 'wcpay/settings/support-phone-input'; import SupportEmailInput from 'wcpay/settings/support-email-input'; import React, { useEffect, useState } from 'react'; +import { select } from '@wordpress/data'; +import { STORE_NAME } from 'wcpay/data/constants'; const ACCOUNT_STATEMENT_MAX_LENGTH = 22; +const ACCOUNT_STATEMENT_MAX_LENGTH_KANJI = 17; +const ACCOUNT_STATEMENT_MAX_LENGTH_KANA = 22; const Transactions = ( { setTransactionInputsValid } ) => { const [ isSavedCardsEnabled, setIsSavedCardsEnabled ] = useSavedCards(); @@ -32,11 +38,20 @@ const Transactions = ( { setTransactionInputsValid } ) => { accountStatementDescriptor, setAccountStatementDescriptor, ] = useAccountStatementDescriptor(); + const [ + accountStatementDescriptorKanji, + setAccountStatementDescriptorKanji, + ] = useAccountStatementDescriptorKanji(); + const [ + accountStatementDescriptorKana, + setAccountStatementDescriptorKana, + ] = useAccountStatementDescriptorKana(); const customerBankStatementErrorMessage = useGetSavingError()?.data?.details ?.account_statement_descriptor?.message; const [ isEmailInputValid, setEmailInputValid ] = useState( true ); const [ isPhoneInputValid, setPhoneInputValid ] = useState( true ); + const settings = select( STORE_NAME ).getSettings(); useEffect( () => { if ( setTransactionInputsValid ) { @@ -64,9 +79,15 @@ const Transactions = ( { setTransactionInputsValid } ) => { ) } /> -

{ __( 'Customer support', 'woocommerce-payments' ) }

+

{ __( 'Customer statements', 'woocommerce-payments' ) }

+

+ { __( + "Edit the way your store name appears on your customers' bank statements.", + 'woocommerce-payments' + ) } +

-
+
{ customerBankStatementErrorMessage && ( { ) } { + { settings.account_country === 'JP' && ( + <> +
+ + +
+
+ + +
+ + ) } +
+ +

{ __( 'Customer support', 'woocommerce-payments' ) }

+

+ { __( + 'Provide contact information where customers can reach you for support.', + 'woocommerce-payments' + ) } +

+
diff --git a/client/settings/transactions/style.scss b/client/settings/transactions/style.scss index 8a33c2b1381..5bd41a858d7 100644 --- a/client/settings/transactions/style.scss +++ b/client/settings/transactions/style.scss @@ -5,7 +5,8 @@ margin-bottom: 1em; } - &__customer-support { + &__customer-support, + &__customer-statements { max-width: 500px; position: relative; @@ -27,3 +28,9 @@ } } } + +.transactions-customer-details { + font-size: 12px; + font-style: normal; + color: #757575; +} diff --git a/client/settings/transactions/test/index.test.js b/client/settings/transactions/test/index.test.js index d8ab3fb6281..c5a3807e51f 100644 --- a/client/settings/transactions/test/index.test.js +++ b/client/settings/transactions/test/index.test.js @@ -10,15 +10,31 @@ import Transactions from '..'; import { useGetSavingError, useAccountStatementDescriptor, + useAccountStatementDescriptorKanji, + useAccountStatementDescriptorKana, useAccountBusinessSupportEmail, useAccountBusinessSupportPhone, useManualCapture, useSavedCards, useCardPresentEligible, } from '../../../data'; +import { select } from '@wordpress/data'; + +jest.mock( '@wordpress/data', () => ( { + select: jest.fn(), +} ) ); +const settingsMock = { + account_country: 'US', +}; + +select.mockReturnValue( { + getSettings: () => settingsMock, +} ); jest.mock( 'wcpay/data', () => ( { useAccountStatementDescriptor: jest.fn(), + useAccountStatementDescriptorKanji: jest.fn(), + useAccountStatementDescriptorKana: jest.fn(), useAccountBusinessSupportEmail: jest.fn(), useAccountBusinessSupportPhone: jest.fn(), useManualCapture: jest.fn(), @@ -30,6 +46,8 @@ jest.mock( 'wcpay/data', () => ( { describe( 'Settings - Transactions', () => { beforeEach( () => { useAccountStatementDescriptor.mockReturnValue( [ '', jest.fn() ] ); + useAccountStatementDescriptorKanji.mockReturnValue( [ '', jest.fn() ] ); + useAccountStatementDescriptorKana.mockReturnValue( [ '', jest.fn() ] ); useAccountBusinessSupportEmail.mockReturnValue( [ 'test@test.com', jest.fn(), @@ -120,4 +138,25 @@ describe( 'Settings - Transactions', () => { ).toBeInTheDocument(); expect( screen.getByLabelText( 'Support email' ) ).toBeInTheDocument(); } ); + + it( 'display customer bank statements for JP', async () => { + const settingsMockCountryJP = { + account_country: 'JP', + }; + + select.mockReturnValue( { + getSettings: () => settingsMockCountryJP, + } ); + render( ); + + expect( + await screen.findByText( 'Use only latin characters.' ) + ).toBeInTheDocument(); + expect( + await screen.findByText( 'Use only kanji characters.' ) + ).toBeInTheDocument(); + expect( + await screen.findByText( 'Use only kana characters.' ) + ).toBeInTheDocument(); + } ); } ); diff --git a/includes/admin/class-wc-rest-payments-settings-controller.php b/includes/admin/class-wc-rest-payments-settings-controller.php index c83d211399e..1742491563b 100644 --- a/includes/admin/class-wc-rest-payments-settings-controller.php +++ b/includes/admin/class-wc-rest-payments-settings-controller.php @@ -426,6 +426,8 @@ public function get_settings(): WP_REST_Response { 'is_subscriptions_plugin_active' => $this->wcpay_gateway->is_subscriptions_plugin_active(), 'account_country' => $this->wcpay_gateway->get_option( 'account_country' ), 'account_statement_descriptor' => $this->wcpay_gateway->get_option( 'account_statement_descriptor' ), + 'account_statement_descriptor_kanji' => $this->wcpay_gateway->get_option( 'account_statement_descriptor_kanji' ), + 'account_statement_descriptor_kana' => $this->wcpay_gateway->get_option( 'account_statement_descriptor_kana' ), 'account_business_name' => $this->wcpay_gateway->get_option( 'account_business_name' ), 'account_business_url' => $this->wcpay_gateway->get_option( 'account_business_url' ), 'account_business_support_address' => $this->wcpay_gateway->get_option( 'account_business_support_address' ), diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 5f459e4c3a2..826ed51d02e 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -67,20 +67,22 @@ class WC_Payment_Gateway_WCPay extends WC_Payment_Gateway_CC { * @type array */ const ACCOUNT_SETTINGS_MAPPING = [ - 'account_statement_descriptor' => 'statement_descriptor', - 'account_business_name' => 'business_name', - 'account_business_url' => 'business_url', - 'account_business_support_address' => 'business_support_address', - 'account_business_support_email' => 'business_support_email', - 'account_business_support_phone' => 'business_support_phone', - 'account_branding_logo' => 'branding_logo', - 'account_branding_icon' => 'branding_icon', - 'account_branding_primary_color' => 'branding_primary_color', - 'account_branding_secondary_color' => 'branding_secondary_color', - - 'deposit_schedule_interval' => 'deposit_schedule_interval', - 'deposit_schedule_weekly_anchor' => 'deposit_schedule_weekly_anchor', - 'deposit_schedule_monthly_anchor' => 'deposit_schedule_monthly_anchor', + 'account_statement_descriptor' => 'statement_descriptor', + 'account_statement_descriptor_kanji' => 'statement_descriptor_kanji', + 'account_statement_descriptor_kana' => 'statement_descriptor_kana', + 'account_business_name' => 'business_name', + 'account_business_url' => 'business_url', + 'account_business_support_address' => 'business_support_address', + 'account_business_support_email' => 'business_support_email', + 'account_business_support_phone' => 'business_support_phone', + 'account_branding_logo' => 'branding_logo', + 'account_branding_icon' => 'branding_icon', + 'account_branding_primary_color' => 'branding_primary_color', + 'account_branding_secondary_color' => 'branding_secondary_color', + + 'deposit_schedule_interval' => 'deposit_schedule_interval', + 'deposit_schedule_weekly_anchor' => 'deposit_schedule_weekly_anchor', + 'deposit_schedule_monthly_anchor' => 'deposit_schedule_monthly_anchor', ]; /** @@ -1817,6 +1819,10 @@ public function get_option( $key, $empty_value = null ) { return $this->get_account_country(); case 'account_statement_descriptor': return $this->get_account_statement_descriptor(); + case 'account_statement_descriptor_kanji': + return $this->get_account_statement_descriptor_kanji(); + case 'account_statement_descriptor_kana': + return $this->get_account_statement_descriptor_kana(); case 'account_business_name': return $this->get_account_business_name(); case 'account_business_url': @@ -1968,6 +1974,41 @@ public function get_account_statement_descriptor( string $empty_value = '' ): st return $empty_value; } + /** + * Gets connected account statement descriptor. + * + * @param string $empty_value Empty value to return when not connected or fails to fetch account descriptor. + * + * @return string Statement descriptor of default value. + */ + public function get_account_statement_descriptor_kanji( string $empty_value = '' ): string { + try { + if ( $this->is_connected() ) { + return $this->account->get_statement_descriptor_kanji(); + } + } catch ( Exception $e ) { + Logger::error( 'Failed to get account statement descriptor.' . $e ); + } + return $empty_value; + } + + /** + * Gets connected account statement descriptor. + * + * @param string $empty_value Empty value to return when not connected or fails to fetch account descriptor. + * + * @return string Statement descriptor of default value. + */ + public function get_account_statement_descriptor_kana( string $empty_value = '' ): string { + try { + if ( $this->is_connected() ) { + return $this->account->get_statement_descriptor_kana(); + } + } catch ( Exception $e ) { + Logger::error( 'Failed to get account statement descriptor.' . $e ); + } + return $empty_value; + } /** * Gets account default currency. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 13f6b6a15e9..82eb9989117 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -275,6 +275,26 @@ public function get_statement_descriptor() : string { return ! empty( $account ) && isset( $account['statement_descriptor'] ) ? $account['statement_descriptor'] : ''; } + /** + * Gets the account statement descriptor for rendering on the settings page. + * + * @return string Account statement descriptor. + */ + public function get_statement_descriptor_kanji() : string { + $account = $this->get_cached_account_data(); + return ! empty( $account ) && isset( $account['statement_descriptor_kanji'] ) ? $account['statement_descriptor_kanji'] : ''; + } + + /** + * Gets the account statement descriptor for rendering on the settings page. + * + * @return string Account statement descriptor. + */ + public function get_statement_descriptor_kana() : string { + $account = $this->get_cached_account_data(); + return ! empty( $account ) && isset( $account['statement_descriptor_kana'] ) ? $account['statement_descriptor_kana'] : ''; + } + /** * Gets the business name. * diff --git a/includes/core/server/request/class-update-account.php b/includes/core/server/request/class-update-account.php index 909c6789177..db63a5eda24 100644 --- a/includes/core/server/request/class-update-account.php +++ b/includes/core/server/request/class-update-account.php @@ -80,6 +80,28 @@ public function set_statement_descriptor( string $statement_descriptor ) { $this->set_param( 'statement_descriptor', $statement_descriptor ); } + /** + * Sets the account statement descriptor kanji. + * + * @param string $statement_descriptor_kanji Statement descriptor kanji. + * + * @return void + */ + public function set_statement_descriptor_kanji( string $statement_descriptor_kanji ) { + $this->set_param( 'statement_descriptor_kanji', $statement_descriptor_kanji ); + } + + /** + * Sets the account statement descriptor kana. + * + * @param string $statement_descriptor_kana Statement descriptor kana. + * + * @return void + */ + public function set_statement_descriptor_kana( string $statement_descriptor_kana ) { + $this->set_param( 'statement_descriptor_kana', $statement_descriptor_kana ); + } + /** * Sets the account business name. * From 6b10aecfb8cc5281cb41ad8dea0740c8c83f3928 Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Thu, 31 Aug 2023 14:22:56 -0300 Subject: [PATCH 14/84] Fix deprecation warnings on blocks checkout (#7070) --- .../fix-deprecation-warning-on-blocks-checkout | 4 ++++ client/checkout/blocks/fields.js | 7 ++----- client/checkout/blocks/hooks.js | 17 ++++++++--------- client/checkout/blocks/saved-token-handler.js | 4 ++-- .../payment-processor.js | 4 ++-- client/checkout/blocks/upe-fields.js | 7 ++----- client/checkout/blocks/upe-split-fields.js | 7 ++----- 7 files changed, 22 insertions(+), 28 deletions(-) create mode 100644 changelog/fix-deprecation-warning-on-blocks-checkout diff --git a/changelog/fix-deprecation-warning-on-blocks-checkout b/changelog/fix-deprecation-warning-on-blocks-checkout new file mode 100644 index 00000000000..ae1241fc85a --- /dev/null +++ b/changelog/fix-deprecation-warning-on-blocks-checkout @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix deprecation warnings on blocks checkout. diff --git a/client/checkout/blocks/fields.js b/client/checkout/blocks/fields.js index 7748c9ee1cd..883a4146ae1 100644 --- a/client/checkout/blocks/fields.js +++ b/client/checkout/blocks/fields.js @@ -23,10 +23,7 @@ const WCPayFields = ( { stripe, elements, billing: { billingData }, - eventRegistration: { - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - }, + eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, emitResponse, shouldSavePayment, } ) => { @@ -87,7 +84,7 @@ const WCPayFields = ( { api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, shouldSavePayment ); diff --git a/client/checkout/blocks/hooks.js b/client/checkout/blocks/hooks.js index fd52b16c93b..34a7a6f504d 100644 --- a/client/checkout/blocks/hooks.js +++ b/client/checkout/blocks/hooks.js @@ -16,21 +16,20 @@ export const usePaymentCompleteHandler = ( api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, shouldSavePayment ) => { // Once the server has completed payment processing, confirm the intent of necessary. useEffect( () => - onCheckoutAfterProcessingWithSuccess( - ( { processingResponse: { paymentDetails } } ) => - confirmCardPayment( - api, - paymentDetails, - emitResponse, - shouldSavePayment - ) + onCheckoutSuccess( ( { processingResponse: { paymentDetails } } ) => + confirmCardPayment( + api, + paymentDetails, + emitResponse, + shouldSavePayment + ) ), // not sure if we need to disable this, but kept it as-is to ensure nothing breaks. Please consider passing all the deps. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/client/checkout/blocks/saved-token-handler.js b/client/checkout/blocks/saved-token-handler.js index a2e2b0ea339..2ec311c8d5e 100644 --- a/client/checkout/blocks/saved-token-handler.js +++ b/client/checkout/blocks/saved-token-handler.js @@ -7,7 +7,7 @@ export const SavedTokenHandler = ( { api, stripe, elements, - eventRegistration: { onCheckoutAfterProcessingWithSuccess }, + eventRegistration: { onCheckoutSuccess }, emitResponse, } ) => { // Once the server has completed payment processing, confirm the intent of necessary. @@ -15,7 +15,7 @@ export const SavedTokenHandler = ( { api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, false // No need to save a payment that has already been saved. ); diff --git a/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js b/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js index 1d0a8d9f564..989773e8400 100644 --- a/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js +++ b/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js @@ -59,7 +59,7 @@ const PaymentProcessor = ( { api, activePaymentMethod, testingInstructions, - eventRegistration: { onPaymentSetup, onCheckoutAfterProcessingWithSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, paymentMethodId, upeMethods, @@ -228,7 +228,7 @@ const PaymentProcessor = ( { api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, shouldSavePayment ); diff --git a/client/checkout/blocks/upe-fields.js b/client/checkout/blocks/upe-fields.js index f8f0b5680c0..9f1ea7d67ee 100644 --- a/client/checkout/blocks/upe-fields.js +++ b/client/checkout/blocks/upe-fields.js @@ -41,10 +41,7 @@ const WCPayUPEFields = ( { activePaymentMethod, billing: { billingData }, shippingData, - eventRegistration: { - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - }, + eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, emitResponse, paymentIntentId, paymentIntentSecret, @@ -206,7 +203,7 @@ const WCPayUPEFields = ( { // Once the server has completed payment processing, confirm the intent if necessary. useEffect( () => - onCheckoutAfterProcessingWithSuccess( + onCheckoutSuccess( ( { orderId, processingResponse: { paymentDetails } } ) => { async function updateIntent() { if ( api.handleDuplicatePayments( paymentDetails ) ) { diff --git a/client/checkout/blocks/upe-split-fields.js b/client/checkout/blocks/upe-split-fields.js index 91d0500f3da..07b9f720da1 100644 --- a/client/checkout/blocks/upe-split-fields.js +++ b/client/checkout/blocks/upe-split-fields.js @@ -45,10 +45,7 @@ const WCPayUPEFields = ( { testingInstructions, billing: { billingData }, shippingData, - eventRegistration: { - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - }, + eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, emitResponse, paymentMethodId, upeMethods, @@ -204,7 +201,7 @@ const WCPayUPEFields = ( { // Once the server has completed payment processing, confirm the intent if necessary. useEffect( () => - onCheckoutAfterProcessingWithSuccess( + onCheckoutSuccess( ( { orderId, processingResponse: { paymentDetails } } ) => { async function updateIntent() { if ( api.handleDuplicatePayments( paymentDetails ) ) { From 8ea5c5f1794ec3751fefb7218cbe9a6f18849c1b Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Thu, 31 Aug 2023 19:58:39 +0200 Subject: [PATCH 15/84] Add/5669 add further payment metadata (#7091) Co-authored-by: Radoslav Georgiev --- .../add-5669-add-further-payment-metadata | 4 ++ includes/class-wc-payment-gateway-wcpay.php | 39 +++++++------ .../test-class-upe-payment-gateway.php | 54 +++++++++++------- .../test-class-upe-split-payment-gateway.php | 55 ++++++++++++------- ...ay-wcpay-subscriptions-process-payment.php | 12 ++-- .../test-class-wc-payment-gateway-wcpay.php | 18 +++--- 6 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 changelog/add-5669-add-further-payment-metadata diff --git a/changelog/add-5669-add-further-payment-metadata b/changelog/add-5669-add-further-payment-metadata new file mode 100644 index 00000000000..347c49daf22 --- /dev/null +++ b/changelog/add-5669-add-further-payment-metadata @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added additional meta data to payment requests diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 826ed51d02e..5f3912b4b1d 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -1484,30 +1484,33 @@ public function set_payment_method_title_for_order( $order, $payment_method_type * @return array Array of keyed metadata values. */ protected function get_metadata_from_order( $order, $payment_type ) { + if ( $this instanceof UPE_Split_Payment_Gateway ) { + $gateway_type = 'split_upe'; + } elseif ( $this instanceof UPE_Payment_Gateway ) { + $gateway_type = 'upe'; + } else { + $gateway_type = 'classic'; + } $name = sanitize_text_field( $order->get_billing_first_name() ) . ' ' . sanitize_text_field( $order->get_billing_last_name() ); $email = sanitize_email( $order->get_billing_email() ); $metadata = [ - 'customer_name' => $name, - 'customer_email' => $email, - 'site_url' => esc_url( get_site_url() ), - 'order_id' => $order->get_id(), - 'order_number' => $order->get_order_number(), - 'order_key' => $order->get_order_key(), - 'payment_type' => $payment_type, + 'customer_name' => $name, + 'customer_email' => $email, + 'site_url' => esc_url( get_site_url() ), + 'order_id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_key' => $order->get_order_key(), + 'payment_type' => $payment_type, + 'gateway_type' => $gateway_type, + 'checkout_type' => $order->get_created_via(), + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ]; - // If the order belongs to a WCPay Subscription, set the payment context to 'wcpay_subscription' (this helps with associating which fees belong to orders). - if ( 'recurring' === (string) $payment_type && ! $this->is_subscriptions_plugin_active() ) { - $subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] ); - - foreach ( $subscriptions as $subscription ) { - if ( WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) { - $metadata['payment_context'] = 'wcpay_subscription'; - break; - } - } + if ( 'recurring' === (string) $payment_type && function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order ) ) { + $metadata['subscription_payment'] = wcs_order_contains_renewal( $order ) ? 'renewal' : 'initial'; + $metadata['payment_context'] = $this->is_subscriptions_plugin_active() ? 'regular_subscription' : 'wcpay_subscription'; } - return apply_filters( 'wcpay_metadata_from_order', $metadata, $order, $payment_type ); } diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php index b01a28df986..715a43ff967 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -374,13 +374,17 @@ public function test_update_payment_intent_adds_customer_save_payment_and_level3 ->method( 'set_metadata' ) ->with( [ - 'customer_name' => 'Jeroen Sormani', - 'customer_email' => 'admin@example.org', - 'site_url' => 'http://example.org', - 'order_id' => $order_id, - 'order_number' => $order_number, - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Jeroen Sormani', + 'customer_email' => 'admin@example.org', + 'site_url' => 'http://example.org', + 'order_id' => $order_id, + 'order_number' => $order_number, + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'upe', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ] ); @@ -472,13 +476,17 @@ public function test_update_payment_intent_with_selected_upe_payment_method() { ->method( 'set_metadata' ) ->with( [ - 'customer_name' => 'Jeroen Sormani', - 'customer_email' => 'admin@example.org', - 'site_url' => 'http://example.org', - 'order_id' => $order_id, - 'order_number' => $order_number, - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Jeroen Sormani', + 'customer_email' => 'admin@example.org', + 'site_url' => 'http://example.org', + 'order_id' => $order_id, + 'order_number' => $order_number, + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'upe', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ] ); @@ -566,13 +574,17 @@ public function test_update_payment_intent_with_payment_country() { ->method( 'set_metadata' ) ->with( [ - 'customer_name' => 'Jeroen Sormani', - 'customer_email' => 'admin@example.org', - 'site_url' => 'http://example.org', - 'order_id' => $order_id, - 'order_number' => $order_number, - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Jeroen Sormani', + 'customer_email' => 'admin@example.org', + 'site_url' => 'http://example.org', + 'order_id' => $order_id, + 'order_number' => $order_number, + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'upe', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ] ); diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index 4b829032e9e..4a613570de1 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -408,13 +408,17 @@ public function test_update_payment_intent_adds_customer_save_payment_and_level3 ->method( 'create_customer_for_user' ); $metadata = [ - 'customer_name' => 'Jeroen Sormani', - 'customer_email' => 'admin@example.org', - 'site_url' => 'http://example.org', - 'order_id' => $order_id, - 'order_number' => $order_number, - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Jeroen Sormani', + 'customer_email' => 'admin@example.org', + 'site_url' => 'http://example.org', + 'order_id' => $order_id, + 'order_number' => $order_number, + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'split_upe', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ]; $level3 = [ @@ -479,13 +483,18 @@ public function test_update_payment_intent_with_selected_upe_payment_method() { ->method( 'create_customer_for_user' ); $metadata = [ - 'customer_name' => 'Jeroen Sormani', - 'customer_email' => 'admin@example.org', - 'site_url' => 'http://example.org', - 'order_id' => $order_id, - 'order_number' => $order_number, - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Jeroen Sormani', + 'customer_email' => 'admin@example.org', + 'site_url' => 'http://example.org', + 'order_id' => $order_id, + 'order_number' => $order_number, + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'split_upe', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', + ]; $level3 = [ @@ -553,13 +562,17 @@ public function test_update_payment_intent_with_payment_country() { ->method( 'create_customer_for_user' ); $metadata = [ - 'customer_name' => 'Jeroen Sormani', - 'customer_email' => 'admin@example.org', - 'site_url' => 'http://example.org', - 'order_id' => $order_id, - 'order_number' => $order_number, - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Jeroen Sormani', + 'customer_email' => 'admin@example.org', + 'site_url' => 'http://example.org', + 'order_id' => $order_id, + 'order_number' => $order_number, + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'split_upe', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ]; $level3 = [ diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php index bef80ba1878..74a4ad79b29 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php @@ -445,15 +445,18 @@ function( $metadata ) { } public function test_saved_card_zero_dollar_subscription() { - $order = WC_Helper_Order::create_order( self::USER_ID, 0 ); + $order = WC_Helper_Order::create_order( self::USER_ID, 0 ); + $subscriptions = [ new WC_Subscription() ]; + $subscriptions[0]->set_parent( $order ); + + $this->mock_wcs_order_contains_subscription( true ); + $this->mock_wcs_get_subscriptions_for_order( $subscriptions ); $_POST = [ 'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID, self::TOKEN_REQUEST_KEY => $this->token->get_id(), ]; - $this->mock_wcs_order_contains_subscription( true ); - // The card is already saved and there's no payment needed, so no Setup Intent needs to be created. $request = $this->mock_wcpay_request( Create_And_Confirm_Setup_Intention::class, 0 ); @@ -463,9 +466,6 @@ public function test_saved_card_zero_dollar_subscription() { ->expects( $this->never() ) ->method( 'add_payment_method_to_user' ); - $subscriptions = [ WC_Helper_Order::create_order( self::USER_ID ) ]; - $this->mock_wcs_get_subscriptions_for_order( $subscriptions ); - $result = $this->mock_wcpay_gateway->process_payment( $order->get_id() ); $result_order = wc_get_order( $order->get_id() ); diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index 0f098b61ba9..f39f4dea3ed 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -1314,13 +1314,17 @@ public function test_capture_charge_metadata() { ); $merged_metadata = [ - 'customer_name' => 'Test', - 'customer_email' => $order->get_billing_email(), - 'site_url' => esc_url( get_site_url() ), - 'order_id' => $order->get_id(), - 'order_number' => $order->get_order_number(), - 'order_key' => $order->get_order_key(), - 'payment_type' => Payment_Type::SINGLE(), + 'customer_name' => 'Test', + 'customer_email' => $order->get_billing_email(), + 'site_url' => esc_url( get_site_url() ), + 'order_id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_key' => $order->get_order_key(), + 'payment_type' => Payment_Type::SINGLE(), + 'gateway_type' => 'classic', + 'checkout_type' => '', + 'client_version' => WCPAY_VERSION_NUMBER, + 'subscription_payment' => 'no', ]; $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id ); From 1a5af3519bd171f25f32433735248d93a69dbab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3fer=20Reykjal=C3=ADn?= <13835680+reykjalin@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:09:22 -0400 Subject: [PATCH 16/84] Fall back to site logo when no custom WooPay logo defined (#7103) --- .../add-use-site-logo-when-no-woopay-logo-defined | 4 ++++ includes/woopay/class-woopay-session.php | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 changelog/add-use-site-logo-when-no-woopay-logo-defined diff --git a/changelog/add-use-site-logo-when-no-woopay-logo-defined b/changelog/add-use-site-logo-when-no-woopay-logo-defined new file mode 100644 index 00000000000..0afbfccf655 --- /dev/null +++ b/changelog/add-use-site-logo-when-no-woopay-logo-defined @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Fall back to site logo when a custom WooPay logo has not been defined diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index c36489f4047..2caf2be5d9e 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -340,7 +340,14 @@ private static function get_init_session_request() { $account_id = WC_Payments::get_account_service()->get_stripe_account_id(); - $store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' ); + $site_logo_id = get_theme_mod( 'custom_logo' ); + $site_logo_url = $site_logo_id ? ( wp_get_attachment_image_src( $site_logo_id, 'full' )[0] ?? '' ) : ''; + $woopay_store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' ); + + $store_logo = $site_logo_url; + if ( ! empty( $woopay_store_logo ) ) { + $store_logo = get_rest_url( null, 'wc/v3/payments/file/' . $woopay_store_logo ); + } include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php'; $blocks_data_extractor = new Blocks_Data_Extractor(); @@ -361,7 +368,7 @@ private static function get_init_session_request() { 'email' => '', 'store_data' => [ 'store_name' => get_bloginfo( 'name' ), - 'store_logo' => ! empty( $store_logo ) ? get_rest_url( null, 'wc/v3/payments/file/' . $store_logo ) : '', + 'store_logo' => $store_logo, 'custom_message' => self::get_formatted_custom_message(), 'blog_id' => Jetpack_Options::get_option( 'id' ), 'blog_url' => get_site_url(), From d0db2a3fc002faa58254fc48d8632641645acfa0 Mon Sep 17 00:00:00 2001 From: Matt Allan Date: Fri, 1 Sep 2023 08:32:18 +1000 Subject: [PATCH 17/84] Schedule individual subscription migrations and add manual migration tool under Status > Tools (#6942) Co-authored-by: James Allan --- ...-6526-schedule-subscription-migration-tool | 5 + includes/class-wc-payments.php | 25 +- .../class-wc-payments-invoice-service.php | 7 +- .../class-wc-payments-product-service.php | 12 +- ...ts-subscription-minimum-amount-handler.php | 7 +- ...class-wc-payments-subscription-service.php | 8 +- ...ass-wc-payments-subscriptions-migrator.php | 302 +++++++++++++++--- .../class-wc-payments-subscriptions.php | 5 + tests/unit/bootstrap.php | 3 + .../class-wcs-helper-background-repairer.php | 22 ++ ...test-class-wc-payments-invoice-service.php | 4 + ...class-wc-payments-subscription-service.php | 3 + 12 files changed, 338 insertions(+), 65 deletions(-) create mode 100644 changelog/issue-6526-schedule-subscription-migration-tool create mode 100644 tests/unit/helpers/class-wcs-helper-background-repairer.php diff --git a/changelog/issue-6526-schedule-subscription-migration-tool b/changelog/issue-6526-schedule-subscription-migration-tool new file mode 100644 index 00000000000..391c0a20ddd --- /dev/null +++ b/changelog/issue-6526-schedule-subscription-migration-tool @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This PR is part of a larger feature coming to WCPay and not single entry is needed for this PR. + + diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 131b61835a8..c80b1f19664 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -624,7 +624,7 @@ public static function init() { } // Load WCPay Subscriptions. - if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() ) { + if ( self::should_load_wcpay_subscriptions() ) { include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions.php'; WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account ); } @@ -633,12 +633,6 @@ public static function init() { add_action( 'woocommerce_onboarding_profile_data_updated', 'WC_Payments_Features::maybe_enable_wcpay_subscriptions_after_onboarding', 10, 2 ); } - // Load the WCPay Subscriptions migration class. - if ( WC_Payments_Features::is_subscription_migration_enabled() ) { - include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions-migrator.php'; - new WC_Payments_Subscriptions_Migrator( self::$api_client ); - } - add_action( 'rest_api_init', [ __CLASS__, 'init_rest_api' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'set_plugin_activation_timestamp' ] ); @@ -1711,4 +1705,21 @@ public static function wcpay_show_old_woocommerce_for_hungary_sweden_and_czech_r
[ 'parent', 'renewal' ] ] ) as $subscription ) { $invoice_id = self::get_subscription_invoice_id( $subscription ); - if ( ! $invoice_id ) { + if ( ! $invoice_id || ! WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) { continue; } diff --git a/includes/subscriptions/class-wc-payments-product-service.php b/includes/subscriptions/class-wc-payments-product-service.php index ed283de271f..995fa4a7478 100644 --- a/includes/subscriptions/class-wc-payments-product-service.php +++ b/includes/subscriptions/class-wc-payments-product-service.php @@ -92,12 +92,16 @@ public function __construct( WC_Payments_API_Client $payments_api_client ) { return; } - add_action( 'shutdown', [ $this, 'create_or_update_products' ] ); + // Only create, update and restore/unarchive WCPay Subscription products when the WC Subscriptions plugin is not active. + if ( ! $this->is_subscriptions_plugin_active() ) { + add_action( 'shutdown', [ $this, 'create_or_update_products' ] ); + add_action( 'untrashed_post', [ $this, 'maybe_unarchive_product' ] ); + + $this->add_product_update_listeners(); + } + add_action( 'wp_trash_post', [ $this, 'maybe_archive_product' ] ); - add_action( 'untrashed_post', [ $this, 'maybe_unarchive_product' ] ); add_filter( 'woocommerce_duplicate_product_exclude_meta', [ $this, 'exclude_meta_wcpay_product' ] ); - - $this->add_product_update_listeners(); } /** diff --git a/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php b/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php index ca8f3f66928..60ff2e0e86d 100644 --- a/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php +++ b/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php @@ -10,6 +10,8 @@ */ class WC_Payments_Subscription_Minimum_Amount_Handler { + use WC_Payments_Subscriptions_Utilities; + /** * The API client object. * @@ -38,7 +40,10 @@ class WC_Payments_Subscription_Minimum_Amount_Handler { */ public function __construct( WC_Payments_API_Client $api_client ) { $this->api_client = $api_client; - add_filter( 'woocommerce_subscriptions_minimum_processable_recurring_amount', [ $this, 'get_minimum_recurring_amount' ], 10, 2 ); + + if ( ! $this->is_subscriptions_plugin_active() ) { + add_filter( 'woocommerce_subscriptions_minimum_processable_recurring_amount', [ $this, 'get_minimum_recurring_amount' ], 10, 2 ); + } } /** diff --git a/includes/subscriptions/class-wc-payments-subscription-service.php b/includes/subscriptions/class-wc-payments-subscription-service.php index 3dc88943780..a30a2413067 100644 --- a/includes/subscriptions/class-wc-payments-subscription-service.php +++ b/includes/subscriptions/class-wc-payments-subscription-service.php @@ -597,7 +597,7 @@ public function maybe_attempt_payment_for_subscription( $subscription, WC_Paymen $wcpay_invoice_id = WC_Payments_Invoice_Service::get_pending_invoice_id( $subscription ); - if ( ! $wcpay_invoice_id ) { + if ( ! $wcpay_invoice_id || ! self::is_wcpay_subscription( $subscription ) ) { return; } @@ -868,7 +868,6 @@ private function update_subscription( WC_Subscription $subscription, array $data $response = null; if ( ! $wcpay_subscription_id ) { - Logger::log( 'There was a problem updating the WCPay subscription in: Subscription does not contain a valid subscription ID.' ); return; } @@ -1045,7 +1044,7 @@ private function validate_subscription_data( $subscription_data ) { * @return bool True if store has active WCPay subscriptions, otherwise false. */ public static function store_has_active_wcpay_subscriptions() { - $results = wcs_get_subscriptions( + $active_wcpay_subscriptions = wcs_get_subscriptions( [ 'subscriptions_per_page' => 1, 'subscription_status' => 'active', @@ -1059,7 +1058,6 @@ public static function store_has_active_wcpay_subscriptions() { ] ); - $store_has_active_wcpay_subscriptions = count( $results ) > 0; - return $store_has_active_wcpay_subscriptions; + return count( $active_wcpay_subscriptions ) > 0; } } diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index 913a7f4116b..7de060a8c8f 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -11,8 +11,10 @@ /** * Handles migrating WCPay Subscriptions to tokenized subscriptions. + * + * This class extends the WCS_Background_Repairer for scheduling and running the individual migration actions. */ -class WC_Payments_Subscriptions_Migrator { +class WC_Payments_Subscriptions_Migrator extends WCS_Background_Repairer { /** * Valid subscription statuses to cancel a subscription at Stripe. @@ -26,11 +28,11 @@ class WC_Payments_Subscriptions_Migrator { * * @var array $migrated_meta_keys */ - private $migrated_meta_keys = [ - '_migrated_wcpay_subscription_id', - '_migrated_wcpay_billing_invoice_id', - '_migrated_wcpay_pending_invoice_id', - '_migrated_wcpay_subscription_discount_ids', + private $meta_keys_to_migrate = [ + WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, + WC_Payments_Invoice_Service::ORDER_INVOICE_ID_KEY, + WC_Payments_Invoice_Service::PENDING_INVOICE_ID_KEY, + WC_Payments_Subscription_Service::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, ]; /** @@ -45,7 +47,28 @@ class WC_Payments_Subscriptions_Migrator { * * @var WC_Payments_Subscription_Migration_Log_Handler */ - private $logger; + protected $logger; + + /** + * The Action Scheduler hook used to find and schedule individual migrations of WCPay Subscriptions. + * + * @var string + */ + public $scheduled_hook = 'wcpay_schedule_subscription_migrations'; + + /** + * The Action Scheduler hook to migrate a WCPay Subscription. + * + * @var string + */ + public $migrate_hook = 'wcpay_migrate_subscription'; + + /** + * The option name used to store a batch identifier for the current migration batch. + * + * @var string + */ + private $migration_batch_identifier_option = 'wcpay_subscription_migration_batch'; /** * Constructor. @@ -56,28 +79,31 @@ public function __construct( $api_client = null ) { $this->api_client = $api_client; $this->logger = new WC_Payments_Subscription_Migration_Log_Handler(); - // Hook onto Scheduled Action to migrate wcpay subscription. - // add_action( 'wcpay_migrate_subscription', [ $this, 'migrate_wcpay_subscription' ] );. - // Don't copy migrated subscription meta keys to related orders. add_filter( 'wc_subscriptions_object_data', [ $this, 'exclude_migrated_meta' ], 10, 1 ); + + // Add manual migration tool to WooCommerce > Status > Tools. + add_filter( 'woocommerce_debug_tools', [ $this, 'add_manual_migration_tool' ] ); + + $this->init(); } /** - * Migrate WCPay Subscription to WC Subscriptions + * Migrates a WCPay Subscription to a tokenized WooPayments subscription powered by WC Subscriptions * * Migration process: - * 1. Validate the request to migrate subscription - * 2. Fetches the subscription from Stripe - * 3. Cancels the subscription at Stripe if it is active - * 4. Update the subscription meta to indicate that it has been migrated - * 5. Add an order note on the subscription + * 1. Validate the request to migrate subscription + * 2. Fetches the subscription from Stripe + * 3. Cancels the subscription at Stripe if it is active + * 4. Update the subscription meta to indicate that it has been migrated + * 5. Add an order note on the subscription * * @param int $subscription_id The ID of the subscription to migrate. */ public function migrate_wcpay_subscription( $subscription_id ) { try { - add_action( 'shutdown', [ $this, 'log_unexpected_shutdown' ] ); + add_action( 'action_scheduler_unexpected_shutdown', [ $this, 'log_unexpected_shutdown' ], 10, 2 ); + add_action( 'action_scheduler_failed_execution', [ $this, 'log_unexpected_action_failure' ], 10, 2 ); $subscription = $this->validate_subscription_to_migrate( $subscription_id ); $wcpay_subscription = $this->fetch_wcpay_subscription( $subscription ); @@ -109,7 +135,8 @@ public function migrate_wcpay_subscription( $subscription_id ) { $this->logger->log( $e->getMessage() ); } - remove_action( 'shutdown', [ $this, 'log_unexpected_shutdown' ] ); + remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'log_unexpected_shutdown' ] ); + remove_action( 'action_scheduler_failed_execution', [ $this, 'log_unexpected_action_failure' ] ); } /** @@ -187,10 +214,10 @@ private function fetch_wcpay_subscription( $subscription ) { * This function checks the status on the subscription at Stripe then cancels it if it's a valid status and logs any errors. * * We skip canceling any subscriptions at Stripe that are: - * - incomplete: the subscription was created but no payment method was added to the subscription - * - incomplete_expired: the incomplete subscription expired after 24hrs of no payment method being added. - * - canceled: the subscription is already canceled - * - unpaid: this status is not used by subscriptions in WooCommerce Payments + * - incomplete: the subscription was created but no payment method was added to the subscription + * - incomplete_expired: the incomplete subscription expired after 24hrs of no payment method being added. + * - canceled: the subscription is already canceled + * - unpaid: this status is not used by subscriptions in WooCommerce Payments * * @param array $wcpay_subscription The subscription data from Stripe. * @@ -216,20 +243,28 @@ private function maybe_cancel_wcpay_subscription( $wcpay_subscription ) { } /** - * Moves the existing WCPay Subscription meta to new meta data prefixed with `_migrated` meta - * and deletes the old meta. + * Migrates WCPay Subscription related metadata to a new key prefixed with `_migrated` and deletes the old meta. * * @param WC_Subscription $subscription The subscription with wcpay meta saved. */ private function update_wcpay_subscription_meta( $subscription ) { $updated = false; - foreach ( $this->migrated_meta_keys as $meta_key ) { - $old_key = str_replace( '_migrated', '', $meta_key ); + /** + * If this subscription is being migrated while scheduling individual actions is on-going, make sure we store meta on the subscription + * so that it's still returned by the query in @see get_items_to_repair() to not affect the limit and pagination. + */ + $migration_start = get_option( $this->migration_batch_identifier_option, 0 ); - if ( $subscription->meta_exists( $old_key ) ) { - $subscription->update_meta_data( $meta_key, $subscription->get_meta( $old_key, true ) ); - $subscription->delete_meta_data( $old_key ); + if ( 0 !== $migration_start ) { + $subscription->update_meta_data( '_wcpay_subscription_migrated_during', $migration_start ); + $updated = true; + } + + foreach ( $this->meta_keys_to_migrate as $meta_key ) { + if ( $subscription->meta_exists( $meta_key ) ) { + $subscription->update_meta_data( '_migrated' . $meta_key, $subscription->get_meta( $meta_key, true ) ); + $subscription->delete_meta_data( $meta_key ); $updated = true; } @@ -243,14 +278,15 @@ private function update_wcpay_subscription_meta( $subscription ) { /** * Returns the subscription status from the WCPay subscription data for logging purposes. * - * When a subscription is on-hold, we don't change the status of the subscription at Stripe, instead, we set - * the subscription as active and set the `pause_collection` behavior to `void` so that the subscription is not charged. + * If a subscription is on-hold in WC we wouldn't have changed the status of the subscription at Stripe, instead, the + * subscription would remain active and set `pause_collection` behavior to `void` so that the subscription is not charged. * - * The purpose of this function is factor in the `paused_collection` value when determining the subscription status at Stripe. + * The purpose of this function is to handle the `paused_collection` value when mapping the subscription status at Stripe to + * a status for logging. * * @param array $wcpay_subscription The subscription data from Stripe. * - * @return string + * @return string The WCPay subscription status for logging purposes. */ private function get_wcpay_subscription_status( $wcpay_subscription ) { if ( empty( $wcpay_subscription['status'] ) ) { @@ -265,28 +301,210 @@ private function get_wcpay_subscription_status( $wcpay_subscription ) { } /** - * Don't copy migrated WCPay subscription metadata to any subscription related orders (renewal/switch/resubscribe). + * Prevents migrated WCPay subscription metadata being copied to subscription related orders (renewal/switch/resubscribe). * * @param array $meta_data The meta data to be copied. - * - * @return array + * @return array The meta data to be copied. */ public function exclude_migrated_meta( $meta_data ) { - foreach ( $this->migrated_meta_keys as $key ) { - unset( $meta_data[ $key ] ); + foreach ( $this->meta_keys_to_migrate as $key ) { + unset( $meta_data[ '_migrated' . $key ] ); } return $meta_data; } /** - * Log any fatal errors occurred while migrating WCPay Subscriptions. + * Logs any fatal errors that occur while processing a scheduled migrate WCPay Subscription action. + * + * @param string $action_id The Action Scheduler action ID. + * @param array $error The error data. */ - public function log_unexpected_shutdown() { - $error = error_get_last(); - + public function log_unexpected_shutdown( $action_id, $error = null ) { if ( ! empty( $error['type'] ) && in_array( $error['type'], [ E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ], true ) ) { $this->logger->log( sprintf( '---- ERROR: %s in %s on line %s.', $error['message'] ?? 'No message', $error['file'] ?? 'no file found', $error['line'] ?? '0' ) ); } } + + /** + * Logs any unexpected failures that occur while processing a scheduled migrate WCPay Subscription action. + * + * @param string $action_id The Action Scheduler action ID. + * @param Exception $exception The exception thrown during action processing. + */ + public function log_unexpected_action_failure( $action_id, $exception ) { + $this->logger->log( sprintf( '---- ERROR: %s', $exception->getMessage() ) ); + } + + /** + * Adds a manual migration tool to WooCommerce > Status > Tools. + * + * This tool is only loaded on stores that have: + * - WC Subscriptions extension activated + * - Subscriptions with WooPayments feature disabled + * - Existing WCPay Subscriptions that can be migrated + * + * @param array $tools List of WC debug tools. + * + * @return array List of WC debug tools. + */ + public function add_manual_migration_tool( $tools ) { + if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() || ! class_exists( 'WC_Subscriptions' ) ) { + return $tools; + } + + // Get number of WCPay Subscriptions that can be migrated. + $wcpay_subscriptions_count = count( + wcs_get_orders_with_meta_query( + [ + 'status' => 'any', + 'return' => 'ids', + 'type' => 'shop_subscription', + 'limit' => -1, + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, + 'compare' => 'EXISTS', + ], + ], + ] + ) + ); + + if ( $wcpay_subscriptions_count < 1 ) { + return $tools; + } + + $disabled = as_next_scheduled_action( $this->scheduled_hook ); + + $tools['migrate_wcpay_subscriptions'] = [ + 'name' => __( 'Migrate Stripe Billing subscriptions', 'woocommerce-payments' ), + 'button' => $disabled ? __( 'Migration in progress', 'woocommerce-payments' ) . '…' : __( 'Migrate Subscriptions', 'woocommerce-payments' ), + 'desc' => sprintf( + // translators: %1$s is a new line character and %3$d is the number of subscriptions. + __( 'This tool will migrate all Stripe Billing subscriptions to tokenized subscriptions with WooPayments.%1$sNumber of Stripe Billing subscriptions found: %2$d', 'woocommerce-payments' ), + '
', + $wcpay_subscriptions_count, + ), + 'callback' => [ $this, 'schedule_migrate_wcpay_subscriptions_action' ], + 'disabled' => $disabled, + 'requires_refresh' => true, + ]; + + return $tools; + } + + /** + * Schedules the initial migration action which signals the start of the migration process. + */ + public function schedule_migrate_wcpay_subscriptions_action() { + if ( as_next_scheduled_action( $this->scheduled_hook ) ) { + return; + } + + update_option( $this->migration_batch_identifier_option, time() ); + + $this->logger->log( 'Started scheduling subscription migrations.' ); + $this->schedule_repair(); + } + + /** + * Override WCS_Background_Repairer methods. + */ + + /** + * Initialize class variables and hooks to handle scheduling and running migration hooks in the background. + */ + public function init() { + $this->repair_hook = $this->migrate_hook; + + parent::init(); + } + + /** + * Schedules an individual action to migrate a subscription. + * + * Overrides the parent class function to make two changes: + * 1. Don't schedule an action if one already exists. + * 2. Schedules the migration to happen in one minute instead of in one hour. + * + * @param int $item The ID of the subscription to migrate. + */ + public function update_item( $item ) { + if ( ! as_next_scheduled_action( $this->migrate_hook, [ 'migrate_subscription' => $item ] ) ) { + as_schedule_single_action( gmdate( 'U' ) + 60, $this->migrate_hook, [ 'migrate_subscription' => $item ] ); + } + + unset( $this->items_to_repair[ $item ] ); + } + + /** + * Migrates an individual subscription. + * + * The repair_item() function is called by the parent class when the individual scheduled action is run. + * This acts as a wrapper for the migrate_wcpay_subscription() function. + * + * @param int $item The ID of the subscription to migrate. + */ + public function repair_item( $item ) { + $this->migrate_wcpay_subscription( $item ); + } + + /** + * Gets a batch of 100 subscriptions to migrate. + * + * Because this function fetches items in batches using limit and paged query args, we need to make sure + * the paging of this query is consistent regardless of whether some subscriptions have been repaired/migrated in between. + * + * To do this, we use the $this->migration_batch_identifier_option value to identify subscriptions previously returned by + * this function that have been migrated so they will still be considered for paging. + * + * @param int $page The page of results to fetch. + * + * @return int[] The IDs of the subscriptions to migrate. + */ + public function get_items_to_repair( $page ) { + $items_to_migrate = wcs_get_orders_with_meta_query( + [ + 'return' => 'ids', + 'type' => 'shop_subscription', + 'limit' => 100, + 'status' => 'any', + 'paged' => $page, + 'order' => 'ASC', + 'orderby' => 'ID', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'relation' => 'OR', + [ + 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, + 'compare' => 'EXISTS', + ], + // We need to include subscriptions which have already been migrated as part of this migration group to make + // sure correct paging is maintained. As subscriptions are migrated they would migrate the WCPay subscription ID + // meta key and therefore fall out of this query's scope - messing with the paging of future queries. + // Subscriptions with the `migrated_during` meta aren't expected to be returned by this query, they are included to pad out the earlier pages. + [ + 'key' => '_wcpay_subscription_migrated_during', + 'value' => get_option( $this->migration_batch_identifier_option, 0 ), + 'compare' => '=', + ], + ], + ] + ); + + if ( empty( $items_to_migrate ) ) { + $this->logger->log( 'Finished scheduling subscription migrations.' ); + } + + return $items_to_migrate; + } + + /** + * Runs any actions that need to handle the completion of the migration. + */ + protected function unschedule_background_updates() { + parent::unschedule_background_updates(); + + delete_option( $this->migration_batch_identifier_option ); + } } diff --git a/includes/subscriptions/class-wc-payments-subscriptions.php b/includes/subscriptions/class-wc-payments-subscriptions.php index 7750469f500..69de9ac83e0 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions.php +++ b/includes/subscriptions/class-wc-payments-subscriptions.php @@ -83,6 +83,11 @@ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Cus new WC_Payments_Subscriptions_Empty_State_Manager( $account ); new WC_Payments_Subscriptions_Onboarding_Handler( $account ); new WC_Payments_Subscription_Minimum_Amount_Handler( $api_client ); + + if ( WC_Payments_Features::is_subscription_migration_enabled() && class_exists( 'WCS_Background_Repairer' ) ) { + include_once __DIR__ . '/class-wc-payments-subscriptions-migrator.php'; + new WC_Payments_Subscriptions_Migrator( $api_client ); + } } /** diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index ee1332e6c38..9c18c491e0b 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -116,8 +116,11 @@ function() { * * Init'ing the subscriptions-core loads all subscriptions class and hooks, which breaks existing WCPAY unit tests. * WCPAY already mocks the WC Subscriptions classes/functions it needs so there's no need to load them anyway. + * + * This function should only be used to load any mocked Subscriptions Core classes that need to be loaded before the PHPUnit FileLoader. */ function wcpay_init_subscriptions_core() { + require_once __DIR__ . '/helpers/class-wcs-helper-background-repairer.php'; } // Placeholder for the test container. diff --git a/tests/unit/helpers/class-wcs-helper-background-repairer.php b/tests/unit/helpers/class-wcs-helper-background-repairer.php new file mode 100644 index 00000000000..f97ccac6347 --- /dev/null +++ b/tests/unit/helpers/class-wcs-helper-background-repairer.php @@ -0,0 +1,22 @@ +payment_method = 'woocommerce_payments'; + $mock_subscription->update_meta_data( self::SUBSCRIPTION_ID_META_KEY, 'sub_123abc' ); + $mock_subscription->save(); + // With the following calls to `maybe_record_first_invoice_payment()`, we only expect 2 calls (see Positive Cases) to result in an API call. $this->mock_api_client->expects( $this->exactly( 2 ) ) ->method( 'charge_invoice' ) diff --git a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php index 1eea00d4e41..550ce8da03d 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php +++ b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php @@ -612,6 +612,9 @@ public function test_maybe_attempt_payment_for_subscription() { $mock_pending_invoice_id = 'wcpay_pending_invoice_idtest123'; $mock_subscription->update_meta_data( WC_Payments_Invoice_Service_Test::PENDING_INVOICE_ID_KEY, $mock_pending_invoice_id ); + $mock_subscription->update_meta_data( self::SUBSCRIPTION_ID_META_KEY, 'sub_123' ); + $mock_subscription->payment_method = 'woocommerce_payments'; + $mock_subscription->save(); WC_Subscriptions::set_wcs_is_subscription( function ( $subscription ) { From 51879eadc5dd08831e80ed1675b1aeb5729cacd3 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:20:46 +1000 Subject: [PATCH 18/84] Migrate DetailsLink component to TS to improve code quality (#7086) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> --- changelog/dev-details-link-ts-migration | 4 ++ client/components/details-link/index.js | 24 --------- client/components/details-link/index.tsx | 53 +++++++++++++++++++ .../{index.js.snap => index.test.tsx.snap} | 0 .../test/{index.js => index.test.tsx} | 1 + 5 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 changelog/dev-details-link-ts-migration delete mode 100644 client/components/details-link/index.js create mode 100644 client/components/details-link/index.tsx rename client/components/details-link/test/__snapshots__/{index.js.snap => index.test.tsx.snap} (100%) rename client/components/details-link/test/{index.js => index.test.tsx} (96%) diff --git a/changelog/dev-details-link-ts-migration b/changelog/dev-details-link-ts-migration new file mode 100644 index 00000000000..daaa601b05b --- /dev/null +++ b/changelog/dev-details-link-ts-migration @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate DetailsLink component to TypeScript to improve code quality diff --git a/client/components/details-link/index.js b/client/components/details-link/index.js deleted file mode 100644 index 1571f5be74c..00000000000 --- a/client/components/details-link/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @format **/ - -/** - * External dependencies - */ -import InfoOutlineIcon from 'gridicons/dist/info-outline'; -import { Link } from '@woocommerce/components'; -import { getAdminUrl } from 'wcpay/utils'; - -export const getDetailsURL = ( id, parentSegment ) => - getAdminUrl( { - page: 'wc-admin', - path: `/payments/${ parentSegment }/details`, - id, - } ); - -const DetailsLink = ( { id, parentSegment } ) => - id ? ( - - - - ) : null; - -export default DetailsLink; diff --git a/client/components/details-link/index.tsx b/client/components/details-link/index.tsx new file mode 100644 index 00000000000..599247ce6c5 --- /dev/null +++ b/client/components/details-link/index.tsx @@ -0,0 +1,53 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import InfoOutlineIcon from 'gridicons/dist/info-outline'; +import { Link } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { getAdminUrl } from 'wcpay/utils'; + +/** + * The parent segment is the first part of the URL after the /payments/ path. + */ +type ParentSegment = 'deposits' | 'transactions' | 'disputes'; + +export const getDetailsURL = ( + /** + * The ID of the object to link to. + */ + id: string, + /** + * The parent segment is the first part of the URL after the /payments/ path. + */ + parentSegment: ParentSegment +): string => + getAdminUrl( { + page: 'wc-admin', + path: `/payments/${ parentSegment }/details`, + id, + } ); + +interface DetailsLinkProps { + /** + * The ID of the object to link to. + */ + id?: string; + /** + * The parent segment is the first part of the URL after the /payments/ path. + */ + parentSegment: ParentSegment; +} +const DetailsLink: React.FC< DetailsLinkProps > = ( { id, parentSegment } ) => + id ? ( + + + + ) : null; + +export default DetailsLink; diff --git a/client/components/details-link/test/__snapshots__/index.js.snap b/client/components/details-link/test/__snapshots__/index.test.tsx.snap similarity index 100% rename from client/components/details-link/test/__snapshots__/index.js.snap rename to client/components/details-link/test/__snapshots__/index.test.tsx.snap diff --git a/client/components/details-link/test/index.js b/client/components/details-link/test/index.test.tsx similarity index 96% rename from client/components/details-link/test/index.js rename to client/components/details-link/test/index.test.tsx index 6c39ea4d343..172eeac2454 100644 --- a/client/components/details-link/test/index.js +++ b/client/components/details-link/test/index.test.tsx @@ -3,6 +3,7 @@ /** * External dependencies */ +import React from 'react'; import { render } from '@testing-library/react'; /** From e00a4af880a9fa9d1d764e2b9ffe93b8ada10e26 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:09:18 +1000 Subject: [PATCH 19/84] =?UTF-8?q?Use=20client-side=20routing=20for=20the?= =?UTF-8?q?=20transaction=20details=20`ch=5F`=20=E2=86=92=20`pi=5F`=20redi?= =?UTF-8?q?rect=20(#7089)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> --- changelog/fix-improve-transaction-details-redirect | 4 ++++ client/payment-details/charge-details/index.tsx | 3 ++- client/payment-details/test/index.test.tsx | 11 +++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-improve-transaction-details-redirect diff --git a/changelog/fix-improve-transaction-details-redirect b/changelog/fix-improve-transaction-details-redirect new file mode 100644 index 00000000000..d61f18b2f6c --- /dev/null +++ b/changelog/fix-improve-transaction-details-redirect @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Improve the transaction details redirect user-experience by using client-side routing. diff --git a/client/payment-details/charge-details/index.tsx b/client/payment-details/charge-details/index.tsx index 25c5794fc9d..9d4ad2a141b 100644 --- a/client/payment-details/charge-details/index.tsx +++ b/client/payment-details/charge-details/index.tsx @@ -2,6 +2,7 @@ * External dependencies */ import React, { useEffect } from 'react'; +import { getHistory } from '@woocommerce/navigation'; /** * Internal dependencies @@ -54,7 +55,7 @@ const PaymentChargeDetails: React.FC< PaymentChargeDetailsProps > = ( { id: data.payment_intent, } ); - window.location.href = url; + getHistory().replace( url ); } }, [ data, isChargeId ] ); diff --git a/client/payment-details/test/index.test.tsx b/client/payment-details/test/index.test.tsx index d0f374e8d71..ce879aabf51 100644 --- a/client/payment-details/test/index.test.tsx +++ b/client/payment-details/test/index.test.tsx @@ -40,6 +40,7 @@ jest.mock( '@wordpress/data', () => ( { useSelect: jest.fn(), } ) ); +const mockHistoryReplace = jest.fn(); jest.mock( '@woocommerce/navigation', () => ( { getQuery: () => { return { @@ -47,6 +48,9 @@ jest.mock( '@woocommerce/navigation', () => ( { type_is: '', }; }, + getHistory: () => ( { + replace: mockHistoryReplace, + } ), addHistoryListener: jest.fn(), } ) ); @@ -151,6 +155,7 @@ describe( 'Payment details page', () => { Object.defineProperty( window, 'location', { value: { href: 'http://example.com' }, } ); + mockHistoryReplace.mockReset(); } ); afterAll( () => { @@ -182,14 +187,12 @@ describe( 'Payment details page', () => { it( 'should redirect from ch_mock to pi_mock', () => { render( ); - expect( window.location.href ).toEqual( redirectUrl ); + expect( mockHistoryReplace ).toHaveBeenCalledWith( redirectUrl ); } ); it( 'should not redirect with a payment intent ID as query param', () => { - const { href } = window.location; - render( ); - expect( window.location.href ).toEqual( href ); + expect( mockHistoryReplace ).not.toHaveBeenCalled(); } ); } ); From 39a70c0c2bdc16f3e19e1e57387092f4a479b92a Mon Sep 17 00:00:00 2001 From: Dat Hoang Date: Fri, 1 Sep 2023 08:39:30 +0700 Subject: [PATCH 20/84] RPP - Load payment methods through the request class (#7100) --- changelog/rpp-6685-load-payment-methods | 4 + src/Internal/Payment/PaymentRequest.php | 61 ++++++++- .../Payment/PaymentRequestException.php | 14 ++ .../Internal/Payment/PaymentRequestTest.php | 127 +++++++++++++++++- 4 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 changelog/rpp-6685-load-payment-methods create mode 100644 src/Internal/Payment/PaymentRequestException.php diff --git a/changelog/rpp-6685-load-payment-methods b/changelog/rpp-6685-load-payment-methods new file mode 100644 index 00000000000..82d45e02d4c --- /dev/null +++ b/changelog/rpp-6685-load-payment-methods @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Load payment methods through the request class (re-engineering payment process). diff --git a/src/Internal/Payment/PaymentRequest.php b/src/Internal/Payment/PaymentRequest.php index 8c73b733a1d..838ae8bb8e5 100644 --- a/src/Internal/Payment/PaymentRequest.php +++ b/src/Internal/Payment/PaymentRequest.php @@ -7,6 +7,14 @@ namespace WCPay\Internal\Payment; +use WC_Payment_Gateway_WCPay; +use WC_Payment_Token; +use WC_Payment_Tokens; +use WCPay\Internal\Payment\PaymentMethod\NewPaymentMethod; +use WCPay\Internal\Payment\PaymentMethod\PaymentMethodInterface; +use WCPay\Internal\Payment\PaymentMethod\SavedPaymentMethod; +use WCPay\Internal\Proxy\LegacyProxy; + /** * Class for loading, sanitizing, and escaping data from payment requests. */ @@ -19,11 +27,20 @@ class PaymentRequest { private $request; /** - * The request array. + * Legacy proxy. + * + * @var LegacyProxy + */ + private $legacy_proxy; + + /** + * Extract information from request data. * - * @param array|null $request Request data, this can be $_POST, or WP_REST_Request::get_params(). + * @param LegacyProxy $legacy_proxy Legacy proxy. + * @param array|null $request Request data, this can be $_POST, or WP_REST_Request::get_params(). */ - public function __construct( array $request = null ) { + public function __construct( LegacyProxy $legacy_proxy, array $request = null ) { + $this->legacy_proxy = $legacy_proxy; // phpcs:ignore WordPress.Security.NonceVerification.Missing $this->request = $request ?? $_POST; } @@ -89,4 +106,42 @@ public function get_payment_method_id(): ?string { ? sanitize_text_field( wp_unslash( ( $this->request['payment_method_id'] ) ) ) : null; } + + /** + * Gets payment method object from request. + * + * @throws PaymentRequestException + */ + public function get_payment_method(): PaymentMethodInterface { + $request = $this->request; + + $is_woopayment_selected = isset( $request['payment_method'] ) && WC_Payment_Gateway_WCPay::GATEWAY_ID === $request['payment_method']; + if ( ! $is_woopayment_selected ) { + throw new PaymentRequestException( __( 'WooPayments is not used during checkout.', 'woocommerce-payments' ) ); + } + + $token_request_key = 'wc-' . WC_Payment_Gateway_WCPay::GATEWAY_ID . '-payment-token'; + if ( isset( $request[ $token_request_key ] ) && 'new' !== $request[ $token_request_key ] ) { + $token_id = absint( wp_unslash( $request [ $token_request_key ] ) ); + + /** + * Retrieved token object. + * + * @var null| WC_Payment_Token $token + */ + $token = $this->legacy_proxy->call_static( WC_Payment_Tokens::class, 'get', $token_id ); + + if ( is_null( $token ) ) { + throw new PaymentRequestException( __( 'Invalid saved payment method (token) ID.', 'woocommerce-payments' ) ); + } + return new SavedPaymentMethod( $token ); + } + + if ( ! empty( $request['wcpay-payment-method'] ) ) { + $payment_method = sanitize_text_field( wp_unslash( $request['wcpay-payment-method'] ) ); + return new NewPaymentMethod( $payment_method ); + } + + throw new PaymentRequestException( __( 'No valid payment method was selected.', 'woocommerce-payments' ) ); + } } diff --git a/src/Internal/Payment/PaymentRequestException.php b/src/Internal/Payment/PaymentRequestException.php new file mode 100644 index 00000000000..d0583457340 --- /dev/null +++ b/src/Internal/Payment/PaymentRequestException.php @@ -0,0 +1,14 @@ +mock_legacy_proxy = $this->createMock( LegacyProxy::class ); + } + /** * @dataProvider provider_text_string_param */ public function test_get_fraud_prevention_token( ?string $value, ?string $expected ) { $request = is_null( $value ) ? [] : [ 'wcpay-fraud-prevention-token' => $value ]; - $this->sut = new PaymentRequest( $request ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); $this->assertSame( $expected, $this->sut->get_fraud_prevention_token() ); } @@ -35,7 +54,7 @@ public function test_get_fraud_prevention_token( ?string $value, ?string $expect */ public function test_get_woopay_intent_id( ?string $value, ?string $expected ) { $request = is_null( $value ) ? [] : [ 'platform-checkout-intent' => $value ]; - $this->sut = new PaymentRequest( $request ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); $this->assertSame( $expected, $this->sut->get_woopay_intent_id() ); } @@ -44,7 +63,7 @@ public function test_get_woopay_intent_id( ?string $value, ?string $expected ) { */ public function test_get_intent_id( ?string $value, ?string $expected ) { $request = is_null( $value ) ? [] : [ 'intent_id' => $value ]; - $this->sut = new PaymentRequest( $request ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); $this->assertSame( $expected, $this->sut->get_intent_id() ); } @@ -53,7 +72,7 @@ public function test_get_intent_id( ?string $value, ?string $expected ) { */ public function test_get_payment_method_id( ?string $value, ?string $expected ) { $request = is_null( $value ) ? [] : [ 'payment_method_id' => $value ]; - $this->sut = new PaymentRequest( $request ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); $this->assertSame( $expected, $this->sut->get_payment_method_id() ); } @@ -100,7 +119,7 @@ public function provider_text_string_for_bool_representation(): array { */ public function test_is_woopay_preflight_check( ?string $value, bool $expected ) { $request = is_null( $value ) ? [] : [ 'is-woopay-preflight-check' => $value ]; - $this->sut = new PaymentRequest( $request ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); $this->assertSame( $expected, $this->sut->is_woopay_preflight_check() ); } @@ -109,7 +128,7 @@ public function test_is_woopay_preflight_check( ?string $value, bool $expected ) */ public function test_get_order_id( ?string $value, ?int $expected ) { $request = is_null( $value ) ? [] : [ 'order_id' => $value ]; - $this->sut = new PaymentRequest( $request ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); $this->assertSame( $expected, $this->sut->get_order_id() ); } @@ -129,4 +148,100 @@ public function provider_test_order_id(): array { ], ]; } + + /** + * @dataProvider provider_get_payment_method_throw_exception_due_to_miss_payment_method_param + */ + public function test_get_payment_method_throw_exception_due_to_miss_payment_method_param( array $request ) { + $this->expectException( PaymentRequestException::class ); + $this->expectExceptionMessage( 'WooPayments is not used during checkout.' ); + $this->sut = new PaymentRequest( $this->mock_legacy_proxy, $request ); + $this->sut->get_payment_method(); + } + + public function provider_get_payment_method_throw_exception_due_to_miss_payment_method_param(): array { + return [ + 'empty payment_method param' => [ [ 'payment_method' => '' ] ], + 'not WooPayments method' => [ + [ 'payment_method' => 'NOT_woocommerce_payments' ], + ], + ]; + } + + public function test_get_payment_throw_exception_due_to_invalid_token_id() { + $request = [ + 'payment_method' => 'woocommerce_payments', + 'wc-woocommerce_payments-payment-token' => 123456, + ]; + $this->sut = new PaymentRequest( + $this->mock_legacy_proxy, + $request + ); + + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_static' ) + ->with( WC_Payment_Tokens::class, 'get', 123456 ) + ->willReturn( null ); + $this->expectException( PaymentRequestException::class ); + $this->expectExceptionMessage( 'Invalid saved payment method (token) ID' ); + + $this->sut->get_payment_method(); + } + + public function test_get_payment_return_saved_payment_method() { + // Prepare. + $request = [ + 'payment_method' => 'woocommerce_payments', + 'wc-woocommerce_payments-payment-token' => 123456, + ]; + $this->sut = new PaymentRequest( + $this->mock_legacy_proxy, + $request + ); + $mock_token = $this->createMock( WC_Payment_Token::class ); + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_static' ) + ->with( WC_Payment_Tokens::class, 'get', 123456 ) + ->willReturn( $mock_token ); + + // Act. + $pm = $this->sut->get_payment_method(); + + // Assert: correct type of instance. + $this->assertInstanceOf( SavedPaymentMethod::class, $pm ); + + // Assert: the same payment method string saved in the token object. + $mock_token->expects( $this->once() ) + ->method( 'get_token' ) + ->willReturn( 'pm_saved_method' ); + $this->assertSame( $pm->get_id(), 'pm_saved_method' ); + } + + public function test_get_payment_return_new_payment_method() { + $request = [ + 'payment_method' => 'woocommerce_payments', + 'wcpay-payment-method' => 'pm_mock', + ]; + $this->sut = new PaymentRequest( + $this->mock_legacy_proxy, + $request + ); + $pm = $this->sut->get_payment_method(); + + $this->assertInstanceOf( NewPaymentMethod::class, $pm ); + $this->assertSame( 'pm_mock', $pm->get_id() ); + } + + public function test_get_payment_method_throw_exception_due_to_no_payment_method_attached() { + $request = [ 'payment_method' => 'woocommerce_payments' ]; + $this->sut = new PaymentRequest( + $this->mock_legacy_proxy, + $request + ); + + $this->expectException( PaymentRequestException::class ); + $this->expectExceptionMessage( 'No valid payment method was selected.' ); + + $this->sut->get_payment_method(); + } } From ce50c7aa31446716b2865e1e1db075a7e18d06a4 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Fri, 1 Sep 2023 10:40:11 +0300 Subject: [PATCH 21/84] RPP: Factor flags (#7035) --- changelog/rpp-6679-factor-flags | 4 + includes/class-database-cache.php | 29 +- includes/class-wc-payment-gateway-wcpay.php | 116 ++++++- ...ayments-payment-request-button-handler.php | 4 + includes/class-wc-payments.php | 1 + includes/core/server/class-request.php | 73 ++--- .../class-get-payment-process-factors.php | 32 ++ .../class-wc-payments-api-client.php | 1 + .../PaymentsServiceProvider.php | 6 + src/Internal/Payment/Factor.php | 134 ++++++++ src/Internal/Payment/Router.php | 115 +++++++ tests/unit/bootstrap.php | 6 +- tests/unit/src/ContainerTest.php | 12 + .../unit/src/Internal/Payment/FactorTest.php | 45 +++ .../unit/src/Internal/Payment/RouterTest.php | 274 ++++++++++++++++ .../test-class-wc-payment-gateway-wcpay.php | 298 ++++++++++++++++++ 16 files changed, 1102 insertions(+), 48 deletions(-) create mode 100644 changelog/rpp-6679-factor-flags create mode 100644 includes/core/server/request/class-get-payment-process-factors.php create mode 100644 src/Internal/Payment/Factor.php create mode 100644 src/Internal/Payment/Router.php create mode 100644 tests/unit/src/Internal/Payment/FactorTest.php create mode 100644 tests/unit/src/Internal/Payment/RouterTest.php diff --git a/changelog/rpp-6679-factor-flags b/changelog/rpp-6679-factor-flags new file mode 100644 index 00000000000..60d7f3c7a0a --- /dev/null +++ b/changelog/rpp-6679-factor-flags @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Adding factor flags to control when to enter the new payment process. diff --git a/includes/class-database-cache.php b/includes/class-database-cache.php index 641efaf08a6..08a22ab66b8 100644 --- a/includes/class-database-cache.php +++ b/includes/class-database-cache.php @@ -13,11 +13,22 @@ * A class for caching data as an option in the database. */ class Database_Cache { - const ACCOUNT_KEY = 'wcpay_account_data'; - const ONBOARDING_FIELDS_DATA_KEY = 'wcpay_onboarding_fields_data'; - const BUSINESS_TYPES_KEY = 'wcpay_business_types_data'; - const CURRENCIES_KEY = 'wcpay_multi_currency_cached_currencies'; - const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_customer_currencies'; + const ACCOUNT_KEY = 'wcpay_account_data'; + const ONBOARDING_FIELDS_DATA_KEY = 'wcpay_onboarding_fields_data'; + const BUSINESS_TYPES_KEY = 'wcpay_business_types_data'; + const CURRENCIES_KEY = 'wcpay_multi_currency_cached_currencies'; + const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_customer_currencies'; + const PAYMENT_PROCESS_FACTORS_KEY = 'wcpay_payment_process_factors'; + + /** + * Refresh during AJAX calls is avoided, but white-listing + * a key here will allow the refresh to happen. + * + * @var string[] + */ + const AJAX_ALLOWED_KEYS = [ + self::PAYMENT_PROCESS_FACTORS_KEY, + ]; /** * Payment methods cache key prefix. Used in conjunction with the customer_id to cache a customer's payment methods. @@ -216,7 +227,10 @@ private function should_refresh_cache( string $key, $cache_contents, callable $v } // Do not refresh if doing ajax or the refresh has been disabled (running an AS job). - if ( defined( 'DOING_CRON' ) || wp_doing_ajax() || $this->refresh_disabled ) { + if ( + defined( 'DOING_CRON' ) + || ( wp_doing_ajax() && ! in_array( $key, self::AJAX_ALLOWED_KEYS, true ) ) + || $this->refresh_disabled ) { return false; } @@ -330,6 +344,9 @@ private function get_ttl( string $key, array $cache_contents ): int { case self::CONNECT_INCENTIVE_KEY: $ttl = $cache_contents['data']['ttl'] ?? HOUR_IN_SECONDS * 6; break; + case self::PAYMENT_PROCESS_FACTORS_KEY: + $ttl = 2 * HOUR_IN_SECONDS; + break; default: // Default to 24h. $ttl = DAY_IN_SECONDS; diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 5f3912b4b1d..754629e0ec2 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -42,6 +42,8 @@ use WCPay\Session_Rate_Limiter; use WCPay\Tracker; use WCPay\Internal\Service\PaymentProcessingService; +use WCPay\Internal\Payment\Factor; +use WCPay\Internal\Payment\Router; /** * Gateway class for WooPayments @@ -706,6 +708,111 @@ public function payment_fields() { do_action( 'wc_payments_add_payment_fields' ); } + /** + * Checks whether the new payment process should be used to pay for a given order. + * + * @param WC_Order $order Order that's being paid. + * @return bool + */ + public function should_use_new_process( WC_Order $order ) { + $order_id = $order->get_id(); + + // The new process us under active development, and not ready for production yet. + if ( ! WC_Payments::mode()->is_dev() ) { + return false; + } + + // This array will contain all factors, present during checkout. + $factors = [ + /** + * The new payment process is a factor itself. + * Even if no other factors are present, this will make entering + * the new payment process possible only if this factor is allowed. + */ + Factor::NEW_PAYMENT_PROCESS(), + ]; + + // If there is a token in the request, we're using a saved PM. + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $using_saved_payment_method = ! empty( Payment_Information::get_token_from_request( $_POST ) ); + if ( $using_saved_payment_method ) { + $factors[] = Factor::USE_SAVED_PM(); + } + + // The PM should be saved when chosen, or when it's a recurrent payment, but not if already saved. + $save_payment_method = ! $using_saved_payment_method && ( + // phpcs:ignore WordPress.Security.NonceVerification.Missing + ! empty( $_POST[ 'wc-' . static::GATEWAY_ID . '-new-payment-method' ] ) + || $this->is_payment_recurring( $order_id ) + ); + if ( $save_payment_method ) { + $factors[] = Factor::SAVE_PM(); + } + + // In case amount is 0 and we're not saving the payment method, we won't be using intents and can confirm the order payment. + if ( + apply_filters( + 'wcpay_confirm_without_payment_intent', + $order->get_total() <= 0 && ! $save_payment_method + ) + ) { + $factors[] = Factor::NO_PAYMENT(); + } + + // Subscription (both WCPay and WCSubs) if when the order contains one. + if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) { + $factors[] = Factor::SUBSCRIPTION_SIGNUP(); + } + + // WooPay might change how payment fields were loaded. + if ( + $this->woopay_util->should_enable_woopay( $this ) + && $this->woopay_util->should_enable_woopay_on_cart_or_checkout() + ) { + $factors[] = Factor::WOOPAY_ENABLED(); + } + + // WooPay payments are indicated by the platform checkout intent. + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( isset( $_POST['platform-checkout-intent'] ) ) { + $factors[] = Factor::WOOPAY_PAYMENT(); + } + + // Check whether the customer is signining up for a WCPay subscription. + if ( + function_exists( 'wcs_order_contains_subscription' ) + && wcs_order_contains_subscription( $order_id ) + && WC_Payments_Features::is_wcpay_subscriptions_enabled() + && ! $this->is_subscriptions_plugin_active() + ) { + $factors[] = Factor::WCPAY_SUBSCRIPTION_SIGNUP(); + } + + if ( $this instanceof UPE_Split_Payment_Gateway ) { + $factors[] = Factor::DEFERRED_INTENT_SPLIT_UPE(); + } + + if ( defined( 'WCPAY_PAYMENT_REQUEST_CHECKOUT' ) && WCPAY_PAYMENT_REQUEST_CHECKOUT ) { + $factors[] = Factor::PAYMENT_REQUEST(); + } + + $router = wcpay_get_container()->get( Router::class ); + return $router->should_use_new_payment_process( $factors ); + } + + /** + * Checks whether the new payment process should be entered, + * and if the answer is yes, uses it and returns the result. + * + * @param WC_Order $order Order that needs payment. + * @return array|null Array if processed, null if the new process is not supported. + */ + public function new_process_payment( WC_Order $order ) { + // Important: No factors are provided here, they were meant just for `Feature`. + $service = wcpay_get_container()->get( PaymentProcessingService::class ); + return $service->process_payment( $order->get_id() ); + } + /** * Process the payment for a given order. * @@ -716,14 +823,13 @@ public function payment_fields() { * @throws Exception Error processing the payment. */ public function process_payment( $order_id ) { + $order = wc_get_order( $order_id ); - if ( defined( 'WCPAY_NEW_PROCESS' ) && true === WCPAY_NEW_PROCESS ) { - $new_process = wcpay_get_container()->get( PaymentProcessingService::class ); - return $new_process->process_payment( $order_id ); + // Use the new payment process if allowed. + if ( $this->should_use_new_process( $order ) ) { + return $this->new_process_payment( $order ); } - $order = wc_get_order( $order_id ); - try { if ( 20 < strlen( $order->get_billing_phone() ) ) { throw new Process_Payment_Exception( diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 04f8b53aec6..56179967d20 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -1379,6 +1379,10 @@ public function ajax_create_order() { define( 'WOOCOMMERCE_CHECKOUT', true ); } + if ( ! defined( 'WCPAY_PAYMENT_REQUEST_CHECKOUT' ) ) { + define( 'WCPAY_PAYMENT_REQUEST_CHECKOUT', true ); + } + // In case the state is required, but is missing, add a more descriptive error notice. $this->validate_state(); diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index c80b1f19664..b77b3589e0a 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -336,6 +336,7 @@ public static function init() { include_once __DIR__ . '/core/server/request/trait-use-test-mode-only-when-dev-mode.php'; include_once __DIR__ . '/core/server/request/class-generic.php'; include_once __DIR__ . '/core/server/request/class-get-intention.php'; + include_once __DIR__ . '/core/server/request/class-get-payment-process-factors.php'; include_once __DIR__ . '/core/server/request/class-create-intention.php'; include_once __DIR__ . '/core/server/request/class-update-intention.php'; include_once __DIR__ . '/core/server/request/class-capture-intention.php'; diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index 97e2162ab51..51c1d82e5fa 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -101,43 +101,44 @@ abstract class Request { * @var string[] */ private $route_list = [ - WC_Payments_API_Client::ACCOUNTS_API => 'accounts', - WC_Payments_API_Client::CAPABILITIES_API => 'accounts/capabilities', - WC_Payments_API_Client::WOOPAY_ACCOUNTS_API => 'accounts/platform_checkout', - WC_Payments_API_Client::WOOPAY_COMPATIBILITY_API => 'woopay/compatibility', - WC_Payments_API_Client::APPLE_PAY_API => 'apple_pay', - WC_Payments_API_Client::CHARGES_API => 'charges', - WC_Payments_API_Client::CONN_TOKENS_API => 'terminal/connection_tokens', - WC_Payments_API_Client::TERMINAL_LOCATIONS_API => 'terminal/locations', - WC_Payments_API_Client::CUSTOMERS_API => 'customers', - WC_Payments_API_Client::CURRENCY_API => 'currency', - WC_Payments_API_Client::INTENTIONS_API => 'intentions', - WC_Payments_API_Client::REFUNDS_API => 'refunds', - WC_Payments_API_Client::DEPOSITS_API => 'deposits', - WC_Payments_API_Client::TRANSACTIONS_API => 'transactions', - WC_Payments_API_Client::DISPUTES_API => 'disputes', - WC_Payments_API_Client::FILES_API => 'files', - WC_Payments_API_Client::ONBOARDING_API => 'onboarding', - WC_Payments_API_Client::TIMELINE_API => 'timeline', - WC_Payments_API_Client::PAYMENT_METHODS_API => 'payment_methods', - WC_Payments_API_Client::SETUP_INTENTS_API => 'setup_intents', - WC_Payments_API_Client::TRACKING_API => 'tracking', - WC_Payments_API_Client::PRODUCTS_API => 'products', - WC_Payments_API_Client::PRICES_API => 'products/prices', - WC_Payments_API_Client::INVOICES_API => 'invoices', - WC_Payments_API_Client::SUBSCRIPTIONS_API => 'subscriptions', - WC_Payments_API_Client::SUBSCRIPTION_ITEMS_API => 'subscriptions/items', - WC_Payments_API_Client::READERS_CHARGE_SUMMARY => 'reader-charges/summary', - WC_Payments_API_Client::TERMINAL_READERS_API => 'terminal/readers', + WC_Payments_API_Client::ACCOUNTS_API => 'accounts', + WC_Payments_API_Client::CAPABILITIES_API => 'accounts/capabilities', + WC_Payments_API_Client::WOOPAY_ACCOUNTS_API => 'accounts/platform_checkout', + WC_Payments_API_Client::WOOPAY_COMPATIBILITY_API => 'woopay/compatibility', + WC_Payments_API_Client::APPLE_PAY_API => 'apple_pay', + WC_Payments_API_Client::CHARGES_API => 'charges', + WC_Payments_API_Client::CONN_TOKENS_API => 'terminal/connection_tokens', + WC_Payments_API_Client::TERMINAL_LOCATIONS_API => 'terminal/locations', + WC_Payments_API_Client::CUSTOMERS_API => 'customers', + WC_Payments_API_Client::CURRENCY_API => 'currency', + WC_Payments_API_Client::INTENTIONS_API => 'intentions', + WC_Payments_API_Client::REFUNDS_API => 'refunds', + WC_Payments_API_Client::DEPOSITS_API => 'deposits', + WC_Payments_API_Client::TRANSACTIONS_API => 'transactions', + WC_Payments_API_Client::DISPUTES_API => 'disputes', + WC_Payments_API_Client::FILES_API => 'files', + WC_Payments_API_Client::ONBOARDING_API => 'onboarding', + WC_Payments_API_Client::TIMELINE_API => 'timeline', + WC_Payments_API_Client::PAYMENT_METHODS_API => 'payment_methods', + WC_Payments_API_Client::SETUP_INTENTS_API => 'setup_intents', + WC_Payments_API_Client::TRACKING_API => 'tracking', + WC_Payments_API_Client::PAYMENT_PROCESS_CONFIG_API => 'payment_process_config', + WC_Payments_API_Client::PRODUCTS_API => 'products', + WC_Payments_API_Client::PRICES_API => 'products/prices', + WC_Payments_API_Client::INVOICES_API => 'invoices', + WC_Payments_API_Client::SUBSCRIPTIONS_API => 'subscriptions', + WC_Payments_API_Client::SUBSCRIPTION_ITEMS_API => 'subscriptions/items', + WC_Payments_API_Client::READERS_CHARGE_SUMMARY => 'reader-charges/summary', + WC_Payments_API_Client::TERMINAL_READERS_API => 'terminal/readers', WC_Payments_API_Client::MINIMUM_RECURRING_AMOUNT_API => 'subscriptions/minimum_amount', - WC_Payments_API_Client::CAPITAL_API => 'capital', - WC_Payments_API_Client::WEBHOOK_FETCH_API => 'webhook/failed_events', - WC_Payments_API_Client::DOCUMENTS_API => 'documents', - WC_Payments_API_Client::VAT_API => 'vat', - WC_Payments_API_Client::LINKS_API => 'links', - WC_Payments_API_Client::AUTHORIZATIONS_API => 'authorizations', - WC_Payments_API_Client::FRAUD_OUTCOMES_API => 'fraud_outcomes', - WC_Payments_API_Client::FRAUD_RULESET_API => 'fraud_ruleset', + WC_Payments_API_Client::CAPITAL_API => 'capital', + WC_Payments_API_Client::WEBHOOK_FETCH_API => 'webhook/failed_events', + WC_Payments_API_Client::DOCUMENTS_API => 'documents', + WC_Payments_API_Client::VAT_API => 'vat', + WC_Payments_API_Client::LINKS_API => 'links', + WC_Payments_API_Client::AUTHORIZATIONS_API => 'authorizations', + WC_Payments_API_Client::FRAUD_OUTCOMES_API => 'fraud_outcomes', + WC_Payments_API_Client::FRAUD_RULESET_API => 'fraud_ruleset', ]; /** diff --git a/includes/core/server/request/class-get-payment-process-factors.php b/includes/core/server/request/class-get-payment-process-factors.php new file mode 100644 index 00000000000..a5e230ffa83 --- /dev/null +++ b/includes/core/server/request/class-get-payment-process-factors.php @@ -0,0 +1,32 @@ +addShared( PaymentProcessingService::class ); + $container->addShared( Router::class ) + ->addArgument( Database_Cache::class ); + $container->addShared( ExampleService::class ); $container->addShared( ExampleServiceWithDependencies::class ) ->addArgument( ExampleService::class ) diff --git a/src/Internal/Payment/Factor.php b/src/Internal/Payment/Factor.php new file mode 100644 index 00000000000..594683f67a4 --- /dev/null +++ b/src/Internal/Payment/Factor.php @@ -0,0 +1,134 @@ +database_cache = $database_cache; + } + + /** + * Checks whether a given payment should use the new payment process. + * + * @param Factor[] $factors Factors, describing the type and conditions of the payment. + * @return bool + * @psalm-suppress MissingThrowsDocblock + */ + public function should_use_new_payment_process( array $factors ): bool { + $allowed_factors = $this->get_allowed_factors(); + + foreach ( $factors as $present_factor ) { + if ( ! in_array( $present_factor, $allowed_factors, true ) ) { + return false; + } + } + + return true; + } + + /** + * Returns all factors, which can be handled by the new payment process. + * + * @return Factor[] + */ + public function get_allowed_factors() { + // Might be false if loading failed. + $cached = $this->get_cached_factors(); + $all_factors = is_array( $cached ) ? $cached : []; + $allowed = []; + + foreach ( ( $all_factors ?? [] ) as $key => $enabled ) { + if ( $enabled ) { + $allowed[] = Factor::$key(); + } + } + + $allowed = apply_filters( 'wcpay_new_payment_process_enabled_factors', $allowed ); + return $allowed; + } + + /** + * Checks if cached data is valid. + * + * @psalm-suppress MissingThrowsDocblock + * @param mixed $cache The cached data. + * @return bool + */ + public function is_valid_cache( $cache ): bool { + return is_array( $cache ) && isset( $cache[ Factor::NEW_PAYMENT_PROCESS()->get_value() ] ); + } + + /** + * Gets and chaches all factors, which can be handled by the new payment process. + * + * @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache. + * @return array Factors, or an empty array. + */ + private function get_cached_factors( bool $force_refresh = false ) { + $factors = $this->database_cache->get_or_add( + Database_Cache::PAYMENT_PROCESS_FACTORS_KEY, + function () { + try { + $request = Get_Payment_Process_Factors::create(); + $response = $request->send( 'wcpay_get_payment_process_factors' ); + return $response->to_array(); + } catch ( API_Exception $e ) { + // Return false to signal retrieval error. + return false; + } + }, + [ $this, 'is_valid_cache' ], + $force_refresh + ); + + return $factors ?? []; + } +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 9c18c491e0b..48ba5bcdf7d 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -142,7 +142,11 @@ function wcpay_get_test_container() { $container = $GLOBALS['wcpay_container'] ?? null; if ( ! $container instanceof Container ) { - throw new Exception( 'Tests require the WCPay dependency container to be set up.' ); + if ( is_null( $container ) ) { + $container = wcpay_get_container(); + } else { + throw new Exception( 'Tests require the WCPay dependency container to be set up.' ); + } } // Load the property through reflection. diff --git a/tests/unit/src/ContainerTest.php b/tests/unit/src/ContainerTest.php index 5198d815358..370e9d08311 100644 --- a/tests/unit/src/ContainerTest.php +++ b/tests/unit/src/ContainerTest.php @@ -73,6 +73,18 @@ protected function setUp(): void { $this->test_sut = wcpay_get_test_container(); } + /** + * Cleans up global replacements after the class. + * + * Without this, other `src` tests will fail. + */ + public static function tearDownAfterClass(): void { + parent::tearDownAfterClass(); + + $GLOBALS['wcpay_container'] = null; + $GLOBALS['wcpay_test_container'] = null; + } + /** * Tests the `wcpay_get_container` function. */ diff --git a/tests/unit/src/Internal/Payment/FactorTest.php b/tests/unit/src/Internal/Payment/FactorTest.php new file mode 100644 index 00000000000..768718d860b --- /dev/null +++ b/tests/unit/src/Internal/Payment/FactorTest.php @@ -0,0 +1,45 @@ +assertEquals( $factors, $result ); + } +} diff --git a/tests/unit/src/Internal/Payment/RouterTest.php b/tests/unit/src/Internal/Payment/RouterTest.php new file mode 100644 index 00000000000..818bf8c33e4 --- /dev/null +++ b/tests/unit/src/Internal/Payment/RouterTest.php @@ -0,0 +1,274 @@ +mock_db_cache = $this->createMock( Database_Cache::class ); + $this->sut = new Router( $this->mock_db_cache ); + } + + /** + * Tests that the router returns false if a factor is **not present** in the account cache. + */ + public function test_should_use_new_payment_process_returns_false_with_missing_factor() { + $this->mock_db_cache_factors( [] ); + + $result = $this->sut->should_use_new_payment_process( [ Factor::USE_SAVED_PM() ] ); + $this->assertFalse( $result ); + } + + /** + * Tests that the router returns false if a factor is **false** in the account cache. + */ + public function test_should_use_new_payment_process_returns_false_with_unavailable_factor() { + $this->mock_db_cache_factors( [ Factor::USE_SAVED_PM => false ] ); + + $result = $this->sut->should_use_new_payment_process( [ Factor::USE_SAVED_PM() ] ); + $this->assertFalse( $result ); + } + + /** + * Tests that the router returns true when a factor is both present, and true in the account cache. + */ + public function test_should_use_new_payment_process_returns_true_with_available_factor() { + $this->mock_db_cache_factors( [ Factor::USE_SAVED_PM => true ] ); + + $result = $this->sut->should_use_new_payment_process( [ Factor::USE_SAVED_PM() ] ); + $this->assertTrue( $result ); + } + + /** + * Tests that the router handles multiple flags properly, + * and returns false in case any of them is not available. + */ + public function test_should_use_new_payment_process_with_multiple_factors_returns_false() { + $this->mock_db_cache_factors( + [ + Factor::USE_SAVED_PM => true, + Factor::SUBSCRIPTION_SIGNUP => false, + Factor::WOOPAY_ENABLED => true, + Factor::PAYMENT_REQUEST => false, + ] + ); + + $result = $this->sut->should_use_new_payment_process( + [ + Factor::USE_SAVED_PM(), + Factor::SUBSCRIPTION_SIGNUP(), + Factor::WOOPAY_ENABLED(), + ] + ); + $this->assertFalse( $result ); + } + + /** + * Tests that the router handles multiple flags properly, + * and returns true when all factors are present. + */ + public function test_should_use_new_payment_process_with_multiple_factors_returns_true() { + $this->mock_db_cache_factors( + [ + Factor::USE_SAVED_PM => true, + Factor::SUBSCRIPTION_SIGNUP => true, + Factor::WOOPAY_ENABLED => true, + Factor::PAYMENT_REQUEST => false, + ] + ); + + $result = $this->sut->should_use_new_payment_process( + [ + Factor::USE_SAVED_PM(), + Factor::SUBSCRIPTION_SIGNUP(), + Factor::WOOPAY_ENABLED(), + ] + ); + $this->assertTrue( $result ); + } + + /** + * Check that `get_allowed_factors` returns the factors, provided by the cache. + */ + public function test_get_allowed_factors_returns_factors() { + $cached_factors = [ + Factor::SAVE_PM => true, + Factor::SUBSCRIPTION_SIGNUP => false, + ]; + $processed_factors = [ Factor::SAVE_PM() ]; + + $this->mock_db_cache_factors( $cached_factors, false ); + + $result = $this->sut->get_allowed_factors(); + + $this->assertIsArray( $result ); + $this->assertSame( $processed_factors, $result ); + } + + /** + * Ensures that `get_allowed_factors` returns an array, even with broken cache. + */ + public function test_get_allowed_factors_returns_empty_array() { + // Return nothing to force an empty array. + $this->mock_db_cache_factors( null, false ); + + $result = $this->sut->get_allowed_factors(); + + $this->assertIsArray( $result ); + $this->assertEmpty( $result ); + } + + /** + * Confirms that `get_allowed_factors` allows filters to work. + */ + public function test_get_allowed_factors_allows_filters() { + $cached_factors = [ + Factor::SAVE_PM => true, + Factor::SUBSCRIPTION_SIGNUP => false, + ]; + $replaced_factors = [ + Factor::NO_PAYMENT(), + ]; + $this->mock_db_cache_factors( $cached_factors, false ); + + $filter_cb = function() use ( $replaced_factors ) { + return $replaced_factors; + }; + add_filter( 'wcpay_new_payment_process_enabled_factors', $filter_cb ); + + $result = $this->sut->get_allowed_factors(); + + $this->assertIsArray( $result ); + $this->assertSame( $replaced_factors, $result ); + + remove_filter( 'wcpay_new_payment_process_enabled_factors', $filter_cb ); + } + + /** + * Verify that `is_valid_cache` returns false with a non-array. + */ + public function test_is_valid_cache_requires_array() { + $this->assertFalse( $this->sut->is_valid_cache( false ) ); + } + + /** + * Verify that `is_valid_cache` returns false with incorrect arrays. + */ + public function test_is_valid_cache_requires_base_factor() { + $cache = [ Factor::NO_PAYMENT => true ]; + $this->assertFalse( $this->sut->is_valid_cache( $cache ) ); + } + + /** + * Verify that `is_valid_cache` accepts well-formed data. + */ + public function test_is_valid_cache_with_well_formed_data() { + $cache = [ Factor::NEW_PAYMENT_PROCESS => true ]; + $this->assertTrue( $this->sut->is_valid_cache( $cache ) ); + } + + /** + * + */ + public function test_get_cached_factors_populates_cache() { + $request_response = [ + Factor::NEW_PAYMENT_PROCESS => true, + ]; + $processed_factors = [ Factor::NEW_PAYMENT_PROCESS() ]; + + $this->mock_wcpay_request( Get_Payment_Process_Factors::class, 1, null, $request_response ); + + $this->mock_db_cache->expects( $this->once() ) + ->method( 'get_or_add' ) + ->with( + Database_Cache::PAYMENT_PROCESS_FACTORS_KEY, + $this->callback( + function ( $cb ) use ( $request_response ) { + return $request_response === $cb(); + } + ), + [ $this->sut, 'is_valid_cache' ], + false + ) + ->willReturn( $request_response ); + + $result = $this->sut->get_allowed_factors(); + $this->assertSame( $processed_factors, $result ); + } + + /** + * Ensures that a server error would handle exceptions correctly. + */ + public function test_get_cached_factors_handles_exceptions() { + $generator = function( $cb ) { + $this->mock_wcpay_request( Get_Payment_Process_Factors::class ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->will( $this->throwException( new API_Exception( 'Does not work', 'forced', 1234 ) ) ); + + $result = $cb(); + return false === $result; + }; + + $this->mock_db_cache->expects( $this->once() ) + ->method( 'get_or_add' ) + ->with( + Database_Cache::PAYMENT_PROCESS_FACTORS_KEY, + $this->callback( $generator ) + ) + ->willReturn( false ); + + $this->assertEmpty( $this->sut->get_allowed_factors() ); + } + + /** + * Simulates specific factors, being returned by `Database_Cache`. + * + * @param array|null $factors The factors to simulate. + * @param bool $add_base Whether to add the base `NEW_PAYMENT_PROCESS` factor. + */ + private function mock_db_cache_factors( array $factors = null, bool $add_base = true ) { + if ( $add_base && ! isset( $factors[ Factor::NEW_PAYMENT_PROCESS ] ) ) { + $factors[ Factor::NEW_PAYMENT_PROCESS ] = true; + } + + $this->mock_db_cache->expects( $this->once() ) + ->method( 'get_or_add' ) + ->with( Database_Cache::PAYMENT_PROCESS_FACTORS_KEY ) + ->willreturn( $factors ); + } +} diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index f39f4dea3ed..6ef8e0a5f83 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -21,6 +21,9 @@ use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\API_Exception; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; +use WCPay\Internal\Payment\Factor; +use WCPay\Internal\Payment\Router; +use WCPay\Internal\Service\PaymentProcessingService; use WCPay\Payment_Information; use WCPay\WooPay\WooPay_Utilities; use WCPay\Session_Rate_Limiter; @@ -211,6 +214,20 @@ public function tear_down() { // Fall back to an US store. update_option( 'woocommerce_store_postcode', '94110' ); $this->wcpay_gateway->update_option( 'saved_cards', 'yes' ); + + // Some tests simulate payment method parameters. + $payment_method_keys = [ + 'payment_method', + 'wc-woocommerce_payments-payment-token', + 'wc-woocommerce_payments-new-payment-method', + ]; + foreach ( $payment_method_keys as $key ) { + // phpcs:disable WordPress.Security.NonceVerification.Missing + if ( isset( $_POST[ $key ] ) ) { + unset( $_POST[ $key ] ); + } + // phpcs:enable WordPress.Security.NonceVerification.Missing + } } public function test_attach_exchange_info_to_order_with_no_conversion() { @@ -2342,6 +2359,287 @@ public function test_no_payment_is_processed_for_woopay_preflight_check_request( $response = $mock_wcpay_gateway->process_payment( $order->get_id() ); } + public function test_should_use_new_process_requires_dev_mode() { + $mock_router = $this->createMock( Router::class ); + wcpay_get_test_container()->replace( Router::class, $mock_router ); + + $order = WC_Helper_Order::create_order(); + + // Assert: The router is never called. + $mock_router->expects( $this->never() ) + ->method( 'should_use_new_payment_process' ); + + $this->assertFalse( $this->wcpay_gateway->should_use_new_process( $order ) ); + } + + public function test_should_use_new_process_returns_false_if_feature_unavailable() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $mock_router = $this->createMock( Router::class ); + wcpay_get_test_container()->replace( Router::class, $mock_router ); + + $order = WC_Helper_Order::create_order(); + + // Assert: Feature returns false. + $mock_router->expects( $this->once() ) + ->method( 'should_use_new_payment_process' ) + ->willReturn( false ); + + // Act: Call the method. + $result = $this->wcpay_gateway->should_use_new_process( $order ); + $this->assertFalse( $result ); + } + + public function test_should_use_new_process_uses_the_new_process() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $mock_router = $this->createMock( Router::class ); + $mock_service = $this->createMock( PaymentProcessingService::class ); + $order = WC_Helper_Order::create_order(); + + wcpay_get_test_container()->replace( Router::class, $mock_router ); + wcpay_get_test_container()->replace( PaymentProcessingService::class, $mock_service ); + + // Assert: Feature returns false. + $mock_router->expects( $this->once() ) + ->method( 'should_use_new_payment_process' ) + ->willReturn( true ); + + // Act: Call the method. + $result = $this->wcpay_gateway->should_use_new_process( $order ); + $this->assertTrue( $result ); + } + + public function test_should_use_new_process_adds_base_factor() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order( 1, 0 ); + + $this->expect_router_factor( Factor::NEW_PAYMENT_PROCESS(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_positive_no_payment() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order( 1, 0 ); + + $this->expect_router_factor( Factor::NO_PAYMENT(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_negative_no_payment() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + $order->set_total( 10 ); + $order->save(); + + $this->expect_router_factor( Factor::NO_PAYMENT(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_negative_no_payment_when_saving_pm() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order( 1, 0 ); + + // Simulate a payment method being saved to force payment processing. + $_POST['wc-woocommerce_payments-new-payment-method'] = 'pm_XYZ'; + + $this->expect_router_factor( Factor::NO_PAYMENT(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_positive_use_saved_pm() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + $token = WC_Helper_Token::create_token( 'pm_XYZ' ); + + // Simulate that a saved token is being used. + $_POST['payment_method'] = 'woocommerce_payments'; + $_POST['wc-woocommerce_payments-payment-token'] = $token->get_id(); + + $this->expect_router_factor( Factor::USE_SAVED_PM(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_negative_use_saved_pm() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + // Simulate that a saved token is being used. + $_POST['payment_method'] = 'woocommerce_payments'; + $_POST['wc-woocommerce_payments-payment-token'] = 'new'; + + $this->expect_router_factor( Factor::USE_SAVED_PM(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_positive_save_pm() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + $_POST['wc-woocommerce_payments-new-payment-method'] = '1'; + + $this->expect_router_factor( Factor::SAVE_PM(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_positive_save_pm_for_subscription() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + WC_Subscriptions::$wcs_order_contains_subscription = '__return_true'; + + $this->expect_router_factor( Factor::SAVE_PM(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_negative_save_pm() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + $token = WC_Helper_Token::create_token( 'pm_XYZ' ); + + // Simulate that a saved token is being used. + $_POST['wc-woocommerce_payments-new-payment-method'] = '1'; + $_POST['payment_method'] = 'woocommerce_payments'; + $_POST['wc-woocommerce_payments-payment-token'] = $token->get_id(); + + $this->expect_router_factor( Factor::SAVE_PM(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_positive_subscription_signup() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + WC_Subscriptions::$wcs_order_contains_subscription = '__return_true'; + + $this->expect_router_factor( Factor::SUBSCRIPTION_SIGNUP(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_negative_subscription_signup() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + WC_Subscriptions::$wcs_order_contains_subscription = '__return_false'; + + $this->expect_router_factor( Factor::SUBSCRIPTION_SIGNUP(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_positive_woopay_payment() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + $_POST['platform-checkout-intent'] = 'pi_ZYX'; + + $this->expect_router_factor( Factor::WOOPAY_PAYMENT(), true ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_should_use_new_process_determines_negative_woopay_payment() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + unset( $_POST['platform-checkout-intent'] ); + + $this->expect_router_factor( Factor::WOOPAY_PAYMENT(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + /** + * Testing the positive WCPay subscription signup factor is not possible, + * as the check relies on the existence of the `WC_Subscriptions` class + * through an un-mockable method, and the class simply exists. + */ + public function test_should_use_new_process_determines_negative_wcpay_subscription_signup() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $order = WC_Helper_Order::create_order(); + + WC_Subscriptions::$wcs_order_contains_subscription = '__return_true'; + add_filter( 'wcpay_is_wcpay_subscriptions_enabled', '__return_true' ); + + $this->expect_router_factor( Factor::WCPAY_SUBSCRIPTION_SIGNUP(), false ); + $this->wcpay_gateway->should_use_new_process( $order ); + } + + public function test_new_process_payment() { + // The new payment process is only accessible in dev mode. + WC_Payments::mode()->dev(); + + $mock_service = $this->createMock( PaymentProcessingService::class ); + $mock_router = $this->createMock( Router::class ); + $order = WC_Helper_Order::create_order(); + $mock_response = [ 'success' => 'maybe' ]; + + wcpay_get_test_container()->replace( PaymentProcessingService::class, $mock_service ); + wcpay_get_test_container()->replace( Router::class, $mock_router ); + + $mock_router->expects( $this->once() ) + ->method( 'should_use_new_payment_process' ) + ->willReturn( true ); + + // Assert: The new service is called. + $mock_service->expects( $this->once() ) + ->method( 'process_payment' ) + ->with( $order->get_id() ) + ->willReturn( $mock_response ); + + $result = $this->wcpay_gateway->process_payment( $order->get_id() ); + $this->assertSame( $mock_response, $result ); + } + + /** + * Sets up the expectation for a certain factor for the new payment + * process to be either set or unset. + * + * @param Factor $factor_name Factor constant. + * @param bool $value Expected value. + */ + private function expect_router_factor( $factor_name, $value ) { + $mock_router = $this->createMock( Router::class ); + wcpay_get_test_container()->replace( Router::class, $mock_router ); + + $checker = function( $factors ) use ( $factor_name, $value ) { + $is_in_array = in_array( $factor_name, $factors, true ); + return $value ? $is_in_array : ! $is_in_array; + }; + + $mock_router->expects( $this->once() ) + ->method( 'should_use_new_payment_process' ) + ->with( $this->callback( $checker ) ); + } + /** * Mocks Fraud_Prevention_Service. * From 379bf0a096135421957133008d380d3fc9df87ca Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto Date: Fri, 1 Sep 2023 14:49:43 -0300 Subject: [PATCH 22/84] Temporarily disable saving SEPA (#7107) --- changelog/temporarily-disable-saving-sepa | 4 ++++ includes/payment-methods/class-sepa-payment-method.php | 2 +- .../unit/payment-methods/test-class-upe-payment-gateway.php | 4 ++-- .../test-class-upe-split-payment-gateway.php | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 changelog/temporarily-disable-saving-sepa diff --git a/changelog/temporarily-disable-saving-sepa b/changelog/temporarily-disable-saving-sepa new file mode 100644 index 00000000000..53e329f4089 --- /dev/null +++ b/changelog/temporarily-disable-saving-sepa @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Temporarily disable saving SEPA diff --git a/includes/payment-methods/class-sepa-payment-method.php b/includes/payment-methods/class-sepa-payment-method.php index 76fbadd541d..914363f7710 100644 --- a/includes/payment-methods/class-sepa-payment-method.php +++ b/includes/payment-methods/class-sepa-payment-method.php @@ -25,7 +25,7 @@ public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; $this->title = 'SEPA Direct Debit'; - $this->is_reusable = true; + $this->is_reusable = false; $this->currencies = [ 'EUR' ]; $this->icon_url = plugins_url( 'assets/images/payment-methods/sepa-debit.svg', WCPAY_PLUGIN_FILE ); } diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php index 715a43ff967..e52cac12c1d 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -1555,7 +1555,7 @@ public function test_payment_methods_show_correct_default_outputs() { $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title() ); $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( $mock_sepa_details ) ); $this->assertTrue( $sepa_method->is_enabled_at_checkout() ); - $this->assertTrue( $sepa_method->is_reusable() ); + $this->assertFalse( $sepa_method->is_reusable() ); $this->assertEquals( 'ideal', $ideal_method->get_id() ); $this->assertEquals( 'iDEAL', $ideal_method->get_title() ); @@ -1602,7 +1602,7 @@ public function test_only_reusabled_payment_methods_enabled_with_subscription_it $this->assertFalse( $sofort_method->is_enabled_at_checkout() ); $this->assertFalse( $bancontact_method->is_enabled_at_checkout() ); $this->assertFalse( $eps_method->is_enabled_at_checkout() ); - $this->assertTrue( $sepa_method->is_enabled_at_checkout() ); + $this->assertFalse( $sepa_method->is_enabled_at_checkout() ); $this->assertFalse( $p24_method->is_enabled_at_checkout() ); $this->assertFalse( $ideal_method->is_enabled_at_checkout() ); $this->assertFalse( $becs_method->is_enabled_at_checkout() ); diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index 4a613570de1..18c0024bb3e 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -1559,7 +1559,7 @@ public function test_payment_methods_show_correct_default_outputs() { $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title() ); $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( $mock_sepa_details ) ); $this->assertTrue( $sepa_method->is_enabled_at_checkout() ); - $this->assertTrue( $sepa_method->is_reusable() ); + $this->assertFalse( $sepa_method->is_reusable() ); $this->assertEquals( 'ideal', $ideal_method->get_id() ); $this->assertEquals( 'iDEAL', $ideal_method->get_title() ); @@ -1594,7 +1594,7 @@ public function test_only_reusabled_payment_methods_enabled_with_subscription_it $this->assertFalse( $sofort_method->is_enabled_at_checkout() ); $this->assertFalse( $bancontact_method->is_enabled_at_checkout() ); $this->assertFalse( $eps_method->is_enabled_at_checkout() ); - $this->assertTrue( $sepa_method->is_enabled_at_checkout() ); + $this->assertFalse( $sepa_method->is_enabled_at_checkout() ); $this->assertFalse( $p24_method->is_enabled_at_checkout() ); $this->assertFalse( $ideal_method->is_enabled_at_checkout() ); $this->assertFalse( $becs_method->is_enabled_at_checkout() ); @@ -2044,7 +2044,7 @@ public function test_save_option_for_sepa_debit() { $this->mock_customer_service ); - $this->assertSame( $upe_checkout->get_payment_fields_js_config()['paymentMethodsConfig'][ Payment_Method::SEPA ]['showSaveOption'], true ); + $this->assertSame( $upe_checkout->get_payment_fields_js_config()['paymentMethodsConfig'][ Payment_Method::SEPA ]['showSaveOption'], false ); } public function test_remove_link_payment_method_if_card_disabled() { From f9d55c6b9ccafc74f3dd39d8033771b3370c8a6e Mon Sep 17 00:00:00 2001 From: Mike Moore Date: Fri, 1 Sep 2023 18:16:12 -0400 Subject: [PATCH 23/84] Provide fallback for email payment method title (#7108) Co-authored-by: Guilherme Pressutto --- changelog/fix-6951-validate-set-title-for-email | 5 +++++ includes/payment-methods/class-upe-payment-gateway.php | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-6951-validate-set-title-for-email diff --git a/changelog/fix-6951-validate-set-title-for-email b/changelog/fix-6951-validate-set-title-for-email new file mode 100644 index 00000000000..8b2a138daf0 --- /dev/null +++ b/changelog/fix-6951-validate-set-title-for-email @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Minor bug fix only adding a defensive check + + diff --git a/includes/payment-methods/class-upe-payment-gateway.php b/includes/payment-methods/class-upe-payment-gateway.php index 8745cdefccf..be04d74de0e 100644 --- a/includes/payment-methods/class-upe-payment-gateway.php +++ b/includes/payment-methods/class-upe-payment-gateway.php @@ -1166,9 +1166,17 @@ public function maybe_filter_gateway_title( $title, $id ) { * Sets the payment method title on the order for emails. * * @param WC_Order $order WC Order object. + * + * @return void */ public function set_payment_method_title_for_email( $order ) { - $payment_method_id = $this->order_service->get_payment_method_id_for_order( $order ); + $payment_method_id = $this->order_service->get_payment_method_id_for_order( $order ); + if ( ! $payment_method_id ) { + $order->set_payment_method_title( $this->title ); + $order->save(); + + return; + } $payment_method_details = $this->payments_api_client->get_payment_method( $payment_method_id ); $payment_method_type = $this->get_payment_method_type_from_payment_details( $payment_method_details ); $this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details ); From 635e38668f37d49143703f2bbb4a76d644e48cdd Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Mon, 4 Sep 2023 18:02:12 +0300 Subject: [PATCH 24/84] Allow requests to be extended by multiple classes in parallel (#7099) --- changelog/fix-7061-extended-requests | 4 + includes/core/server/class-request.php | 17 ++- .../request/test-class-core-request.php | 103 ++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-7061-extended-requests diff --git a/changelog/fix-7061-extended-requests b/changelog/fix-7061-extended-requests new file mode 100644 index 00000000000..1c62343b3c8 --- /dev/null +++ b/changelog/fix-7061-extended-requests @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Allow request classes to be extended more than once. diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index 51c1d82e5fa..8ac0f70d398 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -64,6 +64,14 @@ abstract class Request { */ private $protected_mode = false; + /** + * Stores the base class when `->apply_filters` is called. + * This class will be checked when `::extend` is called. + * + * @var string + */ + private $base_class; + /** * Holds the API client of WCPay. * @@ -416,7 +424,7 @@ private function set_params( $params ) { */ final public static function extend( Request $base_request ) { $current_class = static::class; - $base_request->validate_extended_class( $current_class, get_class( $base_request ) ); + $base_request->validate_extended_class( $current_class, $base_request->base_class ?? get_class( $base_request ) ); if ( ! $base_request->protected_mode ) { throw new Extend_Request_Exception( @@ -425,7 +433,11 @@ final public static function extend( Request $base_request ) { ); } $obj = new $current_class( $base_request->api_client, $base_request->http_interface ); - $obj->set_params( $base_request->params ); + $obj->set_params( array_merge( static::DEFAULT_PARAMS, $base_request->params ) ); + + // Carry over the base class and protected mode into the child request. + $obj->base_class = $base_request->base_class; + $obj->protected_mode = true; return $obj; } @@ -449,6 +461,7 @@ final public static function extend( Request $base_request ) { final public function apply_filters( $hook, ...$args ) { // Lock the class in order to prevent `set_param` for protected props. $this->protected_mode = true; + $this->base_class = get_class( $this ); // Validate API route. $this->validate_api_route( $this->get_api() ); diff --git a/tests/unit/core/server/request/test-class-core-request.php b/tests/unit/core/server/request/test-class-core-request.php index d35e182584b..8f7e48d016b 100644 --- a/tests/unit/core/server/request/test-class-core-request.php +++ b/tests/unit/core/server/request/test-class-core-request.php @@ -9,6 +9,54 @@ use WCPay\Core\Server\Request\Paginated; use WCPay\Core\Server\Request\List_Transactions; +// phpcs:disable +class My_Request extends Request { + const DEFAULT_PARAMS = [ + 'default_1' => 1, + ]; + + public function get_api(): string { + return WC_Payments_API_Client::INTENTIONS_API; + } + + public function get_method(): string { + return 'POST'; + } + + public function set_param_1( int $value ) { + $this->set_param( 'param_1', $value ); + } +} +class WooPay_Request extends My_Request { + const DEFAULT_PARAMS = [ + 'default_2' => 2, + ]; + + public function set_param_2( int $value ) { + $this->set_param( 'param_2', $value ); + } +} +class ThirdParty_Request extends My_Request { + const DEFAULT_PARAMS = [ + 'default_3' => 3, + ]; + + public function set_param_3( int $value ) { + $this->set_param( 'param_3', $value ); + } +} +class Another_ThirdParty_Request extends WooPay_Request { + const DEFAULT_PARAMS = [ + 'default_4' => 4, + ]; + + public function set_param_4( int $value ) { + $this->set_param( 'param_4', $value ); + } +} +// phpcs:enable +// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound + /** * WCPay\Core\Server\Capture_Intention_Test unit tests. */ @@ -31,4 +79,59 @@ public function test_traverse_class_constants() { $result = List_Transactions::traverse_class_constants( 'DEFAULT_PARAMS' ); $this->assertSame( $expected, $result ); } + + /** + * Ensures that `::extend` works with any class, which extends the + * base request (where `apply_filters` is called) directly or indirectly. + */ + public function test_extension_by_multiple_classes() { + $hook = 'some_request_class'; + $request = My_Request::create(); + $request->set_param_1( 1 ); + + add_filter( + $hook, + function( $request ) { + $modified = WooPay_Request::extend( $request ); + $modified->set_param_2( 2 ); + return $modified; + } + ); + + add_filter( + $hook, + function( $request ) { + $modified = ThirdParty_Request::extend( $request ); + $modified->set_param_3( 3 ); + return $modified; + } + ); + + add_filter( + $hook, + function( $request ) { + $modified = Another_ThirdParty_Request::extend( $request ); + $modified->set_param_4( 4 ); + return $modified; + } + ); + + $filtered = $request->apply_filters( $hook ); + $result = $filtered->get_params(); + + // Assert: It's important that we got here without exceptions, but everything should be set. + $this->assertEquals( + [ + 'param_1' => 1, + 'param_2' => 2, + 'param_3' => 3, + 'param_4' => 4, + 'default_1' => 1, + 'default_2' => 2, + 'default_3' => 3, + 'default_4' => 4, + ], + $result + ); + } } From 861de2a43da3b97b60e364c434d2c28cbf2f3880 Mon Sep 17 00:00:00 2001 From: Shendy <73803630+shendy-a8c@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:56:23 +0700 Subject: [PATCH 25/84] Disable refund button when transaction disputed (#7043) Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> --- ...e-6378-disable-refund-button-when-disputed | 4 + client/disputes/filters/config.ts | 5 + client/disputes/utils.ts | 9 +- client/order/index.js | 270 ++++++++++++------ 4 files changed, 198 insertions(+), 90 deletions(-) create mode 100644 changelog/update-6378-disable-refund-button-when-disputed diff --git a/changelog/update-6378-disable-refund-button-when-disputed b/changelog/update-6378-disable-refund-button-when-disputed new file mode 100644 index 00000000000..6f9f041c7d6 --- /dev/null +++ b/changelog/update-6378-disable-refund-button-when-disputed @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Disable refund button on order edit page when there is active or lost dispute. diff --git a/client/disputes/filters/config.ts b/client/disputes/filters/config.ts index e24dc1affd1..3d0d1ab5f63 100644 --- a/client/disputes/filters/config.ts +++ b/client/disputes/filters/config.ts @@ -36,6 +36,11 @@ export const disputeAwaitingResponseStatuses = [ 'warning_needs_response', ]; +export const disputeUnderReviewStatuses = [ + 'under_review', + 'warning_under_review', +]; + export const filters: [ DisputesFilterType, DisputesFilterType ] = [ { label: __( 'Dispute currency', 'woocommerce-payments' ), diff --git a/client/disputes/utils.ts b/client/disputes/utils.ts index 95f59b190d9..aa8ddaf65f0 100644 --- a/client/disputes/utils.ts +++ b/client/disputes/utils.ts @@ -13,7 +13,10 @@ import type { DisputeStatus, EvidenceDetails, } from 'wcpay/types/disputes'; -import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; +import { + disputeAwaitingResponseStatuses, + disputeUnderReviewStatuses, +} from 'wcpay/disputes/filters/config'; interface IsDueWithinProps { dueBy: CachedDispute[ 'due_by' ] | EvidenceDetails[ 'due_by' ]; @@ -58,6 +61,10 @@ export const isAwaitingResponse = ( return disputeAwaitingResponseStatuses.includes( status ); }; +export const isUnderReview = ( status: DisputeStatus | string ): boolean => { + return disputeUnderReviewStatuses.includes( status ); +}; + export const isInquiry = ( dispute: Dispute | CachedDispute ): boolean => { // Inquiry dispute statuses are one of `warning_needs_response`, `warning_under_review` or `warning_closed`. return dispute.status.startsWith( 'warning' ); diff --git a/client/order/index.js b/client/order/index.js index fd3ed472481..35254d5a6da 100644 --- a/client/order/index.js +++ b/client/order/index.js @@ -5,6 +5,7 @@ import { dateI18n } from '@wordpress/date'; import ReactDOM from 'react-dom'; import { dispatch } from '@wordpress/data'; import moment from 'moment'; +import { createInterpolateElement } from '@wordpress/element'; /** * Internal dependencies @@ -16,7 +17,11 @@ import InlineNotice from 'components/inline-notice'; import { formatExplicitCurrency } from 'utils/currency'; import { reasons } from 'wcpay/disputes/strings'; import { getDetailsURL } from 'wcpay/components/details-link'; -import { isAwaitingResponse, isInquiry } from 'wcpay/disputes/utils'; +import { + isAwaitingResponse, + isInquiry, + isUnderReview, +} from 'wcpay/disputes/utils'; import { useCharge } from 'wcpay/data'; import wcpayTracks from 'tracks'; import './style.scss'; @@ -129,102 +134,175 @@ jQuery( function ( $ ) { const DisputeNotice = ( { chargeId } ) => { const { data: charge } = useCharge( chargeId ); - if ( - ! charge?.dispute || - ! charge?.dispute?.evidence_details?.due_by || - // Only show the notice if the dispute is awaiting a response. - ! isAwaitingResponse( charge.dispute.status ) - ) { + if ( ! charge?.dispute ) { return null; } const { dispute } = charge; - const now = moment(); - const dueBy = moment.unix( dispute.evidence_details?.due_by ); - const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) ); + let urgency = 'warning'; + let actions; - // If the dispute is due in the past, we don't want to show the notice. - if ( now.isAfter( dueBy ) ) { - return; - } + // Refunds are only allowed if the dispute is an inquiry or if it's won. + const isRefundable = + isInquiry( dispute ) || [ 'won' ].includes( dispute.status ); + const shouldDisableRefund = ! isRefundable; + let disableRefund = false; - const titleStrings = { - // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date. - dispute_default: __( - // eslint-disable-next-line max-len - 'This order has been disputed in the amount of %1$s. The customer provided the following reason: %2$s. Please respond to this dispute before %3$s.', - 'woocommerce-payments' - ), - // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date. - inquiry_default: __( - // eslint-disable-next-line max-len - 'The card network involved in this order has opened an inquiry into the transaction with the following reason: %2$s. Please respond to this inquiry before %3$s, just like you would for a formal dispute.', - 'woocommerce-payments' - ), - // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date. - dispute_urgent: __( - 'Please resolve the dispute on this order for %1$s labeled "%2$s" by %3$s.', - 'woocommerce-payments' - ), - // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date. - inquiry_urgent: __( - 'Please resolve the inquiry on this order for %1$s labeled "%2$s" by %3$s.', - 'woocommerce-payments' - ), - }; - const amountFormatted = formatExplicitCurrency( - dispute.amount, - dispute.currency - ); + let refundDisabledNotice = ''; + if ( shouldDisableRefund ) { + const refundButton = document.querySelector( 'button.refund-items' ); + if ( refundButton ) { + disableRefund = true; - let urgency = 'warning'; - let buttonLabel = __( 'Respond now', 'woocommerce-payments' ); - let suffix = ''; - - let titleText = isInquiry( dispute ) - ? titleStrings.inquiry_default - : titleStrings.dispute_default; - - // If the dispute is due within 7 days, use different wording. - if ( countdownDays < 7 ) { - titleText = isInquiry( dispute ) - ? titleStrings.inquiry_urgent - : titleStrings.dispute_urgent; - - suffix = sprintf( - // Translators: %s is the number of days left to respond to the dispute. - _n( - '(%s day left)', - '(%s days left)', - countdownDays, - 'woocommerce-payments' - ), - countdownDays - ); - } + // Disable the refund button. + refundButton.disabled = true; - const title = sprintf( - titleText, - amountFormatted, - reasons[ dispute.reason ].display, - dateI18n( 'M j, Y', dueBy.local().toISOString() ) - ); + const disputeDetailsLink = getDetailsURL( dispute.id, 'disputes' ); - // If the dispute is due within 72 hours, we want to highlight it as urgent/red. - if ( countdownDays < 3 ) { - urgency = 'error'; - } + let tooltipText = ''; + + if ( isAwaitingResponse( dispute.status ) ) { + refundDisabledNotice = __( + 'Refunds and order editing are disabled during disputes.', + 'woocommerce-payments' + ); + tooltipText = refundDisabledNotice; + } else if ( isUnderReview( dispute.status ) ) { + refundDisabledNotice = createInterpolateElement( + __( + // eslint-disable-next-line max-len + 'This order has an active payment dispute. Refunds and order editing are disabled during this time. View details', + 'woocommerce-payments' + ), + { + // eslint-disable-next-line jsx-a11y/anchor-has-content + a: , + } + ); + tooltipText = __( + 'Refunds and order editing are disabled during an active dispute.', + 'woocommerce-payments' + ); + } else if ( dispute.status === 'lost' ) { + refundDisabledNotice = createInterpolateElement( + __( + 'Refunds and order editing have been disabled as a result of a lost dispute. View details', + 'woocommerce-payments' + ), + { + // eslint-disable-next-line jsx-a11y/anchor-has-content + a: , + } + ); + tooltipText = __( + 'Refunds and order editing have been disabled as a result of a lost dispute.', + 'woocommerce-payments' + ); + } - if ( countdownDays < 1 ) { - buttonLabel = __( 'Respond today', 'woocommerce-payments' ); - suffix = __( '(Last day today)', 'woocommerce-payments' ); + // Change refund tooltip's text copy. + jQuery( refundButton ) + .parent() + .find( '.woocommerce-help-tip' ) + .attr( { + // jQuery.tipTip uses the title attribute to generate the tooltip. + title: tooltipText, + 'aria-label': tooltipText, + } ) + // Regenerate the tipTip tooltip. + .tipTip(); + } } - return ( - { ); }, }, - ] } + ]; + + warningText = `${ title } ${ suffix }`; + } + } + + if ( ! showWarning && ! disableRefund ) { + return null; + } + + return ( + - - { title } { suffix } - + { showWarning && { warningText } } + + { disableRefund &&
{ refundDisabledNotice }
}
); }; From fc451c55c5d3e0d7a28c8da28f1043c56fdc8d20 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:12:11 +1000 Subject: [PATCH 26/84] =?UTF-8?q?Update=20Transaction=20Details=20?= =?UTF-8?q?=E2=86=92=20HorizontalList=20label=20styles=20to=20uppercase=20?= =?UTF-8?q?(#7126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog/update-horizontal-list-label-style-uppercase | 5 +++++ client/components/horizontal-list/style.scss | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelog/update-horizontal-list-label-style-uppercase diff --git a/changelog/update-horizontal-list-label-style-uppercase b/changelog/update-horizontal-list-label-style-uppercase new file mode 100644 index 00000000000..b7a71d2c30c --- /dev/null +++ b/changelog/update-horizontal-list-label-style-uppercase @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: No changelog required: a particularly insignificant UI change. + + diff --git a/client/components/horizontal-list/style.scss b/client/components/horizontal-list/style.scss index ba52172618f..f133cae1da1 100755 --- a/client/components/horizontal-list/style.scss +++ b/client/components/horizontal-list/style.scss @@ -45,10 +45,14 @@ @include font-size( 14 ); } .woocommerce-list__item-title { - color: $studio-gray-60; + text-transform: uppercase; + color: $gray-700; + font-size: 11px; + font-weight: 600; } .woocommerce-list__item-content { color: $studio-gray-80; + display: flex; } } .woocommerce-list__item:first-child { From 7f796aaba6b18ae9af66670238183a84d3037480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Wed, 6 Sep 2023 14:18:43 +0200 Subject: [PATCH 27/84] Add onboarding task incentive badge (#7132) --- changelog/add-incentive-task-badge | 4 +++ client/globals.d.ts | 2 ++ .../class-wc-payments-incentives-service.php | 18 +++++++++++++ ...t-class-wc-payments-incentives-service.php | 27 +++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 changelog/add-incentive-task-badge diff --git a/changelog/add-incentive-task-badge b/changelog/add-incentive-task-badge new file mode 100644 index 00000000000..f2a30452565 --- /dev/null +++ b/changelog/add-incentive-task-badge @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add onboarding task incentive badge. diff --git a/client/globals.d.ts b/client/globals.d.ts index bdb82e4354c..e25789c5efa 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -105,6 +105,8 @@ declare global { id: string; description: string; tc_url: string; + task_header_content?: string; + task_badge?: string; }; isWooPayStoreCountryAvailable: boolean; }; diff --git a/includes/class-wc-payments-incentives-service.php b/includes/class-wc-payments-incentives-service.php index d59f4eb7c1e..5425175a6d9 100644 --- a/includes/class-wc-payments-incentives-service.php +++ b/includes/class-wc-payments-incentives-service.php @@ -33,6 +33,7 @@ public function __construct( Database_Cache $database_cache ) { add_action( 'admin_menu', [ $this, 'add_payments_menu_badge' ] ); add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] ); + add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] ); } /** @@ -77,6 +78,23 @@ public function allowed_promo_notes( $promo_notes = [] ): array { return $promo_notes; } + /** + * Adds the WooPayments incentive badge to the onboarding task. + * + * @param string $badge Current badge. + * + * @return string + */ + public function onboarding_task_badge( string $badge ): string { + $incentive = $this->get_cached_connect_incentive(); + // Return early if there is no eligible incentive. + if ( empty( $incentive['id'] ) ) { + return $badge; + } + + return $incentive['task_badge'] ?? $badge; + } + /** * Gets and caches eligible connect incentive from the server. * diff --git a/tests/unit/test-class-wc-payments-incentives-service.php b/tests/unit/test-class-wc-payments-incentives-service.php index cb36d1bb341..70df9bc262b 100644 --- a/tests/unit/test-class-wc-payments-incentives-service.php +++ b/tests/unit/test-class-wc-payments-incentives-service.php @@ -73,6 +73,7 @@ public function tear_down() { public function test_filters_registered_properly() { $this->assertNotFalse( has_action( 'admin_menu', [ $this->incentives_service, 'add_payments_menu_badge' ] ) ); $this->assertNotFalse( has_filter( 'woocommerce_admin_allowed_promo_notes', [ $this->incentives_service, 'allowed_promo_notes' ] ) ); + $this->assertNotFalse( has_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this->incentives_service, 'onboarding_task_badge' ] ) ); } public function test_add_payments_menu_badge_without_incentive() { @@ -111,6 +112,32 @@ public function test_allowed_promo_notes_with_incentive() { $this->assertContains( $this->mock_incentive_data['incentive']['id'], $promo_notes ); } + public function test_onboarding_task_badge_without_incentive() { + $this->mock_database_cache_with(); + + $badge = $this->incentives_service->onboarding_task_badge( '' ); + + $this->assertEmpty( $badge ); + } + + public function test_onboarding_task_badge_with_incentive_no_task_badge() { + $this->mock_database_cache_with( $this->mock_incentive_data ); + + $badge = $this->incentives_service->onboarding_task_badge( '' ); + + $this->assertEmpty( $badge ); + } + + public function test_onboarding_task_badge_with_incentive_and_task_badge() { + $incentive_data = $this->mock_incentive_data; + $incentive_data['incentive']['task_badge'] = 'task_badge'; + $this->mock_database_cache_with( $incentive_data ); + + $badge = $this->incentives_service->onboarding_task_badge( '' ); + + $this->assertEquals( $badge, 'task_badge' ); + } + public function test_get_cached_connect_incentive_non_supported_country() { add_filter( 'woocommerce_countries_base_country', From a2a68858fb6dfead5fa5411c1405b0df927dafd1 Mon Sep 17 00:00:00 2001 From: Jesse Pearson Date: Wed, 6 Sep 2023 09:51:59 -0300 Subject: [PATCH 28/84] Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches (#7092) --- ...x-5507-block-currency-update-on-sub-switch | 4 + .../WooCommerceSubscriptions.php | 207 ++-- .../helpers/class-wc-helper-subscription.php | 20 + .../test-class-woocommerce-subscriptions.php | 928 ++++++++---------- 4 files changed, 522 insertions(+), 637 deletions(-) create mode 100644 changelog/fix-5507-block-currency-update-on-sub-switch diff --git a/changelog/fix-5507-block-currency-update-on-sub-switch b/changelog/fix-5507-block-currency-update-on-sub-switch new file mode 100644 index 00000000000..9fd0d5e5019 --- /dev/null +++ b/changelog/fix-5507-block-currency-update-on-sub-switch @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches. diff --git a/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php b/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php index f4cbe19c902..e63c7ea54c3 100644 --- a/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php +++ b/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php @@ -16,6 +16,11 @@ */ class WooCommerceSubscriptions extends BaseCompatibility { + /** + * Our allowed subscription types. + */ + const SUBSCRIPTION_TYPES = [ 'renewal', 'resubscribe', 'switch' ]; + /** * Subscription switch cart item. * @@ -80,11 +85,8 @@ public function get_subscription_product_signup_fee( $price, $product ) { return $price; } - $switch_cart_items = $this->get_subscription_switch_cart_items(); - if ( 0 < count( $switch_cart_items ) ) { - - // There should only ever be one item, so use that item. - $item = array_shift( $switch_cart_items ); + $item = $this->get_subscription_type_from_cart( 'switch' ); + if ( $item ) { $item_id = ! empty( $item['variation_id'] ) ? $item['variation_id'] : $item['product_id']; $switch_cart_item = $this->switch_cart_item; $this->switch_cart_item = $item['key']; @@ -112,7 +114,7 @@ public function get_subscription_product_signup_fee( $price, $product ) { // Check to see if the _subscription_sign_up_fee meta for the product has already been updated. if ( $item['key'] === $switch_cart_item ) { foreach ( $product->get_meta_data() as $meta ) { - if ( '_subscription_sign_up_fee' === $meta->get_data()['key'] && 0 < count( $meta->get_changes() ) ) { + if ( '_subscription_sign_up_fee' === $meta->get_data()['key'] && ! empty( $meta->get_changes() ) ) { return $price; } } @@ -133,7 +135,7 @@ public function get_subscription_product_signup_fee( $price, $product ) { public function maybe_disable_mixed_cart( $value ) { // If there's a subscription switch in the cart, disable multiple items in the cart. // This is so that subscriptions with different currencies cannot be added to the cart. - if ( 0 < count( $this->get_subscription_switch_cart_items() ) ) { + if ( $this->get_subscription_type_from_cart( 'switch' ) ) { return 'no'; } @@ -143,56 +145,36 @@ public function maybe_disable_mixed_cart( $value ) { /** * Checks to see if the if the selected currency needs to be overridden. * + * The running_override_selected_currency_filters property is used here to avoid infinite loops. + * * @param mixed $return Default is false, but could be three letter currency code. * * @return mixed Three letter currency code or false if not. */ public function override_selected_currency( $return ) { - // If it's not false, return it. + // If it's not false, or we are already running filters, exit. if ( $return || $this->running_override_selected_currency_filters ) { return $return; } - $subscription_renewal = $this->cart_contains_renewal(); - if ( $subscription_renewal ) { - $order = wc_get_order( $subscription_renewal['subscription_renewal']['renewal_order_id'] ); - return $order ? $order->get_currency() : $return; - } + // Loop through subscription types and check for cart items. + foreach ( self::SUBSCRIPTION_TYPES as $type ) { + $cart_item = $this->get_subscription_type_from_cart( $type ); + if ( $cart_item ) { + $this->running_override_selected_currency_filters = true; - // The running_override_selected_currency_filters property has been added here due to if it isn't, it will create an infinite loop of calls. - if ( isset( WC()->session ) && WC()->session->get( 'order_awaiting_payment' ) ) { - $this->running_override_selected_currency_filters = true; - $order = wc_get_order( WC()->session->get( 'order_awaiting_payment' ) ); - $this->running_override_selected_currency_filters = false; - if ( $order && $this->order_contains_renewal( $order ) ) { - return $order->get_currency(); - } - } + // If we have a cart item, then we can get the order or subscription to pull the currency from. + $subscription_type = 'subscription_' . $type; + $subscription = $this->get_subscription( $cart_item[ $subscription_type ]['subscription_id'] ); - // The running_override_selected_currency_filters property is used to avoid an infinite loop - // that can occur on the product page when `get_subscription()` is used. - $switch_id = $this->get_subscription_switch_id_from_superglobal(); - if ( $switch_id ) { - $this->running_override_selected_currency_filters = true; - $switch_subscription = $this->get_subscription( $switch_id ); - $this->running_override_selected_currency_filters = false; - return $switch_subscription ? $switch_subscription->get_currency() : $return; - } - - $switch_cart_items = $this->get_subscription_switch_cart_items(); - if ( 0 < count( $switch_cart_items ) ) { - $switch_cart_item = array_shift( $switch_cart_items ); - $switch_subscription = $this->get_subscription( $switch_cart_item['subscription_switch']['subscription_id'] ); - return $switch_subscription ? $switch_subscription->get_currency() : $return; - } - - $subscription_resubscribe = $this->cart_contains_resubscribe(); - if ( $subscription_resubscribe ) { - $subscription = $this->get_subscription( $subscription_resubscribe['subscription_resubscribe']['subscription_id'] ); - return $subscription ? $subscription->get_currency() : $return; + $this->running_override_selected_currency_filters = false; + return $subscription ? $subscription->get_currency() : $return; + } } - return $return; + // This instance is for when the customer lands on the product page to choose a new subscription tier. + $switch_subscription = $this->get_subscription_from_superglobal_switch_id(); + return $switch_subscription ? $switch_subscription->get_currency() : $return; } /** @@ -210,8 +192,8 @@ public function should_convert_product_price( bool $return, $product ): bool { } // Check for subscription renewal or resubscribe. - if ( $this->is_product_subscription_type_in_cart( $product, 'renewal' ) - || $this->is_product_subscription_type_in_cart( $product, 'resubscribe' ) ) { + if ( $this->get_subscription_type_from_cart( 'renewal' ) + || $this->get_subscription_type_from_cart( 'resubscribe' ) ) { $calls = [ 'WC_Cart_Totals->calculate_item_totals', 'WC_Cart->get_product_subtotal', @@ -252,7 +234,7 @@ public function should_convert_coupon_amount( bool $return, $coupon ): bool { } // If there's not a renewal in the cart, we can convert. - $subscription_renewal = $this->cart_contains_renewal(); + $subscription_renewal = $this->get_subscription_type_from_cart( 'renewal' ); if ( ! $subscription_renewal ) { return true; } @@ -284,10 +266,10 @@ public function should_hide_widgets( bool $return ): bool { return $return; } - if ( $this->cart_contains_renewal() - || $this->get_subscription_switch_id_from_superglobal() - || 0 < count( $this->get_subscription_switch_cart_items() ) - || $this->cart_contains_resubscribe() ) { + if ( $this->get_subscription_type_from_cart( 'renewal' ) + || $this->get_subscription_type_from_cart( 'resubscribe' ) + || $this->get_subscription_type_from_cart( 'switch' ) + || $this->get_subscription_from_superglobal_switch_id() ) { return true; } @@ -295,50 +277,53 @@ public function should_hide_widgets( bool $return ): bool { } /** - * Checks the cart to see if it contains a subscription product renewal. + * Checks the cart values to see if there are subscriptions with specific types present. * - * @return mixed The cart item containing the renewal as an array, else false. - */ - private function cart_contains_renewal() { - if ( ! function_exists( 'wcs_cart_contains_renewal' ) ) { - return false; - } - return wcs_cart_contains_renewal(); - } - - /** - * Checks an order to see if it contains a subscription product renewal. + * This checks both the cart itself and the session. This is due to there are times when an item may be present in + * one place and not the other. We need to make sure that if an item is in either we are not creating double conversions. * - * @param object $order Order object. + * @param string $type The type of subscription to look for in the cart. * - * @return bool The cart item containing the renewal as an array, else false. + * @return mixed False if none found, or the subscription cart item as an array. */ - private function order_contains_renewal( $order ): bool { - if ( ! function_exists( 'wcs_order_contains_renewal' ) ) { + private function get_subscription_type_from_cart( $type ) { + // Make sure we're looking for allowed types. + if ( ! in_array( $type, self::SUBSCRIPTION_TYPES, true ) ) { return false; } - return wcs_order_contains_renewal( $order ); - } - /** - * Gets the subscription switch items out of the cart. - * - * @return array Empty array or the cart items in an array.. - */ - private function get_subscription_switch_cart_items(): array { - if ( ! function_exists( 'wcs_get_order_type_cart_items' ) ) { - return []; + // Set the sub type cart key. + $subscription_type = 'subscription_' . $type; + + // Go through each cart item and if it matches the type, return that item. + if ( isset( WC()->cart ) && is_array( WC()->cart->cart_contents ) && ! empty( WC()->cart->cart_contents ) ) { + foreach ( WC()->cart->cart_contents as $cart_item ) { + if ( isset( $cart_item[ $subscription_type ] ) ) { + return $cart_item; + } + } + } + + // Go through each session cart item and if it matches the type, return that item. + if ( isset( WC()->session ) && is_array( WC()->session->get( 'cart' ) ) && ! empty( WC()->session->get( 'cart' ) ) ) { + foreach ( WC()->session->get( 'cart' ) as $cart_item ) { + if ( isset( $cart_item[ $subscription_type ] ) ) { + return $cart_item; + } + } } - return wcs_get_order_type_cart_items( 'switch' ); + + return false; } /** * Getter for subscription objects. * * @param mixed $the_subscription Post object or post ID of the order. + * * @return mixed The subscription object, or false if it cannot be found. - * Note: this is WC_Subscription|bool in normal use, but in tests - * we use WC_Order to simulate a subscription (hence `mixed`). + * Note: This should be WC_Subscription|bool, but Psalm throws errors like: + * Docblock-defined class, interface or enum named WC_Subscription does not exist (see https://psalm.dev/200) */ private function get_subscription( $the_subscription ) { if ( ! function_exists( 'wcs_get_subscription' ) ) { @@ -352,9 +337,11 @@ private function get_subscription( $the_subscription ) { * This `switch-subscription` param is added to the URL when a customer * has initiated a switch from the My Account → Subscription page. * - * @return int|bool The ID of the subscription being switched, or false if it cannot be found. + * @return mixed The subscription object, or false if it cannot be found. + * Note: This should be WC_Subscription|bool, but Psalm throws errors like: + * Docblock-defined class, interface or enum named WC_Subscription does not exist (see https://psalm.dev/200) */ - private function get_subscription_switch_id_from_superglobal() { + private function get_subscription_from_superglobal_switch_id() { // Return false if there's no nonce, or if it fails. if ( ! isset( $_GET['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wcsnonce'] ), 'wcs_switch_request' ) ) { return false; @@ -373,9 +360,9 @@ private function get_subscription_switch_id_from_superglobal() { $switch_subscription = $this->get_subscription( $switch_id ); $this->running_override_selected_currency_filters = false; - // Confirm the sub user matches current user, and return the sub ID. + // Confirm the sub user matches current user, and return the sub. if ( $switch_subscription && $switch_subscription->get_customer_id() === get_current_user_id() ) { - return $switch_subscription->get_id(); + return $switch_subscription; } else { Logger::notice( 'User (' . get_current_user_id() . ') attempted to switch a subscription (' . $switch_subscription->get_id() . ') not assigned to them.' ); } @@ -383,58 +370,6 @@ private function get_subscription_switch_id_from_superglobal() { return false; } - /** - * Checks the cart to see if it contains a resubscription. - * - * @return mixed The cart item containing the resubscription as an array, else false. - */ - private function cart_contains_resubscribe() { - if ( ! function_exists( 'wcs_cart_contains_resubscribe' ) ) { - return false; - } - return wcs_cart_contains_resubscribe(); - } - - /** - * Checks to see if the product passed is in the cart as a subscription type. - * - * @param object $product Product to test. - * @param string $type Type of subscription. - * - * @return bool True if found in the cart, false if not. - */ - private function is_product_subscription_type_in_cart( $product, $type ): bool { - if ( ! function_exists( 'wcs_get_subscription' ) ) { - return false; - } - - $subscription = false; - - switch ( $type ) { - case 'renewal': - $subscription_item = $this->cart_contains_renewal(); - - if ( $subscription_item ) { - $subscription = wcs_get_subscription( $subscription_item['subscription_renewal']['subscription_id'] ); - } - break; - - case 'resubscribe': - $subscription_item = $this->cart_contains_resubscribe(); - - if ( $subscription_item ) { - $subscription = wcs_get_subscription( $subscription_item['subscription_resubscribe']['subscription_id'] ); - } - break; - } - - if ( $subscription && $product && $subscription->has_product( $product->get_id() ) ) { - return true; - } - - return false; - } - /** * Checks to see if the coupon passed is of a specified type. * diff --git a/tests/unit/helpers/class-wc-helper-subscription.php b/tests/unit/helpers/class-wc-helper-subscription.php index 5bb7a3a46a6..2def6724841 100644 --- a/tests/unit/helpers/class-wc-helper-subscription.php +++ b/tests/unit/helpers/class-wc-helper-subscription.php @@ -120,6 +120,13 @@ class WC_Subscription extends WC_Mock_WC_Data { */ public $has_product = false; + /** + * The customer ID for the subscription. + * + * @var null|int + */ + public $customer_id = null; + /** * A helper function for handling function calls not yet implimented on this helper. * @@ -214,6 +221,10 @@ public function get_currency() { return $this->currency; } + public function set_currency( $currency = 'USD' ) { + $this->currency = $currency; + } + public function add_order_note( $note = '' ) { // do nothing. } @@ -257,4 +268,13 @@ public function set_has_product( bool $has_product ) { public function has_product() { return $this->has_product; } + + public function get_customer_id() { + return $this->customer_id ?? get_current_user_id(); + } + + public function set_customer_id( $customer_id = null ) { + $this->customer_id = $customer_id ?? get_current_user_id(); + + } } diff --git a/tests/unit/multi-currency/compatibility/test-class-woocommerce-subscriptions.php b/tests/unit/multi-currency/compatibility/test-class-woocommerce-subscriptions.php index b1ee4f21fb0..341dfb73f9f 100644 --- a/tests/unit/multi-currency/compatibility/test-class-woocommerce-subscriptions.php +++ b/tests/unit/multi-currency/compatibility/test-class-woocommerce-subscriptions.php @@ -77,15 +77,12 @@ public function set_up() { } public function tear_down() { - // Reset cart checks so future tests can pass. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_wcs_get_order_type_cart_items( false ); + // Clear our cart on every iteration, also clears the session cart. + WC()->cart->empty_cart(); parent::tear_down(); } - /** * @dataProvider woocommerce_filter_provider */ @@ -116,119 +113,78 @@ public function woocommerce_filter_provider() { ]; } - // Test should not convert the product price due to all checks return true. - public function test_get_subscription_product_price_does_not_convert_price() { - // Arrange: Create a subscription to be used. - $mock_subscription = new WC_Subscription(); - $mock_subscription->set_has_product( true ); - - // Arrange: Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Arrange: Set our mock return values. - $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( true ); - $this->mock_wcs_cart_contains_renewal( 42, 43, 44 ); - $this->mock_wcs_cart_contains_resubscribe( 42 ); - + // Will not convert the sub price due null is passed as the price. + public function test_get_subscription_product_price_does_not_convert_price_when_no_price_passed() { // Act: Attempt to convert the subscription price. - $result = $this->woocommerce_subscriptions->get_subscription_product_price( 10.0, $this->mock_product ); + $result = $this->woocommerce_subscriptions->get_subscription_product_price( null, $this->mock_product ); - // Assert: Confirm the result value is not converted. - $this->assertSame( 10.0, $result ); + // Assert: Confirm the result value is null. + $this->assertNull( $result ); } - // Test should convert product price due to all checks return false. - public function test_get_subscription_product_price_converts_price_with_all_checks_false() { - $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( false ); - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_multi_currency->method( 'get_price' )->with( 10.0, 'product' )->willReturn( 25.0 ); - $this->assertSame( 25.0, $this->woocommerce_subscriptions->get_subscription_product_price( 10.0, $this->mock_product ) ); - } - - // Test should convert product price due to the backtrace check returns true but the cart contains renewal/resubscribe return checks false. - public function test_get_subscription_product_price_converts_price_if_only_backtrace_found() { + /** + * Will not convert the sub price due to the is_call_in_backtrace calls in should_convert_product_price return true, which + * causes should_convert_product_price to return false to not convert the price. + */ + public function test_get_subscription_product_price_does_not_convert_price() { + // Arrange: Set our mock return values. $this->mock_utils - ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) - ->with( [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] ) - ->willReturn( false ); + ->willReturn( true ); - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_multi_currency->method( 'get_price' )->with( 10.0, 'product' )->willReturn( 25.0 ); - $this->assertSame( 25.0, $this->woocommerce_subscriptions->get_subscription_product_price( 10.0, $this->mock_product ) ); + // Act/Assert: Confirm the result value is not converted. + $this->assertSame( 10.0, $this->woocommerce_subscriptions->get_subscription_product_price( 10.0, $this->mock_product ) ); } - // Test should convert product price due to the backtrace check returns false after the cart contains renewal check returns true. - public function test_get_subscription_product_price_converts_price_if_only_renewal_in_cart() { - // Arrange: Create a subscription to be used. - $mock_subscription = new WC_Subscription(); - $mock_subscription->set_has_product( true ); - - // Arrange: Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Arrange: Set our mock return values. - $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( false ); - $this->mock_wcs_cart_contains_renewal( 42, 43, 44 ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_multi_currency->method( 'get_price' )->with( 10.0, 'product' )->willReturn( 25.0 ); - - // Act: Attempt to convert the subscription price. - $result = $this->woocommerce_subscriptions->get_subscription_product_price( 10.0, $this->mock_product ); - - // Assert: Confirm the result value is converted. - $this->assertSame( 25.0, $result ); + // Will not convert the sub signup fee due null is passed as the fee. + public function test_get_subscription_product_signup_fee_does_not_convert_price_when_no_fee_passed() { + // Act/Assert: Confirm the result value is null. + $this->assertNull( $this->woocommerce_subscriptions->get_subscription_product_signup_fee( null, $this->mock_product ) ); } - // Test should convert product price due to the backtrace check returns false after the cart contains resubscribe check returns true. - public function test_get_subscription_product_price_converts_price_if_only_resubscribe_in_cart() { - // Arrange: Create a subscription to be used. - $mock_subscription = new WC_Subscription(); - $mock_subscription->set_has_product( true ); - - // Arrange: Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Arrange: Set our mock return values. - $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( false ); - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( 42 ); - $this->mock_multi_currency->method( 'get_price' )->with( 10.0, 'product' )->willReturn( 25.0 ); - - // Act: Attempt to convert the subscription price. - $result = $this->woocommerce_subscriptions->get_subscription_product_price( 10.0, $this->mock_product ); + // If there is no switch in the cart, then the signup fee should be converted. + public function test_get_subscription_product_signup_fee_converts_fee_when_no_switch_in_cart() { + // Arrange: Set the expectation and return for the call to get_price. + $this->mock_multi_currency + ->expects( $this->once() ) + ->method( 'get_price' ) + ->with( 10.0, 'product' ) + ->willReturn( 25.0 ); - // Assert: Confirm the result value is converted. - $this->assertSame( 25.0, $result ); + // Act/Assert: Confirm the result value is converted. + $this->assertSame( 25.0, $this->woocommerce_subscriptions->get_subscription_product_signup_fee( 10.0, $this->mock_product ) ); } // Does not convert price due to first backtrace check returns true. public function test_get_subscription_product_signup_fee_does_not_convert_price_on_first_backtrace_match() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'switch' ); + + // Arrange: Set the expectation and return for the is_call_in_backtrace call. $this->mock_utils ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) ->with( [ 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation' ] ) ->willReturn( true ); - $this->mock_wcs_get_order_type_cart_items( 42 ); + + // Arrange: Set the expectation for the call to get_price. + $this->mock_multi_currency + ->expects( $this->never() ) + ->method( 'get_price' ); + + // Act/Assert: Confirm the result value is not converted. $this->assertSame( 10.0, $this->woocommerce_subscriptions->get_subscription_product_signup_fee( 10.0, $this->mock_product ) ); } // Does not convert price due to second check with backtrace and cart item key check returns true. public function test_get_subscription_product_signup_fee_does_not_convert_price_during_proration_calculation() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'switch' ); + + // Arrange: Set our switch_cart_item property. + $this->woocommerce_subscriptions->switch_cart_item = 'abc123'; + + // Arrange: Set the expectations and returns for the is_call_in_backtrace calls. $this->mock_utils ->expects( $this->exactly( 4 ) ) ->method( 'is_call_in_backtrace' ) @@ -239,290 +195,252 @@ public function test_get_subscription_product_signup_fee_does_not_convert_price_ [ [ 'WCS_Switch_Totals_Calculator->apportion_sign_up_fees' ] ] ) ->willReturn( false, true, true, false ); - $this->mock_wcs_get_order_type_cart_items( 42 ); - $this->woocommerce_subscriptions->switch_cart_item = 'abc123'; + + // Arrange: Set the expectation for the call to get_price. + $this->mock_multi_currency + ->expects( $this->never() ) + ->method( 'get_price' ); + + // Act/Assert: Confirm the result value is not converted. $this->assertSame( 10.0, $this->woocommerce_subscriptions->get_subscription_product_signup_fee( 10.0, $this->mock_product ) ); } // Does not convert due to third check for changes in the meta data returns true. public function test_get_subscription_product_signup_fee_does_not_convert_price_when_meta_already_updated() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'switch' ); + + // Arrange: Set our switch_cart_item property. + $this->woocommerce_subscriptions->switch_cart_item = 'abc123'; + + // Arrange: Set the expectation for the call to is_call_in_backtrace and always return false. $this->mock_utils - ->expects( $this->any() ) + ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) ->willReturn( false ); - $this->mock_wcs_get_order_type_cart_items( 42 ); - $this->woocommerce_subscriptions->switch_cart_item = 'abc123'; + // Arrange: Set expectations and returns for get_meta_data, get_data, and get_changes. + $this->mock_product + ->expects( $this->once() ) + ->method( 'get_meta_data' ) + ->willReturn( [ $this->mock_meta_data ] ); $this->mock_meta_data + ->expects( $this->once() ) ->method( 'get_data' ) ->willReturn( [ 'key' => '_subscription_sign_up_fee' ] ); $this->mock_meta_data + ->expects( $this->once() ) ->method( 'get_changes' ) ->willReturn( [ 1, 2 ] ); - $this->mock_product - ->method( 'get_meta_data' ) - ->willReturn( [ $this->mock_meta_data ] ); + // Arrange: Set the expectation for the call to get_price. + $this->mock_multi_currency + ->expects( $this->never() ) + ->method( 'get_price' ); + // Act/Assert: Confirm the result value is not converted. $this->assertSame( 10.0, $this->woocommerce_subscriptions->get_subscription_product_signup_fee( 10.0, $this->mock_product ) ); } - // Converts due to backtraces are not found and the check for changes in meta data returns false. - public function test_get_subscription_product_signup_fee_converts_price_when_meta_not_updated() { + // Converts price due to the switch item does not match the item being checked. + public function test_get_subscription_product_signup_fee_converts_price_when_cart_item_keys_do_not_match() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'switch' ); + + // Arrange: Set our switch_cart_item property so that it does not match what's in the cart. + $this->woocommerce_subscriptions->switch_cart_item = 'def456'; + + // Arrange: Set the expectation for the call to is_call_in_backtrace and always return false. $this->mock_utils - ->expects( $this->any() ) + ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) ->willReturn( false ); - $this->mock_wcs_get_order_type_cart_items( 42 ); - $this->woocommerce_subscriptions->switch_cart_item = 'abc123'; + // Arrange: Set expectations for get_meta_data, get_data, and get_changes. + $this->mock_product + ->expects( $this->never() ) + ->method( 'get_meta_data' ); $this->mock_meta_data - ->method( 'get_data' ) - ->willReturn( [ 'key' => '_subscription_sign_up_fee' ] ); + ->expects( $this->never() ) + ->method( 'get_data' ); $this->mock_meta_data - ->method( 'get_changes' ) - ->willReturn( [] ); + ->expects( $this->never() ) + ->method( 'get_changes' ); - $this->mock_product - ->method( 'get_meta_data' ) - ->willReturn( [ $this->mock_meta_data ] ); + // Arrange: Set the expectation and return value for the call to get_price. + $this->mock_multi_currency + ->expects( $this->once() ) + ->method( 'get_price' ) + ->with( 10.0, 'product' ) + ->willReturn( 25.0 ); - $this->mock_multi_currency->method( 'get_price' )->with( 10.0, 'product' )->willReturn( 25.0 ); + // Act/Assert: Confirm the result value is converted. $this->assertSame( 25.0, $this->woocommerce_subscriptions->get_subscription_product_signup_fee( 10.0, $this->mock_product ) ); } - // Converts due to the same as above, and the cart item keys do not match. - public function test_get_subscription_product_signup_fee_converts_price_when_cart_item_keys_do_not_match() { + // Converts due to backtraces are not found and the check for changes in meta data returns false. + public function test_get_subscription_product_signup_fee_converts_price_when_meta_not_updated() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'switch' ); + + // Arrange: Set our switch_cart_item property. + $this->woocommerce_subscriptions->switch_cart_item = 'abc123'; + + // Arrange: Set the expectation for the call to is_call_in_backtrace and always return false. $this->mock_utils - ->expects( $this->any() ) + ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) ->willReturn( false ); - $this->mock_wcs_get_order_type_cart_items( 42 ); - $this->woocommerce_subscriptions->switch_cart_item = 'def456'; + // Arrange: Set expectations and returns for get_meta_data and get_data. $this->mock_product + ->expects( $this->once() ) ->method( 'get_meta_data' ) + ->willReturn( [ $this->mock_meta_data ] ); + $this->mock_meta_data + ->expects( $this->once() ) + ->method( 'get_data' ) + ->willReturn( [ 'key' => '_subscription_sign_up_fee' ] ); + + // Arrange: Set expectation and return for get_changes so that it is empty. + $this->mock_meta_data + ->expects( $this->once() ) + ->method( 'get_changes' ) ->willReturn( [] ); - $this->mock_multi_currency->method( 'get_price' )->with( 10.0, 'product' )->willReturn( 25.0 ); + // Arrange: Set the expectation and return value for the call to get_price. + $this->mock_multi_currency + ->expects( $this->once() ) + ->method( 'get_price' ) + ->with( 10.0, 'product' ) + ->willReturn( 25.0 ); + + // Act/Assert: Confirm the result value is converted. $this->assertSame( 25.0, $this->woocommerce_subscriptions->get_subscription_product_signup_fee( 10.0, $this->mock_product ) ); } public function test_maybe_disable_mixed_cart_return_no() { - $this->mock_wcs_get_order_type_cart_items( 42 ); + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'switch' ); + + // Act/Assert: 'no' should be returned due to the item in the cart is a switch. $this->assertSame( 'no', $this->woocommerce_subscriptions->maybe_disable_mixed_cart( 'yes' ) ); } public function test_maybe_disable_mixed_cart_return_yes() { - $this->mock_wcs_get_order_type_cart_items( false ); + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'renewal' ); + + // Act/Assert: 'yes' should be returned due to the item in the cart is a renewal and not a switch. $this->assertSame( 'yes', $this->woocommerce_subscriptions->maybe_disable_mixed_cart( 'yes' ) ); } - // Returns code due to code was passed. + // Returns currency code due to code was passed. public function test_override_selected_currency_return_currency_code_when_code_passed() { - // Conditions added to return EUR, but CAD should be returned at the beginning of the method. - $this->mock_wcs_cart_contains_renewal( 42, 43 ); - $this->mock_wcs_cart_contains_resubscribe( false ); - update_post_meta( 42, '_order_currency', 'EUR', true ); - $this->mock_wcs_get_order_type_cart_items( false ); + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items(); + + // Arrange: Set the currency for the sub and update the cart items in the session. + $mock_subscription->set_currency( 'JPY' ); + WC()->session->set( 'cart', $cart_items ); + // Assert: CAD should be returned since it was passed, even though there is an item in the cart. $this->assertSame( 'CAD', $this->woocommerce_subscriptions->override_selected_currency( 'CAD' ) ); } - // Returns false due to all checks return false. - public function test_override_selected_currency_return_false() { - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_wcs_get_order_type_cart_items( false ); + // Returns false due we are not adding products to the cart. + public function test_override_selected_currency_return_false_if_no_cart_items() { + // Assert: False should be received since there's nothing in the cart. $this->assertFalse( $this->woocommerce_subscriptions->override_selected_currency( false ) ); } - // Returns code due to cart contains a subscription renewal. - public function test_override_selected_currency_return_currency_code_when_renewal_in_cart() { - // Set up an order with a non-default currency. - $order = WC_Helper_Order::create_order(); - $order->set_currency( 'JPY' ); - $order->save(); - - // Mock that order has the renewal in the cart. - $this->mock_wcs_cart_contains_renewal( 42, $order->get_id() ); - - $this->assertSame( 'JPY', $this->woocommerce_subscriptions->override_selected_currency( false ) ); - } - - // Returns order currency code when order awaiting payment has renewal in it. - public function test_override_selected_currency_returns_order_currency_code_when_order_awaiting_payment_has_renewal() { - // Set up an order with a non-default currency. - $order = WC_Helper_Order::create_order(); - $order->set_currency( 'JPY' ); - $order->save(); - WC()->session->set( 'order_awaiting_payment', $order->get_id() ); + /** + * Will return the specified codes due to the cart check looks first in the cart object, and then in the session. With the first + * check, the cart object is empty, so the session is checked. With the second check, the cart object now has a subscription, so + * its code is returned. + * + * This confirms that the get_subscription_type_from_cart method is working correctly. + * + * @dataProvider provider_sub_types_renewal_resubscribe_switch + */ + public function test_override_selected_currency_return_currency_code_when_sub_type_in_cart( $sub_type ) { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( $sub_type ); - // Mock that order has the renewal in the cart. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_order_contains_renewal( true ); + // Arrange: Set the currency for the sub and update the cart items in the session. + $mock_subscription->set_currency( 'JPY' ); + WC()->session->set( 'cart', $cart_items ); + // Act/Assert: Confirm that the currency is what we set. $this->assertSame( 'JPY', $this->woocommerce_subscriptions->override_selected_currency( false ) ); - } - // Returns false when order awaiting payment does not have a renewal in it. - public function test_override_selected_currency_return_false_when_order_awaiting_payment_has_no_renewal() { - // Set up an order with a non-default currency. - $order = WC_Helper_Order::create_order(); - $order->set_currency( 'JPY' ); - $order->save(); - WC()->session->set( 'order_awaiting_payment', $order->get_id() ); + // Arrange: Change the sub's currency and update the cart contents in the WC object. + $mock_subscription->set_currency( 'EUR' ); + WC()->cart->set_cart_contents( $cart_items ); - // Mock that order renewal in the cart. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_order_contains_renewal( false ); - - $this->assertFalse( $this->woocommerce_subscriptions->override_selected_currency( false ) ); + // Act/Assert: Confirm the currency is what we set. + $this->assertSame( 'EUR', $this->woocommerce_subscriptions->override_selected_currency( false ) ); } // Test correct currency when shopper clicks upgrade/downgrade button in My Account – "switch". public function test_override_selected_currency_return_currency_code_for_switch_request() { - // Reset/clear any previous mocked state. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_wcs_get_order_type_cart_items( false ); - - // Set up an order with a non-default currency. - // Note we're using a WC_Order as a stand-in for a true WC_Subscription. - $mock_subscription = WC_Helper_Order::create_order(); + // Arrange: Create a mock subscription and assign its currency. + $mock_subscription = $this->create_mock_subscription(); $mock_subscription->set_currency( 'JPY' ); - $mock_subscription->save(); - // Get the current user, then update the current user to the user for the order/sub. - $current_user_id = get_current_user_id(); - wp_set_current_user( $mock_subscription->get_customer_id() ); - - // Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Blatantly hack mock request params for the test. + // Arrange: Blatantly hack mock request params for the test. $_GET['switch-subscription'] = $mock_subscription->get_id(); $_GET['_wcsnonce'] = wp_create_nonce( 'wcs_switch_request' ); + // Act/Assert: Confirm that the currency returned is that of the subscription. $this->assertSame( 'JPY', $this->woocommerce_subscriptions->override_selected_currency( false ) ); - - // Reset the current user. - wp_set_current_user( $current_user_id ); } // Return false if the current user doesn't match the user of the switching subscription. public function test_override_selected_currency_return_false_for_switch_request_when_no_user_match() { - // Reset/clear any previous mocked state. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_wcs_get_order_type_cart_items( false ); - - // Set up an order with a non-default currency. - // Note we're using a WC_Order as a stand-in for a true WC_Subscription. - $mock_subscription = WC_Helper_Order::create_order(); + // Arrange: Create a mock subscription and assign its currency and user. + $mock_subscription = $this->create_mock_subscription(); $mock_subscription->set_currency( 'JPY' ); - $mock_subscription->save(); - - // Get the current user, then update the current user to a random user. - $current_user_id = get_current_user_id(); - wp_set_current_user( 42 ); - - // Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); + $mock_subscription->set_customer_id( 42 ); - // Blatantly hack mock request params for the test. + // Arrange: Blatantly hack mock request params for the test. $_GET['switch-subscription'] = $mock_subscription->get_id(); $_GET['_wcsnonce'] = wp_create_nonce( 'wcs_switch_request' ); + // Act/Assert: Confirm that false is returned. $this->assertFalse( $this->woocommerce_subscriptions->override_selected_currency( false ) ); - - // Reset the current user. - wp_set_current_user( $current_user_id ); - } - - // Returns code due to cart contains a subscription switch. - public function test_override_selected_currency_return_currency_code_when_switch_in_cart() { - // Reset/clear any previous mocked state. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - - // Mock order with custom currency for switch cart item. - // Note we're using a WC_Order as a stand-in for a true WC_Subscription. - $mock_subscription = WC_Helper_Order::create_order(); - $mock_subscription->set_currency( 'JPY' ); - $mock_subscription->save(); - - // Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Mock cart to simulate a switch cart item referencing our subscription. - $this->mock_wcs_get_order_type_cart_items( $mock_subscription->get_id() ); - - $this->assertSame( 'JPY', $this->woocommerce_subscriptions->override_selected_currency( false ) ); } - // Returns code due to cart contains a subscription resubscribe. - public function test_override_selected_currency_return_currency_code_when_resubscribe_in_cart() { - // Reset/clear any previous mocked state. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_get_order_type_cart_items( false ); + // The default passed into should_convert_product_price is true, this passes false to confirm false is returned. + public function test_should_convert_product_price_return_false_when_false_passed() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items(); - // Mock order with custom currency for switch cart item. - // Note we're using a WC_Order as a stand-in for a true WC_Subscription. - $mock_subscription = WC_Helper_Order::create_order(); + // Arrange: Set the currency for the sub and update the cart items in the session. $mock_subscription->set_currency( 'JPY' ); - $mock_subscription->save(); + WC()->session->set( 'cart', $cart_items ); - // Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Mock cart to simulate a resubscribe cart item referencing our subscription. - $this->mock_wcs_cart_contains_resubscribe( $mock_subscription->get_id() ); - - $this->assertSame( 'JPY', $this->woocommerce_subscriptions->override_selected_currency( false ) ); - } - - public function test_should_convert_product_price_return_false_when_false_passed() { - // Conditions added to return true, but it should return false if passed. - $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( false ); - $this->mock_wcs_cart_contains_renewal( 42, 43 ); - $this->mock_wcs_cart_contains_resubscribe( 42 ); + // Arrange: Set expecation that is_call_in_backtrace should not be called. + $this->mock_utils + ->expects( $this->never() ) + ->method( 'is_call_in_backtrace' ); + // Act/Assert: Confirm that false is returned if passed. $this->assertFalse( $this->woocommerce_subscriptions->should_convert_product_price( false, $this->mock_product ) ); } - public function test_should_convert_product_price_return_false_when_renewal_in_cart() { - // Arrange: Create a subscription to be used. - $mock_subscription = new WC_Subscription(); - $mock_subscription->set_has_product( true ); - - // Arrange: Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); + /** + * Confirm that false is returned if specific types of subs are in the cart and there are specific calls in the backtrace. + * + * @dataProvider provider_sub_types_renewal_resubscribe + */ + public function test_should_convert_product_price_return_false_when_sub_type_in_cart_and_backtrace_match( $sub_type ) { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( $sub_type ); - // Arrange: Set our mock return values. - $this->mock_wcs_cart_contains_renewal( 42, 43, 44 ); - $this->mock_wcs_cart_contains_resubscribe( false ); + // Arrange: Set expectation and return for is_call_in_backtrace. $this->mock_utils + ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) ->with( [ @@ -534,85 +452,104 @@ function ( $id ) use ( $mock_subscription ) { ) ->willReturn( true ); - // Act: Attempt to convert the subscription price. - $result = $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ); - - // Assert: Confirm the result value is false. - $this->assertFalse( $result ); + // Act/Assert: Confirm the result value is false. + $this->assertFalse( $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ) ); } - public function test_should_convert_product_price_return_false_when_resubscribe_in_cart() { - // Arrange: Create a subscription to be used. - $mock_subscription = new WC_Subscription(); - $mock_subscription->set_has_product( true ); - - // Arrange: Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); + /** + * Confirm that true is returned even if there are specific sub types in the cart, but the backtraces are not correct. + * + * @dataProvider provider_sub_types_renewal_resubscribe + */ + public function test_should_convert_product_price_return_true_when_sub_type_in_cart_and_backtraces_do_not_match( $sub_type ) { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( $sub_type ); - // Arrange: Set our mock return values. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( 42 ); + // Arrange: Set expectations and returns for is_call_in_backtrace. $this->mock_utils + ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) - ->with( + ->withConsecutive( [ - 'WC_Cart_Totals->calculate_item_totals', - 'WC_Cart->get_product_subtotal', - 'wc_get_price_excluding_tax', - 'wc_get_price_including_tax', - ] + [ + 'WC_Cart_Totals->calculate_item_totals', + 'WC_Cart->get_product_subtotal', + 'wc_get_price_excluding_tax', + 'wc_get_price_including_tax', + ], + ], + [ [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] ] ) - ->willReturn( true ); - - // Act: Attempt to convert the subscription price. - $result = $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ); + ->willReturn( false ); - // Assert: Confirm the result value is false. - $this->assertFalse( $result ); + // Act/Assert: Confirm the result value is true. + $this->assertTrue( $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ) ); } - public function test_should_convert_product_price_return_true_when_backtrace_does_not_match() { - // Arrange: Create a subscription to be used. - $mock_subscription = new WC_Subscription(); - $mock_subscription->set_has_product( true ); - - // Arrange: Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Arrange: Set our mock return values. - $this->mock_wcs_cart_contains_renewal( 42, 43, 44 ); - $this->mock_wcs_cart_contains_resubscribe( 42 ); - $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( false ); + /** + * Confirm that true is returned even if there are specific sub types in the cart, but the backtraces are not correct. + * This is the same as the above, with the second backtrace check being true, so the third one is now checked. + * + * @dataProvider provider_sub_types_renewal_resubscribe + */ + public function test_should_convert_product_price_return_true_when_sub_type_in_cart_and_backtraces_do_not_match_exactly( $sub_type ) { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( $sub_type ); - // Act: Attempt to convert the subscription price. - $result = $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ); + // Arrange: Set expectations and returns for is_call_in_backtrace. + $this->mock_utils + ->expects( $this->exactly( 3 ) ) + ->method( 'is_call_in_backtrace' ) + ->withConsecutive( + [ + [ + 'WC_Cart_Totals->calculate_item_totals', + 'WC_Cart->get_product_subtotal', + 'wc_get_price_excluding_tax', + 'wc_get_price_including_tax', + ], + ], + [ [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] ], + [ [ 'WC_Product->get_price' ] ] + ) + ->willReturn( false, true, false ); - // Assert: Confirm the result value is true. - $this->assertTrue( $result ); + // Act/Assert: Confirm the result value is true. + $this->assertTrue( $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ) ); } - public function test_should_convert_product_price_return_true_with_no_subscription_actions_in_cart() { + // Confirm if there are no sub_types in cart and the first backtrace does not match, true is returned. + public function test_should_convert_product_price_return_true_with_no_sub_types_in_cart_and_no_backtrace_match() { + // Arrange: Set expectation and return for is_call_in_backtrace. $this->mock_utils ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) ->with( [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] ) ->willReturn( false ); - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); + // Act/Assert: Confirm the result value is true. + $this->assertTrue( $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ) ); + } + + // Confirm if there are no sub_types in cart and the second backtrace does not match, true is returned. + public function test_should_convert_product_price_return_true_with_no_sub_types_in_cart_and_no_second_backtrace_match() { + // Arrange: Set expectations and returns for is_call_in_backtrace. + $this->mock_utils + ->expects( $this->exactly( 2 ) ) + ->method( 'is_call_in_backtrace' ) + ->withConsecutive( + [ [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] ], + [ [ 'WC_Product->get_price' ] ] + ) + ->willReturn( true, false ); + + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_product ) ); } // Test for when WCPay Subs is getting the product's price for the sub creation. public function test_should_convert_product_price_return_false_when_get_recurring_item_data_for_subscription() { + // Arrange: Set expectations and returns for is_call_in_backtrace. $this->mock_utils ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) @@ -622,64 +559,93 @@ public function test_should_convert_product_price_return_false_when_get_recurrin ) ->willReturn( true, true ); - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); + // Act/Assert: Confirm the result value is false. $this->assertFalse( $this->woocommerce_subscriptions->should_convert_product_price( true, $this->mock_coupon ) ); } + /** + * This method should return false if false is passed. + * The test does not add a renewal to the cart, which would cause it to return true, but it shouldn't make it there. + * The is_call_in_backtrace call should also never be called. + */ public function test_should_convert_coupon_amount_return_false_if_false_passed() { - // Conditions added to return true, but should return false if false passed. + // Arrange: Set expectation for is_call_in_backtrace. $this->mock_utils - ->expects( $this->exactly( 0 ) ) + ->expects( $this->never() ) ->method( 'is_call_in_backtrace' ); - $this->mock_wcs_cart_contains_renewal( false ); + // Act/Assert: Confirm the result value is false. $this->assertFalse( $this->woocommerce_subscriptions->should_convert_coupon_amount( false, $this->mock_coupon ) ); } - public function test_should_convert_coupon_amount_return_false_when_renewal_in_cart() { - $this->mock_utils - ->expects( $this->exactly( 2 ) ) - ->method( 'is_call_in_backtrace' ) - ->withConsecutive( - [ [ 'WCS_Cart_Early_Renewal->setup_cart' ] ], - [ [ 'WC_Discounts->apply_coupon' ] ] - ) - ->willReturn( false, true ); - + // Confirm that if there's a subscription percentage coupon type, we don't want to convert its amount. + public function test_should_convert_coupon_amount_return_false_when_subscription_percent_coupon_type() { + // Arrange: Set expectation and return for our mock coupon. $this->mock_coupon + ->expects( $this->once() ) ->method( 'get_discount_type' ) - ->willReturn( 'recurring_fee' ); + ->willReturn( 'recurring_percent' ); + + // Arrange: Set expectations and returns for is_call_in_backtrace. + $this->mock_utils + ->expects( $this->never() ) + ->method( 'is_call_in_backtrace' ); - $this->mock_wcs_cart_contains_renewal( 42, 43 ); + // Act/Assert: Confirm the result value is false. $this->assertFalse( $this->woocommerce_subscriptions->should_convert_coupon_amount( true, $this->mock_coupon ) ); } + // Confirm true is returned if there is not a renewal in the cart. public function test_should_convert_coupon_amount_return_true_with_no_renewal_in_cart() { + // Arrange: Set expectation and return for our mock coupon. + $this->mock_coupon + ->expects( $this->once() ) + ->method( 'get_discount_type' ) + ->willReturn( 'recurring_fee' ); + + // Arrange: Set expectations and returns for is_call_in_backtrace. $this->mock_utils - ->expects( $this->exactly( 0 ) ) + ->expects( $this->never() ) ->method( 'is_call_in_backtrace' ); - $this->mock_wcs_cart_contains_renewal( false ); + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_convert_coupon_amount( true, $this->mock_coupon ) ); } + // Confirm true is returned if there's a renewal in the cart, but it's not an early renewal. public function test_should_convert_coupon_amount_return_true_with_early_renewal_in_backtrace() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'renewal' ); + + // Arrange: Set expectation and return for our mock coupon. + $this->mock_coupon + ->expects( $this->once() ) + ->method( 'get_discount_type' ) + ->willReturn( 'recurring_fee' ); + + // Arrange: Set expectation and return for is_call_in_backtrace. This exits our last test and allows the true return. $this->mock_utils ->expects( $this->once() ) ->method( 'is_call_in_backtrace' ) ->with( [ 'WCS_Cart_Early_Renewal->setup_cart' ] ) ->willReturn( true ); - $this->mock_coupon - ->method( 'get_discount_type' ) - ->willReturn( 'recurring_fee' ); - - $this->mock_wcs_cart_contains_renewal( 42, 43 ); + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_convert_coupon_amount( true, $this->mock_coupon ) ); } + // Confirm true is returned if there's a renewal in the cart, if it is an early renewal, but the apply_coupon call is not found in the backtrace. public function test_should_convert_coupon_amount_return_true_when_apply_coupon_not_in_backtrace() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'renewal' ); + + // Arrange: Set expectation and return for our mock coupon. + $this->mock_coupon + ->expects( $this->once() ) + ->method( 'get_discount_type' ) + ->willReturn( 'recurring_fee' ); + + // Arrange: Set expectation and return for is_call_in_backtrace. This exits our last test and allows the true return. $this->mock_utils ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) @@ -689,15 +655,22 @@ public function test_should_convert_coupon_amount_return_true_when_apply_coupon_ ) ->willReturn( false, false ); - $this->mock_coupon - ->method( 'get_discount_type' ) - ->willReturn( 'recurring_fee' ); - - $this->mock_wcs_cart_contains_renewal( 42, 43 ); + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_convert_coupon_amount( true, $this->mock_coupon ) ); } + // Confirm true is returned if there's a renewal in the cart, if it is an early renewal, the coupon is being applied, but it's the wrong coupon type. public function test_should_convert_coupon_amount_return_true_when_coupon_type_does_not_match() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'renewal' ); + + // Arrange: Set expectation and return for our mock coupon. The second call exits our last test and allows the true return. + $this->mock_coupon + ->expects( $this->exactly( 2 ) ) + ->method( 'get_discount_type' ) + ->willReturn( 'failing_fee' ); + + // Arrange: Set expectation and return for is_call_in_backtrace. $this->mock_utils ->expects( $this->exactly( 2 ) ) ->method( 'is_call_in_backtrace' ) @@ -707,190 +680,143 @@ public function test_should_convert_coupon_amount_return_true_when_coupon_type_d ) ->willReturn( false, true ); - $this->mock_coupon - ->method( 'get_discount_type' ) - ->willReturn( 'failing_fee' ); - - $this->mock_wcs_cart_contains_renewal( 42, 43 ); + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_convert_coupon_amount( true, $this->mock_coupon ) ); } - public function test_should_convert_coupon_amount_return_false_when_percentage_coupon_used() { + // Confirm false is returned if there's a renewal in the cart, the backtraces match, and the coupon is the proper type. + public function test_should_convert_coupon_amount_return_false_when_renewal_in_cart() { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( 'renewal' ); + + // Arrange: Set expectations and returns for is_call_in_backtrace. $this->mock_utils - ->expects( $this->exactly( 0 ) ) - ->method( 'is_call_in_backtrace' ); + ->expects( $this->exactly( 2 ) ) + ->method( 'is_call_in_backtrace' ) + ->withConsecutive( + [ [ 'WCS_Cart_Early_Renewal->setup_cart' ] ], + [ [ 'WC_Discounts->apply_coupon' ] ] + ) + ->willReturn( false, true ); + // Arrange: Set expectation and return for our mock coupon. $this->mock_coupon ->method( 'get_discount_type' ) - ->willReturn( 'recurring_percent' ); + ->willReturn( 'recurring_fee' ); - $this->mock_wcs_cart_contains_renewal( false ); + // Act/Assert: Confirm the result value is false. $this->assertFalse( $this->woocommerce_subscriptions->should_convert_coupon_amount( true, $this->mock_coupon ) ); } + // If true is passed to the method, true should be returned immediately. public function test_should_hide_widgets_return_true_if_true_passed() { - // Conditions set to return false, but should return true if true passed. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_wcs_get_order_type_cart_items( false ); - + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_hide_widgets( true ) ); } - // Should return false since all checks return false. + // If false is passed to the method and none of the checks are true, false is returned. public function test_should_hide_widgets_return_false() { - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->mock_wcs_get_order_type_cart_items( false ); + // Act/Assert: Confirm the result value is false. $this->assertFalse( $this->woocommerce_subscriptions->should_hide_widgets( false ) ); } - public function test_should_hide_widgets_return_true_when_renewal_in_cart() { - $this->mock_wcs_cart_contains_renewal( 42, 43 ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->assertTrue( $this->woocommerce_subscriptions->should_hide_widgets( false ) ); - } + /** + * Confirm true is returned when sub types are in cart. + * + * @dataProvider provider_sub_types_renewal_resubscribe_switch + */ + public function test_should_hide_widgets_return_true_when_sub_type_in_cart( $sub_type ) { + // Arrange: Create a subscription and cart_items to be used. + [ $mock_subscription, $cart_items ] = $this->get_mock_subscription_and_session_cart_items( $sub_type ); - public function test_should_hide_widgets_return_true_when_resubscribe_in_cart() { - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_cart_contains_resubscribe( 42 ); + // Act/Assert: Confirm the result value is true. $this->assertTrue( $this->woocommerce_subscriptions->should_hide_widgets( false ) ); } - // Should return true if switch found in GET, like on product page. - public function test_should_hide_widgets_return_true_when_starting_subscrition_switch() { - // Reset/clear any previous mocked state. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_get_order_type_cart_items( false ); - - // Set up an order to use for the test. - // Note we're using a WC_Order as a stand-in for a true WC_Subscription. - $mock_subscription = WC_Helper_Order::create_order(); - $mock_subscription->save(); - - // Get the current user, then update the current user to the user for the order/sub. - $current_user_id = get_current_user_id(); - wp_set_current_user( $mock_subscription->get_customer_id() ); + // Should return true if switch found in GET, for when a customer is doing a subscription switch. + public function test_should_hide_widgets_return_true_when_starting_subscription_switch() { + // Arrange: Create a mock subscription to use. + $mock_subscription = $this->create_mock_subscription(); - // Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); - - // Blatantly hack mock request params for the test. + // Arrange: Blatantly hack mock request params for the test. $_GET['switch-subscription'] = $mock_subscription->get_id(); $_GET['_wcsnonce'] = wp_create_nonce( 'wcs_switch_request' ); + // Act/Assert: Confirm that true is returned. $this->assertTrue( $this->woocommerce_subscriptions->should_hide_widgets( false ) ); - - // Reset the current user. - wp_set_current_user( $current_user_id ); } // Should return false since users will not match. public function test_should_hide_widgets_return_false_when_starting_subscrition_switch_and_no_user_match() { - // Reset/clear any previous mocked state. - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_get_order_type_cart_items( false ); - - // Set up an order to use for the test. - // Note we're using a WC_Order as a stand-in for a true WC_Subscription. - $mock_subscription = WC_Helper_Order::create_order(); - $mock_subscription->save(); - - // Get the current user, then update the current user to a random ID. - $current_user_id = get_current_user_id(); - wp_set_current_user( 42 ); - - // Mock wcs_get_subscription to return our mock subscription. - WC_Subscriptions::set_wcs_get_subscription( - function ( $id ) use ( $mock_subscription ) { - return $mock_subscription; - } - ); + // Arrange: Create a mock subscription and assign its user. + $mock_subscription = $this->create_mock_subscription(); + $mock_subscription->set_customer_id( 42 ); - // Blatantly hack mock request params for the test. + // Arrange: Blatantly hack mock request params for the test. $_GET['switch-subscription'] = $mock_subscription->get_id(); $_GET['_wcsnonce'] = wp_create_nonce( 'wcs_switch_request' ); + // Act/Assert: Confirm that false is returned. $this->assertFalse( $this->woocommerce_subscriptions->should_hide_widgets( false ) ); - - // Reset the current user. - wp_set_current_user( $current_user_id ); } - // Should return true if switch found in cart. - public function test_should_hide_widgets_return_true_when_switch_found_in_cart() { - $this->mock_wcs_cart_contains_renewal( false ); - $this->mock_wcs_get_order_type_cart_items( true ); - $this->mock_wcs_cart_contains_resubscribe( false ); - $this->assertTrue( $this->woocommerce_subscriptions->should_hide_widgets( false ) ); + public function provider_sub_types_renewal_resubscribe_switch() { + return [ + 'renewal' => [ 'renewal' ], + 'resubscribe' => [ 'resubscribe' ], + 'switch' => [ 'switch' ], + ]; } - // Simulate (mock) a renewal in the cart. - // Pass 0 / no args to unmock. - private function mock_wcs_cart_contains_renewal( $product_id = 0, $renewal_order_id = 0, $subscription_id = 0 ) { - WC_Subscriptions::wcs_cart_contains_renewal( - function () use ( $product_id, $renewal_order_id, $subscription_id ) { - if ( $product_id && $renewal_order_id ) { - return [ - 'product_id' => $product_id, - 'subscription_renewal' => [ - 'renewal_order_id' => $renewal_order_id, - 'subscription_id' => $subscription_id, - ], - ]; - } - - return false; - } - ); + public function provider_sub_types_renewal_resubscribe() { + return [ + 'renewal' => [ 'renewal' ], + 'resubscribe' => [ 'resubscribe' ], + ]; } - private function mock_wcs_get_order_type_cart_items( $switch_id = 0 ) { - WC_Subscriptions::wcs_get_order_type_cart_items( - function () use ( $switch_id ) { - if ( $switch_id ) { - return [ - [ - 'product_id' => 42, - 'key' => 'abc123', - 'subscription_switch' => [ - 'subscription_id' => $switch_id, - ], - ], - ]; - } - - return []; + /** + * Creates a mock subscription for us to be able to use in our tests. + * It also sets up the wcs_get_subscription mock method to return that sub. + */ + private function create_mock_subscription() { + // Create the mock subscription. + $mock_subscription = new WC_Subscription( 404 ); + + // Mock wcs_get_subscription to return our mock subscription. + WC_Subscriptions::set_wcs_get_subscription( + function ( $id ) use ( $mock_subscription ) { + return $mock_subscription; } ); + + return $mock_subscription; } - private function mock_wcs_cart_contains_resubscribe( $subscription_id = 0 ) { - WC_Subscriptions::wcs_cart_contains_resubscribe( - function () use ( $subscription_id ) { - if ( $subscription_id ) { - return [ - 'product_id' => 42, - 'subscription_resubscribe' => [ - 'subscription_id' => $subscription_id, - ], - ]; - } + /** + * Creates a mock subsciption, and then adds it to the session's cart array. + */ + private function get_mock_subscription_and_session_cart_items( $sub_type = 'renewal' ) { + // Create the mock subscription. + $mock_subscription = $this->create_mock_subscription(); + + // Create our cart items. + $cart_items = [ + [ + 'subscription_' . $sub_type => [ + 'subscription_id' => $mock_subscription->get_id(), + ], + 'product_id' => $this->mock_product->get_id(), + 'key' => 'abc123', + ], + ]; - return false; - } - ); - } + // Set the cart items in the session. + WC()->session->set( 'cart', $cart_items ); - private function mock_wcs_order_contains_renewal( $renewal = false ) { - WC_Subscriptions::wcs_order_contains_renewal( - function () use ( $renewal ) { - return $renewal; - } - ); + return [ + $mock_subscription, + $cart_items, + ]; } } From ef54b3bf44c1a21132d07fba4e6a64bc78a3bc75 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:56:54 +1000 Subject: [PATCH 29/84] Remove unused import `NoticeOutlineIcon` to fix JS linter warning (#7138) --- changelog/fix-remove-unused-import-noticeoutlineicon | 5 +++++ client/components/deposits-overview/next-deposit.tsx | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-remove-unused-import-noticeoutlineicon diff --git a/changelog/fix-remove-unused-import-noticeoutlineicon b/changelog/fix-remove-unused-import-noticeoutlineicon new file mode 100644 index 00000000000..07e246aa8fc --- /dev/null +++ b/changelog/fix-remove-unused-import-noticeoutlineicon @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: **N/A** this is a fix for a code linting issue + + diff --git a/client/components/deposits-overview/next-deposit.tsx b/client/components/deposits-overview/next-deposit.tsx index 39a84131231..fe55ae2ca13 100644 --- a/client/components/deposits-overview/next-deposit.tsx +++ b/client/components/deposits-overview/next-deposit.tsx @@ -10,7 +10,6 @@ import { Icon, } from '@wordpress/components'; import { calendar } from '@wordpress/icons'; -import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; import interpolateComponents from '@automattic/interpolate-components'; import { __, sprintf } from '@wordpress/i18n'; From 12ac12d4cfacabc8edc9a45ba362c670fa3a9397 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:45:19 +1000 Subject: [PATCH 30/84] Add Transaction Details notice when a dispute has staged evidence (#7139) --- ...tails-dispute-challenge-in-progress-notice | 5 + .../payment-details/dispute-details/index.tsx | 30 ++- .../dispute-details/style.scss | 8 + .../dispute-details/test/index.test.tsx | 171 ++++++++++++++++++ client/types/disputes.d.ts | 15 +- 5 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice create mode 100644 client/payment-details/dispute-details/test/index.test.tsx diff --git a/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice b/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice new file mode 100644 index 00000000000..3b03ed5b61e --- /dev/null +++ b/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Behind feature flag: add staged dispute notice to Transaction Details screen + + diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx index 6872a10ed11..cbcad24ba41 100644 --- a/client/payment-details/dispute-details/index.tsx +++ b/client/payment-details/dispute-details/index.tsx @@ -5,14 +5,18 @@ */ import React from 'react'; import moment from 'moment'; +import { __ } from '@wordpress/i18n'; +import { Card, CardBody } from '@wordpress/components'; +import { edit } from '@wordpress/icons'; + /** * Internal dependencies */ import type { Dispute } from 'wcpay/types/disputes'; -import { Card, CardBody } from '@wordpress/components'; -import './style.scss'; import DisputeNotice from './dispute-notice'; import { isAwaitingResponse } from 'wcpay/disputes/utils'; +import InlineNotice from 'components/inline-notice'; +import './style.scss'; interface DisputeDetailsProps { dispute: Dispute; @@ -22,6 +26,7 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { const now = moment(); const dueBy = moment.unix( dispute.evidence_details?.due_by ?? 0 ); const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) ); + const hasStagedEvidence = dispute.evidence_details?.has_evidence; return (
@@ -29,10 +34,23 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { { isAwaitingResponse( dispute.status ) && countdownDays >= 0 && ( - + <> + + { hasStagedEvidence && ( + + { __( + `You initiated a dispute a challenge to this dispute. Click 'Continue with challenge' to proceed with your drafted response.`, + 'woocommerce-payments' + ) } + + ) } + ) } diff --git a/client/payment-details/dispute-details/style.scss b/client/payment-details/dispute-details/style.scss index c9676e19bc2..fcd55093b5d 100644 --- a/client/payment-details/dispute-details/style.scss +++ b/client/payment-details/dispute-details/style.scss @@ -7,5 +7,13 @@ .transaction-details-dispute-details-body { padding: $grid-unit-20; + + .wcpay-inline-notice.components-notice { + margin: 0 0 10px 0; + + &:last-child { + margin-bottom: 24px; + } + } } } diff --git a/client/payment-details/dispute-details/test/index.test.tsx b/client/payment-details/dispute-details/test/index.test.tsx new file mode 100644 index 00000000000..6c5b1874ee0 --- /dev/null +++ b/client/payment-details/dispute-details/test/index.test.tsx @@ -0,0 +1,171 @@ +/** @format */ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +/** + * Internal dependencies + */ +import type { Dispute } from 'wcpay/types/disputes'; +import type { Charge } from 'wcpay/types/charges'; +import DisputeDetails from '..'; + +declare const global: { + wcSettings: { + locale: { + siteLocale: string; + }; + }; + wcpaySettings: { + isSubscriptionsActive: boolean; + zeroDecimalCurrencies: string[]; + currencyData: Record< string, any >; + connect: { + country: string; + }; + featureFlags: { + isAuthAndCaptureEnabled: boolean; + }; + }; +}; + +global.wcpaySettings = { + isSubscriptionsActive: false, + zeroDecimalCurrencies: [], + connect: { + country: 'US', + }, + featureFlags: { + isAuthAndCaptureEnabled: true, + }, + currencyData: { + US: { + code: 'USD', + symbol: '$', + symbolPosition: 'left', + thousandSeparator: ',', + decimalSeparator: '.', + precision: 2, + }, + }, +}; + +interface ChargeWithDisputeRequired extends Charge { + dispute: Dispute; +} + +const getBaseCharge = (): ChargeWithDisputeRequired => + ( { + id: 'ch_38jdHA39KKA', + /* Stripe data comes in seconds, instead of the default Date milliseconds */ + created: Date.parse( 'Sep 19, 2019, 5:24 pm' ) / 1000, + amount: 2000, + amount_refunded: 0, + application_fee_amount: 70, + disputed: true, + dispute: { + id: 'dp_1', + amount: 6800, + charge: 'ch_38jdHA39KKA', + order: null, + balance_transactions: [ + { + amount: -2000, + currency: 'usd', + fee: 1500, + }, + ], + created: 1693453017, + currency: 'usd', + evidence: { + billing_address: '123 test address', + customer_email_address: 'test@email.com', + customer_name: 'Test customer', + shipping_address: '123 test address', + }, + evidence_details: { + due_by: 1694303999, + has_evidence: false, + past_due: false, + submission_count: 0, + }, + // issuer_evidence: null, + metadata: [], + payment_intent: 'pi_1', + reason: 'fraudulent', + status: 'needs_response', + } as Dispute, + currency: 'usd', + type: 'charge', + status: 'succeeded', + paid: true, + captured: true, + balance_transaction: { + amount: 2000, + currency: 'usd', + fee: 70, + }, + refunds: { + data: [], + }, + order: { + number: 45981, + url: 'https://somerandomorderurl.com/?edit_order=45981', + }, + billing_details: { + name: 'Customer name', + }, + payment_method_details: { + card: { + brand: 'visa', + last4: '4242', + }, + type: 'card', + }, + outcome: { + risk_level: 'normal', + }, + } as any ); + +describe( 'DisputeDetails', () => { + test( 'correctly renders dispute details', () => { + const charge = getBaseCharge(); + render( ); + + screen.getByText( + /The cardholder claims this is an unauthorized transaction/, + { ignore: '.a11y-speak-region' } + ); + // Don't render the staged evidence message + expect( + screen.queryByText( + /You initiated a dispute a challenge to this dispute/, + { ignore: '.a11y-speak-region' } + ) + ).toBeNull(); + } ); + + test( 'correctly renders dispute details for a dispute with staged evidence', () => { + const charge = getBaseCharge(); + charge.dispute.evidence_details = { + has_evidence: true, + due_by: 1694303999, + past_due: false, + submission_count: 0, + }; + + render( ); + + screen.getByText( + /The cardholder claims this is an unauthorized transaction/, + { ignore: '.a11y-speak-region' } + ); + // Render the staged evidence message + screen.getByText( + /You initiated a dispute a challenge to this dispute/, + { ignore: '.a11y-speak-region' } + ); + } ); +} ); diff --git a/client/types/disputes.d.ts b/client/types/disputes.d.ts index 2a5c5e45a76..43eac68b1d6 100644 --- a/client/types/disputes.d.ts +++ b/client/types/disputes.d.ts @@ -11,8 +11,21 @@ interface Evidence { } interface EvidenceDetails { + /** + * Whether evidence has been staged for this dispute. + */ has_evidence: boolean; + /** + * Date by which evidence must be submitted in order to successfully challenge dispute. + */ due_by: number; + /** + * Whether the last evidence submission was submitted past the due date. Defaults to false if no evidence submissions have occurred. If true, then delivery of the latest evidence is not guaranteed. + */ + past_due: boolean; + /** + * The number of times evidence has been submitted. Typically, the merchant may only submit evidence once. + */ submission_count: number; } @@ -51,7 +64,7 @@ export interface Dispute { evidence: Evidence; fileSize?: Record< string, number >; reason: DisputeReason; - charge: Charge; + charge: Charge | string; amount: number; currency: string; created: number; From 18192c3c02c760d70aa89371de1f63bd44e206d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Thu, 7 Sep 2023 14:49:21 +0200 Subject: [PATCH 31/84] Implement `BannerNotice` component (#7097) Co-authored-by: Vlad Olaru --- changelog/add-7048-banner-notice-component | 5 + client/components/banner-notice/index.tsx | 197 ++++++++++++++++++ client/components/banner-notice/style.scss | 65 ++++++ .../tests/__snapshots__/index.test.tsx.snap | 62 ++++++ .../banner-notice/tests/index.test.tsx | 112 ++++++++++ client/onboarding/restored-state-banner.tsx | 11 +- client/stylesheets/abstracts/_colors.scss | 9 + client/stylesheets/abstracts/_variables.scss | 3 - 8 files changed, 455 insertions(+), 9 deletions(-) create mode 100644 changelog/add-7048-banner-notice-component create mode 100644 client/components/banner-notice/index.tsx create mode 100644 client/components/banner-notice/style.scss create mode 100644 client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap create mode 100644 client/components/banner-notice/tests/index.test.tsx diff --git a/changelog/add-7048-banner-notice-component b/changelog/add-7048-banner-notice-component new file mode 100644 index 00000000000..f9ccb0504d8 --- /dev/null +++ b/changelog/add-7048-banner-notice-component @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Add BannerNotice component, with no major UI difference for merchants. + + diff --git a/client/components/banner-notice/index.tsx b/client/components/banner-notice/index.tsx new file mode 100644 index 00000000000..dcba0e665fb --- /dev/null +++ b/client/components/banner-notice/index.tsx @@ -0,0 +1,197 @@ +/** + * Based on the @wordpress/components `Notice` component. + * Adjusted to meet WooCommerce Admin Design Library. + */ + +/** + * External dependencies + */ +import React from 'react'; + +import { __ } from '@wordpress/i18n'; +import { useEffect, renderToString } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; +import classNames from 'classnames'; +import { Icon, Button } from '@wordpress/components'; +import { check, info } from '@wordpress/icons'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import CloseIcon from 'gridicons/dist/cross-small'; + +/** + * Internal dependencies. + */ +import './style.scss'; + +const statusIconMap = { + success: check, + error: NoticeOutlineIcon, + warning: NoticeOutlineIcon, + info: info, +}; + +type Status = keyof typeof statusIconMap; + +/** + * Custom hook which announces the message with politeness based on status, + * if a valid message is provided. + */ +const useSpokenMessage = ( status?: string, message?: React.ReactNode ) => { + const spokenMessage = + typeof message === 'string' ? message : renderToString( message ); + const politeness = status === 'error' ? 'assertive' : 'polite'; + + useEffect( () => { + if ( spokenMessage ) { + speak( spokenMessage, politeness ); + } + }, [ spokenMessage, politeness ] ); +}; + +interface Props { + /** + * A CSS `class` to give to the wrapper element. + */ + className?: string; + /** + * The displayed message of a notice. Also used as the spoken message for + * assistive technology, unless `spokenMessage` is provided as an alternative message. + */ + children: React.ReactNode; + /** + * Determines the color of the notice: `warning` (yellow), + * `success` (green), `error` (red), or `'info'`. + * By default `'info'` will be blue, but if there is a parent Theme component + * with an accent color prop, the notice will take on that color instead. + * + * @default 'info' + */ + status?: Status; + /** + * Whether to display the default icon based on status or the icon to display. + * Supported values are: boolean, JSX.Element and `undefined`. + * + * @default undefined + */ + icon?: boolean | JSX.Element; + /** + * Whether the notice should be dismissible or not. + * + * @default true + */ + isDismissible?: boolean; + /** + * An array of action objects. Each member object should contain: + * + * - `label`: `string` containing the text of the button/link + * - `url`: `string` OR `onClick`: `( event: SyntheticEvent ) => void` to specify + * what the action does. + * - `className`: `string` (optional) to add custom classes to the button styles. + * - `variant`: `'primary' | 'secondary' | 'link'` (optional) You can denote a + * primary button action for a notice by passing a value of `primary`. + * + * The default appearance of an action button is inferred based on whether + * `url` or `onClick` are provided, rendering the button as a link if + * appropriate. If both props are provided, `url` takes precedence, and the + * action button will render as an anchor tag. + * + * @default [] + */ + actions?: ReadonlyArray< { + label: string; + className?: string; + variant?: Button.Props[ 'variant' ]; + url?: string; + onClick?: React.MouseEventHandler< HTMLAnchorElement >; + } >; + /** + * Function called when dismissing the notice + * + * @default undefined + */ + onRemove?: () => void; +} + +const BannerNotice: React.FC< Props > = ( { + icon, + children, + actions = [], + className, + status = 'info', + isDismissible = true, + onRemove, +} ) => { + useSpokenMessage( status, children ); + + const iconToDisplay = icon === true ? statusIconMap[ status ] : icon; + + const classes = classNames( + className, + 'wcpay-banner-notice', + 'is-' + status + ); + + const handleRemove = () => onRemove?.(); + + return ( +
+ { iconToDisplay && ( + + ) } +
+ { children } + { actions.length > 0 && ( +
+ { actions.map( + ( + { + className: buttonCustomClasses, + label, + variant, + onClick, + url, + }, + index + ) => { + let computedVariant = variant; + if ( variant !== 'primary' ) { + computedVariant = ! url + ? 'secondary' + : 'link'; + } + + return ( + + ); + } + ) } +
+ ) } +
+ { isDismissible && ( +
+ ); +}; + +export default BannerNotice; diff --git a/client/components/banner-notice/style.scss b/client/components/banner-notice/style.scss new file mode 100644 index 00000000000..4ab24170454 --- /dev/null +++ b/client/components/banner-notice/style.scss @@ -0,0 +1,65 @@ +.wcpay-banner-notice { + display: flex; + font-family: $default-font; + font-size: $default-font-size; + background-color: $white; + border-left: $gap-smallest solid $components-color-accent; + fill: $components-color-accent; + margin: $gap-large 0; + padding: $gap-small; + box-shadow: 0 2px 6px 0 rgba( 0, 0, 0, 0.05 ); + + &.is-success { + border-left-color: $alert-green; + fill: $alert-green; + } + + &.is-warning { + border-left-color: $alert-yellow; + fill: $alert-yellow; + } + + &.is-error { + border-left-color: $alert-red; + fill: $alert-red; + } + + &.is-warning &__icon, + &.is-error &__icon { + height: 21px; // Adjust gridicon height to match other icons + } + + &__icon { + flex-shrink: 0; + margin-right: $gap-small; + } + + &__content { + flex-grow: 1; + } + + &__actions { + display: grid; + grid-auto-flow: column; + grid-auto-columns: min-content; + column-gap: $gap-small; + margin-top: $gap-small; + } + + &__dismiss.components-button.has-icon { + flex-shrink: 0; + padding: 0; + min-width: 24px; + height: 24px; + + svg { + width: 20px; + } + } + + /* Margin exceptions */ + & + &, + &:first-child { + margin-top: 0; + } +} diff --git a/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap b/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..32e6eb3fdf7 --- /dev/null +++ b/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BannerNotice should match snapshot 1`] = ` +
+
+ + Custom Icon + +
+ Example +
+ + More information + + + +
+
+ +
+
+`; diff --git a/client/components/banner-notice/tests/index.test.tsx b/client/components/banner-notice/tests/index.test.tsx new file mode 100644 index 00000000000..819a9bf935e --- /dev/null +++ b/client/components/banner-notice/tests/index.test.tsx @@ -0,0 +1,112 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; +import { mocked } from 'ts-jest/utils'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import BannerNotice from '../'; + +jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) ); + +describe( 'BannerNotice', () => { + beforeEach( () => { + mocked( speak ).mockClear(); + } ); + + it( 'should match snapshot', () => { + const onClick = jest.fn(); + const { container } = render( + Custom Icon } + actions={ [ + { label: 'More information', url: 'https://example.com' }, + { label: 'Cancel', onClick }, + { label: 'Submit', onClick, variant: 'primary' }, + ] } + > + Example + + ); + + expect( container ).toMatchSnapshot(); + } ); + + it( 'should default to info status', () => { + const { + container: { firstChild }, + } = render( FYI ); + + expect( firstChild ).toHaveClass( 'is-info' ); + } ); + + /***************** */ + + it( 'calls action onClick when clicked', () => { + const onClick = jest.fn(); + render( + + Notice with Action + + ); + + user.click( screen.getByText( 'Action' ) ); + + expect( onClick ).toHaveBeenCalled(); + } ); + + it( 'calls onRemove when dismiss button is clicked', () => { + const onRemove = jest.fn(); + render( + + Dismissible Notice + + ); + + user.click( screen.getByLabelText( 'Dismiss this notice' ) ); + + expect( onRemove ).toHaveBeenCalled(); + } ); + + describe( 'useSpokenMessage', () => { + it( 'should speak the given message', () => { + render( FYI ); + + expect( speak ).toHaveBeenCalledWith( 'FYI', 'polite' ); + } ); + + it( 'should speak the given message by implicit politeness by status', () => { + render( Uh oh! ); + + expect( speak ).toHaveBeenCalledWith( 'Uh oh!', 'assertive' ); + } ); + + it( 'should coerce a message to a string', () => { + render( + + With emphasis this time. + + ); + + expect( speak ).toHaveBeenCalledWith( + 'With emphasis this time.', + 'polite' + ); + } ); + + it( 'should not re-speak an effectively equivalent element message', () => { + const { rerender } = render( + Duplicated notice message. + ); + rerender( Duplicated notice message. ); + + expect( speak ).toHaveBeenCalledTimes( 1 ); + } ); + } ); +} ); diff --git a/client/onboarding/restored-state-banner.tsx b/client/onboarding/restored-state-banner.tsx index 214c73763bc..8ffef4fc431 100644 --- a/client/onboarding/restored-state-banner.tsx +++ b/client/onboarding/restored-state-banner.tsx @@ -7,21 +7,20 @@ import React from 'react'; * Internal dependencies */ import strings from './strings'; -import InlineNotice from 'components/inline-notice'; +import BannerNotice from 'components/banner-notice'; const RestoredStateBanner: React.FC = () => { const [ hidden, setHidden ] = React.useState( false ); if ( hidden || ! wcpaySettings.onboardingFlowState ) return null; return ( - setHidden( true ) } > { strings.restoredState } - + ); }; diff --git a/client/stylesheets/abstracts/_colors.scss b/client/stylesheets/abstracts/_colors.scss index d8535761423..fe2338dc439 100644 --- a/client/stylesheets/abstracts/_colors.scss +++ b/client/stylesheets/abstracts/_colors.scss @@ -66,3 +66,12 @@ $wp-green-70: #005c12; $wp-green-80: #00450c; $wp-green-90: #003008; $wp-green-100: #001c05; + +// Missing from dependencies +$gutenberg-blue: #007cba; + +// Accent color +$components-color-accent: var( + --wp-components-color-accent, + var( --wp-admin-theme-color, $gutenberg-blue ) +); diff --git a/client/stylesheets/abstracts/_variables.scss b/client/stylesheets/abstracts/_variables.scss index 8a48d1224ab..4766b1844fd 100644 --- a/client/stylesheets/abstracts/_variables.scss +++ b/client/stylesheets/abstracts/_variables.scss @@ -9,6 +9,3 @@ $gap-smallest: 4px; // Modals $modal-max-width: 500px; - -// Colors (missing from dependencies) -$gutenberg-blue: #007cba; From bfb391c9557cfcaaf15016787f2e6f9ddf51496f Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Thu, 7 Sep 2023 16:38:45 +0300 Subject: [PATCH 32/84] Add cli npm command (#7111) --- bin/cli.sh | 14 ++++++++++++++ changelog/dev-add-cli-command | 5 +++++ package.json | 3 ++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100755 bin/cli.sh create mode 100644 changelog/dev-add-cli-command diff --git a/bin/cli.sh b/bin/cli.sh new file mode 100755 index 00000000000..86d167477da --- /dev/null +++ b/bin/cli.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +first_arg=${1} +if [ "${first_arg}" = "--as-root" ]; then + user=0 + command=${@:2} +else + user=www-data + command=${@:1} +fi + +command=${command:-bash} + +docker-compose exec -u ${user} wordpress ${command} diff --git a/changelog/dev-add-cli-command b/changelog/dev-add-cli-command new file mode 100644 index 00000000000..4451c5b4326 --- /dev/null +++ b/changelog/dev-add-cli-command @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: It is only dev-facing. + + diff --git a/package.json b/package.json index 3e56b6f367b..246f93e125e 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "tube:stop": "./docker/bin/jt/tunnel.sh break", "psalm": "./bin/run-psalm.sh", "xdebug:toggle": "docker-compose exec -u root wordpress /var/www/html/wp-content/plugins/woocommerce-payments/bin/xdebug-toggle.sh", - "changelog": "./vendor/bin/changelogger add" + "changelog": "./vendor/bin/changelogger add", + "cli": "./bin/cli.sh" }, "dependencies": { "@automattic/interpolate-components": "1.2.1", From a4bc69a2f875ffa4df3e80cab3d3c3fe8b0b1e6a Mon Sep 17 00:00:00 2001 From: Taha Paksu <3295+tpaksu@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:24:09 +0300 Subject: [PATCH 33/84] Fix multicurrency widget error on post/page edit screen (#7141) --- changelog/fix-7135-currency-switch-widget-on-edit-post | 4 ++++ client/data/multi-currency/hooks.js | 7 ------- 2 files changed, 4 insertions(+), 7 deletions(-) create mode 100644 changelog/fix-7135-currency-switch-widget-on-edit-post diff --git a/changelog/fix-7135-currency-switch-widget-on-edit-post b/changelog/fix-7135-currency-switch-widget-on-edit-post new file mode 100644 index 00000000000..357711e2484 --- /dev/null +++ b/changelog/fix-7135-currency-switch-widget-on-edit-post @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Multicurrency widget error on post/page edit screen diff --git a/client/data/multi-currency/hooks.js b/client/data/multi-currency/hooks.js index 8f7c1e12ed0..aa39e24ee6f 100644 --- a/client/data/multi-currency/hooks.js +++ b/client/data/multi-currency/hooks.js @@ -10,13 +10,6 @@ export const useCurrencies = () => useSelect( ( select ) => { const { getCurrencies, isResolving } = select( STORE_NAME ); - if ( wcpaySettings.isMultiCurrencyEnabled !== '1' ) { - return { - currencies: {}, - isLoading: false, - }; - } - return { currencies: getCurrencies(), isLoading: isResolving( 'getCurrencies', [] ), From 9ea339b751e5c9d90e8d90a10510b4d0ce464c07 Mon Sep 17 00:00:00 2001 From: Brian Borman <68524302+bborman22@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:54:48 -0400 Subject: [PATCH 34/84] Session compatible fix for WooPay multi-currency (#7055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: KristĂ³fer ReykjalĂ­n <13835680+reykjalin@users.noreply.github.com> --- ...ix-invalid-currency-from-store-api-request | 4 ++ includes/woopay/class-woopay-session.php | 45 ++++++++++--------- .../unit/woopay/test-class-woopay-session.php | 5 +++ woocommerce-payments.php | 11 +++-- 4 files changed, 39 insertions(+), 26 deletions(-) create mode 100644 changelog/fix-invalid-currency-from-store-api-request diff --git a/changelog/fix-invalid-currency-from-store-api-request b/changelog/fix-invalid-currency-from-store-api-request new file mode 100644 index 00000000000..38f9ee6bc4b --- /dev/null +++ b/changelog/fix-invalid-currency-from-store-api-request @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix WooPay Session Handler in Store API requests. diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 2caf2be5d9e..fbeb4879096 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -21,6 +21,7 @@ use WC_Payments; use WC_Payments_Customer_Service; use WC_Payments_Features; +use WCPay\MultiCurrency\MultiCurrency; use WP_REST_Request; /** @@ -53,7 +54,7 @@ class WooPay_Session { */ public static function init() { add_filter( 'determine_current_user', [ __CLASS__, 'determine_current_user_for_woopay' ], 20 ); - add_filter( 'rest_request_before_callbacks', [ __CLASS__, 'add_woopay_store_api_session_handler' ], 10, 3 ); + add_filter( 'woocommerce_session_handler', [ __CLASS__, 'add_woopay_store_api_session_handler' ], 20 ); add_action( 'woocommerce_order_payment_status_changed', [ __CLASS__, 'remove_order_customer_id_on_requests_with_verified_email' ] ); add_action( 'woopay_restore_order_customer_id', [ __CLASS__, 'restore_order_customer_id_from_requests_with_verified_email' ] ); @@ -64,31 +65,24 @@ public static function init() { * This filter is used to add a custom session handler before processing Store API request callbacks. * This is only necessary because the Store API SessionHandler currently doesn't provide an `init_session_cookie` method. * - * @param mixed $response The response object. - * @param mixed $handler The handler used for the response. - * @param WP_REST_Request $request The request used to generate the response. + * @param string $default_session_handler The default session handler class name. * - * @return mixed + * @return string The session handler class name. */ - public static function add_woopay_store_api_session_handler( $response, $handler, WP_REST_Request $request ) { - $cart_token = $request->get_header( 'Cart-Token' ); + public static function add_woopay_store_api_session_handler( $default_session_handler ) { + $cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ?? null ) ); if ( $cart_token && + self::is_request_from_woopay() && self::is_store_api_request() && class_exists( JsonWebToken::class ) && JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) { - add_filter( - 'woocommerce_session_handler', - function ( $session_handler ) { - return SessionHandler::class; - }, - 20 - ); + return SessionHandler::class; } - return $response; + return $default_session_handler; } /** @@ -338,6 +332,17 @@ private static function get_init_session_request() { $customer_id = WC_Payments::get_customer_service()->create_customer_for_user( $user, $customer_data ); } + if ( 0 !== $user->ID ) { + // Multicurrency selection is stored on user meta when logged in and WC session when logged out. + // This code just makes sure that currency selection is available on WC session for WooPay. + $currency = get_user_meta( $user->ID, MultiCurrency::CURRENCY_META_KEY, true ); + $currency_code = strtoupper( $currency ); + + if ( ! empty( $currency_code ) && WC()->session ) { + WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, $currency_code ); + } + } + $account_id = WC_Payments::get_account_service()->get_stripe_account_id(); $site_logo_id = get_theme_mod( 'custom_logo' ); @@ -500,10 +505,6 @@ private static function get_woopay_verified_email_address() { * @return bool True if request is a Store API request, false otherwise. */ private static function is_store_api_request(): bool { - if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) { - return false; - } - $url_parts = wp_parse_url( esc_url_raw( $_SERVER['REQUEST_URI'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $request_path = rtrim( $url_parts['path'], '/' ); $rest_route = str_replace( trailingslashit( rest_get_url_prefix() ), '', $request_path ); @@ -542,6 +543,11 @@ private static function has_valid_request_signature() { * @return bool True if WooPay is enabled, false otherwise. */ private static function is_woopay_enabled(): bool { + // There were previously instances of this function being called too early. While those should be resolved, adding this defensive check as well. + if ( ! class_exists( WC_Payments_Features::class ) || ! class_exists( WC_Payments::class ) || is_null( WC_Payments::get_gateway() ) ) { + return false; + } + return WC_Payments_Features::is_woopay_eligible() && 'yes' === WC_Payments::get_gateway()->get_option( 'platform_checkout', 'no' ); } @@ -609,5 +615,4 @@ private static function get_formatted_custom_message() { return str_replace( array_keys( $replacement_map ), array_values( $replacement_map ), $custom_message ); } - } diff --git a/tests/unit/woopay/test-class-woopay-session.php b/tests/unit/woopay/test-class-woopay-session.php index d11f5ad2499..647637bb1bc 100644 --- a/tests/unit/woopay/test-class-woopay-session.php +++ b/tests/unit/woopay/test-class-woopay-session.php @@ -14,6 +14,11 @@ * WooPay_Session unit tests. */ class WooPay_Session_Test extends WCPAY_UnitTestCase { + /** + * @var Database_Cache|MockObject + */ + protected $mock_cache; + public function set_up() { parent::set_up(); diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 47198f4131a..8596eb15c92 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -134,12 +134,6 @@ function () { // Jetpack's Rest_Authentication needs to be initialized even before plugins_loaded. Automattic\Jetpack\Connection\Rest_Authentication::init(); -/** - * Needs to be loaded as soon as possible - * Check https://github.com/Automattic/woocommerce-payments/issues/4759 - */ -\WCPay\WooPay\WooPay_Session::init(); - // Jetpack-config will initialize the modules on "plugins_loaded" with priority 2, so this code needs to be run before that. add_action( 'plugins_loaded', 'wcpay_jetpack_init', 1 ); @@ -150,6 +144,11 @@ function () { function wcpay_init() { require_once WCPAY_ABSPATH . '/includes/class-wc-payments.php'; WC_Payments::init(); + /** + * Needs to be loaded as soon as possible + * Check https://github.com/Automattic/woocommerce-payments/issues/4759 + */ + \WCPay\WooPay\WooPay_Session::init(); } // Make sure this is run *after* WooCommerce has a chance to initialize its packages (wc-admin, etc). That is run with priority 10. From 69a25f7a57c13ca517c29d600d92c393791cd011 Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Fri, 8 Sep 2023 16:11:48 +0200 Subject: [PATCH 35/84] Fix incorrect payment intent status check in cancel authorization API endpoint. (#7150) --- ...-cancel-authorization-flaky-error-response | 4 + ...ass-wc-rest-payments-orders-controller.php | 2 +- ...ass-wc-rest-payments-orders-controller.php | 201 ++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-7149-fix-cancel-authorization-flaky-error-response diff --git a/changelog/fix-7149-fix-cancel-authorization-flaky-error-response b/changelog/fix-7149-fix-cancel-authorization-flaky-error-response new file mode 100644 index 00000000000..27cbbbbeea7 --- /dev/null +++ b/changelog/fix-7149-fix-cancel-authorization-flaky-error-response @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Corrected an issue causing incorrect responses at the cancel authorization API endpoint. diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index 782c5358762..b5915678ca7 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -499,7 +499,7 @@ public function cancel_authorization( WP_REST_Request $request ) { $result = $this->gateway->cancel_authorization( $order ); - if ( Intent_Status::SUCCEEDED !== $result['status'] ) { + if ( Intent_Status::CANCELED !== $result['status'] ) { return new WP_Error( 'wcpay_cancel_error', sprintf( diff --git a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php index 680bc81a809..e5f50a01baa 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php @@ -1357,6 +1357,207 @@ public function test_create_terminal_intent_invalid_capture_method() { $this->assertSame( 500, $data['status'] ); } + public function test_cancel_authorization_success() { + $order = $this->create_mock_order(); + $request = new WP_REST_Request( 'POST' ); + $request->set_url_params( + [ + 'order_id' => $order->get_id(), + ] + ); + $request->set_body_params( + [ + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $this->mock_gateway + ->expects( $this->once() ) + ->method( 'cancel_authorization' ) + ->with( $this->isInstanceOf( WC_Order::class ) ) + ->willReturn( + [ + 'status' => Intent_Status::CANCELED, + 'id' => $this->mock_intent_id, + ] + ); + + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'id' => $this->mock_intent_id, + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'metadata' => [ + 'order_id' => $order->get_id(), + ], + ] + ); + $wcpay_request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id ); + + $wcpay_request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $mock_intent ); + $response = $this->controller->cancel_authorization( $request ); + + $response_data = $response->get_data(); + + $this->assertEquals( 200, $response->status ); + $this->assertEquals( + [ + 'status' => Intent_Status::CANCELED, + 'id' => $this->mock_intent_id, + ], + $response_data + ); + } + public function test_cancel_authorization_will_fail_if_order_is_incorrect() { + $order = $this->create_mock_order(); + $request = new WP_REST_Request( 'POST' ); + $request->set_url_params( + [ + 'order_id' => $order->get_id() + 1, + ] + ); + $request->set_body_params( + [ + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $this->mock_gateway + ->expects( $this->never() ) + ->method( 'cancel_authorization' ); + + $this->mock_wcpay_request( Get_Intention::class, 0 ); + + $response = $this->controller->cancel_authorization( $request ); + + $this->assertInstanceOf( 'WP_Error', $response ); + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertSame( 404, $data['status'] ); + } + public function test_cancel_authorization_will_fail_if_order_is_refunded() { + $order = $this->create_mock_order(); + wc_create_refund( + [ + 'order_id' => $order->get_id(), + 'amount' => 10.0, + 'line_items' => [], + ] + ); + $request = new WP_REST_Request( 'POST' ); + $request->set_url_params( + [ + 'order_id' => $order->get_id(), + ] + ); + $request->set_body_params( + [ + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $this->mock_gateway + ->expects( $this->never() ) + ->method( 'cancel_authorization' ); + + $this->mock_wcpay_request( Get_Intention::class, 0 ); + + $response = $this->controller->cancel_authorization( $request ); + + $this->assertInstanceOf( 'WP_Error', $response ); + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertSame( 400, $data['status'] ); + } + public function test_cancel_authorization_will_fail_if_order_does_not_match_with_payment_intent() { + $order = $this->create_mock_order(); + $request = new WP_REST_Request( 'POST' ); + $request->set_url_params( + [ + 'order_id' => $order->get_id(), + ] + ); + $request->set_body_params( + [ + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'id' => $this->mock_intent_id, + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'metadata' => [ + 'order_id' => $order->get_id() + 1, + ], + ] + ); + $wcpay_request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id ); + + $wcpay_request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $mock_intent ); + + $this->mock_gateway + ->expects( $this->never() ) + ->method( 'cancel_authorization' ); + + $response = $this->controller->cancel_authorization( $request ); + + $this->assertInstanceOf( 'WP_Error', $response ); + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertSame( 409, $data['status'] ); + } + + public function test_cancel_authorization_will_fail_if_gateway_fails_to_cancel_authorization() { + $order = $this->create_mock_order(); + $request = new WP_REST_Request( 'POST' ); + $request->set_url_params( + [ + 'order_id' => $order->get_id(), + ] + ); + $request->set_body_params( + [ + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'id' => $this->mock_intent_id, + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'metadata' => [ + 'order_id' => $order->get_id(), + ], + ] + ); + $wcpay_request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id ); + + $wcpay_request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $mock_intent ); + + $this->mock_gateway + ->method( 'cancel_authorization' ) + ->with( $this->isInstanceOf( WC_Order::class ) ) + ->willReturn( + [ + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'id' => $this->mock_intent_id, + ] + ); + + $response = $this->controller->cancel_authorization( $request ); + + $this->assertInstanceOf( 'WP_Error', $response ); + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertSame( 502, $data['status'] ); + } + private function create_mock_order() { $charge = $this->create_charge_object(); From 122d739f408d429c97f1f685f693bf4079775378 Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Mon, 11 Sep 2023 15:14:58 +1200 Subject: [PATCH 36/84] Add Horizontal list of dispute details to the transaction page (#7077) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> --- changelog/add-6924-dispute-details-attributes | 5 + changelog/dev-add-dispute-summary-row-tests | 5 + .../dispute-details/dispute-summary-row.tsx | 131 ++++++++++++++++++ .../payment-details/dispute-details/index.tsx | 7 +- .../dispute-details/style.scss | 31 +++++ .../dispute-details/test/index.test.tsx | 32 +++++ 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 changelog/add-6924-dispute-details-attributes create mode 100644 changelog/dev-add-dispute-summary-row-tests create mode 100644 client/payment-details/dispute-details/dispute-summary-row.tsx diff --git a/changelog/add-6924-dispute-details-attributes b/changelog/add-6924-dispute-details-attributes new file mode 100644 index 00000000000..0e2dfaa37d9 --- /dev/null +++ b/changelog/add-6924-dispute-details-attributes @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Add dispute details to transaction page, hidden behind feature flag. + + diff --git a/changelog/dev-add-dispute-summary-row-tests b/changelog/dev-add-dispute-summary-row-tests new file mode 100644 index 00000000000..1aba1754bb0 --- /dev/null +++ b/changelog/dev-add-dispute-summary-row-tests @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Not user-facing: updates tests for DisputeDetails component only. + + diff --git a/client/payment-details/dispute-details/dispute-summary-row.tsx b/client/payment-details/dispute-details/dispute-summary-row.tsx new file mode 100644 index 00000000000..160abaeb000 --- /dev/null +++ b/client/payment-details/dispute-details/dispute-summary-row.tsx @@ -0,0 +1,131 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import moment from 'moment'; +import HelpOutlineIcon from 'gridicons/dist/help-outline'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { dateI18n } from '@wordpress/date'; +import classNames from 'classnames'; + +/** + * Internal dependencies + */ +import type { Dispute } from 'wcpay/types/disputes'; +import { HorizontalList } from 'wcpay/components/horizontal-list'; +import { formatCurrency } from 'wcpay/utils/currency'; +import { reasons } from 'wcpay/disputes/strings'; +import { formatStringValue } from 'wcpay/utils'; +import { ClickTooltip } from 'wcpay/components/tooltip'; +import Paragraphs from 'wcpay/components/paragraphs'; + +interface Props { + dispute: Dispute; + daysRemaining: number; +} + +const DisputeSummaryRow: React.FC< Props > = ( { dispute, daysRemaining } ) => { + const respondByDate = dispute.evidence_details?.due_by + ? dateI18n( + 'M j, Y, g:ia', + moment( dispute.evidence_details?.due_by * 1000 ).toISOString() + ) + : '–'; + + const disputeReason = formatStringValue( + reasons[ dispute.reason ]?.display || dispute.reason + ); + const disputeReasonSummary = reasons[ dispute.reason ]?.summary || []; + + const columns = [ + { + title: __( 'Dispute Amount', 'woocommerce-payments' ), + content: formatCurrency( dispute.amount, dispute.currency ), + }, + { + title: __( 'Disputed On', 'woocommerce-payments' ), + content: dispute.created + ? dateI18n( + 'M j, Y, g:ia', + moment( dispute.created * 1000 ).toISOString() + ) + : '–', + }, + { + title: __( 'Reason', 'woocommerce-payments' ), + content: ( + <> + { disputeReason } + { disputeReasonSummary.length > 0 && ( + } + buttonLabel={ __( + 'Learn more', + 'woocommerce-payments' + ) } + content={ +
+

{ disputeReason }

+ + { disputeReasonSummary } + +

+ + { __( + 'Learn more', + 'woocommerce-payments' + ) } + +

+
+ } + /> + ) } + + ), + }, + { + title: __( 'Respond By', 'woocommerce-payments' ), + content: ( + + { respondByDate } + 2, + } ) } + > + { daysRemaining === 0 + ? __( '(Last day today)', 'woocommerce-payments' ) + : sprintf( + // Translators: %s is the number of days left to respond to the dispute. + _n( + '(%s day left to respond)', + '(%s days left to respond)', + daysRemaining, + 'woocommerce-payments' + ), + daysRemaining + ) } + + + ), + }, + ]; + + return ( +
+ +
+ ); +}; + +export default DisputeSummaryRow; diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx index cbcad24ba41..4896b3e1e63 100644 --- a/client/payment-details/dispute-details/index.tsx +++ b/client/payment-details/dispute-details/index.tsx @@ -13,8 +13,9 @@ import { edit } from '@wordpress/icons'; * Internal dependencies */ import type { Dispute } from 'wcpay/types/disputes'; -import DisputeNotice from './dispute-notice'; import { isAwaitingResponse } from 'wcpay/disputes/utils'; +import DisputeNotice from './dispute-notice'; +import DisputeSummaryRow from './dispute-summary-row'; import InlineNotice from 'components/inline-notice'; import './style.scss'; @@ -50,6 +51,10 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { ) } ) } + ) } diff --git a/client/payment-details/dispute-details/style.scss b/client/payment-details/dispute-details/style.scss index fcd55093b5d..acaeee0fe39 100644 --- a/client/payment-details/dispute-details/style.scss +++ b/client/payment-details/dispute-details/style.scss @@ -15,5 +15,36 @@ margin-bottom: 24px; } } + + .dispute-summary-row { + margin: 24px 0; + + &__response-date { + display: flex; + align-items: center; + gap: var( --grid-unit-05, 4px ); + flex-wrap: wrap; + &--warning { + color: $wp-yellow-30; + font-weight: 700; + } + &--urgent { + font-weight: 700; + color: $alert-red; + } + } + } + } +} +.dispute-reason-tooltip { + p { + &:first-child { + font-weight: bold; + } + &:last-child { + margin-bottom: 0; + } + margin: 0; + margin-bottom: 8px; } } diff --git a/client/payment-details/dispute-details/test/index.test.tsx b/client/payment-details/dispute-details/test/index.test.tsx index 6c5b1874ee0..e2cd36c11f6 100644 --- a/client/payment-details/dispute-details/test/index.test.tsx +++ b/client/payment-details/dispute-details/test/index.test.tsx @@ -130,14 +130,31 @@ const getBaseCharge = (): ChargeWithDisputeRequired => } as any ); describe( 'DisputeDetails', () => { + beforeEach( () => { + // mock Date.now that moment library uses to get current date for testing purposes + Date.now = jest.fn( () => + new Date( '2023-09-08T12:33:37.000Z' ).getTime() + ); + } ); + afterEach( () => { + // roll it back + Date.now = () => new Date().getTime(); + } ); test( 'correctly renders dispute details', () => { const charge = getBaseCharge(); render( ); + // Expect this warning to be logged to the console + expect( console ).toHaveWarnedWith( + 'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.' + ); + + // Dispute Notice screen.getByText( /The cardholder claims this is an unauthorized transaction/, { ignore: '.a11y-speak-region' } ); + // Don't render the staged evidence message expect( screen.queryByText( @@ -145,6 +162,20 @@ describe( 'DisputeDetails', () => { { ignore: '.a11y-speak-region' } ) ).toBeNull(); + + // Dispute Summary Row + expect( + screen.getByText( /Dispute Amount/i ).nextSibling + ).toHaveTextContent( /\$68.00/ ); + expect( + screen.getByText( /Disputed On/i ).nextSibling + ).toHaveTextContent( /Aug 30, 2023/ ); + expect( screen.getByText( /Reason/i ).nextSibling ).toHaveTextContent( + /Transaction unauthorized/ + ); + expect( + screen.getByText( /Respond By/i ).nextSibling + ).toHaveTextContent( /Sep 9, 2023/ ); } ); test( 'correctly renders dispute details for a dispute with staged evidence', () => { @@ -162,6 +193,7 @@ describe( 'DisputeDetails', () => { /The cardholder claims this is an unauthorized transaction/, { ignore: '.a11y-speak-region' } ); + // Render the staged evidence message screen.getByText( /You initiated a dispute a challenge to this dispute/, From b2602b4faeea7b23d342d189454022e97246d3e4 Mon Sep 17 00:00:00 2001 From: Adam Heckler <5512652+aheckler@users.noreply.github.com> Date: Sun, 10 Sep 2023 23:55:28 -0400 Subject: [PATCH 37/84] Avoid redirects on docs links (#7155) Co-authored-by: Dat Hoang --- README.md | 2 +- changelog/fix-docs-links-part-2 | 4 ++++ .../add-payment-methods-task.js | 2 +- client/checkout/blocks/fields.js | 2 +- client/components/account-balances/strings.ts | 4 ++-- .../account-balances/test/index.test.tsx | 8 ++++---- .../deposits-overview/next-deposit.tsx | 6 +++--- .../suspended-deposit-notice.tsx | 2 +- .../test/__snapshots__/index.tsx.snap | 2 +- .../deposits-overview/test/index.tsx | 4 ++-- client/components/deposits-status/index.tsx | 2 +- .../test/__snapshots__/index.js.snap | 4 ++-- client/connect-account-page/modal/index.js | 2 +- .../modal/test/__snapshots__/index.js.snap | 4 ++-- client/deposits/instant-deposits/modal.tsx | 2 +- .../test/__snapshots__/index.tsx.snap | 2 +- client/order/cancel-confirm-modal/index.js | 2 +- client/payment-details/summary/index.tsx | 2 +- .../summary/test/__snapshots__/index.tsx.snap | 4 ++-- .../disable-confirmation-modal.js | 2 +- client/payment-methods/index.js | 2 +- .../advanced-settings/multi-currency-toggle.js | 2 +- .../wcpay-subscriptions-toggle.js | 2 +- client/settings/deposits/index.js | 4 ++-- client/settings/disable-upe-modal/index.js | 2 +- client/settings/express-checkout/link-item.tsx | 2 +- .../cards/cvc-verification.tsx | 2 +- .../cvc-verification.test.tsx.snap | 2 +- .../test/__snapshots__/index.test.tsx.snap | 16 ++++++++-------- client/settings/general-settings/index.js | 4 ++-- client/settings/settings-manager/index.js | 8 ++++---- client/utils/account-fees.tsx | 4 ++-- .../test/__snapshots__/account-fees.tsx.snap | 6 +++--- includes/class-wc-payment-gateway-wcpay.php | 2 +- ...lass-wc-payments-apple-pay-registration.php | 2 +- includes/class-wc-payments-checkout.php | 2 +- includes/class-wc-payments-upe-checkout.php | 4 ++-- includes/class-wc-payments-utils.php | 2 +- includes/core/CONTRIBUTING.md | 2 +- includes/core/class-mode.php | 4 ++-- .../class-order-fraud-and-risk-meta-box.php | 2 +- ...yments-notes-additional-payment-methods.php | 2 +- ...ayments-notes-instant-deposits-eligible.php | 4 ++-- ...-wc-payments-notes-set-up-refund-policy.php | 2 +- ...ass-wc-payments-notes-set-up-stripelink.php | 2 +- .../html-subscriptions-plugin-notice.php | 2 +- .../html-wcpay-deactivate-warning.php | 4 ++-- readme.txt | 18 +++++++++--------- ...est-class-order-fraud-and-risk-meta-box.php | 8 ++++---- ...yments-notes-additional-payment-methods.php | 2 +- ...ass-wc-payments-notes-set-up-stripelink.php | 2 +- .../test-class-upe-split-payment-gateway.php | 2 +- woocommerce-payments.php | 4 ++-- 53 files changed, 97 insertions(+), 93 deletions(-) create mode 100644 changelog/fix-docs-links-part-2 diff --git a/README.md b/README.md index 54b8403c470..416c6b6a728 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ We currently support the following variables: ## Test account setup -For setting up a test account follow [these instructions](https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/dev-mode/). +For setting up a test account follow [these instructions](https://woocommerce.com/document/woopayments/testing-and-troubleshooting/dev-mode/). You will need a externally accessible URL to set up the plugin. You can use ngrok for this. diff --git a/changelog/fix-docs-links-part-2 b/changelog/fix-docs-links-part-2 new file mode 100644 index 00000000000..6f30cc936c2 --- /dev/null +++ b/changelog/fix-docs-links-part-2 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update outdated public documentation links on WooCommerce.com diff --git a/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js b/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js index 38a94dcc476..7fd26fec10d 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js @@ -258,7 +258,7 @@ const AddPaymentMethodsTask = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/checkout/blocks/fields.js b/client/checkout/blocks/fields.js index 883a4146ae1..5f47df5d286 100644 --- a/client/checkout/blocks/fields.js +++ b/client/checkout/blocks/fields.js @@ -33,7 +33,7 @@ const WCPayFields = ( {

Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC, or any test card numbers listed{ ' ' } - + here . diff --git a/client/components/account-balances/strings.ts b/client/components/account-balances/strings.ts index 547f23da8c7..76e7c2f9721 100644 --- a/client/components/account-balances/strings.ts +++ b/client/components/account-balances/strings.ts @@ -26,7 +26,7 @@ export const fundLabelStrings = { export const documentationUrls = { depositSchedule: - 'https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule', + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/', negativeBalance: - 'https://woocommerce.com/document/woocommerce-payments/fees-and-debits/account-showing-negative-balance', + 'https://woocommerce.com/document/woopayments/fees-and-debits/account-showing-negative-balance/', }; diff --git a/client/components/account-balances/test/index.test.tsx b/client/components/account-balances/test/index.test.tsx index 6018d85b1f2..586b5dc2027 100644 --- a/client/components/account-balances/test/index.test.tsx +++ b/client/components/account-balances/test/index.test.tsx @@ -339,7 +339,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getByRole( 'link' ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/' ); } ); @@ -358,7 +358,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getByRole( 'link' ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/fees-and-debits/account-showing-negative-balance' + 'https://woocommerce.com/document/woopayments/fees-and-debits/account-showing-negative-balance/' ); } ); @@ -377,7 +377,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getByRole( 'link' ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/fees-and-debits/account-showing-negative-balance' + 'https://woocommerce.com/document/woopayments/fees-and-debits/account-showing-negative-balance/' ); } ); @@ -399,7 +399,7 @@ describe( 'AccountBalances', () => { } ); expect( within( tooltip ).getByRole( 'link' ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/' ); } ); diff --git a/client/components/deposits-overview/next-deposit.tsx b/client/components/deposits-overview/next-deposit.tsx index fe55ae2ca13..cf9a08b23fd 100644 --- a/client/components/deposits-overview/next-deposit.tsx +++ b/client/components/deposits-overview/next-deposit.tsx @@ -43,7 +43,7 @@ const DepositIncludesLoanPayoutNotice = () => ( // eslint-disable-next-line jsx-a11y/anchor-has-content ( ), }, @@ -104,7 +104,7 @@ const NegativeBalanceDepositsPausedNotice = () => ( ), }, diff --git a/client/components/deposits-overview/suspended-deposit-notice.tsx b/client/components/deposits-overview/suspended-deposit-notice.tsx index 9ba9c489593..de5aa05fca3 100644 --- a/client/components/deposits-overview/suspended-deposit-notice.tsx +++ b/client/components/deposits-overview/suspended-deposit-notice.tsx @@ -35,7 +35,7 @@ function SuspendedDepositNotice(): JSX.Element { suspendLink: ( ), diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index fa49730a55e..46810596530 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -453,7 +453,7 @@ exports[`Suspended Deposit Notice Renders Component Renders 1`] = ` . Learn more diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index b22c1ef9e50..a69aa3408cb 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -364,7 +364,7 @@ describe( 'Deposits Overview information', () => { } ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/stripe-capital/overview' + 'https://woocommerce.com/document/woopayments/stripe-capital/overview/' ); } ); @@ -428,7 +428,7 @@ describe( 'Deposits Overview information', () => { } ); expect( getByRole( 'link', { name: /Why\?/ } ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule/#section-1' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/#new-accounts' ); } ); } ); diff --git a/client/components/deposits-status/index.tsx b/client/components/deposits-status/index.tsx index 0e2c1611b6f..2d40bd06b48 100644 --- a/client/components/deposits-status/index.tsx +++ b/client/components/deposits-status/index.tsx @@ -61,7 +61,7 @@ const DepositsStatus: React.FC< Props > = ( { icon = ; } else if ( showSuspendedNotice ) { const learnMoreHref = - 'https://woocommerce.com/document/woocommerce-payments/deposits/why-deposits-suspended/'; + 'https://woocommerce.com/document/woopayments/deposits/why-deposits-suspended/'; description = createInterpolateElement( /* translators: - suspended accounts FAQ URL */ __( diff --git a/client/components/deposits-status/test/__snapshots__/index.js.snap b/client/components/deposits-status/test/__snapshots__/index.js.snap index 926ff394eb2..b5853f51013 100644 --- a/client/components/deposits-status/test/__snapshots__/index.js.snap +++ b/client/components/deposits-status/test/__snapshots__/index.js.snap @@ -20,7 +20,7 @@ exports[`DepositsStatus renders blocked status 1`] = ` Temporarily suspended ( @@ -51,7 +51,7 @@ exports[`DepositsStatus renders blocked status 2`] = ` Temporarily suspended ( diff --git a/client/connect-account-page/modal/index.js b/client/connect-account-page/modal/index.js index 388c68aad69..c3cb728c043 100644 --- a/client/connect-account-page/modal/index.js +++ b/client/connect-account-page/modal/index.js @@ -15,7 +15,7 @@ import './style.scss'; const LearnMoreLink = ( props ) => ( @@ -216,7 +216,7 @@ exports[`Onboarding: location check dialog renders correctly when opened 1`] = ` diff --git a/client/deposits/instant-deposits/modal.tsx b/client/deposits/instant-deposits/modal.tsx index c99d9a2a5c6..25b2783a348 100644 --- a/client/deposits/instant-deposits/modal.tsx +++ b/client/deposits/instant-deposits/modal.tsx @@ -29,7 +29,7 @@ const InstantDepositModal: React.FC< InstantDepositModalProps > = ( { inProgress, } ) => { const learnMoreHref = - 'https://woocommerce.com/document/woocommerce-payments/deposits/instant-deposits/'; + 'https://woocommerce.com/document/woopayments/deposits/instant-deposits/'; const feePercentage = `${ percentage }%`; const description = createInterpolateElement( /* translators: %s: amount representing the fee percentage, : instant payout doc URL */ diff --git a/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap b/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap index 2ecd34561dc..6d9772123f8 100644 --- a/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap +++ b/client/deposits/instant-deposits/test/__snapshots__/index.tsx.snap @@ -69,7 +69,7 @@ exports[`Instant deposit button and modal modal renders correctly 1`] = `

Need cash in a hurry? Instant deposits are available within 30 minutes for a nominal 1.5% service fee. diff --git a/client/order/cancel-confirm-modal/index.js b/client/order/cancel-confirm-modal/index.js index 3ce6b3362d1..73acabfd3ba 100644 --- a/client/order/cancel-confirm-modal/index.js +++ b/client/order/cancel-confirm-modal/index.js @@ -60,7 +60,7 @@ const CancelConfirmationModal = ( { originalOrderStatus } ) => { howtoIssueRefunds: ( { __( 'how to issue refunds', 'woocommerce-payments' ) } diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index 8ed8e612bd8..e75f69f2caf 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -394,7 +394,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-no-target-blank diff --git a/client/payment-details/summary/test/__snapshots__/index.tsx.snap b/client/payment-details/summary/test/__snapshots__/index.tsx.snap index 28729a20fa3..9dd86ba7c81 100644 --- a/client/payment-details/summary/test/__snapshots__/index.tsx.snap +++ b/client/payment-details/summary/test/__snapshots__/index.tsx.snap @@ -256,7 +256,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca > You must @@ -573,7 +573,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th > You must diff --git a/client/payment-gateways/disable-confirmation-modal.js b/client/payment-gateways/disable-confirmation-modal.js index 23417958ed9..d5c19084041 100644 --- a/client/payment-gateways/disable-confirmation-modal.js +++ b/client/payment-gateways/disable-confirmation-modal.js @@ -113,7 +113,7 @@ const DisableConfirmationModal = ( { onClose, onConfirm } ) => { strong: , wooCommercePaymentsLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content - + ), contactSupportLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content diff --git a/client/payment-methods/index.js b/client/payment-methods/index.js index fd738363848..355174f37c1 100644 --- a/client/payment-methods/index.js +++ b/client/payment-methods/index.js @@ -111,7 +111,7 @@ const UpeSetupBanner = () => { ) } - + { __( 'Learn more', 'woocommerce-payments' ) }

diff --git a/client/settings/advanced-settings/multi-currency-toggle.js b/client/settings/advanced-settings/multi-currency-toggle.js index 67c6e654a15..8f2cd300088 100644 --- a/client/settings/advanced-settings/multi-currency-toggle.js +++ b/client/settings/advanced-settings/multi-currency-toggle.js @@ -40,7 +40,7 @@ const MultiCurrencyToggle = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index 72734e157aa..fd777c20008 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -51,7 +51,7 @@ const WCPaySubscriptionsToggle = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/deposits/index.js b/client/settings/deposits/index.js index e810f32ba05..5c633a390e0 100644 --- a/client/settings/deposits/index.js +++ b/client/settings/deposits/index.js @@ -176,7 +176,7 @@ const DepositsSchedule = () => { 'Learn more about deposit scheduling.', 'woocommerce-payments' ) } - href="https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule/" + href="https://woocommerce.com/document/woopayments/deposits/deposit-schedule/" target="_blank" rel="external noreferrer noopener" > @@ -203,7 +203,7 @@ const DepositsSchedule = () => { 'Learn more about deposit scheduling.', 'woocommerce-payments' ) } - href="https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule/" + href="https://woocommerce.com/document/woopayments/deposits/deposit-schedule/" target="_blank" rel="external noreferrer noopener" > diff --git a/client/settings/disable-upe-modal/index.js b/client/settings/disable-upe-modal/index.js index c41ebc55218..4dc089783f3 100644 --- a/client/settings/disable-upe-modal/index.js +++ b/client/settings/disable-upe-modal/index.js @@ -29,7 +29,7 @@ const NeedHelpBarSection = () => { components: { docsLink: ( // eslint-disable-next-line max-len - + { sprintf( /* translators: %s: WooPayments */ __( '%s docs', 'woocommerce-payments' ), diff --git a/client/settings/express-checkout/link-item.tsx b/client/settings/express-checkout/link-item.tsx index b1117db68b3..fcfdb41a554 100644 --- a/client/settings/express-checkout/link-item.tsx +++ b/client/settings/express-checkout/link-item.tsx @@ -167,7 +167,7 @@ const LinkExpressCheckoutItem = (): React.ReactElement => { target="_blank" rel="noreferrer" /* eslint-disable-next-line max-len */ - href="https://woocommerce.com/document/woocommerce-payments/payment-methods/link-by-stripe/" + href="https://woocommerce.com/document/woopayments/payment-methods/link-by-stripe/" /> ), }, diff --git a/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx b/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx index 393d058abc1..e3d1826640b 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx +++ b/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx @@ -47,7 +47,7 @@ const CVCVerificationRuleCard: React.FC = () => { target="_blank" type="external" // eslint-disable-next-line max-len - href="https://woocommerce.com/document/woocommerce-payments/fraud-and-disputes/fraud-protection/#advanced-configuration" + href="https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/#advanced-configuration" /> ), }, diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap index 3f68a763b88..2d9cdd41659 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap @@ -189,7 +189,7 @@ exports[`CVC verification card renders correctly when CVC check is enabled 1`] = For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap index be659d3bd3f..b6af7f8078a 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap @@ -979,7 +979,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -1977,7 +1977,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -2913,7 +2913,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -3768,7 +3768,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -4670,7 +4670,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -5488,7 +5488,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -6487,7 +6487,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -7405,7 +7405,7 @@ Object { For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more diff --git a/client/settings/general-settings/index.js b/client/settings/general-settings/index.js index 08ad4cf96d0..e9f554620ea 100644 --- a/client/settings/general-settings/index.js +++ b/client/settings/general-settings/index.js @@ -64,7 +64,7 @@ const GeneralSettings = () => { target="_blank" rel="noreferrer" /* eslint-disable-next-line max-len */ - href="https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/testing/#test-cards" + href="https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" /> ), learnMoreLink: ( @@ -72,7 +72,7 @@ const GeneralSettings = () => { ), }, diff --git a/client/settings/settings-manager/index.js b/client/settings/settings-manager/index.js index 7b61ed5ed51..1bb2e3c64fc 100644 --- a/client/settings/settings-manager/index.js +++ b/client/settings/settings-manager/index.js @@ -52,7 +52,7 @@ const ExpressCheckoutDescription = () => ( 'woocommerce-payments' ) }

- + { __( 'Learn more', 'woocommerce-payments' ) } @@ -83,7 +83,7 @@ const TransactionsDescription = () => ( 'woocommerce-payments' ) }

- + { __( 'View our documentation', 'woocommerce-payments' ) } @@ -104,7 +104,7 @@ const DepositsDescription = () => { depositDelayDays ) }

- + { __( 'Learn more about pending schedules', 'woocommerce-payments' @@ -124,7 +124,7 @@ const FraudProtectionDescription = () => { 'woocommerce-payments' ) }

- + { __( 'Learn more about risk filtering', 'woocommerce-payments' diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 3f1175f44d2..f7cdd94a149 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -18,9 +18,9 @@ import { PaymentMethod } from 'wcpay/types/payment-methods'; import { createInterpolateElement } from '@wordpress/element'; const countryFeeStripeDocsBaseLink = - 'https://woocommerce.com/document/woocommerce-payments/fees-and-debits/fees/#'; + 'https://woocommerce.com/document/woopayments/fees-and-debits/fees/#'; const countryFeeStripeDocsBaseLinkNoCountry = - 'https://woocommerce.com/document/woocommerce-payments/fees-and-debits/fees/'; + 'https://woocommerce.com/document/woopayments/fees-and-debits/fees/'; const countryFeeStripeDocsSectionNumbers: Record< string, string > = { AE: 'united-arab-emirates', AU: 'australia', diff --git a/client/utils/test/__snapshots__/account-fees.tsx.snap b/client/utils/test/__snapshots__/account-fees.tsx.snap index d219db01904..ab1e34eee08 100644 --- a/client/utils/test/__snapshots__/account-fees.tsx.snap +++ b/client/utils/test/__snapshots__/account-fees.tsx.snap @@ -44,7 +44,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base >
@@ -101,7 +101,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base > @@ -158,7 +158,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays custo > diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 754629e0ec2..7a50b57b9c4 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -250,7 +250,7 @@ public function __construct( 'title' => __( 'Customer bank statement', 'woocommerce-payments' ), 'description' => WC_Payments_Utils::esc_interpolated_html( __( 'Edit the way your store name appears on your customers’ bank statements (read more about requirements here).', 'woocommerce-payments' ), - [ 'a' => '' ] + [ 'a' => '' ] ), ], 'manual_capture' => [ diff --git a/includes/class-wc-payments-apple-pay-registration.php b/includes/class-wc-payments-apple-pay-registration.php index 662865af61b..e07c74be3c0 100644 --- a/includes/class-wc-payments-apple-pay-registration.php +++ b/includes/class-wc-payments-apple-pay-registration.php @@ -411,7 +411,7 @@ public function display_error_notice() { $learn_more_text = WC_Payments_Utils::esc_interpolated_html( __( 'Learn more.', 'woocommerce-payments' ), [ - 'a' => '', + 'a' => '', ] ); diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 9c45b25f850..d6f098116b4 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -246,7 +246,7 @@ public function payment_fields() { __( 'Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC, or any test card numbers listed here.', 'woocommerce-payments' ), [ 'strong' => '', - 'a' => '', + 'a' => '', ] ); ?> diff --git a/includes/class-wc-payments-upe-checkout.php b/includes/class-wc-payments-upe-checkout.php index 9edc5758465..6c54ee3e303 100644 --- a/includes/class-wc-payments-upe-checkout.php +++ b/includes/class-wc-payments-upe-checkout.php @@ -254,7 +254,7 @@ public function get_enabled_payment_method_config() { $payment_method->get_testing_instructions(), [ 'strong' => '', - 'a' => '', + 'a' => '', ] ); $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); @@ -330,7 +330,7 @@ function() use ( $payment_fields, $upe_object_name ) { $testing_instructions, [ 'strong' => '', - 'a' => '', + 'a' => '', ] ); } diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 39071b7fe67..7eaa22889bd 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -213,7 +213,7 @@ public static function zero_decimal_currencies(): array { /** * List of countries enabled for Stripe platform account. See also this URL: - * https://woocommerce.com/document/woocommerce-payments/compatibility/countries/#supported-countries + * https://woocommerce.com/document/woopayments/compatibility/countries/#supported-countries * * @return string[] */ diff --git a/includes/core/CONTRIBUTING.md b/includes/core/CONTRIBUTING.md index 3487d1bdd49..a36e462c863 100644 --- a/includes/core/CONTRIBUTING.md +++ b/includes/core/CONTRIBUTING.md @@ -12,7 +12,7 @@ There are a few possible paths when it comes to services: 1. __Create a facade for an existing service:__ Create a new service class within `core/service`, which simply facades the [existing service](service/customer-service.md). Doing so will allow us to modify the facade in the future, keeping existing methods with the same parameters as existing ones. This is what was done with the [customer service](service/customer-service.md), and is the recommended way if a certain feature requires access to an existing service quickly. -2. __Move an existing service to the core directory:__ This should be done with consideration how the service could change in the future, and whether it is core to the gateway. If it more suitable to an extension (ex. [Multi-Currency](https://woocommerce.com/document/woocommerce-payments/currencies/multi-currency-setup/?quid=92bb9bc4a89c89c9445c87865165e025)), or a consumer (ex. [WooPay](https://woocommerce.com/documentation/woopay/)), it likely needs to be somewhere else. +2. __Move an existing service to the core directory:__ This should be done with consideration how the service could change in the future, and whether it is core to the gateway. If it more suitable to an extension (ex. [Multi-Currency](https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/)), or a consumer (ex. [WooPay](https://woocommerce.com/documentation/products/woopay/)), it likely needs to be somewhere else. 3. When __creating a new service__, similarly to moving existing ones here, please consider whether the service belongs to core. If it does, do it with care, as services should be reliable and resilient. đŸ”— Further information about services in core is available [within the services directory](services/README.md). diff --git a/includes/core/class-mode.php b/includes/core/class-mode.php index 89027b76313..1ed73ea7655 100644 --- a/includes/core/class-mode.php +++ b/includes/core/class-mode.php @@ -89,7 +89,7 @@ private function maybe_init() { /** * Allows WooCommerce to enter dev mode. * - * @see https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/dev-mode/ + * @see https://woocommerce.com/document/woopayments/testing-and-troubleshooting/dev-mode/ * @param bool $dev_mode The pre-determined dev mode. */ $this->dev_mode = (bool) apply_filters( 'wcpay_dev_mode', $dev_mode ); @@ -100,7 +100,7 @@ private function maybe_init() { /** * Allows WooCommerce to enter test mode. * - * @see https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/testing/#enabling-test-mode + * @see https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#enabling-test-mode * @param bool $test_mode The pre-determined test mode. */ $this->test_mode = (bool) apply_filters( 'wcpay_test_mode', $test_mode ); diff --git a/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php b/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php index 0a2a5f6b095..28dd3e0d3d2 100644 --- a/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php +++ b/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php @@ -130,7 +130,7 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) { } $callout = __( 'Learn more', 'woocommerce-payments' ); - $callout_url = 'https://woocommerce.com/document/woocommerce-payments/fraud-and-disputes/fraud-protection/'; + $callout_url = 'https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/'; $callout_url = add_query_arg( 'status_is', 'fraud-meta-box-not-wcpay-learn-more', $callout_url ); echo '

' . esc_html( $description ) . '

' . esc_html( $callout ) . ''; break; diff --git a/includes/notes/class-wc-payments-notes-additional-payment-methods.php b/includes/notes/class-wc-payments-notes-additional-payment-methods.php index 5eb97dc477c..20a0cd009d2 100644 --- a/includes/notes/class-wc-payments-notes-additional-payment-methods.php +++ b/includes/notes/class-wc-payments-notes-additional-payment-methods.php @@ -71,7 +71,7 @@ public static function get_note() { 'WooPayments' ), [ - 'a' => '', + 'a' => '', ] ) ); diff --git a/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php b/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php index be4ce07190a..9b4fff947c9 100644 --- a/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php +++ b/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php @@ -41,7 +41,7 @@ public static function get_note() { __( "Get immediate access to your funds when you need them – including nights, weekends, and holidays. With %s' Instant Deposits feature, you're able to transfer your earnings to a debit card within minutes.", 'woocommerce-payments' ), 'WooPayments' ), - [ 'a' => '' ] + [ 'a' => '' ] ) ); $note->set_content_data( (object) [] ); @@ -51,7 +51,7 @@ public static function get_note() { $note->add_action( self::NOTE_NAME, __( 'Request an instant deposit', 'woocommerce-payments' ), - 'https://woocommerce.com/document/woocommerce-payments/deposits/instant-deposits/#request-an-instant-deposit', + 'https://woocommerce.com/document/woopayments/deposits/instant-deposits/#request-an-instant-deposit', 'unactioned', true ); diff --git a/includes/notes/class-wc-payments-notes-set-up-refund-policy.php b/includes/notes/class-wc-payments-notes-set-up-refund-policy.php index a3fe9c7114e..27d5d8fa57e 100644 --- a/includes/notes/class-wc-payments-notes-set-up-refund-policy.php +++ b/includes/notes/class-wc-payments-notes-set-up-refund-policy.php @@ -24,7 +24,7 @@ class WC_Payments_Notes_Set_Up_Refund_Policy { /** * Name of the note for use in the database. */ - const NOTE_DOCUMENTATION_URL = 'https://woocommerce.com/document/woocommerce-refunds#how-do-i-inform-my-customers-about-the-refund-policy'; + const NOTE_DOCUMENTATION_URL = 'https://woocommerce.com/document/woocommerce-refunds/#how-do-i-inform-my-customers-about-the-refund-policy'; /** * Get the note. diff --git a/includes/notes/class-wc-payments-notes-set-up-stripelink.php b/includes/notes/class-wc-payments-notes-set-up-stripelink.php index 5185684bd7c..49db6479d33 100644 --- a/includes/notes/class-wc-payments-notes-set-up-stripelink.php +++ b/includes/notes/class-wc-payments-notes-set-up-stripelink.php @@ -27,7 +27,7 @@ class WC_Payments_Notes_Set_Up_StripeLink { /** * CTA button link */ - const NOTE_DOCUMENTATION_URL = 'https://woocommerce.com/document/woocommerce-payments/payment-methods/link-by-stripe/'; + const NOTE_DOCUMENTATION_URL = 'https://woocommerce.com/document/woopayments/payment-methods/link-by-stripe/'; /** * The account service instance. diff --git a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php index c49906e0929..0a2e8f6fa79 100644 --- a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php +++ b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php @@ -38,7 +38,7 @@ esc_html__( 'Existing subscribers will need to pay for their next renewal manually, after which automatic payments will resume. You will also no longer have access to the %1$s%3$sadvanced features%4$s%2$s of WooCommerce Subscriptions.', 'woocommerce-payments' ), '', '', - '', + '', '' ); ?> diff --git a/includes/subscriptions/templates/html-wcpay-deactivate-warning.php b/includes/subscriptions/templates/html-wcpay-deactivate-warning.php index 1331f77dc81..302392cc340 100644 --- a/includes/subscriptions/templates/html-wcpay-deactivate-warning.php +++ b/includes/subscriptions/templates/html-wcpay-deactivate-warning.php @@ -22,8 +22,8 @@ printf( // translators: $1 $2 $3 placeholders are opening and closing HTML link tags, linking to documentation. $4 $5 placeholders are opening and closing strong HTML tags. $6 is WooPayments. esc_html__( 'Your store has active subscriptions using the built-in %6$s functionality. Due to the %1$soff-site billing engine%3$s these subscriptions use, %4$sthey will continue to renew even after you deactivate %6$s%5$s. %2$sLearn more%3$s.', 'woocommerce-payments' ), - '', - '', + '', + '', '', '', '', diff --git a/readme.txt b/readme.txt index 74cf3b8bde3..4c4df33925b 100644 --- a/readme.txt +++ b/readme.txt @@ -22,13 +22,13 @@ See payments, track cash flow into your bank account, manage refunds, and stay o Features previously only available on your payment provider’s website are now part of your store’s **integrated payments dashboard**. This enables you to: -- View the details of [payments, refunds, and other transactions](https://woocommerce.com/document/woocommerce-payments/managing-money/). -- View and respond to [disputes and chargebacks](https://woocommerce.com/document/woocommerce-payments/fraud-and-disputes/managing-disputes-with-woocommerce-payments/). -- [Track deposits](https://woocommerce.com/document/woocommerce-payments/deposits/) into your bank account or debit card. +- View the details of [payments, refunds, and other transactions](https://woocommerce.com/document/woopayments/managing-money/). +- View and respond to [disputes and chargebacks](https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/). +- [Track deposits](https://woocommerce.com/document/woopayments/deposits/) into your bank account or debit card. **Pay as you go** -WooPayments is **free to install**, with **no setup fees or monthly fees**. Pay-as-you-go fees start at 2.9% + $0.30 per transaction for U.S.-issued cards. [Read more about transaction fees](https://woocommerce.com/document/woocommerce-payments/fees-and-debits/fees/). +WooPayments is **free to install**, with **no setup fees or monthly fees**. Pay-as-you-go fees start at 2.9% + $0.30 per transaction for U.S.-issued cards. [Read more about transaction fees](https://woocommerce.com/document/woopayments/fees-and-debits/fees/). **Supported by the WooCommerce team** @@ -44,7 +44,7 @@ Our global support team is available to answer questions you may have about WooP = Try it now = -To try WooPayments (previously WooCommerce Payments) on your store, simply [install it](https://wordpress.org/plugins/woocommerce-payments/#installation) and follow the prompts. Please see our [Startup Guide](https://woocommerce.com/document/woocommerce-payments/startup-guide/) for a full walkthrough of the process. +To try WooPayments (previously WooCommerce Payments) on your store, simply [install it](https://wordpress.org/plugins/woocommerce-payments/#installation) and follow the prompts. Please see our [Startup Guide](https://woocommerce.com/document/woopayments/startup-guide/) for a full walkthrough of the process. WooPayments has experimental support for the Checkout block from [WooCommerce Blocks](https://wordpress.org/plugins/woo-gutenberg-products-block/). Please check the [FAQ section](#faq) for more information. @@ -56,7 +56,7 @@ Install and activate the WooCommerce and WooPayments plugins, if you haven't alr = What countries and currencies are supported? = -If you are an individual or business based in [one of these countries](https://woocommerce.com/document/woocommerce-payments/compatibility/countries/#supported-countries), you can sign-up with WooPayments. After completing sign up, you can accept payments from customers anywhere in the world. +If you are an individual or business based in [one of these countries](https://woocommerce.com/document/woopayments/compatibility/countries/#supported-countries), you can sign-up with WooPayments. After completing sign up, you can accept payments from customers anywhere in the world. We are actively planning to expand into additional countries based on your interest. Let us know where you would like to [see WooPayments launch next](https://woocommerce.com/payments/#request-invite). @@ -66,15 +66,15 @@ WooPayments uses the WordPress.com connection to authenticate each request, conn = How do I set up a store for a client? = -If you are a developer or agency setting up a site for a client, please see [this page](https://woocommerce.com/document/woocommerce-payments/account-management/developer-or-agency-setup/) of our documentation for some tips on how to install WooPayments on client sites. +If you are a developer or agency setting up a site for a client, please see [this page](https://woocommerce.com/document/woopayments/account-management/developer-or-agency-setup/) of our documentation for some tips on how to install WooPayments on client sites. = How is WooPayments related to Stripe? = -WooPayments is built in partnership with Stripe [Stripe](https://stripe.com/). When you sign up for WooPayments, your personal and business information is verified with Stripe and stored in an account connected to the WooPayments service. This account is then used in the background for managing your business account information and activity via WooPayments. [Learn more](https://woocommerce.com/document/woocommerce-payments/account-management/partnership-with-stripe/). +WooPayments is built in partnership with Stripe [Stripe](https://stripe.com/). When you sign up for WooPayments, your personal and business information is verified with Stripe and stored in an account connected to the WooPayments service. This account is then used in the background for managing your business account information and activity via WooPayments. [Learn more](https://woocommerce.com/document/woopayments/account-management/partnership-with-stripe/). = Are there Terms of Service and data usage policies? = -You can read our Terms of Service and other policies [here](https://woocommerce.com/document/woocommerce-payments/our-policies/). +You can read our Terms of Service and other policies [here](https://woocommerce.com/document/woopayments/our-policies/). = How does the Checkout block work? = diff --git a/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php b/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php index b027ab40280..0ec12e225e1 100644 --- a/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php +++ b/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php @@ -195,17 +195,17 @@ public function display_order_fraud_and_risk_meta_box_message_not_card_provider( 'simulate legacy UPE Popular payment methods' => [ 'payment_method_id' => 'woocommerce_payments', 'payment_method_title' => 'Popular payment methods', - 'expected_output' => '

Risk filtering is only available for orders processed using credit cards with WooPayments.

Learn more', + 'expected_output' => '

Risk filtering is only available for orders processed using credit cards with WooPayments.

Learn more', ], 'simulate legacy UPE Bancontact' => [ 'payment_method_id' => 'woocommerce_payments', 'payment_method_title' => 'Bancontact', - 'expected_output' => '

Risk filtering is only available for orders processed using credit cards with WooPayments. This order was processed with Bancontact.

Learn more', + 'expected_output' => '

Risk filtering is only available for orders processed using credit cards with WooPayments. This order was processed with Bancontact.

Learn more', ], 'simulate split UPE Bancontact' => [ 'payment_method_id' => 'woocommerce_payments_bancontact', 'payment_method_title' => 'Bancontact', - 'expected_output' => '

Risk filtering is only available for orders processed using credit cards with WooPayments. This order was processed with Bancontact.

Learn more', + 'expected_output' => '

Risk filtering is only available for orders processed using credit cards with WooPayments. This order was processed with Bancontact.

Learn more', ], ]; } @@ -235,7 +235,7 @@ public function test_display_order_fraud_and_risk_meta_box_message_not_wcpay() { $this->order_fraud_and_risk_meta_box->display_order_fraud_and_risk_meta_box_message( $this->order ); // Assert: Check to make sure the expected string has been output. - $this->expectOutputString( '

Risk filtering is only available for orders processed using credit cards with WooPayments. This order was processed with Direct bank transfer.

Learn more' ); + $this->expectOutputString( '

Risk filtering is only available for orders processed using credit cards with WooPayments. This order was processed with Direct bank transfer.

Learn more' ); } public function test_display_order_fraud_and_risk_meta_box_message_default() { diff --git a/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php b/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php index 32a019b048c..50c2a12ce45 100644 --- a/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php +++ b/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php @@ -28,7 +28,7 @@ public function test_get_note() { $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); $this->assertSame( 'Boost your sales by accepting new payment methods', $note->get_title() ); - $this->assertSame( 'Get early access to additional payment methods and an improved checkout experience, coming soon to WooPayments. Learn more', $note->get_content() ); + $this->assertSame( 'Get early access to additional payment methods and an improved checkout experience, coming soon to WooPayments. Learn more', $note->get_content() ); $this->assertSame( 'info', $note->get_type() ); $this->assertSame( 'wc-payments-notes-additional-payment-methods', $note->get_name() ); $this->assertSame( 'woocommerce-payments', $note->get_source() ); diff --git a/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php b/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php index 3312bc6ebb3..23800550a95 100644 --- a/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php +++ b/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php @@ -52,7 +52,7 @@ public function test_stripelink_setup_get_note() { list( $set_up_action ) = $note->get_actions(); $this->assertSame( 'wc-payments-notes-set-up-stripe-link', $set_up_action->name ); $this->assertSame( 'Set up now', $set_up_action->label ); - $this->assertStringStartsWith( 'https://woocommerce.com/document/woocommerce-payments/payment-methods/link-by-stripe/', $set_up_action->query ); + $this->assertStringStartsWith( 'https://woocommerce.com/document/woopayments/payment-methods/link-by-stripe/', $set_up_action->query ); } public function test_stripelink_setup_note_null_when_upe_disabled() { diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index 18c0024bb3e..b6a2aa24a93 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -2191,7 +2191,7 @@ function ( $payment_method ) { 'countries' => [], 'upePaymentIntentData' => null, 'upeSetupIntentData' => null, - 'testingInstructions' => 'Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', + 'testingInstructions' => 'Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'forceNetworkSavedCards' => false, ], 'link' => [ diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 8596eb15c92..5c9fb56f634 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -316,7 +316,7 @@ function wcpay_get_jetpack_idc_custom_content(): array { __( 'We’ve detected that you have duplicate sites connected to %s. When Safe Mode is active, payments will not be interrupted. However, some features may not be available until you’ve resolved this issue below. Safe Mode is most frequently activated when you’re transferring your site from one domain to another, or creating a staging site for testing. A site adminstrator can resolve this issue. Learn more', 'woocommerce-payments' ), 'WooPayments' ), - 'supportURL' => 'https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/safe-mode/', + 'supportURL' => 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/safe-mode/', 'adminBarSafeModeLabel' => sprintf( /* translators: %s: WooPayments. */ __( '%s Safe Mode', 'woocommerce-payments' ), @@ -327,7 +327,7 @@ function wcpay_get_jetpack_idc_custom_content(): array { __( "Notice: It appears that your 'wp-config.php' file might be using dynamic site URL values. Dynamic site URLs could cause %s to enter Safe Mode. Learn how to set a static site URL.", 'woocommerce-payments' ), 'WooPayments' ), - 'dynamicSiteUrlSupportLink' => 'https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/safe-mode/#dynamic-site-urls', + 'dynamicSiteUrlSupportLink' => 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/safe-mode/#dynamic-site-urls', ]; $urls = Automattic\Jetpack\Identity_Crisis::get_mismatched_urls(); From 78ed2d6591668f4320663e884e961e549cb37d46 Mon Sep 17 00:00:00 2001 From: James Allan Date: Mon, 11 Sep 2023 15:52:39 +1000 Subject: [PATCH 38/84] Update subscriptions-core to 6.2 (#6976) --- changelog/subscriptions-core-6.2.0-1 | 4 ++++ changelog/subscriptions-core-6.2.0-2 | 4 ++++ changelog/subscriptions-core-6.2.0-3 | 4 ++++ changelog/subscriptions-core-6.2.0-4 | 4 ++++ composer.json | 6 +++--- composer.lock | 14 +++++++------- 6 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 changelog/subscriptions-core-6.2.0-1 create mode 100644 changelog/subscriptions-core-6.2.0-2 create mode 100644 changelog/subscriptions-core-6.2.0-3 create mode 100644 changelog/subscriptions-core-6.2.0-4 diff --git a/changelog/subscriptions-core-6.2.0-1 b/changelog/subscriptions-core-6.2.0-1 new file mode 100644 index 00000000000..2aa534e189f --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-1 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Ensure the shipping phone number field is copied to subscriptions and their orders when copying address meta. diff --git a/changelog/subscriptions-core-6.2.0-2 b/changelog/subscriptions-core-6.2.0-2 new file mode 100644 index 00000000000..1d4cd07d2c0 --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-2 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache to improve performance. diff --git a/changelog/subscriptions-core-6.2.0-3 b/changelog/subscriptions-core-6.2.0-3 new file mode 100644 index 00000000000..0244ee6dbd1 --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-3 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Deprecated the 'woocommerce_subscriptions_not_found_label' filter. diff --git a/changelog/subscriptions-core-6.2.0-4 b/changelog/subscriptions-core-6.2.0-4 new file mode 100644 index 00000000000..9ad82198e89 --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-4 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Updated subscriptions-core to 6.2.0 diff --git a/composer.json b/composer.json index 044e88faa03..668bf0f26a7 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "automattic/jetpack-autoloader": "2.11.18", "automattic/jetpack-identity-crisis": "0.8.43", "automattic/jetpack-sync": "1.47.7", - "woocommerce/subscriptions-core": "6.0.0", + "woocommerce/subscriptions-core": "6.2.0", "psr/container": "^1.1" }, "require-dev": { @@ -91,8 +91,8 @@ "WCPay\\Vendor\\": "lib/packages", "WCPay\\": "src" }, - "files": [ - "src/wcpay-get-container.php" + "files": [ + "src/wcpay-get-container.php" ] }, "repositories": [ diff --git a/composer.lock b/composer.lock index fb2aec0a2d2..a5963e45fdb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e5d6dbd579dee11681fa7a39a11506f1", + "content-hash": "86b5f217949dc6931b79653be4a6dca8", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -988,16 +988,16 @@ }, { "name": "woocommerce/subscriptions-core", - "version": "6.0.0", + "version": "6.2.0", "source": { "type": "git", "url": "https://github.com/Automattic/woocommerce-subscriptions-core.git", - "reference": "b48c46a6a08b73d8afc7a0e686173c5b5c55ecbb" + "reference": "47cfe92d60239d1b8b12a5f640a3772b0e4e1272" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/woocommerce-subscriptions-core/zipball/b48c46a6a08b73d8afc7a0e686173c5b5c55ecbb", - "reference": "b48c46a6a08b73d8afc7a0e686173c5b5c55ecbb", + "url": "https://api.github.com/repos/Automattic/woocommerce-subscriptions-core/zipball/47cfe92d60239d1b8b12a5f640a3772b0e4e1272", + "reference": "47cfe92d60239d1b8b12a5f640a3772b0e4e1272", "shasum": "" }, "require": { @@ -1038,10 +1038,10 @@ "description": "Sell products and services with recurring payments in your WooCommerce Store.", "homepage": "https://github.com/Automattic/woocommerce-subscriptions-core", "support": { - "source": "https://github.com/Automattic/woocommerce-subscriptions-core/tree/6.0.0", + "source": "https://github.com/Automattic/woocommerce-subscriptions-core/tree/6.2.0", "issues": "https://github.com/Automattic/woocommerce-subscriptions-core/issues" }, - "time": "2023-07-18T06:28:51+00:00" + "time": "2023-08-10T23:43:48+00:00" } ], "packages-dev": [ From 9ccdab9462f62192785b8b060a6e76125a08c8a9 Mon Sep 17 00:00:00 2001 From: James Allan Date: Mon, 11 Sep 2023 16:18:19 +1000 Subject: [PATCH 39/84] Add Woo Subscription Stripe Billing settings and migration status notices (#7105) Co-authored-by: mattallan Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Co-authored-by: Dat Hoang --- changelog/stripe-billing-migration-notices | 4 + changelog/stripe-billing-setting | 4 + client/components/inline-notice/styles.scss | 61 ++------ client/data/settings/actions.js | 28 ++++ client/data/settings/hooks.js | 45 +++++- client/data/settings/selectors.js | 16 ++ client/globals.d.ts | 2 + .../settings/advanced-settings/debug-mode.js | 10 +- client/settings/advanced-settings/index.js | 49 ++---- .../settings/advanced-settings/interfaces.ts | 14 ++ .../stripe-billing-notices/context.tsx | 32 ++++ .../migrate-automatically-notice.tsx | 96 ++++++++++++ .../migrate-completed-notice.tsx | 64 ++++++++ .../migrate-option-notice.tsx | 145 ++++++++++++++++++ .../migration-progress-notice.tsx | 99 ++++++++++++ .../stripe-billing-notices/notices.tsx | 47 ++++++ .../stripe-billing-notices/style.scss | 4 + .../stripe-billing-section.tsx | 108 +++++++++++++ .../stripe-billing-toggle.tsx | 67 ++++++++ .../advanced-settings/test/debug-mode.test.js | 6 - .../advanced-settings/test/index.test.js | 41 +++-- .../wcpay-subscriptions-toggle.js | 8 +- client/settings/settings-manager/index.js | 28 +++- includes/admin/class-wc-payments-admin.php | 2 + ...s-wc-rest-payments-settings-controller.php | 91 +++++++++++ includes/class-wc-payment-gateway-wcpay.php | 3 +- includes/class-wc-payments-features.php | 46 ++++++ includes/class-wc-payments.php | 34 +++- ...wc-payment-gateway-wcpay-subscriptions.php | 15 +- ...it-wc-payments-subscriptions-utilities.php | 28 ++++ .../class-wc-payments-product-service.php | 4 +- ...ts-subscription-minimum-amount-handler.php | 2 +- ...class-wc-payments-subscription-service.php | 19 ++- ...ass-wc-payments-subscriptions-migrator.php | 58 ++++--- .../class-wc-payments-subscriptions.php | 20 ++- psalm-baseline.xml | 12 ++ .../helpers/class-wc-helper-subscriptions.php | 14 ++ ...ment-gateway-wcpay-subscriptions-trait.php | 6 +- 38 files changed, 1174 insertions(+), 158 deletions(-) create mode 100644 changelog/stripe-billing-migration-notices create mode 100644 changelog/stripe-billing-setting create mode 100644 client/settings/advanced-settings/interfaces.ts create mode 100644 client/settings/advanced-settings/stripe-billing-notices/context.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-notices/migrate-completed-notice.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-notices/migration-progress-notice.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-notices/notices.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-notices/style.scss create mode 100644 client/settings/advanced-settings/stripe-billing-section.tsx create mode 100644 client/settings/advanced-settings/stripe-billing-toggle.tsx diff --git a/changelog/stripe-billing-migration-notices b/changelog/stripe-billing-migration-notices new file mode 100644 index 00000000000..55bfc08a088 --- /dev/null +++ b/changelog/stripe-billing-migration-notices @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. diff --git a/changelog/stripe-billing-setting b/changelog/stripe-billing-setting new file mode 100644 index 00000000000..8feb7c76c0f --- /dev/null +++ b/changelog/stripe-billing-setting @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Introduce a new setting that enables store to opt into Subscription off-site billing via Stripe Billing. diff --git a/client/components/inline-notice/styles.scss b/client/components/inline-notice/styles.scss index 32c77b618d4..6c4059d7273 100644 --- a/client/components/inline-notice/styles.scss +++ b/client/components/inline-notice/styles.scss @@ -1,12 +1,3 @@ -$is-info: #007cba; -$is-info-hover: #006ba1; -$is-warning: #f0b849; -$is-warning-hover: #a16f00; -$is-error: #cc1818; -$is-error-hover: #b30f0f; -$is-success: #00a32a; -$is-success-hover: #00982a; - .wcpay-inline-notice.components-notice { margin: $gap-large 0; padding: 11px 0 11px 17px; @@ -62,75 +53,51 @@ $is-success-hover: #00982a; /* Specific styles for each variant */ &.is-info { - background-color: #f0f6fc; + background-color: $wp-blue-0; .wcpay-inline-notice__icon svg { - fill: $is-info; + fill: $wp-blue-70; } button.wcpay-inline-notice__action { - box-shadow: inset 0 0 0 1px $is-info; - &:hover { - box-shadow: inset 0 0 0 1px $is-info-hover; - } + box-shadow: inset 0 0 0 1px $wp-blue-70; } .wcpay-inline-notice__action { - color: $is-info; - &:hover { - color: $is-info-hover; - } + color: $wp-blue-70; } } &.is-warning { background-color: #fcf9e8; .wcpay-inline-notice__icon svg { - fill: $is-warning; + fill: $wp-yellow-70; } button.wcpay-inline-notice__action { - box-shadow: inset 0 0 0 1px $is-warning; - &:hover { - box-shadow: inset 0 0 0 1px $is-warning-hover; - } + box-shadow: inset 0 0 0 1px $wp-yellow-70; } .wcpay-inline-notice__action { - color: $is-warning; - &:hover { - color: $is-warning-hover; - } + color: $wp-yellow-70; } } &.is-error { - background-color: #fcf0f1; + background-color: $wp-red-0; .wcpay-inline-notice__icon svg { - fill: $is-error; + fill: $wp-red-70; } button.wcpay-inline-notice__action { - box-shadow: inset 0 0 0 1px $is-error; - &:hover { - box-shadow: inset 0 0 0 1px $is-error-hover; - } + box-shadow: inset 0 0 0 1px $wp-red-70; } .wcpay-inline-notice__action { - color: $is-error; - &:hover { - color: $is-error-hover; - } + color: $wp-red-70; } } &.is-success { background-color: #edfaef; .wcpay-inline-notice__icon svg { - fill: $is-success; + fill: $wp-green-70; } button.wcpay-inline-notice__action { - box-shadow: inset 0 0 0 1px $is-success; - &:hover { - box-shadow: inset 0 0 0 1px $is-success-hover; - } + box-shadow: inset 0 0 0 1px $wp-green-70; } .wcpay-inline-notice__action { - color: $is-success; - &:hover { - color: $is-success-hover; - } + color: $wp-green-70; } } } diff --git a/client/data/settings/actions.js b/client/data/settings/actions.js index 1d5f231d5d8..6444df92b4b 100644 --- a/client/data/settings/actions.js +++ b/client/data/settings/actions.js @@ -269,3 +269,31 @@ export function updateAdvancedFraudProtectionSettings( settings ) { advanced_fraud_protection_settings: settings, } ); } + +export function updateIsStripeBillingEnabled( isEnabled ) { + return updateSettingsValues( { is_stripe_billing_enabled: isEnabled } ); +} + +export function* submitStripeBillingSubscriptionMigration() { + try { + yield dispatch( STORE_NAME ).startResolution( + 'scheduleStripeBillingMigration' + ); + + yield apiFetch( { + path: `${ NAMESPACE }/settings/schedule-stripe-billing-migration`, + method: 'post', + } ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + __( + 'Error starting the Stripe Billing migration.', + 'woocommerce-payments' + ) + ); + } + + yield dispatch( STORE_NAME ).finishResolution( + 'scheduleStripeBillingMigration' + ); +} diff --git a/client/data/settings/hooks.js b/client/data/settings/hooks.js index 3fbd2d375b1..b8bf25e730a 100644 --- a/client/data/settings/hooks.js +++ b/client/data/settings/hooks.js @@ -154,17 +154,13 @@ export const useWCPaySubscriptions = () => { const { getIsWCPaySubscriptionsEnabled, getIsWCPaySubscriptionsEligible, - getIsSubscriptionsPluginActive, } = select( STORE_NAME ); const isWCPaySubscriptionsEnabled = getIsWCPaySubscriptionsEnabled(); const isWCPaySubscriptionsEligible = getIsWCPaySubscriptionsEligible(); - const isSubscriptionsPluginActive = getIsSubscriptionsPluginActive(); - return [ isWCPaySubscriptionsEnabled, isWCPaySubscriptionsEligible, - isSubscriptionsPluginActive, updateIsWCPaySubscriptionsEnabled, ]; }, @@ -609,3 +605,44 @@ export const useWooPayShowIncompatibilityNotice = () => { return getShowWooPayIncompatibilityNotice(); } ); }; + +export const useStripeBilling = () => { + const { updateIsStripeBillingEnabled } = useDispatch( STORE_NAME ); + + return useSelect( + ( select ) => { + const { getIsStripeBillingEnabled } = select( STORE_NAME ); + + return [ + getIsStripeBillingEnabled(), + updateIsStripeBillingEnabled, + ]; + }, + [ updateIsStripeBillingEnabled ] + ); +}; + +export const useStripeBillingMigration = () => { + const { submitStripeBillingSubscriptionMigration } = useDispatch( + STORE_NAME + ); + + return useSelect( ( select ) => { + const { getStripeBillingSubscriptionCount } = select( STORE_NAME ); + const { getIsStripeBillingMigrationInProgress } = select( STORE_NAME ); + const { isResolving } = select( STORE_NAME ); + const hasResolved = select( STORE_NAME ).hasFinishedResolution( + 'scheduleStripeBillingMigration' + ); + const { getStripeBillingMigratedCount } = select( STORE_NAME ); + + return [ + getIsStripeBillingMigrationInProgress(), + getStripeBillingMigratedCount(), + getStripeBillingSubscriptionCount(), + submitStripeBillingSubscriptionMigration, + isResolving( 'scheduleStripeBillingMigration' ), + hasResolved, + ]; + }, [] ); +}; diff --git a/client/data/settings/selectors.js b/client/data/settings/selectors.js index dfdbadfdd2d..41facc3ebb0 100644 --- a/client/data/settings/selectors.js +++ b/client/data/settings/selectors.js @@ -233,3 +233,19 @@ export const getAdvancedFraudProtectionSettings = ( state ) => { export const getShowWooPayIncompatibilityNotice = ( state ) => { return getSettings( state ).show_woopay_incompatibility_notice || false; }; + +export const getIsStripeBillingEnabled = ( state ) => { + return getSettings( state ).is_stripe_billing_enabled || false; +}; + +export const getIsStripeBillingMigrationInProgress = ( state ) => { + return getSettings( state ).is_migrating_stripe_billing || false; +}; + +export const getStripeBillingSubscriptionCount = ( state ) => { + return getSettings( state ).stripe_billing_subscription_count || 0; +}; + +export const getStripeBillingMigratedCount = ( state ) => { + return getSettings( state ).stripe_billing_migrated_count || 0; +}; diff --git a/client/globals.d.ts b/client/globals.d.ts index e25789c5efa..5cac0f5ea58 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -109,6 +109,8 @@ declare global { task_badge?: string; }; isWooPayStoreCountryAvailable: boolean; + isSubscriptionsPluginActive: boolean; + isStripeBillingEligible: boolean; }; const wcTracks: any; diff --git a/client/settings/advanced-settings/debug-mode.js b/client/settings/advanced-settings/debug-mode.js index 3630ee33e39..f1b5a09c7a3 100644 --- a/client/settings/advanced-settings/debug-mode.js +++ b/client/settings/advanced-settings/debug-mode.js @@ -3,7 +3,6 @@ */ import { CheckboxControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -13,17 +12,10 @@ import { useDebugLog, useDevMode } from 'wcpay/data'; const DebugMode = () => { const isDevModeEnabled = useDevMode(); const [ isLoggingChecked, setIsLoggingChecked ] = useDebugLog(); - const headingRef = useRef( null ); - - useEffect( () => { - if ( ! headingRef.current ) return; - - headingRef.current.focus(); - }, [] ); return ( <> -

+

{ __( 'Debug mode', 'woocommerce-payments' ) }

{ - const [ isSectionExpanded, toggleIsSectionExpanded ] = useToggle( false ); - return ( <> - - - - { isSectionExpanded && ( - - - - - - { wcpaySettings.isClientEncryptionEligible && ( - - ) } - - - - - - - ) } + + + + { wcpaySettings.isClientEncryptionEligible && ( + + ) } + { wcpaySettings.isSubscriptionsActive && + wcpaySettings.isStripeBillingEligible ? ( + + ) : ( + + ) } + + + ); }; diff --git a/client/settings/advanced-settings/interfaces.ts b/client/settings/advanced-settings/interfaces.ts new file mode 100644 index 00000000000..78cf985635d --- /dev/null +++ b/client/settings/advanced-settings/interfaces.ts @@ -0,0 +1,14 @@ +/** + * Interface exports + */ + +export type StripeBillingHook = [ boolean, ( value: boolean ) => void ]; + +export type StripeBillingMigrationHook = [ + boolean, + number, + number, + () => void, + boolean, + boolean +]; diff --git a/client/settings/advanced-settings/stripe-billing-notices/context.tsx b/client/settings/advanced-settings/stripe-billing-notices/context.tsx new file mode 100644 index 00000000000..a18da2178f8 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/context.tsx @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { createContext } from 'react'; + +const StripeBillingMigrationNoticeContext = createContext( { + isStripeBillingEnabled: false, + savedIsStripeBillingEnabled: false, + isMigrationOptionShown: false, + isMigrationInProgressShown: false, + isMigrationInProgress: false, + hasSavedSettings: false, + subscriptionCount: 0, + migratedCount: 0, + startMigration: () => null, + isResolvingMigrateRequest: false, + hasResolvedMigrateRequest: false, +} as { + isStripeBillingEnabled: boolean; + savedIsStripeBillingEnabled: boolean; + isMigrationOptionShown: boolean; + isMigrationInProgressShown: boolean; + isMigrationInProgress: boolean; + hasSavedSettings: boolean; + subscriptionCount: number; + migratedCount: number; + startMigration: () => void; + isResolvingMigrateRequest: boolean; + hasResolvedMigrateRequest: boolean; +} ); + +export default StripeBillingMigrationNoticeContext; diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx new file mode 100644 index 00000000000..5d58ef84438 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx @@ -0,0 +1,96 @@ +/** + * External dependencies + */ +import React, { useState, useContext, useEffect } from 'react'; +import InlineNotice from 'wcpay/components/inline-notice'; +import { _n, sprintf } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './context'; + +interface Props { + /** + * The number of subscriptions that will be automatically migrated. + */ + stripeBillingSubscriptionCount: number; +} + +const MigrateAutomaticallyNotice: React.FC< Props > = ( { + stripeBillingSubscriptionCount, +} ) => { + const context = useContext( StripeBillingMigrationNoticeContext ); + + /** + * Whether the notice is eligible to be shown. + * + * Note: We use `useState` here to snapshot the setting value on load. + * This notice should only be shown if Stripe Billing was enabled on load. + */ + const [ isEligible, setIsEligible ] = useState( + context.isStripeBillingEnabled + ); + + // Set the notice to be eligible if Stripe Billing is saved as enabled. ie Once saved, disabling will automatically migrate. + useEffect( () => { + if ( context.hasSavedSettings ) { + setIsEligible( context.savedIsStripeBillingEnabled ); + } + }, [ context.hasSavedSettings, context.savedIsStripeBillingEnabled ] ); + + if ( ! isEligible ) { + return null; + } + + // Don't show the notice if the migration option is shown. + if ( context.isMigrationOptionShown ) { + return null; + } + + // Don't show the notice if there are no Stripe Billing subscriptions to migrate. + if ( stripeBillingSubscriptionCount === 0 ) { + return null; + } + + if ( context.isStripeBillingEnabled ) { + return null; + } + + return ( + + { interpolateComponents( { + mixedString: sprintf( + _n( + 'There is currently %d customer subscription using Stripe Billing for payment processing.' + + ' This subscription will be automatically migrated to use the on-site billing engine' + + ' built into %s once Stripe Billing is disabled.' + + ' {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'There are currently %d customer subscriptions using Stripe Billing for payment processing.' + + ' These subscriptions will be automatically migrated to use the on-site billing engine' + + ' built into %s once Stripe Billing is disabled.' + + ' {{learnMoreLink}}Learn more{{/learnMoreLink}}', + stripeBillingSubscriptionCount, + 'woocommerce-payments' + ), + stripeBillingSubscriptionCount, + 'Woo Subscriptions' + ), + components: { + learnMoreLink: ( + // eslint-disable-next-line max-len + + ), + }, + } ) } + + ); +}; + +export default MigrateAutomaticallyNotice; diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-completed-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-completed-notice.tsx new file mode 100644 index 00000000000..701dbb14e19 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-completed-notice.tsx @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import React, { useState, useContext } from 'react'; +import InlineNotice from 'wcpay/components/inline-notice'; +import { _n, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './context'; + +interface Props { + /** + * The number of subscriptions that have been migrated. + */ + completedMigrationCount: number; +} + +const MigrationCompletedNotice: React.FC< Props > = ( { + completedMigrationCount, +} ) => { + const [ isDismissed, setIsDismissed ] = useState( false ); + const context = useContext( StripeBillingMigrationNoticeContext ); + + /** + * Whether the notice is eligible to be shown. + * + * Note: We use `useState` here to snapshot the setting value on load. + * This "completed" notice should only be shown if Stripe billing was disabled on load and there there's no migration in progress. + */ + const [ isEligible ] = useState( + ! context.isStripeBillingEnabled && ! context.isMigrationInProgress + ); + + if ( ! isEligible || isDismissed || completedMigrationCount === 0 ) { + return null; + } + + return ( + setIsDismissed( true ) } + className="woopayments-stripe-billing-notice" + > + { sprintf( + _n( + '%d customer subscription was successfully migrated from Stripe off-site billing to on-site billing' + + ' powered by %s and %s.', + '%d customer subscriptions were successfully migrated from Stripe off-site billing to on-site billing' + + ' powered by %s and %s.', + completedMigrationCount, + 'woocommerce-payments' + ), + completedMigrationCount, + 'Woo Subscriptions', + 'WooPayments' + ) } + + ); +}; + +export default MigrationCompletedNotice; diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx new file mode 100644 index 00000000000..5a32ee9f3ab --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx @@ -0,0 +1,145 @@ +/** + * External dependencies + */ +import React, { useContext, useState } from 'react'; +import InlineNotice from 'wcpay/components/inline-notice'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './context'; + +interface Props { + /** + * The number of subscriptions that will be migrated if a migration is started. + */ + stripeBillingSubscriptionCount: number; + + /** + * The function to call to start a migration. + */ + startMigration: () => void; + + /** + * Whether the request to start a migration is loading. + */ + isLoading: boolean; + + /** + * Whether the request to start a migration has finished. + */ + hasResolved: boolean; +} + +const MigrateOptionNotice: React.FC< Props > = ( { + stripeBillingSubscriptionCount, + startMigration, + isLoading, + hasResolved, +} ) => { + const context = useContext( StripeBillingMigrationNoticeContext ); + + /** + * Whether the notice is eligible to be shown. + * + * Note: We use `useState` here to snapshot the setting value on load. + * The option notice should only be shown if Stripe Billing is disabled on load and there are subscriptions to migrate. + */ + const [ isEligible, setIsEligible ] = useState( + ! context.isStripeBillingEnabled + ); + + // The class name of the action which sends the request to migrate. + const noticeClassName = 'woopayments-migrate-stripe-billing-action'; + + // Add the `is-busy` class to the button while we process the migrate request. + useEffect( () => { + const button = document.querySelector( + `.${ noticeClassName } .wcpay-inline-notice__action` + ); + + if ( button ) { + if ( isLoading ) { + button.classList.add( 'is-busy' ); + } else { + button.classList.remove( 'is-busy' ); + } + } + }, [ isLoading ] ); + + // The notice is no longer eligible if the settings have been saved and Stripe Billing is enabled. + useEffect( () => { + if ( context.savedIsStripeBillingEnabled ) { + setIsEligible( false ); + } + }, [ context.savedIsStripeBillingEnabled ] ); + + // Once the request is resolved, hide the notice and mark the migration as in progress. + if ( hasResolved ) { + context.isMigrationInProgress = true; + context.isMigrationOptionShown = false; + return null; + } + + if ( context.isMigrationInProgress ) { + return null; + } + + if ( stripeBillingSubscriptionCount === 0 ) { + return null; + } + + if ( ! isEligible ) { + return null; + } + + if ( context.isStripeBillingEnabled ) { + return null; + } + + // Update the context to note the Option Notice is being shown. + context.isMigrationOptionShown = true; + + return ( + + { interpolateComponents( { + mixedString: sprintf( + _n( + 'There is %d customer subscription using Stripe Billing for subscription renewals.' + + ' We suggest migrating it to on-site billing powered by the %s plugin.' + + ' {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'There are %d customer subscriptions using Stripe Billing for payment processing.' + + ' We suggest migrating them to on-site billing powered by the %s plugin.' + + ' {{learnMoreLink}}Learn more{{/learnMoreLink}}', + stripeBillingSubscriptionCount, + 'woocommerce-payments' + ), + stripeBillingSubscriptionCount, + 'Woo Subscriptions' + ), + components: { + learnMoreLink: ( + // eslint-disable-next-line max-len + + ), + }, + } ) } + + ); +}; + +export default MigrateOptionNotice; diff --git a/client/settings/advanced-settings/stripe-billing-notices/migration-progress-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migration-progress-notice.tsx new file mode 100644 index 00000000000..9079f3208d0 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/migration-progress-notice.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import React, { useState, useContext, useEffect } from 'react'; +import InlineNotice from 'wcpay/components/inline-notice'; +import { _n, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './context'; + +interface Props { + /** + * The number of subscriptions that are being migrated. + */ + stripeBillingSubscriptionCount: number; +} + +const MigrationInProgressNotice: React.FC< Props > = ( { + stripeBillingSubscriptionCount, +} ) => { + const [ isDismissed, setIsDismissed ] = useState( false ); + + const context = useContext( StripeBillingMigrationNoticeContext ); + + /** + * Whether the notice is eligible to be shown. + * + * Note: We use `useState` here to snapshot the setting value on load. + * + * This notice should only be shown if a migration is in progress. + * The migration is in progress if the settings have been saved and Stripe Billing is disabled or if the migration option is clicked. + */ + const [ isEligible, setIsEligible ] = useState( + context.isMigrationInProgress + ); + + // Set the notice to be eligible if the user has chosen to migrate. + useEffect( () => { + if ( context.hasResolvedMigrateRequest ) { + setIsEligible( true ); + } + }, [ context.hasResolvedMigrateRequest ] ); + + // Set the notice to be eligible if Stripe Billing is saved as disabled. When disabling Stripe Billing, the migration will automatically start. + useEffect( () => { + if ( context.hasSavedSettings ) { + setIsEligible( ! context.savedIsStripeBillingEnabled ); + } + }, [ context.hasSavedSettings, context.savedIsStripeBillingEnabled ] ); + + // Don't show the notice if it's not eligible. + if ( ! isEligible ) { + return null; + } + + // Don't show the notice if it has been dismissed. + if ( isDismissed ) { + return null; + } + + if ( context.subscriptionCount === 0 ) { + return null; + } + + // Don't show the notice if the migration option is shown. + if ( context.isMigrationOptionShown ) { + return null; + } + + // Mark the notice as shown. + context.isMigrationInProgressShown = true; + + return ( + setIsDismissed( true ) } + className="woopayments-stripe-billing-notice" + > + { sprintf( + _n( + '%d customer subscription is being migrated from Stripe off-site billing to billing powered by' + + ' %s and %s.', + '%d customer subscriptions are being migrated from Stripe off-site billing to billing powered by' + + ' %s and %s.', + stripeBillingSubscriptionCount, + 'woocommerce-payments' + ), + stripeBillingSubscriptionCount, + 'Woo Subscriptions', + 'WooPayments' + ) } + + ); +}; + +export default MigrationInProgressNotice; diff --git a/client/settings/advanced-settings/stripe-billing-notices/notices.tsx b/client/settings/advanced-settings/stripe-billing-notices/notices.tsx new file mode 100644 index 00000000000..5e4ba188375 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/notices.tsx @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import React, { useContext } from 'react'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './context'; +import MigrationInProgressNotice from './migration-progress-notice'; +import MigrateOptionNotice from './migrate-option-notice'; +import MigrateAutomaticallyNotice from './migrate-automatically-notice'; +import MigrationCompletedNotice from './migrate-completed-notice'; +import './style.scss'; + +/** + * Renders the Stripe Billing notices. + * + * @return {JSX.Element} Rendered notices. + */ +const Notices: React.FC = () => { + const context = useContext( StripeBillingMigrationNoticeContext ); + + return ( + <> + + { + context.startMigration(); + } } + isLoading={ context.isResolvingMigrateRequest } + hasResolved={ context.hasResolvedMigrateRequest } + /> + + + + ); +}; + +export default Notices; diff --git a/client/settings/advanced-settings/stripe-billing-notices/style.scss b/client/settings/advanced-settings/stripe-billing-notices/style.scss new file mode 100644 index 00000000000..01482a52ce6 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-notices/style.scss @@ -0,0 +1,4 @@ +.wcpay-inline-notice.components-notice.woopayments-stripe-billing-notice { + margin-top: 0; + margin-bottom: 1em; +} diff --git a/client/settings/advanced-settings/stripe-billing-section.tsx b/client/settings/advanced-settings/stripe-billing-section.tsx new file mode 100644 index 00000000000..bd0e3ce270f --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-section.tsx @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import React, { useState, useEffect } from 'react'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { + useStripeBilling, + useStripeBillingMigration, + useSettings, +} from 'wcpay/data'; +import Notices from './stripe-billing-notices/notices'; +import StripeBillingMigrationNoticeContext from './stripe-billing-notices/context'; +import StripeBillingToggle from './stripe-billing-toggle'; +import { StripeBillingHook, StripeBillingMigrationHook } from './interfaces'; + +/** + * Renders a WooPayments Subscriptions Advanced Settings Section. + * + * @return {JSX.Element} Rendered subscriptions advanced settings section. + */ +const StripeBillingSection: React.FC = () => { + const [ + isStripeBillingEnabled, + updateIsStripeBillingEnabled, + ] = useStripeBilling() as StripeBillingHook; + const [ + isMigrationInProgress, + migratedCount, + subscriptionCount, + startMigration, + isResolving, + hasResolved, + ] = useStripeBillingMigration() as StripeBillingMigrationHook; + + /** + * Notices are shown and hidden based on whether the settings have been saved. + * The following variables track the saving state of the WooPayments settings. + */ + const { isLoading, isSaving } = useSettings(); + const [ hasSavedSettings, setHasSavedSettings ] = useState( false ); + const [ + savedIsStripeBillingEnabled, + setSavedIsStripeBillingEnabled, + ] = useState( isStripeBillingEnabled ); + + // The settings have finished saving when the settings are not actively being saved and we've flagged they were being saved. + const hasFinishedSavingSettings = ! isSaving && hasSavedSettings; + + // When the settings are being saved, set the hasSavedSettings flag to true. + useEffect( () => { + if ( isSaving && ! isLoading ) { + setHasSavedSettings( true ); + } + }, [ isLoading, isSaving ] ); + + // When the settings have finished saving, update the savedIsStripeBillingEnabled value. + useEffect( () => { + if ( hasFinishedSavingSettings ) { + setSavedIsStripeBillingEnabled( isStripeBillingEnabled ); + } + }, [ hasFinishedSavingSettings, isStripeBillingEnabled ] ); + + // Set up the context to be shared between the notices and the toggle. + const [ isMigrationInProgressShown ] = useState( false ); + const [ isMigrationOptionShown ] = useState( false ); + + const noticeContext = { + isStripeBillingEnabled: isStripeBillingEnabled, + savedIsStripeBillingEnabled: savedIsStripeBillingEnabled, + + // Notice logic. + isMigrationOptionShown: isMigrationOptionShown, + isMigrationInProgressShown: isMigrationInProgressShown, + + // Migration logic. + isMigrationInProgress: isMigrationInProgress, + hasSavedSettings: hasFinishedSavingSettings, + + // Migration data. + subscriptionCount: subscriptionCount, + migratedCount: migratedCount, + + // Migration actions & state. + startMigration: startMigration, + isResolvingMigrateRequest: isResolving, + hasResolvedMigrateRequest: hasResolved, + }; + + // When the toggle is changed, update the WooPayments settings and reset the hasSavedSettings flag. + const stripeBillingSettingToggle = ( enabled: boolean ) => { + updateIsStripeBillingEnabled( enabled ); + setHasSavedSettings( false ); + }; + + return ( + +

{ __( 'Subscriptions', 'woocommerce-payments' ) }

+ + +
+ ); +}; + +export default StripeBillingSection; diff --git a/client/settings/advanced-settings/stripe-billing-toggle.tsx b/client/settings/advanced-settings/stripe-billing-toggle.tsx new file mode 100644 index 00000000000..4f8dea69584 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-toggle.tsx @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import React, { useContext } from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import { CheckboxControl, ExternalLink } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './stripe-billing-notices/context'; + +interface Props { + /** + * The function to run when the checkbox is changed. + */ + onChange: ( enabled: boolean ) => void; +} + +/** + * Renders the Stripe Billing toggle. + * + * @return {JSX.Element} Rendered Stripe Billing toggle. + */ +const StripeBillingToggle: React.FC< Props > = ( { onChange } ) => { + const context = useContext( StripeBillingMigrationNoticeContext ); + + return ( + + ), + }, + } ) } + /> + ); +}; + +export default StripeBillingToggle; diff --git a/client/settings/advanced-settings/test/debug-mode.test.js b/client/settings/advanced-settings/test/debug-mode.test.js index abd59036994..54877787745 100644 --- a/client/settings/advanced-settings/test/debug-mode.test.js +++ b/client/settings/advanced-settings/test/debug-mode.test.js @@ -22,12 +22,6 @@ describe( 'DebugMode', () => { jest.clearAllMocks(); } ); - it( 'sets the heading as focused after rendering', () => { - render( ); - - expect( screen.getByText( 'Debug mode' ) ).toHaveFocus(); - } ); - it( 'toggles the logging checkbox', () => { const setDebugLogMock = jest.fn(); useDebugLog.mockReturnValue( [ false, setDebugLogMock ] ); diff --git a/client/settings/advanced-settings/test/index.test.js b/client/settings/advanced-settings/test/index.test.js index 05be0739f75..517f237969e 100644 --- a/client/settings/advanced-settings/test/index.test.js +++ b/client/settings/advanced-settings/test/index.test.js @@ -4,44 +4,59 @@ * External dependencies */ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ import AdvancedSettings from '..'; +import { + useMultiCurrency, + useWCPaySubscriptions, + useDevMode, + useDebugLog, + useClientSecretEncryption, +} from 'wcpay/data'; + +jest.mock( '../../../data', () => ( { + useSettings: jest.fn(), + useMultiCurrency: jest.fn(), + useWCPaySubscriptions: jest.fn(), + useDevMode: jest.fn(), + useDebugLog: jest.fn(), + useClientSecretEncryption: jest.fn(), +} ) ); describe( 'AdvancedSettings', () => { - it( 'toggles the advanced settings section', () => { + beforeEach( () => { + useMultiCurrency.mockReturnValue( [ false, jest.fn() ] ); + useWCPaySubscriptions.mockReturnValue( [ false, jest.fn() ] ); + useDevMode.mockReturnValue( false ); + useDebugLog.mockReturnValue( [ false, jest.fn() ] ); + useClientSecretEncryption.mockReturnValue( [ false, jest.fn() ] ); + } ); + test( 'toggles the advanced settings section', () => { global.wcpaySettings = { isClientEncryptionEligible: true, }; - render( ); - expect( screen.queryByText( 'Debug mode' ) ).not.toBeInTheDocument(); - - userEvent.click( screen.getByText( 'Advanced settings' ) ); + render( ); + // The advanced settings section is expanded by default. expect( screen.queryByText( 'Enable Public Key Encryption' ) ).toBeInTheDocument(); expect( screen.queryByText( 'Debug mode' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Debug mode' ) ).toHaveFocus(); } ); - it( 'hides the client encryption toggle when not eligible', () => { + test( 'hides the client encryption toggle when not eligible', () => { global.wcpaySettings = { isClientEncryptionEligible: false, }; - render( ); - - expect( screen.queryByText( 'Debug mode' ) ).not.toBeInTheDocument(); - userEvent.click( screen.getByText( 'Advanced settings' ) ); + render( ); expect( screen.queryByText( 'Enable Public Key Encryption' ) ).not.toBeInTheDocument(); expect( screen.queryByText( 'Debug mode' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Debug mode' ) ).toHaveFocus(); } ); } ); diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index fd777c20008..e2f921c990c 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -15,7 +15,6 @@ const WCPaySubscriptionsToggle = () => { const [ isWCPaySubscriptionsEnabled, isWCPaySubscriptionsEligible, - isSubscriptionsPluginActive, updateIsWCPaySubscriptionsEnabled, ] = useWCPaySubscriptions(); @@ -31,8 +30,11 @@ const WCPaySubscriptionsToggle = () => { updateIsWCPaySubscriptionsEnabled( value ); }; - return ! isSubscriptionsPluginActive && - ( isWCPaySubscriptionsEligible || isWCPaySubscriptionsEnabled ) ? ( + /** + * Only show the toggle if the site is eligible for wcpay subscriptions or + * if wcpay subscriptions are already enabled. + */ + return isWCPaySubscriptionsEligible || isWCPaySubscriptionsEnabled ? ( { ); }; +const AdvancedDescription = () => { + return ( + <> +

{ __( 'Advanced settings', 'woocommerce-payments' ) }

+

+ { __( + 'More options for specific payment needs.', + 'woocommerce-payments' + ) } +

+ + { __( 'View our documentation', 'woocommerce-payments' ) } + + + ); +}; + const SettingsManager = () => { const { featureFlags: { @@ -252,7 +269,16 @@ const SettingsManager = () => { - + + + + + + + ); diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 6cb36ed67d7..c8434190b7a 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -835,6 +835,8 @@ private function get_js_settings(): array { 'storeCurrency' => get_option( 'woocommerce_currency' ), 'isBnplAffirmAfterpayEnabled' => WC_Payments_Features::is_bnpl_affirm_afterpay_enabled(), 'isWooPayStoreCountryAvailable' => WooPay_Utilities::is_store_country_available(), + 'isStripeBillingEnabled' => WC_Payments_Features::is_stripe_billing_enabled(), + 'isStripeBillingEligible' => WC_Payments_Features::is_stripe_billing_eligible(), ]; return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); diff --git a/includes/admin/class-wc-rest-payments-settings-controller.php b/includes/admin/class-wc-rest-payments-settings-controller.php index 1742491563b..ae5ca0361d2 100644 --- a/includes/admin/class-wc-rest-payments-settings-controller.php +++ b/includes/admin/class-wc-rest-payments-settings-controller.php @@ -271,9 +271,38 @@ public function register_routes() { 'default' => array_keys( $wcpay_form_fields['payment_request_button_locations']['options'] ), 'validate_callback' => 'rest_validate_request_arg', ], + 'is_stripe_billing_enabled' => [ + 'description' => __( 'If Stripe Billing is enabled.', 'woocommerce-payments' ), + 'type' => 'boolean', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'is_migrating_stripe_billing' => [ + 'description' => __( 'Whether there is a Stripe Billing off-site to on-site billing migration in progress.', 'woocommerce-payments' ), + 'type' => 'boolean', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'stripe_billing_subscription_count' => [ + 'description' => __( 'The number of subscriptions using Stripe Billing', 'woocommerce-payments' ), + 'type' => 'int', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'stripe_billing_migrated_count' => [ + 'description' => __( 'The number of subscriptions migrated from Stripe Billing to on-site billing.', 'woocommerce-payments' ), + 'type' => 'int', + 'validate_callback' => 'rest_validate_request_arg', + ], ], ] ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/schedule-stripe-billing-migration', + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'schedule_stripe_billing_migration' ], + 'permission_callback' => [ $this, 'check_permission' ], + ] + ); } /** @@ -410,6 +439,20 @@ public function get_settings(): WP_REST_Response { ) ); + // Gather the status of the Stripe Billing migration for use on the settings page. + if ( class_exists( 'WC_Subscriptions' ) ) { + $stripe_billing_migrated_count = $this->wcpay_gateway->get_subscription_migrated_count(); + + if ( class_exists( 'WC_Payments_Subscriptions' ) ) { + $stripe_billing_migrator = WC_Payments_Subscriptions::get_stripe_billing_migrator(); + + if ( $stripe_billing_migrator ) { + $is_migrating_stripe_billing = $stripe_billing_migrator->is_migrating(); + $stripe_billing_subscription_count = $stripe_billing_migrator->get_stripe_billing_subscription_count(); + } + } + } + return new WP_REST_Response( [ 'enabled_payment_method_ids' => $enabled_payment_methods, @@ -422,6 +465,7 @@ public function get_settings(): WP_REST_Response { 'is_multi_currency_enabled' => WC_Payments_Features::is_customer_multi_currency_enabled(), 'is_client_secret_encryption_enabled' => WC_Payments_Features::is_client_secret_encryption_enabled(), 'is_wcpay_subscriptions_enabled' => WC_Payments_Features::is_wcpay_subscriptions_enabled(), + 'is_stripe_billing_enabled' => WC_Payments_Features::is_stripe_billing_enabled(), 'is_wcpay_subscriptions_eligible' => WC_Payments_Features::is_wcpay_subscriptions_eligible(), 'is_subscriptions_plugin_active' => $this->wcpay_gateway->is_subscriptions_plugin_active(), 'account_country' => $this->wcpay_gateway->get_option( 'account_country' ), @@ -460,6 +504,9 @@ public function get_settings(): WP_REST_Response { 'deposit_completed_waiting_period' => $this->wcpay_gateway->get_option( 'deposit_completed_waiting_period' ), 'current_protection_level' => $this->wcpay_gateway->get_option( 'current_protection_level' ), 'advanced_fraud_protection_settings' => $this->wcpay_gateway->get_option( 'advanced_fraud_protection_settings' ), + 'is_migrating_stripe_billing' => $is_migrating_stripe_billing ?? false, + 'stripe_billing_subscription_count' => $stripe_billing_subscription_count ?? 0, + 'stripe_billing_migrated_count' => $stripe_billing_migrated_count ?? 0, ] ); } @@ -490,6 +537,7 @@ public function update_settings( WP_REST_Request $request ) { // Note: Both "current_protection_level" and "advanced_fraud_protection_settings" // are handled in the below method. $this->update_fraud_protection_settings( $request ); + $this->update_is_stripe_billing_enabled( $request ); return new WP_REST_Response( [], 200 ); } @@ -897,6 +945,49 @@ private function update_fraud_protection_settings( WP_REST_Request $request ) { update_option( 'current_protection_level', $protection_level ); } + /** + * Updates the Stripe Billing Subscriptions feature status. + * + * @param WP_REST_Request $request Request object. + */ + private function update_is_stripe_billing_enabled( WP_REST_Request $request ) { + if ( ! $request->has_param( 'is_stripe_billing_enabled' ) ) { + return; + } + + $is_stripe_billing_enabled = $request->get_param( 'is_stripe_billing_enabled' ); + + update_option( WC_Payments_Features::STRIPE_BILLING_FLAG_NAME, $is_stripe_billing_enabled ? '1' : '0' ); + + // Schedule a migration if Stripe Billing was disabled and there are subscriptions to migrate. + if ( ! $is_stripe_billing_enabled ) { + $this->schedule_stripe_billing_migration(); + } + } + + /** + * Schedule a migration of Stripe Billing subscriptions. + * + * @param WP_REST_Request $request The request object. Optional. If passed, the function will return a REST response. + * + * @return WP_REST_Response|null The response object, if this is a REST request. + */ + public function schedule_stripe_billing_migration( WP_REST_Request $request = null ) { + + if ( class_exists( 'WC_Payments_Subscriptions' ) ) { + $stripe_billing_migrator = WC_Payments_Subscriptions::get_stripe_billing_migrator(); + + if ( $stripe_billing_migrator && ! $stripe_billing_migrator->is_migrating() && $stripe_billing_migrator->get_stripe_billing_subscription_count() > 0 ) { + $stripe_billing_migrator->schedule_migrate_wcpay_subscriptions_action(); + } + } + + // Return a response if this is a REST request. + if ( $request ) { + return new WP_REST_Response( [], 200 ); + } + } + /** * Get the AVS check enabled status from the ruleset config. * diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 7a50b57b9c4..ccee24b21ad 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -782,8 +782,7 @@ public function should_use_new_process( WC_Order $order ) { if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) - && WC_Payments_Features::is_wcpay_subscriptions_enabled() - && ! $this->is_subscriptions_plugin_active() + && WC_Payments_Features::should_use_stripe_billing() ) { $factors[] = Factor::WCPAY_SUBSCRIPTION_SIGNUP(); } diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index cdb963eb5bc..08485ac5e26 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -17,6 +17,7 @@ class WC_Payments_Features { const UPE_SPLIT_FLAG_NAME = '_wcpay_feature_upe_split'; const UPE_DEFERRED_INTENT_FLAG_NAME = '_wcpay_feature_upe_deferred_intent'; const WCPAY_SUBSCRIPTIONS_FLAG_NAME = '_wcpay_feature_subscriptions'; + const STRIPE_BILLING_FLAG_NAME = '_wcpay_feature_stripe_billing'; const WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_express_checkout'; const AUTH_AND_CAPTURE_FLAG_NAME = '_wcpay_feature_auth_and_capture'; const PROGRESSIVE_ONBOARDING_FLAG_NAME = '_wcpay_feature_progressive_onboarding'; @@ -322,6 +323,51 @@ public static function is_bnpl_affirm_afterpay_enabled(): bool { return ! isset( $account['is_bnpl_affirm_afterpay_enabled'] ) || true === $account['is_bnpl_affirm_afterpay_enabled']; } + /** + * Checks whether the Stripe Billing feature is enabled. + * + * @return bool + */ + public static function is_stripe_billing_enabled(): bool { + return '1' === get_option( self::STRIPE_BILLING_FLAG_NAME, '0' ); + } + + /** + * Checks if the site is eligible for Stripe Billing. + * + * Only US merchants are eligible for Stripe Billing. + * + * @return bool + */ + public static function is_stripe_billing_eligible() { + if ( ! function_exists( 'wc_get_base_location' ) ) { + return false; + } + + $store_base_location = wc_get_base_location(); + return ! empty( $store_base_location['country'] ) && 'US' === $store_base_location['country']; + } + + /** + * Checks whether the merchant is using WCPay Subscription or opted into Stripe Billing. + * + * Note: Stripe Billing is only used when the merchant is using WooCommerce Subscriptions and turned it on or is still using WCPay Subscriptions. + * + * @return bool + */ + public static function should_use_stripe_billing() { + // We intentionally check for the existence of the 'WC_Subscriptions' class here as we want to confirm the Plugin is active. + if ( self::is_wcpay_subscriptions_enabled() && ! class_exists( 'WC_Subscriptions' ) ) { + return true; + } + + if ( self::is_stripe_billing_enabled() && class_exists( 'WC_Subscriptions' ) ) { + return true; + } + + return false; + } + /** * Returns feature flags as an array suitable for display on the front-end. * diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index b77b3589e0a..c8c33814845 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -624,8 +624,8 @@ public static function init() { new WCPay\Fraud_Prevention\Order_Fraud_And_Risk_Meta_Box( self::$order_service ); } - // Load WCPay Subscriptions. - if ( self::should_load_wcpay_subscriptions() ) { + // Load Stripe Billing subscription integration. + if ( self::should_load_stripe_billing_integration() ) { include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions.php'; WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account ); } @@ -1708,19 +1708,37 @@ public static function wcpay_show_old_woocommerce_for_hungary_sweden_and_czech_r } /** - * Determines whether we should load WCPay Subscription related classes. + * Determines whether we should load Stripe Billing integration classes. * * Return true when: - * - the WCPay Subscriptions feature flag is enabled, or - * - the migration feature flag is enabled && the store has WC Subscriptions activated + * - the WCPay Subscriptions feature is enabled & the Woo Subscriptions plugin isn't active, or + * - Woo Subscriptions plugin is active and Stripe Billing is enabled or there are Stripe Billing Subscriptions. + * + * @see WC_Payments_Features::should_use_stripe_billing() * * @return bool */ - private static function should_load_wcpay_subscriptions() { - if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() ) { + private static function should_load_stripe_billing_integration() { + if ( WC_Payments_Features::should_use_stripe_billing() ) { return true; } - return WC_Payments_Features::is_subscription_migration_enabled() && class_exists( 'WC_Subscriptions' ); + // If there are any Stripe Billing Subscriptions, we should load the Stripe Billing integration classes. eg while a migration is in progress, or to support legacy subscriptions. + return function_exists( 'wcs_get_orders_with_meta_query' ) && (bool) count( + wcs_get_orders_with_meta_query( + [ + 'status' => 'any', + 'return' => 'ids', + 'type' => 'shop_subscription', + 'limit' => 1, // We only need to know if there are any - at least 1. + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => '_wcpay_subscription_id', + 'compare' => 'EXISTS', + ], + ], + ] + ) + ); } } diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php index eed8333015a..a96963d8022 100644 --- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php +++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php @@ -127,10 +127,10 @@ public function maybe_init_subscriptions() { 'subscriptions', ]; - if ( $this->is_subscriptions_plugin_active() ) { + if ( ! WC_Payments_Features::should_use_stripe_billing() ) { /* * Subscription amount & date changes are only supported - * when WooCommerce Subscriptions is active. + * when Stripe Billing is not in use. */ $payment_gateway_features = array_merge( $payment_gateway_features, @@ -855,12 +855,17 @@ public function update_subscription_token( $updated, $subscription, $new_token ) * Checks if a renewal order is linked to a WCPay subscription. * * @param WC_Order $renewal_order The renewal order to check. + * * @return bool True if the renewal order is linked to a renewal order. Otherwise false. */ private function is_wcpay_subscription_renewal_order( WC_Order $renewal_order ) { - - // Exit early if WCPay subscriptions functionality isn't enabled. - if ( ! WC_Payments_Features::is_wcpay_subscriptions_enabled() ) { + /** + * Check if WC_Payments_Subscription_Service class exists first before fetching the subscription for the renewal order. + * + * This class is only loaded when the store has the Stripe Billing feature turned on or has existing + * WCPay Subscriptions @see WC_Payments::should_load_stripe_billing_integration(). + */ + if ( ! class_exists( 'WC_Payments_Subscription_Service' ) ) { return false; } diff --git a/includes/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php b/includes/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php index 53a53ee2e14..b7c70fb13bb 100644 --- a/includes/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php +++ b/includes/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php @@ -133,4 +133,32 @@ public function get_subscriptions_core_version() { } return $subscriptions_core_instance ? $subscriptions_core_instance->get_plugin_version() : null; } + + /** + * Gets the total number of subscriptions that have already been migrated. + * + * @return int The total number of subscriptions migrated. + */ + public function get_subscription_migrated_count() { + if ( ! function_exists( 'wcs_get_orders_with_meta_query' ) ) { + return 0; + } + + return count( + wcs_get_orders_with_meta_query( + [ + 'status' => 'any', + 'return' => 'ids', + 'type' => 'shop_subscription', + 'limit' => -1, + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => '_migrated_wcpay_subscription_id', + 'compare' => 'EXISTS', + ], + ], + ] + ) + ); + } } diff --git a/includes/subscriptions/class-wc-payments-product-service.php b/includes/subscriptions/class-wc-payments-product-service.php index 995fa4a7478..2c6e12bccc4 100644 --- a/includes/subscriptions/class-wc-payments-product-service.php +++ b/includes/subscriptions/class-wc-payments-product-service.php @@ -92,8 +92,8 @@ public function __construct( WC_Payments_API_Client $payments_api_client ) { return; } - // Only create, update and restore/unarchive WCPay Subscription products when the WC Subscriptions plugin is not active. - if ( ! $this->is_subscriptions_plugin_active() ) { + // Only create, update and restore/unarchive WCPay Subscription products when Stripe Billing is active. + if ( WC_Payments_Features::should_use_stripe_billing() ) { add_action( 'shutdown', [ $this, 'create_or_update_products' ] ); add_action( 'untrashed_post', [ $this, 'maybe_unarchive_product' ] ); diff --git a/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php b/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php index 60ff2e0e86d..21f28d8bf81 100644 --- a/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php +++ b/includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php @@ -41,7 +41,7 @@ class WC_Payments_Subscription_Minimum_Amount_Handler { public function __construct( WC_Payments_API_Client $api_client ) { $this->api_client = $api_client; - if ( ! $this->is_subscriptions_plugin_active() ) { + if ( WC_Payments_Features::should_use_stripe_billing() ) { add_filter( 'woocommerce_subscriptions_minimum_processable_recurring_amount', [ $this, 'get_minimum_recurring_amount' ], 10, 2 ); } } diff --git a/includes/subscriptions/class-wc-payments-subscription-service.php b/includes/subscriptions/class-wc-payments-subscription-service.php index a30a2413067..c4b91bac644 100644 --- a/includes/subscriptions/class-wc-payments-subscription-service.php +++ b/includes/subscriptions/class-wc-payments-subscription-service.php @@ -137,7 +137,7 @@ public function __construct( return; } - if ( ! $this->is_subscriptions_plugin_active() ) { + if ( WC_Payments_Features::should_use_stripe_billing() ) { add_action( 'woocommerce_checkout_subscription_created', [ $this, 'create_subscription' ] ); add_action( 'woocommerce_renewal_order_payment_complete', [ $this, 'create_subscription_for_manual_renewal' ] ); add_action( 'woocommerce_subscription_payment_method_updated', [ $this, 'maybe_create_subscription_from_update_payment_method' ], 10, 2 ); @@ -637,12 +637,23 @@ public function maybe_attempt_payment_for_subscription( $subscription, WC_Paymen * @return bool */ public function prevent_wcpay_subscription_changes( bool $supported, string $feature, WC_Subscription $subscription ) { + $is_stripe_billing = self::is_wcpay_subscription( $subscription ); - if ( ! self::is_wcpay_subscription( $subscription ) ) { - return $supported; + switch ( $feature ) { + case 'subscription_amount_changes': + case 'subscription_date_changes': + $supported = ! $is_stripe_billing; + break; + case 'gateway_scheduled_payments': + $supported = $is_stripe_billing; + break; + } + + if ( $is_stripe_billing ) { + $supported = in_array( $feature, $this->supports, true ) || isset( $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] ); } - return in_array( $feature, $this->supports, true ) || isset( $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] ); + return $supported; } /** diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index 7de060a8c8f..f36ea22ac0e 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -354,34 +354,20 @@ public function add_manual_migration_tool( $tools ) { } // Get number of WCPay Subscriptions that can be migrated. - $wcpay_subscriptions_count = count( - wcs_get_orders_with_meta_query( - [ - 'status' => 'any', - 'return' => 'ids', - 'type' => 'shop_subscription', - 'limit' => -1, - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - [ - 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, - 'compare' => 'EXISTS', - ], - ], - ] - ) - ); + $wcpay_subscriptions_count = $this->get_stripe_billing_subscription_count(); if ( $wcpay_subscriptions_count < 1 ) { return $tools; } - $disabled = as_next_scheduled_action( $this->scheduled_hook ); + // Disable the button if a migration is currently in progress. + $disabled = $this->is_migrating(); $tools['migrate_wcpay_subscriptions'] = [ 'name' => __( 'Migrate Stripe Billing subscriptions', 'woocommerce-payments' ), 'button' => $disabled ? __( 'Migration in progress', 'woocommerce-payments' ) . '…' : __( 'Migrate Subscriptions', 'woocommerce-payments' ), 'desc' => sprintf( - // translators: %1$s is a new line character and %3$d is the number of subscriptions. + // translators: %1$s is a new line character and %2$d is the number of subscriptions. __( 'This tool will migrate all Stripe Billing subscriptions to tokenized subscriptions with WooPayments.%1$sNumber of Stripe Billing subscriptions found: %2$d', 'woocommerce-payments' ), '
', $wcpay_subscriptions_count, @@ -499,6 +485,42 @@ public function get_items_to_repair( $page ) { return $items_to_migrate; } + /** + * Gets the total number of subscriptions to migrate. + * + * @return int The total number of subscriptions to migrate. + */ + public function get_stripe_billing_subscription_count() { + return count( + wcs_get_orders_with_meta_query( + [ + 'status' => 'any', + 'return' => 'ids', + 'type' => 'shop_subscription', + 'limit' => -1, + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, + 'compare' => 'EXISTS', + ], + ], + ] + ) + ); + } + + /** + * Determines if a migration is currently in progress. + * + * A migration is considered to be in progress if either the initial migration action or an individual subscription + * actions are scheduled. + * + * @return bool True if a migration is in progress, false otherwise. + */ + public function is_migrating() { + return is_numeric( as_next_scheduled_action( $this->scheduled_hook ) ) || is_numeric( as_next_scheduled_action( $this->migrate_hook ) ) || is_numeric( as_next_scheduled_action( $this->migrate_hook . '_retry' ) ); + } + /** * Runs any actions that need to handle the completion of the migration. */ diff --git a/includes/subscriptions/class-wc-payments-subscriptions.php b/includes/subscriptions/class-wc-payments-subscriptions.php index 69de9ac83e0..c30c6248d5c 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions.php +++ b/includes/subscriptions/class-wc-payments-subscriptions.php @@ -49,6 +49,13 @@ class WC_Payments_Subscriptions { */ private static $event_handler; + /** + * Instance of WC_Payments_Subscriptions_Migrator, created in init function. + * + * @var WC_Payments_Subscriptions_Migrator + */ + private static $stripe_billing_migrator; + /** * Initialize WooCommerce Payments subscriptions. (Stripe Billing) * @@ -84,9 +91,9 @@ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Cus new WC_Payments_Subscriptions_Onboarding_Handler( $account ); new WC_Payments_Subscription_Minimum_Amount_Handler( $api_client ); - if ( WC_Payments_Features::is_subscription_migration_enabled() && class_exists( 'WCS_Background_Repairer' ) ) { + if ( class_exists( 'WCS_Background_Repairer' ) ) { include_once __DIR__ . '/class-wc-payments-subscriptions-migrator.php'; - new WC_Payments_Subscriptions_Migrator( $api_client ); + self::$stripe_billing_migrator = new WC_Payments_Subscriptions_Migrator( $api_client ); } } @@ -126,6 +133,15 @@ public static function get_subscription_service() { return self::$subscription_service; } + /** + * Returns the the Stripe Billing migrator instance. + * + * @return WC_Payments_Subscriptions_Migrator + */ + public static function get_stripe_billing_migrator() { + return self::$stripe_billing_migrator; + } + /** * Determines if this is a duplicate/staging site. * diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 75fa2eac0f1..07c08ef26c9 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -88,4 +88,16 @@ CheckoutSchema + + + WC_Payments_Subscriptions::get_stripe_billing_migrator() + $stripe_billing_migrator + $stripe_billing_migrator + $stripe_billing_migrator + WC_Payments_Subscriptions::get_stripe_billing_migrator() + $stripe_billing_migrator + $stripe_billing_migrator + $stripe_billing_migrator + + diff --git a/tests/unit/helpers/class-wc-helper-subscriptions.php b/tests/unit/helpers/class-wc-helper-subscriptions.php index fa40ca58aeb..3d361a6446d 100644 --- a/tests/unit/helpers/class-wc-helper-subscriptions.php +++ b/tests/unit/helpers/class-wc-helper-subscriptions.php @@ -83,6 +83,13 @@ function wcs_order_contains_renewal() { return ( WC_Subscriptions::$wcs_order_contains_renewal )(); } +function wcs_get_orders_with_meta_query( $args ) { + if ( ! WC_Subscriptions::$wcs_get_orders_with_meta_query ) { + return []; + } + return ( WC_Subscriptions::$wcs_get_orders_with_meta_query )( $args ); +} + /** * Class WC_Subscriptions. * @@ -166,6 +173,13 @@ class WC_Subscriptions { */ public static $wcs_create_renewal_order = null; + /** + * wcs_get_orders_with_meta_query mock. + * + * @var function + */ + public static $wcs_get_orders_with_meta_query = null; + /** * wcs_order_contains_renewal mock. * diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-trait.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-trait.php index 68c43ee6355..b888bf80cab 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-trait.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-trait.php @@ -76,7 +76,7 @@ public function test_maybe_init_subscriptions_with_wcs_enabled() { $this->assertSame( $expected, $this->mock_wcpay_subscriptions_trait->supports ); } - public function test_maybe_init_subscriptions_with_wcs_disabled() { + public function test_maybe_init_subscriptions_with_stripe_billing_enabled() { $this->mock_wcpay_subscriptions_trait ->method( 'is_subscriptions_enabled' ) ->willReturn( true ); @@ -85,6 +85,8 @@ public function test_maybe_init_subscriptions_with_wcs_disabled() { ->method( 'is_subscriptions_plugin_active' ) ->willReturn( false ); + update_option( '_wcpay_feature_stripe_billing', '1' ); + $this->mock_wcpay_subscriptions_trait->maybe_init_subscriptions(); $expected = [ @@ -100,5 +102,7 @@ public function test_maybe_init_subscriptions_with_wcs_disabled() { ]; $this->assertSame( $expected, $this->mock_wcpay_subscriptions_trait->supports ); + + delete_option( '_wcpay_feature_stripe_billing' ); } } From 7e61084222bf2f41f384e8b01e78477f64b78fd3 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Mon, 11 Sep 2023 10:55:17 +0100 Subject: [PATCH 40/84] Updates to the Inbox Note logic. (#7136) --- changelog/dev-6441-inbox-notifications-update | 4 +++ includes/class-wc-payments-account.php | 11 ++++++ ...ments-notes-additional-payment-methods.php | 14 ++++++-- ...yments-notes-instant-deposits-eligible.php | 2 +- ...ments-notes-additional-payment-methods.php | 36 +++++++++++++++---- 5 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 changelog/dev-6441-inbox-notifications-update diff --git a/changelog/dev-6441-inbox-notifications-update b/changelog/dev-6441-inbox-notifications-update new file mode 100644 index 00000000000..b93787b65af --- /dev/null +++ b/changelog/dev-6441-inbox-notifications-update @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Update inbox note logic to prevent prompt to set up payment methods from showing on not fully onboarded account. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 82eb9989117..8fc2184dffd 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -499,6 +499,17 @@ public function get_progressive_onboarding_details(): array { ]; } + /** + * Determine whether Progressive Onboarding is in progress for this account. + * + * @return boolean + */ + public function is_progressive_onboarding_in_progress(): bool { + $account = $this->get_cached_account_data(); + return $account['progressive_onboarding']['is_enabled'] ?? false + && ! $account['progressive_onboarding']['is_complete'] ?? false; + } + /** * Gets the current account loan data for rendering on the settings pages. * diff --git a/includes/notes/class-wc-payments-notes-additional-payment-methods.php b/includes/notes/class-wc-payments-notes-additional-payment-methods.php index 20a0cd009d2..0341eccc28c 100644 --- a/includes/notes/class-wc-payments-notes-additional-payment-methods.php +++ b/includes/notes/class-wc-payments-notes-additional-payment-methods.php @@ -11,8 +11,6 @@ defined( 'ABSPATH' ) || exit; -use WCPay\Tracker; - /** * Class WC_Payments_Notes_Additional_Payment_Methods */ @@ -53,11 +51,21 @@ public static function get_note() { return; } - // if the user hasn't connected their account (or the account got disconnected) do not add the note. if ( self::$account instanceof WC_Payments_Account ) { + // if the user hasn't connected their account, do not add the note. if ( ! self::$account->is_stripe_connected() ) { return; } + + // If the account hasn't completed intitial Stripe onboarding, do not add the note. + if ( self::$account->is_account_partially_onboarded() ) { + return; + } + + // If this is a PO account which has not yet completed full onboarding, do not add the note. + if ( self::$account->is_progressive_onboarding_in_progress() ) { + return; + } } $note = new Note(); diff --git a/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php b/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php index 9b4fff947c9..af86a20d16e 100644 --- a/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php +++ b/includes/notes/class-wc-payments-notes-instant-deposits-eligible.php @@ -11,7 +11,7 @@ defined( 'ABSPATH' ) || exit; /** - * Class WC_Payments_Notes_Set_Https_For_Checkout + * Class WC_Payments_Notes_Instant_Deposits_Eligible */ class WC_Payments_Notes_Instant_Deposits_Eligible { use NoteTraits; diff --git a/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php b/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php index 50c2a12ce45..0c9dae3b18a 100644 --- a/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php +++ b/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php @@ -59,12 +59,11 @@ public function test_get_note_does_not_return_note_when_account_is_not_connected } public function test_get_note_returns_note_when_account_is_connected() { - $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected' ] )->getMock(); - $account_mock->expects( $this->atLeastOnce() )->method( 'is_stripe_connected' )->will( - $this->returnValue( - true - ) - ); + $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'is_account_partially_onboarded', 'is_progressive_onboarding_in_progress' ] )->getMock(); + $account_mock->expects( $this->once() )->method( 'is_stripe_connected' )->willReturn( true ); + $account_mock->expects( $this->once() )->method( 'is_account_partially_onboarded' )->willReturn( false ); + $account_mock->expects( $this->once() )->method( 'is_progressive_onboarding_in_progress' )->willReturn( false ); + WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); @@ -72,6 +71,31 @@ public function test_get_note_returns_note_when_account_is_connected() { $this->assertSame( 'Boost your sales by accepting new payment methods', $note->get_title() ); } + public function test_get_note_returns_note_when_account_is_partially_onboarded() { + $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'is_account_partially_onboarded', 'is_progressive_onboarding_in_progress' ] )->getMock(); + $account_mock->expects( $this->once() )->method( 'is_stripe_connected' )->willReturn( true ); + $account_mock->expects( $this->once() )->method( 'is_account_partially_onboarded' )->willReturn( true ); + + WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); + + $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); + + $this->assertNull( $note ); + } + + public function test_get_note_returns_note_when_account_is_progressive_in_progress() { + $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'is_account_partially_onboarded', 'is_progressive_onboarding_in_progress' ] )->getMock(); + $account_mock->expects( $this->once() )->method( 'is_stripe_connected' )->willReturn( true ); + $account_mock->expects( $this->once() )->method( 'is_account_partially_onboarded' )->willReturn( false ); + $account_mock->expects( $this->once() )->method( 'is_progressive_onboarding_in_progress' )->willReturn( true ); + + WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); + + $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); + + $this->assertNull( $note ); + } + public function test_maybe_enable_feature_flag_redirects_to_onboarding_when_account_not_connected() { $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'redirect_to_onboarding_welcome_page' ] )->getMock(); $account_mock->expects( $this->atLeastOnce() )->method( 'is_stripe_connected' )->will( $this->returnValue( false ) ); From f86b08e94ee0a4a186f2656dc3e4d48fe54d766d Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Mon, 11 Sep 2023 12:11:48 +0100 Subject: [PATCH 41/84] Add New Task to Enable Payment Methods (#7129) --- changelog/dev-6779-po-new-task | 4 ++ .../add-payment-methods-task.js | 46 ++++++++++--------- client/globals.d.ts | 1 + client/index.js | 4 +- client/overview/index.js | 2 + client/overview/task-list/strings.tsx | 12 ++++- client/overview/task-list/tasks.tsx | 29 ++++++------ .../task-list/tasks/add-apms-task.tsx | 32 +++++++++++++ client/overview/task-list/tasks/po-task.tsx | 42 ++++++++--------- includes/admin/class-wc-payments-admin.php | 26 +++++++++++ 10 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 changelog/dev-6779-po-new-task create mode 100644 client/overview/task-list/tasks/add-apms-task.tsx diff --git a/changelog/dev-6779-po-new-task b/changelog/dev-6779-po-new-task new file mode 100644 index 00000000000..c49c78654fa --- /dev/null +++ b/changelog/dev-6779-po-new-task @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. diff --git a/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js b/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js index 7fd26fec10d..b619ae96044 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js @@ -264,29 +264,31 @@ const AddPaymentMethodsTask = () => { } ) }

- - - { __( - 'Some payment methods cannot be enabled because more information is needed about your account. ', - 'woocommerce-payments' - ) } - - - { __( - 'Learn more about enabling additional payment methods.', - 'woocommerce-payments' - ) } - - + + { __( + 'Some payment methods cannot be enabled because more information is needed about your account. ', + 'woocommerce-payments' + ) } + + + { __( + 'Learn more about enabling additional payment methods.', + 'woocommerce-payments' + ) } + + + ) } { overviewTasksVisibility, showUpdateDetailsTask, wpcomReconnectUrl, + enabledPaymentMethods, } = wcpaySettings; const { isLoading: settingsIsLoading, settings } = useSettings(); @@ -70,6 +71,7 @@ const OverviewPage = () => { showUpdateDetailsTask, wpcomReconnectUrl, activeDisputes, + enabledPaymentMethods, } ); const tasks = Array.isArray( tasksUnsorted ) && tasksUnsorted.sort( taskSort ); diff --git a/client/overview/task-list/strings.tsx b/client/overview/task-list/strings.tsx index a826f9ebed4..8ca397211ed 100644 --- a/client/overview/task-list/strings.tsx +++ b/client/overview/task-list/strings.tsx @@ -285,7 +285,7 @@ export default { ), }, // Strings needed for the progressive onboarding related tasks. - po_tasks: { + tasks: { no_payment_14_days: { title: __( 'Please add your bank details to keep selling', @@ -412,5 +412,15 @@ export default { }, action_label: __( 'Verify bank details', 'woocommerce-payments' ), }, + add_apms: { + title: __( + 'Add more ways for buyers to pay', + 'woocommerce-payments' + ), + description: __( + 'Enable payment methods that work seamlessly with WooPayments.', + 'woocommerce-payments' + ), + }, }, }; diff --git a/client/overview/task-list/tasks.tsx b/client/overview/task-list/tasks.tsx index a8435f75d72..eb1fbf3cfc6 100644 --- a/client/overview/task-list/tasks.tsx +++ b/client/overview/task-list/tasks.tsx @@ -17,6 +17,7 @@ import { getReconnectWpcomTask } from './tasks/reconnect-task'; import { getUpdateBusinessDetailsTask } from './tasks/update-business-details-task'; import { CachedDispute } from 'wcpay/types/disputes'; import { TaskItemProps } from './types'; +import { getAddApmsTask } from './tasks/add-apms-task'; // Requirements we don't want to show to the user because they are too generic/not useful. These refer to Stripe error codes. const requirementBlacklist = [ 'invalid_value_other' ]; @@ -25,12 +26,14 @@ interface TaskListProps { showUpdateDetailsTask: boolean; wpcomReconnectUrl: string; activeDisputes?: CachedDispute[]; + enabledPaymentMethods?: string[]; } export const getTasks = ( { showUpdateDetailsTask, wpcomReconnectUrl, activeDisputes = [], + enabledPaymentMethods = [], }: TaskListProps ): TaskItemProps[] => { const { status, @@ -63,27 +66,26 @@ export const getTasks = ( { }; const isPoEnabled = progressiveOnboarding?.isEnabled; + const isPoComplete = progressiveOnboarding?.isComplete; + const isPoInProgress = isPoEnabled && ! isPoComplete; const errorMessages = getErrorMessagesFromRequirements(); + const isUpdateDetailsTaskVisible = + showUpdateDetailsTask && + ( ! isPoEnabled || ( isPoEnabled && ! detailsSubmitted ) ); + const isDisputeTaskVisible = !! activeDisputes && // Only show the dispute task if there are disputes due within 7 days. 0 < getDisputesDueWithinDays( activeDisputes, 7 ).length; + const isAddApmsTaskVisible = + enabledPaymentMethods?.length === 1 && + detailsSubmitted && + ! isPoInProgress; + return [ - showUpdateDetailsTask && - ! isPoEnabled && - getUpdateBusinessDetailsTask( - errorMessages, - status ?? '', - accountLink, - Number( currentDeadline ) ?? null, - pastDue ?? false, - detailsSubmitted ?? true - ), - showUpdateDetailsTask && - isPoEnabled && - ! detailsSubmitted && + isUpdateDetailsTaskVisible && getUpdateBusinessDetailsTask( errorMessages, status ?? '', @@ -95,6 +97,7 @@ export const getTasks = ( { wpcomReconnectUrl && getReconnectWpcomTask( wpcomReconnectUrl ), isDisputeTaskVisible && getDisputeResolutionTask( activeDisputes ), isPoEnabled && detailsSubmitted && getVerifyBankAccountTask(), + isAddApmsTaskVisible && getAddApmsTask(), ].filter( Boolean ); }; diff --git a/client/overview/task-list/tasks/add-apms-task.tsx b/client/overview/task-list/tasks/add-apms-task.tsx new file mode 100644 index 00000000000..29eb599339b --- /dev/null +++ b/client/overview/task-list/tasks/add-apms-task.tsx @@ -0,0 +1,32 @@ +/** + * External dependencies + */ + +/** + * Internal dependencies + */ +import type { TaskItemProps } from '../types'; +import strings from '../strings'; +import { getAdminUrl } from 'wcpay/utils'; + +export const getAddApmsTask = (): TaskItemProps | null => { + const handleClick = () => { + window.location.href = getAdminUrl( { + page: 'wc-admin', + path: '/payments/additional-payment-methods', + } ); + }; + + return { + key: 'add-apms', + level: 3, + content: '', + title: strings.tasks.add_apms.title, + additionalInfo: strings.tasks.add_apms.description, + completed: false, + onClick: handleClick, + action: handleClick, + expandable: false, + showActionButton: false, + }; +}; diff --git a/client/overview/task-list/tasks/po-task.tsx b/client/overview/task-list/tasks/po-task.tsx index 973a6c0c31a..79aa738415e 100644 --- a/client/overview/task-list/tasks/po-task.tsx +++ b/client/overview/task-list/tasks/po-task.tsx @@ -58,27 +58,27 @@ export const getVerifyBankAccountTask = (): any => { // When account is created less than 14 days ago, we also show a notice but it's just info. if ( 14 > daysFromAccountCreation ) { - title = strings.po_tasks.after_payment.title; + title = strings.tasks.after_payment.title; level = 3; - description = strings.po_tasks.after_payment.description( + description = strings.tasks.after_payment.description( verifyDetailsDueDate ); - actionLabelText = strings.po_tasks.after_payment.action_label; + actionLabelText = strings.tasks.after_payment.action_label; } if ( 14 <= daysFromAccountCreation ) { - title = strings.po_tasks.no_payment_14_days.title; + title = strings.tasks.no_payment_14_days.title; level = 2; - description = strings.po_tasks.no_payment_14_days.description( + description = strings.tasks.no_payment_14_days.description( verifyDetailsDueDate ); - actionLabelText = strings.po_tasks.no_payment_14_days.action_label; + actionLabelText = strings.tasks.no_payment_14_days.action_label; } if ( 30 <= daysFromAccountCreation ) { - title = strings.po_tasks.no_payment_30_days.title; + title = strings.tasks.no_payment_30_days.title; level = 1; - description = strings.po_tasks.no_payment_30_days.description; - actionLabelText = strings.po_tasks.no_payment_30_days.action_label; + description = strings.tasks.no_payment_30_days.description; + actionLabelText = strings.tasks.no_payment_30_days.action_label; } } else { const tpvInUsd = tpv / 100; @@ -87,39 +87,39 @@ export const getVerifyBankAccountTask = (): any => { .format( 'MMMM D, YYYY' ); const daysFromFirstPayment = moment().diff( firstPaymentDate, 'days' ); - title = strings.po_tasks.after_payment.title; + title = strings.tasks.after_payment.title; level = 3; - description = strings.po_tasks.after_payment.description( + description = strings.tasks.after_payment.description( verifyDetailsDueDate ); - actionLabelText = strings.po_tasks.after_payment.action_label; + actionLabelText = strings.tasks.after_payment.action_label; // Balance is rising. if ( tpvLimit * 0.2 <= tpvInUsd || 7 <= daysFromFirstPayment ) { - title = strings.po_tasks.balance_rising.title; + title = strings.tasks.balance_rising.title; level = 2; - description = strings.po_tasks.balance_rising.description( + description = strings.tasks.balance_rising.description( verifyDetailsDueDate ); - actionLabelText = strings.po_tasks.balance_rising.action_label; + actionLabelText = strings.tasks.balance_rising.action_label; } // Near threshold. if ( tpvLimit * 0.6 <= tpvInUsd || 21 <= daysFromFirstPayment ) { - title = strings.po_tasks.near_threshold.title; + title = strings.tasks.near_threshold.title; level = 1; - description = strings.po_tasks.near_threshold.description( + description = strings.tasks.near_threshold.description( verifyDetailsDueDate ); - actionLabelText = strings.po_tasks.near_threshold.action_label; + actionLabelText = strings.tasks.near_threshold.action_label; } // Threshold reached. if ( tpvLimit <= tpvInUsd || 30 <= daysFromFirstPayment ) { - title = strings.po_tasks.threshold_reached.title; + title = strings.tasks.threshold_reached.title; level = 1; - description = strings.po_tasks.threshold_reached.description( + description = strings.tasks.threshold_reached.description( verifyDetailsDueDate ); - actionLabelText = strings.po_tasks.threshold_reached.action_label; + actionLabelText = strings.tasks.threshold_reached.action_label; } } diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index c8434190b7a..ad937541204 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -829,6 +829,7 @@ private function get_js_settings(): array { 'fraudProtection' => [ 'isWelcomeTourDismissed' => WC_Payments_Features::is_fraud_protection_welcome_tour_dismissed(), ], + 'enabledPaymentMethods' => $this->get_enabled_payment_method_ids(), 'progressiveOnboarding' => $this->account->get_progressive_onboarding_details(), 'accountDefaultCurrency' => $this->account->get_account_default_currency(), 'frtDiscoverBannerSettings' => get_option( 'wcpay_frt_discover_banner_settings', '' ), @@ -842,6 +843,31 @@ private function get_js_settings(): array { return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); } + /** + * Helper function to retrieve enabled UPE payment methods. + * + * TODO: This is duplicating code located in the settings container, we should refactor so that + * this is stored in a centralised place and can be retrieved from there. + * + * @return array + */ + private function get_enabled_payment_method_ids(): array { + $available_upe_payment_methods = $this->wcpay_gateway->get_upe_available_payment_methods(); + /** + * It might be possible that enabled payment methods settings have an invalid state. As an example, + * if an account is switched to a new country and earlier country had PM's that are no longer valid; or if the PM is not available anymore. + * To keep saving settings working, we are ensuring the enabled payment methods are yet available. + */ + $enabled_payment_methods = array_values( + array_intersect( + $this->wcpay_gateway->get_upe_enabled_payment_method_ids(), + $available_upe_payment_methods + ) + ); + + return $enabled_payment_methods; + } + /** * Creates an array of features enabled only when external dependencies are of certain versions. * From 83c0c4c492bb2250379874b4ccf28a5eb18a43e8 Mon Sep 17 00:00:00 2001 From: Matt Allan Date: Tue, 12 Sep 2023 10:15:32 +1000 Subject: [PATCH 42/84] Implement a retry system to the WCPay Subscription migration process (#7106) Co-authored-by: James Allan --- changelog/issue-7038-retry-migration | 5 + ...ass-wc-payments-subscriptions-migrator.php | 144 +++++++++++++++--- 2 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 changelog/issue-7038-retry-migration diff --git a/changelog/issue-7038-retry-migration b/changelog/issue-7038-retry-migration new file mode 100644 index 00000000000..62a83cfe6ef --- /dev/null +++ b/changelog/issue-7038-retry-migration @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: No changelog added. This feature is part of a bigger migration feature coming to WCPay + + diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index f36ea22ac0e..1b9337bbb69 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -85,6 +85,9 @@ public function __construct( $api_client = null ) { // Add manual migration tool to WooCommerce > Status > Tools. add_filter( 'woocommerce_debug_tools', [ $this, 'add_manual_migration_tool' ] ); + // Schedule the single migration action with two args. This is needed because the WCS_Background_Repairer parent class only hooks on with one arg. + add_action( $this->migrate_hook . '_retry', [ $this, 'migrate_wcpay_subscription' ], 10, 2 ); + $this->init(); } @@ -99,17 +102,18 @@ public function __construct( $api_client = null ) { * 5. Add an order note on the subscription * * @param int $subscription_id The ID of the subscription to migrate. + * @param int $attempt The number of times migration has been attempted. */ - public function migrate_wcpay_subscription( $subscription_id ) { + public function migrate_wcpay_subscription( $subscription_id, $attempt = 0 ) { try { - add_action( 'action_scheduler_unexpected_shutdown', [ $this, 'log_unexpected_shutdown' ], 10, 2 ); - add_action( 'action_scheduler_failed_execution', [ $this, 'log_unexpected_action_failure' ], 10, 2 ); + add_action( 'action_scheduler_unexpected_shutdown', [ $this, 'handle_unexpected_shutdown' ], 10, 2 ); + add_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ], 10, 2 ); + + $this->logger->log( sprintf( 'Migrating subscription #%1$d.%2$s', $subscription_id, ( $attempt > 0 ? ' Attempt: ' . ( (int) $attempt + 1 ) : '' ) ) ); $subscription = $this->validate_subscription_to_migrate( $subscription_id ); $wcpay_subscription = $this->fetch_wcpay_subscription( $subscription ); - $this->logger->log( sprintf( 'Migrating subscription #%d (%s)', $subscription_id, $wcpay_subscription['id'] ) ); - $this->maybe_cancel_wcpay_subscription( $wcpay_subscription ); /** @@ -123,20 +127,22 @@ public function migrate_wcpay_subscription( $subscription_id ) { $new_next_payment = gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) + 1 ); $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); - $this->logger->log( sprintf( '---- Next payment date updated to %s to ensure active subscription has a pending scheduled payment.', $new_next_payment ) ); + $this->logger->log( sprintf( '---- Next payment date updated to %1$s to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription_id ) ); } $this->update_wcpay_subscription_meta( $subscription ); $subscription->add_order_note( __( 'This subscription has been successfully migrated to a WooPayments tokenized subscription.', 'woocommerce-payments' ) ); - $this->logger->log( '---- SUCCESS: Subscription migrated.' ); + $this->logger->log( sprintf( '---- SUCCESS: Subscription #%d migrated.', $subscription_id ) ); } catch ( \Exception $e ) { $this->logger->log( $e->getMessage() ); + + $this->maybe_reschedule_migration( $subscription_id, $attempt, $e ); } - remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'log_unexpected_shutdown' ] ); - remove_action( 'action_scheduler_failed_execution', [ $this, 'log_unexpected_action_failure' ] ); + remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'handle_unexpected_shutdown' ] ); + remove_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ] ); } /** @@ -154,23 +160,23 @@ public function migrate_wcpay_subscription( $subscription_id ) { */ private function validate_subscription_to_migrate( $subscription_id ) { if ( ! class_exists( 'WC_Subscriptions' ) ) { - throw new \Exception( sprintf( 'Skipping migration of subscription #%d. The WooCommerce Subscriptions extension is not active.', $subscription_id ) ); + throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. The WooCommerce Subscriptions extension is not active.', $subscription_id ) ); } if ( WC_Payments_Subscriptions::is_duplicate_site() ) { - throw new \Exception( sprintf( 'Skipping migration of subscription #%d. Site is in staging mode.', $subscription_id ) ); + throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Site is in staging mode.', $subscription_id ) ); } $subscription = wcs_get_subscription( $subscription_id ); if ( ! $subscription ) { - throw new \Exception( sprintf( 'Skipping migration of subscription #%d. Subscription not found.', $subscription_id ) ); + throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Subscription not found.', $subscription_id ) ); } $migrated_wcpay_subscription_id = $subscription->get_meta( '_migrated_wcpay_subscription_id', true ); if ( ! empty( $migrated_wcpay_subscription_id ) ) { - throw new \Exception( sprintf( 'Skipping migration of subscription #%d (%s). Subscription has already been migrated.', $subscription_id, $migrated_wcpay_subscription_id ) ); + throw new \Exception( sprintf( '---- Skipping migration of subscription #%1$d (%2$s). Subscription has already been migrated.', $subscription_id, $migrated_wcpay_subscription_id ) ); } return $subscription; @@ -191,18 +197,18 @@ private function fetch_wcpay_subscription( $subscription ) { $wcpay_subscription_id = WC_Payments_Subscription_Service::get_wcpay_subscription_id( $subscription ); if ( ! $wcpay_subscription_id ) { - throw new \Exception( sprintf( 'Skipping migration of subscription #%d. Subscription is not a WCPay Subscription.', $subscription->get_id() ) ); + throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Subscription is not a WCPay Subscription.', $subscription->get_id() ) ); } try { // Fetch the subscription from Stripe. $wcpay_subscription = $this->api_client->get_subscription( $wcpay_subscription_id ); } catch ( API_Exception $e ) { - throw new \Exception( sprintf( 'Error migrating subscription #%d (%s). Failed to fetch the subscription. %s', $subscription->get_id(), $wcpay_subscription_id, $e->getMessage() ) ); + throw new \Exception( sprintf( '---- ERROR: Failed to fetch subscription #%1$d (%2$s) from Stripe. %3$s', $subscription->get_id(), $wcpay_subscription_id, $e->getMessage() ) ); } if ( empty( $wcpay_subscription['id'] ) || empty( $wcpay_subscription['status'] ) ) { - throw new \Exception( sprintf( 'Error migrating subscription #%d (%s). Invalid subscription data from Stripe: %s', $subscription->get_id(), $wcpay_subscription_id, var_export( $wcpay_subscription, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export + throw new \Exception( sprintf( '---- ERROR: Cannot migrate subscription #%1$d (%2$s). Invalid data fetched from Stripe: %3$s', $subscription->get_id(), $wcpay_subscription_id, var_export( $wcpay_subscription, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export } return $wcpay_subscription; @@ -226,19 +232,19 @@ private function fetch_wcpay_subscription( $subscription ) { private function maybe_cancel_wcpay_subscription( $wcpay_subscription ) { // Valid statuses to cancel subscription at Stripe: active, past_due, trialing, paused. if ( in_array( $wcpay_subscription['status'], $this->active_statuses, true ) ) { - $this->logger->log( sprintf( '---- Subscription at Stripe has "%s" status. Canceling the subscription.', $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); + $this->logger->log( sprintf( '---- Stripe subscription (%1$s) has "%2$s" status. Canceling the subscription.', $wcpay_subscription['id'], $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); try { // Cancel the subscription in Stripe. $wcpay_subscription = $this->api_client->cancel_subscription( $wcpay_subscription['id'] ); } catch ( API_Exception $e ) { - throw new \Exception( sprintf( '---- ERROR: Failed to cancel the subscription at Stripe. %s', $e->getMessage() ) ); + throw new \Exception( sprintf( '---- ERROR: Failed to cancel the Stripe subscription (%1$s). %2$s', $wcpay_subscription['id'], $e->getMessage() ) ); } - $this->logger->log( '---- Subscription successfully canceled at Stripe.' ); + $this->logger->log( sprintf( '---- Stripe subscription (%1$s) successfully canceled.', $wcpay_subscription['id'] ) ); } else { // Statuses that don't need to be canceled: incomplete, incomplete_expired, canceled, unpaid. - $this->logger->log( sprintf( '---- Subscription has "%s" status. Skipping canceling the subscription at Stripe.', $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); + $this->logger->log( sprintf( '---- Stripe subscription (%1$s) has "%2$s" status. Skipping canceling the subscription at Stripe.', $wcpay_subscription['id'], $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); } } @@ -320,20 +326,36 @@ public function exclude_migrated_meta( $meta_data ) { * @param string $action_id The Action Scheduler action ID. * @param array $error The error data. */ - public function log_unexpected_shutdown( $action_id, $error = null ) { + public function handle_unexpected_shutdown( $action_id, $error = null ) { + $migration_args = $this->get_migration_action_args( $action_id ); + + if ( ! isset( $migration_args['migrate_subscription'], $migration_args['attempt'] ) ) { + return; + } + if ( ! empty( $error['type'] ) && in_array( $error['type'], [ E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ], true ) ) { - $this->logger->log( sprintf( '---- ERROR: %s in %s on line %s.', $error['message'] ?? 'No message', $error['file'] ?? 'no file found', $error['line'] ?? '0' ) ); + $this->logger->log( sprintf( '---- ERROR: Unexpected shutdown while migrating subscription #%1$d: %2$s in %3$s on line %4$s.', $migration_args['migrate_subscription'], $error['message'] ?? 'No message', $error['file'] ?? 'no file found', $error['line'] ?? '0' ) ); } + + $this->maybe_reschedule_migration( $migration_args['migrate_subscription'], $migration_args['attempt'] ); } /** - * Logs any unexpected failures that occur while processing a scheduled migrate WCPay Subscription action. + * Handles any unexpected failures that occur while processing a single migration action + * by logging an error message and rescheduling the action to retry. * * @param string $action_id The Action Scheduler action ID. * @param Exception $exception The exception thrown during action processing. */ - public function log_unexpected_action_failure( $action_id, $exception ) { - $this->logger->log( sprintf( '---- ERROR: %s', $exception->getMessage() ) ); + public function handle_unexpected_action_failure( $action_id, $exception ) { + $migration_args = $this->get_migration_action_args( $action_id ); + + if ( ! isset( $migration_args['migrate_subscription'], $migration_args['attempt'] ) ) { + return; + } + + $this->logger->log( sprintf( '---- ERROR: Unexpected failure while migrating subscription #%1$d: %2$s', $migration_args['migrate_subscription'], $exception->getMessage() ) ); + $this->maybe_reschedule_migration( $migration_args['migrate_subscription'], $migration_args['attempt'] ); } /** @@ -394,6 +416,78 @@ public function schedule_migrate_wcpay_subscriptions_action() { $this->schedule_repair(); } + /** + * Gets the subscription ID and number of attempts from the action args. + * + * @param int $action_id The action ID to get data from. + * + * @return array + */ + private function get_migration_action_args( $action_id ) { + $action = ActionScheduler_Store::instance()->fetch_action( $action_id ); + + if ( ! $action || ( $this->migrate_hook !== $action->get_hook() && $this->migrate_hook . '_retry' !== $action->get_hook() ) ) { + return []; + } + + $action_args = $action->get_args(); + + if ( ! isset( $action_args['migrate_subscription'] ) ) { + return []; + } + + return array_merge( + [ + 'migrate_subscription' => 0, + 'attempt' => 0, + ], + $action_args + ); + } + + /** + * Reschedules a subscription migration with increasing delays depending on number of attempts. + * + * After max retries, an exception is thrown if one was passed. + * + * @param int $subscription_id The ID of the subscription to retry. + * @param int $attempt The number of times migration has been attempted. + * @param \Exception|null $exception The exception thrown during migration. + * + * @throws \Exception If max attempts and exception passed is not null. + */ + public function maybe_reschedule_migration( $subscription_id, $attempt = 0, $exception = null ) { + // Number of seconds to wait before retrying the migration, increasing with each attempt up to 7 attempts (12 hours). + $retry_schedule = [ 60, 300, 600, 1800, HOUR_IN_SECONDS, 6 * HOUR_IN_SECONDS, 12 * HOUR_IN_SECONDS ]; + + // If the exception thrown contains "Skipping migration", don't reschedule the migration. + if ( $exception && false !== strpos( $exception->getMessage(), 'Skipping migration' ) ) { + return; + } + + if ( isset( $retry_schedule[ $attempt ] ) && $attempt < 7 ) { + $this->logger->log( sprintf( '---- Rescheduling migration of subscription #%1$d.', $subscription_id ) ); + + as_schedule_single_action( + gmdate( 'U' ) + $retry_schedule[ $attempt ], + $this->migrate_hook . '_retry', + [ + 'migrate_subscription' => $subscription_id, + 'attempt' => $attempt + 1, + ] + ); + } else { + $this->logger->log( sprintf( '---- FAILED: Subscription #%d could not be migrated.', $subscription_id ) ); + + if ( $exception ) { + // Before throwing the exception, remove the action_scheduler failure hook to prevent the exception being logged again. + remove_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ] ); + + throw $exception; + } + } + } + /** * Override WCS_Background_Repairer methods. */ From 16d8da60fdb93de3fceb903b696c029afbe91487 Mon Sep 17 00:00:00 2001 From: Matt Allan Date: Tue, 12 Sep 2023 13:50:35 +1000 Subject: [PATCH 43/84] Update Subscriptions with WooPayments eligibility as we move to deprecate this functionality (#7117) Co-authored-by: James Allan --- .../issue-6510-deprecate-wcpay-subscriptions | 4 ++ .../wcpay-subscriptions-toggle.js | 7 +- includes/class-wc-payments-features.php | 65 +++++++++++++++++-- .../class-wc-payments-subscriptions.php | 13 ++++ 4 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 changelog/issue-6510-deprecate-wcpay-subscriptions diff --git a/changelog/issue-6510-deprecate-wcpay-subscriptions b/changelog/issue-6510-deprecate-wcpay-subscriptions new file mode 100644 index 00000000000..b40eaae8139 --- /dev/null +++ b/changelog/issue-6510-deprecate-wcpay-subscriptions @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Only display the WCPay Subscriptions setting to existing users as part of deprecating this feature. diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index e2f921c990c..3305739e565 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -30,11 +30,8 @@ const WCPaySubscriptionsToggle = () => { updateIsWCPaySubscriptionsEnabled( value ); }; - /** - * Only show the toggle if the site is eligible for wcpay subscriptions or - * if wcpay subscriptions are already enabled. - */ - return isWCPaySubscriptionsEligible || isWCPaySubscriptionsEnabled ? ( + return ! wcpaySettings.isSubscriptionsActive && + isWCPaySubscriptionsEligible ? ( 1, + 'subscription_status' => 'any', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => '_wcpay_subscription_id', + 'compare' => 'EXISTS', + ], + ], + ] + ); + + if ( count( $wcpay_subscriptions ) > 0 ) { + return true; + } } - $store_base_location = wc_get_base_location(); - return ! empty( $store_base_location['country'] ) && 'US' === $store_base_location['country']; + /** + * Check if they have at least 1 Stripe Billing enabled product. + */ + $stripe_billing_meta_query_handler = function ( $query, $query_vars ) { + if ( ! empty( $query_vars['stripe_billing_product'] ) ) { + $query['meta_query'][] = [ + 'key' => '_wcpay_product_hash', + 'compare' => 'EXISTS', + ]; + } + + return $query; + }; + + add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 ); + + $subscription_products = wc_get_products( + [ + 'limit' => 1, + 'type' => [ 'subscription', 'variable-subscription' ], + 'status' => 'publish', + 'return' => 'ids', + 'stripe_billing_product' => 'true', + ] + ); + + remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 ); + + if ( count( $subscription_products ) > 0 ) { + return true; + } + + return false; } /** diff --git a/includes/subscriptions/class-wc-payments-subscriptions.php b/includes/subscriptions/class-wc-payments-subscriptions.php index c30c6248d5c..cd0a5101337 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions.php +++ b/includes/subscriptions/class-wc-payments-subscriptions.php @@ -95,6 +95,8 @@ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Cus include_once __DIR__ . '/class-wc-payments-subscriptions-migrator.php'; self::$stripe_billing_migrator = new WC_Payments_Subscriptions_Migrator( $api_client ); } + + add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'maybe_disable_wcpay_subscriptions_on_update' ] ); } /** @@ -156,4 +158,15 @@ public static function is_duplicate_site() { return class_exists( 'WCS_Staging' ) && WCS_Staging::is_duplicate_site(); } + + /** + * Disable the WCPay Subscriptions feature on WooPayments plugin update if it's enabled and the store is no longer eligible. + * + * @see WC_Payments_Features::is_wcpay_subscriptions_eligible() for eligibility criteria. + */ + public static function maybe_disable_wcpay_subscriptions_on_update() { + if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() && ! WC_Payments_Features::is_wcpay_subscriptions_eligible() ) { + update_option( WC_Payments_Features::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ); + } + } } From b0c0e79ecc677fe3748567c3310a10c8bc6c6b27 Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 12 Sep 2023 14:21:54 +0300 Subject: [PATCH 44/84] Improve: has_multi_currency_orders query improvement and unit tests (#6829) --- ...as-multi-currency-orders-query-improvement | 4 ++++ includes/multi-currency/Analytics.php | 22 ++++++++++++------- .../multi-currency/test-class-analytics.php | 13 +++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 changelog/6814-has-multi-currency-orders-query-improvement diff --git a/changelog/6814-has-multi-currency-orders-query-improvement b/changelog/6814-has-multi-currency-orders-query-improvement new file mode 100644 index 00000000000..dc97afed7df --- /dev/null +++ b/changelog/6814-has-multi-currency-orders-query-improvement @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Enhanced Analytics SQL, added unit test for has_multi_currency_orders(). Improved code quality and test coverage. diff --git a/includes/multi-currency/Analytics.php b/includes/multi-currency/Analytics.php index 1c5bf22560f..19d8870ec3d 100644 --- a/includes/multi-currency/Analytics.php +++ b/includes/multi-currency/Analytics.php @@ -508,22 +508,28 @@ private function generate_case_when( string $variable, string $then, string $els private function has_multi_currency_orders() { global $wpdb; - // Using full SQL instad of variables to keep WPCS happy. + // Using full SQL instead of variables to keep WPCS happy. if ( $this->is_cot_enabled() ) { $result = $wpdb->get_var( - "SELECT COUNT(order_id) - FROM {$wpdb->prefix}wc_orders_meta - WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate'" + "SELECT EXISTS( + SELECT 1 + FROM {$wpdb->prefix}wc_orders_meta + WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate' + LIMIT 1) + AS count;" ); } else { $result = $wpdb->get_var( - "SELECT COUNT(post_id) - FROM {$wpdb->postmeta} - WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate'" + "SELECT EXISTS( + SELECT 1 + FROM {$wpdb->postmeta} + WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate' + LIMIT 1) + AS count;" ); } - return intval( $result ) > 0; + return intval( $result ) === 1; } /** diff --git a/tests/unit/multi-currency/test-class-analytics.php b/tests/unit/multi-currency/test-class-analytics.php index f2e672fac69..0ca89b7fd6e 100644 --- a/tests/unit/multi-currency/test-class-analytics.php +++ b/tests/unit/multi-currency/test-class-analytics.php @@ -131,6 +131,19 @@ public function test_register_customer_currencies() { $this->assertTrue( $data_registry->exists( 'customerCurrencies' ) ); } + public function test_has_multi_currency_orders() { + + // Use reflection to make the private method has_multi_currency_orders accessible. + $method = new ReflectionMethod( Analytics::class, 'has_multi_currency_orders' ); + $method->setAccessible( true ); + + // Now, you can call the has_multi_currency_orders method using the ReflectionMethod object. + $result = $method->invoke( $this->analytics ); + + $this->assertTrue( $result ); + + } + public function test_register_customer_currencies_for_empty_customer_currencies() { $this->mock_multi_currency->expects( $this->once() ) ->method( 'get_all_customer_currencies' ) From ed190bcfd1d6dd915898b8eff9920516c67c9c98 Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 12 Sep 2023 14:22:06 +0300 Subject: [PATCH 45/84] Improvement: Query optimisation for get_all_customer_currencies method (#6902) --- ...52-get_all_customer_currencies_improvement | 4 ++ includes/multi-currency/MultiCurrency.php | 57 ++++++++++++++----- .../test-class-multi-currency.php | 2 +- 3 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 changelog/6852-get_all_customer_currencies_improvement diff --git a/changelog/6852-get_all_customer_currencies_improvement b/changelog/6852-get_all_customer_currencies_improvement new file mode 100644 index 00000000000..09be89d6180 --- /dev/null +++ b/changelog/6852-get_all_customer_currencies_improvement @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Improved `get_all_customer_currencies` method to retrieve existing order currencies faster. diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php index dbaa98dd0db..bf75d594fef 100644 --- a/includes/multi-currency/MultiCurrency.php +++ b/includes/multi-currency/MultiCurrency.php @@ -1445,28 +1445,57 @@ public function is_multi_currency_settings_page(): bool { ); } + /** + * Function used to compute the customer used currencies, used as internal callable for get_all_customer_currencies function. + * + * @return array + */ + public function callable_get_customer_currencies() { + global $wpdb; + + $currencies = $this->get_available_currencies(); + $query_union = []; + + if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) && + \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { + foreach ( $currencies as $currency ) { + $query_union[] = $wpdb->prepare( + "SELECT %s AS currency_code, EXISTS(SELECT currency FROM {$wpdb->prefix}wc_orders WHERE currency=%s LIMIT 1) AS exists_in_orders", + $currency->code, + $currency->code + ); + } + } else { + foreach ( $currencies as $currency ) { + $query_union[] = $wpdb->prepare( + "SELECT %s AS currency_code, EXISTS(SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key=%s AND meta_value=%s LIMIT 1) AS exists_in_orders", + $currency->code, + '_order_currency', + $currency->code + ); + } + } + + $sub_query = join( ' UNION ALL ', $query_union ); + $query = "SELECT currency_code FROM ( $sub_query ) as subquery WHERE subquery.exists_in_orders=1 ORDER BY currency_code ASC"; + $currencies = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + return [ + 'currencies' => $currencies, + 'updated' => time(), + ]; + } + /** * Get all the currencies that have been used in the store. * * @return array */ public function get_all_customer_currencies(): array { + $data = $this->database_cache->get_or_add( Database_Cache::CUSTOMER_CURRENCIES_KEY, - function() { - global $wpdb; - if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) && - \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { - $currencies = $wpdb->get_col( "SELECT DISTINCT(currency) FROM {$wpdb->prefix}wc_orders" ); - } else { - $currencies = $wpdb->get_col( "SELECT DISTINCT(meta_value) FROM {$wpdb->postmeta} WHERE meta_key = '_order_currency'" ); - } - - return [ - 'currencies' => $currencies, - 'updated' => time(), - ]; - }, + [ $this, 'callable_get_customer_currencies' ], function ( $data ) { // Return true if the data looks valid and was updated an hour or less ago. return is_array( $data ) && diff --git a/tests/unit/multi-currency/test-class-multi-currency.php b/tests/unit/multi-currency/test-class-multi-currency.php index a4ea52bbd2b..4ca6a346919 100644 --- a/tests/unit/multi-currency/test-class-multi-currency.php +++ b/tests/unit/multi-currency/test-class-multi-currency.php @@ -978,7 +978,7 @@ function( $key, $generator, $validator ) { $result = $this->multi_currency->get_all_customer_currencies(); - $this->assertEquals( [ 'GBP', 'EUR', 'USD' ], $result ); + $this->assertEquals( [ 'EUR', 'GBP', 'USD' ], $result ); foreach ( $mock_orders as $order_id ) { wp_delete_post( $order_id, true ); From 8aa1e6856183f8beb3b76fead42d6c69ba5aa391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Qui=C3=B1ones?= Date: Tue, 12 Sep 2023 13:52:36 +0200 Subject: [PATCH 46/84] Avoid empty fields in new onboarding flow (#7180) --- changelog/fix-6981-missing-onboarding-field-data | 4 ++++ includes/wc-payment-api/class-wc-payments-api-client.php | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-6981-missing-onboarding-field-data diff --git a/changelog/fix-6981-missing-onboarding-field-data b/changelog/fix-6981-missing-onboarding-field-data new file mode 100644 index 00000000000..d8170c0d31d --- /dev/null +++ b/changelog/fix-6981-missing-onboarding-field-data @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Avoid empty fields in new onboarding flow diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index eeeddad0111..b1a249ab2fb 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -887,7 +887,11 @@ public function get_onboarding_fields_data( string $locale = '' ): array { ); if ( ! is_array( $fields_data ) ) { - return []; + throw new API_Exception( + __( 'Onboarding field data could not be retrieved', 'woocommerce-payments' ), + 'wcpay_onboarding_fields_data_error', + 400 + ); } return $fields_data; From 384ca6d6e65c50416c9fdab2ba9ea0d1f2696899 Mon Sep 17 00:00:00 2001 From: deepakpathania <68396823+deepakpathania@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:37:13 +0530 Subject: [PATCH 47/84] Extract minimum supported PHP version from plugin file for GH actions (#7179) --- .github/actions/e2e/env-setup/action.yml | 8 ++------ .github/actions/setup-php/action.yml | 19 +++++++++++++++++++ .github/actions/setup-repo/action.yml | 19 +++++-------------- .github/workflows/check-changelog.yml | 7 ++----- .github/workflows/i18n-weekly-release.yml | 8 ++------ .github/workflows/php-compatibility.yml | 7 ++----- .github/workflows/php-lint-test.yml | 7 ++----- changelog/dev-update-workflows | 4 ++++ 8 files changed, 38 insertions(+), 41 deletions(-) create mode 100644 .github/actions/setup-php/action.yml create mode 100644 changelog/dev-update-workflows diff --git a/.github/actions/e2e/env-setup/action.yml b/.github/actions/e2e/env-setup/action.yml index dabaa752c3e..863ad27e75b 100644 --- a/.github/actions/e2e/env-setup/action.yml +++ b/.github/actions/e2e/env-setup/action.yml @@ -10,12 +10,8 @@ runs: run: echo -e "machine github.com\n login $E2E_GH_TOKEN" > ~/.netrc # PHP setup - - name: PHP Setup - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php # Composer setup - name: Setup Composer diff --git a/.github/actions/setup-php/action.yml b/.github/actions/setup-php/action.yml new file mode 100644 index 00000000000..44e797aeb6d --- /dev/null +++ b/.github/actions/setup-php/action.yml @@ -0,0 +1,19 @@ +name: "Set up PHP" +description: "Extracts the required PHP version from plugin file and uses it to build PHP." + +runs: + using: composite + steps: + - name: "Get minimum PHP version" + shell: bash + id: get_min_php_version + run: | + MIN_PHP_VERSION=$(sed -n 's/.*PHP: //p' woocommerce-payments.php) + echo "MIN_PHP_VERSION=$MIN_PHP_VERSION" >> $GITHUB_OUTPUT + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ steps.get_min_php_version.outputs.MIN_PHP_VERSION }} + tools: composer + coverage: none diff --git a/.github/actions/setup-repo/action.yml b/.github/actions/setup-repo/action.yml index 28741b60920..890fe95963f 100644 --- a/.github/actions/setup-repo/action.yml +++ b/.github/actions/setup-repo/action.yml @@ -1,29 +1,20 @@ name: "Setup WooCommerce Payments repository" description: "Handles the installation, building, and caching of the projects within the repository." -inputs: - php-version: - description: "The version of PHP that the action should set up." - default: "7.4" - runs: using: composite steps: - name: "Setup Node" uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' - cache: 'npm' + node-version-file: ".nvmrc" + cache: "npm" - name: "Enable composer dependencies caching" uses: actions/cache@v3 with: path: ~/.cache/composer/ key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} - - - name: "Setup PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ inputs.php-version }} - tools: composer - coverage: none + + - name: "Set up PHP" + uses: ./.github/actions/setup-php diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml index 55f7391fb90..25cf89903ca 100644 --- a/.github/workflows/check-changelog.yml +++ b/.github/workflows/check-changelog.yml @@ -22,11 +22,8 @@ jobs: path: ~/.cache/composer/ key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} # setup PHP, but without debug extensions for reasonable performance - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php # Install composer packages. - run: composer self-update && composer install --no-progress # Fetch the target branch before running the check. diff --git a/.github/workflows/i18n-weekly-release.yml b/.github/workflows/i18n-weekly-release.yml index 197213dec1c..c2abf63e34c 100644 --- a/.github/workflows/i18n-weekly-release.yml +++ b/.github/workflows/i18n-weekly-release.yml @@ -27,12 +27,8 @@ jobs: path: ~/.npm/ key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} # setup PHP, but without debug extensions for reasonable performance - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none - + - name: "Set up PHP" + uses: ./.github/actions/setup-php - name: Build release run: | npm ci diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index 8c0e7d73b37..8161804f6a2 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -14,9 +14,6 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php - run: bash bin/phpcs-compat.sh diff --git a/.github/workflows/php-lint-test.yml b/.github/workflows/php-lint-test.yml index 0399a22735c..5b397ab30d4 100644 --- a/.github/workflows/php-lint-test.yml +++ b/.github/workflows/php-lint-test.yml @@ -27,11 +27,8 @@ jobs: path: ~/.cache/composer/ key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} # setup PHP, but without debug extensions for reasonable performance - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php # install dependencies and run linter - run: composer self-update && composer install --no-progress && ./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$) && ./vendor/bin/psalm diff --git a/changelog/dev-update-workflows b/changelog/dev-update-workflows new file mode 100644 index 00000000000..cdab2b4fa9f --- /dev/null +++ b/changelog/dev-update-workflows @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Comment: Update GH workflows to use PHP version from plugin file. From 5f75b8fd87ad8d34d350a7d7c0c4774fb60d6efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Tue, 12 Sep 2023 15:33:10 +0200 Subject: [PATCH 48/84] Update new onboarding flow components to use admin color schema (#7177) Co-authored-by: Vlad Olaru --- changelog/update-7098-onboarding-components | 5 ++++ client/components/load-bar/style.scss | 2 +- client/components/radio-card/index.tsx | 20 +++++++++---- client/components/radio-card/style.scss | 10 +++++-- .../test/__snapshots__/index.tsx.snap | 30 +++++++++++++++---- client/components/radio-card/test/index.tsx | 2 +- client/onboarding/style.scss | 2 +- 7 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 changelog/update-7098-onboarding-components diff --git a/changelog/update-7098-onboarding-components b/changelog/update-7098-onboarding-components new file mode 100644 index 00000000000..3ecd11636ad --- /dev/null +++ b/changelog/update-7098-onboarding-components @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Ensure new onboarding components (behind an experiment) use admin color schema. + + diff --git a/client/components/load-bar/style.scss b/client/components/load-bar/style.scss index 1329a0cce65..f3f7fb86d7f 100644 --- a/client/components/load-bar/style.scss +++ b/client/components/load-bar/style.scss @@ -8,7 +8,7 @@ display: block; height: 100%; width: 100%; - background-color: $blue-50; + background-color: var( --wp-admin-theme-color ); animation: wcpay-component-load-bar 3s ease-in-out infinite; transform-origin: 0 0; } diff --git a/client/components/radio-card/index.tsx b/client/components/radio-card/index.tsx index 580e3e06170..f8a2f9f74de 100644 --- a/client/components/radio-card/index.tsx +++ b/client/components/radio-card/index.tsx @@ -28,17 +28,23 @@ const RadioCard: React.FC< Props > = ( { onChange, className, } ) => { - const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => - onChange( event.target.value ); - return ( <> { options.map( ( { label, icon, value, content } ) => { + const id = `radio-card-${ name }-${ value }`; const checked = value === selected; + const handleChange = () => onChange( value ); return ( - +
); } ) } diff --git a/client/components/radio-card/style.scss b/client/components/radio-card/style.scss index 2ce0dc954ab..f2e0f75766c 100644 --- a/client/components/radio-card/style.scss +++ b/client/components/radio-card/style.scss @@ -11,8 +11,9 @@ } &:hover, - &.checked { - box-shadow: 0 0 0 1px #007cba; + &.checked, + &:focus-visible { + box-shadow: 0 0 0 1.5px var( --wp-admin-theme-color ); } &__label { @@ -37,10 +38,15 @@ height: 20px; border-color: $gray-700; + &:checked { + border-color: var( --wp-admin-theme-color ); + } + &::before { width: 12px; height: 12px; margin: 3px; + background: var( --wp-admin-theme-color ); } &:focus { diff --git a/client/components/radio-card/test/__snapshots__/index.tsx.snap b/client/components/radio-card/test/__snapshots__/index.tsx.snap index dbeaea5fcb7..f4429bb7ffd 100644 --- a/client/components/radio-card/test/__snapshots__/index.tsx.snap +++ b/client/components/radio-card/test/__snapshots__/index.tsx.snap @@ -2,39 +2,57 @@ exports[`RadioCard Component renders RadioCard component with provided props 1`] = `
- -
`; diff --git a/client/components/radio-card/test/index.tsx b/client/components/radio-card/test/index.tsx index 01d93fc6a97..de4050684ca 100644 --- a/client/components/radio-card/test/index.tsx +++ b/client/components/radio-card/test/index.tsx @@ -49,7 +49,7 @@ describe( 'RadioCard Component', () => { /> ); - user.click( screen.getByRole( 'radio', { name: /Pineapple/i } ) ); + user.click( screen.getByLabelText( /Pineapple/i ) ); expect( mockOnChange ).toHaveBeenCalledWith( 'pineapple' ); } ); } ); diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 89908c25d93..119bf210bd6 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -12,7 +12,7 @@ body.wcpay-onboarding__body { top: 0; left: 0; height: 8px; - background-color: $gutenberg-blue; + background-color: var( --wp-admin-theme-color ); z-index: 11; transition: width 250ms; } From ca7929f37ebaea13126c87904e71b22a38f47855 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:31:24 +0300 Subject: [PATCH 49/84] Warn about dev mode enabled on new onboarding flow choice (#7082) --- ...6429-warn-about-dev-mode-on-new-onboarding | 4 ++++ client/globals.d.ts | 1 + client/onboarding/steps/mode-choice.tsx | 9 ++++++++ client/onboarding/steps/personal-details.tsx | 7 ++++++- client/onboarding/steps/test/mode-choice.tsx | 13 +++++++++++- client/onboarding/strings.tsx | 21 +++++++++++++++++++ client/onboarding/style.scss | 2 +- includes/admin/class-wc-payments-admin.php | 8 +++++++ includes/class-wc-payments-account.php | 4 ++++ .../class-wc-payments-api-client.php | 18 ++++++++++++++++ 10 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 changelog/add-6429-warn-about-dev-mode-on-new-onboarding diff --git a/changelog/add-6429-warn-about-dev-mode-on-new-onboarding b/changelog/add-6429-warn-about-dev-mode-on-new-onboarding new file mode 100644 index 00000000000..386c3f79ba7 --- /dev/null +++ b/changelog/add-6429-warn-about-dev-mode-on-new-onboarding @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Warn about dev mode enabled on new onboarding flow choice diff --git a/client/globals.d.ts b/client/globals.d.ts index 11316b8c4fb..293db5f0e0d 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -19,6 +19,7 @@ declare global { }; fraudServices: unknown[]; testMode: boolean; + devMode: boolean; isJetpackConnected: boolean; isJetpackIdcActive: boolean; accountStatus: { diff --git a/client/onboarding/steps/mode-choice.tsx b/client/onboarding/steps/mode-choice.tsx index 3065560dbbd..5b1346a179a 100644 --- a/client/onboarding/steps/mode-choice.tsx +++ b/client/onboarding/steps/mode-choice.tsx @@ -13,8 +13,16 @@ import RadioCard from 'components/radio-card'; import { useStepperContext } from 'components/stepper'; import { trackModeSelected } from '../tracking'; import strings from '../strings'; +import BannerNotice from 'components/banner-notice'; + +const DevModeNotice = () => ( + + { strings.steps.mode.devModeNotice } + +); const ModeChoice: React.FC = () => { + const { devMode } = wcpaySettings; const liveStrings = strings.steps.mode.live; const testStrings = strings.steps.mode.test; @@ -35,6 +43,7 @@ const ModeChoice: React.FC = () => { return ( <> + { devMode && } { - + { strings.steps.personal.notice } diff --git a/client/onboarding/steps/test/mode-choice.tsx b/client/onboarding/steps/test/mode-choice.tsx index 0b55a787ca7..8c081182324 100644 --- a/client/onboarding/steps/test/mode-choice.tsx +++ b/client/onboarding/steps/test/mode-choice.tsx @@ -22,11 +22,16 @@ jest.mock( 'components/stepper', () => ( { declare const global: { wcpaySettings: { connectUrl: string; + devMode: boolean; }; }; describe( 'ModeChoice', () => { - it( 'displays test and live radio cards', () => { + it( 'displays test and live radio cards, notice for dev mode', () => { + global.wcpaySettings = { + connectUrl: 'https://wcpay.test/connect', + devMode: true, + }; render( ); expect( @@ -35,6 +40,11 @@ describe( 'ModeChoice', () => { expect( screen.getByText( strings.steps.mode.test.label ) ).toBeInTheDocument(); + expect( + screen.getByText( + 'Dev mode is enabled, only test accounts will be created. If you want to process live transactions, please disable it.' + ) + ).toBeInTheDocument(); } ); it( 'calls nextStep by clicking continue when `live` is selected', () => { @@ -50,6 +60,7 @@ describe( 'ModeChoice', () => { it( 'redirects to `connectUrl` with `test_mode` enabled by clicking continue button when `test` is selected', () => { global.wcpaySettings = { connectUrl: 'https://wcpay.test/connect', + devMode: false, }; Object.defineProperty( window, 'location', { configurable: true, diff --git a/client/onboarding/strings.tsx b/client/onboarding/strings.tsx index aa996532fd7..1037b1c0639 100644 --- a/client/onboarding/strings.tsx +++ b/client/onboarding/strings.tsx @@ -3,6 +3,8 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; +import interpolateComponents from '@automattic/interpolate-components'; +import React from 'react'; export default { steps: { @@ -39,6 +41,25 @@ export default { 'WooPayments' ), }, + devModeNotice: interpolateComponents( { + mixedString: __( + 'Dev mode is enabled, only test accounts will be created. If you want to process live transactions, please disable it. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ), }, personal: { heading: __( diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 119bf210bd6..1c118b5bf3c 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -106,7 +106,7 @@ body.wcpay-onboarding__body { padding: $gap-small $gap; } - .wcpay-inline-notice { + .personal-details-notice { margin: 0; } diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index ad937541204..c6695067a9e 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -772,6 +772,13 @@ private function get_js_settings(): array { Logger::log( sprintf( 'WCPay JS settings: Could not determine if WCPay should be in test mode! Message: %s', $e->getMessage() ), 'warning' ); } + $dev_mode = false; + try { + $dev_mode = WC_Payments::mode()->is_dev(); + } catch ( Exception $e ) { + Logger::log( sprintf( 'WCPay JS settings: Could not determine if WCPay should be in dev mode! Message: %s', $e->getMessage() ), 'warning' ); + } + $connect_url = WC_Payments_Account::get_connect_url(); $connect_incentive = $this->incentives_service->get_cached_connect_incentive(); // If we have an incentive ID, attach it to the connect URL. @@ -787,6 +794,7 @@ private function get_js_settings(): array { 'availableStates' => WC()->countries->get_states(), ], 'connectIncentive' => $connect_incentive, + 'devMode' => $dev_mode, 'testMode' => $test_mode, 'onboardingTestMode' => WC_Payments_Onboarding_Service::is_test_mode_enabled(), // Set this flag for use in the front-end to alter messages and notices if on-boarding has been disabled. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 8fc2184dffd..10680f70f5d 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -889,6 +889,10 @@ public function maybe_handle_onboarding() { } if ( isset( $_GET['wcpay-disable-onboarding-test-mode'] ) ) { + // Delete the account if the dev mode is enabled otherwise it'll cause issues to onboard again. + if ( WC_Payments::mode()->is_dev() ) { + $this->payments_api_client->delete_account(); + } WC_Payments_Onboarding_Service::set_test_mode( false ); $this->redirect_to_onboarding_flow_page(); return; diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index b1a249ab2fb..5442700f1b4 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -2345,4 +2345,22 @@ public function get_woopay_compatibility() { false ); } + + /** + * Delete account. + * + * @return array + * @throws API_Exception + */ + public function delete_account() { + return $this->request( + [ + 'test_mode' => WC_Payments::mode()->is_dev(), // only send a test mode request if in dev mode. + ], + self::ACCOUNTS_API . '/delete', + self::POST, + true, + true + ); + } } From b31489f8a7c3b2e41829e9eeec18fcd34b816f2a Mon Sep 17 00:00:00 2001 From: Hsing-yu Flowers Date: Tue, 12 Sep 2023 18:36:58 -0700 Subject: [PATCH 50/84] Add the WooPay Express button to the Pay for order page (#5903) --- changelog/add-pay-for-order | 4 +++ client/checkout/woopay/email-input-iframe.js | 10 ++++-- .../express-button/express-checkout-iframe.js | 14 +++++++-- client/checkout/woopay/utils.js | 19 ++++++++++++ ...xpress-checkout-button-display-handler.php | 31 ++++++++++++++++--- ...lass-wc-payments-woopay-button-handler.php | 4 --- 6 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 changelog/add-pay-for-order diff --git a/changelog/add-pay-for-order b/changelog/add-pay-for-order new file mode 100644 index 00000000000..b44cf523113 --- /dev/null +++ b/changelog/add-pay-for-order @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add the express button on the pay for order page diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index 6c17b14fc3e..47ae94dff85 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -6,7 +6,11 @@ import { getConfig } from 'wcpay/utils/checkout'; import wcpayTracks from 'tracks'; import request from '../utils/request'; import { buildAjaxURL } from '../../payment-request/utils'; -import { getTargetElement, validateEmail } from './utils'; +import { + getTargetElement, + validateEmail, + appendRedirectionParams, +} from './utils'; export const handleWooPayEmailInput = async ( field, @@ -534,7 +538,9 @@ export const handleWooPayEmailInput = async ( true ); if ( e.data.redirectUrl ) { - window.location = e.data.redirectUrl; + window.location = appendRedirectionParams( + e.data.redirectUrl + ); } break; case 'redirect_to_platform_checkout': diff --git a/client/checkout/woopay/express-button/express-checkout-iframe.js b/client/checkout/woopay/express-button/express-checkout-iframe.js index 7ac6bfcb275..95b49e62091 100644 --- a/client/checkout/woopay/express-button/express-checkout-iframe.js +++ b/client/checkout/woopay/express-button/express-checkout-iframe.js @@ -5,7 +5,11 @@ import { __ } from '@wordpress/i18n'; import { getConfig } from 'utils/checkout'; import request from 'wcpay/checkout/utils/request'; import { buildAjaxURL } from 'wcpay/payment-request/utils'; -import { getTargetElement, validateEmail } from '../utils'; +import { + getTargetElement, + validateEmail, + appendRedirectionParams, +} from '../utils'; import wcpayTracks from 'tracks'; export const expressCheckoutIframe = async ( api, context, emailSelector ) => { @@ -250,7 +254,9 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { true ); if ( e.data.redirectUrl ) { - window.location = e.data.redirectUrl; + window.location = appendRedirectionParams( + e.data.redirectUrl + ); } break; case 'redirect_to_platform_checkout': @@ -269,7 +275,9 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { return; } if ( response.result === 'success' ) { - window.location = response.url; + window.location = appendRedirectionParams( + response.url + ); } else { showErrorMessage(); closeIframe( false ); diff --git a/client/checkout/woopay/utils.js b/client/checkout/woopay/utils.js index 48b25423d0b..a7b9a3a6152 100644 --- a/client/checkout/woopay/utils.js +++ b/client/checkout/woopay/utils.js @@ -39,3 +39,22 @@ export const validateEmail = ( value ) => { /* eslint-enable */ return pattern.test( value ); }; + +export const appendRedirectionParams = ( woopayUrl ) => { + const isPayForOrder = window.wcpayConfig.pay_for_order; + const orderId = window.wcpayConfig.order_id; + const key = window.wcpayConfig.key; + const billingEmail = window.wcpayConfig.billing_email; + + if ( ! isPayForOrder || ! orderId || ! key ) { + return woopayUrl; + } + + const url = new URL( woopayUrl ); + url.searchParams.append( 'pay_for_order', isPayForOrder ); + url.searchParams.append( 'order_id', orderId ); + url.searchParams.append( 'key', key ); + url.searchParams.append( 'billing_email', billingEmail ); + + return url.href; +}; diff --git a/includes/class-wc-payments-express-checkout-button-display-handler.php b/includes/class-wc-payments-express-checkout-button-display-handler.php index 57465be523c..b5cc5b1b55b 100644 --- a/includes/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/class-wc-payments-express-checkout-button-display-handler.php @@ -57,11 +57,11 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Paym add_action( 'woocommerce_after_add_to_cart_form', [ $this, 'display_express_checkout_buttons' ], 1 ); add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_buttons' ], 21 ); add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_express_checkout_buttons' ], 1 ); + } - if ( $is_payment_request_enabled ) { - // Load separator on the Pay for Order page. - add_action( 'before_woocommerce_pay_form', [ $this, 'display_express_checkout_buttons' ], 1 ); - } + if ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '10.8.0', '>=' ) ) { + add_action( 'before_woocommerce_pay_form', [ $this, 'add_pay_for_order_params_to_js_config' ] ); + add_action( 'woocommerce_pay_order_before_payment', [ $this, 'display_express_checkout_buttons' ], 1 ); } } @@ -111,4 +111,27 @@ public function display_express_checkout_buttons() { public function is_woopay_enabled() { return $this->platform_checkout_button_handler->is_woopay_enabled(); } + + /** + * Add the Pay for order params to the JS config. + * + * @param WC_Order $order The pay-for-order order. + */ + public function add_pay_for_order_params_to_js_config( $order ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) ) { + add_filter( + 'wcpay_payment_fields_js_config', + function( $js_config ) use ( $order ) { + $js_config['order_id'] = $order->get_id(); + $js_config['pay_for_order'] = sanitize_text_field( wp_unslash( $_GET['pay_for_order'] ) ); + $js_config['key'] = sanitize_text_field( wp_unslash( $_GET['key'] ) ); + $js_config['billing_email'] = $order->get_billing_email(); + + return $js_config; + } + ); + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } } diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index 168ab5ee8f3..eabaa7d1cac 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -529,10 +529,6 @@ public function should_show_woopay_button() { return false; } - if ( $this->is_pay_for_order_page() ) { - return false; - } - if ( ! is_user_logged_in() ) { // On product page for a subscription product, but not logged in, making WooPay unavailable. if ( $this->is_product() ) { From 904a8da1df6ea80d1632109fc0005c2848574f6f Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Wed, 13 Sep 2023 10:07:39 +0300 Subject: [PATCH 51/84] Follow up of Warn about dev mode (#7188) --- changelog/fix-6429-follow-up-fix | 5 +++++ client/onboarding/steps/mode-choice.tsx | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-6429-follow-up-fix diff --git a/changelog/fix-6429-follow-up-fix b/changelog/fix-6429-follow-up-fix new file mode 100644 index 00000000000..426b76ace57 --- /dev/null +++ b/changelog/fix-6429-follow-up-fix @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Warn about dev mode roll back to inline notice + + diff --git a/client/onboarding/steps/mode-choice.tsx b/client/onboarding/steps/mode-choice.tsx index 5b1346a179a..00af3d247be 100644 --- a/client/onboarding/steps/mode-choice.tsx +++ b/client/onboarding/steps/mode-choice.tsx @@ -13,12 +13,12 @@ import RadioCard from 'components/radio-card'; import { useStepperContext } from 'components/stepper'; import { trackModeSelected } from '../tracking'; import strings from '../strings'; -import BannerNotice from 'components/banner-notice'; +import InlineNotice from 'components/inline-notice'; const DevModeNotice = () => ( - + { strings.steps.mode.devModeNotice } - + ); const ModeChoice: React.FC = () => { From 31175a8086ae641a957cea156e50b28b702e8fd0 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 13 Sep 2023 17:21:37 +1000 Subject: [PATCH 52/84] Update the links used in the migrate option and automatically notice (#7189) --- changelog/update-stripe-billing-notice-links | 5 +++++ .../stripe-billing-notices/migrate-automatically-notice.tsx | 2 +- .../stripe-billing-notices/migrate-option-notice.tsx | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog/update-stripe-billing-notice-links diff --git a/changelog/update-stripe-billing-notice-links b/changelog/update-stripe-billing-notice-links new file mode 100644 index 00000000000..8f2ad33678b --- /dev/null +++ b/changelog/update-stripe-billing-notice-links @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: No changelog is needed given these notices are unreleased. + + diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx index 5d58ef84438..4cc69c9b077 100644 --- a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx @@ -85,7 +85,7 @@ const MigrateAutomaticallyNotice: React.FC< Props > = ( { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx index 5a32ee9f3ab..96b94df0081 100644 --- a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx @@ -134,7 +134,7 @@ const MigrateOptionNotice: React.FC< Props > = ( { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } From 732866895bf8f336e04735882e657e0da3e1932c Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 13 Sep 2023 17:21:56 +1000 Subject: [PATCH 53/84] Revert "Update Subscriptions with WooPayments eligibility as we move to deprecate this functionality" (#7194) Co-authored-by: mattallan --- .../issue-6510-deprecate-wcpay-subscriptions | 4 -- .../wcpay-subscriptions-toggle.js | 6 +- includes/class-wc-payments-features.php | 65 ++----------------- .../class-wc-payments-subscriptions.php | 13 ---- 4 files changed, 10 insertions(+), 78 deletions(-) delete mode 100644 changelog/issue-6510-deprecate-wcpay-subscriptions diff --git a/changelog/issue-6510-deprecate-wcpay-subscriptions b/changelog/issue-6510-deprecate-wcpay-subscriptions deleted file mode 100644 index b40eaae8139..00000000000 --- a/changelog/issue-6510-deprecate-wcpay-subscriptions +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Only display the WCPay Subscriptions setting to existing users as part of deprecating this feature. diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index 3305739e565..0f1f3f2937b 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -30,8 +30,12 @@ const WCPaySubscriptionsToggle = () => { updateIsWCPaySubscriptionsEnabled( value ); }; + /** + * Only show the toggle if the site doesn't have WC Subscriptions active and is eligible + * for wcpay subscriptions or if wcpay subscriptions are already enabled. + */ return ! wcpaySettings.isSubscriptionsActive && - isWCPaySubscriptionsEligible ? ( + ( isWCPaySubscriptionsEligible || isWCPaySubscriptionsEnabled ) ? ( 1, - 'subscription_status' => 'any', - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - [ - 'key' => '_wcpay_subscription_id', - 'compare' => 'EXISTS', - ], - ], - ] - ); - - if ( count( $wcpay_subscriptions ) > 0 ) { - return true; - } - } - - /** - * Check if they have at least 1 Stripe Billing enabled product. - */ - $stripe_billing_meta_query_handler = function ( $query, $query_vars ) { - if ( ! empty( $query_vars['stripe_billing_product'] ) ) { - $query['meta_query'][] = [ - 'key' => '_wcpay_product_hash', - 'compare' => 'EXISTS', - ]; - } - - return $query; - }; - - add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 ); - - $subscription_products = wc_get_products( - [ - 'limit' => 1, - 'type' => [ 'subscription', 'variable-subscription' ], - 'status' => 'publish', - 'return' => 'ids', - 'stripe_billing_product' => 'true', - ] - ); - - remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 ); - - if ( count( $subscription_products ) > 0 ) { - return true; + if ( ! function_exists( 'wc_get_base_location' ) ) { + return false; } - return false; + $store_base_location = wc_get_base_location(); + return ! empty( $store_base_location['country'] ) && 'US' === $store_base_location['country']; } /** diff --git a/includes/subscriptions/class-wc-payments-subscriptions.php b/includes/subscriptions/class-wc-payments-subscriptions.php index cd0a5101337..c30c6248d5c 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions.php +++ b/includes/subscriptions/class-wc-payments-subscriptions.php @@ -95,8 +95,6 @@ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Cus include_once __DIR__ . '/class-wc-payments-subscriptions-migrator.php'; self::$stripe_billing_migrator = new WC_Payments_Subscriptions_Migrator( $api_client ); } - - add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'maybe_disable_wcpay_subscriptions_on_update' ] ); } /** @@ -158,15 +156,4 @@ public static function is_duplicate_site() { return class_exists( 'WCS_Staging' ) && WCS_Staging::is_duplicate_site(); } - - /** - * Disable the WCPay Subscriptions feature on WooPayments plugin update if it's enabled and the store is no longer eligible. - * - * @see WC_Payments_Features::is_wcpay_subscriptions_eligible() for eligibility criteria. - */ - public static function maybe_disable_wcpay_subscriptions_on_update() { - if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() && ! WC_Payments_Features::is_wcpay_subscriptions_eligible() ) { - update_option( WC_Payments_Features::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ); - } - } } From 5e89beb5d9b44a171815356a60d2617d58a22542 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 13 Sep 2023 10:23:37 +0200 Subject: [PATCH 54/84] fix: remove max width on WooPay checkout appearance (#7184) --- changelog/fix-woopay-appearance-width | 4 ++++ client/settings/express-checkout-settings/index.scss | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-woopay-appearance-width diff --git a/changelog/fix-woopay-appearance-width b/changelog/fix-woopay-appearance-width new file mode 100644 index 00000000000..5a11ed32a87 --- /dev/null +++ b/changelog/fix-woopay-appearance-width @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix checkout appearance width diff --git a/client/settings/express-checkout-settings/index.scss b/client/settings/express-checkout-settings/index.scss index 8db0f6cbbbe..256f77588f8 100644 --- a/client/settings/express-checkout-settings/index.scss +++ b/client/settings/express-checkout-settings/index.scss @@ -61,7 +61,6 @@ .woopay-settings { &__custom-message-wrapper { - max-width: 500px; position: relative; .components-base-control__field .components-text-control__input { From 152be9b2f416adc0d511ae0da93fde3594ccdec0 Mon Sep 17 00:00:00 2001 From: Eduardo Pieretti Umpierre Date: Wed, 13 Sep 2023 16:44:54 -0300 Subject: [PATCH 55/84] Change ConvertedAmount component to use an updated Tooltip (#7137) --- changelog/update-6991-table-tooltip | 4 ++ client/transactions/list/converted-amount.tsx | 58 +++++++++++++------ client/transactions/list/style.scss | 8 ++- .../__snapshots__/converted-amount.tsx.snap | 4 +- .../list/test/__snapshots__/index.tsx.snap | 12 ++-- 5 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 changelog/update-6991-table-tooltip diff --git a/changelog/update-6991-table-tooltip b/changelog/update-6991-table-tooltip new file mode 100644 index 00000000000..9c50fe720f6 --- /dev/null +++ b/changelog/update-6991-table-tooltip @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Tooltip component on ConvertedAmount. diff --git a/client/transactions/list/converted-amount.tsx b/client/transactions/list/converted-amount.tsx index fce2497957b..6a74b01a25a 100644 --- a/client/transactions/list/converted-amount.tsx +++ b/client/transactions/list/converted-amount.tsx @@ -5,42 +5,54 @@ */ import React from 'react'; import { __, sprintf } from '@wordpress/i18n'; -import { Tooltip } from '@wordpress/components'; +import { Tooltip as FallbackTooltip } from '@wordpress/components'; import SyncIcon from 'gridicons/dist/sync'; +import classNames from 'classnames'; /** * Internal dependencies */ import { formatExplicitCurrency } from 'utils/currency'; +declare const window: any; + interface ConversionIndicatorProps { amount: number; currency: string; baseCurrency: string; + fallback?: boolean; } const ConversionIndicator = ( { amount, currency, + fallback, baseCurrency, -}: ConversionIndicatorProps ): React.ReactElement => ( - - { + // If it's available, use the component from WP, not the one within WCPay, as WP's uses an updated component. + const Tooltip = ! fallback + ? window?.wp?.components?.Tooltip + : FallbackTooltip; + + return ( + - - - -); + + + + + ); +}; interface ConvertedAmountProps { amount: number; @@ -62,11 +74,19 @@ const ConvertedAmount = ( { return <>{ formattedCurrency }; } + const isUpdatedTooltipAvailable = !! window?.wp?.components?.Tooltip; + return ( -
+
{ formattedCurrency } diff --git a/client/transactions/list/style.scss b/client/transactions/list/style.scss index f5ee0405cc3..e33b7755e91 100644 --- a/client/transactions/list/style.scss +++ b/client/transactions/list/style.scss @@ -16,9 +16,11 @@ $space-header-item: 12px; fill: $studio-gray-30; } - .components-popover__content { - position: relative; - top: -( $gap * 2 ); // Positioning the tooltip in a higher position to avoid having it cropped by the bottom of the table + &--fallback { + .components-popover__content { + position: relative; + top: -( $gap * 2 ); // Positioning the tooltip in a higher position to avoid having it cropped by the bottom of the table + } } } diff --git a/client/transactions/list/test/__snapshots__/converted-amount.tsx.snap b/client/transactions/list/test/__snapshots__/converted-amount.tsx.snap index 522c18bb7bf..7c5c9e2fe60 100644 --- a/client/transactions/list/test/__snapshots__/converted-amount.tsx.snap +++ b/client/transactions/list/test/__snapshots__/converted-amount.tsx.snap @@ -3,7 +3,7 @@ exports[`ConvertedAmount renders an amount with conversion icon and tooltip 1`] = `
Date: Wed, 13 Sep 2023 18:46:17 -0300 Subject: [PATCH 56/84] Bump WC and PHP versions (#7134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: CĂ©sar Costa <10233985+cesarcosta99@users.noreply.github.com> Co-authored-by: CĂ©sar Costa --- .github/workflows/compatibility.yml | 9 +++++---- .github/workflows/e2e-test.yml | 2 +- .github/workflows/php-lint-test.yml | 6 +++--- changelog/dev-bump-min-wc-8-1-php-7-4 | 4 ++++ composer.json | 4 ++-- phpcs-compat.xml.dist | 4 ++-- phpcs.xml.dist | 4 ++-- readme.txt | 12 ++++++------ woocommerce-payments.php | 8 ++++---- 9 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 changelog/dev-bump-min-wc-8-1-php-7-4 diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 4dd1d7c6512..808529f66ad 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -4,9 +4,10 @@ on: pull_request env: - WC_MIN_SUPPORTED_VERSION: '7.6.0' - WP_MIN_SUPPORTED_VERSION: '6.0' - PHP_MIN_SUPPORTED_VERSION: '7.3' + WC_MIN_SUPPORTED_VERSION: '7.7.0' + WP_MIN_SUPPORTED_VERSION: '6.1' + PHP_MIN_SUPPORTED_VERSION: '7.4' + GUTENBERG_VERSION_FOR_WP_MIN: '15.7.0' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -23,7 +24,7 @@ jobs: id: generate_matrix run: | WC_VERSIONS=$( echo "[\"$WC_MIN_SUPPORTED_VERSION\", \"latest\", \"beta\"]" ) - MATRIX_INCLUDE=$( echo "[{\"woocommerce\":\"$WC_MIN_SUPPORTED_VERSION\",\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"13.6.0\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) + MATRIX_INCLUDE=$( echo "[{\"woocommerce\":\"$WC_MIN_SUPPORTED_VERSION\",\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"$GUTENBERG_VERSION_FOR_WP_MIN\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) echo "matrix={\"woocommerce\":$WC_VERSIONS,\"wordpress\":[\"latest\"],\"gutenberg\":[\"latest\"],\"php\":[\"7.4\"], \"include\":$MATRIX_INCLUDE}" >> $GITHUB_OUTPUT woocommerce-compatibility: diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 11cc17bafab..75efa83685a 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -23,7 +23,7 @@ env: E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }} E2E_USE_LOCAL_SERVER: false E2E_RESULT_FILEPATH: 'tests/e2e/results.json' - WC_MIN_SUPPORTED_VERSION: '7.6.0' + WC_MIN_SUPPORTED_VERSION: '7.7.0' NODE_ENV: 'test' FORCE_E2E_DEPS_SETUP: true diff --git a/.github/workflows/php-lint-test.yml b/.github/workflows/php-lint-test.yml index 5b397ab30d4..bf0cbd0623a 100644 --- a/.github/workflows/php-lint-test.yml +++ b/.github/workflows/php-lint-test.yml @@ -6,9 +6,9 @@ on: env: WP_VERSION: latest - WC_MIN_SUPPORTED_VERSION: '7.6.0' + WC_MIN_SUPPORTED_VERSION: '7.7.0' GUTENBERG_VERSION: latest - PHP_MIN_SUPPORTED_VERSION: '7.3' + PHP_MIN_SUPPORTED_VERSION: '7.4' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -41,7 +41,7 @@ jobs: - name: "Generate matrix" id: generate_matrix run: | - PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"7.3\", \"7.4\"]" ) + PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"8.0\", \"8.1\"]" ) echo "matrix={\"php\":$PHP_VERSIONS}" >> $GITHUB_OUTPUT test: diff --git a/changelog/dev-bump-min-wc-8-1-php-7-4 b/changelog/dev-bump-min-wc-8-1-php-7-4 new file mode 100644 index 00000000000..989290fa6f3 --- /dev/null +++ b/changelog/dev-bump-min-wc-8-1-php-7-4 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Bump minimum required version of WooCommerce to 7.7 and PHP to 7.4. diff --git a/composer.json b/composer.json index 668bf0f26a7..6b70464800c 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "minimum-stability": "dev", "config": { "platform": { - "php": "7.3" + "php": "7.4" }, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, @@ -20,7 +20,7 @@ } }, "require": { - "php": ">=7.2", + "php": ">=7.4", "ext-json": "*", "automattic/jetpack-connection": "1.51.7", "automattic/jetpack-config": "1.15.2", diff --git a/phpcs-compat.xml.dist b/phpcs-compat.xml.dist index 996f8374292..003c335b266 100644 --- a/phpcs-compat.xml.dist +++ b/phpcs-compat.xml.dist @@ -15,8 +15,8 @@ tests/ - - + + diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 9f8efd70ee5..1b3888752d3 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -16,8 +16,8 @@ ./lib/* - - + + diff --git a/readme.txt b/readme.txt index 4c4df33925b..19e94df58cd 100644 --- a/readme.txt +++ b/readme.txt @@ -1,9 +1,9 @@ === WooPayments - Fully Integrated Solution Built and Supported by Woo === Contributors: woocommerce, automattic Tags: payment gateway, payment, apple pay, credit card, google pay, woocommerce payments -Requires at least: 6.0 -Tested up to: 6.2 -Requires PHP: 7.3 +Requires at least: 6.1 +Tested up to: 6.3 +Requires PHP: 7.4 Stable tag: 6.4.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -38,9 +38,9 @@ Our global support team is available to answer questions you may have about WooP = Requirements = -* WordPress 6.0 or newer. -* WooCommerce 7.6 or newer. -* PHP 7.3 or newer is recommended. +* WordPress 6.1 or newer. +* WooCommerce 7.7 or newer. +* PHP 7.4 or newer is recommended. = Try it now = diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 5c9fb56f634..0cb73a1b183 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -8,10 +8,10 @@ * Woo: 5278104:bf3cf30871604e15eec560c962593c1f * Text Domain: woocommerce-payments * Domain Path: /languages - * WC requires at least: 7.6 - * WC tested up to: 7.8.0 - * Requires at least: 6.0 - * Requires PHP: 7.3 + * WC requires at least: 7.7 + * WC tested up to: 7.9.0 + * Requires at least: 6.1 + * Requires PHP: 7.4 * Version: 6.4.1 * * @package WooCommerce\Payments From 88d45ebeb719934e9e889138231ba3a40e3effec Mon Sep 17 00:00:00 2001 From: deepakpathania <68396823+deepakpathania@users.noreply.github.com> Date: Thu, 14 Sep 2023 15:12:53 +0530 Subject: [PATCH 57/84] Update occurence of all ubuntu versions to `ubuntu-latest` (#7209) --- .github/workflows/build-zip-and-run-smoke-tests.yml | 2 +- .github/workflows/check-changelog.yml | 2 +- .github/workflows/compatibility.yml | 8 ++++---- .github/workflows/coverage.yml | 2 +- .github/workflows/create-pre-release.yml | 2 +- .github/workflows/e2e-pull-request.yml | 2 +- .github/workflows/e2e-test.yml | 8 ++++---- .github/workflows/i18n-weekly-release.yml | 2 +- .github/workflows/js-lint-test.yml | 4 ++-- .github/workflows/php-compatibility.yml | 2 +- .github/workflows/php-lint-test.yml | 6 +++--- .github/workflows/post-release-updates.yml | 10 +++++----- .github/workflows/pr-build-live-branch.yml | 2 +- .github/workflows/release-changelog.yml | 2 +- .github/workflows/release-code-freeze.yml | 4 ++-- .github/workflows/release-pr.yml | 2 +- changelog/dev-ubuntu-workflow | 4 ++++ 17 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 changelog/dev-ubuntu-workflow diff --git a/.github/workflows/build-zip-and-run-smoke-tests.yml b/.github/workflows/build-zip-and-run-smoke-tests.yml index 94599b3b88e..7afaf05a833 100644 --- a/.github/workflows/build-zip-and-run-smoke-tests.yml +++ b/.github/workflows/build-zip-and-run-smoke-tests.yml @@ -24,7 +24,7 @@ on: jobs: build-zip: name: "Build the zip file" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: "Checkout repository" uses: actions/checkout@v3 diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml index 25cf89903ca..c44b2b0191e 100644 --- a/.github/workflows/check-changelog.yml +++ b/.github/workflows/check-changelog.yml @@ -11,7 +11,7 @@ concurrency: jobs: check-changelog: name: Check changelog - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 808529f66ad..6b9ade34d58 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -16,7 +16,7 @@ concurrency: jobs: generate-wc-compat-matrix: name: "Generate the matrix for woocommerce compatibility dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -30,7 +30,7 @@ jobs: woocommerce-compatibility: name: "WC compatibility" needs: generate-wc-compat-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: WP_VERSION: ${{ matrix.wordpress }} WC_VERSION: ${{ matrix.woocommerce }} @@ -58,7 +58,7 @@ jobs: generate-wc-compat-beta-matrix: name: "Generate the matrix for compatibility-woocommerce-beta dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -72,7 +72,7 @@ jobs: compatibility-woocommerce-beta: name: Environment - WC beta needs: generate-wc-compat-beta-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: ${{ fromJSON(needs.generate-wc-compat-beta-matrix.outputs.matrix) }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ddc2db674bc..b1401136de9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,7 +10,7 @@ concurrency: jobs: woocommerce-coverage: name: Code coverage - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 10 diff --git a/.github/workflows/create-pre-release.yml b/.github/workflows/create-pre-release.yml index 80ab331c563..65c20427376 100644 --- a/.github/workflows/create-pre-release.yml +++ b/.github/workflows/create-pre-release.yml @@ -16,7 +16,7 @@ defaults: jobs: create-release: name: "Create the pre-release" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ inputs.releaseVersion }} diff --git a/.github/workflows/e2e-pull-request.yml b/.github/workflows/e2e-pull-request.yml index c8363faa490..ab0cd702a86 100644 --- a/.github/workflows/e2e-pull-request.yml +++ b/.github/workflows/e2e-pull-request.yml @@ -42,7 +42,7 @@ concurrency: jobs: wcpay-e2e-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 75efa83685a..ddc50e45526 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -30,7 +30,7 @@ env: jobs: generate-matrix: name: "Generate the matrix for subscriptions-tests dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -42,7 +42,7 @@ jobs: # Run WCPay & subscriptions tests against specific WC versions wcpay-subscriptions-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: generate-matrix strategy: fail-fast: false @@ -70,7 +70,7 @@ jobs: # Run tests against WC Checkout blocks & WC latest # [TODO] Unskip blocks tests after investigating constant failures. # blocks-tests: - # runs-on: ubuntu-20.04 + # runs-on: ubuntu-latest # name: WC - latest | blocks - shopper # env: @@ -93,7 +93,7 @@ jobs: # Run tests against WP Nightly & WC latest wp-nightly-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/i18n-weekly-release.yml b/.github/workflows/i18n-weekly-release.yml index c2abf63e34c..11dd8a89705 100644 --- a/.github/workflows/i18n-weekly-release.yml +++ b/.github/workflows/i18n-weekly-release.yml @@ -6,7 +6,7 @@ on: jobs: i18n-release: name: Release - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository diff --git a/.github/workflows/js-lint-test.yml b/.github/workflows/js-lint-test.yml index 4824d78ca19..fdbea1d59b0 100644 --- a/.github/workflows/js-lint-test.yml +++ b/.github/workflows/js-lint-test.yml @@ -11,7 +11,7 @@ concurrency: jobs: lint: name: JS linting - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 @@ -32,7 +32,7 @@ jobs: test: name: JS testing - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index 8161804f6a2..abf8413b84c 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -11,7 +11,7 @@ jobs: # Check for version-specific PHP compatibility php-compatibility: name: PHP Compatibility - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: "Set up PHP" diff --git a/.github/workflows/php-lint-test.yml b/.github/workflows/php-lint-test.yml index bf0cbd0623a..0a55baaeb8e 100644 --- a/.github/workflows/php-lint-test.yml +++ b/.github/workflows/php-lint-test.yml @@ -17,7 +17,7 @@ concurrency: jobs: lint: name: PHP linting - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 @@ -34,7 +34,7 @@ jobs: generate-test-matrix: name: "Generate the matrix for php tests dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -47,7 +47,7 @@ jobs: test: name: PHP testing needs: generate-test-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 10 diff --git a/.github/workflows/post-release-updates.yml b/.github/workflows/post-release-updates.yml index f19be159407..141c53e0b16 100644 --- a/.github/workflows/post-release-updates.yml +++ b/.github/workflows/post-release-updates.yml @@ -11,7 +11,7 @@ defaults: jobs: get-last-released-version: name: "Get the last released version" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: releaseVersion: ${{ steps.current-version.outputs.RELEASE_VERSION }} @@ -31,7 +31,7 @@ jobs: create-gh-release: name: "Create a GH release" needs: get-last-released-version - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.get-last-released-version.outputs.releaseVersion }} @@ -75,7 +75,7 @@ jobs: merge-trunk-into-develop: name: "Merge trunk back into develop" needs: get-last-released-version - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.get-last-released-version.outputs.releaseVersion }} @@ -98,7 +98,7 @@ jobs: trigger-translations: name: "Trigger translations update for the release" needs: [ get-last-released-version, create-gh-release ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: "Checkout repository (trunk)" uses: actions/checkout@v3 @@ -114,7 +114,7 @@ jobs: update-wiki: name: "Update the wiki for the next release" needs: get-last-released-version - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.get-last-released-version.outputs.releaseVersion }} diff --git a/.github/workflows/pr-build-live-branch.yml b/.github/workflows/pr-build-live-branch.yml index 3ff165f2898..1fa9742f0ea 100644 --- a/.github/workflows/pr-build-live-branch.yml +++ b/.github/workflows/pr-build-live-branch.yml @@ -10,7 +10,7 @@ concurrency: jobs: build-and-inform-zip-file: name: "Build and inform the zip file" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: "Checkout repository" uses: actions/checkout@v3 diff --git a/.github/workflows/release-changelog.yml b/.github/workflows/release-changelog.yml index c99db69b350..4c9c7a7a8b1 100644 --- a/.github/workflows/release-changelog.yml +++ b/.github/workflows/release-changelog.yml @@ -29,7 +29,7 @@ defaults: jobs: process-changelog: name: "Process the changelog" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: CHANGELOG_ACTION: ${{ inputs.action-type }} RELEASE_VERSION: ${{ inputs.release-version }} diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml index 84760b24ebc..7b81c24d138 100644 --- a/.github/workflows/release-code-freeze.yml +++ b/.github/workflows/release-code-freeze.yml @@ -19,7 +19,7 @@ defaults: jobs: check-code-freeze: name: "Check that today is the day of the code freeze" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: freeze: ${{ steps.check-freeze.outputs.FREEZE }} nextReleaseVersion: ${{ steps.next-version.outputs.NEXT_RELEASE_VERSION }} @@ -81,7 +81,7 @@ jobs: name: "Send notification to Slack" needs: [check-code-freeze, create-release-pr] if: ${{ ! ( inputs.skipSlackPing && needs.create-release-pr.outputs.release-pr-id ) }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.check-code-freeze.outputs.nextReleaseVersion }} RELEASE_DATE: ${{ needs.check-code-freeze.outputs.nextReleaseDate }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index f14a49df77b..0433e03eb51 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -53,7 +53,7 @@ defaults: jobs: prepare-release: name: "Prepare a stable release" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: branch: ${{ steps.create_branch.outputs.branch-name }} release-pr-id: ${{ steps.create-pr-to-trunk.outputs.RELEASE_PR_ID }} diff --git a/changelog/dev-ubuntu-workflow b/changelog/dev-ubuntu-workflow new file mode 100644 index 00000000000..79c42cc4fc7 --- /dev/null +++ b/changelog/dev-ubuntu-workflow @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Comment: Update occurence of all ubuntu versions to ubuntu-latest From 37bbac346e3cfddc130c564cbcb0c15ee8f20e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Thu, 14 Sep 2023 14:25:18 +0200 Subject: [PATCH 58/84] Increase admin enqueue scripts priority (#7197) --- changelog/fix-7067-admin-enqueue-scripts-priority | 4 ++++ includes/admin/class-wc-payments-admin.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-7067-admin-enqueue-scripts-priority diff --git a/changelog/fix-7067-admin-enqueue-scripts-priority b/changelog/fix-7067-admin-enqueue-scripts-priority new file mode 100644 index 00000000000..d23c2da6dd7 --- /dev/null +++ b/changelog/fix-7067-admin-enqueue-scripts-priority @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Increase admin enqueue scripts priority to avoid compatibility issues with WooCommerce Beta Tester plugin. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index c6695067a9e..3080cd9a4f8 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -145,8 +145,8 @@ public function __construct( add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 ); add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_overview_to_connect' ], 1 ); // Run this late (after `admin_init`) but before any scripts are actually enqueued. - add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 12 ); + add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 ); add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] ); add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'show_woopay_payment_method_name_admin' ] ); add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'display_wcpay_transaction_fee' ] ); From 4fa9b058f3b1ee418535c961ce2f22ab85b1c0cd Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Thu, 14 Sep 2023 14:30:09 +0200 Subject: [PATCH 59/84] Notices for deferred UPE rollout (#7210) Co-authored-by: Francesco --- changelog/deferred-upe-rollout-notices | 4 ++ client/payment-methods/index.js | 51 ++++++++++++++++++++++---- client/payment-methods/test/index.js | 12 +++--- 3 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 changelog/deferred-upe-rollout-notices diff --git a/changelog/deferred-upe-rollout-notices b/changelog/deferred-upe-rollout-notices new file mode 100644 index 00000000000..231d4bb7942 --- /dev/null +++ b/changelog/deferred-upe-rollout-notices @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Add notice for legacy UPE users about deferred UPE upcoming, and adjust wording for non-UPE users diff --git a/client/payment-methods/index.js b/client/payment-methods/index.js index 355174f37c1..d47b2ddb9e1 100644 --- a/client/payment-methods/index.js +++ b/client/payment-methods/index.js @@ -8,7 +8,6 @@ import { __ } from '@wordpress/i18n'; import { Button, Card, - CardDivider, CardHeader, DropdownMenu, ExternalLink, @@ -48,6 +47,8 @@ import ConfirmPaymentMethodActivationModal from './activation-modal'; import ConfirmPaymentMethodDeleteModal from './delete-modal'; import { getAdminUrl } from 'wcpay/utils'; import { getPaymentMethodDescription } from 'wcpay/utils/payment-methods'; +import InlineNotice from 'wcpay/components/inline-notice'; +import interpolateComponents from '@automattic/interpolate-components'; const PaymentMethodsDropdownMenu = ( { setOpenModal } ) => { return ( @@ -82,7 +83,6 @@ const UpeSetupBanner = () => { return ( <> - { >

{ __( - 'Boost your sales by accepting additional payment methods', + 'Enable the new WooPayments checkout experience, which will become the default on November 1, 2023', 'woocommerce-payments' ) }

{ __( /* eslint-disable-next-line max-len */ - 'Get access to additional payment methods and an improved checkout experience.', + 'This will improve the checkout experience and boost sales with access to additional payment methods, which you’ll be able to manage from here in settings.', 'woocommerce-payments' ) }

@@ -106,7 +106,7 @@ const UpeSetupBanner = () => { @@ -278,6 +278,30 @@ const PaymentMethods = () => { ) } + { isUpeEnabled && upeType === 'legacy' && ( + + + { interpolateComponents( { + mixedString: __( + 'The new WooPayments checkout experience will become the default on October 11, 2023.' + + ' {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + // eslint-disable-next-line max-len + + ), + }, + } ) } + + + ) } + { availableMethods.map( @@ -341,10 +365,21 @@ const PaymentMethods = () => { ) } - { isUpeSettingsPreviewEnabled && ! isUpeEnabled && ( - - ) } + + { isUpeSettingsPreviewEnabled && ! isUpeEnabled && ( + <> +
+ + + + + ) } + { activationModalParams && ( { diff --git a/client/payment-methods/test/index.js b/client/payment-methods/test/index.js index 2830f9d3c55..5d2ebf69ff9 100644 --- a/client/payment-methods/test/index.js +++ b/client/payment-methods/test/index.js @@ -227,7 +227,7 @@ describe( 'PaymentMethods', () => { ); const enableWooCommercePaymentText = screen.getByText( - 'Boost your sales by accepting additional payment methods' + 'Enable the new WooPayments checkout experience, which will become the default on November 1, 2023' ); expect( enableWooCommercePaymentText ).toBeInTheDocument(); @@ -342,7 +342,7 @@ describe( 'PaymentMethods', () => { ); const enableWooCommercePaymentText = screen.getByText( - 'Boost your sales by accepting additional payment methods' + 'Enable the new WooPayments checkout experience, which will become the default on November 1, 2023' ); expect( enableWooCommercePaymentText.parentElement ).not.toHaveClass( @@ -371,7 +371,7 @@ describe( 'PaymentMethods', () => { ); const enableWooCommercePaymentText = screen.getByText( - 'Boost your sales by accepting additional payment methods' + 'Enable the new WooPayments checkout experience, which will become the default on November 1, 2023' ); expect( enableWooCommercePaymentText.parentElement ).toHaveClass( @@ -404,7 +404,7 @@ describe( 'PaymentMethods', () => { ); const enableWooCommercePaymentText = screen.queryByText( - 'Boost your sales by accepting additional payment methods' + 'Enable the new WooPayments checkout experience, which will become the default on November 1, 2023' ); expect( enableWooCommercePaymentText ).toBeNull(); @@ -444,7 +444,7 @@ describe( 'PaymentMethods', () => { ).not.toBeInTheDocument(); } ); - test( 'clicking "Enable in your store" in express payments enable UPE and redirects', async () => { + test( 'clicking "Enable payment methods" in express payments enable UPE and redirects', async () => { Object.defineProperty( window, 'location', { value: { href: 'example.com/', @@ -471,7 +471,7 @@ describe( 'PaymentMethods', () => { ); const enableInYourStoreButton = screen.queryByRole( 'button', { - name: 'Enable in your store', + name: 'Enable payment methods', } ); expect( enableInYourStoreButton ).toBeInTheDocument(); From 68d8c64bd578c0df887ec3de56e9e343328985e0 Mon Sep 17 00:00:00 2001 From: Matt Allan Date: Thu, 14 Sep 2023 22:44:39 +1000 Subject: [PATCH 60/84] Verify the migrated subscription has a valid WooPayments payment token before completing the migration (#7178) Co-authored-by: James Allan --- changelog/verify_payment_token_on_migration | 5 + includes/class-wc-payments.php | 2 +- ...ass-wc-payments-subscriptions-migrator.php | 112 +++++++++++++++++- .../class-wc-payments-subscriptions.php | 5 +- 4 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 changelog/verify_payment_token_on_migration diff --git a/changelog/verify_payment_token_on_migration b/changelog/verify_payment_token_on_migration new file mode 100644 index 00000000000..87074355bf7 --- /dev/null +++ b/changelog/verify_payment_token_on_migration @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This PR comes as part of the migration script which hasn't been released yet. No changelog needed + + diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index c8c33814845..36ff9512279 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -627,7 +627,7 @@ public static function init() { // Load Stripe Billing subscription integration. if ( self::should_load_stripe_billing_integration() ) { include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions.php'; - WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account ); + WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account, self::$token_service ); } if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.9.0', '<' ) ) { diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index 1b9337bbb69..745d508a10a 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -42,6 +42,13 @@ class WC_Payments_Subscriptions_Migrator extends WCS_Background_Repairer { */ private $api_client; + /** + * WC_Payments_Token_Service instance. + * + * @var WC_Payments_Token_Service + */ + private $token_service; + /** * WC_Payments_Subscription_Migration_Log_Handler instance. * @@ -73,11 +80,13 @@ class WC_Payments_Subscriptions_Migrator extends WCS_Background_Repairer { /** * Constructor. * - * @param WC_Payments_API_Client|null $api_client WC_Payments_API_Client instance. + * @param WC_Payments_API_Client|null $api_client WC_Payments_API_Client instance. + * @param WC_Payments_Token_Service|null $token_service WC_Payments_Token_Service instance. */ - public function __construct( $api_client = null ) { - $this->api_client = $api_client; - $this->logger = new WC_Payments_Subscription_Migration_Log_Handler(); + public function __construct( $api_client = null, $token_service = null ) { + $this->api_client = $api_client; + $this->token_service = $token_service; + $this->logger = new WC_Payments_Subscription_Migration_Log_Handler(); // Don't copy migrated subscription meta keys to related orders. add_filter( 'wc_subscriptions_object_data', [ $this, 'exclude_migrated_meta' ], 10, 1 ); @@ -130,6 +139,11 @@ public function migrate_wcpay_subscription( $subscription_id, $attempt = 0 ) { $this->logger->log( sprintf( '---- Next payment date updated to %1$s to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription_id ) ); } + // If the subscription is active or on-hold, verify the payment method is valid and set correctly that it continues to renew. + if ( $subscription->has_status( [ 'active', 'on-hold' ] ) ) { + $this->verify_subscription_payment_token( $subscription, $wcpay_subscription ); + } + $this->update_wcpay_subscription_meta( $subscription ); $subscription->add_order_note( __( 'This subscription has been successfully migrated to a WooPayments tokenized subscription.', 'woocommerce-payments' ) ); @@ -306,6 +320,96 @@ private function get_wcpay_subscription_status( $wcpay_subscription ) { return $wcpay_subscription['status']; } + /** + * Verifies the payment token on the subscription matches the default payment method on the WCPay Subscription. + * + * This function does two things: + * 1. If the subscription doesn't have a WooPayments payment token, set it to the default payment method from Stripe Billing. + * 2. If the subscription has a token, verify the token matches the token on the Stripe Billing subscription + * + * @param WC_Subscription $subscription The subscription to verify the payment token on. + * @param array $wcpay_subscription The subscription data from Stripe. + */ + private function verify_subscription_payment_token( $subscription, $wcpay_subscription ) { + // If the subscription's payment method isn't set to WooPayments, we skip this token step. + if ( $subscription->get_payment_method() !== WC_Payment_Gateway_WCPay::GATEWAY_ID ) { + $this->logger->log( sprintf( '---- Skipped verifying the payment token. Subscription #%1$d is no longer set to "woocommerce_payments".', $subscription->get_id() ) ); + return; + } + + unset( $wcpay_subscription['default_payment_method'] ); + + if ( empty( $wcpay_subscription['default_payment_method'] ) ) { + $this->logger->log( sprintf( '---- Could not verify the payment method. Stripe Billing subscription (%1$s) does not have a default payment method.', $wcpay_subscription['id'] ?? 'unknown' ) ); + return; + } + + $tokens = $subscription->get_payment_tokens(); + $token_id = end( $tokens ); + $token = ! $token_id ? null : WC_Payment_Tokens::get( $token_id ); + + // If the token matches the default payment method on the Stripe Billing subscription, we're done here. + if ( $token && $token->get_token() === $wcpay_subscription['default_payment_method'] ) { + $this->logger->log( sprintf( '---- Payment token on subscription #%1$d matches the payment method on the Stripe Billing subscription (%2$s).', $subscription->get_id(), $wcpay_subscription['id'] ?? 'unknown' ) ); + return; + } + + // At this point we know the subscription doesn't have a token or the token doesn't match, add one using the default payment method on the WCPay Subscription. + $new_token = $this->maybe_create_and_update_payment_token( $subscription, $wcpay_subscription ); + + if ( $new_token ) { + $this->logger->log( sprintf( '---- Payment token on subscription #%1$d has been updated (from %2$s to %3$s) to match the payment method on the Stripe Billing subscription.', $subscription->get_id(), $token ? $token->get_token() : 'missing', $wcpay_subscription['default_payment_method'] ) ); + } + } + + /** + * Locates a payment token or creates one if it doesn't exist, then updates the subscription with the new token. + * + * @param WC_Subscription $subscription The subscription to add the payment token to. + * @param array $wcpay_subscription The subscription data from Stripe. + * + * @return WC_Payment_Token|false The new payment token or false if the token couldn't be created. + */ + private function maybe_create_and_update_payment_token( $subscription, $wcpay_subscription ) { + $token = false; + $user = new WP_User( $subscription->get_user_id() ); + $customer_tokens = WC_Payment_Tokens::get_tokens( + [ + 'user_id' => $user->ID, + 'gateway_id' => WC_Payment_Gateway_WCPay::GATEWAY_ID, + 'limit' => WC_Payment_Gateway_WCPay::USER_FORMATTED_TOKENS_LIMIT, + ] + ); + + foreach ( $customer_tokens as $customer_token ) { + if ( $customer_token->get_token() === $wcpay_subscription['default_payment_method'] ) { + $token = $customer_token; + break; + } + } + + // If we didn't find a token linked to the subscription customer, create one. + if ( ! $token ) { + try { + $token = $this->token_service->add_payment_method_to_user( $wcpay_subscription['default_payment_method'], $user ); + $this->logger->log( sprintf( '---- Created a new payment token (%1$s) for subscription #%2$d.', $token->get_token(), $subscription->get_id() ) ); + } catch ( \Exception $e ) { + $this->logger->log( sprintf( '---- WARNING: Subscription #%1$d is missing a payment token and we failed to create one. Error: %2$s', $subscription->get_id(), $e->getMessage() ) ); + return; + } + } + + // Prevent the WC_Payments_Subscriptions class from attempting to update the Stripe Billing subscription's payment method while we set the token. + remove_action( 'woocommerce_payment_token_added_to_order', [ WC_Payments_Subscriptions::get_subscription_service(), 'update_wcpay_subscription_payment_method' ], 10 ); + + $subscription->add_payment_token( $token ); + + // Reattach. + add_action( 'woocommerce_payment_token_added_to_order', [ WC_Payments_Subscriptions::get_subscription_service(), 'update_wcpay_subscription_payment_method' ], 10, 3 ); + + return $token; + } + /** * Prevents migrated WCPay subscription metadata being copied to subscription related orders (renewal/switch/resubscribe). * diff --git a/includes/subscriptions/class-wc-payments-subscriptions.php b/includes/subscriptions/class-wc-payments-subscriptions.php index c30c6248d5c..e26020cb335 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions.php +++ b/includes/subscriptions/class-wc-payments-subscriptions.php @@ -63,8 +63,9 @@ class WC_Payments_Subscriptions { * @param WC_Payments_Customer_Service $customer_service WCPay Customer Service. * @param WC_Payments_Order_Service $order_service WCPay Order Service. * @param WC_Payments_Account $account WC_Payments_Account. + * @param WC_Payments_Token_Service $token_service WC_Payments_Token_Service. */ - public static function init( WC_Payments_API_Client $api_client, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service, WC_Payments_Account $account ) { + public static function init( WC_Payments_API_Client $api_client, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service, WC_Payments_Account $account, WC_Payments_Token_Service $token_service ) { // Store dependencies. self::$order_service = $order_service; @@ -93,7 +94,7 @@ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Cus if ( class_exists( 'WCS_Background_Repairer' ) ) { include_once __DIR__ . '/class-wc-payments-subscriptions-migrator.php'; - self::$stripe_billing_migrator = new WC_Payments_Subscriptions_Migrator( $api_client ); + self::$stripe_billing_migrator = new WC_Payments_Subscriptions_Migrator( $api_client, $token_service ); } } From 343cd67eb58018daea9eb1e30c4cc9e1a3efd69c Mon Sep 17 00:00:00 2001 From: Taha Paksu <3295+tpaksu@users.noreply.github.com> Date: Thu, 14 Sep 2023 15:44:52 +0300 Subject: [PATCH 61/84] Fix single currency settings saving manual rate (#7208) --- ...7-multi-currency-manual-exchange-rate-is-not-saved | 4 ++++ .../multi-currency/single-currency-settings/index.js | 11 ++++++++--- includes/multi-currency/MultiCurrency.php | 3 ++- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved diff --git a/changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved b/changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved new file mode 100644 index 00000000000..3b44f6d3ef0 --- /dev/null +++ b/changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix single currency manual rate save producing error when no changes are made diff --git a/client/multi-currency/single-currency-settings/index.js b/client/multi-currency/single-currency-settings/index.js index ae28c53bea4..8285f206dd8 100644 --- a/client/multi-currency/single-currency-settings/index.js +++ b/client/multi-currency/single-currency-settings/index.js @@ -279,11 +279,16 @@ const SingleCurrencySettings = () => { exchangeRateType === 'manual' } - onChange={ () => + onChange={ () => { setExchangeRateType( 'manual' - ) - } + ); + setManualRate( + manualRate + ? manualRate + : targetCurrency.rate + ); + } } />

{ __( diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php index bf75d594fef..d54f1e731d2 100644 --- a/includes/multi-currency/MultiCurrency.php +++ b/includes/multi-currency/MultiCurrency.php @@ -527,6 +527,8 @@ public function update_single_currency_settings( string $currency_code, string $ throw new InvalidCurrencyException( $message, 'wcpay_multi_currency_invalid_currency', 500 ); } + $currency_code = strtolower( $currency_code ); + if ( 'manual' === $exchange_rate_type && ! is_null( $manual_rate ) ) { if ( ! is_numeric( $manual_rate ) || 0 >= $manual_rate ) { $message = 'Invalid manual currency rate passed to update_single_currency_settings: ' . $manual_rate; @@ -536,7 +538,6 @@ public function update_single_currency_settings( string $currency_code, string $ update_option( 'wcpay_multi_currency_manual_rate_' . $currency_code, $manual_rate ); } - $currency_code = strtolower( $currency_code ); update_option( 'wcpay_multi_currency_price_rounding_' . $currency_code, $price_rounding ); update_option( 'wcpay_multi_currency_price_charm_' . $currency_code, $price_charm ); if ( in_array( $exchange_rate_type, [ 'automatic', 'manual' ], true ) ) { From 0c2de49f49a02784b17a638e73c2b4b60f32adca Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Thu, 14 Sep 2023 09:06:10 -0500 Subject: [PATCH 62/84] Disable tracking on Stripe disconnected accounts (#7160) --- .../fix-enable-tracking-on-connected-accounts | 4 ++ includes/class-woopay-tracker.php | 9 ++++- tests/unit/test-class-woopay-tracker.php | 39 ++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-enable-tracking-on-connected-accounts diff --git a/changelog/fix-enable-tracking-on-connected-accounts b/changelog/fix-enable-tracking-on-connected-accounts new file mode 100644 index 00000000000..99395934864 --- /dev/null +++ b/changelog/fix-enable-tracking-on-connected-accounts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix Tracks conditions diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index 027761233ea..555143461b4 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -161,7 +161,7 @@ public function maybe_record_admin_event( $event, $data = [] ) { } /** - * Override parent method to omit the jetpack TOS check. + * Override parent method to omit the jetpack TOS check and include custom tracking conditions. * * @param bool $is_admin_event Indicate whether the event is emitted from admin area. * @param bool $track_on_all_stores Indicate whether the event is tracked on all WCPay stores. @@ -169,6 +169,13 @@ public function maybe_record_admin_event( $event, $data = [] ) { * @return bool */ public function should_enable_tracking( $is_admin_event = false, $track_on_all_stores = false ) { + + // Don't track if the account is not connected. + $account = WC_Payments::get_account_service(); + if ( is_null( $account ) || ! $account->is_stripe_connected() ) { + return false; + } + // Always respect the user specific opt-out cookie. if ( ! empty( $_COOKIE['tk_opt-out'] ) ) { return false; diff --git a/tests/unit/test-class-woopay-tracker.php b/tests/unit/test-class-woopay-tracker.php index 9a8a6733b53..408d9cb56f5 100644 --- a/tests/unit/test-class-woopay-tracker.php +++ b/tests/unit/test-class-woopay-tracker.php @@ -5,6 +5,7 @@ * @package WooCommerce\Payments\Tests */ +use PHPUnit\Framework\MockObject\MockObject; use WCPay\WooPay_Tracker; /** @@ -24,6 +25,11 @@ class WooPay_Tracker_Test extends WCPAY_UnitTestCase { */ private $http_client_stub; + /** + * @var WC_Payments_Account|MockObject + */ + private $mock_account; + /** * Pre-test setup */ @@ -37,6 +43,10 @@ public function set_up() { $this->_cache = WC_Payments::get_database_cache(); $this->mock_cache = $this->createMock( WCPay\Database_Cache::class ); WC_Payments::set_database_cache( $this->mock_cache ); + + $this->mock_account = $this->getMockBuilder( WC_Payments_Account::class ) + ->disableOriginalConstructor() + ->getMock(); } public function tear_down() { @@ -47,6 +57,8 @@ public function tear_down() { } public function test_tracks_obeys_woopay_flag() { + $this->set_account_connected( true ); + WC_Payments::set_account_service( $this->mock_account ); $this->set_is_woopay_eligible( false ); $this->assertFalse( $this->tracker->should_enable_tracking( null, null ) ); } @@ -54,6 +66,8 @@ public function test_tracks_obeys_woopay_flag() { public function test_does_not_track_admin_pages() { wp_set_current_user( 1 ); $this->set_is_woopay_eligible( true ); + $this->set_account_connected( true ); + WC_Payments::set_account_service( $this->mock_account ); $this->set_is_admin( true ); $this->assertFalse( $this->tracker->should_enable_tracking( null, null ) ); } @@ -61,7 +75,9 @@ public function test_does_not_track_admin_pages() { public function test_does_track_non_admins() { global $wp_roles; $this->set_is_woopay_eligible( true ); + $this->set_account_connected( true ); WC_Payments::get_gateway()->update_option( 'platform_checkout', 'yes' ); + WC_Payments::set_account_service( $this->mock_account ); wp_set_current_user( 1 ); $this->set_is_admin( false ); @@ -74,6 +90,16 @@ public function test_does_track_non_admins() { } } + public function test_does_not_track_when_account_not_connected() { + wp_set_current_user( 1 ); + $this->set_is_woopay_eligible( true ); + $this->set_account_connected( false ); + WC_Payments::set_account_service( $this->mock_account ); + $is_admin_event = false; + $track_on_all_stores = true; + $this->assertFalse( $this->tracker->should_enable_tracking( $is_admin_event, $track_on_all_stores ) ); + } + /** * @param bool $is_admin */ @@ -96,9 +122,20 @@ private function set_is_admin( bool $is_admin ) { /** * Cache account details. * - * @param $account + * @param $is_woopay_eligible */ private function set_is_woopay_eligible( $is_woopay_eligible ) { $this->mock_cache->method( 'get' )->willReturn( [ 'platform_checkout_eligible' => $is_woopay_eligible ] ); } + + /** + * Set Stripe Account connections status. + * + * @param $is_stripe_connected + */ + private function set_account_connected( $is_stripe_connected ) { + $this->mock_account + ->method( 'is_stripe_connected' ) + ->willReturn( $is_stripe_connected ); + } } From 502d50c0790f0b9e3049a70053d24cc7bfb4c8ac Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 14 Sep 2023 17:10:58 +0200 Subject: [PATCH 63/84] fix: express checkouts links consistency (#7198) --- .../fix-express-checkouts-links-consistency | 4 +++ .../apple-google-pay-item.tsx | 7 ++-- .../settings/express-checkout/link-item.tsx | 34 +++++++------------ client/settings/express-checkout/style.scss | 10 ------ .../settings/express-checkout/woopay-item.tsx | 7 ++-- 5 files changed, 25 insertions(+), 37 deletions(-) create mode 100644 changelog/fix-express-checkouts-links-consistency diff --git a/changelog/fix-express-checkouts-links-consistency b/changelog/fix-express-checkouts-links-consistency new file mode 100644 index 00000000000..bf3381062c2 --- /dev/null +++ b/changelog/fix-express-checkouts-links-consistency @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +express checkout links UI consistency & area increase diff --git a/client/settings/express-checkout/apple-google-pay-item.tsx b/client/settings/express-checkout/apple-google-pay-item.tsx index 871bbf6ee22..dd9f8f0c078 100644 --- a/client/settings/express-checkout/apple-google-pay-item.tsx +++ b/client/settings/express-checkout/apple-google-pay-item.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { CheckboxControl } from '@wordpress/components'; +import { Button, CheckboxControl } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; import React from 'react'; @@ -150,13 +150,14 @@ const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => {

diff --git a/client/settings/express-checkout/link-item.tsx b/client/settings/express-checkout/link-item.tsx index fcfdb41a554..f0efbf901ee 100644 --- a/client/settings/express-checkout/link-item.tsx +++ b/client/settings/express-checkout/link-item.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import { __ } from '@wordpress/i18n'; -import { CheckboxControl, VisuallyHidden } from '@wordpress/components'; +import { Button, CheckboxControl, VisuallyHidden } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; /** @@ -154,26 +154,18 @@ const LinkExpressCheckoutItem = (): React.ReactElement => {
diff --git a/client/settings/express-checkout/style.scss b/client/settings/express-checkout/style.scss index 8116bc0be7e..3854f058277 100644 --- a/client/settings/express-checkout/style.scss +++ b/client/settings/express-checkout/style.scss @@ -135,18 +135,8 @@ } &__link { - padding: 12px; - border: 1px solid #007cba; - border-radius: 2px; - font-size: 12px; - height: 18px; align-self: center; - a { - text-decoration: none; - white-space: nowrap; - } - @include breakpoint( '<660px' ) { align-self: flex-start; margin-top: 20px; diff --git a/client/settings/express-checkout/woopay-item.tsx b/client/settings/express-checkout/woopay-item.tsx index d0fbc404101..26f19ecebf2 100644 --- a/client/settings/express-checkout/woopay-item.tsx +++ b/client/settings/express-checkout/woopay-item.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { __ } from '@wordpress/i18n'; -import { CheckboxControl, VisuallyHidden } from '@wordpress/components'; +import { Button, CheckboxControl, VisuallyHidden } from '@wordpress/components'; import WooIcon from 'assets/images/payment-methods/woo.svg?asset'; import interpolateComponents from '@automattic/interpolate-components'; import { getPaymentMethodSettingsUrl } from '../../utils'; @@ -162,16 +162,17 @@ const WooPayExpressCheckoutItem = (): React.ReactElement => {
From d5ec63cfe97776d18f10a09b28cd998d59545b84 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Thu, 14 Sep 2023 20:05:19 +0100 Subject: [PATCH 64/84] Add Reference to V3 Experiment (#7168) --- changelog/fix-experiment-v3 | 4 ++++ includes/class-wc-payments-utils.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-experiment-v3 diff --git a/changelog/fix-experiment-v3 b/changelog/fix-experiment-v3 new file mode 100644 index 00000000000..f6ffe9482b6 --- /dev/null +++ b/changelog/fix-experiment-v3 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update the name of the A/B experiment on new onboarding. diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 7eaa22889bd..171f11b4c55 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -727,7 +727,7 @@ public static function is_in_progressive_onboarding_treatment_mode(): bool { 'yes' === get_option( 'woocommerce_allow_tracking' ) ); - return 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v2' ); + return 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v3' ); } /** From 4986288e3abe745cd2eb94fd7bfb41ca7ce6d1ac Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 14 Sep 2023 21:10:38 +0200 Subject: [PATCH 65/84] chore: add checkout appearance documentation link (#7185) --- ...eat-add-checkout-appearance-learn-more-link | 4 ++++ .../woopay-settings.js | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 changelog/feat-add-checkout-appearance-learn-more-link diff --git a/changelog/feat-add-checkout-appearance-learn-more-link b/changelog/feat-add-checkout-appearance-learn-more-link new file mode 100644 index 00000000000..737041997a4 --- /dev/null +++ b/changelog/feat-add-checkout-appearance-learn-more-link @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +add WooPay checkout appearance documentation link diff --git a/client/settings/express-checkout-settings/woopay-settings.js b/client/settings/express-checkout-settings/woopay-settings.js index 5e54975dd63..e725270822c 100644 --- a/client/settings/express-checkout-settings/woopay-settings.js +++ b/client/settings/express-checkout-settings/woopay-settings.js @@ -4,7 +4,12 @@ */ import React from 'react'; import { __ } from '@wordpress/i18n'; -import { Card, CheckboxControl, TextareaControl } from '@wordpress/components'; +import { + Card, + CheckboxControl, + TextareaControl, + ExternalLink, +} from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; import { Link } from '@woocommerce/components'; @@ -213,19 +218,24 @@ const WooPaySettings = ( { section } ) => { help={ interpolateComponents( { mixedString: __( 'Override the default {{privacyLink}}privacy policy{{/privacyLink}}' + - ' and {{termsLink}}terms of service{{/termsLink}}, or add custom text to WooPay checkout.', + ' and {{termsLink}}terms of service{{/termsLink}},' + + ' or add custom text to WooPay checkout. {{learnMoreLink}}Learn more{{/learnMoreLink}}.', 'woocommerce-payments' ), // prettier-ignore components: { /* eslint-disable prettier/prettier */ privacyLink: window.wcSettings?.storePages?.privacy?.permalink ? - : + : , termsLink: window.wcSettings?.storePages?.terms?.permalink ? - : + : , /* eslint-enable prettier/prettier */ + learnMoreLink: ( + // eslint-disable-next-line max-len + + ), } } ) } value={ woopayCustomMessage } From 6b65491ac6498d7cbe5472cc131caaa9be9ddd94 Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 15 Sep 2023 09:03:29 +1000 Subject: [PATCH 66/84] Fix changing Stripe Billing payment method which wasn't cancelling at Stripe (#7195) --- ...tripe-billing-payment-method-doesnt-cancel | 4 +++ ...class-wc-payments-subscription-service.php | 35 ++++++++++++++----- ...class-wc-payments-subscription-service.php | 1 + 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel diff --git a/changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel b/changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel new file mode 100644 index 00000000000..dd67308e7cf --- /dev/null +++ b/changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Ensure the Stripe Billing subscription is cancelled when the subscription is changed from WooPayments to another payment method. diff --git a/includes/subscriptions/class-wc-payments-subscription-service.php b/includes/subscriptions/class-wc-payments-subscription-service.php index c4b91bac644..10f1074c425 100644 --- a/includes/subscriptions/class-wc-payments-subscription-service.php +++ b/includes/subscriptions/class-wc-payments-subscription-service.php @@ -157,6 +157,8 @@ public function __construct( add_action( 'woocommerce_payments_changed_subscription_payment_method', [ $this, 'maybe_attempt_payment_for_subscription' ], 10, 2 ); add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'show_wcpay_subscription_id' ] ); + + add_action( 'woocommerce_subscription_payment_method_updated_from_' . WC_Payment_Gateway_WCPay::GATEWAY_ID, [ $this, 'maybe_cancel_subscription' ], 10, 2 ); } /** @@ -557,16 +559,14 @@ public function set_pending_cancel_for_subscription( WC_Subscription $subscripti * * If the WCPay subscription's payment method was updated while there's a failed invoice, trigger a retry. * - * @param int $post_id Post ID (WC subscription ID) that had its payment method updated. - * @param int $token_id Payment Token post ID stored in DB. - * @param WC_Payment_Token $token Payment Token object. - * - * @return void + * @param int $subscription_id Post ID (WC subscription ID) that had its payment method updated. + * @param int $token_id Payment Token post ID stored in DB. + * @param WC_Payment_Token $token Payment Token object. */ - public function update_wcpay_subscription_payment_method( int $post_id, int $token_id, WC_Payment_Token $token ) { - $subscription = wcs_get_subscription( $post_id ); + public function update_wcpay_subscription_payment_method( int $subscription_id, int $token_id, WC_Payment_Token $token ) { + $subscription = wcs_get_subscription( $subscription_id ); - if ( $subscription ) { + if ( $subscription && self::is_wcpay_subscription( $subscription ) ) { $wcpay_subscription_id = $this->get_wcpay_subscription_id( $subscription ); $wcpay_payment_method_id = $token->get_token(); @@ -826,6 +826,25 @@ public function get_recurring_item_data_for_subscription( WC_Subscription $subsc return $data; } + /** + * Cancels a WCPay subscription when a customer changes their payment method + * + * @param WC_Subscription $subscription The subscription that was updated. + * @param string $new_payment_method The subscription's new payment method ID. + */ + public function maybe_cancel_subscription( $subscription, $new_payment_method ) { + $wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription ); + + if ( (bool) $wcpay_subscription_id && WC_Payment_Gateway_WCPay::GATEWAY_ID !== $new_payment_method ) { + $this->cancel_subscription( $subscription ); + + // Delete the WCPay Subscription meta but keep a record of it. + $subscription->update_meta_data( '_cancelled' . self::SUBSCRIPTION_ID_META_KEY, $wcpay_subscription_id ); + $subscription->delete_meta_data( self::SUBSCRIPTION_ID_META_KEY ); + $subscription->save(); + } + } + /** * Gets one time item data from a subscription needed to create a WCPay subscription. * diff --git a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php index 550ce8da03d..ce399c7227d 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php +++ b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php @@ -400,6 +400,7 @@ public function test_update_wcpay_subscription_payment_method() { $token = WC_Helper_Token::create_token( $mock_wcpay_token_id, 1 ); $subscription->set_parent( $mock_order ); + $subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $subscription->update_meta_data( self::SUBSCRIPTION_ID_META_KEY, $mock_wcpay_subscription_id ); WC_Subscriptions::set_wcs_get_subscription( From c05aaf3ee0deb87080a8fc1e5a06e1ea9cdc14bd Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 15 Sep 2023 09:05:38 +1000 Subject: [PATCH 67/84] Adds environment metadata to Stripe Billing subscription and invoice payment intents (#7190) --- changelog/fix-6529-add-stripe-billing-context | 4 ++++ changelog/fix-6529-add-stripe-billing-context-2 | 4 ++++ ...yment-intent-meta-for-recurring-transactions | 4 ++++ includes/class-wc-payment-gateway-wcpay.php | 4 ++-- .../class-wc-payments-invoice-service.php | 14 ++++++++++++++ .../class-wc-payments-subscription-service.php | 2 ++ ...-wc-payments-subscriptions-event-handler.php | 6 ++++++ .../class-wc-payments-api-client.php | 17 +++++++++++++++++ ...t-class-wc-payments-subscription-service.php | 3 +++ 9 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-6529-add-stripe-billing-context create mode 100644 changelog/fix-6529-add-stripe-billing-context-2 create mode 100644 changelog/fix-payment-intent-meta-for-recurring-transactions diff --git a/changelog/fix-6529-add-stripe-billing-context b/changelog/fix-6529-add-stripe-billing-context new file mode 100644 index 00000000000..010248eefe7 --- /dev/null +++ b/changelog/fix-6529-add-stripe-billing-context @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Record the subscriptions environment context in transaction meta when Stripe Billing payments are handled. diff --git a/changelog/fix-6529-add-stripe-billing-context-2 b/changelog/fix-6529-add-stripe-billing-context-2 new file mode 100644 index 00000000000..fe9672a589a --- /dev/null +++ b/changelog/fix-6529-add-stripe-billing-context-2 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Record the source (Woo Subscriptions or WCPay Subscriptions) when a Stripe Billing subscription is created. diff --git a/changelog/fix-payment-intent-meta-for-recurring-transactions b/changelog/fix-payment-intent-meta-for-recurring-transactions new file mode 100644 index 00000000000..b5e9efeeab0 --- /dev/null +++ b/changelog/fix-payment-intent-meta-for-recurring-transactions @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Fix payment context and subscription payment metadata stored on subscription recurring transactions. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 98f8a7f7817..a03819bbf98 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -1615,9 +1615,9 @@ protected function get_metadata_from_order( $order, $payment_type ) { 'subscription_payment' => 'no', ]; - if ( 'recurring' === (string) $payment_type && function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order ) ) { + if ( 'recurring' === (string) $payment_type && function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order, 'any' ) ) { $metadata['subscription_payment'] = wcs_order_contains_renewal( $order ) ? 'renewal' : 'initial'; - $metadata['payment_context'] = $this->is_subscriptions_plugin_active() ? 'regular_subscription' : 'wcpay_subscription'; + $metadata['payment_context'] = WC_Payments_Features::should_use_stripe_billing() ? 'wcpay_subscription' : 'regular_subscription'; } return apply_filters( 'wcpay_metadata_from_order', $metadata, $order, $payment_type ); } diff --git a/includes/subscriptions/class-wc-payments-invoice-service.php b/includes/subscriptions/class-wc-payments-invoice-service.php index 5e86e1c9e45..66fd2dcc4f4 100644 --- a/includes/subscriptions/class-wc-payments-invoice-service.php +++ b/includes/subscriptions/class-wc-payments-invoice-service.php @@ -309,6 +309,20 @@ public function get_and_attach_intent_info_to_order( $order, $intent_id ) { ); } + /** + * Sends a request to server to record the store's context for an invoice payment. + * + * @param string $invoice_id The subscription invoice ID. + */ + public function record_subscription_payment_context( string $invoice_id ) { + $this->payments_api_client->update_invoice( + $invoice_id, + [ + 'subscription_context' => class_exists( 'WC_Subscriptions' ) && WC_Payments_Features::is_stripe_billing_enabled() ? 'stripe_billing' : 'legacy_wcpay_subscription', + ] + ); + } + /** * Sets the subscription last invoice ID meta for WC subscription. * diff --git a/includes/subscriptions/class-wc-payments-subscription-service.php b/includes/subscriptions/class-wc-payments-subscription-service.php index 10f1074c425..0559111be65 100644 --- a/includes/subscriptions/class-wc-payments-subscription-service.php +++ b/includes/subscriptions/class-wc-payments-subscription-service.php @@ -390,6 +390,8 @@ public function create_subscription( WC_Subscription $subscription ) { $subscription_data = $this->prepare_wcpay_subscription_data( $wcpay_customer_id, $subscription ); $this->validate_subscription_data( $subscription_data ); + $subscription_data['metadata']['subscription_source'] = $this->is_subscriptions_plugin_active() ? 'woo_subscriptions' : 'wcpay_subscriptions'; + $response = $this->payments_api_client->create_subscription( $subscription_data ); $this->set_wcpay_subscription_id( $subscription, $response['id'] ); diff --git a/includes/subscriptions/class-wc-payments-subscriptions-event-handler.php b/includes/subscriptions/class-wc-payments-subscriptions-event-handler.php index a728aa63f02..d91fa30405e 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-event-handler.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-event-handler.php @@ -188,6 +188,9 @@ public function handle_invoice_paid( array $body ) { // Remove pending invoice ID in case one was recorded for previous failed renewal attempts. $this->invoice_service->mark_pending_invoice_paid_for_subscription( $subscription ); + + // Record the store's Stripe Billing environment context on the payment intent. + $this->invoice_service->record_subscription_payment_context( $wcpay_invoice_id ); } /** @@ -248,6 +251,9 @@ public function handle_invoice_payment_failed( array $body ) { // Record invoice ID so we can trigger repayment on payment method update. $this->invoice_service->mark_pending_invoice_for_subscription( $subscription, $wcpay_invoice_id ); + + // Record the store's Stripe Billing environment context on the payment intent. + $this->invoice_service->record_subscription_payment_context( $wcpay_invoice_id ); } /** diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 5442700f1b4..bdb7e245106 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -1122,6 +1122,23 @@ public function charge_invoice( string $invoice_id, array $data = [] ) { ); } + /** + * Updates an invoice. + * + * @param string $invoice_id ID of the invoice to update. + * @param array $data Parameters to send to the invoice endpoint. Optional. Default is an empty array. + * @return array + * + * @throws API_Exception Error updating the invoice. + */ + public function update_invoice( string $invoice_id, array $data = [] ) { + return $this->request( + $data, + self::INVOICES_API . '/' . $invoice_id, + self::POST + ); + } + /** * Fetch a WCPay subscription. * diff --git a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php index ce399c7227d..490d1c74298 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php +++ b/tests/unit/subscriptions/test-class-wc-payments-subscription-service.php @@ -171,6 +171,9 @@ public function test_create_subscription() { ], ], ], + 'metadata' => [ + 'subscription_source' => 'woo_subscriptions', + ], ]; $this->assertNotEquals( $mock_subscription->get_meta( self::SUBSCRIPTION_ID_META_KEY ), $mock_wcpay_subscription_id ); From cda8ef404e296ed4671ebf70c86e7467ee9d5515 Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 15 Sep 2023 09:06:49 +1000 Subject: [PATCH 68/84] Use fallback methods for updating the next payment date after migrating a stripe billing subscription (#7176) Co-authored-by: mattallan --- ...pdate-subscription-next-payment-on-migrate | 5 ++ ...ass-wc-payments-subscriptions-migrator.php | 84 +++++++++++++++---- 2 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 changelog/update-subscription-next-payment-on-migrate diff --git a/changelog/update-subscription-next-payment-on-migrate b/changelog/update-subscription-next-payment-on-migrate new file mode 100644 index 00000000000..54e50bddf0a --- /dev/null +++ b/changelog/update-subscription-next-payment-on-migrate @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: The migrator is unreleased code and improvements to it pre-release don't need a changelog entry. + + diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index 745d508a10a..19353d3190f 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -125,18 +125,8 @@ public function migrate_wcpay_subscription( $subscription_id, $attempt = 0 ) { $this->maybe_cancel_wcpay_subscription( $wcpay_subscription ); - /** - * There's a scenario where a WCPay subscription is active but has no pending renewal scheduled action. - * Once migrated, this results in an active subscription that will remain active forever, without processing a renewal order. - * - * To ensure that all migrated subscriptions have a pending scheduled action, we need to reschedule the next payment date by - * updating the date on the subscription. - */ - if ( $subscription->has_status( 'active' ) && $subscription->get_time( 'next_payment' ) > time() ) { - $new_next_payment = gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) + 1 ); - $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); - - $this->logger->log( sprintf( '---- Next payment date updated to %1$s to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription_id ) ); + if ( $subscription->has_status( 'active' ) ) { + $this->update_next_payment_date( $subscription, $wcpay_subscription ); } // If the subscription is active or on-hold, verify the payment method is valid and set correctly that it continues to renew. @@ -146,9 +136,11 @@ public function migrate_wcpay_subscription( $subscription_id, $attempt = 0 ) { $this->update_wcpay_subscription_meta( $subscription ); - $subscription->add_order_note( __( 'This subscription has been successfully migrated to a WooPayments tokenized subscription.', 'woocommerce-payments' ) ); + if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() ) { + $subscription->add_order_note( __( 'This subscription has been successfully migrated to a WooPayments tokenized subscription.', 'woocommerce-payments' ) ); + } - $this->logger->log( sprintf( '---- SUCCESS: Subscription #%d migrated.', $subscription_id ) ); + $this->logger->log( sprintf( '---- Subscription #%d migration complete.', $subscription_id ) ); } catch ( \Exception $e ) { $this->logger->log( $e->getMessage() ); @@ -295,6 +287,70 @@ private function update_wcpay_subscription_meta( $subscription ) { } } + /** + * Updates the subscription's next payment date in WooCommerce to ensure a smooth transition to on-site billing. + * + * There's a scenario where a WCPay subscription is active but has no pending renewal scheduled action. + * Once migrated, this results in an active subscription that will remain active forever, without processing a renewal order. + * + * To ensure that all migrated subscriptions have a pending scheduled action, we need to reschedule the next payment date by + * updating the date on the subscription. + * + * In priority order the new next payment date will be: + * - The existing WooCommerce next payment date if it's in the future. + * - The Stripe subscription's current_period_end if it's in the future. + * - A newly calculated next payment date using the WC_Subscription::calculate_date() method. + * + * @param WC_Subscription $subscription The WC Subscription being migrated. + * @param array $wcpay_subscription The subscription data from Stripe. + */ + private function update_next_payment_date( $subscription, $wcpay_subscription ) { + try { + // Just update the existing WC Subscription's next payment date if it's in the future. + if ( $subscription->get_time( 'next_payment' ) > time() ) { + $new_next_payment = gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) + 1 ); + + $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); + $this->logger->log( sprintf( '---- Next payment date updated to %1$s to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription->get_id() ) ); + + return; + } + + // If the subscription was still using WooPayments, use the Stripe subscription's next payment time (current_period_end) if it's in the future. + if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() && isset( $wcpay_subscription['current_period_end'] ) && absint( $wcpay_subscription['current_period_end'] ) > time() ) { + $new_next_payment = gmdate( 'Y-m-d H:i:s', absint( $wcpay_subscription['current_period_end'] ) ); + + $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); + $this->logger->log( sprintf( '---- Next payment date updated to %1$s to match Stripe subscription record and to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription->get_id() ) ); + + return; + } + + // Lastly calculate the next payment date. + $new_next_payment = $subscription->calculate_date( 'next_payment' ); + + if ( wcs_date_to_time( $new_next_payment ) > time() ) { + $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); + $this->logger->log( sprintf( '---- Calculated a new next payment date (%1$s) to ensure subscription #%2$d has a pending scheduled payment in the future.', $new_next_payment, $subscription->get_id() ) ); + + return; + } + + // If we got here the next payment date is in the past, the Stripe subscription is missing a "current_period_end" or it's in the past, and calculating a new date also failed. Log an error. + $this->logger->log( + sprintf( + '---- ERROR: Failed to update subscription #%1$d next payment date. Current next payment date (%2$s) is in the past, Stripe "current_period_end" data is invalid (%3$s) and an attempt to calculate a new date also failed (%4$s).', + $subscription->get_id(), + gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) ), + isset( $wcpay_subscription['current_period_end'] ) ? gmdate( 'Y-m-d H:i:s', absint( $wcpay_subscription['current_period_end'] ) ) : 'no data', + $new_next_payment + ) + ); + } catch ( \Exception $e ) { + $this->logger->log( sprintf( '---- ERROR: Failed to update subscription #%1$d next payment date. %2$s', $subscription->get_id(), $e->getMessage() ) ); + } + } + /** * Returns the subscription status from the WCPay subscription data for logging purposes. * From 7ce30b4589f55e54dcff2839b7d4f885fbc99f15 Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Fri, 15 Sep 2023 16:50:15 +1200 Subject: [PATCH 69/84] Allow merchants to see dispute evidence (#7192) Co-authored-by: Rua Haszard Co-authored-by: Rua Haszard --- changelog/add-7173-dispute-evidence | 4 + client/data/files/action-types.ts | 8 + client/data/files/actions.ts | 31 ++++ client/data/files/hooks.ts | 37 +++++ client/data/files/index.ts | 12 ++ client/data/files/reducer.ts | 44 ++++++ client/data/files/resolvers.ts | 37 +++++ client/data/files/selectors.ts | 20 +++ client/data/files/types.d.ts | 73 +++++++++ client/data/index.ts | 1 + client/data/store.js | 5 + client/data/types.d.ts | 2 + .../dispute-details/evidence-list.tsx | 144 ++++++++++++++++++ .../payment-details/dispute-details/index.tsx | 4 + .../dispute-details/style.scss | 37 +++++ .../dispute-details/test/index.test.tsx | 1 + client/types/disputes.d.ts | 19 +++ ...lass-wc-rest-payments-files-controller.php | 68 +++++++++ 18 files changed, 547 insertions(+) create mode 100644 changelog/add-7173-dispute-evidence create mode 100644 client/data/files/action-types.ts create mode 100644 client/data/files/actions.ts create mode 100644 client/data/files/hooks.ts create mode 100644 client/data/files/index.ts create mode 100644 client/data/files/reducer.ts create mode 100644 client/data/files/resolvers.ts create mode 100644 client/data/files/selectors.ts create mode 100644 client/data/files/types.d.ts create mode 100644 client/payment-details/dispute-details/evidence-list.tsx diff --git a/changelog/add-7173-dispute-evidence b/changelog/add-7173-dispute-evidence new file mode 100644 index 00000000000..0e1e7ca1d92 --- /dev/null +++ b/changelog/add-7173-dispute-evidence @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Adding issuer evidence to dispute details. Hidden behind a feature flag diff --git a/client/data/files/action-types.ts b/client/data/files/action-types.ts new file mode 100644 index 00000000000..778562420ed --- /dev/null +++ b/client/data/files/action-types.ts @@ -0,0 +1,8 @@ +/** @format */ + +enum TYPES { + SET_FILE = 'SET_FILE', + SET_ERROR_FOR_FILES = 'SET_ERROR_FOR_FILES', +} + +export default TYPES; diff --git a/client/data/files/actions.ts b/client/data/files/actions.ts new file mode 100644 index 00000000000..25524e5a6eb --- /dev/null +++ b/client/data/files/actions.ts @@ -0,0 +1,31 @@ +/** @format */ + +/** + * Internal dependencies + */ +import ACTION_TYPES from './action-types'; +import type { + File, + UpdateFilesAction, + UpdateErrorForFilesAction, +} from './types'; +import { ApiError } from 'wcpay/types/errors'; + +export function updateFiles( id: string, data: File ): UpdateFilesAction { + return { + type: ACTION_TYPES.SET_FILE, + id, + data, + }; +} + +export function updateErrorForFiles( + id: string, + error: ApiError +): UpdateErrorForFilesAction { + return { + type: ACTION_TYPES.SET_ERROR_FOR_FILES, + id, + error, + }; +} diff --git a/client/data/files/hooks.ts b/client/data/files/hooks.ts new file mode 100644 index 00000000000..c91065146a8 --- /dev/null +++ b/client/data/files/hooks.ts @@ -0,0 +1,37 @@ +/** @format */ + +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import type { File, FileResponse } from './types'; +import { STORE_NAME } from '../constants'; + +export const useFiles = ( id: string ): FileResponse => + useSelect( + ( select ) => { + const selectors = select( STORE_NAME ); + + const { + getFile, + getFileError, + isResolving, + hasFinishedResolution, + } = selectors; + + const file: File = getFile( id ); + + return { + file: file || ( {} as File ), + error: getFileError( id ), + isLoading: + isResolving( 'getFile', [ id ] ) || + ! hasFinishedResolution( 'getFile', [ id ] ), + }; + }, + [ id ] + ); diff --git a/client/data/files/index.ts b/client/data/files/index.ts new file mode 100644 index 00000000000..c524cca8b05 --- /dev/null +++ b/client/data/files/index.ts @@ -0,0 +1,12 @@ +/** @format */ + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; + +export { reducer, selectors, actions, resolvers }; +export * from './hooks'; diff --git a/client/data/files/reducer.ts b/client/data/files/reducer.ts new file mode 100644 index 00000000000..e3b1ff4de06 --- /dev/null +++ b/client/data/files/reducer.ts @@ -0,0 +1,44 @@ +/** @format */ + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { + UpdateErrorForFilesAction, + FilesState, + FilesActions, + UpdateFilesAction, +} from './types'; + +const defaultState = {}; + +const receiveFiles = ( + state: FilesState = defaultState, + action: FilesActions +): FilesState => { + const { type, id } = action; + + switch ( type ) { + case TYPES.SET_FILE: + return { + ...state, + [ id ]: { + ...state[ id ], + data: ( action as UpdateFilesAction ).data, + }, + }; + case TYPES.SET_ERROR_FOR_FILES: + return { + ...state, + [ id ]: { + ...state[ id ], + error: ( action as UpdateErrorForFilesAction ).error, + }, + }; + default: + return state; + } +}; + +export default receiveFiles; diff --git a/client/data/files/resolvers.ts b/client/data/files/resolvers.ts new file mode 100644 index 00000000000..a4ade1ccef3 --- /dev/null +++ b/client/data/files/resolvers.ts @@ -0,0 +1,37 @@ +/** @format */ + +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; +import { controls } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { NAMESPACE } from '../constants'; +import { updateFiles, updateErrorForFiles } from './actions'; +import { File } from './types'; +import { ApiError } from 'wcpay/types/errors'; + +/** + * Retrieve a single file from the files API. + * + * @param {string} id Identifier for specified file to retrieve. + */ +export function* getFile( id: string ): Generator< unknown > { + try { + const result = yield apiFetch( { + path: `${ NAMESPACE }/file/${ id }/details`, + } ); + yield updateFiles( id, result as File ); + } catch ( e ) { + yield controls.dispatch( + 'core/notices', + 'createErrorNotice', + __( 'Error retrieving file.', 'woocommerce-payments' ) + ); + yield updateErrorForFiles( id, e as ApiError ); + } +} diff --git a/client/data/files/selectors.ts b/client/data/files/selectors.ts new file mode 100644 index 00000000000..47cb27c1bb3 --- /dev/null +++ b/client/data/files/selectors.ts @@ -0,0 +1,20 @@ +/** @format */ + +/** + * Internal dependencies + */ +import { State } from 'wcpay/data/types'; +import { File } from './types'; +import { ApiError } from 'wcpay/types/errors'; + +export const getFile = ( { files }: State, id: string ): File => { + const file = files?.[ id ]; + + return file?.data || ( {} as File ); +}; + +export const getFileError = ( { files }: State, id: string ): ApiError => { + const file = files?.[ id ]; + + return file?.error || ( {} as ApiError ); +}; diff --git a/client/data/files/types.d.ts b/client/data/files/types.d.ts new file mode 100644 index 00000000000..2c011de77c8 --- /dev/null +++ b/client/data/files/types.d.ts @@ -0,0 +1,73 @@ +/** @format */ + +/** + * Internal dependencies + */ +import { ApiError } from 'wcpay/types/errors'; +import ACTION_TYPES from './action-types'; + +export interface File { + /** + * Unique identifier for the file, expected to be prefixed by file_ + */ + id: string; + /** + * The purpose for file. eg 'dispute_evidence'. + */ + purpose: string; + /** + * The filetype 'pdf' 'csv' 'jpg' etc. + */ + type: string; + /** + * A filename for the file, suitable for saving to a filesystem. + */ + filename: string; + /** + * The size in bytes. + */ + size: number; + /** + * A user friendly title for the file. + */ + title: string | null; +} + +export interface FileDownload { + /** + * The file mime-type. + */ + content_type: string; + /** + * The file content, base64 encoded. + */ + file_content: string; +} + +export interface FileResponse { + isLoading: boolean; + file?: File; + fileError?: ApiError; +} + +export interface UpdateFilesAction { + type: ACTION_TYPES.SET_FILE; + id: string; + data: File; +} + +export interface UpdateErrorForFilesAction { + type: ACTION_TYPES.SET_ERROR_FOR_FILES; + id: string; + error: ApiError; +} + +export interface FilesState { + [ key: string ]: { + id: string; + data?: File; + error?: ApiError; + }; +} + +export type FilesActions = UpdateFilesAction | UpdateErrorForFilesAction; diff --git a/client/data/index.ts b/client/data/index.ts index f2f4d081d78..0d42f41ce93 100644 --- a/client/data/index.ts +++ b/client/data/index.ts @@ -24,3 +24,4 @@ export * from './capital/hooks'; export * from './documents/hooks'; export * from './payment-intents/hooks'; export * from './authorizations/hooks'; +export * from './files/hooks'; diff --git a/client/data/store.js b/client/data/store.js index 8af70e68c4b..13e4c90733b 100644 --- a/client/data/store.js +++ b/client/data/store.js @@ -20,6 +20,7 @@ import * as capital from './capital'; import * as documents from './documents'; import * as paymentIntents from './payment-intents'; import * as authorizations from './authorizations'; +import * as files from './files'; // Extracted into wrapper function to facilitate testing. export const initStore = () => @@ -37,6 +38,7 @@ export const initStore = () => documents: documents.reducer, paymentIntents: paymentIntents.reducer, authorizations: authorizations.reducer, + files: files.reducer, } ), actions: { ...deposits.actions, @@ -51,6 +53,7 @@ export const initStore = () => ...documents.actions, ...paymentIntents.actions, ...authorizations.actions, + ...files.actions, }, controls, selectors: { @@ -66,6 +69,7 @@ export const initStore = () => ...documents.selectors, ...paymentIntents.selectors, ...authorizations.selectors, + ...files.selectors, }, resolvers: { ...deposits.resolvers, @@ -80,5 +84,6 @@ export const initStore = () => ...documents.resolvers, ...paymentIntents.resolvers, ...authorizations.resolvers, + ...files.resolvers, }, } ); diff --git a/client/data/types.d.ts b/client/data/types.d.ts index 4fe91140b8f..ae79164fb81 100644 --- a/client/data/types.d.ts +++ b/client/data/types.d.ts @@ -5,8 +5,10 @@ */ import { CapitalState } from './capital/types'; import { PaymentIntentsState } from './payment-intents/types'; +import { FilesState } from './files/types'; export interface State { capital?: CapitalState; paymentIntents?: PaymentIntentsState; + files?: FilesState; } diff --git a/client/payment-details/dispute-details/evidence-list.tsx b/client/payment-details/dispute-details/evidence-list.tsx new file mode 100644 index 00000000000..e6610b74206 --- /dev/null +++ b/client/payment-details/dispute-details/evidence-list.tsx @@ -0,0 +1,144 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import { useDispatch } from '@wordpress/data'; +import { Button, PanelBody } from '@wordpress/components'; +import { Icon, page } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import type { IssuerEvidence } from 'wcpay/types/disputes'; +import { useFiles } from 'wcpay/data'; +import Loadable from 'wcpay/components/loadable'; +import { NAMESPACE } from 'wcpay/data/constants'; +import { FileDownload } from 'wcpay/data/files/types'; + +const TextEvidence: React.FC< { + evidence: string; +} > = ( { evidence } ) => { + const onClick = () => { + const link = document.createElement( 'a' ); + link.href = URL.createObjectURL( + new Blob( [ evidence ], { type: 'text/plain' } ) + ); + link.download = 'evidence.txt'; + link.click(); + }; + + return ( + + ); +}; + +const FileEvidence: React.FC< { + fileId: string; +} > = ( { fileId } ) => { + const { file, isLoading } = useFiles( fileId ); + const { createNotice } = useDispatch( 'core/notices' ); + const [ isDownloading, setIsDownloading ] = React.useState( false ); + + const onClick = async () => { + if ( ! file || ! file.id || isDownloading ) { + return; + } + try { + setIsDownloading( true ); + const downloadRequest = await apiFetch< FileDownload >( { + path: `${ NAMESPACE }/file/${ encodeURI( file.id ) }/content`, + method: 'GET', + } ); + + const link = document.createElement( 'a' ); + link.href = + 'data:application/octect-stream;base64,' + + downloadRequest.file_content; + link.download = file.filename; + link.click(); + } catch ( exception ) { + createNotice( + 'error', + __( 'Error downloading file', 'woocommerce-payments' ) + ); + } + setIsDownloading( false ); + }; + + return ( + + { file && file.id ? ( + + ) : ( + <> + ) } + + ); +}; + +interface Props { + issuerEvidence: IssuerEvidence | null; +} + +const IssuerEvidenceList: React.FC< Props > = ( { issuerEvidence } ) => { + if ( + ! issuerEvidence || + ! issuerEvidence.file_evidence.length || + ! issuerEvidence.text_evidence + ) { + return <>; + } + + return ( + +
    + { issuerEvidence.text_evidence && ( +
  • + +
  • + ) } + { issuerEvidence.file_evidence.map( + ( fileId: string, i: any ) => ( +
  • + +
  • + ) + ) } +
+
+ ); +}; + +export default IssuerEvidenceList; diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx index 4896b3e1e63..249032812e7 100644 --- a/client/payment-details/dispute-details/index.tsx +++ b/client/payment-details/dispute-details/index.tsx @@ -15,6 +15,7 @@ import { edit } from '@wordpress/icons'; import type { Dispute } from 'wcpay/types/disputes'; import { isAwaitingResponse } from 'wcpay/disputes/utils'; import DisputeNotice from './dispute-notice'; +import IssuerEvidenceList from './evidence-list'; import DisputeSummaryRow from './dispute-summary-row'; import InlineNotice from 'components/inline-notice'; import './style.scss'; @@ -55,6 +56,9 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { dispute={ dispute } daysRemaining={ countdownDays } /> + ) } diff --git a/client/payment-details/dispute-details/style.scss b/client/payment-details/dispute-details/style.scss index acaeee0fe39..628ab098b39 100644 --- a/client/payment-details/dispute-details/style.scss +++ b/client/payment-details/dispute-details/style.scss @@ -48,3 +48,40 @@ margin-bottom: 8px; } } + +.dispute-evidence { + // Override WordPress core PanelBody boxy styles. Ours is more inline content. + &.components-panel__body { + border: none; + } + // Override WordPress core PanelBody padding so fits snug in our container. + &.components-panel__body.is-opened { + padding-bottom: 0; + } + // Override WordPress core PanelBody title to align with other nearby headings. + .components-panel__body-title button { + // Copy of WooCommerce core list table header style. + text-transform: uppercase; + color: #757575; + font-size: 11px; + font-weight: 600; + } + // Override WordPress core PanelBody button/title – slim padding consistent with surrounding components. + .components-panel__body-toggle.components-button { + padding: 10px; + } + // Override WordPress core PanelBody focus/highlighting. + .components-panel__body-toggle.components-button:focus { + box-shadow: none; + outline: 0; + } + &__list { + list-style: none; + margin: 8px 0 0; + } + &__list-item { + display: inline-block; + margin-right: 12px; + margin-bottom: 0; + } +} diff --git a/client/payment-details/dispute-details/test/index.test.tsx b/client/payment-details/dispute-details/test/index.test.tsx index e2cd36c11f6..6032e497cf9 100644 --- a/client/payment-details/dispute-details/test/index.test.tsx +++ b/client/payment-details/dispute-details/test/index.test.tsx @@ -85,6 +85,7 @@ const getBaseCharge = (): ChargeWithDisputeRequired => customer_name: 'Test customer', shipping_address: '123 test address', }, + issuer_evidence: null, evidence_details: { due_by: 1694303999, has_evidence: false, diff --git a/client/types/disputes.d.ts b/client/types/disputes.d.ts index 43eac68b1d6..cea3ea886c9 100644 --- a/client/types/disputes.d.ts +++ b/client/types/disputes.d.ts @@ -29,6 +29,24 @@ interface EvidenceDetails { submission_count: number; } +/** + * See https://stripe.com/docs/api/disputes/object#dispute_object-issuer_evidence + */ +interface IssuerEvidence { + /** + * Type of issuer evidence supplied. + */ + evidence_type: 'retrieval' | 'chargeback' | 'response'; + /** + * List of up to 5 (ID of a file upload) File-based issuer evidence. + */ + file_evidence: string[]; + /** + * Free-form, text-based issuer evidence. + */ + text_evidence: string | null; +} + export type DisputeReason = | 'bank_cannot_process' | 'check_returned' @@ -62,6 +80,7 @@ export interface Dispute { metadata: Record< string, any >; order: null | OrderDetails; evidence: Evidence; + issuer_evidence: IssuerEvidence | null; fileSize?: Record< string, number >; reason: DisputeReason; charge: Charge | string; diff --git a/includes/admin/class-wc-rest-payments-files-controller.php b/includes/admin/class-wc-rest-payments-files-controller.php index e84ba70c074..db41002e1df 100644 --- a/includes/admin/class-wc-rest-payments-files-controller.php +++ b/includes/admin/class-wc-rest-payments-files-controller.php @@ -33,6 +33,26 @@ public function register_routes() { ] ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P\w+)/details', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_file_detail' ], + 'permission_callback' => [ $this, 'check_permission' ], + ] + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P\w+)/content', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_file_content' ], + 'permission_callback' => [ $this, 'check_permission' ], + ] + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P\w+)', @@ -42,6 +62,7 @@ public function register_routes() { 'permission_callback' => [], ] ); + } /** @@ -116,6 +137,53 @@ function ( bool $served, WP_HTTP_Response $response ) : bool { } + /** + * Retrieve file details via the API. + * + * Example response: + * { + * "id": "file_1Np1S5J5cIRIG92xknlr0iND", + * "object": "file", + * "created": 1694405421, + * "expires_at": 1717733421, + * "filename": "Screenshot 2023-09-04 at 5.08.31\u202fPM.png", + * "purpose": "dispute_evidence", + * "size": 21444, + * "title": null, + * "type": "png", + * } + * + * @param WP_REST_Request $request Full data about the request. + * + * @return mixed|WP_Error + */ + public function get_file_detail( WP_REST_Request $request ) { + $file_id = $request->get_param( 'file_id' ); + $as_account = (bool) $request->get_param( 'as_account' ); + + return $this->forward_request( 'get_file', [ $file_id, $as_account ] ); + } + + /** + * Retrieve file contents via the API as a base64 encoded string. + * + * Example response: + * { + * "content_type": "image\/png", + * "file_content": "iVBORw.......", + * } + * + * @param WP_REST_Request $request Full data about the request. + * + * @return mixed|WP_Error + */ + public function get_file_content( WP_REST_Request $request ) { + $file_id = $request->get_param( 'file_id' ); + $as_account = (bool) $request->get_param( 'as_account' ); + + return $this->forward_request( 'get_file_contents', [ $file_id, $as_account ] ); + } + /** * Convert error response * From f930e7f85ee58bad4369c493e7da3963fb598480 Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 15 Sep 2023 17:42:39 +1000 Subject: [PATCH 70/84] Improve the logging in the Stripe Billing migration of token data (#7218) --- changelog/fix-migrate-subscription-tokens | 5 +++++ .../class-wc-payments-subscriptions-migrator.php | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-migrate-subscription-tokens diff --git a/changelog/fix-migrate-subscription-tokens b/changelog/fix-migrate-subscription-tokens new file mode 100644 index 00000000000..cf7f11b62ce --- /dev/null +++ b/changelog/fix-migrate-subscription-tokens @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This migration code is unreleased and this change isn't noteworthy. + + diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index 19353d3190f..5165653bd39 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -389,12 +389,10 @@ private function get_wcpay_subscription_status( $wcpay_subscription ) { private function verify_subscription_payment_token( $subscription, $wcpay_subscription ) { // If the subscription's payment method isn't set to WooPayments, we skip this token step. if ( $subscription->get_payment_method() !== WC_Payment_Gateway_WCPay::GATEWAY_ID ) { - $this->logger->log( sprintf( '---- Skipped verifying the payment token. Subscription #%1$d is no longer set to "woocommerce_payments".', $subscription->get_id() ) ); + $this->logger->log( sprintf( '---- Skipped verifying the payment token. Subscription #%1$d has "%2$s" as the payment method.', $subscription->get_id(), $subscription->get_payment_method() ) ); return; } - unset( $wcpay_subscription['default_payment_method'] ); - if ( empty( $wcpay_subscription['default_payment_method'] ) ) { $this->logger->log( sprintf( '---- Could not verify the payment method. Stripe Billing subscription (%1$s) does not have a default payment method.', $wcpay_subscription['id'] ?? 'unknown' ) ); return; From e2df30f89429ca8f6d6fb51bfe0a1bea6c8b4f24 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:22:46 +1000 Subject: [PATCH 71/84] =?UTF-8?q?Transaction=20Details=20=E2=86=92=20fix?= =?UTF-8?q?=20typo=20in=20staged=20dispute=20challenge=20notice=20(#7217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog/fix-dispute-staged-evidence-notice-typo | 5 +++++ client/payment-details/dispute-details/index.tsx | 2 +- client/payment-details/dispute-details/test/index.test.tsx | 7 +++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-dispute-staged-evidence-notice-typo diff --git a/changelog/fix-dispute-staged-evidence-notice-typo b/changelog/fix-dispute-staged-evidence-notice-typo new file mode 100644 index 00000000000..f0337ebd600 --- /dev/null +++ b/changelog/fix-dispute-staged-evidence-notice-typo @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Behind feature flag: fix for typo in staged dispute challenge notice. + + diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx index 249032812e7..bb41511d293 100644 --- a/client/payment-details/dispute-details/index.tsx +++ b/client/payment-details/dispute-details/index.tsx @@ -47,7 +47,7 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => { isDismissible={ false } > { __( - `You initiated a dispute a challenge to this dispute. Click 'Continue with challenge' to proceed with your drafted response.`, + `You initiated a challenge to this dispute. Click 'Continue with challenge' to proceed with your draft response.`, 'woocommerce-payments' ) } diff --git a/client/payment-details/dispute-details/test/index.test.tsx b/client/payment-details/dispute-details/test/index.test.tsx index 6032e497cf9..9787972fd17 100644 --- a/client/payment-details/dispute-details/test/index.test.tsx +++ b/client/payment-details/dispute-details/test/index.test.tsx @@ -196,9 +196,8 @@ describe( 'DisputeDetails', () => { ); // Render the staged evidence message - screen.getByText( - /You initiated a dispute a challenge to this dispute/, - { ignore: '.a11y-speak-region' } - ); + screen.getByText( /You initiated a challenge to this dispute/, { + ignore: '.a11y-speak-region', + } ); } ); } ); From c4a74456f5086115f230a7f80fe92efd62dc0a9c Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Fri, 15 Sep 2023 16:22:07 -0300 Subject: [PATCH 72/84] Revert "Bump WC and PHP versions (#7134)" (#7213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com> --- .github/workflows/compatibility.yml | 9 ++++----- .github/workflows/e2e-test.yml | 2 +- .github/workflows/php-lint-test.yml | 6 +++--- changelog/dev-bump-min-wc-8-1-php-7-4 | 4 ---- composer.json | 4 ++-- phpcs-compat.xml.dist | 4 ++-- phpcs.xml.dist | 4 ++-- readme.txt | 12 ++++++------ woocommerce-payments.php | 8 ++++---- 9 files changed, 24 insertions(+), 29 deletions(-) delete mode 100644 changelog/dev-bump-min-wc-8-1-php-7-4 diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 6b9ade34d58..12439ae65b1 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -4,10 +4,9 @@ on: pull_request env: - WC_MIN_SUPPORTED_VERSION: '7.7.0' - WP_MIN_SUPPORTED_VERSION: '6.1' - PHP_MIN_SUPPORTED_VERSION: '7.4' - GUTENBERG_VERSION_FOR_WP_MIN: '15.7.0' + WC_MIN_SUPPORTED_VERSION: '7.6.0' + WP_MIN_SUPPORTED_VERSION: '6.0' + PHP_MIN_SUPPORTED_VERSION: '7.3' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -24,7 +23,7 @@ jobs: id: generate_matrix run: | WC_VERSIONS=$( echo "[\"$WC_MIN_SUPPORTED_VERSION\", \"latest\", \"beta\"]" ) - MATRIX_INCLUDE=$( echo "[{\"woocommerce\":\"$WC_MIN_SUPPORTED_VERSION\",\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"$GUTENBERG_VERSION_FOR_WP_MIN\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) + MATRIX_INCLUDE=$( echo "[{\"woocommerce\":\"$WC_MIN_SUPPORTED_VERSION\",\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"13.6.0\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) echo "matrix={\"woocommerce\":$WC_VERSIONS,\"wordpress\":[\"latest\"],\"gutenberg\":[\"latest\"],\"php\":[\"7.4\"], \"include\":$MATRIX_INCLUDE}" >> $GITHUB_OUTPUT woocommerce-compatibility: diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index ddc50e45526..9c85a291ab1 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -23,7 +23,7 @@ env: E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }} E2E_USE_LOCAL_SERVER: false E2E_RESULT_FILEPATH: 'tests/e2e/results.json' - WC_MIN_SUPPORTED_VERSION: '7.7.0' + WC_MIN_SUPPORTED_VERSION: '7.6.0' NODE_ENV: 'test' FORCE_E2E_DEPS_SETUP: true diff --git a/.github/workflows/php-lint-test.yml b/.github/workflows/php-lint-test.yml index 0a55baaeb8e..4077352f4ce 100644 --- a/.github/workflows/php-lint-test.yml +++ b/.github/workflows/php-lint-test.yml @@ -6,9 +6,9 @@ on: env: WP_VERSION: latest - WC_MIN_SUPPORTED_VERSION: '7.7.0' + WC_MIN_SUPPORTED_VERSION: '7.6.0' GUTENBERG_VERSION: latest - PHP_MIN_SUPPORTED_VERSION: '7.4' + PHP_MIN_SUPPORTED_VERSION: '7.3' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -41,7 +41,7 @@ jobs: - name: "Generate matrix" id: generate_matrix run: | - PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"8.0\", \"8.1\"]" ) + PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"7.3\", \"7.4\"]" ) echo "matrix={\"php\":$PHP_VERSIONS}" >> $GITHUB_OUTPUT test: diff --git a/changelog/dev-bump-min-wc-8-1-php-7-4 b/changelog/dev-bump-min-wc-8-1-php-7-4 deleted file mode 100644 index 989290fa6f3..00000000000 --- a/changelog/dev-bump-min-wc-8-1-php-7-4 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Bump minimum required version of WooCommerce to 7.7 and PHP to 7.4. diff --git a/composer.json b/composer.json index 6b70464800c..668bf0f26a7 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "minimum-stability": "dev", "config": { "platform": { - "php": "7.4" + "php": "7.3" }, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, @@ -20,7 +20,7 @@ } }, "require": { - "php": ">=7.4", + "php": ">=7.2", "ext-json": "*", "automattic/jetpack-connection": "1.51.7", "automattic/jetpack-config": "1.15.2", diff --git a/phpcs-compat.xml.dist b/phpcs-compat.xml.dist index 003c335b266..996f8374292 100644 --- a/phpcs-compat.xml.dist +++ b/phpcs-compat.xml.dist @@ -15,8 +15,8 @@ tests/ - - + + diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 1b3888752d3..9f8efd70ee5 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -16,8 +16,8 @@ ./lib/* - - + + diff --git a/readme.txt b/readme.txt index a3295a441e5..290e56155d9 100644 --- a/readme.txt +++ b/readme.txt @@ -1,9 +1,9 @@ === WooPayments - Fully Integrated Solution Built and Supported by Woo === Contributors: woocommerce, automattic Tags: payment gateway, payment, apple pay, credit card, google pay, woocommerce payments -Requires at least: 6.1 -Tested up to: 6.3 -Requires PHP: 7.4 +Requires at least: 6.0 +Tested up to: 6.2 +Requires PHP: 7.3 Stable tag: 6.4.2 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -38,9 +38,9 @@ Our global support team is available to answer questions you may have about WooP = Requirements = -* WordPress 6.1 or newer. -* WooCommerce 7.7 or newer. -* PHP 7.4 or newer is recommended. +* WordPress 6.0 or newer. +* WooCommerce 7.6 or newer. +* PHP 7.3 or newer is recommended. = Try it now = diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 76926cfe9a1..7893ad0996a 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -8,10 +8,10 @@ * Woo: 5278104:bf3cf30871604e15eec560c962593c1f * Text Domain: woocommerce-payments * Domain Path: /languages - * WC requires at least: 7.7 - * WC tested up to: 7.9.0 - * Requires at least: 6.1 - * Requires PHP: 7.4 + * WC requires at least: 7.6 + * WC tested up to: 7.8.0 + * Requires at least: 6.0 + * Requires PHP: 7.3 * Version: 6.4.2 * * @package WooCommerce\Payments From c8f19eca3515fd6fa591952f13bc7c95f5f8ac2f Mon Sep 17 00:00:00 2001 From: Hsing-yu Flowers Date: Fri, 15 Sep 2023 13:24:11 -0700 Subject: [PATCH 73/84] Redirect back to the pay-for-order page when it's pay-for-order order (#7161) --- changelog/fix-2141 | 4 ++ changelog/fix-init-woopay-error | 4 ++ client/checkout/api/index.js | 4 ++ client/checkout/api/test/index.test.js | 13 ++++- client/checkout/woopay/email-input-iframe.js | 3 + .../express-button/express-checkout-iframe.js | 3 + includes/woopay/class-woopay-session.php | 56 ++++++++++++------- 7 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 changelog/fix-2141 create mode 100644 changelog/fix-init-woopay-error diff --git a/changelog/fix-2141 b/changelog/fix-2141 new file mode 100644 index 00000000000..71442126624 --- /dev/null +++ b/changelog/fix-2141 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Redirect back to the pay-for-order page when it is pay-for-order order diff --git a/changelog/fix-init-woopay-error b/changelog/fix-init-woopay-error new file mode 100644 index 00000000000..58cbd587cec --- /dev/null +++ b/changelog/fix-init-woopay-error @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix init WooPay and empty cart error diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index e7fcee97a18..d53f7170b68 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -690,10 +690,14 @@ export default class WCPayAPI { initWooPay( userEmail, woopayUserSession ) { const wcAjaxUrl = getConfig( 'wcAjaxUrl' ); const nonce = getConfig( 'initWooPayNonce' ); + return this.request( buildAjaxURL( wcAjaxUrl, 'init_woopay' ), { _wpnonce: nonce, email: userEmail, user_session: woopayUserSession, + order_id: getConfig( 'order_id' ), + key: getConfig( 'key' ), + billing_email: getConfig( 'billing_email' ), } ); } diff --git a/client/checkout/api/test/index.test.js b/client/checkout/api/test/index.test.js index 6c1992f6816..5a2711a4da0 100644 --- a/client/checkout/api/test/index.test.js +++ b/client/checkout/api/test/index.test.js @@ -17,7 +17,15 @@ jest.mock( 'wcpay/utils/checkout', () => ( { describe( 'WCPayAPI', () => { test( 'initializes woopay using config params', () => { buildAjaxURL.mockReturnValue( 'https://example.org/' ); - getConfig.mockReturnValue( 'foo' ); + getConfig.mockImplementation( ( key ) => { + const mockProperties = { + initWooPayNonce: 'foo', + order_id: 1, + key: 'testkey', + billing_email: 'test@example.com', + }; + return mockProperties[ key ]; + } ); const api = new WCPayAPI( {}, request ); api.initWooPay( 'foo@bar.com', 'qwerty123' ); @@ -26,6 +34,9 @@ describe( 'WCPayAPI', () => { _wpnonce: 'foo', email: 'foo@bar.com', user_session: 'qwerty123', + order_id: 1, + key: 'testkey', + billing_email: 'test@example.com', } ); } ); } ); diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index 47ae94dff85..a30e7cc26ce 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -190,6 +190,9 @@ export const handleWooPayEmailInput = async ( buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ), { _ajax_nonce: getConfig( 'woopaySessionNonce' ), + order_id: getConfig( 'order_id' ), + key: getConfig( 'key' ), + billing_email: getConfig( 'billing_email' ), } ).then( ( response ) => { if ( response?.data?.session ) { diff --git a/client/checkout/woopay/express-button/express-checkout-iframe.js b/client/checkout/woopay/express-button/express-checkout-iframe.js index 95b49e62091..b021e9eab15 100644 --- a/client/checkout/woopay/express-button/express-checkout-iframe.js +++ b/client/checkout/woopay/express-button/express-checkout-iframe.js @@ -96,6 +96,9 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ), { _ajax_nonce: getConfig( 'woopaySessionNonce' ), + order_id: getConfig( 'order_id' ), + key: getConfig( 'key' ), + billing_email: getConfig( 'billing_email' ), } ).then( ( response ) => { if ( response?.data?.session ) { diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index fbeb4879096..3f6d69b4500 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -44,7 +44,9 @@ class WooPay_Session { '@^\/wc\/store(\/v[\d]+)?\/cart\/update-customer$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/update-item$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/extensions$@', + '@^\/wc\/store(\/v[\d]+)?\/checkout\/(?P[\d]+)@', '@^\/wc\/store(\/v[\d]+)?\/checkout$@', + '@^\/wc\/store(\/v[\d]+)?\/order\/(?P[\d]+)@', ]; /** @@ -289,7 +291,13 @@ public static function get_frontend_init_session_request() { return []; } - $session = self::get_init_session_request(); + // phpcs:disable WordPress.Security.NonceVerification.Missing + $order_id = ! empty( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : null; + $key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : null; + $billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null; + // phpcs:enable + + $session = self::get_init_session_request( $order_id, $key, $billing_email ); $store_blog_token = ( WooPay_Utilities::get_woopay_url() === WooPay_Utilities::DEFAULT_WOOPAY_URL ) ? Jetpack_Options::get_option( 'blog_token' ) : 'dev_mode'; @@ -321,11 +329,16 @@ public static function get_frontend_init_session_request() { /** * Returns the initial session request data. * + * @param int|null $order_id Pay-for-order order ID. + * @param string|null $key Pay-for-order key. + * @param string|null $billing_email Pay-for-order billing email. * @return array The initial session request data without email and user_session. */ - private static function get_init_session_request() { - $user = wp_get_current_user(); - $customer_id = WC_Payments::get_customer_service()->get_customer_id_by_user_id( $user->ID ); + private static function get_init_session_request( $order_id = null, $key = null, $billing_email = null ) { + $user = wp_get_current_user(); + $is_pay_for_order = null !== $order_id; + $order = wc_get_order( $order_id ); + $customer_id = WC_Payments::get_customer_service()->get_customer_id_by_user_id( $user->ID ); if ( null === $customer_id ) { // create customer. $customer_data = WC_Payments_Customer_Service::map_customer_data( null, new WC_Customer( $user->ID ) ); @@ -345,20 +358,15 @@ private static function get_init_session_request() { $account_id = WC_Payments::get_account_service()->get_stripe_account_id(); - $site_logo_id = get_theme_mod( 'custom_logo' ); - $site_logo_url = $site_logo_id ? ( wp_get_attachment_image_src( $site_logo_id, 'full' )[0] ?? '' ) : ''; - $woopay_store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' ); - - $store_logo = $site_logo_url; - if ( ! empty( $woopay_store_logo ) ) { - $store_logo = get_rest_url( null, 'wc/v3/payments/file/' . $woopay_store_logo ); - } + $store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' ); include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php'; $blocks_data_extractor = new Blocks_Data_Extractor(); // This uses the same logic as the Checkout block in hydrate_from_api to get the cart and checkout data. - $cart_data = rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body']; + $cart_data = ! $is_pay_for_order + ? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body'] + : rest_preload_api_request( [], "/wc/store/v1/order/{$order_id}?key={$key}&billing_email={$billing_email}" )[ "/wc/store/v1/order/{$order_id}?key={$key}&billing_email={$billing_email}" ]['body']; add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); $preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' ); remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); @@ -373,11 +381,11 @@ private static function get_init_session_request() { 'email' => '', 'store_data' => [ 'store_name' => get_bloginfo( 'name' ), - 'store_logo' => $store_logo, - 'custom_message' => self::get_formatted_custom_message(), + 'store_logo' => ! empty( $store_logo ) ? get_rest_url( null, 'wc/v3/payments/file/' . $store_logo ) : '', + 'custom_message' => WC_Payments::get_gateway()->get_option( 'platform_checkout_custom_message' ), 'blog_id' => Jetpack_Options::get_option( 'id' ), 'blog_url' => get_site_url(), - 'blog_checkout_url' => wc_get_checkout_url(), + 'blog_checkout_url' => ! $is_pay_for_order ? wc_get_checkout_url() : $order->get_checkout_payment_url(), 'blog_shop_url' => get_permalink( wc_get_page_id( 'shop' ) ), 'store_api_url' => self::get_store_api_url(), 'account_id' => $account_id, @@ -386,14 +394,19 @@ private static function get_init_session_request() { 'is_subscriptions_plugin_active' => WC_Payments::get_gateway()->is_subscriptions_plugin_active(), 'woocommerce_tax_display_cart' => get_option( 'woocommerce_tax_display_cart' ), 'ship_to_billing_address_only' => wc_ship_to_billing_address_only(), - 'return_url' => wc_get_cart_url(), + 'return_url' => ! $is_pay_for_order ? wc_get_cart_url() : $order->get_checkout_payment_url(), 'blocks_data' => $blocks_data_extractor->get_data(), 'checkout_schema_namespaces' => $blocks_data_extractor->get_checkout_schema_namespaces(), ], 'user_session' => null, - 'preloaded_requests' => [ + 'preloaded_requests' => ! $is_pay_for_order ? [ 'cart' => $cart_data, 'checkout' => $checkout_data, + ] : [ + 'cart' => $cart_data, + 'checkout' => [ + 'order_id' => $order_id, // This is a workaround for the checkout order error. https://github.com/woocommerce/woocommerce-blocks/blob/04f36065b34977f02079e6c2c8cb955200a783ff/assets/js/blocks/checkout/block.tsx#L81-L83. + ], ], 'tracks_user_identity' => WC_Payments::woopay_tracker()->tracks_get_identity( $user->ID ), ]; @@ -416,9 +429,12 @@ public static function ajax_init_woopay() { ); } - $email = ! empty( $_POST['email'] ) ? wc_clean( wp_unslash( $_POST['email'] ) ) : ''; + $email = ! empty( $_POST['email'] ) ? wc_clean( wp_unslash( $_POST['email'] ) ) : ''; + $order_id = ! empty( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : null; + $key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : null; + $billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null; - $body = self::get_init_session_request(); + $body = self::get_init_session_request( $order_id, $key, $billing_email ); $body['email'] = $email; $body['user_session'] = isset( $_REQUEST['user_session'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['user_session'] ) ) : null; From 6ba6faba102427ccb1442a2ba328e48eb8d30101 Mon Sep 17 00:00:00 2001 From: ridonibishi Date: Sat, 16 Sep 2023 17:13:54 +0200 Subject: [PATCH 74/84] Fix deprecated string interpolation in Analytics.php (#7170) Co-authored-by: Taha Paksu <3295+tpaksu@users.noreply.github.com> --- changelog/fix-7169-deprecated-string-interpolation | 5 +++++ includes/multi-currency/Analytics.php | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-7169-deprecated-string-interpolation diff --git a/changelog/fix-7169-deprecated-string-interpolation b/changelog/fix-7169-deprecated-string-interpolation new file mode 100644 index 00000000000..b9d7b5b8ea0 --- /dev/null +++ b/changelog/fix-7169-deprecated-string-interpolation @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Fix deprecated string interpolation of ${var} with {$var} + + diff --git a/includes/multi-currency/Analytics.php b/includes/multi-currency/Analytics.php index 19d8870ec3d..949c2cd0632 100644 --- a/includes/multi-currency/Analytics.php +++ b/includes/multi-currency/Analytics.php @@ -316,9 +316,9 @@ public function filter_join_clauses( array $clauses, $context ): array { $clauses[] = "LEFT JOIN {$meta_table} {$currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$currency_tbl}.{$id_field} AND {$currency_tbl}.meta_key = '_order_currency'"; } - $clauses[] = "LEFT JOIN {$meta_table} {$default_currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$default_currency_tbl}.{$id_field} AND ${default_currency_tbl}.meta_key = '_wcpay_multi_currency_order_default_currency'"; - $clauses[] = "LEFT JOIN {$meta_table} {$exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$exchange_rate_tbl}.{$id_field} AND ${exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_order_exchange_rate'"; - $clauses[] = "LEFT JOIN {$meta_table} {$stripe_exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$stripe_exchange_rate_tbl}.{$id_field} AND ${stripe_exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_stripe_exchange_rate'"; + $clauses[] = "LEFT JOIN {$meta_table} {$default_currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$default_currency_tbl}.{$id_field} AND {$default_currency_tbl}.meta_key = '_wcpay_multi_currency_order_default_currency'"; + $clauses[] = "LEFT JOIN {$meta_table} {$exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$exchange_rate_tbl}.{$id_field} AND {$exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_order_exchange_rate'"; + $clauses[] = "LEFT JOIN {$meta_table} {$stripe_exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$stripe_exchange_rate_tbl}.{$id_field} AND {$stripe_exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_stripe_exchange_rate'"; } return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_join_clauses', $clauses ); From 369551404275ed178985e87b53fbc8fe474875f0 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Sat, 16 Sep 2023 21:41:13 +0300 Subject: [PATCH 75/84] Fix double indicators showing under Payments tab (#7201) --- changelog/fix-7042-double-payments-badge | 4 ++++ includes/admin/class-wc-payments-admin.php | 7 +++++-- includes/class-wc-payments-incentives-service.php | 7 +++++-- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-7042-double-payments-badge diff --git a/changelog/fix-7042-double-payments-badge b/changelog/fix-7042-double-payments-badge new file mode 100644 index 00000000000..35b2a889b49 --- /dev/null +++ b/changelog/fix-7042-double-payments-badge @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix double indicators showing under Payments tab diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 3080cd9a4f8..42be3812564 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -954,9 +954,12 @@ public function add_menu_notification_badge() { return; } + $badge = self::MENU_NOTIFICATION_BADGE; foreach ( $menu as $index => $menu_item ) { - if ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) { - $menu[ $index ][0] .= self::MENU_NOTIFICATION_BADGE; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + if ( false === strpos( $menu_item[0], $badge ) && ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) ) { + $menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + // One menu item with a badge is more than enough. break; } } diff --git a/includes/class-wc-payments-incentives-service.php b/includes/class-wc-payments-incentives-service.php index 5425175a6d9..240bbcf8782 100644 --- a/includes/class-wc-payments-incentives-service.php +++ b/includes/class-wc-payments-incentives-service.php @@ -48,9 +48,12 @@ public function add_payments_menu_badge(): void { return; } + $badge = WC_Payments_Admin::MENU_NOTIFICATION_BADGE; foreach ( $menu as $index => $menu_item ) { - if ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) { - $menu[ $index ][0] .= WC_Payments_Admin::MENU_NOTIFICATION_BADGE; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + if ( false === strpos( $menu_item[0], $badge ) && ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) ) { + $menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + // One menu item with a badge is more than enough. break; } } From 20010349765f523321ff664bb5df6f1dc97a0074 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 17 Sep 2023 12:07:16 +0000 Subject: [PATCH 76/84] Update version and add changelog entries for release 6.5.0 --- changelog.txt | 62 ++++++++++++++++++ ...as-multi-currency-orders-query-improvement | 4 -- ...52-get_all_customer_currencies_improvement | 4 -- .../add-5669-add-further-payment-metadata | 4 -- ...6429-warn-about-dev-mode-on-new-onboarding | 4 -- changelog/add-6874-add-kanji-kana | 4 -- changelog/add-6923-dispute-details-notice | 3 - changelog/add-6924-dispute-details-attributes | 5 -- ...tails-dispute-challenge-in-progress-notice | 5 -- changelog/add-7048-banner-notice-component | 5 -- changelog/add-7173-dispute-evidence | 4 -- changelog/add-incentive-task-badge | 4 -- changelog/add-pay-for-order | 4 -- ...-use-site-logo-when-no-woopay-logo-defined | 4 -- changelog/deferred-upe-rollout-notices | 4 -- changelog/dev-6441-inbox-notifications-update | 4 -- changelog/dev-6779-po-new-task | 4 -- changelog/dev-add-cli-command | 5 -- changelog/dev-add-dispute-summary-row-tests | 5 -- changelog/dev-details-link-ts-migration | 4 -- changelog/dev-remove-v1-experiment | 4 -- changelog/dev-ubuntu-workflow | 4 -- changelog/dev-update-workflows | 4 -- ...at-add-checkout-appearance-learn-more-link | 4 -- changelog/fix-2141 | 4 -- ...x-5507-block-currency-update-on-sub-switch | 4 -- changelog/fix-6183-exchange-date | 4 -- ...ix-6192-currency-switcher-block-on-windows | 4 -- changelog/fix-6429-follow-up-fix | 5 -- changelog/fix-6529-add-stripe-billing-context | 4 -- .../fix-6529-add-stripe-billing-context-2 | 4 -- .../fix-6633-sar-aed-currencies-formatting | 4 -- ...currency-manual-exchange-rate-is-not-saved | 4 -- .../fix-6951-validate-set-title-for-email | 5 -- .../fix-6981-missing-onboarding-field-data | 4 -- changelog/fix-7042-double-payments-badge | 4 -- changelog/fix-7061-extended-requests | 4 -- .../fix-7067-admin-enqueue-scripts-priority | 4 -- ...x-7135-currency-switch-widget-on-edit-post | 4 -- ...-cancel-authorization-flaky-error-response | 4 -- .../fix-7169-deprecated-string-interpolation | 5 -- ...tripe-billing-payment-method-doesnt-cancel | 4 -- ...fix-deprecation-warning-on-blocks-checkout | 4 -- .../fix-dispute-staged-evidence-notice-typo | 5 -- changelog/fix-docs-links-part-2 | 4 -- .../fix-enable-tracking-on-connected-accounts | 4 -- changelog/fix-experiment-v3 | 4 -- .../fix-express-checkouts-links-consistency | 4 -- .../fix-improve-transaction-details-redirect | 4 -- changelog/fix-init-woopay-error | 4 -- ...ix-invalid-currency-from-store-api-request | 4 -- changelog/fix-migrate-subscription-tokens | 5 -- ...ent-intent-meta-for-recurring-transactions | 4 -- ...fix-remove-unused-import-noticeoutlineicon | 5 -- changelog/fix-request-constant-traversing | 4 -- changelog/fix-title-task-continue-onboarding | 4 -- changelog/fix-woopay-appearance-width | 4 -- changelog/imp-7052-migrate-to-ts | 4 -- changelog/imp-7052-migrate-to-ts-woopay | 4 -- ...-6526-schedule-subscription-migration-tool | 5 -- changelog/issue-7038-retry-migration | 5 -- changelog/rpp-6679-factor-flags | 4 -- changelog/rpp-6684-request-class | 4 -- changelog/rpp-6685-load-payment-methods | 4 -- changelog/stripe-billing-migration-notices | 4 -- changelog/stripe-billing-setting | 4 -- changelog/subscriptions-core-6.2.0-1 | 4 -- changelog/subscriptions-core-6.2.0-2 | 4 -- changelog/subscriptions-core-6.2.0-3 | 4 -- changelog/subscriptions-core-6.2.0-4 | 4 -- changelog/temporarily-disable-saving-sepa | 4 -- changelog/update-6186-mc-settings-links | 4 -- ...e-6378-disable-refund-button-when-disputed | 4 -- changelog/update-6991-table-tooltip | 4 -- changelog/update-7048-inline-notice-component | 5 -- changelog/update-7098-onboarding-components | 5 -- ...se-constant-return-same-object-static-call | 4 -- ...date-horizontal-list-label-style-uppercase | 5 -- changelog/update-stripe-billing-notice-links | 5 -- ...pdate-subscription-next-payment-on-migrate | 5 -- changelog/verify_payment_token_on_migration | 5 -- package-lock.json | 4 +- package.json | 2 +- readme.txt | 64 ++++++++++++++++++- woocommerce-payments.php | 2 +- 85 files changed, 129 insertions(+), 343 deletions(-) delete mode 100644 changelog/6814-has-multi-currency-orders-query-improvement delete mode 100644 changelog/6852-get_all_customer_currencies_improvement delete mode 100644 changelog/add-5669-add-further-payment-metadata delete mode 100644 changelog/add-6429-warn-about-dev-mode-on-new-onboarding delete mode 100644 changelog/add-6874-add-kanji-kana delete mode 100644 changelog/add-6923-dispute-details-notice delete mode 100644 changelog/add-6924-dispute-details-attributes delete mode 100644 changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice delete mode 100644 changelog/add-7048-banner-notice-component delete mode 100644 changelog/add-7173-dispute-evidence delete mode 100644 changelog/add-incentive-task-badge delete mode 100644 changelog/add-pay-for-order delete mode 100644 changelog/add-use-site-logo-when-no-woopay-logo-defined delete mode 100644 changelog/deferred-upe-rollout-notices delete mode 100644 changelog/dev-6441-inbox-notifications-update delete mode 100644 changelog/dev-6779-po-new-task delete mode 100644 changelog/dev-add-cli-command delete mode 100644 changelog/dev-add-dispute-summary-row-tests delete mode 100644 changelog/dev-details-link-ts-migration delete mode 100644 changelog/dev-remove-v1-experiment delete mode 100644 changelog/dev-ubuntu-workflow delete mode 100644 changelog/dev-update-workflows delete mode 100644 changelog/feat-add-checkout-appearance-learn-more-link delete mode 100644 changelog/fix-2141 delete mode 100644 changelog/fix-5507-block-currency-update-on-sub-switch delete mode 100644 changelog/fix-6183-exchange-date delete mode 100644 changelog/fix-6192-currency-switcher-block-on-windows delete mode 100644 changelog/fix-6429-follow-up-fix delete mode 100644 changelog/fix-6529-add-stripe-billing-context delete mode 100644 changelog/fix-6529-add-stripe-billing-context-2 delete mode 100644 changelog/fix-6633-sar-aed-currencies-formatting delete mode 100644 changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved delete mode 100644 changelog/fix-6951-validate-set-title-for-email delete mode 100644 changelog/fix-6981-missing-onboarding-field-data delete mode 100644 changelog/fix-7042-double-payments-badge delete mode 100644 changelog/fix-7061-extended-requests delete mode 100644 changelog/fix-7067-admin-enqueue-scripts-priority delete mode 100644 changelog/fix-7135-currency-switch-widget-on-edit-post delete mode 100644 changelog/fix-7149-fix-cancel-authorization-flaky-error-response delete mode 100644 changelog/fix-7169-deprecated-string-interpolation delete mode 100644 changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel delete mode 100644 changelog/fix-deprecation-warning-on-blocks-checkout delete mode 100644 changelog/fix-dispute-staged-evidence-notice-typo delete mode 100644 changelog/fix-docs-links-part-2 delete mode 100644 changelog/fix-enable-tracking-on-connected-accounts delete mode 100644 changelog/fix-experiment-v3 delete mode 100644 changelog/fix-express-checkouts-links-consistency delete mode 100644 changelog/fix-improve-transaction-details-redirect delete mode 100644 changelog/fix-init-woopay-error delete mode 100644 changelog/fix-invalid-currency-from-store-api-request delete mode 100644 changelog/fix-migrate-subscription-tokens delete mode 100644 changelog/fix-payment-intent-meta-for-recurring-transactions delete mode 100644 changelog/fix-remove-unused-import-noticeoutlineicon delete mode 100644 changelog/fix-request-constant-traversing delete mode 100644 changelog/fix-title-task-continue-onboarding delete mode 100644 changelog/fix-woopay-appearance-width delete mode 100644 changelog/imp-7052-migrate-to-ts delete mode 100644 changelog/imp-7052-migrate-to-ts-woopay delete mode 100644 changelog/issue-6526-schedule-subscription-migration-tool delete mode 100644 changelog/issue-7038-retry-migration delete mode 100644 changelog/rpp-6679-factor-flags delete mode 100644 changelog/rpp-6684-request-class delete mode 100644 changelog/rpp-6685-load-payment-methods delete mode 100644 changelog/stripe-billing-migration-notices delete mode 100644 changelog/stripe-billing-setting delete mode 100644 changelog/subscriptions-core-6.2.0-1 delete mode 100644 changelog/subscriptions-core-6.2.0-2 delete mode 100644 changelog/subscriptions-core-6.2.0-3 delete mode 100644 changelog/subscriptions-core-6.2.0-4 delete mode 100644 changelog/temporarily-disable-saving-sepa delete mode 100644 changelog/update-6186-mc-settings-links delete mode 100644 changelog/update-6378-disable-refund-button-when-disputed delete mode 100644 changelog/update-6991-table-tooltip delete mode 100644 changelog/update-7048-inline-notice-component delete mode 100644 changelog/update-7098-onboarding-components delete mode 100644 changelog/update-base-constant-return-same-object-static-call delete mode 100644 changelog/update-horizontal-list-label-style-uppercase delete mode 100644 changelog/update-stripe-billing-notice-links delete mode 100644 changelog/update-subscription-next-payment-on-migrate delete mode 100644 changelog/verify_payment_token_on_migration diff --git a/changelog.txt b/changelog.txt index 13f4ed780c8..0750efdbacd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,67 @@ *** WooPayments Changelog *** += 6.5.0 - 2023-09-20 = +* Add - Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. +* Add - Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. +* Add - Added additional meta data to payment requests +* Add - Add onboarding task incentive badge. +* Add - Add payment request class for loading, sanitizing, and escaping data (reengineering payment process) +* Add - Add the express button on the pay for order page +* Add - add WooPay checkout appearance documentation link +* Add - Fall back to site logo when a custom WooPay logo has not been defined +* Add - Introduce a new setting that enables store to opt into Subscription off-site billing via Stripe Billing. +* Add - Load payment methods through the request class (re-engineering payment process). +* Add - Record the source (Woo Subscriptions or WCPay Subscriptions) when a Stripe Billing subscription is created. +* Add - Record the subscriptions environment context in transaction meta when Stripe Billing payments are handled. +* Add - Redirect back to the pay-for-order page when it is pay-for-order order +* Add - Support kanji and kana statement descriptors for Japanese merchants +* Add - Warn about dev mode enabled on new onboarding flow choice +* Fix - Allow request classes to be extended more than once. +* Fix - Avoid empty fields in new onboarding flow +* Fix - Corrected an issue causing incorrect responses at the cancel authorization API endpoint. +* Fix - Ensure the shipping phone number field is copied to subscriptions and their orders when copying address meta. +* Fix - Ensure the Stripe Billing subscription is cancelled when the subscription is changed from WooPayments to another payment method. +* Fix - express checkout links UI consistency & area increase +* Fix - fix checkout appearance width +* Fix - Fix Currency Switcher Block flag rendering on Windows platform. +* Fix - Fix deprecation warnings on blocks checkout. +* Fix - Fix double indicators showing under Payments tab +* Fix - Fixes the currency formatting for AED and SAR currencies. +* Fix - Fix init WooPay and empty cart error +* Fix - Fix Multi-currency exchange rate date format when using custom date or time settings. +* Fix - Fix Multicurrency widget error on post/page edit screen +* Fix - Fix single currency manual rate save producing error when no changes are made +* Fix - Fix the way request params are loaded between parent and child classes. +* Fix - Fix WooPay Session Handler in Store API requests. +* Fix - Increase admin enqueue scripts priority to avoid compatibility issues with WooCommerce Beta Tester plugin. +* Fix - Modify title in task to continue with onboarding +* Fix - Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches. +* Fix - Update inbox note logic to prevent prompt to set up payment methods from showing on not fully onboarded account. +* Update - Add notice for legacy UPE users about deferred UPE upcoming, and adjust wording for non-UPE users +* Update - Disable refund button on order edit page when there is active or lost dispute. +* Update - Enhanced Analytics SQL, added unit test for has_multi_currency_orders(). Improved code quality and test coverage. +* Update - Improved `get_all_customer_currencies` method to retrieve existing order currencies faster. +* Update - Improve the transaction details redirect user-experience by using client-side routing. +* Update - Temporarily disable saving SEPA +* Update - Update Multi-currency documentation links. +* Update - Update outdated public documentation links on WooCommerce.com +* Update - Update Tooltip component on ConvertedAmount. +* Update - When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache to improve performance. +* Dev - Adding factor flags to control when to enter the new payment process. +* Dev - Adding issuer evidence to dispute details. Hidden behind a feature flag +* Dev - Comment: Update GH workflows to use PHP version from plugin file. +* Dev - Comment: Update occurence of all ubuntu versions to ubuntu-latest +* Dev - Deprecated the 'woocommerce_subscriptions_not_found_label' filter. +* Dev - Fix payment context and subscription payment metadata stored on subscription recurring transactions. +* Dev - Fix Tracks conditions +* Dev - Migrate DetailsLink component to TypeScript to improve code quality +* Dev - Migrate link-item.js to typescript +* Dev - Migrate woopay-item to typescript +* Dev - Remove reference to old experiment. +* Dev - Update Base_Constant to return the singleton object for same static calls. +* Dev - Updated subscriptions-core to 6.2.0 +* Dev - Update the name of the A/B experiment on new onboarding. + = 6.4.2 - 2023-09-14 = * Fix - Fix an error in the checkout when Afterpay is selected as payment method. diff --git a/changelog/6814-has-multi-currency-orders-query-improvement b/changelog/6814-has-multi-currency-orders-query-improvement deleted file mode 100644 index dc97afed7df..00000000000 --- a/changelog/6814-has-multi-currency-orders-query-improvement +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Enhanced Analytics SQL, added unit test for has_multi_currency_orders(). Improved code quality and test coverage. diff --git a/changelog/6852-get_all_customer_currencies_improvement b/changelog/6852-get_all_customer_currencies_improvement deleted file mode 100644 index 09be89d6180..00000000000 --- a/changelog/6852-get_all_customer_currencies_improvement +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Improved `get_all_customer_currencies` method to retrieve existing order currencies faster. diff --git a/changelog/add-5669-add-further-payment-metadata b/changelog/add-5669-add-further-payment-metadata deleted file mode 100644 index 347c49daf22..00000000000 --- a/changelog/add-5669-add-further-payment-metadata +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Added additional meta data to payment requests diff --git a/changelog/add-6429-warn-about-dev-mode-on-new-onboarding b/changelog/add-6429-warn-about-dev-mode-on-new-onboarding deleted file mode 100644 index 386c3f79ba7..00000000000 --- a/changelog/add-6429-warn-about-dev-mode-on-new-onboarding +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Warn about dev mode enabled on new onboarding flow choice diff --git a/changelog/add-6874-add-kanji-kana b/changelog/add-6874-add-kanji-kana deleted file mode 100644 index ecd1574347c..00000000000 --- a/changelog/add-6874-add-kanji-kana +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Support kanji and kana statement descriptors for Japanese merchants diff --git a/changelog/add-6923-dispute-details-notice b/changelog/add-6923-dispute-details-notice deleted file mode 100644 index 027a1fa2448..00000000000 --- a/changelog/add-6923-dispute-details-notice +++ /dev/null @@ -1,3 +0,0 @@ -Significance: patch -Type: add -Comment: Dispute notice added to transactions screen behind a feature flag. diff --git a/changelog/add-6924-dispute-details-attributes b/changelog/add-6924-dispute-details-attributes deleted file mode 100644 index 0e2dfaa37d9..00000000000 --- a/changelog/add-6924-dispute-details-attributes +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: add -Comment: Add dispute details to transaction page, hidden behind feature flag. - - diff --git a/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice b/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice deleted file mode 100644 index 3b03ed5b61e..00000000000 --- a/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: add -Comment: Behind feature flag: add staged dispute notice to Transaction Details screen - - diff --git a/changelog/add-7048-banner-notice-component b/changelog/add-7048-banner-notice-component deleted file mode 100644 index f9ccb0504d8..00000000000 --- a/changelog/add-7048-banner-notice-component +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: add -Comment: Add BannerNotice component, with no major UI difference for merchants. - - diff --git a/changelog/add-7173-dispute-evidence b/changelog/add-7173-dispute-evidence deleted file mode 100644 index 0e1e7ca1d92..00000000000 --- a/changelog/add-7173-dispute-evidence +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Adding issuer evidence to dispute details. Hidden behind a feature flag diff --git a/changelog/add-incentive-task-badge b/changelog/add-incentive-task-badge deleted file mode 100644 index f2a30452565..00000000000 --- a/changelog/add-incentive-task-badge +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add onboarding task incentive badge. diff --git a/changelog/add-pay-for-order b/changelog/add-pay-for-order deleted file mode 100644 index b44cf523113..00000000000 --- a/changelog/add-pay-for-order +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Add the express button on the pay for order page diff --git a/changelog/add-use-site-logo-when-no-woopay-logo-defined b/changelog/add-use-site-logo-when-no-woopay-logo-defined deleted file mode 100644 index 0afbfccf655..00000000000 --- a/changelog/add-use-site-logo-when-no-woopay-logo-defined +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Fall back to site logo when a custom WooPay logo has not been defined diff --git a/changelog/deferred-upe-rollout-notices b/changelog/deferred-upe-rollout-notices deleted file mode 100644 index 231d4bb7942..00000000000 --- a/changelog/deferred-upe-rollout-notices +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Add notice for legacy UPE users about deferred UPE upcoming, and adjust wording for non-UPE users diff --git a/changelog/dev-6441-inbox-notifications-update b/changelog/dev-6441-inbox-notifications-update deleted file mode 100644 index b93787b65af..00000000000 --- a/changelog/dev-6441-inbox-notifications-update +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Update inbox note logic to prevent prompt to set up payment methods from showing on not fully onboarded account. diff --git a/changelog/dev-6779-po-new-task b/changelog/dev-6779-po-new-task deleted file mode 100644 index c49c78654fa..00000000000 --- a/changelog/dev-6779-po-new-task +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. diff --git a/changelog/dev-add-cli-command b/changelog/dev-add-cli-command deleted file mode 100644 index 4451c5b4326..00000000000 --- a/changelog/dev-add-cli-command +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: It is only dev-facing. - - diff --git a/changelog/dev-add-dispute-summary-row-tests b/changelog/dev-add-dispute-summary-row-tests deleted file mode 100644 index 1aba1754bb0..00000000000 --- a/changelog/dev-add-dispute-summary-row-tests +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Not user-facing: updates tests for DisputeDetails component only. - - diff --git a/changelog/dev-details-link-ts-migration b/changelog/dev-details-link-ts-migration deleted file mode 100644 index daaa601b05b..00000000000 --- a/changelog/dev-details-link-ts-migration +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Migrate DetailsLink component to TypeScript to improve code quality diff --git a/changelog/dev-remove-v1-experiment b/changelog/dev-remove-v1-experiment deleted file mode 100644 index f4d0231167e..00000000000 --- a/changelog/dev-remove-v1-experiment +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Remove reference to old experiment. diff --git a/changelog/dev-ubuntu-workflow b/changelog/dev-ubuntu-workflow deleted file mode 100644 index 79c42cc4fc7..00000000000 --- a/changelog/dev-ubuntu-workflow +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Comment: Update occurence of all ubuntu versions to ubuntu-latest diff --git a/changelog/dev-update-workflows b/changelog/dev-update-workflows deleted file mode 100644 index cdab2b4fa9f..00000000000 --- a/changelog/dev-update-workflows +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Comment: Update GH workflows to use PHP version from plugin file. diff --git a/changelog/feat-add-checkout-appearance-learn-more-link b/changelog/feat-add-checkout-appearance-learn-more-link deleted file mode 100644 index 737041997a4..00000000000 --- a/changelog/feat-add-checkout-appearance-learn-more-link +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -add WooPay checkout appearance documentation link diff --git a/changelog/fix-2141 b/changelog/fix-2141 deleted file mode 100644 index 71442126624..00000000000 --- a/changelog/fix-2141 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Redirect back to the pay-for-order page when it is pay-for-order order diff --git a/changelog/fix-5507-block-currency-update-on-sub-switch b/changelog/fix-5507-block-currency-update-on-sub-switch deleted file mode 100644 index 9fd0d5e5019..00000000000 --- a/changelog/fix-5507-block-currency-update-on-sub-switch +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches. diff --git a/changelog/fix-6183-exchange-date b/changelog/fix-6183-exchange-date deleted file mode 100644 index 240f51b7e1a..00000000000 --- a/changelog/fix-6183-exchange-date +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix Multi-currency exchange rate date format when using custom date or time settings. diff --git a/changelog/fix-6192-currency-switcher-block-on-windows b/changelog/fix-6192-currency-switcher-block-on-windows deleted file mode 100644 index ba18cbf9042..00000000000 --- a/changelog/fix-6192-currency-switcher-block-on-windows +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix Currency Switcher Block flag rendering on Windows platform. diff --git a/changelog/fix-6429-follow-up-fix b/changelog/fix-6429-follow-up-fix deleted file mode 100644 index 426b76ace57..00000000000 --- a/changelog/fix-6429-follow-up-fix +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Warn about dev mode roll back to inline notice - - diff --git a/changelog/fix-6529-add-stripe-billing-context b/changelog/fix-6529-add-stripe-billing-context deleted file mode 100644 index 010248eefe7..00000000000 --- a/changelog/fix-6529-add-stripe-billing-context +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Record the subscriptions environment context in transaction meta when Stripe Billing payments are handled. diff --git a/changelog/fix-6529-add-stripe-billing-context-2 b/changelog/fix-6529-add-stripe-billing-context-2 deleted file mode 100644 index fe9672a589a..00000000000 --- a/changelog/fix-6529-add-stripe-billing-context-2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Record the source (Woo Subscriptions or WCPay Subscriptions) when a Stripe Billing subscription is created. diff --git a/changelog/fix-6633-sar-aed-currencies-formatting b/changelog/fix-6633-sar-aed-currencies-formatting deleted file mode 100644 index 160ef981508..00000000000 --- a/changelog/fix-6633-sar-aed-currencies-formatting +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixes the currency formatting for AED and SAR currencies. diff --git a/changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved b/changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved deleted file mode 100644 index 3b44f6d3ef0..00000000000 --- a/changelog/fix-6857-multi-currency-manual-exchange-rate-is-not-saved +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix single currency manual rate save producing error when no changes are made diff --git a/changelog/fix-6951-validate-set-title-for-email b/changelog/fix-6951-validate-set-title-for-email deleted file mode 100644 index 8b2a138daf0..00000000000 --- a/changelog/fix-6951-validate-set-title-for-email +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Minor bug fix only adding a defensive check - - diff --git a/changelog/fix-6981-missing-onboarding-field-data b/changelog/fix-6981-missing-onboarding-field-data deleted file mode 100644 index d8170c0d31d..00000000000 --- a/changelog/fix-6981-missing-onboarding-field-data +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Avoid empty fields in new onboarding flow diff --git a/changelog/fix-7042-double-payments-badge b/changelog/fix-7042-double-payments-badge deleted file mode 100644 index 35b2a889b49..00000000000 --- a/changelog/fix-7042-double-payments-badge +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix double indicators showing under Payments tab diff --git a/changelog/fix-7061-extended-requests b/changelog/fix-7061-extended-requests deleted file mode 100644 index 1c62343b3c8..00000000000 --- a/changelog/fix-7061-extended-requests +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Allow request classes to be extended more than once. diff --git a/changelog/fix-7067-admin-enqueue-scripts-priority b/changelog/fix-7067-admin-enqueue-scripts-priority deleted file mode 100644 index d23c2da6dd7..00000000000 --- a/changelog/fix-7067-admin-enqueue-scripts-priority +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Increase admin enqueue scripts priority to avoid compatibility issues with WooCommerce Beta Tester plugin. diff --git a/changelog/fix-7135-currency-switch-widget-on-edit-post b/changelog/fix-7135-currency-switch-widget-on-edit-post deleted file mode 100644 index 357711e2484..00000000000 --- a/changelog/fix-7135-currency-switch-widget-on-edit-post +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix Multicurrency widget error on post/page edit screen diff --git a/changelog/fix-7149-fix-cancel-authorization-flaky-error-response b/changelog/fix-7149-fix-cancel-authorization-flaky-error-response deleted file mode 100644 index 27cbbbbeea7..00000000000 --- a/changelog/fix-7149-fix-cancel-authorization-flaky-error-response +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Corrected an issue causing incorrect responses at the cancel authorization API endpoint. diff --git a/changelog/fix-7169-deprecated-string-interpolation b/changelog/fix-7169-deprecated-string-interpolation deleted file mode 100644 index b9d7b5b8ea0..00000000000 --- a/changelog/fix-7169-deprecated-string-interpolation +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Fix deprecated string interpolation of ${var} with {$var} - - diff --git a/changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel b/changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel deleted file mode 100644 index dd67308e7cf..00000000000 --- a/changelog/fix-changing-stripe-billing-payment-method-doesnt-cancel +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Ensure the Stripe Billing subscription is cancelled when the subscription is changed from WooPayments to another payment method. diff --git a/changelog/fix-deprecation-warning-on-blocks-checkout b/changelog/fix-deprecation-warning-on-blocks-checkout deleted file mode 100644 index ae1241fc85a..00000000000 --- a/changelog/fix-deprecation-warning-on-blocks-checkout +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix deprecation warnings on blocks checkout. diff --git a/changelog/fix-dispute-staged-evidence-notice-typo b/changelog/fix-dispute-staged-evidence-notice-typo deleted file mode 100644 index f0337ebd600..00000000000 --- a/changelog/fix-dispute-staged-evidence-notice-typo +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Behind feature flag: fix for typo in staged dispute challenge notice. - - diff --git a/changelog/fix-docs-links-part-2 b/changelog/fix-docs-links-part-2 deleted file mode 100644 index 6f30cc936c2..00000000000 --- a/changelog/fix-docs-links-part-2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update outdated public documentation links on WooCommerce.com diff --git a/changelog/fix-enable-tracking-on-connected-accounts b/changelog/fix-enable-tracking-on-connected-accounts deleted file mode 100644 index 99395934864..00000000000 --- a/changelog/fix-enable-tracking-on-connected-accounts +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Fix Tracks conditions diff --git a/changelog/fix-experiment-v3 b/changelog/fix-experiment-v3 deleted file mode 100644 index f6ffe9482b6..00000000000 --- a/changelog/fix-experiment-v3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update the name of the A/B experiment on new onboarding. diff --git a/changelog/fix-express-checkouts-links-consistency b/changelog/fix-express-checkouts-links-consistency deleted file mode 100644 index bf3381062c2..00000000000 --- a/changelog/fix-express-checkouts-links-consistency +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -express checkout links UI consistency & area increase diff --git a/changelog/fix-improve-transaction-details-redirect b/changelog/fix-improve-transaction-details-redirect deleted file mode 100644 index d61f18b2f6c..00000000000 --- a/changelog/fix-improve-transaction-details-redirect +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Improve the transaction details redirect user-experience by using client-side routing. diff --git a/changelog/fix-init-woopay-error b/changelog/fix-init-woopay-error deleted file mode 100644 index 58cbd587cec..00000000000 --- a/changelog/fix-init-woopay-error +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix init WooPay and empty cart error diff --git a/changelog/fix-invalid-currency-from-store-api-request b/changelog/fix-invalid-currency-from-store-api-request deleted file mode 100644 index 38f9ee6bc4b..00000000000 --- a/changelog/fix-invalid-currency-from-store-api-request +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix WooPay Session Handler in Store API requests. diff --git a/changelog/fix-migrate-subscription-tokens b/changelog/fix-migrate-subscription-tokens deleted file mode 100644 index cf7f11b62ce..00000000000 --- a/changelog/fix-migrate-subscription-tokens +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This migration code is unreleased and this change isn't noteworthy. - - diff --git a/changelog/fix-payment-intent-meta-for-recurring-transactions b/changelog/fix-payment-intent-meta-for-recurring-transactions deleted file mode 100644 index b5e9efeeab0..00000000000 --- a/changelog/fix-payment-intent-meta-for-recurring-transactions +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Fix payment context and subscription payment metadata stored on subscription recurring transactions. diff --git a/changelog/fix-remove-unused-import-noticeoutlineicon b/changelog/fix-remove-unused-import-noticeoutlineicon deleted file mode 100644 index 07e246aa8fc..00000000000 --- a/changelog/fix-remove-unused-import-noticeoutlineicon +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: **N/A** this is a fix for a code linting issue - - diff --git a/changelog/fix-request-constant-traversing b/changelog/fix-request-constant-traversing deleted file mode 100644 index 0449e00a842..00000000000 --- a/changelog/fix-request-constant-traversing +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix the way request params are loaded between parent and child classes. diff --git a/changelog/fix-title-task-continue-onboarding b/changelog/fix-title-task-continue-onboarding deleted file mode 100644 index 84241736c04..00000000000 --- a/changelog/fix-title-task-continue-onboarding +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Modify title in task to continue with onboarding diff --git a/changelog/fix-woopay-appearance-width b/changelog/fix-woopay-appearance-width deleted file mode 100644 index 5a11ed32a87..00000000000 --- a/changelog/fix-woopay-appearance-width +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -fix checkout appearance width diff --git a/changelog/imp-7052-migrate-to-ts b/changelog/imp-7052-migrate-to-ts deleted file mode 100644 index 1f1fa0afad0..00000000000 --- a/changelog/imp-7052-migrate-to-ts +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Migrate link-item.js to typescript diff --git a/changelog/imp-7052-migrate-to-ts-woopay b/changelog/imp-7052-migrate-to-ts-woopay deleted file mode 100644 index f866f25573f..00000000000 --- a/changelog/imp-7052-migrate-to-ts-woopay +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Migrate woopay-item to typescript diff --git a/changelog/issue-6526-schedule-subscription-migration-tool b/changelog/issue-6526-schedule-subscription-migration-tool deleted file mode 100644 index 391c0a20ddd..00000000000 --- a/changelog/issue-6526-schedule-subscription-migration-tool +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This PR is part of a larger feature coming to WCPay and not single entry is needed for this PR. - - diff --git a/changelog/issue-7038-retry-migration b/changelog/issue-7038-retry-migration deleted file mode 100644 index 62a83cfe6ef..00000000000 --- a/changelog/issue-7038-retry-migration +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: No changelog added. This feature is part of a bigger migration feature coming to WCPay - - diff --git a/changelog/rpp-6679-factor-flags b/changelog/rpp-6679-factor-flags deleted file mode 100644 index 60d7f3c7a0a..00000000000 --- a/changelog/rpp-6679-factor-flags +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Adding factor flags to control when to enter the new payment process. diff --git a/changelog/rpp-6684-request-class b/changelog/rpp-6684-request-class deleted file mode 100644 index 76f23856692..00000000000 --- a/changelog/rpp-6684-request-class +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add payment request class for loading, sanitizing, and escaping data (reengineering payment process) diff --git a/changelog/rpp-6685-load-payment-methods b/changelog/rpp-6685-load-payment-methods deleted file mode 100644 index 82d45e02d4c..00000000000 --- a/changelog/rpp-6685-load-payment-methods +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Load payment methods through the request class (re-engineering payment process). diff --git a/changelog/stripe-billing-migration-notices b/changelog/stripe-billing-migration-notices deleted file mode 100644 index 55bfc08a088..00000000000 --- a/changelog/stripe-billing-migration-notices +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. diff --git a/changelog/stripe-billing-setting b/changelog/stripe-billing-setting deleted file mode 100644 index 8feb7c76c0f..00000000000 --- a/changelog/stripe-billing-setting +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Introduce a new setting that enables store to opt into Subscription off-site billing via Stripe Billing. diff --git a/changelog/subscriptions-core-6.2.0-1 b/changelog/subscriptions-core-6.2.0-1 deleted file mode 100644 index 2aa534e189f..00000000000 --- a/changelog/subscriptions-core-6.2.0-1 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Ensure the shipping phone number field is copied to subscriptions and their orders when copying address meta. diff --git a/changelog/subscriptions-core-6.2.0-2 b/changelog/subscriptions-core-6.2.0-2 deleted file mode 100644 index 1d4cd07d2c0..00000000000 --- a/changelog/subscriptions-core-6.2.0-2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache to improve performance. diff --git a/changelog/subscriptions-core-6.2.0-3 b/changelog/subscriptions-core-6.2.0-3 deleted file mode 100644 index 0244ee6dbd1..00000000000 --- a/changelog/subscriptions-core-6.2.0-3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Deprecated the 'woocommerce_subscriptions_not_found_label' filter. diff --git a/changelog/subscriptions-core-6.2.0-4 b/changelog/subscriptions-core-6.2.0-4 deleted file mode 100644 index 9ad82198e89..00000000000 --- a/changelog/subscriptions-core-6.2.0-4 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Updated subscriptions-core to 6.2.0 diff --git a/changelog/temporarily-disable-saving-sepa b/changelog/temporarily-disable-saving-sepa deleted file mode 100644 index 53e329f4089..00000000000 --- a/changelog/temporarily-disable-saving-sepa +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Temporarily disable saving SEPA diff --git a/changelog/update-6186-mc-settings-links b/changelog/update-6186-mc-settings-links deleted file mode 100644 index 3bff5d53ef6..00000000000 --- a/changelog/update-6186-mc-settings-links +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update Multi-currency documentation links. diff --git a/changelog/update-6378-disable-refund-button-when-disputed b/changelog/update-6378-disable-refund-button-when-disputed deleted file mode 100644 index 6f9f041c7d6..00000000000 --- a/changelog/update-6378-disable-refund-button-when-disputed +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Disable refund button on order edit page when there is active or lost dispute. diff --git a/changelog/update-6991-table-tooltip b/changelog/update-6991-table-tooltip deleted file mode 100644 index 9c50fe720f6..00000000000 --- a/changelog/update-6991-table-tooltip +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update Tooltip component on ConvertedAmount. diff --git a/changelog/update-7048-inline-notice-component b/changelog/update-7048-inline-notice-component deleted file mode 100644 index debb19e3fe5..00000000000 --- a/changelog/update-7048-inline-notice-component +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: update -Comment: Mainly code refactor and component updates without noticiable UI changes. - - diff --git a/changelog/update-7098-onboarding-components b/changelog/update-7098-onboarding-components deleted file mode 100644 index 3ecd11636ad..00000000000 --- a/changelog/update-7098-onboarding-components +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: update -Comment: Ensure new onboarding components (behind an experiment) use admin color schema. - - diff --git a/changelog/update-base-constant-return-same-object-static-call b/changelog/update-base-constant-return-same-object-static-call deleted file mode 100644 index d15f76ec43e..00000000000 --- a/changelog/update-base-constant-return-same-object-static-call +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Update Base_Constant to return the singleton object for same static calls. diff --git a/changelog/update-horizontal-list-label-style-uppercase b/changelog/update-horizontal-list-label-style-uppercase deleted file mode 100644 index b7a71d2c30c..00000000000 --- a/changelog/update-horizontal-list-label-style-uppercase +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: update -Comment: No changelog required: a particularly insignificant UI change. - - diff --git a/changelog/update-stripe-billing-notice-links b/changelog/update-stripe-billing-notice-links deleted file mode 100644 index 8f2ad33678b..00000000000 --- a/changelog/update-stripe-billing-notice-links +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: No changelog is needed given these notices are unreleased. - - diff --git a/changelog/update-subscription-next-payment-on-migrate b/changelog/update-subscription-next-payment-on-migrate deleted file mode 100644 index 54e50bddf0a..00000000000 --- a/changelog/update-subscription-next-payment-on-migrate +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: The migrator is unreleased code and improvements to it pre-release don't need a changelog entry. - - diff --git a/changelog/verify_payment_token_on_migration b/changelog/verify_payment_token_on_migration deleted file mode 100644 index 87074355bf7..00000000000 --- a/changelog/verify_payment_token_on_migration +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This PR comes as part of the migration script which hasn't been released yet. No changelog needed - - diff --git a/package-lock.json b/package-lock.json index fcc0dc2f024..ac730941806 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "woocommerce-payments", - "version": "6.4.2", + "version": "6.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "woocommerce-payments", - "version": "6.4.2", + "version": "6.5.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index b8126a88d40..ff61db2c8d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-payments", - "version": "6.4.2", + "version": "6.5.0", "main": "webpack.config.js", "author": "Automattic", "license": "GPL-3.0-or-later", diff --git a/readme.txt b/readme.txt index 290e56155d9..0566ad83efa 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: payment gateway, payment, apple pay, credit card, google pay, woocommerce Requires at least: 6.0 Tested up to: 6.2 Requires PHP: 7.3 -Stable tag: 6.4.2 +Stable tag: 6.5.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -94,6 +94,68 @@ Please note that our support for the checkout block is still experimental and th == Changelog == += 6.5.0 - 2023-09-20 = +* Add - Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. +* Add - Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. +* Add - Added additional meta data to payment requests +* Add - Add onboarding task incentive badge. +* Add - Add payment request class for loading, sanitizing, and escaping data (reengineering payment process) +* Add - Add the express button on the pay for order page +* Add - add WooPay checkout appearance documentation link +* Add - Fall back to site logo when a custom WooPay logo has not been defined +* Add - Introduce a new setting that enables store to opt into Subscription off-site billing via Stripe Billing. +* Add - Load payment methods through the request class (re-engineering payment process). +* Add - Record the source (Woo Subscriptions or WCPay Subscriptions) when a Stripe Billing subscription is created. +* Add - Record the subscriptions environment context in transaction meta when Stripe Billing payments are handled. +* Add - Redirect back to the pay-for-order page when it is pay-for-order order +* Add - Support kanji and kana statement descriptors for Japanese merchants +* Add - Warn about dev mode enabled on new onboarding flow choice +* Fix - Allow request classes to be extended more than once. +* Fix - Avoid empty fields in new onboarding flow +* Fix - Corrected an issue causing incorrect responses at the cancel authorization API endpoint. +* Fix - Ensure the shipping phone number field is copied to subscriptions and their orders when copying address meta. +* Fix - Ensure the Stripe Billing subscription is cancelled when the subscription is changed from WooPayments to another payment method. +* Fix - express checkout links UI consistency & area increase +* Fix - fix checkout appearance width +* Fix - Fix Currency Switcher Block flag rendering on Windows platform. +* Fix - Fix deprecation warnings on blocks checkout. +* Fix - Fix double indicators showing under Payments tab +* Fix - Fixes the currency formatting for AED and SAR currencies. +* Fix - Fix init WooPay and empty cart error +* Fix - Fix Multi-currency exchange rate date format when using custom date or time settings. +* Fix - Fix Multicurrency widget error on post/page edit screen +* Fix - Fix single currency manual rate save producing error when no changes are made +* Fix - Fix the way request params are loaded between parent and child classes. +* Fix - Fix WooPay Session Handler in Store API requests. +* Fix - Increase admin enqueue scripts priority to avoid compatibility issues with WooCommerce Beta Tester plugin. +* Fix - Modify title in task to continue with onboarding +* Fix - Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches. +* Fix - Update inbox note logic to prevent prompt to set up payment methods from showing on not fully onboarded account. +* Update - Add notice for legacy UPE users about deferred UPE upcoming, and adjust wording for non-UPE users +* Update - Disable refund button on order edit page when there is active or lost dispute. +* Update - Enhanced Analytics SQL, added unit test for has_multi_currency_orders(). Improved code quality and test coverage. +* Update - Improved `get_all_customer_currencies` method to retrieve existing order currencies faster. +* Update - Improve the transaction details redirect user-experience by using client-side routing. +* Update - Temporarily disable saving SEPA +* Update - Update Multi-currency documentation links. +* Update - Update outdated public documentation links on WooCommerce.com +* Update - Update Tooltip component on ConvertedAmount. +* Update - When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache to improve performance. +* Dev - Adding factor flags to control when to enter the new payment process. +* Dev - Adding issuer evidence to dispute details. Hidden behind a feature flag +* Dev - Comment: Update GH workflows to use PHP version from plugin file. +* Dev - Comment: Update occurence of all ubuntu versions to ubuntu-latest +* Dev - Deprecated the 'woocommerce_subscriptions_not_found_label' filter. +* Dev - Fix payment context and subscription payment metadata stored on subscription recurring transactions. +* Dev - Fix Tracks conditions +* Dev - Migrate DetailsLink component to TypeScript to improve code quality +* Dev - Migrate link-item.js to typescript +* Dev - Migrate woopay-item to typescript +* Dev - Remove reference to old experiment. +* Dev - Update Base_Constant to return the singleton object for same static calls. +* Dev - Updated subscriptions-core to 6.2.0 +* Dev - Update the name of the A/B experiment on new onboarding. + = 6.4.2 - 2023-09-14 = * Fix - Fix an error in the checkout when Afterpay is selected as payment method. diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 7893ad0996a..f2614acdbcb 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -12,7 +12,7 @@ * WC tested up to: 7.8.0 * Requires at least: 6.0 * Requires PHP: 7.3 - * Version: 6.4.2 + * Version: 6.5.0 * * @package WooCommerce\Payments */ From 37d30175f2adc24a411fc461a7d445b50ddbe39f Mon Sep 17 00:00:00 2001 From: James Allan Date: Mon, 18 Sep 2023 09:39:59 +1000 Subject: [PATCH 77/84] Fix issues when the Stripe Billing `is_migrating()` function would return false if the one of the actions was actively running (#7227) --- changelog/fix-subscription-migration-in-progress-check | 5 +++++ .../class-wc-payments-subscriptions-migrator.php | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-subscription-migration-in-progress-check diff --git a/changelog/fix-subscription-migration-in-progress-check b/changelog/fix-subscription-migration-in-progress-check new file mode 100644 index 00000000000..650e1b1d627 --- /dev/null +++ b/changelog/fix-subscription-migration-in-progress-check @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This change fixes a bug in unreleased code. No changelog entry needed. + + diff --git a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php index 5165653bd39..d8fa2b7738a 100644 --- a/includes/subscriptions/class-wc-payments-subscriptions-migrator.php +++ b/includes/subscriptions/class-wc-payments-subscriptions-migrator.php @@ -764,13 +764,13 @@ public function get_stripe_billing_subscription_count() { /** * Determines if a migration is currently in progress. * - * A migration is considered to be in progress if either the initial migration action or an individual subscription - * actions are scheduled. + * A migration is considered to be in progress if the initial migration action or an individual subscription + * action (or retry) is scheduled. * * @return bool True if a migration is in progress, false otherwise. */ public function is_migrating() { - return is_numeric( as_next_scheduled_action( $this->scheduled_hook ) ) || is_numeric( as_next_scheduled_action( $this->migrate_hook ) ) || is_numeric( as_next_scheduled_action( $this->migrate_hook . '_retry' ) ); + return (bool) as_next_scheduled_action( $this->scheduled_hook ) || (bool) as_next_scheduled_action( $this->migrate_hook ) || (bool) as_next_scheduled_action( $this->migrate_hook . '_retry' ); } /** From e4f764fda52c769de317d74d690b0acd60494a63 Mon Sep 17 00:00:00 2001 From: James Allan Date: Mon, 18 Sep 2023 09:42:06 +1000 Subject: [PATCH 78/84] Remove changelog file --- changelog/fix-subscription-migration-in-progress-check | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 changelog/fix-subscription-migration-in-progress-check diff --git a/changelog/fix-subscription-migration-in-progress-check b/changelog/fix-subscription-migration-in-progress-check deleted file mode 100644 index 650e1b1d627..00000000000 --- a/changelog/fix-subscription-migration-in-progress-check +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This change fixes a bug in unreleased code. No changelog entry needed. - - From 724b940b9d5ed82d2a2df0205fbe8317b537b8c8 Mon Sep 17 00:00:00 2001 From: botwoo Date: Mon, 18 Sep 2023 18:59:10 +0000 Subject: [PATCH 79/84] Amend changelog entries for release 6.5.0 --- changelog.txt | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0750efdbacd..74b42c5f5d3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ *** WooPayments Changelog *** -= 6.5.0 - 2023-09-20 = += 6.5.0 - 2023-09-21 = * Add - Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. * Add - Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. * Add - Added additional meta data to payment requests diff --git a/readme.txt b/readme.txt index 0566ad83efa..ba714bcc074 100644 --- a/readme.txt +++ b/readme.txt @@ -94,7 +94,7 @@ Please note that our support for the checkout block is still experimental and th == Changelog == -= 6.5.0 - 2023-09-20 = += 6.5.0 - 2023-09-21 = * Add - Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. * Add - Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. * Add - Added additional meta data to payment requests From 7f31adccf22510bb1a5eac8af8dd44f16c002e42 Mon Sep 17 00:00:00 2001 From: Jesse Pearson Date: Mon, 18 Sep 2023 11:52:13 -0300 Subject: [PATCH 80/84] Disable automatic currency switching and switcher widgets on pay_for_order page (#7152) Co-authored-by: Taha Paksu <3295+tpaksu@users.noreply.github.com> --- ...disable-currency-switcher-on-pay-for-order | 4 ++ includes/multi-currency/Compatibility.php | 33 ++++++++++++++++- .../WooCommerceSubscriptions.php | 6 +-- .../multi-currency/CurrencySwitcherBlock.php | 10 ++++- .../multi-currency/CurrencySwitcherWidget.php | 2 +- includes/multi-currency/MultiCurrency.php | 4 +- .../test-class-woocommerce-subscriptions.php | 22 +++++------ .../test-class-compatibility.php | 27 ++++++++++++++ .../test-class-currency-switcher-block.php | 37 ++++++++++++++++++- .../test-class-currency-switcher-widget.php | 16 ++++---- .../test-class-multi-currency.php | 37 +++++++++++++++++++ 11 files changed, 167 insertions(+), 31 deletions(-) create mode 100644 changelog/fix-5031-disable-currency-switcher-on-pay-for-order diff --git a/changelog/fix-5031-disable-currency-switcher-on-pay-for-order b/changelog/fix-5031-disable-currency-switcher-on-pay-for-order new file mode 100644 index 00000000000..1a8faa30591 --- /dev/null +++ b/changelog/fix-5031-disable-currency-switcher-on-pay-for-order @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Disable automatic currency switching and switcher widgets on pay_for_order page. diff --git a/includes/multi-currency/Compatibility.php b/includes/multi-currency/Compatibility.php index 31ecbfcb0f9..c88fc94cdcd 100644 --- a/includes/multi-currency/Compatibility.php +++ b/includes/multi-currency/Compatibility.php @@ -87,12 +87,41 @@ public function override_selected_currency() { } /** - * Checks to see if the widgets should be hidden. + * Deprecated method, please use should_disable_currency_switching. * * @return bool False if it shouldn't be hidden, true if it should. */ public function should_hide_widgets(): bool { - return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', false ); + wc_deprecated_function( __FUNCTION__, '6.5.0', 'Compatibility::should_disable_currency_switching' ); + return $this->should_disable_currency_switching(); + } + + /** + * Checks to see if currency switching should be disabled, such as the widgets and the automatic geolocation switching. + * + * @return bool False if no, true if yes. + */ + public function should_disable_currency_switching(): bool { + $return = false; + + /** + * If the pay_for_order parameter is set, we disable currency switching. + * + * WooCommerce itself handles all the heavy lifting and verification on the Order Pay page, we just need to + * make sure the currency switchers are not displayed. This is due to once the order is created, the currency + * itself should remain static. + */ + if ( isset( $_GET['pay_for_order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $return = true; + } + + // If someone has hooked into the deprecated filter, throw a notice and then apply the filtering. + if ( has_action( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets' ) ) { + wc_deprecated_hook( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', '6.5.0', MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching' ); + $return = apply_filters( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', $return ); + } + + return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching', $return ); } /** diff --git a/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php b/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php index e63c7ea54c3..74cc36ecc15 100644 --- a/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php +++ b/includes/multi-currency/Compatibility/WooCommerceSubscriptions.php @@ -51,7 +51,7 @@ protected function init() { add_filter( MultiCurrency::FILTER_PREFIX . 'override_selected_currency', [ $this, 'override_selected_currency' ], 50 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_coupon_amount', [ $this, 'should_convert_coupon_amount' ], 50, 2 ); - add_filter( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', [ $this, 'should_hide_widgets' ], 50 ); + add_filter( MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching', [ $this, 'should_disable_currency_switching' ], 50 ); } } } @@ -254,13 +254,13 @@ public function should_convert_coupon_amount( bool $return, $coupon ): bool { } /** - * Checks to see if the widgets should be hidden. + * Checks to see if currency switching should be disabled. * * @param bool $return Whether widgets should be hidden or not. Default is false. * * @return bool */ - public function should_hide_widgets( bool $return ): bool { + public function should_disable_currency_switching( bool $return ): bool { // If it's already true, return it. if ( $return ) { return $return; diff --git a/includes/multi-currency/CurrencySwitcherBlock.php b/includes/multi-currency/CurrencySwitcherBlock.php index 6caf8bf7515..92a6e7e2ad1 100644 --- a/includes/multi-currency/CurrencySwitcherBlock.php +++ b/includes/multi-currency/CurrencySwitcherBlock.php @@ -117,7 +117,13 @@ public function init_block_widget() { * @return string The content to be displayed inside the block widget. */ public function render_block_widget( $block_attributes, $content ): string { - if ( $this->compatibility->should_hide_widgets() ) { + if ( $this->compatibility->should_disable_currency_switching() ) { + return ''; + } + + $enabled_currencies = $this->multi_currency->get_enabled_currencies(); + + if ( 1 === count( $enabled_currencies ) ) { return ''; } @@ -133,7 +139,7 @@ public function render_block_widget( $block_attributes, $content ): string { $widget_content .= '
'; $widget_content .= '', $result ); $this->assertStringContainsString( '', $result ); } + + /** + * The widget should not be displayed if should_disable_currency_switching returns true. + */ + public function test_widget_does_not_render_on_hide() { + // Arrange: Set the expected call and return value for should_disable_currency_switching. + $this->mock_compatibility + ->expects( $this->once() ) + ->method( 'should_disable_currency_switching' ) + ->willReturn( true ); + + // Act/Assert: Confirm that when calling the renger method nothing is returned. + $this->assertSame( '', $this->currency_switcher_block->render_block_widget( [], '' ) ); + } + + /** + * The widget should not be displayed if there's only a single currency enabled. + */ + public function test_widget_does_not_render_on_single_currency() { + // Arrange: Set the expected call and return values for should_disable_currency_switching and get_enabled_currencies. + $this->mock_compatibility + ->expects( $this->once() ) + ->method( 'should_disable_currency_switching' ) + ->willReturn( false ); + + $this->mock_multi_currency + ->expects( $this->once() ) + ->method( 'get_enabled_currencies' ) + ->willReturn( [ new Currency( 'USD' ) ] ); + + // Act/Assert: Confirm that when calling the renger method nothing is returned. + $this->assertSame( '', $this->currency_switcher_block->render_block_widget( [], '' ) ); + } } diff --git a/tests/unit/multi-currency/test-class-currency-switcher-widget.php b/tests/unit/multi-currency/test-class-currency-switcher-widget.php index 62afd3cce34..7f6f88e299c 100644 --- a/tests/unit/multi-currency/test-class-currency-switcher-widget.php +++ b/tests/unit/multi-currency/test-class-currency-switcher-widget.php @@ -55,7 +55,7 @@ public function set_up() { } public function test_widget_renders_title_with_args() { - $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); + $this->mock_compatibility->method( 'should_disable_currency_switching' )->willReturn( false ); $instance = [ 'title' => 'Test Title', ]; @@ -64,13 +64,13 @@ public function test_widget_renders_title_with_args() { } public function test_widget_renders_enabled_currencies_with_symbol() { - $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); + $this->mock_compatibility->method( 'should_disable_currency_switching' )->willReturn( false ); $this->expectOutputRegex( '/value="USD">$ USD.+value="CAD">$ CAD.+value="EUR">€ EUR.+value="CHF">CHF/s' ); $this->render_widget(); } public function test_widget_renders_enabled_currencies_without_symbol() { - $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); + $this->mock_compatibility->method( 'should_disable_currency_switching' )->willReturn( false ); $instance = [ 'symbol' => 0, ]; @@ -79,7 +79,7 @@ public function test_widget_renders_enabled_currencies_without_symbol() { } public function test_widget_renders_enabled_currencies_with_symbol_and_flag() { - $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); + $this->mock_compatibility->method( 'should_disable_currency_switching' )->willReturn( false ); $instance = [ 'symbol' => 1, 'flag' => 1, @@ -99,20 +99,20 @@ public function test_widget_renders_hidden_input() { } public function test_widget_selects_selected_currency() { - $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); + $this->mock_compatibility->method( 'should_disable_currency_switching' )->willReturn( false ); $this->mock_multi_currency->method( 'get_selected_currency' )->willReturn( new Currency( 'CAD' ) ); $this->expectOutputRegex( '/