diff --git a/changelog.txt b/changelog.txt index df7c73b6fa1..5c506b70c8d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,54 @@ *** WooPayments Changelog *** += 6.9.0 - 2023-12-06 = +* Add - Added cleanup code after Payment Processing - RPP. +* Add - Adds new option to track dismissal of PO eligibility modal. +* Add - Display an error banner on the connect page when the WooCommerce country is not supported. +* Add - Filter to disable WooPay checkout auto-redirect and email input hooks. +* Add - Handle failed transaction rate limiter in RPP. +* Add - Handle fraud prevention service in InitialState (project RPP). +* Add - Handle mimium amount in InitialState (project RPP). +* Add - Introduce filters for channel, customer country, and risk level on the transactions list page. +* Add - Store the working mode of the gateway (RPP). +* Fix - Add AutomateWoo - Refer A Friend Add-On support on WooPay. +* Fix - Add date_between filter for Authorization Reporting API. +* Fix - Add invalid product id error check. +* Fix - Allow Gradual signup accounts to continue with the Gradual KYC after abandoning it. +* Fix - Allow requests with item IDs to be extended without exceptions. +* Fix - Check that the email is set in the post global. +* Fix - Display notice when clicking the WooPay button if variable product selection is incomplete. +* Fix - Do not show the WooPay button on the product page when WC Bookings require confirmation. +* Fix - Enable deferred intent creation when initialization process encounters cache unavailability. +* Fix - Ensure express payment methods (Google and Apple Pay) correctly reflect eligible shipping methods after closing and reattempting payment. +* Fix - Fixes a redirect to show the new onboarding when coming from WC Core. +* Fix - Fix saved card payments not working on block checkout while card testing prevention is active. +* Fix - Pass the pay-for-order params to the first-party auth flow. +* Fix - Prevent merchants to access onboarding again after starting it in new flow. +* Fix - Remove unsupported EUR currency from Afterpay payment method. +* Fix - Show Payments menu sub-items only for merchants that completed KYC. +* Fix - Support 'variation' product type when re-adding items to a cart. +* Fix - When rendering customer reference in transaction details, fallback to order data. +* Fix - When rendering customer reference on transaction details page, handle case with name being not provided in the order. +* Update - Change PRB default height for new installations. +* Update - Cleanup the deprecated payment gateway processing - part I. +* Update - Correct some links that now lead to better documentation. +* Update - Enable the new onboarding flow as default for all users. +* Update - Exclude estimated deposits from the deposits list screen. +* Update - Improvements to the dev mode and test mode indicators. +* Update - Remove estimated status option from the advanced filters on the deposits list screen. +* Update - Replace the deposit overview transactions list with a "transaction history is unavailable for instant deposits" message. +* Update - Update Payments Overview deposits UI to simplify how we communicate upcoming deposits. +* Update - Update to the new onboarding builder flow to not prefill country/address to US. +* Dev - Add client user-agent value to Tracks event props. +* Dev - Add E2E tests for Affirm and Afterpay checkouts. +* Dev - Add E2E tests for checking out with Giropay. +* Dev - Added customer details management within the re-engineered payment process. +* Dev - Adds WCPay options to Woo Core option allow list to avoid 403 responses from Options API when getting and updating options in non-prod env. +* Dev - Bump WC tested up to version to 8.3.1. +* Dev - Fix a bug in WooPay button update Tracks. +* Dev - Introduce filter `wcpay_payment_request_is_cart_supported`. Allow plugins to conditionally disable payment request buttons on cart and checkout pages containing products that do not support them. +* Dev - Upgrade the csv-export JS package to the latest version. + = 6.8.0 - 2023-11-16 = * Add - Added mechanism to track and log changes to the payment context (reengineering payment process) * Add - Add rejected payment method capability status diff --git a/client/capital/index.tsx b/client/capital/index.tsx index a53b18b02ef..8d1ab3f4741 100644 --- a/client/capital/index.tsx +++ b/client/capital/index.tsx @@ -12,7 +12,7 @@ import { dateI18n } from '@wordpress/date'; * Internal dependencies. */ import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import ErrorBoundary from 'components/error-boundary'; import ActiveLoanSummary from 'components/active-loan-summary'; import { formatExplicitCurrency, isZeroDecimalCurrency } from 'utils/currency'; @@ -21,7 +21,6 @@ import ClickableCell from 'components/clickable-cell'; import Chip from 'components/chip'; import { useLoans } from 'wcpay/data'; import { getAdminUrl } from 'wcpay/utils'; - import './style.scss'; const columns = [ @@ -205,7 +204,8 @@ const CapitalPage = (): JSX.Element => { return ( - + + { wcpaySettings.accountLoans.has_active_loan && ( diff --git a/client/checkout/blocks/fields.js b/client/checkout/blocks/fields.js index 67cd5ad2a3d..c89c34888bc 100644 --- a/client/checkout/blocks/fields.js +++ b/client/checkout/blocks/fields.js @@ -23,7 +23,7 @@ const WCPayFields = ( { stripe, elements, billing: { billingData }, - eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, shouldSavePayment, } ) => { @@ -49,7 +49,7 @@ const WCPayFields = ( { // When it's time to process the payment, generate a Stripe payment method object. useEffect( () => - onPaymentProcessing( () => { + onPaymentSetup( () => { if ( PAYMENT_METHOD_NAME_CARD !== activePaymentMethod ) { return; } diff --git a/client/checkout/blocks/generate-payment-method.js b/client/checkout/blocks/generate-payment-method.js index 01e55a4f267..61e9e74c42c 100644 --- a/client/checkout/blocks/generate-payment-method.js +++ b/client/checkout/blocks/generate-payment-method.js @@ -11,7 +11,7 @@ import { PAYMENT_METHOD_NAME_CARD } from '../constants.js'; * @param {Object} billingData The billing data, which was collected from the checkout block. * @param {string} fingerprint User fingerprint. * - * @return {Object} The `onPaymentProcessing` response object, including a type and meta data/error message. + * @return {Object} The `onPaymentSetup` response object, including a type and meta data/error message. */ const generatePaymentMethod = async ( api, diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 6b506531107..a9cf210c815 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -70,6 +70,7 @@ registerPaymentMethod( { if ( getConfig( 'isWooPayEnabled' ) ) { if ( document.querySelector( '[data-block-name="woocommerce/checkout"]' ) && + getConfig( 'isWooPayEmailInputEnabled' ) && ! isPreviewing() ) { handleWooPayEmailInput( '#email', api, true ); diff --git a/client/checkout/blocks/saved-token-handler.js b/client/checkout/blocks/saved-token-handler.js index 2ec311c8d5e..0f8d6e42d46 100644 --- a/client/checkout/blocks/saved-token-handler.js +++ b/client/checkout/blocks/saved-token-handler.js @@ -1,15 +1,41 @@ /** * Internal dependencies */ +import { useEffect } from 'react'; import { usePaymentCompleteHandler } from './hooks'; +import { useSelect } from '@wordpress/data'; export const SavedTokenHandler = ( { api, stripe, elements, - eventRegistration: { onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, } ) => { + const paymentMethodData = useSelect( ( select ) => { + const store = select( 'wc/store/payment' ); + return store.getPaymentMethodData(); + } ); + + useEffect( () => { + return onPaymentSetup( () => { + const fraudPreventionToken = document + .querySelector( '#wcpay-fraud-prevention-token' ) + ?.getAttribute( 'value' ); + + return { + type: 'success', + meta: { + paymentMethodData: { + ...paymentMethodData, + 'wcpay-fraud-prevention-token': + fraudPreventionToken ?? '', + }, + }, + }; + } ); + }, [ onPaymentSetup, paymentMethodData ] ); + // Once the server has completed payment processing, confirm the intent of necessary. usePaymentCompleteHandler( api, diff --git a/client/checkout/blocks/upe-fields.js b/client/checkout/blocks/upe-fields.js index 9f1ea7d67ee..2f8c06a7ccf 100644 --- a/client/checkout/blocks/upe-fields.js +++ b/client/checkout/blocks/upe-fields.js @@ -41,7 +41,7 @@ const WCPayUPEFields = ( { activePaymentMethod, billing: { billingData }, shippingData, - eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, paymentIntentId, paymentIntentSecret, @@ -135,7 +135,7 @@ const WCPayUPEFields = ( { // When it's time to process the payment, generate a Stripe payment method object. useEffect( () => - onPaymentProcessing( () => { + onPaymentSetup( () => { if ( PAYMENT_METHOD_NAME_CARD !== activePaymentMethod ) { return; } diff --git a/client/checkout/blocks/upe-split-fields.js b/client/checkout/blocks/upe-split-fields.js index 07b9f720da1..ff75fd8ea81 100644 --- a/client/checkout/blocks/upe-split-fields.js +++ b/client/checkout/blocks/upe-split-fields.js @@ -45,7 +45,7 @@ const WCPayUPEFields = ( { testingInstructions, billing: { billingData }, shippingData, - eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, paymentMethodId, upeMethods, @@ -133,7 +133,7 @@ const WCPayUPEFields = ( { // When it's time to process the payment, generate a Stripe payment method object. useEffect( () => - onPaymentProcessing( () => { + onPaymentSetup( () => { if ( upeMethods[ paymentMethodId ] !== activePaymentMethod ) { return; } diff --git a/client/checkout/classic/index.js b/client/checkout/classic/index.js index f91fba53a72..2a421453337 100644 --- a/client/checkout/classic/index.js +++ b/client/checkout/classic/index.js @@ -551,7 +551,11 @@ jQuery( function ( $ ) { } } ); - if ( getConfig( 'isWooPayEnabled' ) && ! isPreviewing() ) { + if ( + getConfig( 'isWooPayEnabled' ) && + getConfig( 'isWooPayEmailInputEnabled' ) && + ! isPreviewing() + ) { handleWooPayEmailInput( '#billing_email', api ); } } ); diff --git a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js index 2738fa6537c..0f5c833e2c1 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js +++ b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js @@ -107,7 +107,11 @@ jQuery( function ( $ ) { return processPaymentIfNotUsingSavedMethod( $( 'form#order_review' ) ); } ); - if ( getUPEConfig( 'isWooPayEnabled' ) && ! isPreviewing() ) { + if ( + getUPEConfig( 'isWooPayEnabled' ) && + getUPEConfig( 'isWooPayEmailInputEnabled' ) && + ! isPreviewing() + ) { handleWooPayEmailInput( '#billing_email', api ); } diff --git a/client/checkout/woopay/express-button/index.js b/client/checkout/woopay/express-button/index.js index bd2aeff02c2..6058d71a8f4 100644 --- a/client/checkout/woopay/express-button/index.js +++ b/client/checkout/woopay/express-button/index.js @@ -1,4 +1,3 @@ -/* global jQuery */ /** * External dependencies */ @@ -58,16 +57,16 @@ const renderWooPayExpressCheckoutButtonWithCallbacks = () => { renderWooPayExpressCheckoutButton( listenForCartChanges ); }; -jQuery( ( $ ) => { +document.addEventListener( 'DOMContentLoaded', () => { listenForCartChanges = { start: () => { - $( document.body ).on( + document.body.addEventListener( 'updated_cart_totals', renderWooPayExpressCheckoutButtonWithCallbacks ); }, stop: () => { - $( document.body ).off( + document.body.removeEventListener( 'updated_cart_totals', renderWooPayExpressCheckoutButtonWithCallbacks ); diff --git a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js index f37f92efac2..5fd5b1d3235 100644 --- a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js +++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js @@ -38,9 +38,6 @@ jest.mock( '../use-express-checkout-product-handler', () => jest.fn() ); jest.spyOn( window, 'alert' ).mockImplementation( () => {} ); global.fetch = jest.fn( () => Promise.resolve( { json: () => ( {} ) } ) ); -global.window.wc_add_to_cart_variation_params = { - i18n_make_a_selection_text: 'Mock text', -}; describe( 'WoopayExpressCheckoutButton', () => { const buttonSettings = { @@ -62,7 +59,6 @@ describe( 'WoopayExpressCheckoutButton', () => { }; useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, - isAddToCartDisabled: false, } ) ); } ); @@ -214,15 +210,21 @@ describe( 'WoopayExpressCheckoutButton', () => { } ); } ); - test( 'should shown an alert when clicking the button when add to cart button is disabled', () => { + test( 'should show an alert when clicking the button when add to cart button is disabled', () => { getConfig.mockImplementation( ( v ) => { return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo'; } ); useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, - isAddToCartDisabled: true, } ) ); + // Add a disabled add to cart button to the DOM. + const addToCartButton = document.createElement( 'button' ); + addToCartButton.classList.add( 'single_add_to_cart_button' ); + addToCartButton.classList.add( 'disabled' ); + addToCartButton.classList.add( 'wc-variation-selection-needed' ); + document.body.appendChild( addToCartButton ); + render( { userEvent.click( expressButton ); expect( window.alert ).toBeCalledWith( - window.wc_add_to_cart_variation_params - .i18n_make_a_selection_text + 'Please select your product options before proceeding.' ); + + document.body.removeChild( addToCartButton ); } ); test( 'call `addToCart` and `expressCheckoutIframe` on express button click on product page', async () => { @@ -252,7 +255,6 @@ describe( 'WoopayExpressCheckoutButton', () => { useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, getProductData: jest.fn().mockReturnValue( {} ), - isAddToCartDisabled: false, } ) ); render( { useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, getProductData: jest.fn().mockReturnValue( false ), - isAddToCartDisabled: false, } ) ); render( { - const [ isAddToCartDisabled, setIsAddToCartDisabled ] = useState( false ); - +const useExpressCheckoutProductHandler = ( api ) => { const getAttributes = () => { const select = document .querySelector( '.variations_form' ) @@ -142,95 +139,9 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { return api.expressCheckoutAddToCart( data ); }; - useEffect( () => { - if ( ! isProductPage ) { - return; - } - - const getIsAddToCartDisabled = () => { - const addToCartButton = document.querySelector( - '.single_add_to_cart_button' - ); - - return ( - addToCartButton.disabled || - addToCartButton.classList.contains( 'disabled' ) - ); - }; - - setIsAddToCartDisabled( getIsAddToCartDisabled() ); - - const enableAddToCartButton = () => { - setIsAddToCartDisabled( false ); - }; - - const disableAddToCartButton = () => { - setIsAddToCartDisabled( true ); - }; - - const bundleForm = document.querySelector( '.bundle_form' ); - const mixAndMatchForm = document.querySelector( '.mnm_form' ); - const variationForm = document.querySelector( '.variations_form' ); - - if ( bundleForm ) { - // eslint-disable-next-line no-undef - jQuery( bundleForm ) - .on( 'woocommerce-product-bundle-show', enableAddToCartButton ) - .on( - 'woocommerce-product-bundle-hide', - disableAddToCartButton - ); - } else if ( mixAndMatchForm ) { - // eslint-disable-next-line no-undef - jQuery( mixAndMatchForm ) - .on( - 'wc-mnm-display-add-to-cart-button', - enableAddToCartButton - ) - .on( 'wc-mnm-hide-add-to-cart-button', disableAddToCartButton ); - } else if ( variationForm ) { - // eslint-disable-next-line no-undef - jQuery( variationForm ) - .on( 'show_variation', enableAddToCartButton ) - .on( 'hide_variation', disableAddToCartButton ); - } - - return () => { - if ( bundleForm ) { - // eslint-disable-next-line no-undef - jQuery( bundleForm ) - .off( - 'woocommerce-product-bundle-show', - enableAddToCartButton - ) - .off( - 'woocommerce-product-bundle-hide', - disableAddToCartButton - ); - } else if ( mixAndMatchForm ) { - // eslint-disable-next-line no-undef - jQuery( mixAndMatchForm ) - .off( - 'wc-mnm-display-add-to-cart-button', - enableAddToCartButton - ) - .off( - 'wc-mnm-hide-add-to-cart-button', - disableAddToCartButton - ); - } else if ( variationForm ) { - // eslint-disable-next-line no-undef - jQuery( variationForm ) - .off( 'show_variation', enableAddToCartButton ) - .off( 'hide_variation', disableAddToCartButton ); - } - }; - }, [ isProductPage, setIsAddToCartDisabled ] ); - return { addToCart, getProductData, - isAddToCartDisabled, }; }; diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index d0900eee4c3..a7b26ef4853 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { sprintf, __ } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import React, { useCallback, useEffect, useState, useRef } from 'react'; import classNames from 'classnames'; @@ -17,9 +17,18 @@ import { getConfig } from 'wcpay/utils/checkout'; import request from 'wcpay/checkout/utils/request'; import { showErrorMessage } from 'wcpay/checkout/woopay/express-button/utils'; import { buildAjaxURL } from 'wcpay/payment-request/utils'; +import interpolateComponents from '@automattic/interpolate-components'; +import { appendRedirectionParams } from 'wcpay/checkout/woopay/utils'; const BUTTON_WIDTH_THRESHOLD = 140; +const ButtonTypeTextMap = { + default: __( 'WooPay', 'woocommerce-payments' ), + buy: __( 'Buy with WooPay', 'woocommerce-payments' ), + donate: __( 'Donate with WooPay', 'woocommerce-payments' ), + book: __( 'Book with WooPay', 'woocommerce-payments' ), +}; + export const WoopayExpressCheckoutButton = ( { listenForCartChanges = {}, isPreview = false, @@ -43,21 +52,15 @@ export const WoopayExpressCheckoutButton = ( { buttonWidthTypes.wide ); - const text = - buttonType !== 'default' - ? sprintf( - __( `%s with`, 'woocommerce-payments' ), - buttonType.charAt( 0 ).toUpperCase() + - buttonType.slice( 1 ).toLowerCase() - ) - : ''; + const buttonText = + ButtonTypeTextMap[ buttonType || 'default' ] ?? + ButtonTypeTextMap.default; + const ThemedWooPayIcon = theme === 'dark' ? WoopayIcon : WoopayIconLight; - const { - addToCart, - getProductData, - isAddToCartDisabled, - } = useExpressCheckoutProductHandler( api, isProductPage ); + const { addToCart, getProductData } = useExpressCheckoutProductHandler( + api + ); const getProductDataRef = useRef( getProductData ); const addToCartRef = useRef( addToCart ); @@ -84,14 +87,64 @@ export const WoopayExpressCheckoutButton = ( { } }, [ isPreview, context ] ); - const defaultOnClick = useCallback( ( event ) => { - // This will only be called if user clicks the button too quickly. - // It saves the event for later use. - initialOnClickEventRef.current = event; - // Set isLoadingRef to true to prevent multiple clicks. - isLoadingRef.current = true; - setIsLoading( true ); - }, [] ); + const canAddProductToCart = useCallback( () => { + if ( ! isProductPage ) { + return true; + } + + const addToCartButton = document.querySelector( + '.single_add_to_cart_button' + ); + + if ( + addToCartButton && + ( addToCartButton.disabled || + addToCartButton.classList.contains( 'disabled' ) ) + ) { + if ( + addToCartButton.classList.contains( + 'wc-variation-is-unavailable' + ) + ) { + window.alert( + window?.wc_add_to_cart_variation_params + ?.i18n_unavailable_text || + __( + 'Sorry, this product is unavailable. Please choose a different combination.', + 'woocommerce-payments' + ) + ); + } else { + window.alert( + __( + 'Please select your product options before proceeding.', + 'woocommerce-payments' + ) + ); + } + + return false; + } + + return true; + }, [ isProductPage ] ); + + const defaultOnClick = useCallback( + ( event ) => { + event?.preventDefault(); + + if ( ! canAddProductToCart() ) { + return; + } + // This will only be called if user clicks the button too quickly. + // It saves the event for later use. + initialOnClickEventRef.current = event; + // Set isLoadingRef to true to prevent multiple clicks. + isLoadingRef.current = true; + setIsLoading( true ); + }, + [ canAddProductToCart ] + ); const onClickFallback = useCallback( // OTP flow @@ -109,19 +162,11 @@ export const WoopayExpressCheckoutButton = ( { } ); - if ( isProductPage ) { - if ( isAddToCartDisabled ) { - alert( - window.wc_add_to_cart_variation_params - ?.i18n_make_a_selection_text || - __( - 'Please select all required options to continue.', - 'woocommerce-payments' - ) - ); - return; - } + if ( ! canAddProductToCart() ) { + return; + } + if ( isProductPage ) { const productData = getProductDataRef.current(); if ( ! productData ) { return; @@ -147,9 +192,9 @@ export const WoopayExpressCheckoutButton = ( { api, context, emailSelector, - isAddToCartDisabled, isPreview, isProductPage, + canAddProductToCart, ] ); @@ -196,10 +241,6 @@ export const WoopayExpressCheckoutButton = ( { return; } - // Set isLoadingRef to true to prevent multiple clicks. - isLoadingRef.current = true; - setIsLoading( true ); - wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_BUTTON_CLICK, { @@ -207,6 +248,14 @@ export const WoopayExpressCheckoutButton = ( { } ); + if ( ! canAddProductToCart() ) { + return; + } + + // Set isLoadingRef to true to prevent multiple clicks. + isLoadingRef.current = true; + setIsLoading( true ); + if ( isProductPage ) { const productData = getProductDataRef.current(); @@ -292,6 +341,7 @@ export const WoopayExpressCheckoutButton = ( { isPreview, listenForCartChanges, onClickFallback, + canAddProductToCart, ] ); useEffect( () => { @@ -327,7 +377,9 @@ export const WoopayExpressCheckoutButton = ( { } if ( isSessionDataSuccess ) { - window.location.href = event.data.value.redirect_url; + window.location.href = appendRedirectionParams( + event.data.value.redirect_url + ); } else if ( isSessionDataError ) { onClickFallback( null ); @@ -374,7 +426,7 @@ export const WoopayExpressCheckoutButton = ( { diff --git a/client/components/account-balances/test/index.test.tsx b/client/components/account-balances/test/index.test.tsx index 6d1cf7d6cdf..3a09b9b2004 100644 --- a/client/components/account-balances/test/index.test.tsx +++ b/client/components/account-balances/test/index.test.tsx @@ -138,19 +138,7 @@ const createMockOverview = ( fee_percentage: 0, status: 'paid', }, - nextScheduled: { - id: '456', - type: 'deposit', - amount: 0, - automatic: true, - currency: null, - bankAccount: null, - created: Date.now(), - date: Date.now(), - fee: 0, - fee_percentage: 0, - status: 'estimated', - }, + nextScheduled: undefined, instant: { currency: currencyCode, amount: instantAmount, diff --git a/client/components/banner-notice/index.tsx b/client/components/banner-notice/index.tsx index dcba0e665fb..b7a7c4658f3 100644 --- a/client/components/banner-notice/index.tsx +++ b/client/components/banner-notice/index.tsx @@ -85,6 +85,7 @@ interface Props { * - `label`: `string` containing the text of the button/link * - `url`: `string` OR `onClick`: `( event: SyntheticEvent ) => void` to specify * what the action does. + * - `urlTarget`: `string` (optional) to specify the target attribute of the link. * - `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`. @@ -101,6 +102,7 @@ interface Props { className?: string; variant?: Button.Props[ 'variant' ]; url?: string; + urlTarget?: string; onClick?: React.MouseEventHandler< HTMLAnchorElement >; } >; /** @@ -152,6 +154,7 @@ const BannerNotice: React.FC< Props > = ( { variant, onClick, url, + urlTarget, }, index ) => { @@ -169,6 +172,7 @@ const BannerNotice: React.FC< Props > = ( { variant={ computedVariant } onClick={ url ? undefined : onClick } className={ buttonCustomClasses } + target={ urlTarget } > { label } diff --git a/client/components/banner-notice/style.scss b/client/components/banner-notice/style.scss index 4ab24170454..2348891d528 100644 --- a/client/components/banner-notice/style.scss +++ b/client/components/banner-notice/style.scss @@ -41,7 +41,7 @@ &__actions { display: grid; grid-auto-flow: column; - grid-auto-columns: min-content; + grid-auto-columns: max-content; column-gap: $gap-small; margin-top: $gap-small; } diff --git a/client/components/customer-link/index.tsx b/client/components/customer-link/index.tsx index 449c0376fdd..7421fd8bfca 100644 --- a/client/components/customer-link/index.tsx +++ b/client/components/customer-link/index.tsx @@ -13,20 +13,26 @@ import { getAdminUrl } from 'wcpay/utils'; import { ChargeBillingDetails } from 'wcpay/types/charges'; const CustomerLink = ( props: { - customer: null | ChargeBillingDetails; + billing_details: null | ChargeBillingDetails; + order_details: null | OrderDetails; } ): JSX.Element => { - const customer = props.customer; - if ( customer && customer.name ) { - const searchTerm = customer.email - ? `${ customer.name } (${ customer.email })` - : customer.name; + // Depending on the transaction chanel, charge billing details might be missing, and we have to rely on order for those. + const name = + props.billing_details?.name || + props.order_details?.customer_name || + null; + if ( name ) { + const email = + props.billing_details?.email || + props.order_details?.customer_email || + null; const url = getAdminUrl( { page: 'wc-admin', path: '/payments/transactions', - search: [ searchTerm ], + search: [ email ? `${ name } (${ email })` : name ], } ); - return { customer.name }; + return { name }; } return <>–; diff --git a/client/components/customer-link/test/__snapshots__/index.tsx.snap b/client/components/customer-link/test/__snapshots__/index.tsx.snap index ec0d0d870a9..86027aac8df 100644 --- a/client/components/customer-link/test/__snapshots__/index.tsx.snap +++ b/client/components/customer-link/test/__snapshots__/index.tsx.snap @@ -12,7 +12,18 @@ exports[`CustomerLink renders a dash if customer name is undefined 2`] = ` `; -exports[`CustomerLink renders a link to a customer with name and email 1`] = ` +exports[`CustomerLink renders a link to a customer from billing details 1`] = ` + +`; + +exports[`CustomerLink renders a link to a customer from order details 1`] = `
); +function renderCustomer( customer: ChargeBillingDetails, order: OrderDetails ) { + return render( + + ); } describe( 'CustomerLink', () => { - test( 'renders a link to a customer with name and email', () => { - const { container: customerLink } = renderCustomer( { - name: 'Some Name', - email: 'some@email.com', - } as any ); + test( 'renders a link to a customer from billing details', () => { + const { container: customerLink } = renderCustomer( + { + name: 'Some Name', + email: 'some@email.com', + } as any, + null as any + ); + expect( customerLink ).toMatchSnapshot(); + } ); + + test( 'renders a link to a customer from order details', () => { + const { container: customerLink } = renderCustomer( + null as any, + { + customer_name: 'Some Name', + customer_email: 'some@email.com', + } as any + ); expect( customerLink ).toMatchSnapshot(); } ); test( 'renders a dash if customer name is undefined', () => { - const { container: customerLink1 } = renderCustomer( null as any ); + const { container: customerLink1 } = renderCustomer( + null as any, + null as any + ); expect( customerLink1 ).toMatchSnapshot(); - const { container: customerLink2 } = renderCustomer( {} as any ); + const { container: customerLink2 } = renderCustomer( + {} as any, + {} as any + ); expect( customerLink2 ).toMatchSnapshot(); } ); } ); diff --git a/client/components/deposit-status-chip/index.tsx b/client/components/deposit-status-chip/index.tsx index e0e7ca10050..f6b456d2f24 100644 --- a/client/components/deposit-status-chip/index.tsx +++ b/client/components/deposit-status-chip/index.tsx @@ -14,7 +14,6 @@ import type { DepositStatus } from 'wcpay/types/deposits'; * Maps a DepositStatus to a ChipType. */ const mappings: Record< DepositStatus, ChipType > = { - estimated: 'light', pending: 'warning', in_transit: 'success', paid: 'success', diff --git a/client/components/deposit-status-chip/test/index.test.tsx b/client/components/deposit-status-chip/test/index.test.tsx index ee1575e1d85..54abc52546c 100644 --- a/client/components/deposit-status-chip/test/index.test.tsx +++ b/client/components/deposit-status-chip/test/index.test.tsx @@ -10,24 +10,17 @@ import { render } from '@testing-library/react'; import DepositStatusChip from '..'; describe( 'Deposits status chip renders', () => { - test( 'Renders In Transit status chip.', () => { - const { getByText } = render( - - ); - expect( getByText( 'Estimated' ) ).toBeTruthy(); - } ); - - test( 'Renders In Transit status chip.', () => { + test( 'Renders "Pending" status chip.', () => { const { getByText } = render( ); expect( getByText( 'Pending' ) ).toBeTruthy(); } ); - test( 'Renders In Transit status chip.', () => { + test( 'Renders "Paid" status chip.', () => { const { getByText } = render( ); expect( getByText( 'Paid' ) ).toBeTruthy(); } ); - test( 'Renders In Transit status chip.', () => { + test( 'Renders "In transit" status chip.', () => { const { getByText } = render( ); diff --git a/client/components/deposits-overview/deposit-notices.tsx b/client/components/deposits-overview/deposit-notices.tsx new file mode 100644 index 00000000000..7a38783a33b --- /dev/null +++ b/client/components/deposits-overview/deposit-notices.tsx @@ -0,0 +1,152 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; +import { tip } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import InlineNotice from 'components/inline-notice'; + +/** + * Renders a notice informing the user that their deposits are suspended. + */ +export const SuspendedDepositNotice: React.FC = () => { + return ( + + { interpolateComponents( { + /** translators: {{strong}}: placeholders are opening and closing strong tags. {{suspendLink}}: is a link element */ + mixedString: __( + 'Your deposits are {{strong}}temporarily suspended{{/strong}}. {{suspendLink}}Learn more{{/suspendLink}}', + 'woocommerce-payments' + ), + components: { + strong: , + suspendLink: ( + + ), + }, + } ) } + + ); +}; + +/** + * Renders a notice informing the user that the next deposit will include funds from a loan disbursement. + */ +export const DepositIncludesLoanPayoutNotice: React.FC = () => ( + + { interpolateComponents( { + mixedString: __( + 'This deposit will include funds from your WooCommerce Capital loan. {{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 + + ), + }, + } ) } + +); + +/** + * Renders a notice informing the user of the new account deposit waiting period. + */ +export const NewAccountWaitingPeriodNotice: React.FC = () => ( + + { interpolateComponents( { + mixedString: __( + 'Your first deposit is held for seven business days. {{whyLink}}Why?{{/whyLink}}', + 'woocommerce-payments' + ), + components: { + whyLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + +); + +/** + * Renders a notice informing the user of the number of days it may take for deposits to appear in their bank account. + */ +export const DepositTransitDaysNotice: React.FC = () => ( + + { __( + 'It may take 1-3 business days for deposits to reach your bank account.', + 'woocommerce-payments' + ) } + +); + +/** + * Renders a notice informing the user that their deposits may be paused due to a negative balance. + */ +export const NegativeBalanceDepositsPausedNotice: React.FC = () => ( + + { interpolateComponents( { + mixedString: sprintf( + /* translators: %s: WooPayments */ + __( + 'Deposits may be interrupted while your %s balance remains negative. {{whyLink}}Why?{{/whyLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + whyLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + +); diff --git a/client/components/deposits-overview/deposit-schedule.tsx b/client/components/deposits-overview/deposit-schedule.tsx index 86c9493bfd0..c7eae7bca78 100644 --- a/client/components/deposits-overview/deposit-schedule.tsx +++ b/client/components/deposits-overview/deposit-schedule.tsx @@ -9,33 +9,35 @@ import moment from 'moment'; /** * Internal dependencies */ -import { getDepositMonthlyAnchorLabel } from 'wcpay/deposits/utils'; +import { + getDepositMonthlyAnchorLabel, + getNextDepositDate, +} from 'wcpay/deposits/utils'; import type * as AccountOverview from 'wcpay/types/account-overview'; -/** - * The type of the props for the DepositScheduleDescription component. - * Mimics the AccountOverview.Account['deposits_schedule'] declaration. - */ -type DepositsScheduleProps = AccountOverview.Account[ 'deposits_schedule' ]; - +interface DepositScheduleProps { + depositsSchedule: AccountOverview.Account[ 'deposits_schedule' ]; +} /** * Renders the Deposit Schedule details component. * * eg "Your deposits are dispatched automatically every day" - * - * @param {DepositsScheduleProps} depositsSchedule The account's deposit schedule. - * @return {JSX.Element} Rendered element with Deposit Schedule details. */ -const DepositSchedule: React.FC< DepositsScheduleProps > = ( - depositsSchedule: DepositsScheduleProps -): JSX.Element => { +const DepositSchedule: React.FC< DepositScheduleProps > = ( { + depositsSchedule, +} ) => { + const nextDepositDate = getNextDepositDate( depositsSchedule ); + switch ( depositsSchedule.interval ) { case 'daily': return interpolateComponents( { - /** translators: {{strong}}: placeholders are opening and closing strong tags. */ - mixedString: __( - 'Your deposits are dispatched {{strong}}automatically every day{{/strong}}', - 'woocommerce-payments' + mixedString: sprintf( + /** translators: {{strong}}: placeholders are opening and closing strong tags. %s: is the date of the next deposit, e.g. "January 1st, 2023". */ + __( + 'Available funds are automatically dispatched {{strong}}every day{{/strong}} – your next deposit is scheduled for {{strong}}%s{{/strong}}.', + 'woocommerce-payments' + ), + nextDepositDate ), components: { strong: , @@ -50,12 +52,13 @@ const DepositSchedule: React.FC< DepositsScheduleProps > = ( return interpolateComponents( { mixedString: sprintf( - /** translators: %s: is the day of the week. eg "Friday". {{strong}}: placeholders are opening and closing strong tags.*/ + /** translators: %1$s: is the day of the week. eg "Friday". %2$s: is the date of the next deposit, e.g. "January 1st, 2023". {{strong}}: placeholders are opening and closing strong tags. */ __( - 'Your deposits are dispatched {{strong}}automatically every %s{{/strong}}', + 'Available funds are automatically dispatched {{strong}}every %1$s{{/strong}} – your next deposit is scheduled for {{strong}}%2$s{{/strong}}.', 'woocommerce-payments' ), - dayOfWeek + dayOfWeek, + nextDepositDate ), components: { strong: , @@ -67,10 +70,13 @@ const DepositSchedule: React.FC< DepositsScheduleProps > = ( // If the monthly anchor is 31, it means the deposit is scheduled for the last day of the month and has special handling. if ( monthlyAnchor === 31 ) { return interpolateComponents( { - /** translators: {{strong}}: placeholders are opening and closing strong tags. */ - mixedString: __( - 'Your deposits are dispatched {{strong}}automatically on the last day of every month{{/strong}}', - 'woocommerce-payments' + mixedString: sprintf( + /** translators: {{strong}}: placeholders are opening and closing strong tags. %s: is the date of the next deposit, e.g. "January 1st, 2023". */ + __( + 'Available funds are automatically dispatched {{strong}}on the last day of every month{{/strong}} – your next deposit is scheduled for {{strong}}%s{{/strong}}.', + 'woocommerce-payments' + ), + nextDepositDate ), components: { strong: , @@ -80,15 +86,16 @@ const DepositSchedule: React.FC< DepositsScheduleProps > = ( return interpolateComponents( { mixedString: sprintf( - /** translators: %s: is the day of the month. eg "15th". {{strong}}: placeholders are opening and closing strong tags.*/ + /** translators: {{strong}}: placeholders are opening and closing strong tags. %1$s: is the day of the month. eg "31st". %2$s: is the date of the next deposit, e.g. "January 1st, 2023". */ __( - 'Your deposits are dispatched {{strong}}automatically on the %s of every month{{/strong}}', + 'Available funds are automatically dispatched {{strong}}on the %1$s of every month{{/strong}} – your next deposit is scheduled for {{strong}}%2$s{{/strong}}.', 'woocommerce-payments' ), getDepositMonthlyAnchorLabel( { monthlyAnchor: monthlyAnchor, capitalize: false, - } ) + } ), + nextDepositDate ), components: { strong: , diff --git a/client/components/deposits-overview/footer.tsx b/client/components/deposits-overview/footer.tsx deleted file mode 100644 index 497412288b4..00000000000 --- a/client/components/deposits-overview/footer.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { CardFooter, Button, Flex } from '@wordpress/components'; -import { Link } from '@woocommerce/components'; - -/** - * Internal dependencies. - */ -import { getAdminUrl } from 'wcpay/utils'; -import strings from './strings'; -import wcpayTracks from 'tracks'; - -/** - * Renders the footer of the deposits overview card. - * - * @return {JSX.Element} Rendered footer of the deposits overview card. - */ -const DepositsOverviewFooter: React.FC = () => { - // The URL to the deposits list table. - const depositListTableUrl = getAdminUrl( { - page: 'wc-admin', - path: '/payments/deposits', - } ); - - // The URL to the deposit schedule settings page. - const depositScheduleUrl = - getAdminUrl( { - page: 'wc-settings', - tab: 'checkout', - section: 'woocommerce_payments', - } ) + '#deposit-schedule'; - - return ( - - - - - wcpayTracks.recordEvent( - wcpayTracks.events - .OVERVIEW_DEPOSITS_CHANGE_SCHEDULE_CLICK - ) - } - > - { strings.changeDepositSchedule } - - - - ); -}; - -export default DepositsOverviewFooter; diff --git a/client/components/deposits-overview/index.tsx b/client/components/deposits-overview/index.tsx index 59c6b762e2d..27c754c0640 100644 --- a/client/components/deposits-overview/index.tsx +++ b/client/components/deposits-overview/index.tsx @@ -2,87 +2,184 @@ * External dependencies */ import * as React from 'react'; -import { Card, CardHeader } from '@wordpress/components'; +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies. */ +import { getAdminUrl } from 'wcpay/utils'; +import wcpayTracks from 'tracks'; +import Loadable from 'components/loadable'; import { useSelectedCurrencyOverview } from 'wcpay/overview/hooks'; -import strings from './strings'; -import NextDepositDetails from './next-deposit'; import RecentDepositsList from './recent-deposits-list'; import DepositSchedule from './deposit-schedule'; -import SuspendedDepositNotice from './suspended-deposit-notice'; -import DepositsOverviewFooter from './footer'; -import DepositOverviewSectionHeading from './section-heading'; +import { + DepositTransitDaysNotice, + NegativeBalanceDepositsPausedNotice, + NewAccountWaitingPeriodNotice, + SuspendedDepositNotice, +} from './deposit-notices'; import useRecentDeposits from './hooks'; import './style.scss'; -const DepositsOverview = (): JSX.Element => { +const DepositsOverview: React.FC = () => { const { account, overview, isLoading: isLoadingOverview, } = useSelectedCurrencyOverview(); + const selectedCurrency = + overview?.currency || wcpaySettings.accountDefaultCurrency; + const { isLoading: isLoadingDeposits, deposits } = useRecentDeposits( + selectedCurrency + ); - let currency = wcpaySettings.accountDefaultCurrency; + const isLoading = isLoadingOverview || isLoadingDeposits; - if ( overview?.currency ) { - currency = overview.currency; - } + const availableFunds = overview?.available?.amount ?? 0; - const { isLoading: isLoadingDeposits, deposits } = useRecentDeposits( - currency - ); + // If the account has deposits blocked, there is no available balance or it is negative, there is no future deposit expected. + const isNextDepositExpected = + ! account?.deposits_blocked && availableFunds > 0; + // If the available balance is negative, deposits may be paused. + const isNegativeBalanceDepositsPaused = availableFunds < 0; + const hasCompletedWaitingPeriod = + wcpaySettings.accountStatus.deposits?.completed_waiting_period; + // Only show the deposit history section if the page is finished loading and there are deposits. */ } + const showRecentDeposits = + ! isLoading && + deposits?.length > 0 && + !! account && + ! account?.deposits_blocked; - const hasNextDeposit = !! overview?.nextScheduled; + // Show a loading state if the page is still loading. + if ( isLoading ) { + return ( + + + { __( 'Deposits', 'woocommerce-payments' ) } + - const isLoading = isLoadingOverview || isLoadingDeposits; + + + } + /> + + + ); + } // This card isn't shown if there are no deposits, so we can bail early. - if ( ! hasNextDeposit && ! isLoading && deposits.length === 0 ) { - return <>; + if ( ! isLoading && availableFunds === 0 && deposits.length === 0 ) { + return null; } return ( - { strings.heading } - { /* Only show the next deposit section if the page is loading or if deposits are not blocked. */ } - { ( isLoading || ! account?.deposits_blocked ) && ( - <> - - + { __( 'Deposits', 'woocommerce-payments' ) } + + + { /* Deposit schedule message */ } + { isNextDepositExpected && !! account && ( + + - + ) } - { /* Only show the deposit history section if the page is finished loading and there are deposits. */ } - { ! isLoading && !! account && !! deposits && deposits.length > 0 && ( + + { /* Notices */ } + + { account?.deposits_blocked ? ( + + ) : ( + <> + { isNextDepositExpected && ( + + ) } + { ! hasCompletedWaitingPeriod && ( + + ) } + { isNegativeBalanceDepositsPaused && ( + + ) } + + ) } + + + { showRecentDeposits && ( <> - { account.deposits_blocked ? ( - } - /> - ) : ( - - } - /> - ) } + + + { __( 'Deposit history', 'woocommerce-payments' ) } + + ) } - + + + + + { ! account?.deposits_blocked && ( + + ) } + ); }; diff --git a/client/components/deposits-overview/next-deposit.tsx b/client/components/deposits-overview/next-deposit.tsx deleted file mode 100644 index bcb588e60cc..00000000000 --- a/client/components/deposits-overview/next-deposit.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { - CardBody, - CardDivider, - Flex, - FlexItem, - Icon, -} from '@wordpress/components'; -import { calendar } from '@wordpress/icons'; -import interpolateComponents from '@automattic/interpolate-components'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import Loadable from 'components/loadable'; -import { getNextDeposit } from './utils'; -import DepositStatusChip from 'components/deposit-status-chip'; -import { getDepositDate } from 'deposits/utils'; -import { useAllDepositsOverviews, useDepositIncludesLoan } from 'wcpay/data'; -import InlineNotice from 'components/inline-notice'; -import { useSelectedCurrency } from 'wcpay/overview/hooks'; -import type * as AccountOverview from 'wcpay/types/account-overview'; - -type NextDepositProps = { - isLoading: boolean; - overview?: AccountOverview.Overview; -}; - -const DepositIncludesLoanPayoutNotice = () => ( - - { interpolateComponents( { - mixedString: __( - 'This deposit will include funds from your WooCommerce Capital loan. {{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 - - ), - }, - } ) } - -); - -const NewAccountWaitingPeriodNotice = () => ( - - { interpolateComponents( { - mixedString: __( - 'Your first deposit is held for seven business days. {{whyLink}}Why?{{/whyLink}}', - 'woocommerce-payments' - ), - components: { - whyLink: ( - // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - }, - } ) } - -); - -const NegativeBalanceDepositsPausedNotice = () => ( - - { interpolateComponents( { - mixedString: sprintf( - /* translators: %s: WooPayments */ - __( - 'Deposits may be interrupted while your %s balance remains negative. {{whyLink}}Why?{{/whyLink}}', - 'woocommerce-payments' - ), - 'WooPayments' - ), - components: { - whyLink: ( - // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - }, - } ) } - -); - -/** - * Renders the Next Deposit details component. - * - * This component included the next deposit heading, table and notice. - * - * @param {NextDepositProps} props Next Deposit details props. - * @return {JSX.Element} Rendered element with Next Deposit details. - */ -const NextDepositDetails: React.FC< NextDepositProps > = ( { - isLoading, - overview, -} ): JSX.Element => { - const tableClass = 'wcpay-deposits-overview__table'; - const nextDeposit = getNextDeposit( overview ); - const nextDepositDate = getDepositDate( - nextDeposit.date > 0 ? nextDeposit : null - ); - - const { includesFinancingPayout } = useDepositIncludesLoan( - nextDeposit.id - ); - const completedWaitingPeriod = - wcpaySettings.accountStatus.deposits?.completed_waiting_period; - - const { - overviews, - } = useAllDepositsOverviews() as AccountOverview.OverviewsResponse; - const { selectedCurrency } = useSelectedCurrency(); - const displayedCurrency = - selectedCurrency ?? wcpaySettings.accountDefaultCurrency; - - const availableBalance = overviews?.currencies.find( - ( currencyOverview ) => displayedCurrency === currencyOverview.currency - )?.available; - - const negativeBalanceDepositsPaused = - availableBalance && availableBalance.amount < 0; - - return ( - <> - { /* Next Deposit Table */ } - - - - - - - - - - - - - - - - - - { ! isLoading && ( - - ) } - - - - - } - /> - - - - - - - { /* Notices */ } - { ! isLoading && ( - - { includesFinancingPayout && ( - - ) } - { ! completedWaitingPeriod && ( - - ) } - { negativeBalanceDepositsPaused && ( - - ) } - - ) } - - ); -}; - -export default NextDepositDetails; diff --git a/client/components/deposits-overview/recent-deposits-list.tsx b/client/components/deposits-overview/recent-deposits-list.tsx index a0cbfea5fc2..119e7adc92f 100644 --- a/client/components/deposits-overview/recent-deposits-list.tsx +++ b/client/components/deposits-overview/recent-deposits-list.tsx @@ -23,7 +23,6 @@ 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 InlineNotice from '../inline-notice'; interface DepositRowProps { deposit: CachedDeposit; @@ -76,28 +75,9 @@ const RecentDepositsList: React.FC< RecentDepositsProps > = ( { return <>; } - // Add a notice indicating the potential business day delay for pending and in_transit deposits. - // The notice is added after the oldest pending or in_transit deposit. - const oldestPendingDepositId = [ ...deposits ] - .reverse() - .find( - ( deposit ) => - 'pending' === deposit.status || 'in_transit' === deposit.status - )?.id; const depositRows = deposits.map( ( deposit ) => ( - { deposit.id === oldestPendingDepositId && ( - - ) } ) ); diff --git a/client/components/deposits-overview/section-heading.tsx b/client/components/deposits-overview/section-heading.tsx deleted file mode 100644 index a7573b50ae4..00000000000 --- a/client/components/deposits-overview/section-heading.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { CardBody } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import Loadable from '../loadable'; - -/** - * SectionHeadingProps - * - * @typedef {Object} SectionHeadingProps - * - * @property {string} title Section heading title. - * @property {string} description Section heading description. - * @property {boolean} [isLoading] Optional. Whether the section heading is loading. - */ -type SectionHeadingProps = { - title: string; - text?: string | React.ReactNode; - children?: React.ReactNode; - isLoading?: boolean; -}; - -/** - * Renders the section heading component. - * - * @param {SectionHeadingProps} props Section heading props. - * @param {string} props.title Section heading title. - * @param {string} props.description Section heading description. - * @param {boolean} [props.isLoading] Optional. Whether the section heading should is loading. - * - * @return {JSX.Element} Rendered element with section heading. - */ -const DepositOverviewSectionHeading: React.FC< SectionHeadingProps > = ( { - title, - text = '', - children = null, - isLoading = false, -} ): JSX.Element => { - return ( - - - - -
- - { text !== '' ? ( - - { text } - - ) : ( - <>{ children } - ) } - -
-
- ); -}; - -export default DepositOverviewSectionHeading; diff --git a/client/components/deposits-overview/strings.ts b/client/components/deposits-overview/strings.ts deleted file mode 100644 index 2e6ab00b6da..00000000000 --- a/client/components/deposits-overview/strings.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -export default { - heading: __( 'Deposits', 'woocommerce-payments' ), - nextDeposit: { - title: __( 'Next deposit', 'woocommerce-payments' ), - description: __( - 'The amount may change while payments are still accumulating', - 'woocommerce-payments' - ), - }, - viewAllDeposits: __( 'View full deposits history', 'woocommerce-payments' ), - changeDepositSchedule: __( - 'Change deposit schedule', - 'woocommerce-payments' - ), - depositHistoryHeading: __( 'Deposit history', 'woocommerce-payments' ), -}; diff --git a/client/components/deposits-overview/style.scss b/client/components/deposits-overview/style.scss index 9b2c500af31..bfecab9f98c 100644 --- a/client/components/deposits-overview/style.scss +++ b/client/components/deposits-overview/style.scss @@ -11,31 +11,18 @@ line-height: 24px; color: $gray-900; } - - &__description { - & > &__text { - line-height: 16px; - color: $gray-700; - } - } } .wcpay-inline-notice.components-notice { - margin: 0; + margin: 16px 0; } - // Apply a margin bottom to all except the last notice - // in the notices container and to the business delay - // notice if it's the last child of the Deposit history table. - &__notices__container - > .wcpay-inline-notice.components-notice:not( :last-child ), - .wcpay-deposits-overview__business-day-delay-notice:last-child { - margin-bottom: 16px; - } + .components-card__body.wcpay-deposits-overview__schedule__container { + padding-top: 24px; + padding-bottom: 0; - // If the notices container is the last element before the footer (no deposit history), apply a margin to the footer, to float the notices on. - .wcpay-deposits-overview__notices__container:not( :empty ) - + .wcpay-deposits-overview__footer { - margin-top: 16px; + .is-loadable-placeholder { + margin-bottom: 24px; + } } // Override extraneous CardBody vertical padding - @@ -52,6 +39,15 @@ display: flex; } + // Override notice colors for transit days notice + .wcpay-inline-notice.components-notice.wcpay-deposit-transit-days-notice { + background-color: $gray-0; + + .wcpay-inline-notice__icon svg { + fill: $gray-900; + } + } + &__table { &__row { &__header { @@ -83,12 +79,21 @@ } } } - &__footer { - :not( :first-child ) { - margin-left: 12px; - } - a:not( .components-button ) { - text-decoration: none; + + &__footer.components-card__footer { + padding: 24px; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + + @media screen and ( max-width: 550px ) { + flex-direction: column; + justify-content: center; + padding: 16px 24px; + + > * { + margin: 0; + } } } } diff --git a/client/components/deposits-overview/suspended-deposit-notice.tsx b/client/components/deposits-overview/suspended-deposit-notice.tsx deleted file mode 100644 index 58f6b23e8a4..00000000000 --- a/client/components/deposits-overview/suspended-deposit-notice.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { __ } from '@wordpress/i18n'; -import interpolateComponents from '@automattic/interpolate-components'; -import { Link } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import InlineNotice from 'components/inline-notice'; - -/** - * Renders a notice informing the user that their deposits are suspended. - * - * @return {JSX.Element} Rendered notice. - */ -function SuspendedDepositNotice(): JSX.Element { - return ( - - { interpolateComponents( { - /** translators: {{strong}}: placeholders are opening and closing strong tags. {{suspendLink}}: is a
link element */ - mixedString: __( - 'Your deposits are {{strong}}temporarily suspended{{/strong}}. {{suspendLink}}Learn more{{/suspendLink}}', - 'woocommerce-payments' - ), - components: { - strong: , - suspendLink: ( - - ), - }, - } ) } - - ); -} - -export default SuspendedDepositNotice; diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index 93b88c89b33..9dcdb85548d 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -1,34 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Deposits Overview footer renders Component Renders 1`] = ` - -`; - exports[`Deposits Overview information Component Renders 1`] = `
Deposits
-
- - Next deposit - -
- - The amount may change while payments are still accumulating - -
-
-
-
-
- Estimated dispatch date -
-
- Status -
-
- Amount -
-
-
- -
-
-
- - — -
-
- - Estimated - -
-
- $1.00 -
-
-
Deposit history -
- - Your deposits are dispatched - - automatically every Monday - - -
-
-
-
-
- - - - - -
-
- Deposits pending or in-transit may take 1-2 business days to appear in your bank account once dispatched -
-
-
-
-
{ includesFinancingPayout: false, isLoading: false, } ); + mockAccount.deposits_blocked = false; } ); afterEach( () => { jest.clearAllMocks(); } ); test( 'Component Renders', () => { - mockOverviews( [ createMockOverview( 'usd', 100, 0, 'estimated' ) ] ); + mockOverviews( [ createMockOverview( 'usd', 100, 0, 'pending' ) ] ); mockUseDeposits.mockReturnValue( { depositsCount: 0, deposits: mockDeposits, @@ -261,65 +260,71 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const { container } = render( ); + const { container, getByText } = render( ); + // Check that the button and link is rendered. + getByText( 'View full deposits history' ); + getByText( 'Change deposit schedule' ); expect( container ).toMatchSnapshot(); } ); - test( 'Component renders without errors for new account', () => { + test( `Component doesn't render for new account`, () => { mockOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: [], + isLoading: false, + } ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, } ); - const { getByText } = render( ); - getByText( '€0.00' ); + const { container } = render( ); + expect( container ).toBeEmptyDOMElement(); } ); - test( 'Confirm next deposit in EUR amount', () => { - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + test( `Component doesn't render for new accounts with pending funds but no available funds`, () => { + mockOverviews( [ createMockNewAccountOverview( 'eur', 5000, 0 ) ] ); + mockDepositOverviews( [ + createMockNewAccountOverview( 'eur', 5000, 0 ), + ] ); + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: [], + isLoading: false, + } ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, } ); - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); - const { getByText } = render( - - ); - - expect( getByText( '$1.00' ) ).toBeTruthy(); + const { container } = render( ); + expect( container ).toBeEmptyDOMElement(); } ); - test( 'Confirm next deposit in EUR amount', () => { - global.wcpaySettings.connect.country = 'EU'; - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + test( 'Confirm notice renders if deposits blocked', () => { + mockAccount.deposits_blocked = true; + mockOverviews( [ + createMockOverview( 'usd', 30000, 50000, 'pending' ), + ] ); + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: mockDeposits, + isLoading: false, + } ); + mockDepositOverviews( [ createMockNewAccountOverview( 'usd' ) ] ); mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'eur', + selectedCurrency: 'usd', setSelectedCurrency: mockSetSelectedCurrency, } ); - const overview = createMockOverview( 'EUR', 647049, 0, 'estimated' ); - const { getByText } = render( - - ); - - expect( getByText( '€6.470,49' ) ).toBeTruthy(); - } ); + const { getByText, queryByText } = render( ); - test( 'Confirm next deposit dates', () => { - const date = Date.parse( '2021-10-01' ); - const overview = createMockOverview( 'usd', 100, date, 'estimated' ); + getByText( /Your deposits are temporarily suspended/ ); - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); - mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'eur', - setSelectedCurrency: mockSetSelectedCurrency, - } ); - - const { getByText } = render( - - ); - expect( getByText( 'October 1, 2021' ) ).toBeTruthy(); + // Check that the buttons are rendered as expected. + getByText( 'View full deposits history' ); + // This one is not rendered when deposits are blocked. + expect( queryByText( 'Change deposit schedule' ) ).toBeFalsy(); } ); test( 'Confirm recent deposits renders ', () => { @@ -335,8 +340,8 @@ describe( 'Deposits Overview information', () => { expect( container ).toBeEmptyDOMElement(); } ); - test( 'Renders capital loan notice if deposit includes financing payout', () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + // Capital loans notice temporarily disabled, tests skipped until resolved. See #7689. + test.skip( 'Renders capital loan notice if deposit includes financing payout', () => { mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: true, isLoading: false, @@ -347,9 +352,7 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const { getByRole, getByText } = render( - - ); + const { getByRole, getByText } = render( ); getByText( 'deposit will include funds from your WooCommerce Capital loan', @@ -368,8 +371,8 @@ describe( 'Deposits Overview information', () => { ); } ); - test( `Doesn't render capital loan notice if deposit does not include financing payout`, () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + // Capital loans notice temporarily disabled, tests skipped until resolved. See #7689. + test.skip( `Doesn't render capital loan notice if deposit does not include financing payout`, () => { mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: false, isLoading: false, @@ -380,9 +383,7 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const { queryByRole, queryByText } = render( - - ); + const { queryByRole, queryByText } = render( ); expect( queryByText( @@ -402,7 +403,13 @@ describe( 'Deposits Overview information', () => { test( 'Confirm new account waiting period notice does not show', () => { global.wcpaySettings.accountStatus.deposits.completed_waiting_period = true; - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + const accountOverview = createMockNewAccountOverview( + 'eur', + 12300, + 45600 + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, @@ -416,7 +423,13 @@ describe( 'Deposits Overview information', () => { test( 'Confirm new account waiting period notice shows', () => { global.wcpaySettings.accountStatus.deposits.completed_waiting_period = false; - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + const accountOverview = createMockNewAccountOverview( + 'eur', + 12300, + 45600 + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, @@ -433,84 +446,75 @@ describe( 'Deposits Overview information', () => { } ); } ); -describe( 'Deposits Overview footer renders', () => { - test( 'Component Renders', () => { - const { container, getByText } = render( ); - expect( container ).toMatchSnapshot(); - - // Check that the button and link is rendered. - getByText( 'View full deposits history' ); - getByText( 'Change deposit schedule' ); - } ); -} ); - describe( 'Deposit Schedule renders', () => { test( 'with a weekly schedule', () => { const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically every Monday' - ); + expect( descriptionText ).toContain( 'every Monday' ); } ); test( 'with a monthly schedule on the 14th', () => { mockAccount.deposits_schedule.interval = 'monthly'; mockAccount.deposits_schedule.monthly_anchor = 14; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically on the 14th of every month' - ); + expect( descriptionText ).toContain( 'on the 14th of every month' ); } ); test( 'with a monthly schedule on the last day', () => { mockAccount.deposits_schedule.interval = 'monthly'; mockAccount.deposits_schedule.monthly_anchor = 31; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically on the last day of every month' - ); + expect( descriptionText ).toContain( 'on the last day of every month' ); } ); test( 'with a monthly schedule on the 2nd', () => { mockAccount.deposits_schedule.interval = 'monthly'; mockAccount.deposits_schedule.monthly_anchor = 2; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically on the 2nd of every month' - ); + expect( descriptionText ).toContain( 'on the 2nd of every month' ); } ); test( 'with a daily schedule', () => { mockAccount.deposits_schedule.interval = 'daily'; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically every day' - ); + expect( descriptionText ).toContain( 'every day' ); } ); test( 'with a daily schedule', () => { mockAccount.deposits_schedule.interval = 'manual'; const { container } = render( - + ); // Check that a manual schedule is not rendered. @@ -527,51 +531,34 @@ describe( 'Suspended Deposit Notice Renders', () => { describe( 'Paused Deposit notice Renders', () => { test( 'When available balance is negative', () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); - mockUseDeposits.mockReturnValue( { - depositsCount: 0, - deposits: mockDeposits, - isLoading: false, - } ); - mockDepositOverviews( [ - // Negative 100 available balance - createMockNewAccountOverview( 'usd', 100, -100 ), - ] ); + const accountOverview = createMockNewAccountOverview( + 'usd', + 100, + -100 // Negative 100 available balance + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'usd', setSelectedCurrency: mockSetSelectedCurrency, } ); - const { getByText } = render( - - ); - getByText( - 'Deposits may be interrupted while your WooPayments balance remains negative. Why?' - ); + const { getByText } = render( ); + getByText( /Deposits may be interrupted/, { + ignore: '.a11y-speak-region', + } ); } ); test( 'When available balance is positive', () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); - mockUseDeposits.mockReturnValue( { - depositsCount: 0, - deposits: mockDeposits, - isLoading: false, - } ); - mockDepositOverviews( [ - // Positive 100 available balance - createMockNewAccountOverview( 'usd', 100, 100 ), - ] ); - mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'usd', - setSelectedCurrency: mockSetSelectedCurrency, - } ); - - const { queryByText } = render( - + const accountOverview = createMockNewAccountOverview( + 'usd', + 100, + 100 // Positive 100 available balance ); - expect( - queryByText( - 'Deposits may be interrupted while your WooPayments balance remains negative. Why?' - ) - ).toBeFalsy(); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + + const { queryByText } = render( ); + expect( queryByText( /Deposits may be interrupted/ ) ).toBeFalsy(); } ); } ); diff --git a/client/components/deposits-overview/utils.ts b/client/components/deposits-overview/utils.ts deleted file mode 100644 index e3c93323fd0..00000000000 --- a/client/components/deposits-overview/utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Internal dependencies - */ -import { formatCurrency } from 'utils/currency'; -import type { DepositStatus } from 'wcpay/types/deposits'; -import type * as AccountOverview from 'wcpay/types/account-overview'; - -type NextDepositTableData = { - id?: string; - date: number; - status: DepositStatus; - amount: string; -}; - -/** - * Formats the next deposit data from the overview object into an object that can be used in the Next Deposits table. - * - * @param {AccountOverview.Overview} [overview] - The overview object containing information about the next scheduled deposit. - * @return {NextDepositTableData} An object containing the formatted next deposit data, with the following properties: - * - id: An optional string representing the ID of the next scheduled deposit. - * - date: A Unix timestamp representing the date of the next scheduled deposit. - * - status: A string representing the status of the next scheduled deposit. If no status is provided, defaults to 'estimated. - * - amount: A formatted string representing the amount of the next scheduled deposit in the currency specified in the overview object. - */ -export const getNextDeposit = ( - overview?: AccountOverview.Overview -): NextDepositTableData => { - if ( ! overview?.nextScheduled ) { - return { - id: undefined, - date: 0, - status: 'estimated', - amount: formatCurrency( 0, overview?.currency ), - }; - } - - const { currency, nextScheduled } = overview; - - return { - id: nextScheduled.id, - date: nextScheduled.date ?? 0, - status: nextScheduled.status ?? 'estimated', - amount: formatCurrency( nextScheduled.amount ?? 0, currency ), - }; -}; diff --git a/client/components/test-mode-notice/index.js b/client/components/test-mode-notice/index.js deleted file mode 100644 index 5d3d8738d38..00000000000 --- a/client/components/test-mode-notice/index.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * External dependencies - */ -import { __, _n, sprintf } from '@wordpress/i18n'; -import { Notice } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { isInTestMode, getPaymentSettingsUrl } from 'utils'; - -// The topics (i.e. pages) that have test mode notices. -export const topics = { - overview: sprintf( - /* translators: %s: WooPayments */ - __( '%s is in test mode.', 'woocommerce-payments' ), - 'WooPayments' - ), - transactions: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test transactions. To view live transactions, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - paymentDetails: __( 'Test payment:', 'woocommerce-payments' ), - deposits: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test deposits. To view live deposits, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - depositDetails: __( 'Test deposit:', 'woocommerce-payments' ), - disputes: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test disputes. To view live disputes, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - disputeDetails: __( 'Test dispute:', 'woocommerce-payments' ), - documents: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test documents. To view live documents, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - documentDetails: __( 'Test document:', 'woocommerce-payments' ), - loans: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test loans. To view live loans, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - authorizations: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test authorizations. To view live authorizations, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - riskReviewTransactions: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test on review transactions. To view live on review transactions, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - blockedTransactions: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test blocked transactions. To view live blocked transactions, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), -}; - -// These are all the topics used for details pages where the notice is slightly different. -const detailsTopics = [ - topics.paymentDetails, - topics.disputeDetails, - topics.depositDetails, - topics.documentDetails, -]; - -/** - * Returns an tag with the href attribute set to the Payments settings - * page, and the provided text. - * - * @param {string} topic The notice message topic. - * - * @return {*} An HTML component with a link to wcpay settings page. - */ -export const getPaymentsSettingsUrlComponent = () => { - return ( - - { sprintf( - /* translators: %s: WooPayments */ - __( 'View %s settings', 'woocommerce-payments' ), - 'WooPayments' - ) } - - ); -}; - -/** - * Returns notice details depending on the topic provided. - * - * @param {string} topic The notice message topic. - * - * @return {string} The specific details the notice is supposed to contain. - */ -export const getTopicDetails = ( topic ) => { - return sprintf( - /* translators: %s: WooPayments */ - _n( - '%s was in test mode when this order was placed.', - '%s was in test mode when these orders were placed.', - topics.depositDetails === topic ? 2 : 1, - 'woocommerce-payments' - ), - 'WooPayments' - ); -}; - -/** - * Returns the correct notice message wrapped in a span for a given topic. - * - * The message is wrapped in a span to make it easier to apply styling to - * different parts of the text, i.e. to include multiple HTML elements. - * - * @param {string} topic The notice message topic. - * - * @return {string} The correct notice message. - */ -export const getNoticeMessage = ( topic ) => { - const urlComponent = getPaymentsSettingsUrlComponent(); - - if ( detailsTopics.includes( topic ) ) { - return ( - - { topic } { getTopicDetails( topic ) } { urlComponent } - - ); - } - - return ( - - { topic } { urlComponent } - - ); -}; - -export const TestModeNotice = ( { topic } ) => { - if ( isInTestMode() ) { - return ( - - { getNoticeMessage( topic ) } - - ); - } - return <>; -}; diff --git a/client/components/test-mode-notice/index.tsx b/client/components/test-mode-notice/index.tsx new file mode 100644 index 00000000000..80310707a46 --- /dev/null +++ b/client/components/test-mode-notice/index.tsx @@ -0,0 +1,172 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __, _n, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { getPaymentSettingsUrl, isInTestMode } from 'utils'; +import BannerNotice from '../banner-notice'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; + +type CurrentPage = + | 'overview' + | 'documents' + | 'deposits' + | 'disputes' + | 'loans' + | 'payments' + | 'transactions'; + +interface Props { + currentPage: CurrentPage; + actions?: React.ComponentProps< typeof BannerNotice >[ 'actions' ]; + isDetailsView?: boolean; + isDevMode?: boolean; +} + +const nounToUse = { + documents: __( 'document', 'woocommerce-payments' ), + deposits: __( 'deposit', 'woocommerce-payments' ), + disputes: __( 'dispute', 'woocommerce-payments' ), + loans: __( 'loan', 'woocommerce-payments' ), + payments: __( 'order', 'woocommerce-payments' ), + transactions: __( 'order', 'woocommerce-payments' ), +}; + +const verbToUse = { + documents: __( 'created', 'woocommerce-payments' ), + deposits: __( 'created', 'woocommerce-payments' ), + disputes: __( 'created', 'woocommerce-payments' ), + loans: __( 'created', 'woocommerce-payments' ), + payments: __( 'placed', 'woocommerce-payments' ), + transactions: __( 'placed', 'woocommerce-payments' ), +}; + +const getNoticeContent = ( + currentPage: CurrentPage, + isDetailsView: boolean, + isDevMode: boolean +): JSX.Element => { + switch ( currentPage ) { + case 'overview': + return isDevMode ? ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{strong}}%1$s is in dev mode.{{/strong}} You need to set up a live %1$s account before you can accept real transactions.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + strong: , + }, + } ) } + + ) : ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{strong}}%1$s is in test mode.{{/strong}} All transactions will be simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + strong: , + 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 + + ), + }, + } ) } + + ); + case 'documents': + case 'deposits': + case 'disputes': + case 'payments': + case 'loans': + case 'transactions': + return isDetailsView ? ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + _n( + '%1$s was in test mode when this %2$s was %3$s. To view live %2$ss, disable test mode in {{settingsLink}}%1$s settings{{/settingsLink}}.', + '%1$s was in test mode when these %2$ss were %3$s. To view live %2$ss, disable test mode in {{settingsLink}}%1$s settings{{/settingsLink}}.', + 'deposits' === currentPage ? 2 : 1, + 'woocommerce-payments' + ), + 'WooPayments', + nounToUse[ currentPage ], + verbToUse[ currentPage ] + ), + components: { + settingsLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ) : ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + 'Viewing test %1$s. To view live %1s, disable test mode in {{settingsLink}}%2s settings{{/settingsLink}}.', + 'woocommerce-payments' + ), + currentPage, + 'WooPayments' + ), + components: { + settingsLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ); + } +}; + +export const TestModeNotice: React.FC< Props > = ( { + currentPage, + actions, + isDetailsView = false, + isDevMode = false, +} ) => { + if ( ! isInTestMode() ) return null; + + return ( + + { getNoticeContent( currentPage, isDetailsView, isDevMode ) } + + ); +}; diff --git a/client/components/test-mode-notice/test/__snapshots__/index.js.snap b/client/components/test-mode-notice/test/__snapshots__/index.js.snap deleted file mode 100644 index e4a830ae214..00000000000 --- a/client/components/test-mode-notice/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,233 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Test mode notification Component is rendered correctly 1`] = ` -
- -`; - -exports[`Test mode notification Component is rendered correctly 2`] = ` -
-
-
- - Viewing test deposits. To view live deposits, disable test mode in WooPayments settings. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 3`] = ` -
-
-
- - Viewing test disputes. To view live disputes, disable test mode in WooPayments settings. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 4`] = ` -
-
-
- - Viewing test documents. To view live documents, disable test mode in WooPayments settings. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 5`] = ` -
-
-
- - - Test deposit: - - - WooPayments was in test mode when these orders were placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 6`] = ` -
-
-
- - - Test dispute: - - - WooPayments was in test mode when this order was placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 7`] = ` -
-
-
- - - Test payment: - - - WooPayments was in test mode when this order was placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 8`] = ` -
-
-
- - - Test document: - - - WooPayments was in test mode when this order was placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 9`] = `
`; - -exports[`Test mode notification Component is rendered correctly 10`] = `
`; - -exports[`Test mode notification Component is rendered correctly 11`] = `
`; - -exports[`Test mode notification Component is rendered correctly 12`] = `
`; - -exports[`Test mode notification Component is rendered correctly 13`] = `
`; - -exports[`Test mode notification Component is rendered correctly 14`] = `
`; - -exports[`Test mode notification Component is rendered correctly 15`] = `
`; - -exports[`Test mode notification Component is rendered correctly 16`] = `
`; diff --git a/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap b/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap new file mode 100644 index 00000000000..6005f079030 --- /dev/null +++ b/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test mode notification Returns empty div if not in test mode 1`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 2`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 3`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 4`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 5`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 6`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 7`] = `
`; + +exports[`Test mode notification Returns valid component for deposits page 1`] = ` +
+
+
+ Viewing test deposits. To view live deposits, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for disputes page 1`] = ` +
+
+
+ Viewing test disputes. To view live disputes, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for documents page 1`] = ` +
+
+
+ Viewing test documents. To view live documents, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for loans page 1`] = ` +
+
+
+ Viewing test loans. To view live loans, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for overview page 1`] = ` +
+
+
+ + WooPayments is in test mode. + + All transactions will be simulated. + + Learn more + +
+
+
+`; + +exports[`Test mode notification Returns valid component for payments page 1`] = ` +
+
+
+ Viewing test payments. To view live payments, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for transactions page 1`] = ` +
+
+
+ Viewing test transactions. To view live transactions, disable test mode in + + WooPayments settings + + . +
+
+
+`; diff --git a/client/components/test-mode-notice/test/index.js b/client/components/test-mode-notice/test/index.js deleted file mode 100644 index 8a39450739e..00000000000 --- a/client/components/test-mode-notice/test/index.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import { getPaymentSettingsUrl, isInTestMode } from 'utils'; -import { - topics, - getPaymentsSettingsUrlComponent, - getTopicDetails, - getNoticeMessage, - TestModeNotice, -} from '../index'; - -jest.mock( 'utils', () => ( { - isInTestMode: jest.fn(), - getPaymentSettingsUrl: jest.fn().mockReturnValue( 'https://example.com/' ), -} ) ); - -describe( 'Test mode notification', () => { - // Set up easy to use lists containing test inputs. - const listTopics = [ - topics.transactions, - topics.deposits, - topics.disputes, - topics.documents, - ]; - const detailsTopics = [ - topics.depositDetails, - topics.disputeDetails, - topics.paymentDetails, - topics.documentDetails, - ]; - const allTopics = [ ...listTopics, ...detailsTopics ]; - - const topicsWithTestMode = [ - ...allTopics.map( ( topic ) => [ topic, true ] ), - ...allTopics.map( ( topic ) => [ topic, false ] ), - ]; - - test( 'Returns correct URL component', () => { - const expected = ( - - { 'View WooPayments settings' } - - ); - - expect( getPaymentsSettingsUrlComponent() ).toStrictEqual( expected ); - } ); - - test.each( listTopics )( - 'Returns right notice message for list topics', - ( topic ) => { - const expected = ( - - { topic } { getPaymentsSettingsUrlComponent( topic ) } - - ); - - expect( getNoticeMessage( topic ) ).toStrictEqual( expected ); - } - ); - - test( 'Notice details are correct for details topics', () => { - expect( getTopicDetails( topics.depositDetails ) ).toBe( - 'WooPayments was in test mode when these orders were placed.' - ); - - expect( getTopicDetails( topics.disputeDetails ) ).toBe( - 'WooPayments was in test mode when this order was placed.' - ); - - expect( getTopicDetails( topics.paymentDetails ) ).toBe( - 'WooPayments was in test mode when this order was placed.' - ); - } ); - - test.each( detailsTopics )( - 'Returns right notice message for details topics', - ( topic ) => { - const topicDetails = getTopicDetails( topic ); - const urlComponent = getPaymentsSettingsUrlComponent( topic ); - const expected = ( - - { topic } { topicDetails } { urlComponent } - - ); - - expect( getNoticeMessage( topic ) ).toStrictEqual( expected ); - } - ); - - test.each( topicsWithTestMode )( - 'Component is rendered correctly', - ( topic, isTestMode ) => { - isInTestMode.mockReturnValue( isTestMode ); - const { container: testModeNotice } = render( - - ); - - expect( testModeNotice ).toMatchSnapshot(); - } - ); -} ); diff --git a/client/components/test-mode-notice/test/index.tsx b/client/components/test-mode-notice/test/index.tsx new file mode 100644 index 00000000000..7dabdb3afc1 --- /dev/null +++ b/client/components/test-mode-notice/test/index.tsx @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { isInTestMode } from 'utils'; +import { TestModeNotice } from '../index'; + +declare const global: { + wcSettings: { countries: Record< string, string > }; + wcpaySettings: { + accountStatus: { + detailsSubmitted: boolean; + }; + }; +}; + +jest.mock( 'utils', () => ( { + isInTestMode: jest.fn(), + getPaymentSettingsUrl: jest.fn().mockReturnValue( 'https://example.com/' ), +} ) ); + +const mockIsInTestMode = isInTestMode as jest.MockedFunction< + typeof isInTestMode +>; + +type CurrentPage = + | 'overview' + | 'documents' + | 'deposits' + | 'disputes' + | 'loans' + | 'payments' + | 'transactions'; + +describe( 'Test mode notification', () => { + beforeEach( () => { + global.wcpaySettings = { + accountStatus: { + detailsSubmitted: true, + }, + }; + } ); + + const pages: CurrentPage[] = [ + 'overview', + 'documents', + 'deposits', + 'disputes', + 'loans', + 'payments', + 'transactions', + ]; + + test.each( pages )( 'Returns valid component for %s page', ( page ) => { + mockIsInTestMode.mockReturnValue( true ); + + const { container: testModeNotice } = render( + + ); + + expect( testModeNotice ).toMatchSnapshot(); + } ); + + test.each( pages )( 'Returns empty div if not in test mode', ( page ) => { + mockIsInTestMode.mockReturnValue( false ); + + const { container: testModeNotice } = render( + + ); + + expect( testModeNotice ).toMatchSnapshot(); + } ); +} ); diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 1b12eaafe43..a70a1f1ceb1 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -23,6 +23,7 @@ import scheduled from 'gridicons/dist/scheduled'; */ import wcpayTracks from 'tracks'; import Page from 'components/page'; +import BannerNotice from 'components/banner-notice'; import PaymentMethods from './payment-methods'; import Incentive from './incentive'; import InfoNotice from './info-notice-modal'; @@ -46,6 +47,8 @@ const ConnectAccountPage: React.FC = () => { connect: { availableCountries, country }, } = wcpaySettings; + const isCountrySupported = !! availableCountries[ country ]; + useEffect( () => { wcpayTracks.recordEvent( wcpayTracks.events.CONNECT_ACCOUNT_VIEW, { path: 'payments_connect_v2', @@ -125,7 +128,7 @@ const ConnectAccountPage: React.FC = () => { } // Inform the merchant if country specified in business address is not yet supported, but allow to proceed. - if ( ! availableCountries[ country ] ) { + if ( ! isCountrySupported ) { return handleLocationCheck(); } @@ -149,6 +152,11 @@ const ConnectAccountPage: React.FC = () => { ) : ( <> + { ! isCountrySupported && ( + + { strings.nonSupportedCountry } + + ) }
logo diff --git a/client/connect-account-page/strings.tsx b/client/connect-account-page/strings.tsx index 08c4aab5aee..4ff71010f79 100644 --- a/client/connect-account-page/strings.tsx +++ b/client/connect-account-page/strings.tsx @@ -285,4 +285,25 @@ export default { 'woocommerce-payments' ), }, + nonSupportedCountry: createInterpolateElement( + sprintf( + /* translators: %1$s: WooPayments */ + __( + '%1$s is not currently available in your location. To be eligible for %1$s, your business address must be in one of the following supported countries.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + { + b: , + a: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + } + ), }; diff --git a/client/connect-account-page/test/index.test.tsx b/client/connect-account-page/test/index.test.tsx index 8b067726df3..d593a48e250 100644 --- a/client/connect-account-page/test/index.test.tsx +++ b/client/connect-account-page/test/index.test.tsx @@ -118,6 +118,26 @@ describe( 'ConnectAccountPage', () => { ).toBeInTheDocument(); } ); + test( 'should display non supported country banner', () => { + global.wcpaySettings = { + connectUrl: '/wcpay-connect-url', + connect: { + country: 'CA', + availableCountries: { + GB: 'United Kingdom (UK)', + US: 'United States (US)', + }, + }, + }; + render( ); + + expect( + screen.queryAllByText( + /woopayments is not currently available in your location/i + )[ 0 ] + ).toBeInTheDocument(); + } ); + test( 'should prompt unsupported countries', () => { global.wcpaySettings = { connectUrl: '/wcpay-connect-url', diff --git a/client/data/deposits/hooks.ts b/client/data/deposits/hooks.ts index 7e0b53bac21..bcb4878b06f 100644 --- a/client/data/deposits/hooks.ts +++ b/client/data/deposits/hooks.ts @@ -24,11 +24,15 @@ export const useDeposit = ( ): { deposit: CachedDeposit; isLoading: boolean } => useSelect( ( select ) => { - const { getDeposit, isResolving } = select( STORE_NAME ); + const { getDeposit, isResolving, hasFinishedResolution } = select( + STORE_NAME + ); return { deposit: getDeposit( id ), - isLoading: isResolving( 'getDeposit', [ id ] ), + isLoading: + ! hasFinishedResolution( 'getDeposit', [ id ] ) || + isResolving( 'getDeposit', [ id ] ), }; }, [ id ] @@ -124,8 +128,14 @@ export const useDeposits = ( { date_between: dateBetween, status_is: statusIs, status_is_not: statusIsNot, -}: Query ): CachedDeposits => - useSelect( +}: Query ): CachedDeposits => { + // Temporarily default to excluding estimated deposits. + // Client components can (temporarily) opt-in by passing `status_is=estimated`. + // When we remove estimated deposits from server / APIs we can remove this default. + if ( ! statusIsNot && statusIs !== 'estimated' ) { + statusIsNot = 'estimated'; + } + return useSelect( ( select ) => { const { getDeposits, @@ -176,6 +186,7 @@ export const useDeposits = ( { statusIsNot, ] ); +}; export const useDepositsSummary = ( { match, @@ -185,8 +196,14 @@ export const useDepositsSummary = ( { date_between: dateBetween, status_is: statusIs, status_is_not: statusIsNot, -}: Query ): DepositsSummaryCache => - useSelect( +}: Query ): DepositsSummaryCache => { + // Temporarily default to excluding estimated deposits. + // Client components can (temporarily) opt-in by passing `status_is=estimated`. + // When we remove estimated deposits from server / APIs we can remove this default. + if ( ! statusIsNot && statusIs !== 'estimated' ) { + statusIsNot = 'estimated'; + } + return useSelect( ( select ) => { const { getDepositsSummary, isResolving } = select( STORE_NAME ); @@ -215,6 +232,7 @@ export const useDepositsSummary = ( { statusIsNot, ] ); +}; export const useInstantDeposit = ( transactionIds: string[] diff --git a/client/data/payment-intents/test/hooks.ts b/client/data/payment-intents/test/hooks.ts index 68513573c36..370816ac23a 100644 --- a/client/data/payment-intents/test/hooks.ts +++ b/client/data/payment-intents/test/hooks.ts @@ -57,6 +57,8 @@ export const chargeMock: Charge = { number: Number( '67' ), url: 'http://order.url', customer_url: 'customer.url', + customer_name: '', + customer_email: '', subscriptions: [], }, outcome: { @@ -89,6 +91,8 @@ export const paymentIntentMock: PaymentIntent = { number: 123, url: 'http://order.url', customer_url: 'customer.url', + customer_name: '', + customer_email: '', fraud_meta_box_type: 'review', }, }; diff --git a/client/data/transactions/hooks.ts b/client/data/transactions/hooks.ts index be8386916fc..d0a983e75f9 100644 --- a/client/data/transactions/hooks.ts +++ b/client/data/transactions/hooks.ts @@ -10,6 +10,7 @@ import type { Query } from '@woocommerce/navigation'; * Internal dependencies */ import { STORE_NAME } from '../constants'; +import type { DepositStatus } from 'wcpay/types/deposits'; // TODO: refine this type with more detailed information. export interface Transaction { @@ -25,13 +26,7 @@ export interface Transaction { customer_country: string; customer_currency: string; deposit_id?: string; - deposit_status?: - | 'paid' - | 'pending' - | 'in_transit' - | 'canceled' - | 'failed' - | 'estimated'; + deposit_status?: DepositStatus; available_on: string; currency: string; transaction_id: string; @@ -153,6 +148,12 @@ export const useTransactions = ( type_is_not: typeIsNot, source_device_is: sourceDeviceIs, source_device_is_not: sourceDeviceIsNot, + channel_is: channelIs, + channel_is_not: channelIsNot, + customer_country_is: customerCountryIs, + customer_country_is_not: customerCountryIsNot, + risk_level_is: riskLevelIs, + risk_level_is_not: riskLevelIsNot, store_currency_is: storeCurrencyIs, customer_currency_is: customerCurrencyIs, customer_currency_is_not: customerCurrencyIsNot, @@ -193,6 +194,12 @@ export const useTransactions = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, search, @@ -220,6 +227,12 @@ export const useTransactions = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, JSON.stringify( search ), @@ -239,6 +252,12 @@ export const useTransactionsSummary = ( store_currency_is: storeCurrencyIs, customer_currency_is: customerCurrencyIs, customer_currency_is_not: customerCurrencyIsNot, + channel_is: channelIs, + channel_is_not: channelIsNot, + customer_country_is: customerCountryIs, + customer_country_is_not: customerCountryIsNot, + risk_level_is: riskLevelIs, + risk_level_is_not: riskLevelIsNot, loan_id_is: loanIdIs, search, }: Query, @@ -262,6 +281,12 @@ export const useTransactionsSummary = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, search, @@ -284,6 +309,12 @@ export const useTransactionsSummary = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, JSON.stringify( search ), diff --git a/client/data/transactions/resolvers.js b/client/data/transactions/resolvers.js index 864e1289d2d..8f5c3ea3a96 100644 --- a/client/data/transactions/resolvers.js +++ b/client/data/transactions/resolvers.js @@ -42,6 +42,12 @@ export const formatQueryFilters = ( query ) => ( { type_is_not: query.typeIsNot, source_device_is: query.sourceDeviceIs, source_device_is_not: query.sourceDeviceIsNot, + channel_is: query.channelIs, + channel_is_not: query.channelIsNot, + customer_country_is: query.customerCountryIs, + customer_country_is_not: query.customerCountryIsNot, + risk_level_is: query.riskLevelIs, + risk_level_is_not: query.riskLevelIsNot, store_currency_is: query.storeCurrencyIs, loan_id_is: query.loanIdIs, deposit_id: query.depositId, diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 84968f1207c..1ab114e27e9 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -7,43 +7,58 @@ import React from 'react'; import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; import moment from 'moment'; -import { Card } from '@wordpress/components'; +import { + Card, + CardBody, + CardHeader, + ExternalLink, + // @ts-expect-error: Suppressing Module '"@wordpress/components"' has no exported member '__experimentalText'. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- used by TableCard component which we replicate here. + __experimentalText as Text, +} from '@wordpress/components'; import { SummaryListPlaceholder, SummaryList, OrderStatus, } from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; import classNames from 'classnames'; /** * Internal dependencies. */ -import { useDeposit } from 'wcpay/data'; -import { displayStatus } from '../strings'; +import type { CachedDeposit } from 'types/deposits'; +import { useDeposit } from 'data'; import TransactionsList from 'transactions/list'; import Page from 'components/page'; import ErrorBoundary from 'components/error-boundary'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import { formatCurrency, formatExplicitCurrency } from 'utils/currency'; +import { displayStatus } from '../strings'; import './style.scss'; -import { CachedDeposit } from 'wcpay/types/deposits'; -const Status = ( { status }: { status: string } ): JSX.Element => ( - // Re-purpose order status indicator for deposit status. +/** + * Renders the deposit status indicator UI, re-purposing the OrderStatus component from @woocommerce/components. + */ +const Status: React.FC< { status: string } > = ( { status } ) => ( ); -// Custom SummaryNumber with custom value className reusing @woocommerce/components styles. -const SummaryItem = ( { - label, - value, - valueClass, - detail, -}: { +interface SummaryItemProps { label: string; value: string | JSX.Element; valueClass?: string | false; detail?: string; +} + +/** + * A custom SummaryNumber with custom value className, reusing @woocommerce/components styles. + */ +const SummaryItem: React.FC< SummaryItemProps > = ( { + label, + value, + valueClass, + detail, } ) => (
  • @@ -65,15 +80,13 @@ const SummaryItem = ( {
  • ); -export const DepositOverview = ( { - depositId, -}: { - depositId: string; -} ): JSX.Element => { - const { deposit = {} as CachedDeposit, isLoading } = useDeposit( - depositId - ); +interface DepositOverviewProps { + deposit: CachedDeposit; +} +export const DepositOverview: React.FC< DepositOverviewProps > = ( { + deposit, +} ) => { const depositDateLabel = deposit.automatic ? __( 'Deposit date', 'woocommerce-payments' ) : __( 'Instant deposit date', 'woocommerce-payments' ); @@ -94,7 +107,6 @@ export const DepositOverview = ( { /> ); - if ( isLoading ) return ; return (
    { deposit.automatic ? ( @@ -160,20 +172,64 @@ export const DepositOverview = ( { ); }; -export const DepositDetails = ( { +interface DepositDetailsProps { + query: { + id: string; + }; +} + +export const DepositDetails: React.FC< DepositDetailsProps > = ( { query: { id: depositId }, -}: { - query: { id: string }; -} ): JSX.Element => ( - - - - - - - - - -); +} ) => { + const { deposit, isLoading } = useDeposit( depositId ); + + const isInstantDeposit = ! isLoading && deposit && ! deposit.automatic; + + return ( + + + + { isLoading ? ( + + ) : ( + + ) } + + + + { isInstantDeposit ? ( + // If instant deposit, show a message instead of the transactions list. + // Matching the components used in @woocommerce/components TableCard for consistent UI. + + + + { __( + 'Deposit transactions', + 'woocommerce-payments' + ) } + + + + { interpolateComponents( { + /* Translators: {{learnMoreLink}} is a link element (). */ + mixedString: __( + `We're unable to show transaction history on instant deposits. {{learnMoreLink}}Learn more{{/learnMoreLink}}`, + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + + ), + }, + } ) } + + + ) : ( + + ) } + + + ); +}; export default DepositDetails; diff --git a/client/deposits/details/style.scss b/client/deposits/details/style.scss index 8f6fc09dda4..01e91c29955 100644 --- a/client/deposits/details/style.scss +++ b/client/deposits/details/style.scss @@ -63,4 +63,10 @@ line-height: 82px; } } + + &--instant { + &__transactions-list-message.components-card__body { + padding: 24px; + } + } } diff --git a/client/deposits/details/test/index.tsx b/client/deposits/details/test/index.tsx index cd1bd3abc75..ce8e480760f 100644 --- a/client/deposits/details/test/index.tsx +++ b/client/deposits/details/test/index.tsx @@ -9,15 +9,8 @@ import React from 'react'; /** * Internal dependencies */ +import type { CachedDeposit } from 'types/deposits'; import { DepositOverview } from '../'; -import { useDeposit } from 'wcpay/data'; -import { CachedDeposit } from 'wcpay/types/deposits'; - -jest.mock( 'wcpay/data', () => ( { - useDeposit: jest.fn(), -} ) ); - -const mockUseDeposit = useDeposit as jest.MockedFunction< typeof useDeposit >; const mockDeposit = { id: 'po_mock', @@ -39,6 +32,7 @@ declare const global: { country: string; }; }; + wcSettings: { countries: Record< string, string > }; }; describe( 'Deposit overview', () => { @@ -63,25 +57,15 @@ describe( 'Deposit overview', () => { } ); test( 'renders automatic deposit correctly', () => { - mockUseDeposit.mockReturnValue( { - deposit: mockDeposit, - isLoading: false, - } ); - const { container: overview } = render( - + ); expect( overview ).toMatchSnapshot(); } ); test( 'renders instant deposit correctly', () => { - mockUseDeposit.mockReturnValue( { - deposit: { ...mockDeposit, automatic: false }, - isLoading: false, - } ); - const { container: overview } = render( - + ); expect( overview ).toMatchSnapshot(); } ); diff --git a/client/deposits/filters/test/__snapshots__/index.js.snap b/client/deposits/filters/test/__snapshots__/index.js.snap index 505550cbfb6..5f02b248a6c 100644 --- a/client/deposits/filters/test/__snapshots__/index.js.snap +++ b/client/deposits/filters/test/__snapshots__/index.js.snap @@ -27,10 +27,5 @@ HTMLOptionsCollection [ > Failed , - , ] `; diff --git a/client/deposits/filters/test/index.js b/client/deposits/filters/test/index.js index b89278b01dc..ba69a1ab7fe 100644 --- a/client/deposits/filters/test/index.js +++ b/client/deposits/filters/test/index.js @@ -172,19 +172,6 @@ describe( 'Deposits filters', () => { expect( getQuery().status_is ).toEqual( 'failed' ); } ); - - test( 'should filter by estimated', () => { - user.selectOptions( ruleSelector, 'is' ); - - // need to include $ in name, otherwise "Select a deposit status filter" is also matched. - user.selectOptions( - screen.getByRole( 'combobox', { name: /deposit status$/i } ), - 'estimated' - ); - user.click( screen.getByRole( 'link', { name: /Filter/ } ) ); - - expect( getQuery().status_is ).toEqual( 'estimated' ); - } ); } ); function addAdvancedFilter( filter ) { diff --git a/client/deposits/index.js b/client/deposits/index.tsx similarity index 58% rename from client/deposits/index.js rename to client/deposits/index.tsx index c1528092bc2..ae5ba238679 100644 --- a/client/deposits/index.js +++ b/client/deposits/index.tsx @@ -3,18 +3,19 @@ /** * External dependencies */ +import React from 'react'; /** * Internal dependencies. */ import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import DepositsList from './list'; -const DepositsPage = () => { +const DepositsPage: React.FC = () => { return ( - + ); diff --git a/client/deposits/strings.ts b/client/deposits/strings.ts index a2ea34690aa..f4cbfbfb3a6 100644 --- a/client/deposits/strings.ts +++ b/client/deposits/strings.ts @@ -5,16 +5,21 @@ */ import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ + +import type { DepositStatus } from 'wcpay/types/deposits'; + export const displayType = { deposit: __( 'Deposit', 'woocommerce-payments' ), withdrawal: __( 'Withdrawal', 'woocommerce-payments' ), }; -export const displayStatus = { +export const displayStatus: Record< DepositStatus, string > = { paid: __( 'Paid', 'woocommerce-payments' ), pending: __( 'Pending', 'woocommerce-payments' ), in_transit: __( 'In transit', 'woocommerce-payments' ), canceled: __( 'Canceled', 'woocommerce-payments' ), failed: __( 'Failed', 'woocommerce-payments' ), - estimated: __( 'Estimated', 'woocommerce-payments' ), }; diff --git a/client/disputes/evidence/index.js b/client/disputes/evidence/index.js index e25fa89f09f..0d4cf5b0c0a 100644 --- a/client/disputes/evidence/index.js +++ b/client/disputes/evidence/index.js @@ -27,11 +27,11 @@ import '../style.scss'; import { useDisputeEvidence } from 'wcpay/data'; import evidenceFields from './fields'; import { FileUploadControl, UploadedReadOnly } from 'components/file-upload'; +import { TestModeNotice } from 'components/test-mode-notice'; import Info from '../info'; import Page from 'components/page'; import ErrorBoundary from 'components/error-boundary'; import Loadable, { LoadableBlock } from 'components/loadable'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; import useConfirmNavigation from 'utils/use-confirm-navigation'; import wcpayTracks from 'tracks'; import { getAdminUrl } from 'wcpay/utils'; @@ -295,7 +295,6 @@ export const DisputeEvidencePage = ( props ) => { dispute.status !== 'needs_response' && dispute.status !== 'warning_needs_response'; const disputeIsAvailable = ! isLoading && dispute.id; - const testModeNotice = ; const readOnlyNotice = ( { if ( ! isLoading && ! disputeIsAvailable ) { return ( - { testModeNotice } +
    { __( 'Dispute not loaded', 'woocommerce-payments' ) }
    @@ -323,7 +322,7 @@ export const DisputeEvidencePage = ( props ) => { return ( - { testModeNotice } + { readOnly && ! isLoading && readOnlyNotice } diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index 90880e592d2..ed2524d641a 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -30,7 +30,7 @@ import DisputeStatusChip from 'components/dispute-status-chip'; import ClickableCell from 'components/clickable-cell'; import DetailsLink, { getDetailsURL } from 'components/details-link'; import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import { reasons } from './strings'; import { formatStringValue } from 'utils'; import { formatExplicitCurrency } from 'utils/currency'; @@ -508,7 +508,7 @@ export const DisputesList = (): JSX.Element => { return ( - + { return ( - + ); diff --git a/client/globals.d.ts b/client/globals.d.ts index 7132f49949f..eaffe24b8b6 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -84,6 +84,7 @@ declare global { isNewFlowEnabled: boolean; isEnabled: boolean; isComplete: boolean; + isEligibilityModalDismissed: boolean; }; enabledPaymentMethods: string[]; accountDefaultCurrency: string; diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js b/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js index 82bc78c1d52..619e51a01c5 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js @@ -212,6 +212,7 @@ const EnabledCurrenciesModal = ( { className } ) => { variant="secondary" className={ className } onClick={ handleEnabledCurrenciesAddButtonClick } + data-testid="enabled-currencies-add-button" > { __( 'Add/remove currencies', 'woocommerce-payments' ) } 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 72c3958e9cf..b6ffaee1a8e 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 @@ -260,6 +260,7 @@ exports[`Multi-Currency enabled currencies list Enabled currencies list renders > + +
    + + ); +}; + +export default SetupLivePaymentsModal; diff --git a/client/overview/modal/setup-live-payments/style.scss b/client/overview/modal/setup-live-payments/style.scss new file mode 100644 index 00000000000..b4067be8ba1 --- /dev/null +++ b/client/overview/modal/setup-live-payments/style.scss @@ -0,0 +1,43 @@ +.wcpay-setup-real-payments-modal { + color: $gray-900; + fill: $studio-woocommerce-purple-50; + + .components-modal__content { + box-sizing: border-box; + max-width: 600px; + margin: auto; + padding: $gap-smaller $gap-larger $gap-larger; + } + + .components-modal__header { + position: initial; + padding: 0; + border: 0; + + h1 { + @include wp-title-small; + margin-bottom: $gap-smaller; + } + } + + &__title { + @include wp-title-small; + } + + &__headline { + font-weight: 600; + } + + &__content { + display: grid; + grid-template-columns: auto 1fr; + gap: $gap; + padding: $gap-smallest; + align-items: center; + margin-bottom: $gap-large; + } + + &__footer { + @include modal-footer-buttons; + } +} diff --git a/client/overview/modal/setup-live-payments/test/index.test.tsx b/client/overview/modal/setup-live-payments/test/index.test.tsx new file mode 100644 index 00000000000..be83254beda --- /dev/null +++ b/client/overview/modal/setup-live-payments/test/index.test.tsx @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import SetupLivePaymentsModal from '../index'; + +jest.mock( '@wordpress/data', () => ( { + useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), +} ) ); + +declare const global: { + wcpaySettings: { + connectUrl: string; + }; +}; + +describe( 'Setup Live Payments Modal', () => { + global.wcpaySettings = { + connectUrl: 'https://wcpay.test/connect', + }; + + it( 'modal is open by default', () => { + render( jest.fn() } /> ); + + expect( + screen.queryByText( + 'Before proceeding, please take note of the following information:' + ) + ).toBeInTheDocument(); + } ); + + it( 'calls `handleSetup` when setup button is clicked', () => { + Object.defineProperty( window, 'location', { + configurable: true, + enumerable: true, + value: new URL( window.location.href ), + } ); + + render( jest.fn() } /> ); + + user.click( + screen.getByRole( 'button', { + name: 'Continue setup', + } ) + ); + + expect( window.location.href ).toBe( + `https://wcpay.test/connect?wcpay-disable-onboarding-test-mode=true` + ); + } ); +} ); diff --git a/client/overview/setup-real-payments.tsx b/client/overview/setup-real-payments.tsx deleted file mode 100644 index 50a08d8c069..00000000000 --- a/client/overview/setup-real-payments.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/** - * External dependencies - */ -import React, { useState } from 'react'; -import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; -import { - Button, - Card, - CardBody, - CardFooter, - CardHeader, - Modal, - Flex, -} from '@wordpress/components'; -import { Icon, payment, globe, currencyDollar } from '@wordpress/icons'; -import ScheduledIcon from 'gridicons/dist/scheduled'; - -/** - * Internal dependencies - */ -import BlockEmbedIcon from 'components/icons/block-embed'; -import BlockPostAuthorIcon from 'components/icons/block-post-author'; - -const SetupRealPayments: React.FC = () => { - const [ modalVisible, setModalVisible ] = useState( false ); - - const handleContinue = () => { - window.location.href = addQueryArgs( wcpaySettings.connectUrl, { - 'wcpay-disable-onboarding-test-mode': true, - } ); - }; - - return ( - <> - - - { __( - 'Set up real payments on your store', - 'woocommerce-payments' - ) } - - -
    - - { __( - 'Offer a wide range of card payments', - 'woocommerce-payments' - ) } -
    -
    - - { __( - '135 different currencies and local payment methods', - 'woocommerce-payments' - ) } -
    -
    - - { __( - 'Enjoy direct deposits into your bank account', - 'woocommerce-payments' - ) } -
    -
    - - - - - -
    - { modalVisible && ( - setModalVisible( false ) } - > -

    - { __( - 'Before proceeding, please take note of the following information:', - 'woocommerce-payments' - ) } -

    -
    - - { __( - 'Your test account will be deactivated and your transaction records will be preserved for future reference.', - 'woocommerce-payments' - ) } - - { __( - 'The owner, business and contact information will be required.', - 'woocommerce-payments' - ) } - - { __( - 'We will need your banking details in order to process any payouts to you.', - 'woocommerce-payments' - ) } -
    -
    - - -
    -
    - ) } - - ); -}; - -export default SetupRealPayments; diff --git a/client/overview/strings.tsx b/client/overview/strings.tsx new file mode 100644 index 00000000000..43ab5d47bb5 --- /dev/null +++ b/client/overview/strings.tsx @@ -0,0 +1,45 @@ +/* eslint-disable max-len */ +/** + * External dependencies + */ +import interpolateComponents from '@automattic/interpolate-components'; +import { __, sprintf } from '@wordpress/i18n'; +import React from 'react'; + +export default { + notice: { + content: { + test: interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{bold}}%1s is in test mode.{{bold /}}. All transactions will be simulated.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + bold: , + }, + } ), + dev: interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{bold}}%1s is in dev mode.{{bold /}}. You need to set up a live %1s account before you can accept real transactions.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + bold: , + }, + } ), + }, + actions: { + goLive: __( 'Ready to go live?', 'woocommerce-payments' ), + setUpPayments: __( 'Set up payments', 'woocommerce-payments' ), + learnMore: __( 'Learn more', 'woocommerce-payments' ), + }, + }, +}; diff --git a/client/overview/style.scss b/client/overview/style.scss index 201452431f5..4dbe66d79e7 100644 --- a/client/overview/style.scss +++ b/client/overview/style.scss @@ -112,47 +112,3 @@ } } } - -.wcpay-setup-real-payments-modal { - color: $gray-900; - fill: $studio-woocommerce-purple-50; - - .components-modal__content { - box-sizing: border-box; - max-width: 600px; - margin: auto; - padding: $gap-smaller $gap-larger $gap-larger; - } - - .components-modal__header { - position: initial; - padding: 0; - border: 0; - - h1 { - @include wp-title-small; - margin-bottom: $gap-smaller; - } - } - - &__title { - @include wp-title-small; - } - - &__headline { - font-weight: 600; - } - - &__content { - display: grid; - grid-template-columns: auto 1fr; - gap: $gap; - padding: $gap-smallest; - align-items: center; - margin-bottom: $gap-large; - } - - &__footer { - @include modal-footer-buttons; - } -} diff --git a/client/overview/test/index.js b/client/overview/test/index.js index adc214b993a..2c677e27e31 100644 --- a/client/overview/test/index.js +++ b/client/overview/test/index.js @@ -295,32 +295,6 @@ describe( 'Overview page', () => { ).toBeInTheDocument(); } ); - it( 'displays SetupRealPayments if onboardingTestMode is true', () => { - global.wcpaySettings = { - ...global.wcpaySettings, - onboardingTestMode: true, - }; - - render( ); - - expect( - screen.getByText( 'Set up real payments on your store' ) - ).toBeInTheDocument(); - } ); - - it( 'does not displays SetupRealPayments if onboardingTestMode is false', () => { - global.wcpaySettings = { - ...global.wcpaySettings, - onboardingTestMode: false, - }; - - render( ); - - expect( - screen.queryByText( 'Set up real payments on your store' ) - ).not.toBeInTheDocument(); - } ); - it( 'displays ProgressiveOnboardingEligibilityModal if showProgressiveOnboardingEligibilityModal is true', () => { getQuery.mockReturnValue( { 'wcpay-connection-success': '1' } ); diff --git a/client/overview/test/setup-real-payments.test.tsx b/client/overview/test/setup-real-payments.test.tsx deleted file mode 100644 index 25ce482ddfb..00000000000 --- a/client/overview/test/setup-real-payments.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import user from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import SetupRealPayments from '../setup-real-payments'; - -declare const global: { - wcpaySettings: { - connectUrl: string; - }; -}; - -describe( 'Setup Real Payments', () => { - it( 'opens modal when set up payments button is clicked', () => { - render( ); - - const queryHeading = () => - screen.queryByRole( 'heading', { - name: 'Set up live payments on your store', - } ); - - expect( queryHeading() ).not.toBeInTheDocument(); - - user.click( screen.getByRole( 'button' ) ); - - expect( queryHeading() ).toBeInTheDocument(); - } ); - - it( 'closes modal when cancel button is clicked', () => { - render( ); - - user.click( screen.getByRole( 'button' ) ); - user.click( - screen.getByRole( 'button', { - name: 'Cancel', - } ) - ); - - expect( - screen.queryByRole( 'heading', { - name: 'Set up live payments on your store', - } ) - ).not.toBeInTheDocument(); - } ); - - it( 'calls handleContinue when continue setup button is clicked', () => { - global.wcpaySettings = { - connectUrl: 'https://wcpay.test/connect', - }; - - Object.defineProperty( window, 'location', { - configurable: true, - enumerable: true, - value: new URL( window.location.href ), - } ); - - render( ); - - user.click( screen.getByRole( 'button' ) ); - user.click( - screen.getByRole( 'button', { - name: 'Continue setup', - } ) - ); - - expect( window.location.href ).toBe( - `https://wcpay.test/connect?wcpay-disable-onboarding-test-mode=true` - ); - } ); -} ); diff --git a/client/payment-details/order-details/test/index.test.tsx b/client/payment-details/order-details/test/index.test.tsx index e4ca6cd55be..e90265721d6 100644 --- a/client/payment-details/order-details/test/index.test.tsx +++ b/client/payment-details/order-details/test/index.test.tsx @@ -65,6 +65,8 @@ const chargeFromOrderMock = { url: 'http://wcpay.test/wp-admin/post.php?post=776&action=edit', customer_url: 'admin.php?page=wc-admin&path=/customers&filter=single_customer&customers=55', + customer_name: '', + customer_email: '', subscriptions: [], fraud_meta_box_type: 'succeeded', }, diff --git a/client/payment-details/payment-details/index.tsx b/client/payment-details/payment-details/index.tsx index d6a55279a8a..cdf568520f2 100644 --- a/client/payment-details/payment-details/index.tsx +++ b/client/payment-details/payment-details/index.tsx @@ -7,7 +7,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { TestModeNotice, topics } from '../../components/test-mode-notice'; +import { TestModeNotice } from '../../components/test-mode-notice'; import Page from '../../components/page'; import { Card, CardBody } from '@wordpress/components'; import ErrorBoundary from '../../components/error-boundary'; @@ -37,13 +37,11 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { showTimeline = true, paymentIntent, } ) => { - const testModeNotice = ; - // Check instance of error because its default value is empty object if ( ! isLoading && error instanceof Error ) { return ( - { testModeNotice } + { __( @@ -58,8 +56,7 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { return ( - { testModeNotice } - + { @@ -26,13 +26,12 @@ const PaymentCardReaderChargeDetails = ( props ) => { props.chargeId, props.transactionId ); - const testModeNotice = ; // Check instance of chargeError because its default value is empty object if ( ! isLoading && chargeError instanceof Error ) { return ( - { testModeNotice } + { __( @@ -56,7 +55,6 @@ const PaymentCardReaderChargeDetails = ( props ) => { const RenderPaymentCardReaderChargeDetails = ( props ) => { const readers = props.readers; const isLoading = props.isLoading; - const testModeNotice = ; const headers = [ { @@ -128,7 +126,7 @@ const RenderPaymentCardReaderChargeDetails = ( props ) => { const downloadable = !! rows.length; return ( - { testModeNotice } + , + content: ( + + ), }, { title: __( 'Order', 'woocommerce-payments' ), diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index 91fb76fb187..49b74106f6d 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -70,7 +70,7 @@ const getDepositTimelineItem = ( body = [] ) => { let headline = ''; - if ( event.deposit ) { + if ( event.deposit && ! event.deposit.id.includes( 'wcpay_estimated_' ) ) { headline = sprintf( isPositive ? // translators: %1$s - formatted amount, %2$s - deposit arrival date,
    - link to the deposit @@ -89,7 +89,6 @@ const getDepositTimelineItem = ( moment( event.deposit.arrival_date * 1000 ).toISOString() ) ); - const depositUrl = getAdminUrl( { page: 'wc-admin', path: '/payments/deposits/details', @@ -136,7 +135,7 @@ const getDepositTimelineItem = ( */ const getFinancingPaydownTimelineItem = ( event, formattedAmount, body ) => { let headline = ''; - if ( event.deposit ) { + if ( event.deposit && ! event.deposit.id.includes( 'wcpay_estimated_' ) ) { headline = sprintf( // translators: %1$s - formatted amount, %2$s - deposit arrival date, - link to the deposit __( diff --git a/client/payment-request/index.js b/client/payment-request/index.js index 2eb36640050..9578b42517c 100644 --- a/client/payment-request/index.js +++ b/client/payment-request/index.js @@ -1,7 +1,8 @@ -/* global jQuery, wcpayPaymentRequestParams, wcpayPaymentRequestPayForOrderParams, wc_add_to_cart_variation_params */ +/* global jQuery, wcpayPaymentRequestParams, wcpayPaymentRequestPayForOrderParams */ /** * External dependencies */ +import { __ } from '@wordpress/i18n'; import { doAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -68,6 +69,11 @@ jQuery( ( $ ) => { * Object to handle Stripe payment forms. */ const wcpayPaymentRequest = { + /** + * Whether the payment was aborted by the customer. + */ + paymentAborted: false, + getAttributes: function () { const select = $( '.variations_form' ).find( '.variations select' ); const data = {}; @@ -237,6 +243,10 @@ jQuery( ( $ ) => { wcpayPaymentRequest.showPaymentRequestButton( prButton ); } ); + paymentRequest.on( 'cancel', () => { + wcpayPaymentRequest.paymentAborted = true; + } ); + paymentRequest.on( 'shippingaddresschange', ( event ) => shippingAddressChangeHandler( api, event ) ); @@ -374,13 +384,19 @@ jQuery( ( $ ) => { addToCartButton.is( '.wc-variation-is-unavailable' ) ) { window.alert( - wc_add_to_cart_variation_params.i18n_unavailable_text + window?.wc_add_to_cart_variation_params + ?.i18n_unavailable_text || + __( + 'Sorry, this product is unavailable. Please choose a different combination.', + 'woocommerce-payments' + ) ); - } else if ( - addToCartButton.is( '.wc-variation-selection-needed' ) - ) { + } else { window.alert( - wc_add_to_cart_variation_params.i18n_make_a_selection_text + __( + 'Please select your product options before proceeding.', + 'woocommerce-payments' + ) ); } return; @@ -400,11 +416,27 @@ jQuery( ( $ ) => { $.when( wcpayPaymentRequest.getSelectedProductData() ) .then( ( response ) => { - // If a variation doesn't need shipping, re-init the `wcpayPaymentRequest` with response params. + /** + * If the customer aborted the payment request, we need to re init the payment request button to ensure the shipping + * options are refetched. If the customer didn't abort the payment request, and the product's shipping status is + * consistent, we can simply update the payment request button with the new total and display items. + */ if ( - wcpayPaymentRequestParams.product.needs_shipping !== - response.needs_shipping + ! wcpayPaymentRequest.paymentAborted && + wcpayPaymentRequestParams.product.needs_shipping === + response.needs_shipping ) { + paymentRequest.update( { + total: response.total, + displayItems: response.displayItems, + } ); + } else { + /** + * Re init the payment request button. + * + * This ensures that when the customer clicks on the payment button, the available shipping options are + * refetched based on the selected variable product's data and the chosen address. + */ wcpayPaymentRequestParams.product.needs_shipping = response.needs_shipping; wcpayPaymentRequestParams.product.total = @@ -412,25 +444,9 @@ jQuery( ( $ ) => { wcpayPaymentRequestParams.product.displayItems = response.displayItems; wcpayPaymentRequest.init(); - wcpayPaymentRequest.unblockPaymentRequestButton(); - } else { - const responseTotal = response.total; - - // If a variation `needs_shipping` is `false`, the `pending` param needs to be set to `false`. - // Because the additional shipping address call is not executed to set the pending to `false`. - if ( response.needs_shipping === false ) { - responseTotal.pending = false; - } - - $.when( - paymentRequest.update( { - total: responseTotal, - displayItems: response.displayItems, - } ) - ).then( () => { - wcpayPaymentRequest.unblockPaymentRequestButton(); - } ); } + + wcpayPaymentRequest.unblockPaymentRequestButton(); } ) .catch( () => { wcpayPaymentRequest.hide(); @@ -561,6 +577,9 @@ jQuery( ( $ ) => { } ); } ); } + + // After initializing a new payment request, we need to reset the paymentAborted flag. + wcpayPaymentRequest.paymentAborted = false; }, }; 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 b55a2af7cac..b56f06a3d58 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 504b6a1a689..89a0005e954 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 - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-toggle.tsx b/client/settings/advanced-settings/stripe-billing-toggle.tsx index f41ea396a69..a340fabbc88 100644 --- a/client/settings/advanced-settings/stripe-billing-toggle.tsx +++ b/client/settings/advanced-settings/stripe-billing-toggle.tsx @@ -56,7 +56,7 @@ const StripeBillingToggle: React.FC< Props > = ( { onChange } ) => { 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 e08216cbec1..187feff74e1 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -54,7 +54,7 @@ const WCPaySubscriptionsToggle = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/express-checkout-settings/general-payment-request-button-settings.js b/client/settings/express-checkout-settings/general-payment-request-button-settings.js index 773562d7dd2..c5a3f231b69 100644 --- a/client/settings/express-checkout-settings/general-payment-request-button-settings.js +++ b/client/settings/express-checkout-settings/general-payment-request-button-settings.js @@ -40,11 +40,11 @@ const buttonSizeOptions = [ { label: makeButtonSizeText( __( - 'Default {{helpText}}(40 px){{/helpText}}', + 'Small {{helpText}}(40 px){{/helpText}}', 'woocommerce-payments' ) ), - value: 'default', + value: 'small', }, { label: makeButtonSizeText( 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 ed5a865fee0..da09e6c7cbd 100644 --- a/client/settings/express-checkout-settings/payment-request-button-preview.js +++ b/client/settings/express-checkout-settings/payment-request-button-preview.js @@ -56,7 +56,7 @@ const BrowserHelpText = () => { }; const buttonSizeToPxMap = { - default: 40, + small: 40, medium: 48, large: 56, }; @@ -120,7 +120,7 @@ const PaymentRequestButtonPreview = () => { theme: theme, height: `${ buttonSizeToPxMap[ size ] || - buttonSizeToPxMap.default + buttonSizeToPxMap.medium }px`, size, } } @@ -142,7 +142,7 @@ const PaymentRequestButtonPreview = () => { theme: theme, height: `${ buttonSizeToPxMap[ size ] || - buttonSizeToPxMap.default + buttonSizeToPxMap.medium }px`, }, }, diff --git a/client/settings/express-checkout-settings/test/index.js b/client/settings/express-checkout-settings/test/index.js index 73d6af2e49d..b9b55537fb0 100644 --- a/client/settings/express-checkout-settings/test/index.js +++ b/client/settings/express-checkout-settings/test/index.js @@ -24,7 +24,7 @@ jest.mock( '../../../data', () => ( { useWooPayCustomMessage: jest.fn().mockReturnValue( [ 'test', jest.fn() ] ), useWooPayStoreLogo: jest.fn().mockReturnValue( [ 'test', jest.fn() ] ), usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'buy' ] ), - usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), + usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayLocations: jest .fn() diff --git a/client/settings/express-checkout-settings/test/payment-request-settings.test.js b/client/settings/express-checkout-settings/test/payment-request-settings.test.js index d0d4360b6b1..3b21c6bf3a6 100644 --- a/client/settings/express-checkout-settings/test/payment-request-settings.test.js +++ b/client/settings/express-checkout-settings/test/payment-request-settings.test.js @@ -24,7 +24,7 @@ jest.mock( '../../../data', () => ( { usePaymentRequestEnabledSettings: jest.fn(), usePaymentRequestLocations: jest.fn(), usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'buy' ] ), - usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), + usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayEnabledSettings: jest.fn(), useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), @@ -147,7 +147,7 @@ describe( 'PaymentRequestSettings', () => { // confirm default values expect( screen.getByLabelText( 'Buy with' ) ).toBeChecked(); - expect( screen.getByLabelText( 'Default (40 px)' ) ).toBeChecked(); + expect( screen.getByLabelText( 'Small (40 px)' ) ).toBeChecked(); expect( screen.getByLabelText( /Dark/ ) ).toBeChecked(); } ); @@ -191,7 +191,7 @@ describe( 'PaymentRequestSettings', () => { setButtonTypeMock, ] ); usePaymentRequestButtonSize.mockReturnValue( [ - 'default', + 'small', setButtonSizeMock, ] ); usePaymentRequestButtonTheme.mockReturnValue( [ diff --git a/client/settings/express-checkout-settings/test/woopay-settings.test.js b/client/settings/express-checkout-settings/test/woopay-settings.test.js index fef25431a56..5846e397ad6 100644 --- a/client/settings/express-checkout-settings/test/woopay-settings.test.js +++ b/client/settings/express-checkout-settings/test/woopay-settings.test.js @@ -94,7 +94,7 @@ describe( 'WooPaySettings', () => { ); usePaymentRequestButtonSize.mockReturnValue( - getMockPaymentRequestButtonSize( [ 'default' ], jest.fn() ) + getMockPaymentRequestButtonSize( [ 'small' ], jest.fn() ) ); usePaymentRequestButtonTheme.mockReturnValue( diff --git a/client/settings/general-settings/index.js b/client/settings/general-settings/index.js index 0ba33e13592..23512d7a66c 100644 --- a/client/settings/general-settings/index.js +++ b/client/settings/general-settings/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import React from 'react'; +import React, { useState } from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { Card, CheckboxControl } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; @@ -11,75 +11,129 @@ import interpolateComponents from '@automattic/interpolate-components'; */ import { useDevMode, useIsWCPayEnabled, useTestMode } from 'wcpay/data'; import CardBody from '../card-body'; +import InlineNotice from 'wcpay/components/inline-notice'; +import SetupLivePaymentsModal from 'wcpay/overview/modal/setup-live-payments'; const GeneralSettings = () => { const [ isWCPayEnabled, setIsWCPayEnabled ] = useIsWCPayEnabled(); const [ isEnabled, updateIsTestModeEnabled ] = useTestMode(); + const [ modalVisible, setModalVisible ] = useState( false ); const isDevModeEnabled = useDevMode(); return ( - - - + + + + { ! isDevModeEnabled && ( + <> +

    + { __( 'Test mode', 'woocommerce-payments' ) } +

    + + ), + learnMoreLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content +
    + ), + }, + } ) } + /> + ) } - help={ sprintf( - /* translators: %s: WooPayments */ - __( - 'When enabled, payment methods powered by %s will appear on checkout.', - 'woocommerce-payments' - ), - 'WooPayments' + { isDevModeEnabled && ( + { + setModalVisible( true ); + }, + }, + ] } + className="wcpay-general-settings__notice" + > + + { interpolateComponents( { + mixedString: sprintf( + /* translators: %s: WooPayments */ + __( + '{{b}}%1$s is in dev mode.{{/b}} You need to set up a live %1$s account before ' + + 'you can accept real transactions. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + b: , + learnMoreLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ) } + + + { modalVisible && ( + setModalVisible( false ) } /> -

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

    - - ), - learnMoreLink: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content -
    - ), - }, - } ) } - /> - - + ) } + ); }; diff --git a/client/transactions/declarations.d.ts b/client/transactions/declarations.d.ts index 3ff75a40ef2..07b2db0955c 100644 --- a/client/transactions/declarations.d.ts +++ b/client/transactions/declarations.d.ts @@ -129,6 +129,12 @@ declare module '@woocommerce/navigation' { type_is_not?: unknown; source_device_is?: unknown; source_device_is_not?: unknown; + channel_is?: string; + channel_is_not?: string; + customer_country_is?: string; + customer_country_is_not?: string; + risk_level_is?: string; + risk_level_is_not?: string; customer_currency_is?: unknown; customer_currency_is_not?: unknown; store_currency_is?: string; diff --git a/client/transactions/filters/config.ts b/client/transactions/filters/config.ts index a79bce31560..b6e3c34f534 100644 --- a/client/transactions/filters/config.ts +++ b/client/transactions/filters/config.ts @@ -7,7 +7,12 @@ import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies */ -import { displayType, sourceDevice } from 'transactions/strings'; +import { + displayType, + sourceDevice, + channel, + riskLevel, +} from 'transactions/strings'; interface TransactionsFilterEntryType { label: string; @@ -59,6 +64,24 @@ const transactionSourceDeviceOptions = Object.entries( sourceDevice ).map( } ); +const transactionChannelOptions = Object.entries( channel ).map( + ( [ type, label ] ) => { + return { label, value: type }; + } +); + +const transactionRiskLevelOptions = Object.entries( riskLevel ).map( + ( [ type, label ] ) => { + return { label, value: type }; + } +); + +const transactionCustomerCounryOptions = Object.entries( + wcSettings.countries +).map( ( [ type, label ] ) => { + return { label, value: type }; +} ); + export const getFilters = ( depositCurrencyOptions: TransactionsFilterEntryType[], showDepositCurrencyFilter: boolean @@ -81,6 +104,12 @@ export const getFilters = ( 'date_between', 'source_device_is', 'source_device_is_not', + 'channel_is', + 'channel_is_not', + 'customer_country_is', + 'customer_country_is_not', + 'risk_level_is', + 'risk_level_is_not', ], showFilters: () => showDepositCurrencyFilter, filters: [ @@ -368,6 +397,154 @@ export const getAdvancedFilters = ( options: transactionSourceDeviceOptions, }, }, + channel: { + labels: { + add: __( 'Channel', 'woocommerce-payments' ), + remove: __( + 'Remove transaction channel filter', + 'woocommerce-payments' + ), + rule: __( + 'Select a transaction channel filter match', + 'woocommerce-payments' + ), + /* translators: A sentence describing a Transaction Channel filter. */ + title: + wooCommerceVersion < 7.8 + ? __( + '{{title}}Channel{{/title}} {{rule /}} {{filter /}}', + 'woocommerce-payments' + ) + : __( + 'Channel ', + 'woocommerce-payments' + ), + filter: __( + 'Select a transaction channel', + 'woocommerce-payments' + ), + }, + rules: [ + { + value: 'is', + /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction channel type. */ + label: _x( 'Is', 'Channel', 'woocommerce-payments' ), + }, + { + value: 'is_not', + /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction channel type. */ + label: _x( + 'Is not', + 'Channel', + 'woocommerce-payments' + ), + }, + ], + input: { + component: 'SelectControl', + options: transactionChannelOptions, + }, + }, + customer_country: { + labels: { + add: __( 'Customer Country', 'woocommerce-payments' ), + remove: __( + 'Remove transaction customer country filter', + 'woocommerce-payments' + ), + rule: __( + 'Select a transaction customer country filter match', + 'woocommerce-payments' + ), + /* translators: A sentence describing a Transaction customer country. */ + title: + wooCommerceVersion < 7.8 + ? __( + '{{title}}Customer country{{/title}} {{rule /}} {{filter /}}', + 'woocommerce-payments' + ) + : __( + 'Customer country ', + 'woocommerce-payments' + ), + filter: __( + 'Select a transaction customer country', + 'woocommerce-payments' + ), + }, + rules: [ + { + value: 'is', + /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction customer country. */ + label: _x( + 'Is', + 'Customer Country', + 'woocommerce-payments' + ), + }, + { + value: 'is_not', + /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction customer country. */ + label: _x( + 'Is not', + 'Customer Country', + 'woocommerce-payments' + ), + }, + ], + input: { + component: 'SelectControl', + options: transactionCustomerCounryOptions, + }, + }, + risk_level: { + labels: { + add: __( 'Risk Level', 'woocommerce-payments' ), + remove: __( + 'Remove transaction Risk Level filter', + 'woocommerce-payments' + ), + rule: __( + 'Select a transaction Risk Level filter match', + 'woocommerce-payments' + ), + /* translators: A sentence describing a Transaction Risk Level filter. */ + title: + wooCommerceVersion < 7.8 + ? __( + '{{title}}Risk Level{{/title}} {{rule /}} {{filter /}}', + 'woocommerce-payments' + ) + : __( + 'Risk Level ', + 'woocommerce-payments' + ), + filter: __( + 'Select a transaction Risk Level', + 'woocommerce-payments' + ), + }, + rules: [ + { + value: 'is', + /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction risk level. */ + label: _x( 'Is', 'Risk Level', 'woocommerce-payments' ), + }, + { + value: 'is_not', + /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction risk level. */ + label: _x( + 'Is not', + 'Risk Level', + 'woocommerce-payments' + ), + }, + ], + input: { + component: 'SelectControl', + options: transactionRiskLevelOptions, + }, + }, }, }; }; diff --git a/client/transactions/filters/test/__snapshots__/index.tsx.snap b/client/transactions/filters/test/__snapshots__/index.tsx.snap index c1956c8960a..b124ea5379b 100644 --- a/client/transactions/filters/test/__snapshots__/index.tsx.snap +++ b/client/transactions/filters/test/__snapshots__/index.tsx.snap @@ -1,5 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Transactions filters when filtering by channel should render all types 1`] = ` +HTMLOptionsCollection [ + , + , +] +`; + +exports[`Transactions filters when filtering by customer country should render all types 1`] = ` +HTMLOptionsCollection [ + , + , + , +] +`; + exports[`Transactions filters when filtering by customer currency should render all types 1`] = ` HTMLOptionsCollection [ , + , + , +] +`; + exports[`Transactions filters when filtering by source device should render all types 1`] = ` HTMLOptionsCollection [
    @@ -17,17 +15,6 @@ exports[`Deposit renders with date available but no deposit 1`] = `
    `; exports[`Deposit renders with deposit but no date available 1`] = `
    `; -exports[`Deposit renders with estimated date and deposit available 1`] = ` - -`; +exports[`Deposit renders with estimated date and deposit available 1`] = `
    `; exports[`Deposit renders with no date or deposit available 1`] = `
    `; diff --git a/client/transactions/list/test/__snapshots__/index.tsx.snap b/client/transactions/list/test/__snapshots__/index.tsx.snap index 8f9669c6be4..2f460c0f997 100644 --- a/client/transactions/list/test/__snapshots__/index.tsx.snap +++ b/client/transactions/list/test/__snapshots__/index.tsx.snap @@ -767,8 +767,6 @@ exports[`Transactions list renders correctly when can filter by several currenci data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -1700,8 +1698,6 @@ exports[`Transactions list renders correctly when filtered by currency 1`] = ` data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -3336,8 +3332,6 @@ exports[`Transactions list subscription column renders correctly 1`] = ` data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -4314,8 +4308,6 @@ exports[`Transactions list when not filtered by deposit renders correctly 1`] = data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -5289,8 +5281,6 @@ exports[`Transactions list when not filtered by deposit renders table summary on data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx index be1bf5d9fe5..d2706511578 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -102,6 +102,8 @@ const getMockTransactions: () => Transaction[] = () => [ url: 'https://example.com/order/123', // eslint-disable-next-line camelcase customer_url: 'https://example.com/customer/my-name', + customer_name: '', + customer_email: '', }, channel: 'online', source_identifier: '1234', @@ -131,6 +133,8 @@ const getMockTransactions: () => Transaction[] = () => [ url: 'https://example.com/order/125', // eslint-disable-next-line camelcase customer_url: 'https://example.com/customer/my-name', + customer_name: '', + customer_email: '', }, channel: 'online', source_identifier: '1234', @@ -160,6 +164,8 @@ const getMockTransactions: () => Transaction[] = () => [ url: 'https://example.com/order/335', // eslint-disable-next-line camelcase customer_url: 'https://example.com/customer/my-name', + customer_name: '', + customer_email: '', }, channel: 'in_person', source_identifier: '1234', diff --git a/client/transactions/strings.ts b/client/transactions/strings.ts index 9883b8a0b0d..5865f2a0784 100644 --- a/client/transactions/strings.ts +++ b/client/transactions/strings.ts @@ -28,3 +28,16 @@ export const sourceDevice = { android: __( 'Android', 'woocommerce-payments' ), ios: __( 'iPhone', 'woocommerce-payments' ), }; + +// Mapping of transaction channel type string. +export const channel = { + online: __( 'Online', 'woocommerce-payments' ), + in_person: __( 'In-Person', 'woocommerce-payments' ), +}; + +// Mapping of transaction risk level string. +export const riskLevel = { + '0': __( 'Normal', 'woocommerce-payments' ), + '1': __( 'Elevated', 'woocommerce-payments' ), + '2': __( 'Highest', 'woocommerce-payments' ), +}; diff --git a/client/types/deposits.d.ts b/client/types/deposits.d.ts index 9dab619c2c7..ce749a58c30 100644 --- a/client/types/deposits.d.ts +++ b/client/types/deposits.d.ts @@ -43,5 +43,4 @@ export type DepositStatus = | 'pending' | 'in_transit' | 'canceled' - | 'failed' - | 'estimated'; + | 'failed'; diff --git a/client/types/orders.d.ts b/client/types/orders.d.ts index 216f1caa13a..1865fd23c5a 100644 --- a/client/types/orders.d.ts +++ b/client/types/orders.d.ts @@ -15,6 +15,8 @@ interface OrderDetails { number: number; url: string; customer_url: null | string; + customer_email: null | string; + customer_name: null | string; subscriptions?: SubscriptionDetails[]; fraud_meta_box_type?: string; } diff --git a/composer.json b/composer.json index 55a19d72d4b..01444821e33 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "automattic/jetpack-changelogger": "3.3.2", "spatie/phpunit-watcher": "1.23", "woocommerce/qit-cli": "0.3.4", - "slevomat/coding-standard": "8.0.0" + "slevomat/coding-standard": "8.0.0", + "dg/bypass-finals": "1.5.1" }, "scripts": { "test": [ diff --git a/composer.lock b/composer.lock index caa1ac7f326..54c7ccceb4e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,25 +4,25 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "95d672ebb995c8245e0659d2e4d7db3d", + "content-hash": "9dfc107cbaa90afb69bed66432c8cf70", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", - "version": "v1.4.20", + "version": "v1.4.22", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git", - "reference": "6743d34fe7556455e17cbe1b7c90ed39a1f69089" + "reference": "d7fdf2fc7ae33d75e24e82d81269e33ec718446f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/6743d34fe7556455e17cbe1b7c90ed39a1f69089", - "reference": "6743d34fe7556455e17cbe1b7c90ed39a1f69089", + "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/d7fdf2fc7ae33d75e24e82d81269e33ec718446f", + "reference": "d7fdf2fc7ae33d75e24e82d81269e33ec718446f", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", - "yoast/phpunit-polyfills": "1.0.4" + "automattic/jetpack-changelogger": "^3.3.9", + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -49,29 +49,29 @@ ], "description": "Used to record internal usage stats for Automattic. Not visible to site owners.", "support": { - "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v1.4.20" + "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v1.4.22" }, - "time": "2023-04-10T11:43:38+00:00" + "time": "2023-09-19T18:18:33+00:00" }, { "name": "automattic/jetpack-admin-ui", - "version": "v0.2.20", + "version": "v0.2.25", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-admin-ui.git", - "reference": "90f4de6c9d936bbf161f1c2356d98b00ba33576f" + "reference": "d9566f47ab310d675779273eeead6d0ca64fff82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/90f4de6c9d936bbf161f1c2356d98b00ba33576f", - "reference": "90f4de6c9d936bbf161f1c2356d98b00ba33576f", + "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/d9566f47ab310d675779273eeead6d0ca64fff82", + "reference": "d9566f47ab310d675779273eeead6d0ca64fff82", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", - "automattic/jetpack-logo": "^1.6.1", + "automattic/jetpack-changelogger": "^3.3.11", + "automattic/jetpack-logo": "^1.6.3", "automattic/wordbless": "dev-master", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -102,32 +102,32 @@ ], "description": "Generic Jetpack wp-admin UI elements", "support": { - "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.2.20" + "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.2.25" }, - "time": "2023-04-25T15:05:53+00:00" + "time": "2023-11-14T16:36:17+00:00" }, { "name": "automattic/jetpack-assets", - "version": "v1.18.7", + "version": "v1.18.15", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-assets.git", - "reference": "094d19cc5649b8a9e5d12beecee9371ff0ea4279" + "reference": "8b0c0eaf371b17eaf58bc38ab0b12d1a2ac9811e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/094d19cc5649b8a9e5d12beecee9371ff0ea4279", - "reference": "094d19cc5649b8a9e5d12beecee9371ff0ea4279", + "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/8b0c0eaf371b17eaf58bc38ab0b12d1a2ac9811e", + "reference": "8b0c0eaf371b17eaf58bc38ab0b12d1a2ac9811e", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^1.6.22" + "automattic/jetpack-constants": "^1.6.23" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.6", + "automattic/jetpack-changelogger": "^3.3.11", "brain/monkey": "2.6.1", "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -158,9 +158,9 @@ ], "description": "Asset management utilities for Jetpack ecosystem packages", "support": { - "source": "https://github.com/Automattic/jetpack-assets/tree/v1.18.7" + "source": "https://github.com/Automattic/jetpack-assets/tree/v1.18.15" }, - "time": "2023-07-11T05:13:08+00:00" + "time": "2023-11-14T16:36:44+00:00" }, { "name": "automattic/jetpack-autoloader", @@ -325,22 +325,22 @@ }, { "name": "automattic/jetpack-constants", - "version": "v1.6.22", + "version": "v1.6.23", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", - "reference": "7b5c44d763c7b0dd7498be2b41a89bfefe84834c" + "reference": "0825fb1fa94956f26adebc01be0d716a0fd3ade0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/7b5c44d763c7b0dd7498be2b41a89bfefe84834c", - "reference": "7b5c44d763c7b0dd7498be2b41a89bfefe84834c", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/0825fb1fa94956f26adebc01be0d716a0fd3ade0", + "reference": "0825fb1fa94956f26adebc01be0d716a0fd3ade0", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", + "automattic/jetpack-changelogger": "^3.3.8", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -367,9 +367,9 @@ ], "description": "A wrapper for defining constants in a more testable way.", "support": { - "source": "https://github.com/Automattic/jetpack-constants/tree/v1.6.22" + "source": "https://github.com/Automattic/jetpack-constants/tree/v1.6.23" }, - "time": "2023-04-10T11:43:45+00:00" + "time": "2023-08-23T17:56:35+00:00" }, { "name": "automattic/jetpack-identity-crisis", @@ -432,22 +432,22 @@ }, { "name": "automattic/jetpack-ip", - "version": "v0.1.4", + "version": "v0.1.6", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-ip.git", - "reference": "fde10bea279aca8adbae9d7ae27d971da3a932e3" + "reference": "39a3b6084336a0a76e4f95f83c2306102e46990e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/fde10bea279aca8adbae9d7ae27d971da3a932e3", - "reference": "fde10bea279aca8adbae9d7ae27d971da3a932e3", + "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/39a3b6084336a0a76e4f95f83c2306102e46990e", + "reference": "39a3b6084336a0a76e4f95f83c2306102e46990e", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.4", + "automattic/jetpack-changelogger": "^3.3.9", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -478,27 +478,27 @@ ], "description": "Utilities for working with IP addresses.", "support": { - "source": "https://github.com/Automattic/jetpack-ip/tree/v0.1.4" + "source": "https://github.com/Automattic/jetpack-ip/tree/v0.1.6" }, - "time": "2023-05-29T19:04:13+00:00" + "time": "2023-09-19T18:18:29+00:00" }, { "name": "automattic/jetpack-logo", - "version": "v1.6.1", + "version": "v1.6.3", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-logo.git", - "reference": "6a7b9e5602ca81c207e573dfed9e4fc1dd6a279b" + "reference": "4fb83219cd579e2ad47441afc402fb867d1906ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-logo/zipball/6a7b9e5602ca81c207e573dfed9e4fc1dd6a279b", - "reference": "6a7b9e5602ca81c207e573dfed9e4fc1dd6a279b", + "url": "https://api.github.com/repos/Automattic/jetpack-logo/zipball/4fb83219cd579e2ad47441afc402fb867d1906ee", + "reference": "4fb83219cd579e2ad47441afc402fb867d1906ee", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", - "yoast/phpunit-polyfills": "1.0.4" + "automattic/jetpack-changelogger": "^3.3.9", + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -525,28 +525,28 @@ ], "description": "A logo for Jetpack", "support": { - "source": "https://github.com/Automattic/jetpack-logo/tree/v1.6.1" + "source": "https://github.com/Automattic/jetpack-logo/tree/v1.6.3" }, - "time": "2023-04-10T11:43:42+00:00" + "time": "2023-09-19T18:18:39+00:00" }, { "name": "automattic/jetpack-password-checker", - "version": "v0.2.13", + "version": "v0.2.14", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-password-checker.git", - "reference": "16b88d370ca2f59b38e6c44bc37fc43e72090dad" + "reference": "e15e0e01e363c25c2c6b105f4388b4b7d6f7b1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/16b88d370ca2f59b38e6c44bc37fc43e72090dad", - "reference": "16b88d370ca2f59b38e6c44bc37fc43e72090dad", + "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/e15e0e01e363c25c2c6b105f4388b4b7d6f7b1db", + "reference": "e15e0e01e363c25c2c6b105f4388b4b7d6f7b1db", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", + "automattic/jetpack-changelogger": "^3.3.8", "automattic/wordbless": "@dev", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -574,31 +574,31 @@ ], "description": "Password Checker.", "support": { - "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.2.13" + "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.2.14" }, - "time": "2023-04-10T11:43:53+00:00" + "time": "2023-08-23T17:56:39+00:00" }, { "name": "automattic/jetpack-redirect", - "version": "v1.7.25", + "version": "v1.7.27", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-redirect.git", - "reference": "67d7dce123d4af4fec4b4fe15e99aaad85308314" + "reference": "43dd3ae2bef71281fe70f62733bfaa44c988f1b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/67d7dce123d4af4fec4b4fe15e99aaad85308314", - "reference": "67d7dce123d4af4fec4b4fe15e99aaad85308314", + "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/43dd3ae2bef71281fe70f62733bfaa44c988f1b1", + "reference": "43dd3ae2bef71281fe70f62733bfaa44c988f1b1", "shasum": "" }, "require": { - "automattic/jetpack-status": "^1.16.4" + "automattic/jetpack-status": "^1.18.4" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", + "automattic/jetpack-changelogger": "^3.3.9", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -625,28 +625,28 @@ ], "description": "Utilities to build URLs to the jetpack.com/redirect/ service", "support": { - "source": "https://github.com/Automattic/jetpack-redirect/tree/v1.7.25" + "source": "https://github.com/Automattic/jetpack-redirect/tree/v1.7.27" }, - "time": "2023-04-10T11:44:05+00:00" + "time": "2023-09-19T18:19:22+00:00" }, { "name": "automattic/jetpack-roles", - "version": "v1.4.23", + "version": "v1.4.25", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-roles.git", - "reference": "f147b3e8061fc0de2a892ddc4f4156eb995545f9" + "reference": "708b33f16a879fc2ab5939a972c968c9aeefbe38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/f147b3e8061fc0de2a892ddc4f4156eb995545f9", - "reference": "f147b3e8061fc0de2a892ddc4f4156eb995545f9", + "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/708b33f16a879fc2ab5939a972c968c9aeefbe38", + "reference": "708b33f16a879fc2ab5939a972c968c9aeefbe38", "shasum": "" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", + "automattic/jetpack-changelogger": "^3.3.9", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -673,32 +673,32 @@ ], "description": "Utilities, related with user roles and capabilities.", "support": { - "source": "https://github.com/Automattic/jetpack-roles/tree/v1.4.23" + "source": "https://github.com/Automattic/jetpack-roles/tree/v1.4.25" }, - "time": "2023-04-10T11:43:48+00:00" + "time": "2023-09-19T18:18:38+00:00" }, { "name": "automattic/jetpack-status", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-status.git", - "reference": "98feb85e54ec04ccd2dd37acc4df18ec521249d3" + "reference": "3281c2311752e9df1b2809d8feacb7bf7b9b7b8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/98feb85e54ec04ccd2dd37acc4df18ec521249d3", - "reference": "98feb85e54ec04ccd2dd37acc4df18ec521249d3", + "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/3281c2311752e9df1b2809d8feacb7bf7b9b7b8d", + "reference": "3281c2311752e9df1b2809d8feacb7bf7b9b7b8d", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^1.6.22" + "automattic/jetpack-constants": "^1.6.23" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.7", - "automattic/jetpack-ip": "^0.1.4", + "automattic/jetpack-changelogger": "^3.3.11", + "automattic/jetpack-ip": "^0.1.6", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.0.4" + "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -711,7 +711,7 @@ "link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "1.18.x-dev" + "dev-trunk": "1.19.x-dev" } }, "autoload": { @@ -725,9 +725,9 @@ ], "description": "Used to retrieve information about the current status of Jetpack and the site overall.", "support": { - "source": "https://github.com/Automattic/jetpack-status/tree/v1.18.0" + "source": "https://github.com/Automattic/jetpack-status/tree/v1.19.0" }, - "time": "2023-07-18T23:45:14+00:00" + "time": "2023-11-13T13:50:12+00:00" }, { "name": "automattic/jetpack-sync", @@ -1578,16 +1578,16 @@ }, { "name": "composer/semver", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { @@ -1637,9 +1637,9 @@ "versioning" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" + "source": "https://github.com/composer/semver/tree/3.4.0" }, "funding": [ { @@ -1655,7 +1655,7 @@ "type": "tidelift" } ], - "time": "2022-04-01T19:23:25+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { "name": "composer/xdebug-handler", @@ -1841,6 +1841,59 @@ }, "time": "2020-06-25T14:57:39+00:00" }, + { + "name": "dg/bypass-finals", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/dg/bypass-finals.git", + "reference": "12ef25e1f8d4144e4ec80d13a28895e8942f4104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dg/bypass-finals/zipball/12ef25e1f8d4144e4ec80d13a28895e8942f4104", + "reference": "12ef25e1f8d4144e4ec80d13a28895e8942f4104", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.3", + "phpstan/phpstan": "^0.12" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes", + "keywords": [ + "finals", + "mocking", + "phpunit", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/dg/bypass-finals/issues", + "source": "https://github.com/dg/bypass-finals/tree/v1.5.1" + }, + "time": "2023-09-16T09:13:54+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "v0.1.1", @@ -1950,28 +2003,28 @@ }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1991,9 +2044,9 @@ ], "support": { "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "time": "2017-07-23T21:35:13+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -2332,16 +2385,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.16.0", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -2382,9 +2435,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2023-06-25T14:52:30+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "openlss/lib-array2xml", @@ -3088,16 +3141,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.27", + "version": "9.2.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", - "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", "shasum": "" }, "require": { @@ -3154,7 +3207,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" }, "funding": [ { @@ -3162,7 +3215,7 @@ "type": "github" } ], - "time": "2023-07-26T13:44:30+00:00" + "time": "2023-09-19T04:57:46+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3608,16 +3661,16 @@ }, { "name": "react/event-loop", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05" + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6e7e587714fff7a83dcc7025aee42ab3b265ae05", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { @@ -3668,7 +3721,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.4.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, "funding": [ { @@ -3676,7 +3729,7 @@ "type": "open_collective" } ], - "time": "2023-05-05T10:11:24+00:00" + "time": "2023-11-13T13:48:05+00:00" }, { "name": "react/stream", @@ -4308,16 +4361,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -4360,7 +4413,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -4368,7 +4421,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -4948,16 +5001,16 @@ }, { "name": "symfony/console", - "version": "v5.4.26", + "version": "v5.4.31", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273" + "reference": "11ac5f154e0e5c4c77af83ad11ead9165280b92a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/b504a3d266ad2bb632f196c0936ef2af5ff6e273", - "reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273", + "url": "https://api.github.com/repos/symfony/console/zipball/11ac5f154e0e5c4c77af83ad11ead9165280b92a", + "reference": "11ac5f154e0e5c4c77af83ad11ead9165280b92a", "shasum": "" }, "require": { @@ -5027,7 +5080,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.26" + "source": "https://github.com/symfony/console/tree/v5.4.31" }, "funding": [ { @@ -5043,7 +5096,7 @@ "type": "tidelift" } ], - "time": "2023-07-19T20:11:33+00:00" + "time": "2023-10-31T07:58:33+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5177,16 +5230,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -5201,7 +5254,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5239,7 +5292,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -5255,20 +5308,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -5280,7 +5333,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5320,7 +5373,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -5336,20 +5389,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -5361,7 +5414,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5404,7 +5457,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -5420,20 +5473,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -5448,7 +5501,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5487,7 +5540,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -5503,20 +5556,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", "shasum": "" }, "require": { @@ -5525,7 +5578,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5566,7 +5619,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" }, "funding": [ { @@ -5582,20 +5635,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -5604,7 +5657,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5649,7 +5702,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -5665,20 +5718,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v5.4.26", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "1a44dc377ec86a50fab40d066cd061e28a6b482f" + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/1a44dc377ec86a50fab40d066cd061e28a6b482f", - "reference": "1a44dc377ec86a50fab40d066cd061e28a6b482f", + "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", "shasum": "" }, "require": { @@ -5711,7 +5764,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.26" + "source": "https://github.com/symfony/process/tree/v5.4.28" }, "funding": [ { @@ -5727,7 +5780,7 @@ "type": "tidelift" } ], - "time": "2023-07-12T15:44:31+00:00" + "time": "2023-08-07T10:36:04+00:00" }, { "name": "symfony/service-contracts", @@ -5814,16 +5867,16 @@ }, { "name": "symfony/string", - "version": "v5.4.26", + "version": "v5.4.31", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1181fe9270e373537475e826873b5867b863883c" + "reference": "2765096c03f39ddf54f6af532166e42aaa05b24b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", - "reference": "1181fe9270e373537475e826873b5867b863883c", + "url": "https://api.github.com/repos/symfony/string/zipball/2765096c03f39ddf54f6af532166e42aaa05b24b", + "reference": "2765096c03f39ddf54f6af532166e42aaa05b24b", "shasum": "" }, "require": { @@ -5880,7 +5933,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.26" + "source": "https://github.com/symfony/string/tree/v5.4.31" }, "funding": [ { @@ -5896,20 +5949,20 @@ "type": "tidelift" } ], - "time": "2023-06-28T12:46:07+00:00" + "time": "2023-11-09T08:19:44+00:00" }, { "name": "symfony/yaml", - "version": "v5.4.23", + "version": "v5.4.31", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b" + "reference": "f387675d7f5fc4231f7554baa70681f222f73563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4cd2e3ea301aadd76a4172756296fe552fb45b0b", - "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f387675d7f5fc4231f7554baa70681f222f73563", + "reference": "f387675d7f5fc4231f7554baa70681f222f73563", "shasum": "" }, "require": { @@ -5955,7 +6008,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.23" + "source": "https://github.com/symfony/yaml/tree/v5.4.31" }, "funding": [ { @@ -5971,7 +6024,7 @@ "type": "tidelift" } ], - "time": "2023-04-23T19:33:36+00:00" + "time": "2023-11-03T14:41:28+00:00" }, { "name": "theseer/tokenizer", @@ -6593,5 +6646,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/docs/dependencies.md b/docs/dependencies.md index 832feac7900..4b6357367df 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -69,6 +69,7 @@ to catalog our packages and provide guidance to a developer who wants to test an | [@types/react](https://www.npmjs.com/package/@types/react) | Contains type definitions for React. | JS unit tests are passing. | You should pick a version `x.y.z` with `x.y` similar to the version defined for React. | | [@woocommerce/components](https://www.npmjs.com/package/@woocommerce/components) | A library of components that can be used to create pages in the WooCommerce dashboard and reports pages. | JS unit tests are passing. | | | [@wordpress/components](https://www.npmjs.com/package/@wordpress/components) | A library of generic WordPress components to be used for creating common UI elements. | JS unit tests are passing and UI isn't affected at places of usage. | This package is one of the few `@wordpress/x` packages that doesn't come from WordPress directly, because we decided to bundle it ourselves (see our [dependency extractor webpack config](https://github.com/Automattic/woocommerce-payments/blob/b7634468560d905a479d50066233f807da62413f/webpack/shared.js#L132-L150)). | +| [@woocommerce/csv-export](https://www.npmjs.com/package/@woocommerce/csv-export) | A set of functions to convert data into CSV values, and enable a browser download of the CSV data. We use it to export reports for Transactions, Deposits, and Disputes. | JS unit tests are passing. | | ### PHP Runtime Dependencies | Package Name | Usage Summary | Testing | Notes | diff --git a/docs/rest-api/source/includes/wp-api-v3/reports.md b/docs/rest-api/source/includes/wp-api-v3/reports.md index 6e78cd6ee60..38d8dba5f55 100644 --- a/docs/rest-api/source/includes/wp-api-v3/reports.md +++ b/docs/rest-api/source/includes/wp-api-v3/reports.md @@ -155,6 +155,9 @@ Fetch a detailed overview of authorizations. ### Optional parameters +- `date_before`: Filter authorizations before this date. +- `date_after`: Filter authorizations after this date. If it's not provided the default it will be 7 days before today. +- `date_between`: Filter authorizations between these dates. - `order_id`: Filter authorizations based on the associated order ID. - `deposit_id`: Filter authorizations based on the associated deposit ID. - `customer_email`: Filter authorizations based on the customer email. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 1398f37da59..c44adb42ac8 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -343,14 +343,21 @@ public function add_payments_menu() { } global $submenu; + // If the user is redirected to the page after Stripe KYC with an error, refresh the account data. + // The GET parameter accessed here comes from server and is just to indicate that some error occured. For this reason we're not using a nonce. + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['wcpay-connection-error'] ) ) { + $this->account->refresh_account_data(); + } try { - $should_render_full_menu = $this->account->is_stripe_account_valid(); + // Render full payments menu with sub-items only if the merchant completed the KYC (details_submitted = true). + $should_render_full_menu = $this->account->is_account_fully_onboarded(); } catch ( Exception $e ) { - // There is an issue with connection but render full menu anyways to provide access to settings. - $should_render_full_menu = true; + // There is an issue with connection, don't render full menu, user will get redirected to the connect page. + $should_render_full_menu = false; } - $top_level_link = $should_render_full_menu ? '/payments/overview' : '/payments/connect'; + $top_level_link = $this->account->is_stripe_connected() ? '/payments/overview' : '/payments/connect'; $menu_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNjciCiAgIHNvZGlwb2RpOmRvY25hbWU9IndjcGF5X21lbnVfaWNvbi5zdmciCiAgIHdpZHRoPSI4NTIiCiAgIGhlaWdodD0iNjg0IgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEgKGM0ZThmOWUsIDIwMjEtMDUtMjQpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM3MSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzY5IgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOnpvb209IjI1NiIKICAgICBpbmtzY2FwZTpjeD0iLTg0Ljg1NzQyMiIKICAgICBpbmtzY2FwZTpjeT0iLTgzLjI5NDkyMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA4MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTE2IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyMDIiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc2NyIgLz4KICA8cGF0aAogICAgIHRyYW5zZm9ybT0ic2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtODUwLCAwKSIKICAgICBkPSJNIDc2OCw4NiBWIDU5OCBIIDg0IFYgODYgWiBtIDAsNTk4IGMgNDgsMCA4NCwtMzggODQsLTg2IFYgODYgQyA4NTIsMzggODE2LDAgNzY4LDAgSCA4NCBDIDM2LDAgMCwzOCAwLDg2IHYgNTEyIGMgMCw0OCAzNiw4NiA4NCw4NiB6IE0gMzg0LDEyOCB2IDQ0IGggLTg2IHYgODQgaCAxNzAgdiA0NCBIIDM0MCBjIC0yNCwwIC00MiwxOCAtNDIsNDIgdiAxMjggYyAwLDI0IDE4LDQyIDQyLDQyIGggNDQgdiA0NCBoIDg0IHYgLTQ0IGggODYgViA0MjggSCAzODQgdiAtNDQgaCAxMjggYyAyNCwwIDQyLC0xOCA0MiwtNDIgViAyMTQgYyAwLC0yNCAtMTgsLTQyIC00MiwtNDIgaCAtNDQgdiAtNDQgeiIKICAgICBmaWxsPSIjYTJhYWIyIgogICAgIGlkPSJwYXRoNjUiIC8+Cjwvc3ZnPgo='; @@ -377,8 +384,8 @@ public function add_payments_menu() { return; } - if ( ! $should_render_full_menu ) { - if ( WC_Payments_Utils::should_use_progressive_onboarding_flow() ) { + if ( ! $this->account->is_stripe_connected() ) { + if ( WC_Payments_Utils::should_use_new_onboarding_flow() ) { wc_admin_register_page( [ 'id' => 'wc-payments-onboarding', diff --git a/includes/admin/class-wc-rest-payments-transactions-controller.php b/includes/admin/class-wc-rest-payments-transactions-controller.php index 5c353b1113b..68044ac904c 100644 --- a/includes/admin/class-wc-rest-payments-transactions-controller.php +++ b/includes/admin/class-wc-rest-payments-transactions-controller.php @@ -237,6 +237,12 @@ function ( $transaction_date ) use ( $user_timezone ) { 'type_is_not' => $request->get_param( 'type_is_not' ), 'source_device_is' => $request->get_param( 'source_device_is' ), 'source_device_is_not' => $request->get_param( 'source_device_is_not' ), + 'channel_is' => $request->get_param( 'channel_is' ), + 'channel_is_not' => $request->get_param( 'channel_is_not' ), + 'customer_country_is' => $request->get_param( 'customer_country_is' ), + 'customer_country_is_not' => $request->get_param( 'customer_country_is_not' ), + 'risk_level_is' => $request->get_param( 'risk_level_is' ), + 'risk_level_is_not' => $request->get_param( 'risk_level_is_not' ), 'store_currency_is' => $request->get_param( 'store_currency_is' ), 'customer_currency_is' => $request->get_param( 'customer_currency_is' ), 'customer_currency_is_not' => $request->get_param( 'customer_currency_is_not' ), diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 541b1ba53c4..df4116e1cec 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -374,12 +374,12 @@ public function __construct( 'title' => __( 'Size of the button displayed for Express Checkouts', 'woocommerce-payments' ), 'type' => 'select', 'description' => __( 'Select the size of the button.', 'woocommerce-payments' ), - 'default' => 'default', + 'default' => 'medium', 'desc_tip' => true, 'options' => [ - 'default' => __( 'Default', 'woocommerce-payments' ), - 'medium' => __( 'Medium', 'woocommerce-payments' ), - 'large' => __( 'Large', 'woocommerce-payments' ), + 'small' => __( 'Small', 'woocommerce-payments' ), + 'medium' => __( 'Medium', 'woocommerce-payments' ), + 'large' => __( 'Large', 'woocommerce-payments' ), ], ], 'platform_checkout_button_locations' => [ @@ -711,7 +711,6 @@ public function should_use_stripe_platform_on_checkout_page() { if ( WC_Payments_Features::is_woopay_eligible() && 'yes' === $this->get_option( 'platform_checkout', 'no' ) && - ! $this->is_upe_incompatible_with_woopay() && ( is_checkout() || has_block( 'woocommerce/checkout' ) ) && ! is_wc_endpoint_url( 'order-pay' ) && WC()->cart instanceof WC_Cart && @@ -724,16 +723,6 @@ public function should_use_stripe_platform_on_checkout_page() { return false; } - /** - * The legacy UPE is incompatible with WooPay, whereas split UPE and deferred intent UPE are compatible. - * This method checks if there's incompatibility between WooPay and currently enabled UPE settings, applying the rule above. - * - * $return bool - true if UPE is incompatible with WooPay, false otherwise. - */ - private function is_upe_incompatible_with_woopay() { - return WC_Payments_Features::is_upe_legacy_enabled() && ! WC_Payments_Features::is_upe_deferred_intent_enabled(); - } - /** * Renders the credit card input fields needed to get the user's payment information on the checkout page. * @@ -946,10 +935,6 @@ public function process_payment( $order_id ) { ]; } - if ( WC_Payments_Features::is_upe_legacy_enabled() ) { - UPE_Payment_Gateway::remove_upe_payment_intent_from_session(); - } - $check_session_order = $this->duplicate_payment_prevention_service->check_against_session_processing_order( $order ); if ( is_array( $check_session_order ) ) { return $check_session_order; @@ -1050,10 +1035,6 @@ public function process_payment( $order_id ) { $order->add_order_note( $note ); } - if ( WC_Payments_Features::is_upe_legacy_enabled() ) { - UPE_Payment_Gateway::remove_upe_payment_intent_from_session(); - } - // Re-throw the exception after setting everything up. // This makes the error notice show up both in the regular and block checkout. throw new Exception( WC_Payments_Utils::get_filtered_error_message( $e ) ); @@ -1345,8 +1326,8 @@ public function process_payment_for_order( $cart, $payment_information, $schedul } } - // For Stripe Link & SEPA with deferred intent UPE, we must create mandate to acknowledge that terms have been shown to customer. - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() && $this->is_mandate_data_required() ) { + // For Stripe Link & SEPA, we must create mandate to acknowledge that terms have been shown to customer. + if ( $this->is_mandate_data_required() ) { $request->set_mandate_data( $this->get_mandate_data() ); } @@ -1420,7 +1401,6 @@ public function process_payment_for_order( $cart, $payment_information, $schedul $request->set_hook_args( $payment_information, false, $save_user_in_woopay ); if ( - WC_Payments_Features::is_upe_deferred_intent_enabled() && Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() && in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true ) ) { @@ -1578,10 +1558,8 @@ protected function is_mandate_data_required() { * @return string|null Payment method to use for the intent. */ public function get_payment_method_to_use_for_intent() { - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification - return $this->get_payment_methods_from_gateway_id( $requested_payment_method )[0]; - } + $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification + return $this->get_payment_methods_from_gateway_id( $requested_payment_method )[0]; } /** @@ -1627,20 +1605,14 @@ public function get_payment_methods_from_gateway_id( $gateway_id, $order_id = nu $eligible_payment_methods = WC_Payments::get_gateway()->get_payment_method_ids_enabled_at_checkout( $order_id, true ); - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - // If split or deferred intent UPE is enabled and $gateway_id is `woocommerce_payments`, this must be the CC gateway. - // We only need to return single `card` payment method, adding `link` since deferred intent UPE gateway is compatible with Link. - $payment_methods = [ Payment_Method::CARD ]; - if ( in_array( Payment_Method::LINK, $eligible_payment_methods, true ) ) { - $payment_methods[] = Payment_Method::LINK; - } - - return $payment_methods; + // If $gateway_id is `woocommerce_payments`, this must be the CC gateway. + // We only need to return single `card` payment method, adding `link` since Stripe Link is also supported. + $payment_methods = [ Payment_Method::CARD ]; + if ( in_array( Payment_Method::LINK, $eligible_payment_methods, true ) ) { + $payment_methods[] = Payment_Method::LINK; } - // $gateway_id must be `woocommerce_payments` and gateway is either legacy UPE or legacy card. - // Find the relevant gateway and return all available payment methods. - return $eligible_payment_methods; + return $payment_methods; } /** @@ -2106,13 +2078,6 @@ public function update_is_woopay_enabled( $is_woopay_enabled ) { if ( ! $is_woopay_enabled ) { WooPay_Order_Status_Sync::remove_webhook(); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - update_option( WC_Payments_Features::UPE_FLAG_NAME, '0' ); - update_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, '1' ); - - if ( function_exists( 'wc_admin_record_tracks_event' ) ) { - wc_admin_record_tracks_event( 'wcpay_deferred_intent_upe_enabled' ); - } } } } diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 5d8a63ed520..ee45c0c3d39 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -96,7 +96,7 @@ public function init_hooks() { add_action( 'admin_init', [ $this, 'maybe_redirect_to_wcpay_connect' ], 12 ); // Run this after the redirect to onboarding logic. add_action( 'admin_init', [ $this, 'maybe_redirect_to_capital_offer' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect_to_server_link' ] ); - add_action( 'admin_init', [ $this, 'maybe_redirect_settings_to_connect' ] ); + add_action( 'admin_init', [ $this, 'maybe_redirect_settings_to_connect_or_overview' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_flow_to_overview' ] ); add_action( 'admin_init', [ $this, 'maybe_activate_woopay' ] ); @@ -238,6 +238,21 @@ public function is_account_partially_onboarded(): bool { return false === $account['details_submitted']; } + /** + * Checks if the account has completed onboarding/KYC. + * Returns true if the onboarding/KYC is completed. + * + * @return bool True if the account is connected and details are submitted, false otherwise. + */ + public function is_account_fully_onboarded(): bool { + if ( ! $this->is_stripe_connected() ) { + return false; + } + + $account = $this->get_cached_account_data(); + return true === $account['details_submitted']; + } + /** * Gets the account status data for rendering on the settings page. * @@ -517,9 +532,10 @@ public function get_fees() { public function get_progressive_onboarding_details(): array { $account = $this->get_cached_account_data(); return [ - 'isEnabled' => $account['progressive_onboarding']['is_enabled'] ?? false, - 'isComplete' => $account['progressive_onboarding']['is_complete'] ?? false, - 'isNewFlowEnabled' => WC_Payments_Utils::should_use_progressive_onboarding_flow(), + 'isEnabled' => $account['progressive_onboarding']['is_enabled'] ?? false, + 'isComplete' => $account['progressive_onboarding']['is_complete'] ?? false, + 'isNewFlowEnabled' => WC_Payments_Utils::should_use_new_onboarding_flow(), + 'isEligibilityModalDismissed' => get_option( WC_Payments_Onboarding_Service::ONBOARDING_ELIGIBILITY_MODAL_OPTION, false ), ]; } @@ -741,7 +757,7 @@ public function maybe_redirect_to_onboarding() { return false; } - // Redirect directly to onboarding page if come from WC Admin task and are in treatment mode. + // Redirect directly to onboarding page if come from WC Admin task. $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); if ( 0 < strpos( $http_referer, 'task=payments' ) ) { $this->redirect_to_onboarding_flow_page(); @@ -818,15 +834,15 @@ public function maybe_redirect_to_wcpay_connect(): bool { } /** - * Redirects WooPayments settings to the connect page for partially - * onboarded accounts. + * Redirects WooPayments settings to the overview page for partially + * onboarded accounts, and the connect page when there is no account. * * Every WooPayments page except connect are already hidden, but merchants can still access * it through WooCommerce settings. * - * @return bool True if the redirection happened, false otherwise. + * @return bool True if a redirection happened, false otherwise. */ - public function maybe_redirect_settings_to_connect(): bool { + public function maybe_redirect_settings_to_connect_or_overview(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } @@ -842,22 +858,37 @@ public function maybe_redirect_settings_to_connect(): bool { return false; } - // Account not partially onboarded, don't redirect. - if ( ! $this->is_account_partially_onboarded() ) { + // Account fully onboarded, don't redirect. + if ( $this->is_account_fully_onboarded() ) { return false; } - $this->redirect_to( - admin_url( - add_query_arg( - [ - 'page' => 'wc-admin', - 'path' => '/payments/connect', - ], - 'admin.php' + // Account partially onboarded, redirect to overview. + if ( $this->is_account_partially_onboarded() ) { + $this->redirect_to( + admin_url( + add_query_arg( + [ + 'page' => 'wc-admin', + 'path' => '/payments/overview', + ], + 'admin.php' + ) ) - ) - ); + ); + } else { + $this->redirect_to( + admin_url( + add_query_arg( + [ + 'page' => 'wc-admin', + 'path' => '/payments/connect', + ], + 'admin.php' + ) + ) + ); + } return true; } @@ -885,6 +916,14 @@ public function maybe_redirect_onboarding_flow_to_overview(): bool { return false; } + // We check it here after refreshing the cache, because merchant might have clicked back in browser (after Stripe KYC). + // That will mean that no redirect from Stripe happened and user might be able to go through onboarding again if no webhook processed yet. + // That might cause issues if user selects dev onboarding after live one. + // Shouldn't be called with force disconnected option enabled, otherwise we'll get current account data. + if ( ! WC_Payments_Utils::force_disconnected_enabled() ) { + $this->refresh_account_data(); + } + // Don't redirect merchants that have no Stripe account connected. if ( ! $this->is_stripe_connected() ) { return false; @@ -930,6 +969,12 @@ public function maybe_handle_onboarding() { if ( $this->is_account_partially_onboarded() ) { $args = $_GET; $args['type'] = 'complete_kyc_link'; + + // Allow progressive onboarding accounts to continue onboarding without payout collection. + if ( $this->is_progressive_onboarding_in_progress() ) { + $args['is_progressive_onboarding'] = $this->is_progressive_onboarding_in_progress() ?? false; + } + $this->redirect_to_account_link( $args ); } @@ -975,9 +1020,10 @@ public function maybe_handle_onboarding() { $wcpay_connect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect'] ) ); - $from_wc_admin_task = 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param; - $from_wc_pay_connect_page = false !== strpos( wp_get_referer(), 'path=%2Fpayments%2Fconnect' ); - if ( ( $from_wc_admin_task || $from_wc_pay_connect_page ) ) { + $from_wc_admin_task = 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param; + $from_wc_admin_incentive_page = false !== strpos( wp_get_referer(), 'path=%2Fwc-pay-welcome-page' ); + $from_wc_pay_connect_page = false !== strpos( wp_get_referer(), 'path=%2Fpayments%2Fconnect' ); + if ( $from_wc_admin_task || $from_wc_pay_connect_page || $from_wc_admin_incentive_page ) { // Redirect non-onboarded account to the onboarding flow, otherwise to payments overview page. if ( ! $this->is_stripe_connected() ) { $this->redirect_to_onboarding_flow_page(); @@ -987,11 +1033,16 @@ public function maybe_handle_onboarding() { } } + // Handle the flow for a builder moving from test to live. 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(); + $test_mode = WC_Payments_Onboarding_Service::is_test_mode_enabled(); + + // Delete the account if the test mode is enabled otherwise it'll cause issues to onboard again. + if ( $test_mode ) { + $this->payments_api_client->delete_account( $test_mode ); } + + // Set the test mode to false now that we are handling a real onboarding. WC_Payments_Onboarding_Service::set_test_mode( false ); $this->redirect_to_onboarding_flow_page(); return; @@ -1275,8 +1326,13 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = // Clear account transient when generating Stripe's oauth data. $this->clear_cache(); + // Flags to enable progressive onboarding and collect payout requirements. + $progressive = ! empty( $_GET['progressive'] ) && 'true' === $_GET['progressive']; + $collect_payout_requirements = ! empty( $_GET['collect_payout_requirements'] ) && 'true' === $_GET['collect_payout_requirements']; + // Enable dev mode if the test_mode query param is set. $test_mode = isset( $_GET['test_mode'] ) ? boolval( wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) : false; + if ( $test_mode ) { WC_Payments_Onboarding_Service::set_test_mode( true ); } @@ -1284,15 +1340,20 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = // Clear persisted onboarding flow state. WC_Payments_Onboarding_Service::clear_onboarding_flow_state(); + if ( ! $collect_payout_requirements ) { + // Clear onboarding related account options if this is an initial onboarding attempt. + WC_Payments_Onboarding_Service::clear_account_options(); + } else { + // Since we assume user has already either gotten here from the eligibility modal, + // or has already dismissed it, we should set the modal as dismissed so it doesn't display again. + WC_Payments_Onboarding_Service::set_onboarding_eligibility_modal_dismissed(); + } + $return_url = $this->get_onboarding_return_url( $wcpay_connect_from ); if ( ! empty( $additional_args ) ) { $return_url = add_query_arg( $additional_args, $return_url ); } - // Flags to enable progressive onboarding and collect payout requirements. - $progressive = ! empty( $_GET['progressive'] ) && 'true' === $_GET['progressive']; - $collect_payout_requirements = ! empty( $_GET['collect_payout_requirements'] ) && 'true' === $_GET['collect_payout_requirements']; - // Onboarding self-assessment data. $self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : []; if ( $self_assessment_data ) { @@ -1329,26 +1390,7 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = } $account_data = [ 'setup_mode' => 'test', - 'country' => 'US', 'business_type' => 'individual', - 'individual' => [ - 'first_name' => 'John', - 'last_name' => 'Woolliams', - 'address' => [ - 'country' => 'US', - 'state' => 'California', - 'city' => 'South San Francisco', - 'line1' => '1040 Grand Ave', - 'postal_code' => '94080', - ], - 'ssn_last_4' => '0000', - 'phone' => '+10000000000', - 'dob' => [ - 'day' => '1', - 'month' => '1', - 'year' => '1980', - ], - ], 'mcc' => '5734', 'url' => $url, 'business_name' => get_bloginfo( 'name' ), @@ -1870,13 +1912,13 @@ public function is_card_testing_protection_eligible(): bool { } /** - * Redirects to the onboarding flow page if the Progressive Onboarding feature flag is enabled or in the experiment treatment mode. + * Redirects to the onboarding flow page. * Also checks if the server is connected and try to connect it otherwise. * * @return void */ private function redirect_to_onboarding_flow_page() { - if ( ! WC_Payments_Utils::should_use_progressive_onboarding_flow() ) { + if ( ! WC_Payments_Utils::should_use_new_onboarding_flow() ) { return; } diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index a7f077e23a4..8815afe74f7 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -145,14 +145,8 @@ public function register_scripts_for_zero_order_total() { ! has_block( 'woocommerce/checkout' ) ) { WC_Payments::get_gateway()->tokenization_script(); - $script_handle = 'WCPAY_CHECKOUT'; - $js_object = 'wcpayConfig'; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $script_handle = 'wcpay-upe-checkout'; - $js_object = 'wcpay_upe_config'; - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - $script_handle = 'wcpay-upe-checkout'; - } + $script_handle = 'wcpay-upe-checkout'; + $js_object = 'wcpay_upe_config'; wp_localize_script( $script_handle, $js_object, WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ); wp_enqueue_script( $script_handle ); } @@ -193,12 +187,13 @@ public function get_payment_fields_js_config() { 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), 'isPreview' => is_preview(), 'isUPEEnabled' => WC_Payments_Features::is_upe_enabled(), - 'isUPESplitEnabled' => WC_Payments_Features::is_upe_split_enabled(), - 'isUPEDeferredEnabled' => WC_Payments_Features::is_upe_deferred_intent_enabled(), + 'isUPESplitEnabled' => false, + 'isUPEDeferredEnabled' => true, 'isSavedCardsEnabled' => $this->gateway->is_saved_cards_enabled(), 'isWooPayEnabled' => $this->woopay_util->should_enable_woopay( $this->gateway ) && $this->woopay_util->should_enable_woopay_on_cart_or_checkout(), 'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(), 'isWoopayFirstPartyAuthEnabled' => $this->woopay_util->is_woopay_first_party_auth_enabled(), + 'isWooPayEmailInputEnabled' => $this->woopay_util->is_woopay_email_input_enabled(), 'isClientEncryptionEnabled' => WC_Payments_Features::is_client_secret_encryption_enabled(), 'woopayHost' => WooPay_Utilities::get_woopay_url(), 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ), diff --git a/includes/class-wc-payments-customer-service.php b/includes/class-wc-payments-customer-service.php index 0b5bf35c5f2..ecfabd4077e 100644 --- a/includes/class-wc-payments-customer-service.php +++ b/includes/class-wc-payments-customer-service.php @@ -107,7 +107,7 @@ public function __construct( /** * Get WCPay customer ID for the given WordPress user ID * - * @param int $user_id The user ID to look for a customer ID with. + * @param int|null $user_id The user ID to look for a customer ID with. * * @return string|null WCPay customer ID or null if not found. */ @@ -134,21 +134,21 @@ public function get_customer_id_by_user_id( $user_id ) { /** * Create a customer and associate it with a WordPress user. * - * @param WP_User $user User to create a customer for. - * @param array $customer_data Customer data. + * @param WP_User|null $user User to create a customer for. + * @param array $customer_data Customer data. * * @return string The created customer's ID * * @throws API_Exception Error creating customer. */ - public function create_customer_for_user( WP_User $user, array $customer_data ): string { + public function create_customer_for_user( ?WP_User $user, array $customer_data = [] ): string { // Include the session ID for the user. $customer_data['session_id'] = $this->session_service->get_sift_session_id() ?? null; // Create a customer on the WCPay server. $customer_id = $this->payments_api_client->create_customer( $customer_data ); - if ( $user->ID > 0 ) { + if ( $user instanceof WP_User && $user->ID > 0 ) { $this->update_user_customer_id( $user->ID, $customer_id ); } @@ -163,38 +163,37 @@ public function create_customer_for_user( WP_User $user, array $customer_data ): /** * Manages customer details held on WCPay server for WordPress user associated with an order. * - * @param int $user_id ID of the WP user to associate with the customer. + * @param int|null $user_id ID of the WP user to associate with the customer. * @param WC_Order $order Woo Order. + * * @return string WooPayments customer ID. - */ - public function get_or_create_customer_id_from_order( int $user_id, WC_Order $order ): string { + * @throws API_Exception Throws when server API request fails. +*/ + public function get_or_create_customer_id_from_order( ?int $user_id, WC_Order $order ): string { // Determine the customer making the payment, create one if we don't have one already. - $customer_id = $this->get_customer_id_by_user_id( $user_id ); + $customer_id = $this->get_customer_id_by_user_id( $user_id ); + $customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ?? 0 ) ); + $user = null === $user_id ? null : get_user_by( 'id', $user_id ); if ( null !== $customer_id ) { - // @todo: We need to update the customer here. + $this->update_customer_for_user( $customer_id, $user, $customer_data ); return $customer_id; } - - $customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ) ); - $user = get_user_by( 'id', $user_id ); - $customer_id = $this->create_customer_for_user( $user, $customer_data ); - - return $customer_id; + return $this->create_customer_for_user( $user, $customer_data ); } /** * Update the customer details held on the WCPay server associated with the given WordPress user. * - * @param string $customer_id WCPay customer ID. - * @param WP_User $user WordPress user. - * @param array $customer_data Customer data. + * @param string $customer_id WCPay customer ID. + * @param WP_User|null $user WordPress user. + * @param array $customer_data Customer data. * * @return string The updated customer's ID. Can be different to the ID parameter if the customer was re-created. * * @throws API_Exception Error updating the customer. */ - public function update_customer_for_user( string $customer_id, WP_User $user, array $customer_data ): string { + public function update_customer_for_user( string $customer_id, ?WP_User $user, array $customer_data ): string { try { // Update the customer on the WCPay server. $this->payments_api_client->update_customer( @@ -385,15 +384,15 @@ public function delete_cached_payment_methods() { /** * Recreates the customer for this user. * - * @param WP_User $user User to recreate a customer for. - * @param array $customer_data Customer data. + * @param WP_User|null $user User to recreate a customer for. + * @param array $customer_data Customer data. * * @return string The newly created customer's ID * * @throws API_Exception Error creating customer. */ - private function recreate_customer( WP_User $user, array $customer_data ): string { - if ( $user->ID > 0 ) { + private function recreate_customer( ?WP_User $user, array $customer_data ): string { + if ( $user instanceof WP_User && $user->ID > 0 ) { $result = delete_user_option( $user->ID, $this->get_customer_id_option() ); if ( ! $result ) { // Log the error, but continue since we'll be trying to update this option in create_customer. 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 cdc04b3fd69..ad8db375684 100644 --- a/includes/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/class-wc-payments-express-checkout-button-display-handler.php @@ -151,7 +151,7 @@ function( $js_config ) use ( $order ) { // Silence the filter_input warning because we are sanitizing the input with sanitize_email(). // nosemgrep: audit.php.lang.misc.filter-input-no-filter - $user_email = sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) ?? $session_email; + $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) : $session_email; $js_config['order_id'] = $order->get_id(); $js_config['pay_for_order'] = sanitize_text_field( wp_unslash( $_GET['pay_for_order'] ) ); diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 84c45998636..ef9ae19c263 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -21,9 +21,7 @@ class WC_Payments_Features { const WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_express_checkout'; const WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME = '_wcpay_feature_woopay_first_party_auth'; const AUTH_AND_CAPTURE_FLAG_NAME = '_wcpay_feature_auth_and_capture'; - const PROGRESSIVE_ONBOARDING_FLAG_NAME = '_wcpay_feature_progressive_onboarding'; const PAY_FOR_ORDER_FLOW = '_wcpay_feature_pay_for_order_flow'; - const DEFERRED_UPE_SERVER_FLAG_NAME = 'is_deferred_intent_creation_upe_enabled'; const DISPUTE_ISSUER_EVIDENCE = '_wcpay_feature_dispute_issuer_evidence'; const STREAMLINE_REFUNDS_FLAG_NAME = '_wcpay_feature_streamline_refunds'; @@ -33,7 +31,7 @@ class WC_Payments_Features { * @return bool */ public static function is_upe_enabled() { - return self::is_upe_legacy_enabled() || self::is_upe_split_enabled() || self::is_upe_deferred_intent_enabled(); + return true; } /** @@ -42,55 +40,7 @@ public static function is_upe_enabled() { * @return string */ public static function get_enabled_upe_type() { - if ( self::is_upe_deferred_intent_enabled() ) { - return 'deferred_intent'; - } - - if ( self::is_upe_split_enabled() ) { - return 'split'; - } - - if ( self::is_upe_legacy_enabled() ) { - return 'legacy'; - } - - return ''; - } - - /** - * Checks whether the legacy UPE gateway is enabled - * - * @return bool - */ - public static function is_upe_legacy_enabled() { - return '1' === get_option( self::UPE_FLAG_NAME, '0' ); - } - - /** - * Checks whether the Split-UPE gateway is enabled - */ - public static function is_upe_split_enabled() { - return '1' === get_option( self::UPE_SPLIT_FLAG_NAME, '0' ); - } - - /** - * Checks whether the Split UPE with deferred intent creation is enabled - */ - public static function is_upe_deferred_intent_enabled() { - $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); - return is_array( $account ) && ( $account[ self::DEFERRED_UPE_SERVER_FLAG_NAME ] ?? false ); - } - - /** - * Checks for the requirements to have the split-UPE enabled. - */ - private static function is_upe_split_eligible() { - $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); - if ( empty( $account['capabilities']['sepa_debit_payments'] ) ) { - return true; - } - - return 'active' !== $account['capabilities']['sepa_debit_payments']; + return 'deferred_intent'; } /** @@ -351,15 +301,6 @@ public static function is_auth_and_capture_enabled() { return '1' === get_option( self::AUTH_AND_CAPTURE_FLAG_NAME, '1' ); } - /** - * Checks whether Progressive Onboarding is enabled. - * - * @return bool - */ - public static function is_progressive_onboarding_enabled(): bool { - return '1' === get_option( self::PROGRESSIVE_ONBOARDING_FLAG_NAME, '0' ); - } - /** * Checks whether the Fraud and Risk Tools feature flag is enabled. * @@ -458,8 +399,8 @@ public static function to_array() { return array_filter( [ 'upe' => self::is_upe_enabled(), - 'upeSplit' => self::is_upe_split_enabled(), - 'upeDeferred' => self::is_upe_deferred_intent_enabled(), + 'upeSplit' => false, + 'upeDeferred' => true, 'upeSettingsPreview' => self::is_upe_settings_preview_enabled(), 'multiCurrency' => self::is_customer_multi_currency_enabled(), 'woopay' => self::is_woopay_eligible(), @@ -467,7 +408,6 @@ public static function to_array() { 'clientSecretEncryption' => self::is_client_secret_encryption_enabled(), 'woopayExpressCheckout' => self::is_woopay_express_checkout_enabled(), 'isAuthAndCaptureEnabled' => self::is_auth_and_capture_enabled(), - 'progressiveOnboarding' => self::is_progressive_onboarding_enabled(), 'isPayForOrderFlowEnabled' => self::is_pay_for_order_flow_enabled(), 'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(), 'isRefundControlsEnabled' => self::is_streamline_refunds_enabled(), diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 904462c37ef..42fad9d1af3 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -17,8 +17,9 @@ */ class WC_Payments_Onboarding_Service { - const TEST_MODE_OPTION = 'wcpay_onboarding_test_mode'; - const ONBOARDING_FLOW_STATE_OPTION = 'wcpay_onboarding_flow_state'; + const TEST_MODE_OPTION = 'wcpay_onboarding_test_mode'; + const ONBOARDING_FLOW_STATE_OPTION = 'wcpay_onboarding_flow_state'; + const ONBOARDING_ELIGIBILITY_MODAL_OPTION = 'wcpay_onboarding_eligibility_modal_dismissed'; /** * Client for making requests to the WooCommerce Payments API @@ -227,6 +228,25 @@ public static function clear_onboarding_flow_state(): bool { return delete_option( self::ONBOARDING_FLOW_STATE_OPTION ); } + /** + * Clear any account options we may want to reset when a new onboarding flow is initialised. + * Currently, just deletes the option which stores whether the eligibility modal has been dismissed. + * + * @return boolean Whether the option was deleted successfully. + */ + public static function clear_account_options(): bool { + return delete_option( self::ONBOARDING_ELIGIBILITY_MODAL_OPTION ); + } + + /** + * Set the onboarding eligibility modal dismissed option to true. + * + * @return void + */ + public static function set_onboarding_eligibility_modal_dismissed(): void { + update_option( self::ONBOARDING_ELIGIBILITY_MODAL_OPTION, true ); + } + /** * Set onboarding test mode. * Will also switch WC_Payments mode immediately. diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 414ce0fca37..82da5a8d449 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -197,7 +197,7 @@ public function get_button_height() { return '56'; } - // for the "default" and "catch-all" scenarios. + // for the "default"/"small" and "catch-all" scenarios. return '40'; } @@ -596,6 +596,18 @@ public function has_allowed_items_in_cart() { return false; } + /** + * Filter whether product supports Payment Request Button on cart page. + * + * @since 6.9.0 + * + * @param boolean $is_supported Whether product supports Payment Request Button on cart page. + * @param object $_product Product object. + */ + if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) { + return false; + } + // Trial subscriptions with shipping are not supported. if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) { return false; @@ -1137,16 +1149,25 @@ public function ajax_add_to_cart() { define( 'WOOCOMMERCE_CART', true ); } - $subscription_types = [ - 'subscription', - 'subscription_variation', - ]; - WC()->shipping->reset_shipping(); - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + wp_send_json( + [ + 'error' => [ + 'code' => 'invalid_product_id', + 'message' => __( 'Invalid product id', 'woocommerce-payments' ), + ], + ], + 404 + ); + return; + } + $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] ); - $product = wc_get_product( $product_id ); $product_type = $product->get_type(); // First empty the cart to prevent wrong calculation. @@ -1161,7 +1182,7 @@ public function ajax_add_to_cart() { WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes ); } - if ( 'simple' === $product_type || in_array( $product_type, $subscription_types, true ) ) { + if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation' ], true ) ) { WC()->cart->add_to_cart( $product->get_id(), $qty ); } diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php index d075e69fde0..cb556ae2260 100644 --- a/includes/class-wc-payments-token-service.php +++ b/includes/class-wc-payments-token-service.php @@ -71,9 +71,7 @@ public function add_token_to_user( $payment_method, $user ) { switch ( $payment_method['type'] ) { case Payment_Method::SEPA: $token = new WC_Payment_Token_WCPay_SEPA(); - $gateway_id = WC_Payments_Features::is_upe_deferred_intent_enabled() ? - WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA : - CC_Payment_Gateway::GATEWAY_ID; + $gateway_id = WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA; $token->set_gateway_id( $gateway_id ); $token->set_last4( $payment_method[ Payment_Method::SEPA ]['last4'] ); break; @@ -119,11 +117,7 @@ public function add_payment_method_to_user( $payment_method_id, $user ) { * @return bool True, if payment method type matches gateway, false if otherwise. */ public function is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id ) { - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id; - } else { - return WC_Payments::get_gateway()->id === $gateway_id; - } + return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id; } /** diff --git a/includes/class-wc-payments-upe-blocks-payment-method.php b/includes/class-wc-payments-upe-blocks-payment-method.php deleted file mode 100644 index f354a27adc5..00000000000 --- a/includes/class-wc-payments-upe-blocks-payment-method.php +++ /dev/null @@ -1,39 +0,0 @@ -gateway, 'save_upe_appearance_ajax' ] ); add_action( 'switch_theme', [ $this->gateway, 'clear_upe_appearance_transient' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this->gateway, 'clear_upe_appearance_transient' ] ); - if ( ! WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - add_action( 'wc_ajax_wcpay_create_payment_intent', [ $this->gateway, 'create_payment_intent_ajax' ] ); - add_action( 'wc_ajax_wcpay_update_payment_intent', [ $this->gateway, 'update_payment_intent_ajax' ] ); - } add_action( 'wc_ajax_wcpay_init_setup_intent', [ $this->gateway, 'init_setup_intent_ajax' ] ); add_action( 'wc_ajax_wcpay_log_payment_error', [ $this->gateway, 'log_payment_error_ajax' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 ); - add_action( 'woocommerce_email_before_order_table', [ $this->gateway, 'set_payment_method_title_for_email' ], 10, 3 ); } /** @@ -137,11 +132,7 @@ public function register_scripts() { $script_dependencies[] = 'woocommerce-tokenization-form'; } - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $script = 'dist/upe_with_deferred_intent_creation_checkout'; - } else { - $script = 'dist/upe_checkout'; - } + $script = 'dist/upe_with_deferred_intent_creation_checkout'; WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', $script, $script_dependencies ); } @@ -166,16 +157,9 @@ public function get_payment_fields_js_config() { $payment_fields['upeAppearance'] = get_transient( UPE_Payment_Gateway::UPE_APPEARANCE_TRANSIENT ); $payment_fields['wcBlocksUPEAppearance'] = get_transient( UPE_Payment_Gateway::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart(); - - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $payment_fields['currency'] = get_woocommerce_currency(); - $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); - $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - $payment_fields['checkoutTitle'] = $this->gateway->get_checkout_title(); - $payment_fields['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session(); - $payment_fields['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session(); - } + $payment_fields['currency'] = get_woocommerce_currency(); + $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); + $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); $enabled_billing_fields = []; foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) { @@ -264,20 +248,18 @@ public function get_enabled_payment_method_config() { 'countries' => $payment_method->get_countries(), ]; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); - $settings[ $payment_method_id ]['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session( $payment_method_id ); - $settings[ $payment_method_id ]['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session( $payment_method_id ); - $settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html( - /* translators: link to Stripe testing page */ - $payment_method->get_testing_instructions(), - [ - 'strong' => '', - 'a' => '', - ] - ); - $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); - } + $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); + $settings[ $payment_method_id ]['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session( $payment_method_id ); + $settings[ $payment_method_id ]['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session( $payment_method_id ); + $settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html( + /* translators: link to Stripe testing page */ + $payment_method->get_testing_instructions(), + [ + 'strong' => '', + 'a' => '', + ] + ); + $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); } return $settings; @@ -311,7 +293,7 @@ public function payment_fields() { * before `$this->saved_payment_methods()`. */ $payment_fields = $this->get_payment_fields_js_config(); - $upe_object_name = WC_Payments_Features::is_upe_deferred_intent_enabled() ? 'wcpay_upe_config' : 'wcpayConfig'; + $upe_object_name = 'wcpay_upe_config'; wp_enqueue_script( 'wcpay-upe-checkout' ); add_action( 'wp_footer', diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index a4c164ddbca..181f5837940 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -26,6 +26,11 @@ class WC_Payments_Utils { */ const ORDER_INTENT_CURRENCY_META_KEY = '_wcpay_intent_currency'; + /** + * Force disconnected flag name. + */ + const FORCE_DISCONNECTED_FLAG_NAME = 'wcpaydev_force_disconnected'; + /** * Mirrors JS's createInterpolateElement functionality. * Returns a string where angle brackets expressions are replaced with unescaped html while the rest is escaped. @@ -723,35 +728,25 @@ public static function get_last_refund_from_order_id( $order_id ) { } /** - * Helper function to check whether the user is either in the PO experiment, or has manually enabled PO via the dev tools. + * Helper function to check whether to show default new onboarding flow or as an exception disable it (if specific constant is set) . * * @return boolean */ - public static function should_use_progressive_onboarding_flow(): bool { - if ( self::is_in_progressive_onboarding_treatment_mode() || WC_Payments_Features::is_progressive_onboarding_enabled() ) { - return true; + public static function should_use_new_onboarding_flow(): bool { + if ( defined( 'WCPAY_DISABLE_NEW_ONBOARDING' ) && WCPAY_DISABLE_NEW_ONBOARDING ) { + return false; } - return false; + return true; } /** - * Check to see if the current user is in progressive onboarding experiment treatment mode. + * Checks whether the Force disconnected option is enabled. * * @return bool */ - public static function is_in_progressive_onboarding_treatment_mode(): bool { - if ( ! isset( $_COOKIE['tk_ai'] ) ) { - return false; - } - - $abtest = new \WCPay\Experimental_Abtest( - sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ), - 'woocommerce', - 'yes' === get_option( 'woocommerce_allow_tracking' ) - ); - - return 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v3' ); + public static function force_disconnected_enabled(): bool { + return '1' === get_option( self::FORCE_DISCONNECTED_FLAG_NAME, '0' ); } /** diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index b60e325a126..f3467f9cc8d 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -118,7 +118,7 @@ public function init() { $this->gateway->update_option( 'platform_checkout_button_locations', array_keys( $all_locations ) ); - WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, $all_locations ); + WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, array_keys( $all_locations ) ); } add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); @@ -226,9 +226,23 @@ public function ajax_add_to_cart() { WC()->shipping->reset_shipping(); - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; - $quantity = ! isset( $_POST['quantity'] ) ? 1 : absint( $_POST['quantity'] ); - $product = wc_get_product( $product_id ); + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $quantity = ! isset( $_POST['quantity'] ) ? 1 : absint( $_POST['quantity'] ); + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + wp_send_json( + [ + 'error' => [ + 'code' => 'invalid_product_id', + 'message' => __( 'Invalid product id', 'woocommerce-payments' ), + ], + ], + 404 + ); + return; + } + $product_type = $product->get_type(); // First empty the cart to prevent wrong calculation. @@ -634,6 +648,11 @@ private function is_product_supported() { $is_supported = false; } + // WC Bookings require confirmation products are not supported. + if ( is_a( $product, 'WC_Product_Booking' ) && $product->get_requires_confirmation() ) { + $is_supported = false; + } + return apply_filters( 'wcpay_woopay_button_is_product_supported', $is_supported, $product ); } @@ -647,14 +666,19 @@ private function is_product_supported() { private function has_allowed_items_in_cart() { $is_supported = true; + /** + * Psalm throws an error here even though we check the class existence. + * + * @psalm-suppress UndefinedClass + */ // We don't support pre-order products to be paid upon release. - if ( - class_exists( 'WC_Pre_Orders_Cart' ) && - WC_Pre_Orders_Cart::cart_contains_pre_order() && - class_exists( 'WC_Pre_Orders_Product' ) && - WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) - ) { - $is_supported = false; + if ( class_exists( 'WC_Pre_Orders_Cart' ) && class_exists( 'WC_Pre_Orders_Product' ) ) { + if ( + WC_Pre_Orders_Cart::cart_contains_pre_order() && + WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) + ) { + $is_supported = false; + } } return apply_filters( 'wcpay_platform_checkout_button_are_cart_items_supported', $is_supported ); diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index a167138b7b2..28775dc9599 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -523,41 +523,28 @@ public static function init() { Afterpay_Payment_Method::class, Klarna_Payment_Method::class, ]; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $payment_methods = []; - foreach ( $payment_method_classes as $payment_method_class ) { - $payment_method = new $payment_method_class( self::$token_service ); - $payment_methods[ $payment_method->get_id() ] = $payment_method; - } - foreach ( $payment_methods as $payment_method ) { - self::$upe_payment_method_map[ $payment_method->get_id() ] = $payment_method; - $split_gateway = new UPE_Split_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service ); + $payment_methods = []; + foreach ( $payment_method_classes as $payment_method_class ) { + $payment_method = new $payment_method_class( self::$token_service ); + $payment_methods[ $payment_method->get_id() ] = $payment_method; + } + foreach ( $payment_methods as $payment_method ) { + self::$upe_payment_method_map[ $payment_method->get_id() ] = $payment_method; - // Card gateway hooks are registered once below. - if ( 'card' !== $payment_method->get_id() ) { - $split_gateway->init_hooks(); - } + $split_gateway = new UPE_Split_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service ); - self::$upe_payment_gateway_map[ $payment_method->get_id() ] = $split_gateway; + // Card gateway hooks are registered once below. + if ( 'card' !== $payment_method->get_id() ) { + $split_gateway->init_hooks(); } - self::$card_gateway = self::get_payment_gateway_by_id( 'card' ); - self::$wc_payments_checkout = new WC_Payments_UPE_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - $payment_methods = []; - foreach ( $payment_method_classes as $payment_method_class ) { - $payment_method = new $payment_method_class( self::$token_service ); - $payment_methods[ $payment_method->get_id() ] = $payment_method; - } - - self::$card_gateway = new UPE_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service ); - self::$wc_payments_checkout = new WC_Payments_UPE_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); - } else { - self::$card_gateway = self::$legacy_card_gateway; - self::$wc_payments_checkout = new WC_Payments_Checkout( self::$legacy_card_gateway, self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); + self::$upe_payment_gateway_map[ $payment_method->get_id() ] = $split_gateway; } + self::$card_gateway = self::get_payment_gateway_by_id( 'card' ); + self::$wc_payments_checkout = new WC_Payments_UPE_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); + self::$card_gateway->init_hooks(); self::$wc_payments_checkout->init_hooks(); @@ -591,6 +578,7 @@ public static function init() { add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ], 2 ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ], 3 ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'replace_wcpay_gateway_with_payment_methods' ], 4 ); + add_filter( 'woocommerce_rest_api_option_permissions', [ __CLASS__, 'add_wcpay_options_to_woocommerce_permissions_list' ], 5 ); add_filter( 'woocommerce_admin_get_user_data_fields', [ __CLASS__, 'add_user_data_fields' ] ); // Add note query support for source. @@ -601,10 +589,12 @@ public static function init() { add_action( 'woocommerce_admin_field_payment_gateways', [ __CLASS__, 'hide_gateways_on_settings_page' ], 5 ); require_once __DIR__ . '/migrations/class-allowed-payment-request-button-types-update.php'; + require_once __DIR__ . '/migrations/class-allowed-payment-request-button-sizes-update.php'; require_once __DIR__ . '/migrations/class-update-service-data-from-server.php'; require_once __DIR__ . '/migrations/class-track-upe-status.php'; require_once __DIR__ . '/migrations/class-delete-active-woopay-webhook.php'; add_action( 'woocommerce_woocommerce_payments_updated', [ new Allowed_Payment_Request_Button_Types_Update( self::get_gateway() ), 'maybe_migrate' ] ); + add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Allowed_Payment_Request_Button_Sizes_Update( self::get_gateway() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Update_Service_Data_From_Server( self::get_account_service() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ '\WCPay\Migrations\Track_Upe_Status', 'maybe_track' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ '\WCPay\Migrations\Delete_Active_WooPay_Webhook', 'maybe_delete' ] ); @@ -748,49 +738,41 @@ public static function get_plugin_headers() { * @return array The list of payment gateways that will be available, including WooPayments' Gateway class. */ public static function register_gateway( $gateways ) { - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { + $payment_methods = self::$card_gateway->get_payment_method_ids_enabled_at_checkout(); - $payment_methods = self::$card_gateway->get_payment_method_ids_enabled_at_checkout(); - - $key = array_search( 'link', $payment_methods, true ); - - if ( false !== $key && WC_Payments_Features::is_woopay_enabled() ) { - unset( $payment_methods[ $key ] ); - - self::get_gateway()->update_option( 'upe_enabled_payment_method_ids', $payment_methods ); - } + $key = array_search( 'link', $payment_methods, true ); - self::$registered_card_gateway = self::$card_gateway; + if ( false !== $key && WC_Payments_Features::is_woopay_enabled() ) { + unset( $payment_methods[ $key ] ); - $gateways[] = self::$registered_card_gateway; - $all_upe_gateways = []; - $reusable_methods = []; - foreach ( $payment_methods as $payment_method_id ) { - if ( 'card' === $payment_method_id || 'link' === $payment_method_id ) { - continue; - } - $upe_gateway = self::get_payment_gateway_by_id( $payment_method_id ); - $upe_payment_method = self::get_payment_method_by_id( $payment_method_id ); - - if ( $upe_payment_method->is_reusable() ) { - $reusable_methods[] = $upe_gateway; - } + self::get_gateway()->update_option( 'upe_enabled_payment_method_ids', $payment_methods ); + } - $all_upe_gateways[] = $upe_gateway; + self::$registered_card_gateway = self::$card_gateway; + $gateways[] = self::$registered_card_gateway; + $all_upe_gateways = []; + $reusable_methods = []; + foreach ( $payment_methods as $payment_method_id ) { + if ( 'card' === $payment_method_id || 'link' === $payment_method_id ) { + continue; } + $upe_gateway = self::get_payment_gateway_by_id( $payment_method_id ); + $upe_payment_method = self::get_payment_method_by_id( $payment_method_id ); - if ( is_add_payment_method_page() ) { - return array_merge( $gateways, $reusable_methods ); + if ( $upe_payment_method->is_reusable() ) { + $reusable_methods[] = $upe_gateway; } - return array_merge( $gateways, $all_upe_gateways ); - } elseif ( WC_Payments_Features::is_upe_enabled() ) { - self::$registered_card_gateway = self::$card_gateway; - } else { - self::$registered_card_gateway = self::$legacy_card_gateway; + $all_upe_gateways[] = $upe_gateway; + + } + + if ( is_add_payment_method_page() ) { + return array_merge( $gateways, $reusable_methods ); } - return array_merge( $gateways, [ self::$registered_card_gateway ] ); + + return array_merge( $gateways, $all_upe_gateways ); } /** @@ -1340,16 +1322,8 @@ public static function get_session_service() { */ public static function register_checkout_gateway( $payment_method_registry ) { require_once __DIR__ . '/class-wc-payments-blocks-payment-method.php'; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - require_once __DIR__ . '/class-wc-payments-upe-split-blocks-payment-method.php'; - $payment_method_registry->register( new WC_Payments_UPE_Split_Blocks_Payment_Method() ); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - require_once __DIR__ . '/class-wc-payments-upe-blocks-payment-method.php'; - $payment_method_registry->register( new WC_Payments_UPE_Blocks_Payment_Method() ); - } else { - $payment_method_registry->register( new WC_Payments_Blocks_Payment_Method() ); - } - + require_once __DIR__ . '/class-wc-payments-upe-split-blocks-payment-method.php'; + $payment_method_registry->register( new WC_Payments_UPE_Split_Blocks_Payment_Method() ); } /** @@ -1671,6 +1645,39 @@ public static function enqueue_dev_runtime_scripts() { } } + /** + * Adds WCPay options to Woo Core option allow list. + * + * @param array $permissions Array containing the permissions. + * + * @return array An array containing the modified permissions. + */ + public static function add_wcpay_options_to_woocommerce_permissions_list( $permissions ) { + $wcpay_permissions_list = array_fill_keys( + [ + 'wcpay_frt_discover_banner_settings', + 'wcpay_multi_currency_setup_completed', + 'woocommerce_dismissed_todo_tasks', + 'woocommerce_remind_me_later_todo_tasks', + 'woocommerce_deleted_todo_tasks', + 'wcpay_fraud_protection_welcome_tour_dismissed', + 'wcpay_capability_request_dismissed_notices', + 'wcpay_onboarding_eligibility_modal_dismissed', + ], + true + ); + + if ( is_array( $permissions ) ) { + return array_merge( + $permissions, + $wcpay_permissions_list + ); + } + + return $wcpay_permissions_list; + } + + /** * Creates a new request object for a server call. * diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index 564fd0c5033..123ec606a7b 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -278,8 +278,7 @@ private function tracks_build_event_obj( $user, $event_name, $properties = [] ) $identity = $this->tracks_get_identity( $user->ID ); $site_url = get_option( 'siteurl' ); - //phpcs:ignore WordPress.Security.ValidatedSanitizedInput - $properties['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; + $properties['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ): ''; $properties['blog_url'] = $site_url; $properties['blog_id'] = \Jetpack_Options::get_option( 'id' ); $properties['user_lang'] = $user->get( 'WPLANG' ); @@ -288,6 +287,11 @@ private function tracks_build_event_obj( $user, $event_name, $properties = [] ) $properties['test_mode'] = WC_Payments::mode()->is_test() ? 1 : 0; $properties['wcpay_version'] = WCPAY_VERSION_NUMBER; + // Add client's user agent to the event properties. + if ( !empty( $_SERVER['HTTP_USER_AGENT'] ) ) { + $properties['_via_ua'] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); + } + $blog_details = [ 'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ), ]; diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index 68195675987..80e35dba0de 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -460,7 +460,7 @@ final public static function extend( Request $base_request ) { 'wcpay_core_extend_class_incorrectly' ); } - $obj = new $current_class( $base_request->api_client, $base_request->http_interface ); + $obj = new $current_class( $base_request->api_client, $base_request->http_interface, $base_request->id ?? null ); $obj->set_params( array_merge( static::DEFAULT_PARAMS, $base_request->params ) ); // Carry over the base class and protected mode into the child request. diff --git a/includes/core/server/request/class-list-transactions.md b/includes/core/server/request/class-list-transactions.md index e3f0fb595b1..d791bcc322d 100644 --- a/includes/core/server/request/class-list-transactions.md +++ b/includes/core/server/request/class-list-transactions.md @@ -9,23 +9,29 @@ The `WCPay\Core\Server\Request\List_Transactions` class is used to construct the ## Parameters -| Parameter | Setter | Immutable | Required | Default value | -|----------------------------|------------------------------------------------------------|:---------:|:--------:|:-------------:| -| `customer_currency_is` | `set_customer_currency_is( string $customer_currency_is )` | - | - | - | -| `customer_currency_is_not` | `set_customer_currency_is_not( string $currency )` | - | - | - | -| `deposit_id` | `set_deposit_id( $deposit_id )` | - | - | - | -| `loan_id_is` | `set_loan_id_is( string $loan_id )` | - | - | - | -| `match` | `set_match( string $match )` | - | - | - | -| `page` | `set_page( int $page )` | Yes | - | - | -| `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | -| `search` | `set_search( array $search )` | - | - | - | -| `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | -| `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | -| `store_currency_is` | `set_store_currency_is( string $currency )` | - | - | - | -| `type_is` | `set_type_is( string $type_is )` | - | - | - | -| `type_is_not` | `set_type_is_not( string $type_is_not )` | - | - | - | -| `source_device_is` | `set_source_device_is( string $source_device_is )` | - | - | - | -| `source_device_is_not` | `set_source_device_is_not( string $source_device_is_not )` | - | - | - | +| Parameter | Setter | Immutable | Required | Default value | +|----------------------------|-----------------------------------------------------------------|:---------:|:--------:|:-------------:| +| `customer_currency_is` | `set_customer_currency_is( string $customer_currency_is )` | - | - | - | +| `customer_currency_is_not` | `set_customer_currency_is_not( string $currency )` | - | - | - | +| `deposit_id` | `set_deposit_id( $deposit_id )` | - | - | - | +| `loan_id_is` | `set_loan_id_is( string $loan_id )` | - | - | - | +| `match` | `set_match( string $match )` | - | - | - | +| `page` | `set_page( int $page )` | Yes | - | - | +| `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | +| `search` | `set_search( array $search )` | - | - | - | +| `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | +| `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | +| `store_currency_is` | `set_store_currency_is( string $currency )` | - | - | - | +| `type_is` | `set_type_is( string $type_is )` | - | - | - | +| `type_is_not` | `set_type_is_not( string $type_is_not )` | - | - | - | +| `source_device_is` | `set_source_device_is( string $source_device_is )` | - | - | - | +| `source_device_is_not` | `set_source_device_is_not( string $source_device_is_not )` | - | - | - | +| `channel_is` | `set_channel_is( string $channel_is )` | - | - | - | +| `channel_is_not` | `set_channel_is_not( string $channel_is_not )` | - | - | - | +| `customer_country_is` | `set_customer_country_is( string $customer_country_is )` | - | - | - | +| `customer_country_is_not` | `set_customer_country_is_not( string $customer_country_is_not )`| - | - | - | +| `risk_level_is` | `set_risk_level_is( string $risk_level_is )` | - | - | - | +| `risk_level_is_not` | `set_risk_level_is_not( string $risk_level_is_not )` | - | - | - | ## Filter @@ -52,5 +58,11 @@ $request->set_type_is( $type_is ); $request->set_type_is_not( $type_is_not ); $request->set_source_device_is( $source_device_is ); $request->set_source_device_is_not( $source_device_is_not ); +$request->set_channel_is( $channel_is ); +$request->set_channel_is_not( $channel_is_not ); +$request->set_customer_country_is( $customer_country_is ); +$request->set_customer_country_not( $customer_country_is_not ); +$request->set_risk_level_is( $risk_level_is ); +$request->set_risk_level_is_not( $risk_level_is_not ); $request->send(); ``` diff --git a/includes/core/server/request/class-list-transactions.php b/includes/core/server/request/class-list-transactions.php index 4d10a3555ab..baaa0154a92 100644 --- a/includes/core/server/request/class-list-transactions.php +++ b/includes/core/server/request/class-list-transactions.php @@ -83,6 +83,12 @@ function ( $transaction_date ) use ( $user_timezone ) { 'type_is_not' => $request->get_param( 'type_is_not' ), 'source_device_is' => $request->get_param( 'source_device_is' ), 'source_device_is_not' => $request->get_param( 'source_device_is_not' ), + 'channel_is' => $request->get_param( 'channel_is' ), + 'channel_is_not' => $request->get_param( 'channel_is_not' ), + 'customer_country_is' => $request->get_param( 'customer_country_is' ), + 'customer_country_is_not' => $request->get_param( 'customer_country_is_not' ), + 'risk_level_is' => $request->get_param( 'risk_level_is' ), + 'risk_level_is_not' => $request->get_param( 'risk_level_is_not' ), 'store_currency_is' => $request->get_param( 'store_currency_is' ), 'customer_currency_is' => $request->get_param( 'customer_currency_is' ), 'customer_currency_is_not' => $request->get_param( 'customer_currency_is_not' ), @@ -207,6 +213,72 @@ public function set_source_device_is_not( string $source_device_is_not ) { $this->set_param( 'source_device_is_not', $source_device_is_not ); } + /** + * Set Channel type is. + * + * @param string $channel_is Channel type is. + * + * @return void + */ + public function set_channel_is( string $channel_is ) { + $this->set_param( 'channel_is', $channel_is ); + } + + /** + * Set Channel type is not. + * + * @param string $channel_is_not Channel type is not. + * + * @return void + */ + public function set_channel_is_not( string $channel_is_not ) { + $this->set_param( 'channel_is_not', $channel_is_not ); + } + + /** + * Set Customer country is. + * + * @param string $customer_country_is Customer country is. + * + * @return void + */ + public function set_customer_country_is( string $customer_country_is ) { + $this->set_param( 'customer_country_is', $customer_country_is ); + } + + /** + * Set Customer country is not. + * + * @param string $customer_country_is_not Customer country is not. + * + * @return void + */ + public function set_customer_country_is_not( string $customer_country_is_not ) { + $this->set_param( 'customer_country_is_not', $customer_country_is_not ); + } + + /** + * Set Risk level is. + * + * @param string $risk_level_is Risk level is. + * + * @return void + */ + public function set_risk_level_is( string $risk_level_is ) { + $this->set_param( 'risk_level_is', $risk_level_is ); + } + + /** + * Set Risk level is not. + * + * @param string $risk_level_is_not Risk level is not. + * + * @return void + */ + public function set_risk_level_is_not( string $risk_level_is_not ) { + $this->set_param( 'risk_level_is_not', $risk_level_is_not ); + } + /** * Return formatted response. * diff --git a/includes/migrations/class-allowed-payment-request-button-sizes-update.php b/includes/migrations/class-allowed-payment-request-button-sizes-update.php new file mode 100644 index 00000000000..4be05fa2bf2 --- /dev/null +++ b/includes/migrations/class-allowed-payment-request-button-sizes-update.php @@ -0,0 +1,69 @@ +gateway = $gateway; + } + + /** + * Only execute the migration if not applied yet. + */ + public function maybe_migrate() { + $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); + if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { + $this->migrate(); + } + } + + /** + * Does the actual migration as described in the class docblock. + */ + private function migrate() { + $button_size = $this->gateway->get_option( 'payment_request_button_size' ); + if ( 'default' === $button_size ) { + $this->gateway->update_option( + 'payment_request_button_size', + 'small' + ); + } + + } +} diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php index fd7bbcabb7b..5cc180b55c7 100644 --- a/includes/payment-methods/class-afterpay-payment-method.php +++ b/includes/payment-methods/class-afterpay-payment-method.php @@ -28,7 +28,7 @@ public function __construct( $token_service ) { $this->title = __( 'Afterpay', 'woocommerce-payments' ); $this->is_reusable = false; $this->icon_url = plugins_url( 'assets/images/payment-methods/afterpay.svg', WCPAY_PLUGIN_FILE ); - $this->currencies = [ 'USD', 'CAD', 'AUD', 'NZD', 'GBP', 'EUR' ]; + $this->currencies = [ 'USD', 'CAD', 'AUD', 'NZD', 'GBP' ]; $this->accept_only_domestic_payment = true; $this->limits_per_currency = [ 'AUD' => [ @@ -61,12 +61,6 @@ public function __construct( $token_service ) { 'max' => 400000, ], // Represents USD 1 - 4,000 USD. ], - 'EUR' => [ - 'default' => [ - 'min' => 100, - 'max' => 100000, - ], // Represents EUR 1 - 1,000 EUR. - ], ]; } diff --git a/includes/payment-methods/class-upe-payment-gateway.php b/includes/payment-methods/class-upe-payment-gateway.php index 7023ba0ca2d..a8cfbbfced1 100644 --- a/includes/payment-methods/class-upe-payment-gateway.php +++ b/includes/payment-methods/class-upe-payment-gateway.php @@ -121,19 +121,6 @@ public function __construct( $this->payment_methods = $payment_methods; } - /** - * Initializes this class's WP hooks. - * - * @return void - */ - public function init_hooks() { - // Initializing a hook within this function increases the probability of multiple calls for each split UPE gateway. Consider adding the hook in the parent hook initialization. - if ( ! is_admin() ) { - add_filter( 'woocommerce_gateway_title', [ $this, 'maybe_filter_gateway_title' ], 10, 2 ); - } - parent::init_hooks(); - } - /** * Displays HTML tags for WC payment gateway radio button. */ @@ -273,52 +260,6 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null ]; } - /** - * Handle AJAX request for creating a payment intent for Stripe UPE. - * - * @throws Process_Payment_Exception - If nonce or setup intent is invalid. - */ - public function create_payment_intent_ajax() { - try { - $is_nonce_valid = check_ajax_referer( 'wcpay_create_payment_intent_nonce', false, false ); - if ( ! $is_nonce_valid ) { - throw new Process_Payment_Exception( - __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), - 'wcpay_upe_intent_error' - ); - } - - // If paying from order, we need to get the total from the order instead of the cart. - $order_id = isset( $_POST['wcpay_order_id'] ) ? absint( $_POST['wcpay_order_id'] ) : null; - $fingerprint = isset( $_POST['wcpay-fingerprint'] ) ? wc_clean( wp_unslash( $_POST['wcpay-fingerprint'] ) ) : ''; - - $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout( $order_id, true ); - - $response = $this->create_payment_intent( $enabled_payment_methods, $order_id, $fingerprint ); - - // Encrypt client secret before exposing it to the browser. - if ( $response['client_secret'] ) { - $response['client_secret'] = WC_Payments_Utils::encrypt_client_secret( $this->account->get_stripe_account_id(), $response['client_secret'] ); - } - - if ( strpos( $response['id'], 'pi_' ) === 0 ) { // response is a payment intent (could possibly be a setup intent). - $this->add_upe_payment_intent_to_session( $response['id'], $response['client_secret'] ); - } - - wp_send_json_success( $response, 200 ); - } catch ( Exception $e ) { - // Send back error so it can be displayed to the customer. - wp_send_json_error( - [ - 'error' => [ - 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), - ], - ], - WC_Payments_Utils::get_filtered_error_status_code( $e ), - ); - } - } - /** * Creates payment intent using current cart or order and store details. * @@ -1132,58 +1073,6 @@ public function clear_upe_appearance_transient() { delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); } - /** - * Sets the title on checkout correctly before the title is displayed. - * - * @param string $title The title of the gateway being filtered. - * @param string $id The id of the gateway being filtered. - * - * @return string Filtered gateway title. - */ - public function maybe_filter_gateway_title( $title, $id ) { - if ( ! WC_Payments_Features::is_upe_deferred_intent_enabled() && self::GATEWAY_ID === $id && $this->title === $title ) { - $title = $this->checkout_title; - $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout(); - - if ( 1 === count( $enabled_payment_methods ) ) { - $title = $this->payment_methods[ $enabled_payment_methods[0] ]->get_title(); - } - - if ( 0 === count( $enabled_payment_methods ) ) { - $title = $this->payment_methods['card']->get_title(); - } - } - return $title; - } - - /** - * 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_gateway = wc_get_payment_gateway_by_order( $order ); - - if ( ! empty( $payment_gateway ) && self::GATEWAY_ID !== $payment_gateway->id || ! WC_Payments_Features::is_upe_legacy_enabled() ) { - return; - } - - $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 ); - } - /** * Validate order_id received from the request vs value saved in the intent metadata. * Throw an exception if they're not matched. diff --git a/includes/payment-methods/class-upe-split-payment-gateway.php b/includes/payment-methods/class-upe-split-payment-gateway.php index 9fe50bcdf75..e1fe2be2cff 100644 --- a/includes/payment-methods/class-upe-split-payment-gateway.php +++ b/includes/payment-methods/class-upe-split-payment-gateway.php @@ -104,10 +104,6 @@ public function __construct( * @return void */ public function init_hooks() { - if ( ! WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - add_action( "wc_ajax_wcpay_create_payment_intent_$this->stripe_id", [ $this, 'create_payment_intent_ajax' ] ); - add_action( "wc_ajax_wcpay_update_payment_intent_$this->stripe_id", [ $this, 'update_payment_intent_ajax' ] ); - } add_action( "wc_ajax_wcpay_init_setup_intent_$this->stripe_id", [ $this, 'init_setup_intent_ajax' ] ); parent::init_hooks(); @@ -229,64 +225,6 @@ public function update_payment_intent_ajax() { } } - /** - * Handle AJAX request for creating a payment intent for Stripe UPE. - * - * @throws Process_Payment_Exception - If nonce or setup intent is invalid. - */ - public function create_payment_intent_ajax() { - try { - $is_nonce_valid = check_ajax_referer( 'wcpay_create_payment_intent_nonce', false, false ); - if ( ! $is_nonce_valid ) { - throw new Process_Payment_Exception( - __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), - 'wcpay_upe_intent_error' - ); - } - - // If paying from order, we need to get the total from the order instead of the cart. - $order_id = isset( $_POST['wcpay_order_id'] ) ? absint( $_POST['wcpay_order_id'] ) : null; - $fingerprint = isset( $_POST['wcpay-fingerprint'] ) ? wc_clean( wp_unslash( $_POST['wcpay-fingerprint'] ) ) : ''; - - $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout( $order_id, true ); - if ( ! in_array( $this->payment_method->get_id(), $enabled_payment_methods, true ) ) { - throw new Process_Payment_Exception( - __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), - 'wcpay_upe_intent_error' - ); - } - $displayed_payment_methods = [ $this->payment_method->get_id() ]; - if ( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID === $this->payment_method->get_id() ) { - if ( in_array( Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $enabled_payment_methods, true ) ) { - $displayed_payment_methods[] = Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID; - } - } - - $response = $this->create_payment_intent( $displayed_payment_methods, $order_id, $fingerprint ); - - // Encrypt client secret before exposing it to the browser. - if ( $response['client_secret'] ) { - $response['client_secret'] = WC_Payments_Utils::encrypt_client_secret( $this->account->get_stripe_account_id(), $response['client_secret'] ); - } - - if ( strpos( $response['id'], 'pi_' ) === 0 ) { // response is a payment intent (could possibly be a setup intent). - $this->add_upe_payment_intent_to_session( $response['id'], $response['client_secret'] ); - } - - wp_send_json_success( $response, 200 ); - } catch ( Exception $e ) { - // Send back error so it can be displayed to the customer. - wp_send_json_error( - [ - 'error' => [ - 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), - ], - ], - WC_Payments_Utils::get_filtered_error_status_code( $e ), - ); - } - } - /** * Handle AJAX request for creating a setup intent without confirmation for Stripe UPE. * diff --git a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php index 09c2e191816..a9abef775e8 100644 --- a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php +++ b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php @@ -62,14 +62,20 @@ public function get_authorizations( $request ) { $wcpay_request = List_Authorizations::from_rest_request( $request ); $wcpay_request->set_page_size( $request->get_param( 'per_page' ) ?? 25 ); - $date_between_filter = $request->get_param( 'date_between' ); - $user_timezone = $request->get_param( 'user_timezone' ); - $filters = [ + $user_timezone = $request->get_param( 'user_timezone' ); + $filters = [ 'match' => $request->get_param( 'match' ), 'order_id_is' => $request->get_param( 'order_id' ), 'customer_email_is' => $request->get_param( 'customer_email' ), 'source_is' => $request->get_param( 'payment_method_type' ), ]; + + if ( $request->get_param( 'date_between' ) ) { + $date_between_filter = $request->get_param( 'date_between' ); + $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $date_between_filter[0], $user_timezone ) ); + $filters['to_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $date_between_filter[1], $user_timezone ) ); + } + if ( $request->get_param( 'date_before' ) ) { $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_before' ), $user_timezone ) ); } @@ -238,7 +244,7 @@ public function get_collection_params() { 'description' => __( 'Field on which to sort.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, - 'default' => 'date', + 'default' => 'created', ], 'direction' => [ 'description' => __( 'Direction on which to sort.', 'woocommerce-payments' ), diff --git a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php index a38038a247a..983c5f86ac0 100644 --- a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php +++ b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php @@ -22,7 +22,7 @@ printf( // Translators: %1-%4 placeholders are opening and closing a or strong HTML tags. %5$s: WooPayments, %6$s: Woo Subscriptions. esc_html__( 'Your store has subscriptions using %5$s Stripe Billing functionality for payment processing. Due to the %1$soff-site billing engine%2$s these subscriptions use,%3$s they will continue to renew even after you deactivate %6$s%4$s.', 'woocommerce-payments' ), - '', + '', '', '', '', diff --git a/includes/subscriptions/templates/html-wcpay-deactivate-warning.php b/includes/subscriptions/templates/html-wcpay-deactivate-warning.php index 25c443258ed..e111793e290 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/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php b/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php index 05b54e6f36a..bfbd8f32b3a 100644 --- a/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php +++ b/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php @@ -22,8 +22,8 @@ printf( // Translators: placeholders are opening and closing strong HTML tags. %6$s: WooPayments, %7$s: Woo Subscriptions. esc_html__( 'Your store has subscriptions using %6$s Stripe Billing functionality for payment processing. Due to the %1$soff-site billing engine%3$s these subscriptions use,%4$s they will continue to renew even after you deactivate %6$s%5$s.', 'woocommerce-payments' ), - '', - '', + '', + '', '', '', '', 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 13b7f43e3be..4416906767d 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -2097,6 +2097,8 @@ public function build_order_info( WC_Order $order ): array { 'number' => $order->get_order_number(), 'url' => $order->get_edit_order_url(), 'customer_url' => $this->get_customer_url( $order ), + 'customer_name' => trim( $order->get_formatted_billing_full_name() ), + 'customer_email' => $order->get_billing_email(), 'fraud_meta_box_type' => $order->get_meta( '_wcpay_fraud_meta_box_type' ), ]; @@ -2396,13 +2398,15 @@ public function get_woopay_compatibility() { /** * Delete account. * + * @param bool $test_mode Whether we are in test mode or not. + * * @return array * @throws API_Exception */ - public function delete_account() { + public function delete_account( bool $test_mode = false ) { return $this->request( [ - 'test_mode' => WC_Payments::mode()->is_dev(), // only send a test mode request if in dev mode. + 'test_mode' => $test_mode, ], self::ACCOUNTS_API . '/delete', self::POST, diff --git a/includes/woopay/class-woopay-adapted-extensions.php b/includes/woopay/class-woopay-adapted-extensions.php index e5a602dca9f..cd6ed56f2d0 100644 --- a/includes/woopay/class-woopay-adapted-extensions.php +++ b/includes/woopay/class-woopay-adapted-extensions.php @@ -180,6 +180,14 @@ public function get_extension_data() { ]; } + if ( $this->is_automate_woo_referrals_enabled() ) { + $advocate_id = $this->get_automate_woo_advocate_id_from_cookie(); + + $extension_data[ 'automatewoo-referrals' ] = [ + 'advocate_id' => $advocate_id, + ]; + } + return $extension_data; } @@ -236,4 +244,35 @@ class_exists( 'AFWC_API' ) && method_exists( 'AFWC_API', 'get_instance' ) && method_exists( 'AFWC_API', 'track_conversion' ); } + + /** + * Check if Automate Woo Referrals is enabled and + * its functions used on WCPay are available. + * + * @psalm-suppress UndefinedClass + * @psalm-suppress UndefinedFunction + * + * @return boolean + */ + private function is_automate_woo_referrals_enabled() { + return function_exists( 'AW_Referrals' ) && + method_exists( AW_Referrals(), 'options' ) && + AW_Referrals()->options()->type === 'link' && + class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) && + method_exists( \AutomateWoo\Referrals\Referral_Manager::class, 'get_advocate_key_from_cookie' ) && class_exists( 'AFWC_API' ) && + method_exists( 'AFWC_API', 'get_instance' ) && + method_exists( 'AFWC_API', 'track_conversion' ); + } + + /** + * Get AutomateWoo advocate id from cookie. + * + * @psalm-suppress UndefinedClass + * + * @return string|null + */ + private function get_automate_woo_advocate_id_from_cookie() { + $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie(); + return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null; + } } diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 29952ce5f9f..5fb2af81ef5 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -61,6 +61,8 @@ public static function init() { add_action( 'woopay_restore_order_customer_id', [ __CLASS__, 'restore_order_customer_id_from_requests_with_verified_email' ] ); register_deactivation_hook( WCPAY_PLUGIN_FILE, [ __CLASS__, 'run_and_remove_woopay_restore_order_customer_id_schedules' ] ); + + add_filter( 'automatewoo/referrals/referred_order_advocate', [ __CLASS__, 'automatewoo_refer_a_friend_referral_from_parameter' ] ); } /** @@ -256,6 +258,20 @@ public static function run_and_remove_woopay_restore_order_customer_id_schedules wp_clear_scheduled_hook( 'woopay_restore_order_customer_id' ); } + /** + * Fix for AutomateWoo - Refer A Friend Add-on + * plugin when using link referrals. + */ + public static function automatewoo_refer_a_friend_referral_from_parameter() { + if ( empty( $_GET['automatewoo_referral_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return false; + } + + $automatewoo_referral = (int) wc_clean( wp_unslash( $_GET['automatewoo_referral_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + + return $automatewoo_referral; + } + /** * Returns the payload from a cart token. * diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php index a9837776291..dc13d5c9d46 100644 --- a/includes/woopay/class-woopay-utilities.php +++ b/includes/woopay/class-woopay-utilities.php @@ -87,6 +87,19 @@ public function is_woopay_express_checkout_enabled() { public function is_woopay_first_party_auth_enabled() { return WC_Payments_Features::is_woopay_first_party_auth_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag. } + + /** + * Determines if the WooPay email input hooks should be enabled. + * + * This function doesn't affect the appearance of the email input, + * only whether or not the email exists check or auto-redirection should be enabled. + * + * @return bool + */ + public function is_woopay_email_input_enabled() { + return apply_filters( 'wcpay_is_woopay_email_input_enabled', true ); + } + /** * Generates a hash based on the store's blog token, merchant ID, and the time step window. * diff --git a/includes/woopay/services/class-checkout-service.php b/includes/woopay/services/class-checkout-service.php index d11d3135700..f10ca7bad7b 100644 --- a/includes/woopay/services/class-checkout-service.php +++ b/includes/woopay/services/class-checkout-service.php @@ -68,7 +68,7 @@ public function is_platform_payment_method( Payment_Information $payment_informa return false; } - $should_use_stripe_platform = WC_Payments_Features::is_upe_deferred_intent_enabled() ? \WC_Payments::get_payment_gateway_by_id( $payment_information->get_payment_method_stripe_id() )->should_use_stripe_platform_on_checkout_page() : \WC_Payments::get_gateway()->should_use_stripe_platform_on_checkout_page(); + $should_use_stripe_platform = \WC_Payments::get_payment_gateway_by_id( $payment_information->get_payment_method_stripe_id() )->should_use_stripe_platform_on_checkout_page(); // Make sure the payment method being charged was created in the platform. if ( diff --git a/package-lock.json b/package-lock.json index 508c0e6ac4f..3dcb176adcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "woocommerce-payments", - "version": "6.8.0", + "version": "6.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "woocommerce-payments", - "version": "6.8.0", + "version": "6.9.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { @@ -40,7 +40,7 @@ "@typescript-eslint/parser": "4.15.2", "@woocommerce/api": "0.2.0", "@woocommerce/components": "12.0.0", - "@woocommerce/csv-export": "1.7.0", + "@woocommerce/csv-export": "1.8.0", "@woocommerce/currency": "4.2.0", "@woocommerce/date": "4.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", @@ -9881,6 +9881,16 @@ "@types/wordpress__rich-text": "*" } }, + "node_modules/@woocommerce/components/node_modules/@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "dependencies": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "node_modules/@woocommerce/components/node_modules/@wordpress/api-fetch": { "version": "6.30.0", "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.30.0.tgz", @@ -10380,15 +10390,26 @@ } }, "node_modules/@woocommerce/csv-export": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", - "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.8.0.tgz", + "integrity": "sha512-LfbwPWu1fyN3lNcuigiJ7g86Ute7rRQokbRkUP5m48wCrgxWmulmbXJD19642oXNLM1G8JY1UM0VFNIn7qMI3w==", "dev": true, "dependencies": { + "@types/node": "^16.18.18", "browser-filesaver": "^1.1.1", "moment": "^2.29.1" + }, + "engines": { + "node": "^16.14.1", + "pnpm": "^8.6.7" } }, + "node_modules/@woocommerce/csv-export/node_modules/@types/node": { + "version": "16.18.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz", + "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==", + "dev": true + }, "node_modules/@woocommerce/currency": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@woocommerce/currency/-/currency-4.2.0.tgz", @@ -11266,6 +11287,16 @@ "react-dom": "^17.0.0" } }, + "node_modules/@woocommerce/experimental/node_modules/@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "dependencies": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "node_modules/@woocommerce/experimental/node_modules/core-js": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", @@ -51349,6 +51380,16 @@ "@types/wordpress__rich-text": "*" } }, + "@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "requires": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "@wordpress/api-fetch": { "version": "6.30.0", "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.30.0.tgz", @@ -51768,13 +51809,22 @@ } }, "@woocommerce/csv-export": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", - "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.8.0.tgz", + "integrity": "sha512-LfbwPWu1fyN3lNcuigiJ7g86Ute7rRQokbRkUP5m48wCrgxWmulmbXJD19642oXNLM1G8JY1UM0VFNIn7qMI3w==", "dev": true, "requires": { + "@types/node": "^16.18.18", "browser-filesaver": "^1.1.1", "moment": "^2.29.1" + }, + "dependencies": { + "@types/node": { + "version": "16.18.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz", + "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==", + "dev": true + } } }, "@woocommerce/currency": { @@ -52497,6 +52547,16 @@ "react-transition-group": "^4.4.2" } }, + "@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "requires": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "core-js": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", diff --git a/package.json b/package.json index 743122e5a15..8ad150e193e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-payments", - "version": "6.8.0", + "version": "6.9.0", "main": "webpack.config.js", "author": "Automattic", "license": "GPL-3.0-or-later", @@ -99,7 +99,7 @@ "@typescript-eslint/parser": "4.15.2", "@woocommerce/api": "0.2.0", "@woocommerce/components": "12.0.0", - "@woocommerce/csv-export": "1.7.0", + "@woocommerce/csv-export": "1.8.0", "@woocommerce/currency": "4.2.0", "@woocommerce/date": "4.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", diff --git a/readme.txt b/readme.txt index ef83e11eb59..1f28867184a 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment Requires at least: 6.0 Tested up to: 6.4 Requires PHP: 7.3 -Stable tag: 6.8.0 +Stable tag: 6.9.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -94,6 +94,55 @@ Please note that our support for the checkout block is still experimental and th == Changelog == += 6.9.0 - 2023-12-06 = +* Add - Added cleanup code after Payment Processing - RPP. +* Add - Adds new option to track dismissal of PO eligibility modal. +* Add - Display an error banner on the connect page when the WooCommerce country is not supported. +* Add - Filter to disable WooPay checkout auto-redirect and email input hooks. +* Add - Handle failed transaction rate limiter in RPP. +* Add - Handle fraud prevention service in InitialState (project RPP). +* Add - Handle mimium amount in InitialState (project RPP). +* Add - Introduce filters for channel, customer country, and risk level on the transactions list page. +* Add - Store the working mode of the gateway (RPP). +* Fix - Add AutomateWoo - Refer A Friend Add-On support on WooPay. +* Fix - Add date_between filter for Authorization Reporting API. +* Fix - Add invalid product id error check. +* Fix - Allow Gradual signup accounts to continue with the Gradual KYC after abandoning it. +* Fix - Allow requests with item IDs to be extended without exceptions. +* Fix - Check that the email is set in the post global. +* Fix - Display notice when clicking the WooPay button if variable product selection is incomplete. +* Fix - Do not show the WooPay button on the product page when WC Bookings require confirmation. +* Fix - Enable deferred intent creation when initialization process encounters cache unavailability. +* Fix - Ensure express payment methods (Google and Apple Pay) correctly reflect eligible shipping methods after closing and reattempting payment. +* Fix - Fixes a redirect to show the new onboarding when coming from WC Core. +* Fix - Fix saved card payments not working on block checkout while card testing prevention is active. +* Fix - Pass the pay-for-order params to the first-party auth flow. +* Fix - Prevent merchants to access onboarding again after starting it in new flow. +* Fix - Remove unsupported EUR currency from Afterpay payment method. +* Fix - Show Payments menu sub-items only for merchants that completed KYC. +* Fix - Support 'variation' product type when re-adding items to a cart. +* Fix - When rendering customer reference in transaction details, fallback to order data. +* Fix - When rendering customer reference on transaction details page, handle case with name being not provided in the order. +* Update - Change PRB default height for new installations. +* Update - Cleanup the deprecated payment gateway processing - part I. +* Update - Correct some links that now lead to better documentation. +* Update - Enable the new onboarding flow as default for all users. +* Update - Exclude estimated deposits from the deposits list screen. +* Update - Improvements to the dev mode and test mode indicators. +* Update - Remove estimated status option from the advanced filters on the deposits list screen. +* Update - Replace the deposit overview transactions list with a "transaction history is unavailable for instant deposits" message. +* Update - Update Payments Overview deposits UI to simplify how we communicate upcoming deposits. +* Update - Update to the new onboarding builder flow to not prefill country/address to US. +* Dev - Add client user-agent value to Tracks event props. +* Dev - Add E2E tests for Affirm and Afterpay checkouts. +* Dev - Add E2E tests for checking out with Giropay. +* Dev - Added customer details management within the re-engineered payment process. +* Dev - Adds WCPay options to Woo Core option allow list to avoid 403 responses from Options API when getting and updating options in non-prod env. +* Dev - Bump WC tested up to version to 8.3.1. +* Dev - Fix a bug in WooPay button update Tracks. +* Dev - Introduce filter `wcpay_payment_request_is_cart_supported`. Allow plugins to conditionally disable payment request buttons on cart and checkout pages containing products that do not support them. +* Dev - Upgrade the csv-export JS package to the latest version. + = 6.8.0 - 2023-11-16 = * Add - Added mechanism to track and log changes to the payment context (reengineering payment process) * Add - Add rejected payment method capability status diff --git a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php index e4467539f14..eeca1e94153 100644 --- a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php +++ b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php @@ -14,6 +14,7 @@ use WCPay\Database_Cache; use WCPay\Internal\Logger; use WCPay\Internal\DependencyManagement\AbstractServiceProvider; +use WCPay\Internal\Payment\FailedTransactionRateLimiter; use WCPay\Internal\Payment\Router; use WCPay\Internal\Payment\State\AuthenticationRequiredState; use WCPay\Internal\Payment\State\CompletedState; @@ -25,8 +26,10 @@ use WCPay\Internal\Payment\State\SystemErrorState; use WCPay\Internal\Proxy\HooksProxy; use WCPay\Internal\Proxy\LegacyProxy; +use WCPay\Internal\Service\MinimumAmountService; use WCPay\Internal\Service\PaymentContextLoggerService; use WCPay\Internal\Service\DuplicatePaymentPreventionService; +use WCPay\Internal\Service\FraudPreventionService; use WCPay\Internal\Service\PaymentProcessingService; use WCPay\Internal\Service\ExampleService; use WCPay\Internal\Service\ExampleServiceWithDependencies; @@ -47,6 +50,7 @@ class PaymentsServiceProvider extends AbstractServiceProvider { protected $provides = [ PaymentProcessingService::class, Router::class, + StateFactory::class, InitialState::class, DuplicateOrderDetectedState::class, @@ -55,10 +59,14 @@ class PaymentsServiceProvider extends AbstractServiceProvider { CompletedState::class, SystemErrorState::class, PaymentErrorState::class, + ExampleService::class, ExampleServiceWithDependencies::class, PaymentRequestService::class, DuplicatePaymentPreventionService::class, + MinimumAmountService::class, + FraudPreventionService::class, + FailedTransactionRateLimiter::class, ]; /** @@ -73,7 +81,8 @@ public function register(): void { $container->addShared( PaymentProcessingService::class ) ->addArgument( StateFactory::class ) ->addArgument( LegacyProxy::class ) - ->addArgument( PaymentContextLoggerService::class ); + ->addArgument( PaymentContextLoggerService::class ) + ->addArgument( Mode::class ); $container->addShared( PaymentRequestService::class ); @@ -84,18 +93,33 @@ public function register(): void { ->addArgument( HooksProxy::class ) ->addArgument( LegacyProxy::class ); + $container->addShared( MinimumAmountService::class ) + ->addArgument( LegacyProxy::class ); + + $container->addShared( FraudPreventionService::class ) + ->addArgument( SessionService::class ) + ->addArgument( \WC_Payments_Account::class ); + + $container->addShared( FailedTransactionRateLimiter::class ) + ->addArgument( SessionService::class ) + ->addArgument( LegacyProxy::class ); + $container->add( InitialState::class ) ->addArgument( StateFactory::class ) ->addArgument( OrderService::class ) ->addArgument( WC_Payments_Customer_Service::class ) ->addArgument( Level3Service::class ) ->addArgument( PaymentRequestService::class ) - ->addArgument( DuplicatePaymentPreventionService::class ); + ->addArgument( DuplicatePaymentPreventionService::class ) + ->addArgument( MinimumAmountService::class ) + ->addArgument( FraudPreventionService::class ) + ->addArgument( FailedTransactionRateLimiter::class ); $container->add( ProcessedState::class ) ->addArgument( StateFactory::class ) ->addArgument( OrderService::class ) - ->addArgument( DuplicatePaymentPreventionService::class ); + ->addArgument( DuplicatePaymentPreventionService::class ) + ->addArgument( LegacyProxy::class ); $container->add( AuthenticationRequiredState::class ) ->addArgument( StateFactory::class ); diff --git a/src/Internal/Payment/AbstractSessionRateLimiter.php b/src/Internal/Payment/AbstractSessionRateLimiter.php new file mode 100644 index 00000000000..f85754978aa --- /dev/null +++ b/src/Internal/Payment/AbstractSessionRateLimiter.php @@ -0,0 +1,116 @@ +key = $key; + $this->threshold = $threshold; + $this->delay = $delay; + $this->session_service = $session_service; + $this->legacy_proxy = $legacy_proxy; + } + + /** + * Saves an event in an specified registry using a key. + * If the number of events in the registry match the threshold, + * a new rate limiter is enabled with the given delay. + * + * The registry of declined card attemps is cleaned after a new rate limiter is enabled. + */ + final public function bump() { + $registry = $this->session_service->get( $this->key ) ?? []; + $registry[] = $this->legacy_proxy->call_function( 'time' ); + + $this->session_service->set( $this->key, $registry ); + } + + /** + * Checks if the rate limiter is enabled. + * + * Returns a boolean. + * + * @return bool The rate limiter is in use. + */ + final public function is_limited(): bool { + if ( 'yes' === $this->legacy_proxy->call_function( 'get_option', 'wcpay_session_rate_limiter_disabled_' . $this->key ) ) { + return false; + } + + $registry = $this->session_service->get( $this->key ) ?? []; + + if ( ( is_countable( $registry ) ? count( $registry ) : 0 ) >= $this->threshold ) { + $start_time_limiter = end( $registry ); + $next_try_allowed_at = $start_time_limiter + $this->delay; + $is_limited = time() <= $next_try_allowed_at; + if ( ! $is_limited ) { + $this->session_service->set( $this->key, [] ); + } + + return $is_limited; + } + + return false; + } +} diff --git a/src/Internal/Payment/FailedTransactionRateLimiter.php b/src/Internal/Payment/FailedTransactionRateLimiter.php new file mode 100644 index 00000000000..7c7a7771579 --- /dev/null +++ b/src/Internal/Payment/FailedTransactionRateLimiter.php @@ -0,0 +1,47 @@ +set( 'user_id', $user_id ); } @@ -291,6 +291,24 @@ public function get_intent(): ?WC_Payments_API_Abstract_Intention { return $this->get( 'intent' ); } + /** + * Stores the fraud prevention token. + * + * @param string $token Token from request. + */ + public function set_fraud_prevention_token( string $token ) { + $this->set( 'fraud_prevention_token', $token ); + } + + /** + * Returns the fraud prevention token. + * + * @return string|null + */ + public function get_fraud_prevention_token(): ?string { + return $this->get( 'fraud_prevention_token' ); + } + /** * Returns the transitions array. * @@ -300,6 +318,24 @@ public function get_transitions(): array { return $this->transitions; } + /** + * Sets the mode (test or prod). + * + * @param string $mode mode. + */ + public function set_mode( string $mode ) { + $this->set( 'mode', $mode ); + } + + /** + * Returns the mode (test or prod). + * + * @return string|null mode. + */ + public function get_mode(): ?string { + return $this->get( 'mode' ); + } + /** * Updates previous transition with the next state and creates new transition. * diff --git a/src/Internal/Payment/State/InitialState.php b/src/Internal/Payment/State/InitialState.php index 3041c93f990..5f266a3233b 100644 --- a/src/Internal/Payment/State/InitialState.php +++ b/src/Internal/Payment/State/InitialState.php @@ -7,11 +7,16 @@ namespace WCPay\Internal\Payment\State; +use WCPay\Exceptions\API_Exception; +use WCPay\Internal\Payment\FailedTransactionRateLimiter; use WC_Payments_Customer_Service; use WCPay\Constants\Intent_Status; use WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception; use WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; +use WCPay\Exceptions\Amount_Too_Small_Exception; +use WCPay\Internal\Service\MinimumAmountService; +use WCPay\Internal\Service\FraudPreventionService; use WCPay\Internal\Service\PaymentRequestService; use WCPay\Internal\Service\DuplicatePaymentPreventionService; use WCPay\Vendor\League\Container\Exception\ContainerException; @@ -61,6 +66,27 @@ class InitialState extends AbstractPaymentState { */ private $dpps; + /** + * Service for handling minimum amount. + * + * @var MinimumAmountService + */ + private $minimum_amount_service; + + /** + * FraudPreventionService instance. + * + * @var FraudPreventionService + */ + private $fraud_prevention_service; + + /** + * FailedTransactionRateLimiter instance. + * + * @var FailedTransactionRateLimiter + */ + private $failed_transaction_rate_limiter; + /** * Class constructor, only meant for storing dependencies. * @@ -70,6 +96,9 @@ class InitialState extends AbstractPaymentState { * @param Level3Service $level3_service Service for Level3 Data. * @param PaymentRequestService $payment_request_service Connection with the server. * @param DuplicatePaymentPreventionService $dpps Service for preventing duplicate payments. + * @param MinimumAmountService $minimum_amount_service Service for handling minimum amount. + * @param FraudPreventionService $fraud_prevention_service Service for preventing fraud payments. + * @param FailedTransactionRateLimiter $failed_transaction_rate_limiter Failed Transaction Rate Limiter instance. */ public function __construct( StateFactory $state_factory, @@ -77,15 +106,21 @@ public function __construct( WC_Payments_Customer_Service $customer_service, Level3Service $level3_service, PaymentRequestService $payment_request_service, - DuplicatePaymentPreventionService $dpps + DuplicatePaymentPreventionService $dpps, + MinimumAmountService $minimum_amount_service, + FraudPreventionService $fraud_prevention_service, + FailedTransactionRateLimiter $failed_transaction_rate_limiter ) { parent::__construct( $state_factory ); - $this->order_service = $order_service; - $this->customer_service = $customer_service; - $this->level3_service = $level3_service; - $this->payment_request_service = $payment_request_service; - $this->dpps = $dpps; + $this->order_service = $order_service; + $this->customer_service = $customer_service; + $this->level3_service = $level3_service; + $this->payment_request_service = $payment_request_service; + $this->dpps = $dpps; + $this->minimum_amount_service = $minimum_amount_service; + $this->fraud_prevention_service = $fraud_prevention_service; + $this->failed_transaction_rate_limiter = $failed_transaction_rate_limiter; } /** @@ -93,11 +128,13 @@ public function __construct( * * @param PaymentRequest $request The incoming payment processing request. * - * @return AbstractPaymentState The next state. - * @throws StateTransitionException In case the completed state could not be initialized. - * @throws ContainerException When the dependency container cannot instantiate the state. - * @throws Order_Not_Found_Exception Order could not be found. - * @throws PaymentRequestException When data is not available or invalid. + * @return AbstractPaymentState The next state. + * @throws StateTransitionException In case the completed state could not be initialized. + * @throws ContainerException When the dependency container cannot instantiate the state. + * @throws Order_Not_Found_Exception Order could not be found. + * @throws PaymentRequestException When data is not available or invalid. + * @throws API_Exception When server request fails. + * @throws Amount_Too_Small_Exception When the order amount is too small. */ public function start_processing( PaymentRequest $request ) { // Populate basic details from the request. @@ -109,6 +146,22 @@ public function start_processing( PaymentRequest $request ) { // Start multiple verification checks. $this->process_order_phone_number(); + $context = $this->get_context(); + + if ( ! $this->fraud_prevention_service->verify_token( $context->get_fraud_prevention_token() ) ) { + throw new StateTransitionException( + __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ) + ); + } + + if ( $this->failed_transaction_rate_limiter->is_limited() ) { + $this->order_service->add_rate_limiter_note( $context->get_order_id() ); + throw new StateTransitionException( + __( 'Your payment was not processed.', 'woocommerce-payments' ), + 400 + ); + } + $duplicate_order_result = $this->process_duplicate_order(); if ( null !== $duplicate_order_result ) { return $duplicate_order_result; @@ -118,20 +171,47 @@ public function start_processing( PaymentRequest $request ) { if ( null !== $duplicate_payment_result ) { return $duplicate_payment_result; } + + $this->minimum_amount_service->verify_amount( + $context->get_currency(), + $context->get_amount() + ); // End multiple verification checks. - // Payments are currently based on intents, request one from the API. + /** + * Payments are based on intents, and intents use customer objects for billing details. + * + * The customer is created/updated right before requesting the creation of + * a payment intent, and the two actions must be adjacent to each-other. + */ try { - $context = $this->get_context(); - $intent = $this->payment_request_service->create_intent( $context ); + $order_id = $context->get_order_id(); + + // Create or update customer and customer details. + $customer_id = $this->customer_service->get_or_create_customer_id_from_order( + $context->get_user_id(), + $this->order_service->_deprecated_get_order( $order_id ) + ); + $context->set_customer_id( $customer_id ); + + // After customer is updated or created, make sure that intent is created. + $intent = $this->payment_request_service->create_intent( $context ); $context->set_intent( $intent ); + } catch ( Amount_Too_Small_Exception $e ) { + $this->minimum_amount_service->store_amount_from_exception( $e ); + throw $e; } catch ( Invalid_Request_Parameter_Exception | Extend_Request_Exception | Immutable_Parameter_Exception $e ) { return $this->create_state( SystemErrorState::class ); + } catch ( API_Exception $e ) { + if ( $this->failed_transaction_rate_limiter->should_bump_rate_limiter( $e->get_error_code() ) ) { + $this->failed_transaction_rate_limiter->bump(); + } + throw $e; } // Intent requires authorization (3DS check). if ( Intent_Status::REQUIRES_ACTION === $intent->get_status() ) { - $this->order_service->update_order_from_intent_that_requires_action( $context->get_order_id(), $intent, $context ); + $this->order_service->update_order_from_intent_that_requires_action( $order_id, $intent, $context ); return $this->create_state( AuthenticationRequiredState::class ); } @@ -166,11 +246,15 @@ protected function populate_context_from_request( PaymentRequest $request ) { if ( ! is_null( $fingerprint ) ) { $context->set_fingerprint( $fingerprint ); } + + $fraud_prevention_token = $request->get_fraud_prevention_token(); + if ( ! is_null( $fraud_prevention_token ) ) { + $context->set_fraud_prevention_token( $fraud_prevention_token ); + } } /** * Populates the context with details, available in the order. - * This includes the update/creation of a customer. * * @throws Order_Not_Found_Exception In case the order could not be found. */ @@ -187,13 +271,6 @@ protected function populate_context_from_order() { ) ); $context->set_level3_data( $this->level3_service->get_data_from_order( $order_id ) ); - - // Customer management involves a remote call. - $customer_id = $this->customer_service->get_or_create_customer_id_from_order( - $context->get_user_id(), - $this->order_service->_deprecated_get_order( $order_id ) - ); - $context->set_customer_id( $customer_id ); } /** diff --git a/src/Internal/Payment/State/ProcessedState.php b/src/Internal/Payment/State/ProcessedState.php index 9ad557eb019..a7f222a0caf 100644 --- a/src/Internal/Payment/State/ProcessedState.php +++ b/src/Internal/Payment/State/ProcessedState.php @@ -12,6 +12,9 @@ use WCPay\Internal\Service\DuplicatePaymentPreventionService; use WCPay\Internal\Service\OrderService; use WCPay\Vendor\League\Container\Exception\ContainerException; +use WCPay\Internal\Proxy\LegacyProxy; +use WCPay\Payment_Methods\UPE_Payment_Gateway; +use WCPay\Payment_Methods\UPE_Split_Payment_Gateway; /** * This state is used when payment is completed on the server, and we need to update date on the plugin side. @@ -31,22 +34,32 @@ class ProcessedState extends AbstractPaymentState { */ private $dpps; + /** + * Legacy proxy. + * + * @var LegacyProxy + */ + private $legacy_proxy; + /** * Class constructor, only meant for storing dependencies. * * @param StateFactory $state_factory Factory for payment states. * @param OrderService $order_service Service for order-related actions. * @param DuplicatePaymentPreventionService $dpps Service for preventing duplicate payments. + * @param LegacyProxy $legacy_proxy Legacy proxy. */ public function __construct( StateFactory $state_factory, OrderService $order_service, - DuplicatePaymentPreventionService $dpps + DuplicatePaymentPreventionService $dpps, + LegacyProxy $legacy_proxy ) { parent::__construct( $state_factory ); $this->order_service = $order_service; $this->dpps = $dpps; + $this->legacy_proxy = $legacy_proxy; } /** @@ -65,7 +78,34 @@ public function complete_processing() { $this->dpps->remove_session_processing_order( $order_id ); $this->order_service->update_order_from_successful_intent( $order_id, $context->get_intent(), $context ); + // cleaning up. + $this->legacy_proxy->call_function( 'wc_reduce_stock_levels', $order_id ); + $this->clear_cart(); + $this->clear_upe_payment_intent_from_session(); + // If everything went well, transition to the completed state. return $this->create_state( CompletedState::class ); } + + /** + * Clear the cart. + * + * @return void + */ + private function clear_cart() { + $cart = $this->legacy_proxy->call_function( 'wc' )->cart; + if ( isset( $cart ) ) { + $cart->empty_cart(); + } + } + + /** + * Remove UPE payment intents from session. + * Using Legacy_Proxy temporarily to provide functionality until replaced by deferred intents. + */ + private function clear_upe_payment_intent_from_session() : void { + $this->legacy_proxy->call_static( UPE_Payment_Gateway::class, 'remove_upe_payment_intent_from_session' ); + $this->legacy_proxy->call_static( UPE_Split_Payment_Gateway::class, 'remove_upe_payment_intent_from_session' ); + } + } diff --git a/src/Internal/Service/FraudPreventionService.php b/src/Internal/Service/FraudPreventionService.php new file mode 100644 index 00000000000..9e7152c9a72 --- /dev/null +++ b/src/Internal/Service/FraudPreventionService.php @@ -0,0 +1,96 @@ +session_service = $session_service; + $this->account_service = $account_service; + } + + /** + * Checks if fraud prevention feature is enabled for the account. + * + * @return bool + */ + public function is_enabled(): bool { + return $this->account_service->is_card_testing_protection_eligible(); + } + + /** + * Returns current valid token. + * + * For the first page load generates the token, + * for consecutive loads - takes from session. + * + * @return string + */ + public function get_token(): string { + return $this->session_service->get( self::TOKEN_NAME ) ?? $this->regenerate_token(); + } + + /** + * Generates a new token, persists in session and returns for immediate use. + * + * @return string + */ + public function regenerate_token(): string { + $token = wp_generate_password( 16, false ); + $this->session_service->set( self::TOKEN_NAME, $token ); + return $token; + } + + /** + * Verifies the token against POST data. + * + * @param string|null $token Token sent in request. + * @return bool + */ + public function verify_token( ?string $token = null ): bool { + if ( ! $this->is_enabled() ) { + return true; + } + + $session_token = $this->session_service->get( self::TOKEN_NAME ); + + // Check if the tokens are both strings. + if ( ! is_string( $session_token ) || ! is_string( $token ) ) { + return false; + } + // Compare the hashes to check request validity. + return hash_equals( $session_token, $token ); + } +} diff --git a/src/Internal/Service/MinimumAmountService.php b/src/Internal/Service/MinimumAmountService.php new file mode 100644 index 00000000000..c30a6094c4c --- /dev/null +++ b/src/Internal/Service/MinimumAmountService.php @@ -0,0 +1,90 @@ +legacy_proxy = $legacy_proxy; + } + + /** + * Extracts and stores the amount, provided by the API through an exception. + * + * @param Amount_Too_Small_Exception $exception The exception that was thrown. + */ + public function store_amount_from_exception( Amount_Too_Small_Exception $exception ): void { + $this->set_cached_amount( + $exception->get_currency(), + $exception->get_minimum_amount() + ); + } + + /** + * Checks if there is a minimum amount required for transactions in a given currency. + * + * @param string $currency Currency to check. + * @param int $amount Amount in cents to check. + * + * @throws Amount_Too_Small_Exception + */ + public function verify_amount( string $currency, int $amount ): void { + $minimum_amount = $this->get_cached_amount( $currency ); + + if ( $minimum_amount > $amount ) { + throw new Amount_Too_Small_Exception( __( 'Order amount too small', 'woocommerce-payments' ), $minimum_amount, $currency, 400 ); + } + } + + /** + * Saves the minimum amount required for transactions in a given currency. + * + * @param string $currency The currency. + * @param int $amount The minimum amount in cents. + */ + private function set_cached_amount( string $currency, int $amount ): void { + $key = self::TRANSIENT_KEY . strtolower( $currency ); + $this->legacy_proxy->call_function( 'set_transient', $key, $amount, DAY_IN_SECONDS ); + } + + /** + * Checks if there is a minimum amount required for transactions in a given currency. + * + * @param string $currency The currency to check for. + * + * @return int The minimum amount in cents. 0 if the cache has not been set, or it is an invalid value. + */ + private function get_cached_amount( string $currency ): int { + $key = self::TRANSIENT_KEY . strtolower( $currency ); + $cached = $this->legacy_proxy->call_function( 'get_transient', $key ); + + return (int) $cached; + } +} diff --git a/src/Internal/Service/OrderService.php b/src/Internal/Service/OrderService.php index c10ce3b63bd..150509ac8db 100644 --- a/src/Internal/Service/OrderService.php +++ b/src/Internal/Service/OrderService.php @@ -12,6 +12,7 @@ use WC_Payments_API_Abstract_Intention; use WC_Payments_API_Charge; use WC_Payments_API_Payment_Intention; +use WC_Payments_Explicit_Price_Formatter; use WC_Payments_Features; use WC_Payments_Order_Service; use WC_Payments_Utils; @@ -162,15 +163,12 @@ public function import_order_data_to_payment_context( int $order_id, PaymentCont $currency = strtolower( $order->get_currency() ); $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ); - - $user = $order->get_user(); - if ( false === $user ) { // Default to the current user. - $user = $this->legacy_proxy->call_function( 'wp_get_current_user' ); - } + $user = $order->get_user(); $context->set_currency( $currency ); $context->set_amount( $amount ); - $context->set_user_id( $user->ID ); + // In case we don't have user, we are setting user id to be 0 which could cause more harm since we don't have a real user. + $context->set_user_id( $user->ID ?? null ); } /** @@ -207,12 +205,39 @@ public function update_order_from_successful_intent( $this->legacy_service->attach_transaction_fee_to_order( $order, $charge ); $this->legacy_service->update_order_status_from_intent( $order, $intent ); + $this->set_mode( $order_id, $context->get_mode() ); if ( ! is_null( $charge ) ) { $this->attach_exchange_info_to_order( $order_id, $charge ); } } + /** + * Sets the '_wcpay_mode' meta data on an order. + * + * @param string $order_id The order id. + * @param string $mode Mode from the context. + * @throws Order_Not_Found_Exception + */ + public function set_mode( string $order_id, string $mode ) : void { + $order = $this->get_order( $order_id ); + $order->update_meta_data( '_wcpay_mode', $mode ); + $order->save_meta_data(); + } + + /** + * Gets the '_wcpay_mode' meta data on an order. + * + * @param string $order_id The order id. + * + * @return string The mode. + * @throws Order_Not_Found_Exception + */ + public function get_mode( string $order_id ) : string { + $order = $this->get_order( $order_id ); + return $order->get_meta( '_wcpay_mode', true ); + } + /** * Updates the order with the necessary details whenever an intent requires action. * @@ -380,6 +405,44 @@ public function add_note( int $order_id, string $note ): int { return $this->get_order( $order_id )->add_order_note( $note ); } + /** + * Adds a note to order when rate limiter is triggered. + * + * @param int $order_id ID of the order. + * + * @return int Note ID. + * @throws Order_Not_Found_Exception + */ + public function add_rate_limiter_note( int $order_id ) { + $order = $this->get_order( $order_id ); + + $wc_price = $this->legacy_proxy->call_function( 'wc_price', $order->get_total(), [ 'currency' => $order->get_currency() ] ); + $explicit_price = $this->legacy_proxy->call_static( + WC_Payments_Explicit_Price_Formatter::class, + 'get_explicit_price', + $wc_price, + $order + ); + + $note = sprintf( + $this->legacy_proxy->call_static( + WC_Payments_Utils::class, + 'esc_interpolated_html', + /* translators: %1: the failed payment amount */ + __( + 'A payment of %1$s failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.', + 'woocommerce-payments' + ), + [ + 'strong' => '', + ] + ), + $explicit_price + ); + + return $order->add_order_note( $note ); + } + /** * Deletes order. * @@ -421,4 +484,5 @@ protected function get_order( int $order_id ): WC_Order { } return $order; } + } diff --git a/src/Internal/Service/PaymentProcessingService.php b/src/Internal/Service/PaymentProcessingService.php index e2a93aeb23d..ca3647fcfc7 100644 --- a/src/Internal/Service/PaymentProcessingService.php +++ b/src/Internal/Service/PaymentProcessingService.php @@ -7,10 +7,13 @@ namespace WCPay\Internal\Service; +use Exception; use WC_Payments_API_Abstract_Intention; use WC_Payments_API_Setup_Intention; +use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Vendor\League\Container\Exception\ContainerException; +use WCPay\Core\Mode; use WCPay\Internal\Payment\PaymentContext; use WCPay\Internal\Payment\State\InitialState; use WCPay\Internal\Payment\State\StateFactory; @@ -45,6 +48,12 @@ class PaymentProcessingService { */ private $context_logger_service; + /** + * Mode + * + * @var Mode + */ + private $mode; /** * Service constructor. @@ -52,15 +61,18 @@ class PaymentProcessingService { * @param StateFactory $state_factory Factory for payment states. * @param LegacyProxy $legacy_proxy Legacy proxy. * @param PaymentContextLoggerService $context_logger_service Context Logging Service. + * @param Mode $mode Mode. */ public function __construct( StateFactory $state_factory, LegacyProxy $legacy_proxy, - PaymentContextLoggerService $context_logger_service + PaymentContextLoggerService $context_logger_service, + Mode $mode ) { $this->state_factory = $state_factory; $this->legacy_proxy = $legacy_proxy; $this->context_logger_service = $context_logger_service; + $this->mode = $mode; } /** @@ -73,6 +85,7 @@ public function __construct( * @throws PaymentRequestException When the request is malformed. This should be converted to a failure state. * @throws Order_Not_Found_Exception When order is not found. * @throws ContainerException When the dependency container cannot instantiate the state. + * @throws API_Exception When server requests fails. */ public function process_payment( int $order_id, bool $automatic_capture = false ) { // Start with a basis context. @@ -133,6 +146,11 @@ public function get_authentication_redirect_url( $intent, int $order_id ) { */ protected function create_payment_context( int $order_id, bool $automatic_capture = false ): PaymentContext { $context = new PaymentContext( $order_id ); + try { + $context->set_mode( $this->mode->is_test() ? 'test' : 'prod' ); + } catch ( Exception $e ) { + $context->set_mode( 'unknown' ); + } $context->toggle_automatic_capture( $automatic_capture ); return $context; diff --git a/tests/e2e/specs/upe-split/shopper/shopper-bnpls-checkout.spec.js b/tests/e2e/specs/upe-split/shopper/shopper-bnpls-checkout.spec.js new file mode 100644 index 00000000000..3cb6a37c02a --- /dev/null +++ b/tests/e2e/specs/upe-split/shopper/shopper-bnpls-checkout.spec.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +const { shopper, merchant } = require( '@woocommerce/e2e-utils' ); +import config from 'config'; +import { uiUnblocked } from '@woocommerce/e2e-utils/build/page-utils'; +/** + * Internal dependencies + */ +import { merchantWCP, shopperWCP } from '../../../utils/flows'; +import { setupProductCheckout } from '../../../utils/payments'; + +const bnplProviders = [ [ 'Affirm' ], [ 'Afterpay' ] ]; + +const UPE_METHOD_CHECKBOXES = [ + "//label[contains(text(), 'Affirm')]/preceding-sibling::span/input[@type='checkbox']", // affirm + "//label[contains(text(), 'Afterpay')]/preceding-sibling::span/input[@type='checkbox']", // afterpay +]; + +describe( 'BNPL checkout', () => { + beforeAll( async () => { + await merchant.login(); + await merchantWCP.activateUPEWithDefferedIntentCreation(); + await merchantWCP.enablePaymentMethod( UPE_METHOD_CHECKBOXES ); + await merchant.logout(); + await shopper.login(); + await shopperWCP.changeAccountCurrencyTo( 'USD' ); + } ); + + afterAll( async () => { + await shopperWCP.logout(); + await merchant.login(); + await merchantWCP.disablePaymentMethod( UPE_METHOD_CHECKBOXES ); + await merchantWCP.deactivateUPEWithDefferedIntentCreation(); + await merchant.logout(); + } ); + + describe.each( bnplProviders )( 'Checkout with %s', ( providerName ) => { + beforeEach( async () => { + await setupProductCheckout( + config.get( 'addresses.customer.billing' ), + [ [ 'Beanie', 3 ] ] + ); + } ); + + it( `should successfully place order with ${ providerName }`, async () => { + await uiUnblocked(); + // Select BNPL provider as payment method. + const xPathPaymentMethodSelector = `//*[@id='payment']/ul/li/label[contains(text(), '${ providerName }')]`; + await page.waitForXPath( xPathPaymentMethodSelector ); + const [ paymentMethodLabel ] = await page.$x( + xPathPaymentMethodSelector + ); + await paymentMethodLabel.click(); + await shopper.placeOrder(); + + // Authorize payment with Stripe. + // This XPath selector matches the Authorize Payment button, that is either a button or an anchor. + const xPathAuthorizePaymentButton = `//*[self::button or self::a][contains(text(), 'Authorize Test Payment')]`; + await page.waitForXPath( xPathAuthorizePaymentButton ); + const [ stripeButton ] = await page.$x( + xPathAuthorizePaymentButton + ); + await stripeButton.click(); + + // Wait for the order confirmation page to load. + await page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + await expect( page ).toMatch( 'Order received' ); + } ); + } ); +} ); diff --git a/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js b/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js index cb05d0220ca..21c1c471815 100644 --- a/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js +++ b/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js @@ -11,16 +11,18 @@ import { confirmCardAuthentication, fillCardDetails, setupProductCheckout, + selectGiropayOnCheckout, + completeGiropayPayment, } from '../../../utils/payments'; import { uiUnblocked } from '@woocommerce/e2e-utils/build/page-utils'; const { shopper, merchant } = require( '@woocommerce/e2e-utils' ); const UPE_METHOD_CHECKBOXES = [ - '#inspector-checkbox-control-3', // bancontact - '#inspector-checkbox-control-4', // eps - '#inspector-checkbox-control-5', // giropay - '#inspector-checkbox-control-6', // ideal - '#inspector-checkbox-control-7', // sofort + '#inspector-checkbox-control-5', // bancontact + '#inspector-checkbox-control-6', // eps + '#inspector-checkbox-control-7', // giropay + '#inspector-checkbox-control-8', // ideal + '#inspector-checkbox-control-9', // sofort ]; const card = config.get( 'cards.basic' ); const MIN_WAIT_TIME_BETWEEN_PAYMENT_METHODS = 20000; @@ -45,6 +47,19 @@ describe( 'Enabled UPE with deferred intent creation', () => { } ); describe( 'Enabled UPE with deferred intent creation', () => { + it( 'should successfully place order with Giropay', async () => { + await setupProductCheckout( + config.get( 'addresses.customer.billing' ) + ); + await selectGiropayOnCheckout( page ); + await shopper.placeOrder(); + await completeGiropayPayment( page, 'success' ); + await page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + await expect( page ).toMatch( 'Order received' ); + } ); + it( 'should successfully place order with the default card', async () => { await setupProductCheckout( config.get( 'addresses.customer.billing' ) diff --git a/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js index 7bd99bbe4e5..5530ea61776 100644 --- a/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js +++ b/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js @@ -125,9 +125,12 @@ describe( 'Disputes > Submit winning dispute', () => { await merchantWCP.openPaymentDetails( paymentDetailsLink ); // Confirm dispute status is Won. - await page.waitForSelector( 'li.woocommerce-timeline-item' ); - await expect( page ).toMatchElement( 'li.woocommerce-timeline-item', { - text: 'Dispute won! The bank ruled in your favor.', - } ); + await page.waitForSelector( '.transaction-details-dispute-footer' ); + await expect( page ).toMatchElement( + '.transaction-details-dispute-footer', + { + text: 'Good news! You won this dispute', + } + ); } ); } ); diff --git a/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js index e7cf88553e1..ed2382dcd2c 100644 --- a/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js +++ b/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js @@ -11,12 +11,10 @@ import { merchantWCP, uiLoaded } from '../../../utils'; describe( 'Admin merchant progressive onboarding', () => { beforeAll( async () => { await merchant.login(); - await merchantWCP.enableProgressiveOnboarding(); await merchantWCP.enableActAsDisconnectedFromWCPay(); } ); afterAll( async () => { - await merchantWCP.disableProgressiveOnboarding(); await merchantWCP.disableActAsDisconnectedFromWCPay(); await merchant.logout(); } ); diff --git a/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js new file mode 100644 index 00000000000..ba91e4f6251 --- /dev/null +++ b/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js @@ -0,0 +1,147 @@ +/** + * External dependencies + */ +import config from 'config'; +const { shopper, merchant } = require( '@woocommerce/e2e-utils' ); +/** + * Internal dependencies + */ +import { fillCardDetails, setupProductCheckout } from '../../../utils/payments'; +import { merchantWCP, shopperWCP } from '../../../utils'; + +const ORDER_RECEIVED_ORDER_TOTAL_SELECTOR = + '.woocommerce-order-overview__total'; +const ORDER_HISTORY_ORDER_ROW_SELECTOR = '.woocommerce-orders-table__row'; +const ORDER_HISTORY_ORDER_ROW_NUMBER_COL_SELECTOR = + '.woocommerce-orders-table__cell-order-number'; +const ORDER_HISTORY_ORDER_ROW_TOTAL_COL_SELECTOR = + '.woocommerce-orders-table__cell-order-total'; +const ORDER_DETAILS_ORDER_TOTAL_SELECTOR = + '.woocommerce-table--order-details tfoot tr:last-child td'; + +const placeOrderWithCurrency = async ( currency ) => { + try { + await shopperWCP.goToShopWithCurrency( currency ); + await setupProductCheckout( + config.get( 'addresses.customer.billing' ), + [ [ config.get( 'products.simple.name' ), 1 ] ], + currency + ); + const card = config.get( 'cards.basic' ); + await fillCardDetails( page, card ); + await shopper.placeOrder(); + await expect( page ).toMatch( 'Order received' ); + + const url = await page.url(); + return url.match( /\/order-received\/(\d+)\// )[ 1 ]; + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( + `Error placing order with currency ${ currency }: `, + error + ); + throw error; + } +}; + +const getOrderTotalTextForOrder = async ( orderId ) => { + return await page.$$eval( + ORDER_HISTORY_ORDER_ROW_SELECTOR, + ( rows, currentOrderId, orderNumberColSelector, totalColSelector ) => { + const orderSelector = `${ orderNumberColSelector } a[href*="view-order/${ currentOrderId }/"]`; + return rows + .filter( ( row ) => row.querySelector( orderSelector ) ) + .map( ( row ) => + row.querySelector( totalColSelector )?.textContent.trim() + ) + .find( ( text ) => text !== null ); + }, + orderId, + ORDER_HISTORY_ORDER_ROW_NUMBER_COL_SELECTOR, + ORDER_HISTORY_ORDER_ROW_TOTAL_COL_SELECTOR + ); +}; + +describe( 'Shopper Multi-Currency checkout', () => { + let wasMulticurrencyEnabled; + const currenciesOrders = { + USD: null, + EUR: null, + }; + beforeAll( async () => { + // Enable multi-currency + await merchant.login(); + + wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency(); + for ( const currency in currenciesOrders ) { + await merchantWCP.addCurrency( currency ); + } + + await merchant.logout(); + + await shopper.login(); + } ); + + afterAll( async () => { + await shopperWCP.emptyCart(); + await shopper.logout(); + + // Disable multi-currency if it was not initially enabled. + if ( ! wasMulticurrencyEnabled ) { + await merchant.login(); + await merchantWCP.deactivateMulticurrency(); + await merchant.logout(); + } + } ); + + describe.each( Object.keys( currenciesOrders ) )( + 'Checkout process with %s currency', + ( testCurrency ) => { + it( 'should allow checkout', async () => { + currenciesOrders[ testCurrency ] = await placeOrderWithCurrency( + testCurrency + ); + } ); + + it( 'should display the correct currency on the order received page', async () => { + expect( + await page.$eval( + ORDER_RECEIVED_ORDER_TOTAL_SELECTOR, + ( el ) => el.textContent + ) + ).toMatch( new RegExp( testCurrency ) ); + } ); + } + ); + + describe.each( Object.keys( currenciesOrders ) )( + 'My account order details for %s order', + ( testCurrency ) => { + beforeEach( async () => { + await shopperWCP.goToOrder( currenciesOrders[ testCurrency ] ); + } ); + + it( 'should show the correct currency in the order page for the order', async () => { + expect( + await page.$eval( + ORDER_DETAILS_ORDER_TOTAL_SELECTOR, + ( el ) => el.textContent + ) + ).toMatch( new RegExp( testCurrency ) ); + } ); + } + ); + + describe( 'My account order history', () => { + it( 'should show the correct currency in the order history table', async () => { + await shopperWCP.goToOrders(); + + for ( const currency in currenciesOrders ) { + const orderTotalText = await getOrderTotalTextForOrder( + currenciesOrders[ currency ] + ); + expect( orderTotalText ).toMatch( new RegExp( currency ) ); + } + } ); + } ); +} ); diff --git a/tests/e2e/utils/flows.js b/tests/e2e/utils/flows.js index 5c90bedb0f3..1f2863e181f 100644 --- a/tests/e2e/utils/flows.js +++ b/tests/e2e/utils/flows.js @@ -12,6 +12,7 @@ const { uiUnblocked, clearAndFillInput, setCheckbox, + SHOP_PAGE, } = require( '@woocommerce/e2e-utils' ); const { fillCardDetails, @@ -66,12 +67,24 @@ export const shopperWCP = { } ); }, + goToShopWithCurrency: async ( currency ) => { + await page.goto( SHOP_PAGE + `/?currency=${ currency }`, { + waitUntil: 'networkidle0', + } ); + }, + goToOrders: async () => { await page.goto( MY_ACCOUNT_ORDERS, { waitUntil: 'networkidle0', } ); }, + goToOrder: async ( orderId ) => { + await page.goto( SHOP_MY_ACCOUNT_PAGE + `view-order/${ orderId }`, { + waitUntil: 'networkidle0', + } ); + }, + logout: async () => { await page.goto( SHOP_MY_ACCOUNT_PAGE, { waitUntil: 'networkidle0', @@ -462,46 +475,6 @@ export const merchantWCP = { } ); }, - enableProgressiveOnboarding: async () => { - await page.goto( WCPAY_DEV_TOOLS, { - waitUntil: 'networkidle0', - } ); - - if ( - ! ( await page.$( - '#_wcpay_feature_progressive_onboarding:checked' - ) ) - ) { - await expect( page ).toClick( - 'label[for="_wcpay_feature_progressive_onboarding"]' - ); - } - - await expect( page ).toClick( 'input#submit' ); - await page.waitForNavigation( { - waitUntil: 'networkidle0', - } ); - }, - - disableProgressiveOnboarding: async () => { - await page.goto( WCPAY_DEV_TOOLS, { - waitUntil: 'networkidle0', - } ); - - if ( - await page.$( '#_wcpay_feature_progressive_onboarding:checked' ) - ) { - await expect( page ).toClick( - 'label[for="_wcpay_feature_progressive_onboarding"]' - ); - } - - await expect( page ).toClick( 'input#submit' ); - await page.waitForNavigation( { - waitUntil: 'networkidle0', - } ); - }, - enableActAsDisconnectedFromWCPay: async () => { await page.goto( WCPAY_DEV_TOOLS, { waitUntil: 'networkidle0', @@ -551,7 +524,17 @@ export const merchantWCP = { ); } - await page.$eval( paymentMethod, ( method ) => method.click() ); + // Check if paymentMethod is an XPath + if ( paymentMethod.startsWith( '//' ) ) { + // Find the element using XPath and click it + const elements = await page.$x( paymentMethod ); + if ( elements.length > 0 ) { + await elements[ 0 ].click(); + } + } else { + // If it's a CSS selector, use $eval + await page.$eval( paymentMethod, ( method ) => method.click() ); + } await new Promise( ( resolve ) => setTimeout( resolve, 2000 ) ); } @@ -643,6 +626,37 @@ export const merchantWCP = { await uiLoaded(); }, + addCurrency: async ( currencyCode ) => { + if ( currencyCode === 'USD' ) { + return; + } + await merchantWCP.openMultiCurrency(); + await page.click( '[data-testid="enabled-currencies-add-button"]' ); + + await page.evaluate( ( code ) => { + const inputs = Array.from( + document.querySelectorAll( 'input[type="checkbox"]' ) + ); + const targetInput = inputs.find( + ( input ) => input.getAttribute( 'code' ) === code + ); + if ( targetInput && ! targetInput.checked ) { + targetInput.click(); + } + }, currencyCode ); + + await page.click( + 'div.wcpay-confirmation-modal__footer button.components-button.is-primary', + { text: 'Update selected' } + ); + + const selector = `li.enabled-currency.${ currencyCode.toLowerCase() }`; + await page.waitForSelector( selector ); + const element = await page.$( selector ); + + expect( element ).not.toBeNull(); + }, + openConnectPage: async () => { await page.goto( WCPAY_CONNECT, { waitUntil: 'networkidle0', @@ -806,7 +820,16 @@ export const merchantWCP = { activateMulticurrency: async () => { await merchantWCP.openWCPSettings(); - await merchantWCP.setCheckboxByTestId( 'multi-currency-toggle' ); - await merchantWCP.wcpSettingsSaveChanges(); + const wasInitiallyEnabled = await page.evaluate( () => { + const checkbox = document.querySelector( + "[data-testid='multi-currency-toggle']" + ); + return checkbox ? checkbox.checked : false; + } ); + if ( ! wasInitiallyEnabled ) { + await merchantWCP.setCheckboxByTestId( 'multi-currency-toggle' ); + await merchantWCP.wcpSettingsSaveChanges(); + } + return wasInitiallyEnabled; }, }; diff --git a/tests/e2e/utils/payments.js b/tests/e2e/utils/payments.js index b625eed5607..3c8084c0811 100644 --- a/tests/e2e/utils/payments.js +++ b/tests/e2e/utils/payments.js @@ -296,3 +296,32 @@ export async function setupCheckout( billingDetails ) { '.wc_payment_method.payment_method_woocommerce_payments' ); } + +/** + * Selects the Giropay payment method on the checkout page. + * + * @param {*} page The page reference object. + */ +export async function selectGiropayOnCheckout( page ) { + await page.$( '#payment .payment_method_woocommerce_payments_giropay' ); + const giropayRadioLabel = await page.waitForSelector( + '#payment .payment_method_woocommerce_payments_giropay label' + ); + giropayRadioLabel.click(); + await page.waitFor( 1000 ); +} + +/** + * Authorizes or fails a Giropay payment. + * + * @param {*} page The page reference object. + * @param {string} action Either of 'success' or 'failure'. + */ +export async function completeGiropayPayment( page, action ) { + await page.$( '.actions .common-ButtonGroup' ); + const actionButton = await page.waitForSelector( + `.actions .common-ButtonGroup a[name=${ action }]` + ); + actionButton.click(); + await page.waitFor( 1000 ); +} diff --git a/tests/js/jest-test-file-setup.js b/tests/js/jest-test-file-setup.js index 364a95b905d..5956c6c748e 100644 --- a/tests/js/jest-test-file-setup.js +++ b/tests/js/jest-test-file-setup.js @@ -84,6 +84,11 @@ global.wcSettings = { // woocommerce_excluded_report_order_statuses: [], // }, siteTitle: 'WooCommerce Payments Dev', + countries: { + US: 'United States of America', + CA: 'Canada', + UK: 'United Kingdom', + }, }; global.wpApiSettings = { diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 0f902436028..ea7aaaed164 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -149,7 +149,8 @@ public function test_it_does_not_render_settings_badge( $is_upe_settings_preview update_option( '_wcpay_feature_upe', $is_upe_enabled ? '1' : '0' ); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu['wc-admin&path=/payments/overview'], 0, 2 ); @@ -163,7 +164,8 @@ public function test_it_does_not_render_payments_badge_if_stripe_is_connected() $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $menu, 0, 2 ); @@ -171,12 +173,33 @@ public function test_it_does_not_render_payments_badge_if_stripe_is_connected() $this->assertArrayNotHasKey( 'wc-admin&path=/payments/connect', $item_names_by_urls ); } - public function test_it_renders_payments_badge_if_activation_date_is_older_than_3_days_and_stripe_is_not_connected() { + public function test_it_refreshes_the_cache_if_get_param_exists() { global $menu; $this->mock_current_user_is_admin(); + $_GET = [ + 'page' => 'wc-admin', + 'path' => '/payments/overview', + 'wcpay-connection-error' => '1', + ]; // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( false ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); + $this->mock_account->expects( $this->once() )->method( 'refresh_account_data' ); + $this->payments_admin->add_payments_menu(); + + $item_names_by_urls = wp_list_pluck( $menu, 0, 2 ); + $this->assertEquals( 'Payments', $item_names_by_urls['wc-admin&path=/payments/overview'] ); + $this->assertArrayNotHasKey( 'wc-admin&path=/payments/connect', $item_names_by_urls ); + } + + public function test_it_renders_payments_badge_if_activation_date_is_older_than_3_days_and_stripe_is_not_connected() { + global $menu; + $this->mock_current_user_is_admin(); + + // Make sure we render the menu without submenu items. + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( false ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( false ); update_option( 'wcpay_activation_timestamp', time() - ( 3 * DAY_IN_SECONDS ) ); $this->payments_admin->add_payments_menu(); @@ -189,8 +212,9 @@ public function test_it_does_not_render_payments_badge_if_activation_date_is_les global $menu; $this->mock_current_user_is_admin(); - // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( false ); + // Make sure we render the menu without submenu items. + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( false ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( false ); update_option( 'wcpay_menu_badge_hidden', 'no' ); update_option( 'wcpay_activation_timestamp', time() - ( DAY_IN_SECONDS * 2 ) ); $this->payments_admin->add_payments_menu(); @@ -493,7 +517,8 @@ public function test_disputes_notification_badge_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); @@ -534,7 +559,8 @@ public function test_disputes_notification_badge_no_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); @@ -577,7 +603,8 @@ public function test_transactions_notification_badge_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); @@ -622,7 +649,8 @@ public function test_transactions_notification_badge_no_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php index 9bb51089ac7..4d42756e1ef 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php @@ -69,16 +69,16 @@ class WC_REST_Payments_Settings_Controller_Test extends WCPAY_UnitTestCase { /** * An array of mocked split UPE payment gateways mapped to payment method ID. * - * @var array + * @var UPE_Payment_Gateway */ private $mock_upe_payment_gateway; /** * An array of mocked split UPE payment gateways mapped to payment method ID. * - * @var array + * @var UPE_Split_Payment_Gateway */ - private $mock_split_upe_payment_gateways; + private $mock_split_upe_payment_gateway; /** * UPE system under test. @@ -201,7 +201,7 @@ public function set_up() { $this->upe_controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->mock_upe_payment_gateway, $this->mock_wcpay_account ); - $this->mock_upe_split_payment_gateway = new UPE_Split_Payment_Gateway( + $this->mock_split_upe_payment_gateway = new UPE_Split_Payment_Gateway( $this->mock_api_client, $this->mock_wcpay_account, $customer_service, @@ -216,7 +216,7 @@ public function set_up() { $this->mock_fraud_service ); - $this->upe_split_controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->mock_upe_split_payment_gateway, $this->mock_wcpay_account ); + $this->upe_split_controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->mock_split_upe_payment_gateway, $this->mock_wcpay_account ); $this->mock_api_client ->method( 'is_server_connected' ) @@ -451,22 +451,14 @@ public function test_upe_update_settings_saves_enabled_payment_methods() { } public function test_upe_split_update_settings_saves_enabled_payment_methods() { - $this->mock_upe_split_payment_gateway->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD ] ); + $this->mock_split_upe_payment_gateway->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD ] ); - $request = new WP_REST_Request(); - $request->set_param( 'enabled_payment_method_ids', [ Payment_Method::CARD, Payment_Method::GIROPAY ] ); + $request = new WP_REST_Request(); + $request->set_param( 'enabled_payment_method_ids', [ Payment_Method::CARD, Payment_Method::GIROPAY ] ); - $this->upe_split_controller->update_settings( $request ); + $this->upe_split_controller->update_settings( $request ); - $this->assertEquals( [ Payment_Method::CARD, Payment_Method::GIROPAY ], $this->mock_upe_split_payment_gateway->get_option( 'upe_enabled_payment_method_ids' ) ); - } - - public function test_update_settings_validation_fails_if_invalid_gateway_id_supplied() { - $request = new WP_REST_Request( 'POST', self::$settings_route ); - $request->set_param( 'enabled_payment_method_ids', [ 'foo', 'baz' ] ); - - $response = rest_do_request( $request ); - $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( [ Payment_Method::CARD, Payment_Method::GIROPAY ], $this->mock_split_upe_payment_gateway->get_option( 'upe_enabled_payment_method_ids' ) ); } public function test_update_settings_fails_if_user_cannot_manage_woocommerce() { @@ -624,14 +616,14 @@ public function test_update_settings_saves_payment_request_button_theme() { } public function test_update_settings_saves_payment_request_button_size() { - $this->assertEquals( 'default', $this->gateway->get_option( 'payment_request_button_size' ) ); + $this->assertEquals( 'medium', $this->gateway->get_option( 'payment_request_button_size' ) ); $request = new WP_REST_Request(); - $request->set_param( 'payment_request_button_size', 'medium' ); + $request->set_param( 'payment_request_button_size', 'default' ); $this->controller->update_settings( $request ); - $this->assertEquals( 'medium', $this->gateway->get_option( 'payment_request_button_size' ) ); + $this->assertEquals( 'default', $this->gateway->get_option( 'payment_request_button_size' ) ); } public function test_update_settings_saves_payment_request_button_type() { @@ -673,22 +665,6 @@ public function test_update_settings_disables_saved_cards() { $this->assertEquals( 'no', $this->gateway->get_option( 'saved_cards' ) ); } - public function test_enable_woopay_converts_upe_flag() { - update_option( WC_Payments_Features::UPE_FLAG_NAME, '1' ); - update_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME, '0' ); - update_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, '0' ); - $this->gateway->update_option( 'platform_checkout', 'no' ); - - $request = new WP_REST_Request(); - $request->set_param( 'is_woopay_enabled', true ); - - $this->controller->update_settings( $request ); - - $this->assertEquals( '0', get_option( WC_Payments_Features::UPE_FLAG_NAME ) ); - $this->assertEquals( '0', get_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME ) ); - $this->assertEquals( '1', get_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME ) ); - } - public function deposit_schedules_data_provider() { return [ [ diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index b986ab01ef4..6feb10417ce 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -98,6 +98,18 @@ function() { require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-payment-intents-controller.php'; require_once $_plugin_dir . 'includes/class-woopay-tracker.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php'; + + // Load currency helper class early to ensure its implementation is used over the one resolved during further test initialization. + require_once __DIR__ . '/helpers/class-wc-helper-site-currency.php'; + + // Assist testing methods and classes with keyword `final`. + // Woo Core uses the similar approach from this package, and implements it as class `CodeHacker`. + DG\BypassFinals::enable( false, true ); + DG\BypassFinals::setWhitelist( + [ + '*/AbstractSessionRateLimiter.php', + ] + ); } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); 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 8f7e48d016b..c71dce2c3f3 100644 --- a/tests/unit/core/server/request/test-class-core-request.php +++ b/tests/unit/core/server/request/test-class-core-request.php @@ -8,6 +8,7 @@ use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\Paginated; use WCPay\Core\Server\Request\List_Transactions; +use WCPay\Core\Server\Request\Update_Intention; // phpcs:disable class My_Request extends Request { @@ -54,6 +55,10 @@ public function set_param_4( int $value ) { $this->set_param( 'param_4', $value ); } } + +class Request_With_Id extends Update_Intention { + +} // phpcs:enable // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound @@ -134,4 +139,21 @@ function( $request ) { $result ); } + + public function test_extension_works_with_ids() { + $intent_id = 'pi_XYZ'; + $hook = 'some_request_class_with_id'; + $base = Update_Intention::create( $intent_id ); + + add_filter( + $hook, + function( $base ) { + return Request_With_Id::extend( $base ); + } + ); + + $filtered = $base->apply_filters( $hook ); + $this->assertInstanceOf( Request_With_Id::class, $filtered ); + $this->assertStringContainsString( $intent_id, $filtered->get_api() ); + } } diff --git a/tests/unit/core/server/request/test-class-list-transactions-request.php b/tests/unit/core/server/request/test-class-list-transactions-request.php index 9a73d65e32b..0d932deafcf 100644 --- a/tests/unit/core/server/request/test-class-list-transactions-request.php +++ b/tests/unit/core/server/request/test-class-list-transactions-request.php @@ -53,25 +53,31 @@ public function test_exception_will_throw_if_date_before_is_invalid_format() { } public function test_list_transactions_request_will_be_date() { - $page = 2; - $page_size = 50; - $direction = 'asc'; - $sort = 'date'; - $filters = [ + $page = 2; + $page_size = 50; + $direction = 'asc'; + $sort = 'date'; + $filters = [ 'key' => 'value', ]; - $date_after = '2022-01-01 00:00:00'; - $date_before = '2022-02-01 00:00:00'; - $date_between = [ $date_after, $date_before ]; - $match = 'match'; - $type = 'bill'; - $type_is_not = 'passport'; - $device = 'ios'; - $device_is_not = 'android'; - $search = [ 'search' ]; - $currency = 'usd'; - $cs_currency = 'eur'; - $loan_id = 'loan_id'; + $date_after = '2022-01-01 00:00:00'; + $date_before = '2022-02-01 00:00:00'; + $date_between = [ $date_after, $date_before ]; + $match = 'match'; + $type = 'bill'; + $type_is_not = 'passport'; + $device = 'ios'; + $device_is_not = 'android'; + $channel = 'online'; + $channel_is_not = 'in_person'; + $country = 'US'; + $country_is_not = 'CA'; + $risk_level = '0'; + $risk_level_is_not = '1'; + $search = [ 'search' ]; + $currency = 'usd'; + $cs_currency = 'eur'; + $loan_id = 'loan_id'; $request = new List_Transactions( $this->mock_api_client, $this->mock_wc_payments_http_client ); $request->set_page( $page ); @@ -86,6 +92,12 @@ public function test_list_transactions_request_will_be_date() { $request->set_type_is_not( $type_is_not ); $request->set_source_device_is( $device ); $request->set_source_device_is_not( $device_is_not ); + $request->set_channel_is( $channel ); + $request->set_channel_is_not( $channel_is_not ); + $request->set_customer_country_is( $country ); + $request->set_customer_country_is_not( $country_is_not ); + $request->set_risk_level_is( $risk_level ); + $request->set_risk_level_is_not( $risk_level_is_not ); $request->set_search( $search ); $request->set_store_currency_is( $currency ); $request->set_customer_currency_is_not( $currency ); @@ -108,6 +120,12 @@ public function test_list_transactions_request_will_be_date() { $this->assertSame( $type_is_not, $params['type_is_not'] ); $this->assertSame( $device, $params['source_device_is'] ); $this->assertSame( $device_is_not, $params['source_device_is_not'] ); + $this->assertSame( $channel, $params['channel_is'] ); + $this->assertSame( $channel_is_not, $params['channel_is_not'] ); + $this->assertSame( $country, $params['customer_country_is'] ); + $this->assertSame( $country_is_not, $params['customer_country_is_not'] ); + $this->assertSame( $risk_level, $params['risk_level_is'] ); + $this->assertSame( $risk_level_is_not, $params['risk_level_is_not'] ); $this->assertSame( $search, $params['search'] ); $this->assertSame( $loan_id, $params['loan_id_is'] ); $this->assertSame( $currency, $params['customer_currency_is_not'] ); @@ -119,22 +137,28 @@ public function test_list_transactions_request_will_be_date() { } public function test_list_transactions_request_will_be_date_using_from_rest_request_function() { - $page = 2; - $page_size = 50; - $direction = 'asc'; - $sort = 'date'; - $date_after = '2022-01-01 00:00:00'; - $date_before = '2022-02-01 00:00:00'; - $date_between = [ $date_after, $date_before ]; - $match = 'match'; - $type = 'bill'; - $type_is_not = 'passport'; - $device = 'ios'; - $device_is_not = 'android'; - $search = [ 'search' ]; - $currency = 'usd'; - $cs_currency = 'eur'; - $loan_id = 'loan_id'; + $page = 2; + $page_size = 50; + $direction = 'asc'; + $sort = 'date'; + $date_after = '2022-01-01 00:00:00'; + $date_before = '2022-02-01 00:00:00'; + $date_between = [ $date_after, $date_before ]; + $match = 'match'; + $type = 'bill'; + $type_is_not = 'passport'; + $device = 'ios'; + $device_is_not = 'android'; + $channel = 'online'; + $channel_is_not = 'in_person'; + $country = 'US'; + $country_is_not = 'CA'; + $risk_level = '0'; + $risk_level_is_not = '1'; + $search = [ 'search' ]; + $currency = 'usd'; + $cs_currency = 'eur'; + $loan_id = 'loan_id'; $rest_request = new WP_REST_Request( 'GET' ); $rest_request->set_param( 'page', $page ); @@ -149,6 +173,12 @@ public function test_list_transactions_request_will_be_date_using_from_rest_requ $rest_request->set_param( 'type_is_not', $type_is_not ); $rest_request->set_param( 'source_device_is', $device ); $rest_request->set_param( 'source_device_is_not', $device_is_not ); + $rest_request->set_param( 'channel_is', $channel ); + $rest_request->set_param( 'channel_is_not', $channel_is_not ); + $rest_request->set_param( 'customer_country_is', $country ); + $rest_request->set_param( 'customer_country_is_not', $country_is_not ); + $rest_request->set_param( 'risk_level_is', $risk_level ); + $rest_request->set_param( 'risk_level_is_not', $risk_level_is_not ); $rest_request->set_param( 'loan_id_is', $loan_id ); $rest_request->set_param( 'search', $search ); $rest_request->set_param( 'store_currency_is', $currency ); @@ -172,6 +202,12 @@ public function test_list_transactions_request_will_be_date_using_from_rest_requ $this->assertSame( $type_is_not, $params['type_is_not'] ); $this->assertSame( $device, $params['source_device_is'] ); $this->assertSame( $device_is_not, $params['source_device_is_not'] ); + $this->assertSame( $channel, $params['channel_is'] ); + $this->assertSame( $channel_is_not, $params['channel_is_not'] ); + $this->assertSame( $country, $params['customer_country_is'] ); + $this->assertSame( $country_is_not, $params['customer_country_is_not'] ); + $this->assertSame( $risk_level, $params['risk_level_is'] ); + $this->assertSame( $risk_level_is_not, $params['risk_level_is_not'] ); $this->assertSame( $search, $params['search'] ); $this->assertSame( $loan_id, $params['loan_id_is'] ); $this->assertSame( $currency, $params['customer_currency_is_not'] ); diff --git a/tests/unit/helpers/class-wc-helper-site-currency.php b/tests/unit/helpers/class-wc-helper-site-currency.php index 4869f2a3b3e..49cfbed41b1 100644 --- a/tests/unit/helpers/class-wc-helper-site-currency.php +++ b/tests/unit/helpers/class-wc-helper-site-currency.php @@ -8,7 +8,7 @@ namespace WCPay\Payment_Methods; /** - * Overriding global function within namespace for testing + * If mock value is set, return mock value. Otherwise, return the global function value. */ function get_woocommerce_currency() { return WC_Helper_Site_Currency::$mock_site_currency ? WC_Helper_Site_Currency::$mock_site_currency : \get_woocommerce_currency(); diff --git a/tests/unit/migrations/test-class-track-upe-status.php b/tests/unit/migrations/test-class-track-upe-status.php index 5acd90e9f87..6fbf4817162 100644 --- a/tests/unit/migrations/test-class-track-upe-status.php +++ b/tests/unit/migrations/test-class-track-upe-status.php @@ -59,21 +59,6 @@ public function test_track_enabled_on_upgrade() { $this->assertSame( '1', get_option( Track_Upe_Status::IS_TRACKED_OPTION ) ); } - public function test_track_disabled_on_upgrade() { - update_option( WC_Payments_Features::UPE_FLAG_NAME, 'disabled' ); - - Track_Upe_Status::maybe_track(); - - $this->assertEquals( - [ - 'wcpay_upe_disabled' => [], - ], - Tracker::get_admin_events() - ); - - $this->assertSame( '1', get_option( Track_Upe_Status::IS_TRACKED_OPTION ) ); - } - public function test_do_nothing_default_on_upgrade() { Track_Upe_Status::maybe_track(); 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 095cc756132..53553340b2b 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 @@ -24,78 +24,6 @@ public function tear_down() { delete_option( '_wcpay_feature_upe' ); } - 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( 'info', $note->get_type() ); - $this->assertSame( 'wc-payments-notes-additional-payment-methods', $note->get_name() ); - $this->assertSame( 'woocommerce-payments', $note->get_source() ); - - list( $enable_upe_action ) = $note->get_actions(); - $this->assertSame( 'wc-payments-notes-additional-payment-methods', $enable_upe_action->name ); - $this->assertSame( 'Enable on your store', $enable_upe_action->label ); - $this->assertStringStartsWith( 'http://example.org/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments&action=enable-upe', $enable_upe_action->query ); - - /** - * The $primary property was deprecated from WooCommerce core. Keeping this to maintain the compatibility with old WooCommerce versions. - * @see https://github.com/woocommerce/woocommerce/blob/ff2d7d704a8f72aeb4990811b6972097aa167bea/plugins/woocommerce/src/Admin/Notes/Note.php#L623-L623. - * @see https://github.com/woocommerce/woocommerce-admin/pull/8474 - */ - if ( isset( $enable_upe_action->primary ) ) { - $this->assertSame( true, $enable_upe_action->primary ); - } - } - - public function test_get_note_does_not_return_note_when_account_is_not_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( false ) ); - 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_connected() { - $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(); - - $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 ) ); 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 84294afade2..883bd5cd96e 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 @@ -55,14 +55,6 @@ public function test_stripelink_setup_get_note() { $this->assertStringStartsWith( 'https://woo.com/document/woopayments/payment-methods/link-by-stripe/', $set_up_action->query ); } - public function test_stripelink_setup_note_null_when_upe_disabled() { - $this->mock_gateway_data( '0', [ 'card', 'link' ], [ 'card' ] ); - - $note = \WC_Payments_Notes_Set_Up_StripeLink::get_note(); - - $this->assertNull( $note ); - } - public function test_stripelink_setup_note_null_when_link_not_available() { $this->mock_gateway_data( '1', [ 'card' ], [ 'card' ] ); 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 3f0aed8474a..955fc151476 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -41,8 +41,6 @@ use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; -require_once dirname( __FILE__ ) . '/../helpers/class-wc-helper-site-currency.php'; - /** * UPE_Payment_Gateway unit tests */ @@ -1365,89 +1363,6 @@ public function test_correct_payment_method_title_for_order() { } } - public function test_set_payment_method_title_for_email_updates_title() { - $mock_visa_details = [ - 'type' => 'card', - 'card' => [ - 'network' => 'visa', - 'funding' => 'debit', - ], - ]; - - $this->mock_order_service - ->expects( $this->once() ) - ->method( 'get_payment_method_id_for_order' ) - ->will( - $this->returnValue( 'pm_XXXXXXX' ) - ); - - $this->mock_api_client - ->expects( $this->once() ) - ->method( 'get_payment_method' ) - ->will( - $this->returnValue( $mock_visa_details ) - ); - - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'woocommerce_payments' ); - $order->set_payment_method_title( 'Popular Payment Methods' ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( 'Visa debit card', $order->get_payment_method_title() ); - } - - public function test_correct_payment_method_title_for_order_when_set_for_email() { - $payment_methods = [ - 'cheque' => 'Check payments', - 'cod' => 'Cash on delivery', - 'bacs' => 'Direct bank transfer', - ]; - - // Emulates order creation which sets the payment method and title. - $order = WC_Helper_Order::create_order(); - - foreach ( $payment_methods as $method => $title ) { - $order->set_payment_method( $method ); - $order->set_payment_method_title( $title ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( $title, $order->get_payment_method_title() ); - } - } - - public function test_set_payment_method_title_for_email_fallback() { - $this->mock_order_service - ->expects( $this->once() ) - ->method( 'get_payment_method_id_for_order' ) - ->will( - $this->returnValue( '' ) - ); - - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'woocommerce_payments' ); - $order->set_payment_method_title( 'Popular Payment Methods' ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( 'WooPayments', $order->get_payment_method_title() ); - } - - public function test_set_payment_method_title_for_email_only_runs_for_legacy_upe() { - update_option( '_wcpay_feature_upe', '0' ); - update_option( '_wcpay_feature_upe_split', '1' ); - - // set_payment_method_title_for_email should return before this functions runs. - $this->mock_order_service - ->expects( $this->never() ) - ->method( 'get_payment_method_id_for_order' ); - - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'woocommerce_payments' ); - $order->set_payment_method_title( 'Credit / Debit Card' ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( 'Credit / Debit Card', $order->get_payment_method_title() ); - } - public function test_payment_methods_show_correct_default_outputs() { $mock_token = WC_Helper_Token::create_token( 'pm_mock' ); $this->mock_token_service->expects( $this->any() ) @@ -1888,81 +1803,6 @@ function( $argument ) { } } - /** - * @dataProvider maybe_filter_gateway_title_data_provider - */ - public function test_maybe_filter_gateway_title_with_no_additional_feature_flags_enabled( $data ) { - $data = $data[0]; - $default_option = $this->mock_upe_gateway->get_option( 'upe_enabled_payment_method_ids' ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $data['methods'] ); - WC_Helper_Site_Currency::$mock_site_currency = $data['currency']; - $this->set_get_upe_enabled_payment_method_statuses_return_value( $data['statuses'] ); - $this->assertSame( $data['expected'], $this->mock_upe_gateway->maybe_filter_gateway_title( $data['title'], $data['id'] ) ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $default_option ); - } - - public function test_maybe_filter_gateway_title_skips_update_due_to_enabled_split_upe() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); - - $data = [ - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => 'WooPayments', - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'WooPayments', - ]; - - $default_option = $this->mock_upe_gateway->get_option( 'upe_enabled_payment_method_ids' ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $data['methods'] ); - - WC_Helper_Site_Currency::$mock_site_currency = $data['currency']; - $this->set_get_upe_enabled_payment_method_statuses_return_value( $data['statuses'] ); - $this->assertSame( $data['expected'], $this->mock_upe_gateway->maybe_filter_gateway_title( $data['title'], $data['id'] ) ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $default_option ); - } - - public function test_maybe_filter_gateway_title_skips_update_due_to_enabled_upe_with_deferred_intent_creation() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); - - $data = [ - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => 'WooPayments', - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'WooPayments', - ]; - - $default_option = $this->mock_upe_gateway->get_option( 'upe_enabled_payment_method_ids' ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $data['methods'] ); - - WC_Helper_Site_Currency::$mock_site_currency = $data['currency']; - $this->set_get_upe_enabled_payment_method_statuses_return_value( $data['statuses'] ); - $this->assertSame( $data['expected'], $this->mock_upe_gateway->maybe_filter_gateway_title( $data['title'], $data['id'] ) ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $default_option ); - } - public function test_remove_link_payment_method_if_card_disabled() { $mock_upe_gateway = $this->getMockBuilder( UPE_Payment_Gateway::class ) @@ -2080,140 +1920,80 @@ public function test_link_payment_method_if_card_enabled() { $upe_checkout->get_payment_fields_js_config()['paymentMethodsConfig'], [ 'card' => [ - 'isReusable' => true, - 'title' => 'Credit card / debit card', - 'icon' => $this->icon_url, - 'showSaveOption' => true, - 'countries' => [], + 'isReusable' => true, + 'title' => 'Credit card / debit card', + 'icon' => $this->icon_url, + 'showSaveOption' => true, + '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.', + 'forceNetworkSavedCards' => false, ], 'link' => [ - 'isReusable' => true, - 'title' => 'Link', - 'icon' => $this->icon_url, - 'showSaveOption' => true, - 'countries' => [], + 'isReusable' => true, + 'title' => 'Link', + 'icon' => $this->icon_url, + 'showSaveOption' => true, + 'countries' => [], + 'upePaymentIntentData' => null, + 'upeSetupIntentData' => null, + 'testingInstructions' => '', + 'forceNetworkSavedCards' => false, ], ] ); } - public function maybe_filter_gateway_title_data_provider() { - $method_title = 'WooPayments'; - $checkout_title = 'Popular payment methods'; - $card_title = 'Credit card / debit card'; + /** + * @dataProvider available_payment_methods_provider + */ + public function test_get_upe_available_payment_methods( $payment_methods, $expected_result ) { + $mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_fees' ) + ->willReturn( $payment_methods ); - $data_set[] = [ // Allows for $checkout_title due to UPE method and EUR. - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $checkout_title, - ]; - $data_set[] = [ // No UPE method, only card, so $card_title is expected. - 'methods' => [ - 'card', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $card_title, - ]; - $data_set[] = [ // Only UPE method, so UPE method title is expected. - 'methods' => [ - 'bancontact', - ], - 'statuses' => [ - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'Bancontact', - ]; - $data_set[] = [ // Card and UPE enabled, but USD, $card_title expected. - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], + $gateway = new UPE_Payment_Gateway( + $this->mock_api_client, + $mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->mock_order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + + $this->assertEquals( $expected_result, $gateway->get_upe_available_payment_methods() ); + } + + public function available_payment_methods_provider() { + return [ + 'card only' => [ + [ 'card' => [ 'base' => 0.1 ] ], + [ 'card' ], ], - 'currency' => 'USD', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $card_title, - ]; - $data_set[] = [ // Card and UPE enabled, but not our title, other title expected. - 'methods' => [ - 'card', - 'bancontact', + 'no match with fees' => [ + [ 'some_other_payment_method' => [ 'base' => 0.1 ] ], + [], ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', + 'multiple matches with fees' => [ + [ + 'card' => [ 'base' => 0.1 ], + 'bancontact' => [ 'base' => 0.2 ], ], + [ 'card', 'bancontact' ], ], - 'currency' => 'EUR', - 'title' => 'Some other title', - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'Some other title', - ]; - $data_set[] = [ // Card and UPE enabled, but not our id, $method_title expected. - 'methods' => [ - 'card', - 'bancontact', + 'no fees no methods' => [ + [], + [], ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'USD', - 'title' => $method_title, - 'id' => 'some_other_id', - 'expected' => $method_title, - ]; - $data_set[] = [ // No methods at all, so defaults to card, so $card_title is expected. - 'methods' => [], - 'statuses' => [], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $card_title, ]; - foreach ( $data_set as $data ) { - $return_data[] = [ [ $data ] ]; - } - return $return_data; } /** 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 0d6e8fc4b97..eb86595cd96 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 @@ -41,9 +41,6 @@ use WCPay\Database_Cache; use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; - -require_once dirname( __FILE__ ) . '/../helpers/class-wc-helper-site-currency.php'; - /** * UPE_Split_Payment_Gateway unit tests */ @@ -2363,7 +2360,6 @@ public function test_get_payment_methods_without_request_context_or_token() { * @return void */ public function test_get_payment_methods_from_gateway_id_upe() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); WC_Helper_Order::create_order(); $mock_upe_gateway = $this->getMockBuilder( UPE_Split_Payment_Gateway::class ) ->setConstructorArgs( @@ -2398,80 +2394,23 @@ public function test_get_payment_methods_from_gateway_id_upe() { ->will( $this->returnValue( [ Payment_Method::CARD, Payment_Method::LINK ] ) ); - $mock_upe_gateway->expects( $this->any() ) - ->method( 'get_payment_method_ids_enabled_at_checkout' ) - ->will( - $this->returnValue( - [ Payment_Method::CARD, Payment_Method::LINK ] - ) - ); - - $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID ); - - $this->assertSame( [ Payment_Method::CARD, Payment_Method::LINK ], $payment_methods ); $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID . '_' . Payment_Method::BANCONTACT ); - $this->assertSame( [ Payment_Method::BANCONTACT ], $payment_methods ); - WC_Payments::set_gateway( $gateway ); - } - - /** - * Test get_payment_methods_from_gateway_id function with UPE disabled. - * - * @return void - */ - public function test_get_payment_methods_from_gateway_id_non_upe() { - $this->mock_cache - ->method( 'get' ) - ->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => false ] ); - - $order = WC_Helper_Order::create_order(); - $mock_upe_gateway = $this->getMockBuilder( UPE_Split_Payment_Gateway::class ) - ->setConstructorArgs( - [ - $this->mock_api_client, - $this->mock_wcpay_account, - $this->mock_customer_service, - $this->mock_token_service, - $this->mock_action_scheduler_service, - $this->mock_payment_methods[ Payment_Method::CARD ], - $this->mock_payment_methods, - $this->mock_rate_limiter, - $this->order_service, - $this->mock_dpps, - $this->mock_localization_service, - $this->mock_fraud_service, - ] - ) - ->onlyMethods( - [ - 'get_upe_enabled_payment_method_ids', - 'get_payment_method_ids_enabled_at_checkout', - ] - ) - ->getMock(); - - $gateway = WC_Payments::get_gateway(); - WC_Payments::set_gateway( $mock_upe_gateway ); $mock_upe_gateway->expects( $this->any() ) ->method( 'get_payment_method_ids_enabled_at_checkout' ) ->will( - $this->returnValueMap( - [ - [ null, true, [ Payment_Method::CARD, Payment_Method::BANCONTACT ] ], - [ $order->get_id(), true, [ Payment_Method::CARD ] ], - ] + $this->onConsecutiveCalls( + [ Payment_Method::CARD, Payment_Method::LINK ], + [ Payment_Method::CARD ] ) ); $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID ); + $this->assertSame( [ Payment_Method::CARD, Payment_Method::LINK ], $payment_methods ); - $this->assertSame( [ Payment_Method::CARD, Payment_Method::BANCONTACT ], $payment_methods ); - - $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID, $order->get_id() ); - + $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID ); $this->assertSame( [ Payment_Method::CARD ], $payment_methods ); WC_Payments::set_gateway( $gateway ); diff --git a/tests/unit/src/Internal/Payment/AbstractSessionRateLimiterTest.php b/tests/unit/src/Internal/Payment/AbstractSessionRateLimiterTest.php new file mode 100644 index 00000000000..14b32aac1ca --- /dev/null +++ b/tests/unit/src/Internal/Payment/AbstractSessionRateLimiterTest.php @@ -0,0 +1,176 @@ +mock_key = 'session_key'; + $this->mock_threshold = 2; + $this->mock_delay = 600; + $this->mock_session_service = $this->createMock( SessionService::class ); + $this->mock_legacy_proxy = $this->createMock( LegacyProxy::class ); + + $this->sut = new TestConcreteSessionRateLimiter( + $this->mock_key, + $this->mock_threshold, + $this->mock_delay, + $this->mock_session_service, + $this->mock_legacy_proxy + ); + } + + public function test_bump() { + $timestamp_1 = time(); + $timestamp_2 = $timestamp_1 + 10; + + $this->mock_session_service->expects( $this->exactly( 2 ) ) + ->method( 'get' ) + ->with( $this->mock_key ) + ->willReturnOnConsecutiveCalls( null, [ $timestamp_1 ] ); + + $this->mock_legacy_proxy->expects( $this->exactly( 2 ) ) + ->method( 'call_function' ) + ->withConsecutive( [ 'time' ], [ 'time' ] ) + ->willReturnOnConsecutiveCalls( $timestamp_1, $timestamp_2 ); + + $this->mock_session_service->expects( $this->exactly( 2 ) ) + ->method( 'set' ) + ->withConsecutive( + [ $this->mock_key, [ $timestamp_1 ] ], + [ $this->mock_key, [ $timestamp_1, $timestamp_2 ] ] + ); + + // Calling this two times so that can test the session value is either null or an array. + $this->sut->bump(); + $this->sut->bump(); + } + + public function test_is_limited_always_returns_false_when_rate_limiter_is_disabled() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_option', 'wcpay_session_rate_limiter_disabled_' . $this->mock_key ) + ->willReturn( 'yes' ); + + $this->assertFalse( $this->sut->is_limited() ); + } + + public function provider_is_limited(): array { + $time = time(); + return [ + 'un-initialized registry' => [ + null, + false, + false, + ], + 'empty registry' => [ + null, + false, + false, + ], + 'yet reach threshold' => [ + [ $time ], + false, + false, + ], + 'reach threshold but not within delay' => [ + [ $time - 800, $time - 700 ], + true, + false, + ], + 'reach threshold, got limited' => [ + [ $time - 200, $time - 100 ], + false, + true, + ], + ]; + } + + /** + * @dataProvider provider_is_limited + */ + public function test_is_limited( ?array $session_registry, bool $is_clear_session, bool $expected ) { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_option', 'wcpay_session_rate_limiter_disabled_' . $this->mock_key ) + ->willReturn( 'no' ); + + $this->mock_session_service->expects( $this->once() ) + ->method( 'get' ) + ->with( $this->mock_key ) + ->willReturn( $session_registry ); + + $this->mock_session_service->expects( $this->exactly( $is_clear_session ? 1 : 0 ) ) + ->method( 'set' ) + ->with( $this->mock_key, [] ); + + $this->assertSame( $expected, $this->sut->is_limited() ); + } +} + +/** + * A simple class to extend the SUT abstract for testing purpose. + */ +// phpcs:disable +class TestConcreteSessionRateLimiter extends AbstractSessionRateLimiter { + public function __construct( + string $key, + int $threshold, + int $delay, + SessionService $session_service, + LegacyProxy $legacy_proxy + ) { + parent::__construct( $key, $threshold, $delay, $session_service, $legacy_proxy ); + } +} +// phpcs:enable diff --git a/tests/unit/src/Internal/Payment/FailedTransactionRateLimiterTest.php b/tests/unit/src/Internal/Payment/FailedTransactionRateLimiterTest.php new file mode 100644 index 00000000000..75b56ee5327 --- /dev/null +++ b/tests/unit/src/Internal/Payment/FailedTransactionRateLimiterTest.php @@ -0,0 +1,67 @@ +mock_session_service = $this->createMock( SessionService::class ); + $this->mock_legacy_proxy = $this->createMock( LegacyProxy::class ); + + $this->sut = new FailedTransactionRateLimiter( + $this->mock_session_service, + $this->mock_legacy_proxy + ); + } + + public function provider_should_bump_rate_limiter(): array { + return [ + 'card_declined' => [ 'card_declined', true ], + 'incorrect_number' => [ 'incorrect_number', true ], + 'incorrect_cvc' => [ 'incorrect_cvc', true ], + 'any_other_code' => [ 'any_other_code', false ], + ]; + } + + /** + * @dataProvider provider_should_bump_rate_limiter + */ + public function test_should_bump_rate_limiter( string $error_code, bool $expected ) { + $this->assertSame( $expected, $this->sut->should_bump_rate_limiter( $error_code ) ); + } +} diff --git a/tests/unit/src/Internal/Payment/PaymentContextTest.php b/tests/unit/src/Internal/Payment/PaymentContextTest.php index c34171136d2..84e10204ea5 100644 --- a/tests/unit/src/Internal/Payment/PaymentContextTest.php +++ b/tests/unit/src/Internal/Payment/PaymentContextTest.php @@ -140,6 +140,20 @@ public function test_intent() { $this->assertSame( $intent, $this->sut->get_intent() ); } + public function test_fraud_prevention_token() { + $token = 'random_token'; + + $this->sut->set_fraud_prevention_token( $token ); + $this->assertSame( $token, $this->sut->get_fraud_prevention_token() ); + } + + public function test_mode() { + $mode = 'prod'; + + $this->sut->set_mode( $mode ); + $this->assertSame( $mode, $this->sut->get_mode() ); + } + public function test_log_state_transition() { $this->sut->log_state_transition( 'First_State' ); // first transition has 'from_state' null and 'to_state' as 'First_State'. diff --git a/tests/unit/src/Internal/Payment/State/InitialStateTest.php b/tests/unit/src/Internal/Payment/State/InitialStateTest.php index 1244fe69a67..30f86e04f23 100644 --- a/tests/unit/src/Internal/Payment/State/InitialStateTest.php +++ b/tests/unit/src/Internal/Payment/State/InitialStateTest.php @@ -9,12 +9,16 @@ use WC_Helper_Intention; use WCPay\Constants\Intent_Status; +use WCPay\Exceptions\Amount_Too_Small_Exception; +use WCPay\Exceptions\API_Exception; use WCPay\Internal\Payment\Exception\StateTransitionException; +use WCPay\Internal\Payment\FailedTransactionRateLimiter; use WCPay\Internal\Payment\State\AuthenticationRequiredState; use WCPay\Internal\Payment\State\ProcessedState; -use Exception; use WCPay\Internal\Payment\State\DuplicateOrderDetectedState; use WCPay\Internal\Service\DuplicatePaymentPreventionService; +use WCPay\Internal\Service\FraudPreventionService; +use WCPay\Internal\Service\MinimumAmountService; use WCPAY_UnitTestCase; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit_Utils; @@ -86,28 +90,42 @@ class InitialStateTest extends WCPAY_UnitTestCase { */ private $mock_context; + /** + * @var MinimumAmountService|MockObject + */ + private $mock_minimum_amount_service; + + /** + * @var FailedTransactionRateLimiter|MockObject + */ + private $mock_failed_transaction_rate_limiter; + + /** + * Mocked dependencies. + * + * @var MockObject[] + */ + private $mock_deps; + /** * Set up the test. */ protected function setUp(): void { parent::setUp(); + $this->mock_context = $this->createMock( PaymentContext::class ); + $this->mock_deps = [ + $this->mock_state_factory = $this->createMock( StateFactory::class ), + $this->mock_order_service = $this->createMock( OrderService::class ), + $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class ), + $this->mock_level3_service = $this->createMock( Level3Service::class ), + $this->mock_payment_request_service = $this->createMock( PaymentRequestService::class ), + $this->mock_dpps = $this->createMock( DuplicatePaymentPreventionService::class ), + $this->mock_minimum_amount_service = $this->createMock( MinimumAmountService::class ), + $this->mock_fraud_prevention_service = $this->createMock( FraudPreventionService::class ), + $this->mock_failed_transaction_rate_limiter = $this->createMock( FailedTransactionRateLimiter::class ), + ]; - $this->mock_state_factory = $this->createMock( StateFactory::class ); - $this->mock_order_service = $this->createMock( OrderService::class ); - $this->mock_context = $this->createMock( PaymentContext::class ); - $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class ); - $this->mock_level3_service = $this->createMock( Level3Service::class ); - $this->mock_payment_request_service = $this->createMock( PaymentRequestService::class ); - $this->mock_dpps = $this->createMock( DuplicatePaymentPreventionService::class ); - - $this->sut = new InitialState( - $this->mock_state_factory, - $this->mock_order_service, - $this->mock_customer_service, - $this->mock_level3_service, - $this->mock_payment_request_service, - $this->mock_dpps - ); + $this->sut = new InitialState( ... $this->mock_deps ); $this->sut->set_context( $this->mock_context ); /** @@ -126,24 +144,19 @@ protected function setUp(): void { 'process_duplicate_payment', ] ) - ->setConstructorArgs( - [ - $this->mock_state_factory, - $this->mock_order_service, - $this->mock_customer_service, - $this->mock_level3_service, - $this->mock_payment_request_service, - $this->mock_dpps, - ] - ) + ->setConstructorArgs( $this->mock_deps ) ->getMock(); $this->mocked_sut->set_context( $this->mock_context ); } public function test_start_processing() { + $order_id = 123; + $user_id = 456; + $customer_id = 'cus_123'; $mock_request = $this->createMock( PaymentRequest::class ); $mock_processed_state = $this->createMock( ProcessedState::class ); $mock_completed_state = $this->createMock( CompletedState::class ); + $mock_order = $this->createMock( WC_Order::class ); $mock_processed_state->expects( $this->once() ) ->method( 'complete_processing' ) @@ -152,9 +165,19 @@ public function test_start_processing() { // Verify that the context is populated. $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 100 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'usd' ); + $this->mocked_sut->expects( $this->once() )->method( 'process_duplicate_order' )->willReturn( null ); $this->mocked_sut->expects( $this->once() )->method( 'process_duplicate_payment' )->willReturn( null ); + $this->mock_customer_data( $user_id, $order_id, $mock_order, $customer_id ); + $intent = WC_Helper_Intention::create_intention(); $this->mock_payment_request_service @@ -175,9 +198,53 @@ public function test_start_processing() { $this->assertSame( $mock_completed_state, $result ); } + public function test_start_processing_will_throw_exception_when_minimum_amount_occurs() { + $mock_request = $this->createMock( PaymentRequest::class ); + $mock_order = $this->createMock( WC_Order::class ); + $small_amount_exception = new Amount_Too_Small_Exception( 'Amount too small', 50, 'EUR', 400 ); + + $this->mock_payment_request_service + ->expects( $this->once() ) + ->method( 'create_intent' ) + ->with( $this->mock_context ) + ->willThrowException( $small_amount_exception ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 1 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'eur' ); + + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + // Mock get customer. + $this->mock_customer_data( 1, 1, $mock_order, 'cus_mock' ); + + $this->mock_failed_transaction_rate_limiter + ->expects( $this->once() ) + ->method( 'is_limited' ) + ->willReturn( false ); + $this->mock_minimum_amount_service->expects( $this->once() ) + ->method( 'store_amount_from_exception' ) + ->with( $small_amount_exception ); + + $this->expectExceptionObject( $small_amount_exception ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); + } + public function test_start_processing_will_transition_to_error_state_when_api_exception_occurs() { + $order_id = 123; + $user_id = 456; + $customer_id = 'cus_123'; $mock_request = $this->createMock( PaymentRequest::class ); $mock_error_state = $this->createMock( SystemErrorState::class ); + $mock_order = $this->createMock( WC_Order::class ); + + $this->mock_customer_data( $user_id, $order_id, $mock_order, $customer_id ); $this->mock_payment_request_service ->expects( $this->once() ) @@ -189,6 +256,13 @@ public function test_start_processing_will_transition_to_error_state_when_api_ex $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 100 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'usd' ); + $this->mock_state_factory->expects( $this->once() ) ->method( 'create_state' ) ->with( SystemErrorState::class, $this->mock_context ) @@ -197,11 +271,61 @@ public function test_start_processing_will_transition_to_error_state_when_api_ex $this->assertSame( $mock_error_state, $result ); } + public function test_start_processing_will_bump_rate_limiter_when_specific_error_codes_happen() { + $mock_request = $this->createMock( PaymentRequest::class ); + $mock_order = $this->createMock( WC_Order::class ); + $error_code = 'card_declined'; + $exception = new API_Exception( 'Card decline', $error_code, 400 ); + + $this->mock_payment_request_service + ->expects( $this->once() ) + ->method( 'create_intent' ) + ->with( $this->mock_context ) + ->willThrowException( $exception ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 1 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'eur' ); + + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + // Mock get customer. + $this->mock_customer_data( 1, 1, $mock_order, 'cus_mock' ); + + $this->mock_failed_transaction_rate_limiter + ->expects( $this->once() ) + ->method( 'is_limited' ) + ->willReturn( false ); + $this->mock_failed_transaction_rate_limiter + ->expects( $this->once() ) + ->method( 'should_bump_rate_limiter' ) + ->with( $error_code ) + ->willReturn( true ); + + $this->mock_failed_transaction_rate_limiter + ->expects( $this->once() ) + ->method( 'bump' ); + + $this->expectExceptionObject( $exception ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); + } + public function test_processing_will_transition_to_auth_required_state() { $order_id = 123; + $user_id = 456; + $customer_id = 'cus_123'; + $mock_order = $this->createMock( WC_Order::class ); $mock_request = $this->createMock( PaymentRequest::class ); $mock_auth_state = $this->createMock( AuthenticationRequiredState::class ); + $this->mock_customer_data( $user_id, $order_id, $mock_order, $customer_id ); + // Create an intent, and make sure it will be returned by the service. $mock_intent = $this->createMock( WC_Payments_API_Payment_Intention::class ); $mock_intent->expects( $this->once() )->method( 'get_status' )->willReturn( Intent_Status::REQUIRES_ACTION ); @@ -214,6 +338,13 @@ public function test_processing_will_transition_to_auth_required_state() { $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 100 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'usd' ); + // Before the transition, the order service should update the order. $this->mock_context->expects( $this->once() ) ->method( 'get_order_id' ) @@ -231,6 +362,34 @@ public function test_processing_will_transition_to_auth_required_state() { $this->assertSame( $mock_auth_state, $result ); } + public function test_start_processing_throw_exceptions_due_to_rate_limited() { + $order_id = 123; + $mock_request = $this->createMock( PaymentRequest::class ); + + // Arrange mocks. + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + $this->mock_failed_transaction_rate_limiter->expects( $this->once() ) + ->method( 'is_limited' ) + ->willReturn( true ); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_order_id' ) + ->willReturn( $order_id ); + $this->mock_order_service->expects( $this->once() ) + ->method( 'add_rate_limiter_note' ) + ->with( $order_id ); + + $this->expectException( StateTransitionException::class ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); + } + public function test_start_processing_throw_exceptions_due_to_invalid_phone() { $mock_request = $this->createMock( PaymentRequest::class ); @@ -245,7 +404,22 @@ public function test_start_processing_throw_exceptions_due_to_invalid_phone() { // Act. $this->mocked_sut->start_processing( $mock_request ); + } + + public function test_start_processing_throw_exceptions_due_to_invalid_fraud_prevention_token() { + $mock_request = $this->createMock( PaymentRequest::class ); + // Arrange mocks. + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( false ); + + $this->expectException( StateTransitionException::class ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); } public function provider_start_processing_then_detect_duplicates() { @@ -273,37 +447,76 @@ public function test_start_processing_then_detect_duplicates( bool $is_duplicate ->method( 'process_duplicate_payment' ) ->willReturn( $is_duplicate_order ? null : $mock_returned_state ); + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + // Act. $result = $this->mocked_sut->start_processing( $mock_request ); $this->assertInstanceOf( $return_state_class, $result ); } + public function test_start_processing_throws_exception_due_to_minimum_amount() { + $mock_request = $this->createMock( PaymentRequest::class ); + $small_amount_exception = new Amount_Too_Small_Exception( 'Amount too small', 50, 'EUR', 400 ); + + // Arrange mocks. + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Verify that FraudPreventionService is called. + $this->mock_fraud_prevention_service->method( 'verify_token' )->willReturn( true ); + + $this->mock_failed_transaction_rate_limiter + ->expects( $this->once() ) + ->method( 'is_limited' ) + ->willReturn( false ); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_currency' ) + ->willReturn( 'EUR' ); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_amount' ) + ->willReturn( 50 ); + + $this->mock_minimum_amount_service->expects( $this->once() ) + ->method( 'verify_amount' ) + ->with( 'EUR', 50 ) + ->willThrowException( $small_amount_exception ); + + $this->expectExceptionObject( $small_amount_exception ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); + } + public function test_populate_context_from_request() { $payment_method = new NewPaymentMethod( 'pm_123' ); $fingerprint = 'fingerprint'; $cvc_confirmation = 'CVCConfirmation'; + $fraud_token = 'fraud_prevention_token'; // Setup the mock request. $mock_request = $this->createMock( PaymentRequest::class ); $mock_request->expects( $this->once() )->method( 'get_payment_method' )->willReturn( $payment_method ); $mock_request->expects( $this->once() )->method( 'get_cvc_confirmation' )->willReturn( $cvc_confirmation ); $mock_request->expects( $this->once() )->method( 'get_fingerprint' )->willReturn( $fingerprint ); + $mock_request->expects( $this->once() )->method( 'get_fraud_prevention_token' )->willReturn( $fraud_token ); // Assume that everything from the request would be imported into the context. $this->mock_context->expects( $this->once() )->method( 'set_payment_method' )->with( $payment_method ); $this->mock_context->expects( $this->once() )->method( 'set_cvc_confirmation' )->with( $cvc_confirmation ); $this->mock_context->expects( $this->once() )->method( 'set_fingerprint' )->with( $fingerprint ); + $this->mock_context->expects( $this->once() )->method( 'set_fraud_prevention_token' )->with( $fraud_token ); PHPUnit_Utils::call_method( $this->sut, 'populate_context_from_request', [ $mock_request ] ); } public function test_populate_context_from_order() { - $order_id = 123; - $user_id = 456; - $customer_id = 'cus_123'; + $order_id = 123; + $metadata = [ 'sample' => 'true' ]; $level3_data = [ 'items' => [] ]; - $mock_order = $this->createMock( WC_Order::class ); // Prepare the order ID. $this->mock_context->expects( $this->once() ) @@ -333,22 +546,6 @@ public function test_populate_context_from_order() { ->method( 'set_level3_data' ) ->with( $level3_data ); - // Arrange customer management. - $this->mock_context->expects( $this->once() ) - ->method( 'get_user_id' ) - ->willReturn( $user_id ); - $this->mock_order_service->expects( $this->once() ) - ->method( '_deprecated_get_order' ) - ->with( $order_id ) - ->willReturn( $mock_order ); - $this->mock_customer_service->expects( $this->once() ) - ->method( 'get_or_create_customer_id_from_order' ) - ->with( $user_id, $mock_order ) - ->willReturn( $customer_id ); - $this->mock_context->expects( $this->once() ) - ->method( 'set_customer_id' ) - ->with( $customer_id ); - PHPUnit_Utils::call_method( $this->sut, 'populate_context_from_order', [] ); } @@ -520,4 +717,35 @@ public function test_process_duplicate_payment_returns_completed_state() { $result = PHPUnit_Utils::call_method( $this->sut, 'process_duplicate_payment', [] ); $this->assertInstanceOf( CompletedState::class, $result ); } + + /** + * Mock customer data. + * @param int $user_id User id. + * @param int $order_id Order id. + * @param MockObject|WC_Order $mock_order Mock order. + * @param string $customer_id Customer id. + * + * @return void + */ + private function mock_customer_data( int $user_id, int $order_id, $mock_order, string $customer_id ) { + + // Arrange customer management. + $this->mock_context->expects( $this->once() ) + ->method( 'get_user_id' ) + ->willReturn( $user_id ); + $this->mock_context->expects( $this->once() ) + ->method( 'get_order_id' ) + ->willReturn( $order_id ); + $this->mock_order_service->expects( $this->once() ) + ->method( '_deprecated_get_order' ) + ->with( $order_id ) + ->willReturn( $mock_order ); + $this->mock_customer_service->expects( $this->once() ) + ->method( 'get_or_create_customer_id_from_order' ) + ->with( $user_id, $mock_order ) + ->willReturn( $customer_id ); + $this->mock_context->expects( $this->once() ) + ->method( 'set_customer_id' ) + ->with( $customer_id ); + } } diff --git a/tests/unit/src/Internal/Payment/State/ProcessedStateTest.php b/tests/unit/src/Internal/Payment/State/ProcessedStateTest.php index 6a097b90aa7..b7f2febece9 100644 --- a/tests/unit/src/Internal/Payment/State/ProcessedStateTest.php +++ b/tests/unit/src/Internal/Payment/State/ProcessedStateTest.php @@ -15,6 +15,9 @@ use WCPay\Internal\Payment\State\StateFactory; use WCPay\Internal\Service\DuplicatePaymentPreventionService; use WCPay\Internal\Service\OrderService; +use WCPay\Internal\Proxy\LegacyProxy; +use WCPay\Payment_Methods\UPE_Payment_Gateway; +use WCPay\Payment_Methods\UPE_Split_Payment_Gateway; use WCPAY_UnitTestCase; /** @@ -48,6 +51,11 @@ class ProcessedStateTest extends WCPAY_UnitTestCase { */ private $mock_context; + /** + * @var LegacyProxy|MockObject + */ + private $mock_legacy_proxy; + /** * Set up the test. */ @@ -58,11 +66,13 @@ protected function setUp(): void { $this->mock_order_service = $this->createMock( OrderService::class ); $this->mock_dpps = $this->createMock( DuplicatePaymentPreventionService::class ); $this->mock_context = $this->createMock( PaymentContext::class ); + $this->mock_legacy_proxy = $this->createMock( LegacyProxy::class ); $this->sut = new ProcessedState( $this->mock_state_factory, $this->mock_order_service, - $this->mock_dpps + $this->mock_dpps, + $this->mock_legacy_proxy ); $this->sut->set_context( $this->mock_context ); } @@ -93,8 +103,77 @@ public function test_complete_processing_will_transition_to_completed_state() { ->with( CompletedState::class, $this->mock_context ) ->willReturn( $mock_completed_state ); + $this->mock_legacy_proxy + ->expects( $this->exactly( 2 ) ) + ->method( 'call_function' ) + ->withConsecutive( [ 'wc_reduce_stock_levels', 1 ], [ 'wc' ] ) + ->willReturnCallback( + function ( $arg ) { + if ( 'wc' === $arg ) { + $mock_cart = $this->getMockBuilder( \stdClass::class ) + ->addMethods( [ 'empty_cart' ] ) + ->getMock(); + return (object) [ + 'cart' => $mock_cart, + ]; + } + } + ); + $result = $this->sut->complete_processing(); $this->assertSame( $mock_completed_state, $result ); } + + public function test_clean_up_functions() { + + // setup. + $intent = WC_Helper_Intention::create_intention(); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_intent' ) + ->willReturn( $intent ); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_order_id' ) + ->willReturn( 1 ); + + $mock_completed_state = $this->createMock( CompletedState::class ); + $this->mock_state_factory->expects( $this->once() ) + ->method( 'create_state' ) + ->with( CompletedState::class, $this->mock_context ) + ->willReturn( $mock_completed_state ); + + $mock_cart = $this->getMockBuilder( \stdClass::class ) + ->addMethods( [ 'empty_cart' ] ) + ->getMock(); + + // Test that 'wc_reduce_stock_levels' is called. + $this->mock_legacy_proxy + ->expects( $this->exactly( 2 ) ) + ->method( 'call_function' ) + ->withConsecutive( [ 'wc_reduce_stock_levels', 1 ], [ 'wc' ] ) + ->willReturnCallback( + function ( $arg ) use ( $mock_cart ) { + if ( 'wc' === $arg ) { + return (object) [ + 'cart' => $mock_cart, + ]; + } + } + ); + + // Test the 'empty_cart' is called. + $mock_cart->expects( $this->once() ) + ->method( 'empty_cart' ); + + // Test methods to remove upe payment intent are called. + $this->mock_legacy_proxy + ->expects( $this->exactly( 2 ) ) + ->method( 'call_static' ) + ->withConsecutive( [ UPE_Payment_Gateway::class, 'remove_upe_payment_intent_from_session' ], [ UPE_Split_Payment_Gateway::class, 'remove_upe_payment_intent_from_session' ] ); + + $this->sut->complete_processing(); + } + } diff --git a/tests/unit/src/Internal/Service/FraudPreventionServiceTest.php b/tests/unit/src/Internal/Service/FraudPreventionServiceTest.php new file mode 100644 index 00000000000..e8186e171cd --- /dev/null +++ b/tests/unit/src/Internal/Service/FraudPreventionServiceTest.php @@ -0,0 +1,167 @@ +mock_session_service = $this->createMock( SessionService::class ); + $this->mock_account_service = $this->createMock( WC_Payments_Account::class ); + + $this->sut = new FraudPreventionService( + $this->mock_session_service, + $this->mock_account_service + ); + } + + public function provider_enabled_options(): array { + return [ + [ true, true ], + [ false, false ], + ]; + } + + /** + * @dataProvider provider_enabled_options + */ + public function test_is_enabled( $account_flag, $return_value ) { + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'is_card_testing_protection_eligible' ) + ->willReturn( $account_flag ); + + $this->assertSame( $return_value, $this->sut->is_enabled() ); + } + + public function test_get_token_from_session() { + $token_stub = 'test-token'; + $this->mock_session_service + ->expects( $this->once() ) + ->method( 'get' ) + ->with( FraudPreventionService::TOKEN_NAME ) + ->willReturn( $token_stub ); + + $this->assertSame( $token_stub, $this->sut->get_token() ); + } + + public function test_get_token_on_first_page_load() { + $new_token_stub = 'new-token'; + $this->mock_sut = $this->getMockBuilder( FraudPreventionService::class ) + ->setConstructorArgs( [ $this->mock_session_service, $this->mock_account_service ] ) + ->onlyMethods( [ 'regenerate_token' ] ) + ->getMock(); + + $this->mock_session_service + ->expects( $this->once() ) + ->method( 'get' ) + ->with( FraudPreventionService::TOKEN_NAME ) + ->willReturn( null ); + + $this->mock_sut + ->expects( $this->once() ) + ->method( 'regenerate_token' ) + ->willReturn( $new_token_stub ); + + $this->assertSame( $new_token_stub, $this->mock_sut->get_token() ); + } + + public function test_regenerate_token() { + $this->mock_session_service + ->expects( $this->once() ) + ->method( 'set' ) + ->with( FraudPreventionService::TOKEN_NAME, $this->isType( 'string' ) ); + + $token_value = $this->sut->regenerate_token(); + + $this->assertIsString( $token_value ); + } + + public function test_verify_token_returns_true_when_is_disabled() { + // Prepare mock for method `is_enabled`. + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'is_card_testing_protection_eligible' ) + ->willReturn( false ); + + $is_valid = $this->sut->verify_token( 'any-token' ); + + $this->assertTrue( $is_valid ); + } + + public function provider_verify_token_when_is_enabled(): array { + return [ + 'Both tokens null' => [ null, null, false ], + 'Only provided token null' => [ 'session-token', null, false ], + 'Only session token null ' => [ null, 'provided-token', false ], + 'Two tokens mismatched' => [ 'session-token', 'provided-token', false ], + 'Valid tokens' => [ 'valid-token', 'valid-token', true ], + ]; + } + + /** + * @dataProvider provider_verify_token_when_is_enabled + */ + public function test_verify_token_when_is_enabled( + ?string $session_token, + ?string $provided_token, + bool $expected + ) { + // Prepare mock for method `is_enabled`. + $this->mock_account_service + ->expects( $this->once() ) + ->method( 'is_card_testing_protection_eligible' ) + ->willReturn( true ); + + $this->mock_session_service + ->expects( $this->once() ) + ->method( 'get' ) + ->with( FraudPreventionService::TOKEN_NAME ) + ->willReturn( $session_token ); + + $result = $this->sut->verify_token( $provided_token ); + + $this->assertSame( $expected, $result ); + } +} diff --git a/tests/unit/src/Internal/Service/MinimumAmountServiceTest.php b/tests/unit/src/Internal/Service/MinimumAmountServiceTest.php new file mode 100644 index 00000000000..9d834026e30 --- /dev/null +++ b/tests/unit/src/Internal/Service/MinimumAmountServiceTest.php @@ -0,0 +1,100 @@ +mock_legacy_proxy = $this->createMock( LegacyProxy::class ); + $this->sut = new MinimumAmountService( $this->mock_legacy_proxy ); + } + + public function test_store_amount_from_exception() { + $exception = new Amount_Too_Small_Exception( 'Amount too small', 100, 'EUR', 400 ); + + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'set_transient', 'wcpay_minimum_amount_eur', 100, DAY_IN_SECONDS ); + + $this->sut->store_amount_from_exception( $exception ); + } + + public function test_verify_amount_returns_void() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_transient', 'wcpay_minimum_amount_eur' ) + ->willReturn( 100 ); + + $this->sut->verify_amount( 'EUR', 150 ); + } + + public function test_verify_amount_throw_exception() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_transient', 'wcpay_minimum_amount_eur' ) + ->willReturn( 100 ); + + $this->expectException( Amount_Too_Small_Exception::class ); + $this->expectExceptionMessage( 'Order amount too small' ); + + $this->sut->verify_amount( 'EUR', 50 ); + } + + public function test_set_cached_amount() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'set_transient', 'wcpay_minimum_amount_usd', 100, DAY_IN_SECONDS ); + + \PHPUnit_Utils::call_method( $this->sut, 'set_cached_amount', [ 'USD', 100 ] ); + } + + public function provider_get_cached_amount() { + return [ + 'Transient not set' => [ false, 0 ], + 'Transient invalid value' => [ null, 0 ], + 'Transient valid value ' => [ 123, 123 ], + ]; + } + + /** + * @dataProvider provider_get_cached_amount + */ + public function test_get_cached_amount( $transient_value, $expected ) { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_transient', 'wcpay_minimum_amount_eur' ) + ->willReturn( $transient_value ); + + \PHPUnit_Utils::call_method( $this->sut, 'get_cached_amount', [ 'EUR' ] ); + } +} diff --git a/tests/unit/src/Internal/Service/OrderServiceTest.php b/tests/unit/src/Internal/Service/OrderServiceTest.php index 1aba6fa24d2..63f40cf074f 100644 --- a/tests/unit/src/Internal/Service/OrderServiceTest.php +++ b/tests/unit/src/Internal/Service/OrderServiceTest.php @@ -13,8 +13,10 @@ use WC_Payments_API_Charge; use WC_Payments_API_Payment_Intention; use WC_Payments_API_Setup_Intention; +use WC_Payments_Explicit_Price_Formatter; use WC_Payments_Features; use WC_Payments_Order_Service; +use WC_Payments_Utils; use WCPay\Constants\Payment_Type; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Internal\Payment\PaymentContext; @@ -277,18 +279,11 @@ public function test_import_order_data_to_payment_context( $user ) { $mock_order->expects( $this->once() ) ->method( 'get_user' ) ->willReturn( $user ?? false ); - if ( ! $user ) { - $user = $this->createMock( WP_User::class ); - $user->ID = 10; - - $this->mock_legacy_proxy->expects( $this->once() ) - ->method( 'call_function' ) - ->with( 'wp_get_current_user' ) - ->willReturn( $user ); - } + + // Mock set user id. $mock_context->expects( $this->once() ) ->method( 'set_user_id' ) - ->with( 10 ); + ->with( $user->ID ?? null ); // Act. $this->sut->import_order_data_to_payment_context( $this->order_id, $mock_context ); @@ -319,7 +314,7 @@ public function test_update_order_from_successful_intent( $intent ) { // Create a mock order that will be used. $mock_order = $this->createMock( WC_Order::class ); - $this->sut->expects( $this->once() ) + $this->sut->expects( $this->exactly( 2 ) ) ->method( 'get_order' ) ->with( $this->order_id ) ->willReturn( $mock_order ); @@ -355,6 +350,9 @@ public function test_update_order_from_successful_intent( $intent ) { $mock_context->expects( $this->once() ) ->method( 'get_currency' ) ->willReturn( $currency ); + $mock_context->expects( $this->once() ) + ->method( 'get_mode' ) + ->willReturn( 'prod' ); $this->mock_legacy_service->expects( $this->once() ) ->method( 'attach_intent_info_to_order' ) @@ -623,6 +621,49 @@ public function test_add_note() { $this->assertSame( $note_id, $result ); } + public function test_add_rate_limiter_note() { + $mock_order = $this->mock_get_order(); + $mock_order->expects( $this->once() ) + ->method( 'get_total' ) + ->willReturn( 50.12 ); + $mock_order->expects( $this->once() ) + ->method( 'get_currency' ) + ->willReturn( 'EUR' ); + + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'wc_price', 50.12, [ 'currency' => 'EUR' ] ) + ->willReturn( '€50.12' ); + + $first_call = [ + WC_Payments_Explicit_Price_Formatter::class, + 'get_explicit_price', + '€50.12', + $mock_order, + ]; + $second_call = [ + WC_Payments_Utils::class, + 'esc_interpolated_html', + 'A payment of %1$s failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.', + [ 'strong' => '' ], + ]; + $explicit_price = '€50.12 EUR'; + $note_content = 'A payment of €50.12 EUR failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.'; + $this->mock_legacy_proxy->expects( $this->exactly( 2 ) ) + ->method( 'call_static' ) + ->withConsecutive( $first_call, $second_call ) + ->willReturnOnConsecutiveCalls( $explicit_price, $note_content ); + + $note_id = 777; + $mock_order->expects( $this->once() ) + ->method( 'add_order_note' ) + ->with( $note_content ) + ->willReturn( $note_id ); + + $result = $this->sut->add_rate_limiter_note( $this->order_id ); + $this->assertSame( $note_id, $result ); + } + public function test_delete_order() { $force_delete = false; $expected = true; @@ -637,6 +678,24 @@ public function test_delete_order() { $this->assertSame( $expected, $result ); } + public function test_set_mode() { + $this->mock_get_order() + ->expects( $this->once() ) + ->method( 'update_meta_data' ) + ->with( '_wcpay_mode', 'prod' ); + $this->sut->set_mode( $this->order_id, 'prod' ); + } + + public function test_get_mode() { + $this->mock_get_order() + ->expects( $this->once() ) + ->method( 'get_meta' ) + ->with( '_wcpay_mode', true ) + ->willReturn( 'test' ); + $result = $this->sut->get_mode( $this->order_id, true ); + $this->assertSame( 'test', $result ); + } + /** * Mocks order retrieval. * @@ -654,4 +713,5 @@ private function mock_get_order( int $order_id = null ) { return $mock_order; } + } diff --git a/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php b/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php index feb7b538a16..b2512762a3e 100644 --- a/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php +++ b/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php @@ -7,6 +7,9 @@ namespace WCPay\Tests\Internal\Service; +use Exception; +use WCPAY_UnitTestCase; +use PHPUnit_Utils; use PHPUnit\Framework\MockObject\MockObject; use WC_Helper_Intention; use WC_Payment_Gateway_WCPay; @@ -17,7 +20,7 @@ use WCPay\Internal\Payment\PaymentRequest; use WCPay\Internal\Payment\State\CompletedState; use WCPay\Internal\Payment\State\InitialState; -use WCPAY_UnitTestCase; +use WCPay\Core\Mode; use WCPay\Internal\Proxy\LegacyProxy; use WCPay\Internal\Payment\State\StateFactory; use WCPay\Internal\Service\PaymentProcessingService; @@ -27,6 +30,7 @@ * Payment processing service unit tests. */ class PaymentProcessingServiceTest extends WCPAY_UnitTestCase { + /** * Service under test. * @@ -49,6 +53,11 @@ class PaymentProcessingServiceTest extends WCPAY_UnitTestCase { */ private $mock_context_logger; + /** + * @var Mode|MockObject + */ + private $mock_mode; + /** * Set up the test. */ @@ -58,6 +67,7 @@ protected function setUp(): void { $this->mock_state_factory = $this->createMock( StateFactory::class ); $this->mock_legacy_proxy = $this->createMock( LegacyProxy::class ); $this->mock_context_logger = $this->createMock( PaymentContextLoggerService::class ); + $this->mock_mode = $this->createMock( Mode::class ); $this->sut = $this->getMockBuilder( PaymentProcessingService::class ) ->setConstructorArgs( @@ -65,6 +75,7 @@ protected function setUp(): void { $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, + $this->mock_mode, ] ) ->onlyMethods( [ 'create_payment_context' ] ) @@ -79,7 +90,7 @@ public function test_process_payment_happy_path() { $mock_initial_state = $this->createMock( InitialState::class ); $mock_completed_state = $this->createMock( CompletedState::class ); - $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); $this->mock_state_factory->expects( $this->once() ) ->method( 'create_state' ) @@ -103,7 +114,7 @@ public function test_process_payment_happy_path() { * Test the basic happy path of processing a payment. */ public function test_process_payment_happy_path_without_mock_builder() { - $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); $mock_initial_state = $this->createMock( InitialState::class ); $mock_completed_state = $this->createMock( CompletedState::class ); @@ -126,8 +137,21 @@ public function test_process_payment_happy_path_without_mock_builder() { $this->assertSame( $mock_completed_state, $result ); } + /** + * Test the process payment when mode not initialized. + */ + public function test_process_payment_mode_throws_exception() { + $this->mock_mode + ->expects( $this->once() ) + ->method( 'is_test' ) + ->willThrowException( new Exception( 'Could not initialize' ) ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); + $context = PHPUnit_Utils::call_method( $sut, 'create_payment_context', [ 123 ] ); + $this->assertEquals( 'unknown', $context->get_mode() ); + } + public function test_get_authentication_redirect_url_will_return_url_from_payment_intent() { - $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); $url = 'localhost'; $intent_data = [ @@ -149,7 +173,6 @@ public function test_get_authentication_redirect_url_will_return_url_from_paymen $result = $sut->get_authentication_redirect_url( $intent, 1 ); $this->assertSame( $url, $result ); - } /** diff --git a/tests/unit/src/Internal/Service/SessionServiceTest.php b/tests/unit/src/Internal/Service/SessionServiceTest.php index b2218754ca0..41be39e2f27 100644 --- a/tests/unit/src/Internal/Service/SessionServiceTest.php +++ b/tests/unit/src/Internal/Service/SessionServiceTest.php @@ -24,13 +24,11 @@ class SessionServiceTest extends WCPAY_UnitTestCase { */ private $sut; - /** * @var LegacyProxy|MockObject */ private $mock_legacy_proxy; - /** * Set up the test. */ diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php index a125c94a57e..da7ed2798db 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php @@ -321,7 +321,7 @@ public function test_intent_status_success() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process a successful payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -385,7 +385,7 @@ public function test_intent_status_success_logged_out_user() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process a successful payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -490,7 +490,7 @@ public function test_intent_status_requires_capture() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::MANUAL() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::MANUAL(), 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -510,7 +510,7 @@ public function test_exception_thrown() { ->expects( $this->once() ) ->method( 'get_customer_id_by_user_id' ) ->willReturn( 'cus_mock' ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::AUTOMATIC() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::AUTOMATIC(), 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Arrange: Throw an exception in create_and_confirm_intention. $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); @@ -937,7 +937,7 @@ public function test_intent_status_requires_action() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -1052,7 +1052,7 @@ public function test_setup_intent_status_requires_action() { ->method( 'empty_cart' ); // Act: prepare payment information. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information->must_save_payment_method_to_store(); // Act: process payment. @@ -1205,7 +1205,7 @@ public function test_updates_customer_with_order_data() { // Arrange: Create a mock cart. $mock_cart = $this->createMock( 'WC_Cart' ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1417,7 +1417,7 @@ public function test_process_payment_using_platform_payment_method_adds_platform ->method( 'format_response' ) ->willReturn( [ 'id' => 'ch_mock' ] ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1476,7 +1476,7 @@ public function test_process_payment_for_subscription_in_woopay_adds_subscriptio $request->expects( $this->once() ) ->method( 'format_response' ) ->willReturn( $intent ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1548,7 +1548,7 @@ public function test_process_payment_from_woopay_adds_meta_to_oder() { $charge_request->expects( $this->once() ) ->method( 'format_response' ) ->willReturn( [ 'id' => 'ch_mock' ] ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1591,7 +1591,7 @@ public function test_process_payment_for_subscription_from_woopay_does_not_save_ ->method( 'format_response' ) ->willReturn( $intent ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information->must_save_payment_method_to_store(); // Assert: The payment method is not added to the user. @@ -1637,7 +1637,7 @@ public function test_process_payment_for_subscription_from_woopay_save_token_if_ ->method( 'format_response' ) ->willReturn( $intent ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information->must_save_payment_method_to_store(); // Assert: The payment method is added to the user. diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index fe235a0c7fa..82ee481b01f 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -28,6 +28,9 @@ use WCPay\Internal\Service\OrderService; use WCPay\Internal\Service\PaymentProcessingService; use WCPay\Payment_Information; +use WCPay\Payment_Methods\CC_Payment_Method; +use WCPay\Payment_Methods\Sepa_Payment_Method; +use WCPay\Payment_Methods\UPE_Split_Payment_Gateway; use WCPay\WooPay\WooPay_Utilities; use WCPay\Session_Rate_Limiter; use WCPay\WC_Payments_Checkout; @@ -201,6 +204,7 @@ public function set_up() { $this->mock_localization_service, $this->mock_fraud_service ); + WC_Payments::set_gateway( $this->wcpay_gateway ); $this->woopay_utilities = new WooPay_Utilities(); @@ -364,12 +368,6 @@ public function test_attach_exchange_info_to_order_with_different_order_currency $this->assertEquals( 0.853, $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate' ) ); } - public function test_payment_fields_outputs_fields() { - $this->wcpay_gateway->payment_fields(); - - $this->expectOutputRegex( '/
    <\/div>/' ); - } - public function test_save_card_checkbox_not_displayed_when_saved_cards_disabled() { $this->wcpay_gateway->update_option( 'saved_cards', 'no' ); @@ -1333,7 +1331,7 @@ public function test_payment_request_form_field_defaults() { $this->wcpay_gateway->get_option( 'payment_request_button_locations' ) ); $this->assertEquals( - 'default', + 'medium', $this->wcpay_gateway->get_option( 'payment_request_button_size' ) ); @@ -1404,7 +1402,7 @@ public function test_process_payment_for_order_not_from_request() { $order->add_payment_token( $token ); $order->save(); - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); $request->expects( $this->once() ) @@ -1425,13 +1423,222 @@ public function test_process_payment_for_order_rejects_with_cached_minimum_amoun $order->set_total( 0.45 ); $order->save(); - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $this->expectException( Exception::class ); $this->expectExceptionMessage( 'The selected payment method requires a total amount of at least $0.50.' ); $this->wcpay_gateway->process_payment_for_order( WC()->cart, $pi ); } + public function test_mandate_data_not_added_to_payment_intent_if_not_required() { + // Mandate data is required for SEPA and Stripe Link only, hence initializing the gateway with a CC payment method should not add mandate data. + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new CC_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $payment_method = 'woocommerce_payments_sepa_debit'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->save(); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) ); + $request->expects( $this->never() ) + ->method( 'set_mandate_data' ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + + public function test_mandate_data_added_to_payment_intent_if_required() { + // Mandate data is required for SEPA and Stripe Link, hence initializing the gateway with a SEPA payment method should add mandate data. + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new Sepa_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $payment_method = 'woocommerce_payments_sepa_debit'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->save(); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) ); + + $request->expects( $this->once() ) + ->method( 'set_mandate_data' ) + ->with( + $this->callback( + function ( $data ) { + return isset( $data['customer_acceptance']['type'] ) && + 'online' === $data['customer_acceptance']['type'] && + isset( $data['customer_acceptance']['online'] ) && + is_array( $data['customer_acceptance']['online'] ); + } + ) + ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + + public function test_mandate_data_not_added_to_setup_intent_request_when_link_is_disabled() { + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new CC_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $gateway->settings['upe_enabled_payment_method_ids'] = [ 'card' ]; + + $payment_method = 'woocommerce_payments'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 0 ); + $order->save(); + $customer = 'cus_12345'; + + $this->mock_customer_service + ->expects( $this->once() ) + ->method( 'get_customer_id_by_user_id' ) + ->will( $this->returnValue( $customer ) ); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + $pi->must_save_payment_method_to_store(); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Setup_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_setup_intention( + [ 'id' => 'seti_mock_123' ] + ) + ); + $this->mock_token_service + ->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->willReturn( new WC_Payment_Token_CC() ); + + $request->expects( $this->never() ) + ->method( 'set_mandate_data' ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + + public function test_mandate_data_added_to_setup_intent_request_when_link_is_enabled() { + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new CC_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $gateway->settings['upe_enabled_payment_method_ids'] = [ 'card', 'link' ]; + + $payment_method = 'woocommerce_payments'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 0 ); + $order->save(); + $customer = 'cus_12345'; + + $this->mock_customer_service + ->expects( $this->once() ) + ->method( 'get_customer_id_by_user_id' ) + ->will( $this->returnValue( $customer ) ); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + $pi->must_save_payment_method_to_store(); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Setup_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_setup_intention( + [ 'id' => 'seti_mock_123' ] + ) + ); + $this->mock_token_service + ->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->willReturn( new WC_Payment_Token_CC() ); + + $request->expects( $this->once() ) + ->method( 'set_mandate_data' ) + ->with( + $this->callback( + function ( $data ) { + return isset( $data['customer_acceptance']['type'] ) && + 'online' === $data['customer_acceptance']['type'] && + isset( $data['customer_acceptance']['online'] ) && + is_array( $data['customer_acceptance']['online'] ); + } + ) + ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + public function test_process_payment_for_order_cc_payment_method() { $payment_method = 'woocommerce_payments'; $expected_upe_payment_method_for_pi_creation = 'card'; @@ -1442,7 +1649,7 @@ public function test_process_payment_for_order_cc_payment_method() { $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; $_POST['payment_method'] = $payment_method; - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); $request->expects( $this->once() ) @@ -1465,7 +1672,7 @@ public function test_process_payment_for_order_upe_payment_method() { $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; $_POST['payment_method'] = $payment_method; - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); $request->expects( $this->once() ) diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index af9a8438e8b..35a94949c56 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -94,7 +94,7 @@ public function test_filters_registered_properly() { $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_wcpay_connect' ] ), 'maybe_redirect_to_wcpay_connect action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_capital_offer' ] ), 'maybe_redirect_to_capital_offer action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_server_link' ] ), 'maybe_redirect_to_server_link action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_settings_to_connect' ] ), 'maybe_redirect_settings_to_connect action does not exist.' ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_settings_to_connect_or_overview' ] ), 'maybe_redirect_settings_to_connect_or_overview action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_onboarding_flow_to_overview' ] ), 'maybe_redirect_onboarding_flow_to_overview action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_activate_woopay' ] ), 'maybe_activate_woopay action does not exist.' ); $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_instant_deposits_inbox_note' ] ), 'handle_instant_deposits_inbox_note action does not exist.' ); @@ -434,42 +434,45 @@ public function data_maybe_redirect_onboarding_flow_to_overview() { } /** - * @dataProvider data_maybe_redirect_settings_to_connect + * @dataProvider data_maybe_redirect_settings_to_connect_or_overview */ - public function test_maybe_redirect_settings_to_connect( $expected_redirect_to_count, $details_submitted, $get_params ) { + public function test_maybe_redirect_settings_to_connect_or_overview( $expected_redirect_to_count, $details_submitted, $get_params, $no_account = false, $path = null ) { wp_set_current_user( 1 ); $_GET = $get_params; - $this->cache_account_details( - [ - 'account_id' => 'acc_test', - 'is_live' => true, - 'details_submitted' => $details_submitted, - ] - ); - + if ( ! $no_account ) { + $this->cache_account_details( + [ + 'account_id' => 'acc_test', + 'is_live' => true, + 'details_submitted' => $details_submitted, + ] + ); + } // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. $mock_wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) ->setMethods( [ 'redirect_to' ] ) ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) ->getMock(); - $mock_wcpay_account->expects( $this->exactly( $expected_redirect_to_count ) )->method( 'redirect_to' ); + $mock_wcpay_account->expects( $this->exactly( $expected_redirect_to_count ) ) + ->method( 'redirect_to' ) + ->with( "http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/$path" ); - $mock_wcpay_account->maybe_redirect_settings_to_connect(); + $mock_wcpay_account->maybe_redirect_settings_to_connect_or_overview(); } /** - * Data provider for test_maybe_redirect_settings_to_connect + * Data provider for test_maybe_redirect_settings_to_connect_or_overview */ - public function data_maybe_redirect_settings_to_connect() { + public function data_maybe_redirect_settings_to_connect_or_overview() { return [ - 'no_get_params' => [ + 'no_get_params' => [ 0, false, [], ], - 'missing_param' => [ + 'missing_param' => [ 0, false, [ @@ -477,7 +480,7 @@ public function data_maybe_redirect_settings_to_connect() { 'tab' => 'checkout', ], ], - 'incorrect_param' => [ + 'incorrect_param' => [ 0, false, [ @@ -486,16 +489,18 @@ public function data_maybe_redirect_settings_to_connect() { 'section' => 'woocommerce_payments', ], ], - 'account_fully_onboarded' => [ - 0, - true, + 'no_account' => [ + 1, + false, [ 'page' => 'wc-settings', 'tab' => 'checkout', 'section' => 'woocommerce_payments', ], + true, + 'connect', ], - 'happy_path' => [ + 'account_partially_onboarded' => [ 1, false, [ @@ -503,6 +508,17 @@ public function data_maybe_redirect_settings_to_connect() { 'tab' => 'checkout', 'section' => 'woocommerce_payments', ], + false, + 'overview', + ], + 'account_fully_onboarded' => [ + 0, + true, + [ + 'page' => 'wc-settings', + 'tab' => 'checkout', + 'section' => 'woocommerce_payments', + ], ], ]; } diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index 0c7967dd8d6..5176215dc59 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -19,13 +19,12 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase { protected $mock_cache; const FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING = [ - '_wcpay_feature_upe' => 'upe', - '_wcpay_feature_upe_split' => 'upeSplit', - '_wcpay_feature_upe_settings_preview' => 'upeSettingsPreview', - '_wcpay_feature_customer_multi_currency' => 'multiCurrency', - '_wcpay_feature_documents' => 'documents', - '_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled', - '_wcpay_feature_progressive_onboarding' => 'progressiveOnboarding', + '_wcpay_feature_upe' => 'upe', + '_wcpay_feature_upe_settings_preview' => 'upeSettingsPreview', + '_wcpay_feature_customer_multi_currency' => 'multiCurrency', + '_wcpay_feature_documents' => 'documents', + '_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled', + 'is_deferred_intent_creation_upe_enabled' => 'upeDeferred', ]; public function set_up() { @@ -63,7 +62,7 @@ public function test_it_returns_expected_to_array_result( array $enabled_flags ) public function enabled_flags_provider() { return [ - 'no flags' => [ [] ], + 'no flags' => [ [ '_wcpay_feature_upe', 'is_deferred_intent_creation_upe_enabled' ] ], 'all flags' => [ array_keys( self::FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING ) ], ]; } @@ -232,7 +231,7 @@ function ( $pre_option, $option, $default ) { public function test_is_woopay_express_checkout_enabled_returns_false_when_woopay_eligible_is_false() { add_filter( - 'pre_option_' . WC_Payments_Features::PROGRESSIVE_ONBOARDING_FLAG_NAME, + 'pre_option_' . WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, function ( $pre_option, $option, $default ) { return '1'; }, @@ -243,81 +242,6 @@ function ( $pre_option, $option, $default ) { $this->assertFalse( WC_Payments_Features::is_woopay_express_checkout_enabled() ); } - public function test_is_progressive_onboarding_enabled_returns_true() { - add_filter( - 'pre_option_' . WC_Payments_Features::PROGRESSIVE_ONBOARDING_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '1'; - }, - 10, - 3 - ); - $this->assertTrue( WC_Payments_Features::is_progressive_onboarding_enabled() ); - } - - public function test_is_progressive_onboarding_enabled_returns_false_when_flag_is_false() { - add_filter( - 'pre_option_' . WC_Payments_Features::PROGRESSIVE_ONBOARDING_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - $this->assertFalse( WC_Payments_Features::is_progressive_onboarding_enabled() ); - $this->assertArrayNotHasKey( 'progressiveOnboarding', WC_Payments_Features::to_array() ); - } - - public function test_is_progressive_onboarding_enabled_returns_false_when_flag_is_not_set() { - $this->assertFalse( WC_Payments_Features::is_progressive_onboarding_enabled() ); - } - - public function test_split_upe_disabled_with_ineligible_merchant() { - $this->mock_cache->method( 'get' )->willReturn( [ 'capabilities' => [ 'sepa_debit_payments' => 'active' ] ] ); - add_filter( - 'pre_option_' . WC_Payments_Features::UPE_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - add_filter( - 'pre_option_' . WC_Payments_Features::UPE_SPLIT_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - add_filter( - 'pre_option_' . WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - - $this->assertFalse( WC_Payments_Features::is_upe_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_legacy_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_split_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_deferred_intent_enabled() ); - } - - public function test_deferred_upe_enabled_with_sepa() { - $this->mock_cache->method( 'get' )->willReturn( - [ - 'capabilities' => [ 'sepa_debit_payments' => 'active' ], - 'is_deferred_intent_creation_upe_enabled' => true, - ] - ); - - $this->assertTrue( WC_Payments_Features::is_upe_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_legacy_enabled() ); - $this->assertTrue( WC_Payments_Features::is_upe_deferred_intent_enabled() ); - } - public function test_is_wcpay_frt_review_feature_active_returns_true() { add_filter( 'pre_option_wcpay_frt_review_feature_active', diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php index 4f32c569d4e..b0e5dd3fd90 100644 --- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php +++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php @@ -252,7 +252,7 @@ public function test_get_button_settings() { [ 'type' => 'buy', 'theme' => 'dark', - 'height' => '40', + 'height' => '48', 'locale' => 'en', 'branded_type' => 'long', ], diff --git a/tests/unit/test-class-wc-payments-token-service.php b/tests/unit/test-class-wc-payments-token-service.php index 0aa3a4b866f..a5c24e5fb03 100644 --- a/tests/unit/test-class-wc-payments-token-service.php +++ b/tests/unit/test-class-wc-payments-token-service.php @@ -114,7 +114,7 @@ public function test_add_token_to_user_for_sepa() { $token = $this->token_service->add_token_to_user( $mock_payment_method, wp_get_current_user() ); - $this->assertEquals( 'woocommerce_payments', $token->get_gateway_id() ); + $this->assertEquals( 'woocommerce_payments_sepa_debit', $token->get_gateway_id() ); $this->assertEquals( 1, $token->get_user_id() ); $this->assertEquals( 'pm_mock', $token->get_token() ); $this->assertEquals( '3000', $token->get_last4() ); @@ -645,6 +645,26 @@ public function test_woocommerce_get_customer_payment_tokens_payment_methods_onl $this->token_service->woocommerce_get_customer_payment_tokens( $tokens, 1, $gateway_id ); } + /** + * @dataProvider valid_and_invalid_payment_methods_for_comparison_provider + */ + public function test_is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id, $expected_result ) { + $this->assertEquals( + $expected_result, + $this->token_service->is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id ) + ); + } + + public function valid_and_invalid_payment_methods_for_comparison_provider() { + return [ + [ 'card', 'woocommerce_payments', true ], + [ 'sepa_debit', 'woocommerce_payments_sepa_debit', true ], + [ 'link', 'woocommerce_payments', true ], + [ 'card', 'card', false ], + [ 'card', 'woocommerce_payments_bancontact', false ], + ]; + } + private function generate_card_pm_response( $stripe_id ) { return [ 'type' => Payment_Method::CARD, diff --git a/tests/unit/test-class-woopay-tracker.php b/tests/unit/test-class-woopay-tracker.php index 408d9cb56f5..94df55b5e11 100644 --- a/tests/unit/test-class-woopay-tracker.php +++ b/tests/unit/test-class-woopay-tracker.php @@ -43,6 +43,7 @@ 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 ); + WC_Payments::get_gateway()->enable(); $this->mock_account = $this->getMockBuilder( WC_Payments_Account::class ) ->disableOriginalConstructor() diff --git a/tests/unit/woopay/services/test-checkout-service.php b/tests/unit/woopay/services/test-checkout-service.php index 3cd514ab0e2..2fdab51f567 100644 --- a/tests/unit/woopay/services/test-checkout-service.php +++ b/tests/unit/woopay/services/test-checkout-service.php @@ -39,7 +39,7 @@ public function set_up() { $this->checkout_service = new Checkout_Service(); $this->request = new Create_And_Confirm_Intention( $this->createMock( WC_Payments_API_Client::class ), $this->createMock( WC_Payments_Http_Interface::class ) ); - $this->payment_information = new Payment_Information( 'pm_mock', wc_create_order(), Payment_Type::SINGLE(), null ); + $this->payment_information = new Payment_Information( 'pm_mock', wc_create_order(), Payment_Type::SINGLE(), null, null, null, null, '', 'card' ); } public function test_exception_will_throw_if_base_request_parameter_is_invalid() { diff --git a/woocommerce-payments.php b/woocommerce-payments.php index ebd28565578..153fb20b234 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -9,10 +9,10 @@ * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 7.6 - * WC tested up to: 8.2.0 + * WC tested up to: 8.3.1 * Requires at least: 6.0 * Requires PHP: 7.3 - * Version: 6.8.0 + * Version: 6.9.0 * * @package WooCommerce\Payments */