diff --git a/.github/actions/e2e/env-setup/action.yml b/.github/actions/e2e/env-setup/action.yml index f799e16d31a..dabaa752c3e 100644 --- a/.github/actions/e2e/env-setup/action.yml +++ b/.github/actions/e2e/env-setup/action.yml @@ -20,7 +20,7 @@ runs: # Composer setup - name: Setup Composer shell: bash - run: composer self-update 2.0.6 + run: composer self-update # Use node version from .nvmrc - name: Setup NodeJS diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 726df8516cf..832125d04d4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,18 +4,23 @@ updates: - package-ecosystem: 'npm' # Look for `package.json` and `lock` files in the `root` directory directory: '/' - # Check the npm registry for updates every day (weekdays) + # Check for updates once a week schedule: interval: 'weekly' + ignore: + # For @wordpress dependencies, ignore all updates as most of them need to be synced to the min WP version + - dependency-name: "@wordpress/*" + # For @woocommerce dependencies, ignore all updates as most of them need to be synced to the min WC version + - dependency-name: "@woocommerce/*" # Reviewers for issues created reviewers: - - 'Automattic/harmony' + - 'Automattic/harmony' # Enable version updates for composer - package-ecosystem: 'composer' # Look for `package.json` and `lock` files in the `root` directory directory: '/' - # Check the npm registry for updates every day (weekdays) + # Check for updates once a week schedule: interval: 'weekly' # Reviewers for issues created diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml index a922efb9e6f..55f7391fb90 100644 --- a/.github/workflows/check-changelog.yml +++ b/.github/workflows/check-changelog.yml @@ -28,7 +28,7 @@ jobs: tools: composer coverage: none # Install composer packages. - - run: composer self-update 2.0.6 && composer install --no-progress + - run: composer self-update && composer install --no-progress # Fetch the target branch before running the check. - name: Fetch the target origin branch run: git fetch origin $GITHUB_BASE_REF diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index c57281e6a24..5e2ef9bdcb2 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -4,9 +4,9 @@ on: pull_request env: - WC_MIN_SUPPORTED_VERSION: '7.4.1' + WC_MIN_SUPPORTED_VERSION: '7.5.0' WP_MIN_SUPPORTED_VERSION: '6.0' - PHP_MIN_SUPPORTED_VERSION: '7.2' + PHP_MIN_SUPPORTED_VERSION: '7.3' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 440c8d0d95e..1a6ca8e5fdc 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -23,7 +23,7 @@ env: E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }} E2E_USE_LOCAL_SERVER: false E2E_RESULT_FILEPATH: 'tests/e2e/results.json' - WC_MIN_SUPPORTED_VERSION: '7.4.1' + WC_MIN_SUPPORTED_VERSION: '7.5.0' NODE_ENV: 'test' jobs: diff --git a/.github/workflows/php-lint-test.yml b/.github/workflows/php-lint-test.yml index 6abfb1fbbd7..602c7c0755e 100644 --- a/.github/workflows/php-lint-test.yml +++ b/.github/workflows/php-lint-test.yml @@ -6,9 +6,9 @@ on: env: WP_VERSION: latest - WC_MIN_SUPPORTED_VERSION: '7.4.1' + WC_MIN_SUPPORTED_VERSION: '7.5.0' GUTENBERG_VERSION: latest - PHP_MIN_SUPPORTED_VERSION: '7.2' + PHP_MIN_SUPPORTED_VERSION: '7.3' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -33,7 +33,7 @@ jobs: tools: composer coverage: none # install dependencies and run linter - - run: composer self-update 2.0.6 && composer install --no-progress && ./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$) && ./vendor/bin/psalm + - run: composer self-update && composer install --no-progress && ./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$) && ./vendor/bin/psalm generate-test-matrix: name: "Generate the matrix for php tests dynamically" @@ -46,7 +46,7 @@ jobs: run: | PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"7.3\", \"7.4\"]" ) echo "matrix={\"php\":$PHP_VERSIONS}" >> $GITHUB_OUTPUT - + test: name: PHP testing needs: generate-test-matrix diff --git a/assets/images/fraud-protection/discoverability-banner@2x.png b/assets/images/fraud-protection/discoverability-banner@2x.png new file mode 100644 index 00000000000..dc209d571f7 Binary files /dev/null and b/assets/images/fraud-protection/discoverability-banner@2x.png differ diff --git a/assets/images/illustrations/po-eligibility.svg b/assets/images/illustrations/po-eligibility.svg new file mode 100644 index 00000000000..27f0885b30a --- /dev/null +++ b/assets/images/illustrations/po-eligibility.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bin/check-test-coverage.sh b/bin/check-test-coverage.sh new file mode 100755 index 00000000000..e4bf6651134 --- /dev/null +++ b/bin/check-test-coverage.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e + +echo "Installing the test environment..." + +docker-compose exec -u www-data wordpress \ + /var/www/html/wp-content/plugins/woocommerce-payments/bin/install-wp-tests.sh + +echo "Checking coverage..." + +docker-compose exec -u www-data wordpress \ + php -d xdebug.remote_autostart=on \ + /var/www/html/wp-content/plugins/woocommerce-payments/vendor/bin/phpunit \ + --configuration /var/www/html/wp-content/plugins/woocommerce-payments/phpunit.xml.dist \ + --coverage-html /var/www/html/php-test-coverage + $* diff --git a/bin/run-ci-tests-check-coverage.bash b/bin/run-ci-tests-check-coverage.bash index 1050c21b4b5..7c2d5d33c24 100644 --- a/bin/run-ci-tests-check-coverage.bash +++ b/bin/run-ci-tests-check-coverage.bash @@ -7,7 +7,7 @@ IFS=$'\n\t' # set environment variables WCPAY_DIR="$GITHUB_WORKSPACE" -composer self-update 2.0.6 && composer install --no-progress +composer self-update && composer install --no-progress sudo systemctl start mysql.service bash bin/install-wp-tests.sh woocommerce_test root root localhost $WP_VERSION $WC_VERSION false echo 'Running the tests...' diff --git a/bin/run-ci-tests.bash b/bin/run-ci-tests.bash index 659ef0f15df..e7a9a03e340 100644 --- a/bin/run-ci-tests.bash +++ b/bin/run-ci-tests.bash @@ -8,7 +8,7 @@ IFS=$'\n\t' WCPAY_DIR="$GITHUB_WORKSPACE" echo 'Updating composer version & Install dependencies...' -composer self-update 2.0.6 && composer install --no-progress +composer self-update && composer install --no-progress echo 'Starting MySQL service...' sudo systemctl start mysql.service diff --git a/changelog.txt b/changelog.txt index 3385d1e5afa..7eaf6fdec12 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,75 @@ *** WooCommerce Payments Changelog *** += 5.9.0 - 2023-05-17 = +* Add - Adds the minimal functionality for the new Stripe payment flow that allows deferred payment/setup intent creation. The functionality is hidden behind the feature flag. +* Add - Add support for 6 new countries in WCPay: Bulgaria, Croatia, and Romania +* Add - Add support for complete_kyc_link wcpay-link-handler +* Add - Disable the WooPay auto-redirect and SMS OTP modal in unsupported contexts. +* Add - Enhanced fraud protection for your store. Reduce fraudulent transactions by using a set of customizable rules. +* Add - Hide WooPay button in unsupported contexts +* Add - PO fields data and controls, behind a feature flag. +* Add - Support pending verification account status +* Fix - Add fraud prevention token to cart and checkout payment request buttons +* Fix - Check for the `AbstractCartRoute` class before making WooPay available. +* Fix - Fatal error from third-party extensions using the `woocommerce_update_order` expecting the second parameter. +* Fix - Fix AJAX response notice for multi-currency due to empty currencies data +* Fix - Fixed blocks currency switcher widget +* Fix - fixed php 8.1 wp-admin errors +* Fix - Fix keyboard navigation for account balance tooltips on the Payments → Overview screen. +* Fix - Handle WooPay requests using Store API cart token and Jetpack blog token. +* Fix - Minor change for i18n - Remove extra trailing space in translation string, outside of the __ tag. +* Fix - No longer display the Deposits card on the Payments Overview page for new merchants who don't have an estimated deposit +* Fix - Prevent express checkout buttons from displaying when payments are disabled. +* Fix - Prevent WooPay redirection when OTP frame is closed +* Fix - Remove WooPay subscriptions user check endpoint. +* Fix - Supply correct payment method instance to process_redirect_payment. +* Fix - Usage tracking props when placing WooPay orders +* Fix - Use timezone to check WooPay available countries +* Update - Change from convention Platform Checkout to WooPay consistently. +* Update - Handle incorrect address errors in terminal location API +* Update - Refactor express payment method button display +* Update - Remove the `simplifyDepositsUi` feature flag and legacy deposits UI code. +* Update - Show a link to the documentation in the tooltip when the pending balance is negative. +* Update - Update @woocommerce/experimental to v3.2.0 +* Update - Update @wordpress/data-controls to v2.6.1 +* Update - WooPay specific admin area usage tracking +* Dev - Adds HTML coverage report for developer reference. +* Dev - Add the 'wcs_recurring_shipping_package_rates_match_standard_rates' filter to enable third-parties to override whether the subscription packages match during checkout validation. +* Dev - Behind progressive onboarding feature flag – Add PO eligibility modal. +* Dev - Convert fraud protection settings related JavaScript files to TypeScript. +* Dev - Enable third-party code to alter the delete payment token URL returned from flag_subscription_payment_token_deletions. +* Dev - Explicitly mention gridicons and @wordpress/primitives as dev dependencies. +* Dev - Pass the subscription object as the second parameter to `woocommerce_update_subscription` hook (and `woocommerce_update_order` for backwards compatibility). +* Dev - Remove pinned composer version 2.0.6 from workflows +* Dev - Resolve errors for third-party code using the URLs returned from WC_Subscriptions_Admin::add_subscription_url() and WCS_Cart_Renewal::get_checkout_payment_url() because they were erroneously escaped. +* Dev - Return a response from the WC_Subscription::set_status() function in line with the parent WC_Order::set_status() function. +* Dev - Run only pending timers to avoid recursive loop for AddPaymentMethodsTask tests. +* Dev - Update @woocommerce/currency to v 4.2.0 +* Dev - Update @woocommerce/date to v4.2.0 +* Dev - Update @woocommerce/explat to v2.3.0 +* Dev - Update @wordpress/api-fetch to v6.3.1 +* Dev - Update @wordpress/babel-plugin-makepot to v4.3.2 +* Dev - Update @wordpress/base-styles to v4.3.1 +* Dev - Update @wordpress/block-editor to v8.5.10 +* Dev - Update @wordpress/blocks to v11.5.3 +* Dev - Update @wordpress/data to v6.6.1 +* Dev - Update @wordpress/date to v4.5.0 +* Dev - Update @wordpress/element dependency to 4.4.1 +* Dev - Update @wordpress/hooks to v3.6.1 +* Dev - Update @wordpress/html-entities to v3.6.1 +* Dev - Update @wordpress/i18n to v4.6.1 +* Dev - Update @wordpress/icons to v8.2.3 +* Dev - Update @wordpress/jest-preset-default to v8.1.2 +* Dev - Update @wordpress/plugins to v.4.4.3 +* Dev - Update @wordpress/scripts to v19.2.3 +* Dev - Update @wordpress/url to v3.7.1 +* Dev - Update react-dom dependency to 17.0.2 +* Dev - Update react dev dependency to 17.0.2 +* Dev - Update subscriptions-core to 5.7.1 +* Dev - Update version detection API for subscriptions-core +* Dev - Update `@wordpress/dom-ready` to v3.6.1 +* Dev - Usage tracking for deposits admin UI. + = 5.8.1 - 2023-05-03 = * Fix - Fix WooPay express checkout button display issue on Cart blocks. diff --git a/client/account-status-settings/test/__snapshots__/index.js.snap b/client/account-status-settings/test/__snapshots__/index.js.snap index a03b6de5a81..8195f567ae4 100644 --- a/client/account-status-settings/test/__snapshots__/index.js.snap +++ b/client/account-status-settings/test/__snapshots__/index.js.snap @@ -25,7 +25,7 @@ exports[`AccountStatus renders connected account 1`] = ` > @@ -48,7 +48,7 @@ exports[`AccountStatus renders connected account 1`] = ` > @@ -86,7 +86,7 @@ exports[`AccountStatus renders manual deposits 1`] = ` > @@ -109,7 +109,7 @@ exports[`AccountStatus renders manual deposits 1`] = ` > @@ -891,7 +891,7 @@ exports[`AccountStatus renders restricted soon account 1`] = ` > @@ -914,7 +914,7 @@ exports[`AccountStatus renders restricted soon account 1`] = ` > diff --git a/client/additional-methods-setup/upe-preview-methods-selector/test/add-payment-methods-task.test.js b/client/additional-methods-setup/upe-preview-methods-selector/test/add-payment-methods-task.test.js index a8682992e3f..59a515090a1 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/test/add-payment-methods-task.test.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/test/add-payment-methods-task.test.js @@ -240,7 +240,7 @@ describe( 'AddPaymentMethodsTask', () => { jest.useFakeTimers(); act( () => { userEvent.click( screen.getByLabelText( 'Przelewy24 (P24)' ) ); - jest.runAllTimers(); + jest.runOnlyPendingTimers(); } ); expect( screen.getByText( 'Continue' ) ).toBeEnabled(); @@ -312,7 +312,7 @@ describe( 'AddPaymentMethodsTask', () => { const methodsToCheck = [ 'Bancontact', 'giropay' ]; methodsToCheck.forEach( function ( checkboxName ) { userEvent.click( screen.getByLabelText( checkboxName ) ); - jest.runAllTimers(); + jest.runOnlyPendingTimers(); } ); } ); @@ -423,7 +423,7 @@ describe( 'AddPaymentMethodsTask', () => { act( () => { // Enabling a PM with requirements should show the activation modal userEvent.click( cardCheckbox ); - jest.runAllTimers(); + jest.runOnlyPendingTimers(); } ); expect( diff --git a/client/capital/index.tsx b/client/capital/index.tsx index a38629ecfb4..6a715fbdbd7 100644 --- a/client/capital/index.tsx +++ b/client/capital/index.tsx @@ -27,8 +27,8 @@ import './style.scss'; const columns = [ { key: 'paid_out_at', - label: __( 'Dispursed', 'woocommerce-payments' ), - screenReaderLabel: __( 'Dispursed', 'woocommerce-payments' ), + label: __( 'Disbursed', 'woocommerce-payments' ), + screenReaderLabel: __( 'Disbursed', 'woocommerce-payments' ), required: true, isLeftAligned: true, defaultSort: true, diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 5ccb7ba513d..d79ebfcd7de 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -245,7 +245,7 @@ export default class WCPayAPI { 'accountIdForIntentConfirmation' ); - // If this is a setup intent we're not processing a platform checkout payment so we can + // If this is a setup intent we're not processing a woopay payment so we can // use the regular getStripe function. if ( isSetupIntent ) { return this.getStripe().confirmCardSetup( @@ -253,7 +253,7 @@ export default class WCPayAPI { ); } - // For platform checkout we need the capability to switch up the account ID specifically for + // For woopay we need the capability to switch up the account ID specifically for // the intent confirmation step, that's why we create a new instance of the Stripe JS here. if ( accountIdForIntentConfirmation ) { return this.createStripe( @@ -268,7 +268,7 @@ export default class WCPayAPI { ); } - // When not dealing with a setup intent or platform checkout we need to force an account + // When not dealing with a setup intent or woopay we need to force an account // specific request in Stripe. return this.getStripe( true ).confirmCardPayment( decryptClientSecret( clientSecret ) @@ -671,17 +671,14 @@ export default class WCPayAPI { } ); } - initPlatformCheckout( userEmail, platformCheckoutUserSession ) { + initWooPay( userEmail, woopayUserSession ) { const wcAjaxUrl = getConfig( 'wcAjaxUrl' ); - const nonce = getConfig( 'initPlatformCheckoutNonce' ); - return this.request( - buildAjaxURL( wcAjaxUrl, 'init_platform_checkout' ), - { - _wpnonce: nonce, - email: userEmail, - user_session: platformCheckoutUserSession, - } - ); + const nonce = getConfig( 'initWooPayNonce' ); + return this.request( buildAjaxURL( wcAjaxUrl, 'init_woopay' ), { + _wpnonce: nonce, + email: userEmail, + user_session: woopayUserSession, + } ); } expressCheckoutAddToCart( productData ) { diff --git a/client/checkout/api/test/index.test.js b/client/checkout/api/test/index.test.js index 1ae8c25e508..6c1992f6816 100644 --- a/client/checkout/api/test/index.test.js +++ b/client/checkout/api/test/index.test.js @@ -15,12 +15,12 @@ jest.mock( 'wcpay/utils/checkout', () => ( { } ) ); describe( 'WCPayAPI', () => { - test( 'initializes platform checkout using config params', () => { + test( 'initializes woopay using config params', () => { buildAjaxURL.mockReturnValue( 'https://example.org/' ); getConfig.mockReturnValue( 'foo' ); const api = new WCPayAPI( {}, request ); - api.initPlatformCheckout( 'foo@bar.com', 'qwerty123' ); + api.initWooPay( 'foo@bar.com', 'qwerty123' ); expect( request ).toHaveBeenLastCalledWith( 'https://example.org/', { _wpnonce: 'foo', diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 97803a20393..6b506531107 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -21,8 +21,8 @@ import { SavedTokenHandler } from './saved-token-handler'; import request from '../utils/request'; import enqueueFraudScripts from 'fraud-scripts'; import paymentRequestPaymentMethod from '../../payment-request/blocks'; -import { handlePlatformCheckoutEmailInput } from '../platform-checkout/email-input-iframe'; -import wooPayExpressCheckoutPaymentMethod from '../platform-checkout/express-button/woopay-express-checkout-payment-method'; +import { handleWooPayEmailInput } from '../woopay/email-input-iframe'; +import wooPayExpressCheckoutPaymentMethod from '../woopay/express-button/woopay-express-checkout-payment-method'; import { isPreviewing } from '../preview'; // Create an API object, which will be used throughout the checkout. @@ -60,19 +60,19 @@ registerPaymentMethod( { showSavedCards: getConfig( 'isSavedCardsEnabled' ) ?? false, showSaveOption: ( getConfig( 'isSavedCardsEnabled' ) && - ! getConfig( 'isPlatformCheckoutEnabled' ) ) ?? + ! getConfig( 'isWooPayEnabled' ) ) ?? false, features: getConfig( 'features' ), }, } ); -// Call handlePlatformCheckoutEmailInput if platform checkout is enabled and this is the checkout page. -if ( getConfig( 'isPlatformCheckoutEnabled' ) ) { +// Call handleWooPayEmailInput if woopay is enabled and this is the checkout page. +if ( getConfig( 'isWooPayEnabled' ) ) { if ( document.querySelector( '[data-block-name="woocommerce/checkout"]' ) && ! isPreviewing() ) { - handlePlatformCheckoutEmailInput( '#email', api, true ); + handleWooPayEmailInput( '#email', api, true ); } if ( getConfig( 'isWoopayExpressCheckoutEnabled' ) ) { diff --git a/client/checkout/blocks/style.scss b/client/checkout/blocks/style.scss index 9489f880bb7..22f8aaa7dd2 100644 --- a/client/checkout/blocks/style.scss +++ b/client/checkout/blocks/style.scss @@ -7,7 +7,7 @@ } /* stylelint-disable-next-line selector-id-pattern */ -#express-payment-method-platform_checkout { +#express-payment-method-woopay { width: 100%; } @@ -49,5 +49,5 @@ button.wcpay-stripelink-modal-trigger:hover { } } -@import '../platform-checkout/style'; +@import '../woopay/style'; @import '../../components/loadable/style'; diff --git a/client/checkout/classic/index.js b/client/checkout/classic/index.js index b2c7c22ecd5..68452b243a7 100644 --- a/client/checkout/classic/index.js +++ b/client/checkout/classic/index.js @@ -6,7 +6,7 @@ import './style.scss'; import { PAYMENT_METHOD_NAME_CARD } from '../constants.js'; import { getConfig } from 'utils/checkout'; -import { handlePlatformCheckoutEmailInput } from '../platform-checkout/email-input-iframe'; +import { handleWooPayEmailInput } from '../woopay/email-input-iframe'; import WCPayAPI from './../api'; import enqueueFraudScripts from 'fraud-scripts'; import { isWCPayChosen } from '../utils/upe'; @@ -548,7 +548,7 @@ jQuery( function ( $ ) { } } ); - if ( getConfig( 'isPlatformCheckoutEnabled' ) && ! isPreviewing() ) { - handlePlatformCheckoutEmailInput( '#billing_email', api ); + if ( getConfig( 'isWooPayEnabled' ) && ! isPreviewing() ) { + handleWooPayEmailInput( '#billing_email', api ); } } ); diff --git a/client/checkout/classic/test/upe-split.test.js b/client/checkout/classic/test/upe-split.test.js index 8b8f922f7e6..5c38cf14c10 100644 --- a/client/checkout/classic/test/upe-split.test.js +++ b/client/checkout/classic/test/upe-split.test.js @@ -2,11 +2,12 @@ * Internal dependencies */ import * as CheckoutUtils from 'utils/checkout'; +import { getSetupIntentFromSession } from '../upe-split'; + import { - isUsingSavedPaymentMethod, getSelectedUPEGatewayPaymentMethod, - getSetupIntentFromSession, -} from '../upe-split'; + isUsingSavedPaymentMethod, +} from 'wcpay/checkout/utils/upe'; describe( 'UPE split checkout', () => { describe( 'isUsingSavedPaymentMethod', () => { diff --git a/client/checkout/classic/upe-deferred-intent-creation/3ds-flow-handling.js b/client/checkout/classic/upe-deferred-intent-creation/3ds-flow-handling.js new file mode 100644 index 00000000000..fcc229605fb --- /dev/null +++ b/client/checkout/classic/upe-deferred-intent-creation/3ds-flow-handling.js @@ -0,0 +1,72 @@ +/** + * Internal dependencies + */ +import { isWCPayChosen } from 'wcpay/checkout/utils/upe'; +import { getConfig } from 'wcpay/utils/checkout'; +import showErrorCheckout from 'wcpay/checkout/utils/show-error-checkout'; + +const getPaymentMethodId = () => { + return isWCPayChosen() + ? document.querySelector( '#wcpay-payment-method-sepa' )?.value ?? '' + : document.querySelector( '#wcpay-payment-method' )?.value ?? ''; +}; + +export const shouldSavePaymentPaymentMethod = () => { + return ( + document.querySelector( '#wc-woocommerce_payments-new-payment-method' ) + ?.checked ?? false + ); +}; + +const cleanupURL = () => { + // Cleanup the URL. + // https://stackoverflow.com/a/5298684 + history.replaceState( + '', + document.title, + window.location.pathname + window.location.search + ); +}; + +export const showAuthenticationModalIfRequired = ( api ) => { + const url = window.location.href; + const paymentMethodId = getPaymentMethodId(); + + const confirmation = api.confirmIntent( + url, + shouldSavePaymentPaymentMethod() ? paymentMethodId : null + ); + + // Boolean `true` means that there is nothing to confirm. + if ( true === confirmation ) { + return; + } + + const { request } = confirmation; + cleanupURL(); + + request + .then( ( redirectUrl ) => { + window.location = redirectUrl; + } ) + .catch( ( error ) => { + document + .querySelector( 'form.checkout' ) + .classList.remove( 'processing' ); + + const elements = document.getElementsByClassName( 'blockUI' ); + Array.from( elements ).forEach( ( element ) => { + element.parentNode.removeChild( element ); + } ); + + let errorMessage = error.message; + + // If this is a generic error, we probably don't want to display the error message to the user, + // so display a generic message instead. + if ( error instanceof Error ) { + errorMessage = getConfig( 'genericErrorMessage' ); + } + + showErrorCheckout( errorMessage ); + } ); +}; diff --git a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js new file mode 100644 index 00000000000..4ebe3c84852 --- /dev/null +++ b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js @@ -0,0 +1,69 @@ +/* global jQuery */ + +/** + * Internal dependencies + */ +import { getUPEConfig } from 'wcpay/utils/checkout'; +import { + generateCheckoutEventNames, + getSelectedUPEGatewayPaymentMethod, + isUsingSavedPaymentMethod, +} from '../../utils/upe'; +import { + checkout, + mountStripePaymentElement, + renderTerms, +} from './stripe-checkout'; +import enqueueFraudScripts from 'fraud-scripts'; +import { showAuthenticationModalIfRequired } from './3ds-flow-handling'; +import WCPayAPI from 'wcpay/checkout/api'; +import apiRequest from '../../utils/request'; + +jQuery( function ( $ ) { + enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); + const api = new WCPayAPI( + { + publishableKey: getUPEConfig( 'publishableKey' ), + accountId: getUPEConfig( 'accountId' ), + forceNetworkSavedCards: getUPEConfig( 'forceNetworkSavedCards' ), + locale: getUPEConfig( 'locale' ), + }, + apiRequest + ); + showAuthenticationModalIfRequired( api ); + + $( document.body ).on( 'updated_checkout', () => { + if ( + $( '.wcpay-upe-element' ).length && + ! $( '.wcpay-upe-element' ).children().length + ) { + $( '.wcpay-upe-element' ) + .toArray() + .forEach( ( domElement ) => + mountStripePaymentElement( api, domElement ) + ); + } + } ); + + $( 'form.checkout' ).on( generateCheckoutEventNames(), function () { + const paymentMethodType = getSelectedUPEGatewayPaymentMethod(); + if ( ! isUsingSavedPaymentMethod( paymentMethodType ) ) { + return checkout( api, jQuery( this ), paymentMethodType ); + } + } ); + + window.addEventListener( 'hashchange', () => { + if ( window.location.hash.startsWith( '#wcpay-confirm-' ) ) { + showAuthenticationModalIfRequired( api ); + } + } ); + + document.addEventListener( 'change', function ( event ) { + if ( + event.target && + 'wc-woocommerce_payments-new-payment-method' === event.target.id + ) { + renderTerms( event ); + } + } ); +} ); diff --git a/client/checkout/classic/upe-deferred-intent-creation/stripe-checkout.js b/client/checkout/classic/upe-deferred-intent-creation/stripe-checkout.js new file mode 100644 index 00000000000..adf4d74126e --- /dev/null +++ b/client/checkout/classic/upe-deferred-intent-creation/stripe-checkout.js @@ -0,0 +1,242 @@ +/** + * Internal dependencies + */ +import { getUPEConfig } from 'wcpay/utils/checkout'; +import { getAppearance } from '../../upe-styles'; +import showErrorCheckout from 'wcpay/checkout/utils/show-error-checkout'; +import { + appendFingerprintInputToForm, + getFingerprint, +} from 'wcpay/checkout/utils/fingerprint'; +import { + appendPaymentMethodIdToForm, + getSelectedUPEGatewayPaymentMethod, + getTerms, + getUpeSettings, +} from 'wcpay/checkout/utils/upe'; + +const gatewayUPEComponents = {}; +let fingerprint = null; + +for ( const paymentMethodType in getUPEConfig( 'paymentMethodsConfig' ) ) { + gatewayUPEComponents[ paymentMethodType ] = { + elements: null, + upeElement: null, + }; +} + +/** + * Initializes the appearance of the payment element by retrieving the UPE configuration + * from the API and saving the appearance if it doesn't exist. If the appearance already exists, + * it is simply returned. + * + * @param {Object} api The API object used to save the UPE configuration. + * @return {Object} The appearance object for the UPE. + */ +function initializeAppearance( api ) { + let appearance = getUPEConfig( 'upeAppearance' ); + if ( ! appearance ) { + appearance = getAppearance(); + api.saveUPEAppearance( appearance ); + } + return appearance; +} + +/** + * Block UI to indicate processing and avoid duplicate submission. + * + * @param {Object} jQueryForm The jQuery object for the jQueryForm. + */ +function blockUI( jQueryForm ) { + jQueryForm.addClass( 'processing' ).block( { + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6, + }, + } ); +} + +/** + * Validates the Stripe elements by submitting them and handling any errors that occur during submission. + * If an error occurs, the function removes loading effect from the provided jQuery form and thus unblocks it, + * and shows an error message in the checkout. + * + * @param {Object} elements The Stripe elements object to be validated. + * @param {Object} jQueryForm The jQuery object for the form being validated. + */ +function validateElements( elements, jQueryForm ) { + elements.submit().then( ( result ) => { + if ( result.error ) { + jQueryForm.removeClass( 'processing' ).unblock(); + showErrorCheckout( result.error.message ); + } + } ); +} + +/** + * Submits the provided jQuery form and removes the 'processing' class from it. + * + * @param {Object} jQueryForm The jQuery object for the form being submitted. + */ +function submitForm( jQueryForm ) { + jQueryForm.removeClass( 'processing' ).submit(); +} + +/** + * Creates a Stripe payment method by calling the Stripe API's createPaymentMethod with the provided elements + * and billing details. The billing details are obtained from various form elements on the page. + * + * @param {Object} api The API object used to call the Stripe API's createPaymentMethod method. + * @param {Object} elements The Stripe elements object used to create a Stripe payment method. + * @return {Object} A promise that resolves with the created Stripe payment method. + */ +function createStripePaymentMethod( api, elements ) { + return api.getStripe().createPaymentMethod( { + elements, + params: { + billing_details: { + name: document.querySelector( '#billing_first_name' ) + ? ( + document.querySelector( '#billing_first_name' ) + .value + + ' ' + + document.querySelector( '#billing_last_name' ).value + ).trim() + : undefined, + email: document.querySelector( '#billing_email' ).value, + phone: document.querySelector( '#billing_phone' ).value, + address: { + city: document.querySelector( '#billing_city' ).value, + country: document.querySelector( '#billing_country' ).value, + line1: document.querySelector( '#billing_address_1' ).value, + line2: document.querySelector( '#billing_address_2' ).value, + postal_code: document.querySelector( '#billing_postcode' ) + .value, + state: document.querySelector( '#billing_state' ).value, + }, + }, + }, + } ); +} + +/** + * Creates a Stripe payment element with the specified payment method type and options. The function + * retrieves the necessary data from the UPE configuration and initializes the appearance. It then creates the + * Stripe elements and the Stripe payment element, which is attached to the gatewayUPEComponents object afterward. + * + * @param {Object} api The API object used to create the Stripe payment element. + * @param {string} paymentMethodType The type of Stripe payment method to create. + * @return {Object} A promise that resolves with the created Stripe payment element. + */ +async function createStripePaymentElement( api, paymentMethodType ) { + const amount = Number( getUPEConfig( 'cartTotal' ) ); + const options = { + mode: 1 > amount ? 'setup' : 'payment', + currency: getUPEConfig( 'currency' ).toLowerCase(), + amount: amount, + paymentMethodCreation: 'manual', + paymentMethodTypes: [ paymentMethodType ], + appearance: initializeAppearance( api ), + }; + + const elements = api.getStripe().elements( options ); + const createdStripePaymentElement = elements.create( 'payment', { + ...getUpeSettings(), + wallets: { + applePay: 'never', + googlePay: 'never', + }, + } ); + + gatewayUPEComponents[ paymentMethodType ].elements = elements; + gatewayUPEComponents[ + paymentMethodType + ].upeElement = createdStripePaymentElement; + return createdStripePaymentElement; +} + +/** + * Mounts the existing Stripe Payment Element to the DOM element. + * Creates the Stipe Payment Element instance if it doesn't exist and mounts it to the DOM element. + * + * @param {Object} api The API object. + * @param {string} domElement The selector of the DOM element of particular payment method to mount the UPE element to. + **/ +export async function mountStripePaymentElement( api, domElement ) { + try { + if ( ! fingerprint ) { + const { visitorId } = await getFingerprint(); + fingerprint = visitorId; + } + } catch ( error ) { + showErrorCheckout( error.message ); + return; + } + const paymentMethodType = domElement.dataset.paymentMethodType; + const upeElement = + gatewayUPEComponents[ paymentMethodType ].upeElement || + ( await createStripePaymentElement( api, paymentMethodType ) ); + upeElement.mount( domElement ); +} + +/** + * Handles the checkout process for the provided jQuery form and Stripe payment method type. The function blocks the + * form UI to prevent duplicate submission and validates the Stripe elements. It then creates a Stripe payment method + * object and appends the necessary data to the form for checkout completion. Finally, it submits the form and prevents + * the default form submission from WC Core. + * + * @param {Object} api The API object used to create the Stripe payment method. + * @param {Object} jQueryForm The jQuery object for the form being submitted. + * @param {string} paymentMethodType The type of Stripe payment method being used. + * @return {boolean} return false to prevent the default form submission from WC Core. + */ +let hasCheckoutCompleted; +export const checkout = ( api, jQueryForm, paymentMethodType ) => { + if ( hasCheckoutCompleted ) { + hasCheckoutCompleted = false; + return; + } + + blockUI( jQueryForm ); + + const elements = gatewayUPEComponents[ paymentMethodType ].elements; + validateElements( elements, jQueryForm ); + createStripePaymentMethod( api, elements ) + .then( ( paymentMethodObject ) => { + appendFingerprintInputToForm( jQueryForm, fingerprint ); + appendPaymentMethodIdToForm( + jQueryForm, + paymentMethodObject.paymentMethod.id + ); + hasCheckoutCompleted = true; + submitForm( jQueryForm ); + } ) + .catch( ( error ) => { + jQueryForm.removeClass( 'processing' ).unblock(); + showErrorCheckout( error.message ); + } ); + + // Prevent WC Core default form submission (see woocommerce/assets/js/frontend/checkout.js) from happening. + return false; +}; + +/** + * Updates the terms parameter in the Payment Element based on the "save payment information" checkbox. + * + * @param {Event} event The change event that triggers the function. + */ +export function renderTerms( event ) { + const isChecked = event.target.checked; + const value = isChecked ? 'always' : 'never'; + const paymentMethodType = getSelectedUPEGatewayPaymentMethod(); + if ( ! paymentMethodType ) { + return; + } + const upeElement = gatewayUPEComponents[ paymentMethodType ].upeElement; + if ( getUPEConfig( 'isUPEEnabled' ) && upeElement ) { + upeElement.update( { + terms: getTerms( getUPEConfig( 'paymentMethodsConfig' ), value ), + } ); + } +} diff --git a/client/checkout/classic/upe-deferred-intent-creation/test/3ds-flow-handling.test.js b/client/checkout/classic/upe-deferred-intent-creation/test/3ds-flow-handling.test.js new file mode 100644 index 00000000000..9ca751c896c --- /dev/null +++ b/client/checkout/classic/upe-deferred-intent-creation/test/3ds-flow-handling.test.js @@ -0,0 +1,42 @@ +/** + * Internal dependencies + */ +import { showAuthenticationModalIfRequired } from '../3ds-flow-handling'; + +jest.mock( 'wcpay/checkout/utils/upe', () => { + return { + isWCPayChosen: jest.fn( () => true ), + }; +} ); + +describe( 'showAuthenticationModalIfRequired', () => { + it( 'Should stop processing when no confirmation is needed', () => { + const replaceStateSpy = jest.spyOn( history, 'replaceState' ); + const apiMock = { + confirmIntent: jest.fn( () => true ), + }; + + showAuthenticationModalIfRequired( apiMock ); + + expect( apiMock.confirmIntent ).toHaveBeenCalled(); + expect( replaceStateSpy ).not.toHaveBeenCalled(); + } ); + + it( 'Should cleanup the URL when confirmation is needed', async () => { + const cleanupURLSpy = jest.spyOn( history, 'replaceState' ); + const mockedRequest = Promise.resolve( 'https://example.com/checkout' ); + + const apiMock = { + confirmIntent: jest.fn( () => { + return { + request: mockedRequest, + }; + } ), + }; + + showAuthenticationModalIfRequired( apiMock ); + + expect( apiMock.confirmIntent ).toHaveBeenCalled(); + expect( cleanupURLSpy ).toHaveBeenCalled(); + } ); +} ); diff --git a/client/checkout/classic/upe-deferred-intent-creation/test/stripe-checkout.test.js b/client/checkout/classic/upe-deferred-intent-creation/test/stripe-checkout.test.js new file mode 100644 index 00000000000..a6f6b8ed156 --- /dev/null +++ b/client/checkout/classic/upe-deferred-intent-creation/test/stripe-checkout.test.js @@ -0,0 +1,342 @@ +/** + * Internal dependencies + */ +import { + checkout, + mountStripePaymentElement, + renderTerms, +} from '../stripe-checkout'; +import { getAppearance } from '../../../upe-styles'; +import { getUPEConfig } from 'wcpay/utils/checkout'; +import { getFingerprint } from 'wcpay/checkout/utils/fingerprint'; +import showErrorCheckout from 'wcpay/checkout/utils/show-error-checkout'; +import { waitFor } from '@testing-library/react'; +import { getSelectedUPEGatewayPaymentMethod } from 'wcpay/checkout/utils/upe'; + +jest.mock( '../../../upe-styles' ); + +jest.mock( 'wcpay/checkout/utils/upe' ); + +jest.mock( 'wcpay/utils/checkout', () => { + return { + getUPEConfig: jest.fn( ( argument ) => { + if ( 'paymentMethodsConfig' === argument ) { + return { + card: { + label: 'Card', + }, + giropay: { + label: 'Giropay', + }, + ideal: { + label: 'iDEAL', + }, + sepa: { + label: 'SEPA', + }, + }; + } + + if ( 'currency' === argument ) { + return 'eur'; + } + } ), + getConfig: jest.fn(), + }; +} ); + +jest.mock( 'wcpay/checkout/utils/fingerprint', () => { + return { + getFingerprint: jest.fn(), + }; +} ); + +jest.mock( 'wcpay/checkout/utils/show-error-checkout', () => { + return jest.fn(); +} ); + +const mockUpdateFunction = jest.fn(); + +const mockMountFunction = jest.fn(); + +const mockCreateFunction = jest.fn( () => { + return { + mount: mockMountFunction, + update: mockUpdateFunction, + }; +} ); + +const mockSubmit = jest.fn( () => { + return { + then: jest.fn(), + }; +} ); + +const mockElements = jest.fn( () => { + return { + create: mockCreateFunction, + submit: mockSubmit, + }; +} ); + +const mockThen = jest.fn( () => { + return { + catch: jest.fn(), + }; +} ); + +const mockCreatePaymentMethod = jest.fn( () => { + return { + then: mockThen, + }; +} ); + +const mockGetStripe = jest.fn( () => { + return { + elements: mockElements, + createPaymentMethod: mockCreatePaymentMethod, + }; +} ); + +const saveUPEAppearanceMock = jest.fn(); + +const apiMock = { + saveUPEAppearance: saveUPEAppearanceMock, + getStripe: mockGetStripe, +}; + +describe( 'Mount Stripe Payment Element', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + test( 'initializes the appearance when it is not set and saves it', async () => { + const appearanceMock = { backgroundColor: '#fff' }; + getAppearance.mockReturnValue( appearanceMock ); + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'giropay'; + + mountStripePaymentElement( apiMock, mockDomElement ); + + await waitFor( () => { + expect( getAppearance ).toHaveBeenCalled(); + expect( apiMock.saveUPEAppearance ).toHaveBeenCalledWith( + appearanceMock + ); + } ); + } ); + + test( 'does not call getAppearance or saveUPEAppearance if appearance is already set', async () => { + const appearanceMock = { backgroundColor: '#fff' }; + getAppearance.mockReturnValue( appearanceMock ); + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + getUPEConfig.mockImplementation( ( argument ) => { + if ( 'currency' === argument ) { + return 'eur'; + } + + if ( 'upeAppearance' === argument ) { + return { + backgroundColor: '#fff', + }; + } + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'ideal'; + + mountStripePaymentElement( apiMock, mockDomElement ); + + await waitFor( () => { + expect( getAppearance ).not.toHaveBeenCalled(); + expect( apiMock.saveUPEAppearance ).not.toHaveBeenCalled(); + } ); + } ); + + test( 'Prevents from mounting when no figerprint is available', async () => { + getFingerprint.mockImplementation( () => { + throw new Error( 'No fingerprint' ); + } ); + + mountStripePaymentElement( apiMock, null ); + + await waitFor( () => { + expect( showErrorCheckout ).toHaveBeenCalledWith( + 'No fingerprint' + ); + expect( apiMock.getStripe ).not.toHaveBeenCalled(); + expect( mockElements ).not.toHaveBeenCalled(); + expect( mockCreateFunction ).not.toHaveBeenCalled(); + } ); + } ); + + test( 'upeElement is created and mounted when fingerprint is available', async () => { + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'card'; + + mountStripePaymentElement( apiMock, mockDomElement ); + + await waitFor( () => { + expect( apiMock.getStripe ).toHaveBeenCalled(); + expect( mockElements ).toHaveBeenCalled(); + expect( mockCreateFunction ).toHaveBeenCalled(); + expect( mockMountFunction ).toHaveBeenCalled(); + } ); + } ); + + test( 'Terms are rendered for an already mounted element which should be saved', () => { + const event = { + target: { + checked: true, + }, + }; + getUPEConfig.mockImplementation( ( argument ) => { + if ( 'currency' === argument ) { + return 'eur'; + } + + if ( 'isUPEEnabled' === argument ) { + return true; + } + } ); + + getSelectedUPEGatewayPaymentMethod.mockReturnValue( 'card' ); + renderTerms( event ); + expect( getUPEConfig ).toHaveBeenCalledWith( 'isUPEEnabled' ); + expect( mockUpdateFunction ).toHaveBeenCalled(); + } ); + + test( 'Terms are not rendered when no selected payment method is found', () => { + const event = { + target: { + checked: true, + }, + }; + getSelectedUPEGatewayPaymentMethod.mockReturnValue( null ); + renderTerms( event ); + expect( getUPEConfig ).not.toHaveBeenCalledWith( 'isUPEEnabled' ); + expect( mockUpdateFunction ).not.toHaveBeenCalled(); + } ); + + test( 'existing upeElement is not created again but instead mounted immediately when fingerprint is available', async () => { + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'sepa'; + + mountStripePaymentElement( apiMock, mockDomElement ); + mountStripePaymentElement( apiMock, mockDomElement ); + + await waitFor( () => { + expect( apiMock.getStripe ).toHaveBeenCalled(); + expect( mockElements ).toHaveBeenCalledTimes( 1 ); + } ); + } ); +} ); + +describe( 'Checkout', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + test( 'Successful checkout', async () => { + setupBillingDetailsFields(); + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'card'; + + await mountStripePaymentElement( apiMock, mockDomElement ); + + const mockJqueryForm = { + submit: jest.fn(), + addClass: jest.fn( () => { + return { + block: jest.fn(), + }; + } ), + removeClass: jest.fn(), + unblock: jest.fn(), + }; + + const checkoutResult = checkout( apiMock, mockJqueryForm, 'card' ); + + expect( mockJqueryForm.addClass ).toHaveBeenCalledWith( 'processing' ); + expect( mockJqueryForm.removeClass ).not.toHaveBeenCalledWith( + 'processing' + ); + expect( mockSubmit ).toHaveBeenCalled(); + expect( mockCreatePaymentMethod ).toHaveBeenCalled(); + expect( mockThen ).toHaveBeenCalled(); + expect( checkoutResult ).toBe( false ); + } ); + + function setupBillingDetailsFields() { + // Create DOM elements for the test + const firstNameInput = document.createElement( 'input' ); + firstNameInput.id = 'billing_first_name'; + firstNameInput.value = 'John'; + + const lastNameInput = document.createElement( 'input' ); + lastNameInput.id = 'billing_last_name'; + lastNameInput.value = 'Doe'; + + const emailInput = document.createElement( 'input' ); + emailInput.id = 'billing_email'; + emailInput.value = 'john.doe@example.com'; + + const phoneInput = document.createElement( 'input' ); + phoneInput.id = 'billing_phone'; + phoneInput.value = '555-1234'; + + const cityInput = document.createElement( 'input' ); + cityInput.id = 'billing_city'; + cityInput.value = 'New York'; + + const countryInput = document.createElement( 'input' ); + countryInput.id = 'billing_country'; + countryInput.value = 'US'; + + const address1Input = document.createElement( 'input' ); + address1Input.id = 'billing_address_1'; + address1Input.value = '123 Main St'; + + const address2Input = document.createElement( 'input' ); + address2Input.id = 'billing_address_2'; + address2Input.value = ''; + + const postcodeInput = document.createElement( 'input' ); + postcodeInput.id = 'billing_postcode'; + postcodeInput.value = '10001'; + + const stateInput = document.createElement( 'input' ); + stateInput.id = 'billing_state'; + stateInput.value = 'NY'; + + // Add the DOM elements to the document + document.body.appendChild( firstNameInput ); + document.body.appendChild( lastNameInput ); + document.body.appendChild( emailInput ); + document.body.appendChild( phoneInput ); + document.body.appendChild( cityInput ); + document.body.appendChild( countryInput ); + document.body.appendChild( address1Input ); + document.body.appendChild( address2Input ); + document.body.appendChild( postcodeInput ); + document.body.appendChild( stateInput ); + } +} ); diff --git a/client/checkout/classic/upe-split.js b/client/checkout/classic/upe-split.js index cb87c285a87..4af50b72c35 100644 --- a/client/checkout/classic/upe-split.js +++ b/client/checkout/classic/upe-split.js @@ -28,6 +28,9 @@ import { getTerms, isWCPayChosen, getPaymentIntentFromSession, + getSelectedUPEGatewayPaymentMethod, + getUpeSettings, + isUsingSavedPaymentMethod, } from '../utils/upe'; import { decryptClientSecret } from '../utils/encryption'; import enableStripeLinkPaymentMethod from '../stripe-link'; @@ -46,7 +49,6 @@ jQuery( function ( $ ) { const isUPEEnabled = getUPEConfig( 'isUPEEnabled' ); const isUPESplitEnabled = getUPEConfig( 'isUPESplitEnabled' ); const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); - const enabledBillingFields = getUPEConfig( 'enabledBillingFields' ); const isStripeLinkEnabled = paymentMethodsConfig.link !== undefined && paymentMethodsConfig.card !== undefined; @@ -83,40 +85,6 @@ jQuery( function ( $ ) { ); let fingerprint = null; - const hiddenBillingFields = { - name: - enabledBillingFields.includes( 'billing_first_name' ) || - enabledBillingFields.includes( 'billing_last_name' ) - ? 'never' - : 'auto', - email: enabledBillingFields.includes( 'billing_email' ) - ? 'never' - : 'auto', - phone: enabledBillingFields.includes( 'billing_phone' ) - ? 'never' - : 'auto', - address: { - country: enabledBillingFields.includes( 'billing_country' ) - ? 'never' - : 'auto', - line1: enabledBillingFields.includes( 'billing_address_1' ) - ? 'never' - : 'auto', - line2: enabledBillingFields.includes( 'billing_address_2' ) - ? 'never' - : 'auto', - city: enabledBillingFields.includes( 'billing_city' ) - ? 'never' - : 'auto', - state: enabledBillingFields.includes( 'billing_state' ) - ? 'never' - : 'auto', - postalCode: enabledBillingFields.includes( 'billing_postcode' ) - ? 'never' - : 'auto', - }, - }; - /** * Block UI to indicate processing and avoid duplicate submission. * @@ -213,7 +181,6 @@ jQuery( function ( $ ) { // If paying from order, we need to create Payment Intent from order not cart. const isOrderPay = getUPEConfig( 'isOrderPay' ); - const isCheckout = getUPEConfig( 'isCheckout' ); let orderId; if ( isOrderPay ) { orderId = getUPEConfig( 'orderId' ); @@ -315,18 +282,8 @@ jQuery( function ( $ ) { } ); } - const upeSettings = {}; - if ( getUPEConfig( 'cartContainsSubscription' ) ) { - upeSettings.terms = getTerms( paymentMethodsConfig, 'always' ); - } - if ( isCheckout && ! ( isOrderPay || isChangingPayment ) ) { - upeSettings.fields = { - billingDetails: hiddenBillingFields, - }; - } - upeElement = elements.create( 'payment', { - ...upeSettings, + ...getUpeSettings(), wallets: { applePay: 'never', googlePay: 'never', @@ -564,6 +521,7 @@ jQuery( function ( $ ) { obj[ field.name ] = field.value; return obj; }, {} ); + try { const upeComponents = gatewayUPEComponents[ paymentMethodType ]; formFields.wcpay_payment_country = upeComponents.country; @@ -782,71 +740,6 @@ jQuery( function ( $ ) { } ); } ); -/** - * Checks if the customer is using a saved payment method. - * - * @param {string} paymentMethodType Stripe payment method type ID. - * @return {boolean} Boolean indicating whether a saved payment method is being used. - */ -export function isUsingSavedPaymentMethod( paymentMethodType ) { - const prefix = '#wc-woocommerce_payments'; - const suffix = '-payment-token-new'; - const savedPaymentSelector = - 'card' === paymentMethodType - ? prefix + suffix - : prefix + '_' + paymentMethodType + suffix; - - return ( - null !== document.querySelector( savedPaymentSelector ) && - ! document.querySelector( savedPaymentSelector ).checked - ); -} - -/** - * Finds selected payment gateway and returns matching Stripe payment method for gateway. - * - * @return {string} Stripe payment method type - */ -export function getSelectedUPEGatewayPaymentMethod() { - const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); - const gatewayCardId = getUPEConfig( 'gatewayId' ); - let selectedGatewayId = null; - - // Handle payment method selection on the Checkout page or Add Payment Method page where class names differ. - - if ( null !== document.querySelector( 'li.wc_payment_method' ) ) { - selectedGatewayId = document - .querySelector( 'li.wc_payment_method input.input-radio:checked' ) - .getAttribute( 'id' ); - } else if ( - null !== document.querySelector( 'li.woocommerce-PaymentMethod' ) - ) { - selectedGatewayId = document - .querySelector( - 'li.woocommerce-PaymentMethod input.input-radio:checked' - ) - .getAttribute( 'id' ); - } - - if ( 'payment_method_woocommerce_payments' === selectedGatewayId ) { - selectedGatewayId = 'payment_method_woocommerce_payments_card'; - } - - let selectedPaymentMethod = null; - - for ( const paymentMethodType in paymentMethodsConfig ) { - if ( - `payment_method_${ gatewayCardId }_${ paymentMethodType }` === - selectedGatewayId - ) { - selectedPaymentMethod = paymentMethodType; - break; - } - } - - return selectedPaymentMethod; -} - /** * Returns the cached setup intent. * diff --git a/client/checkout/constants.js b/client/checkout/constants.js index 25ff84f2ca4..cd4bd2f942a 100644 --- a/client/checkout/constants.js +++ b/client/checkout/constants.js @@ -13,3 +13,17 @@ export const PAYMENT_METHOD_NAME_PAYMENT_REQUEST = export const PAYMENT_METHOD_NAME_WOOPAY_EXPRESS_CHECKOUT = 'woocommerce_payments_woopay_express_checkout'; export const WC_STORE_CART = 'wc/store/cart'; + +export function getPaymentMethodsConstants() { + return [ + PAYMENT_METHOD_NAME_BANCONTACT, + PAYMENT_METHOD_NAME_BECS, + PAYMENT_METHOD_NAME_EPS, + PAYMENT_METHOD_NAME_GIROPAY, + PAYMENT_METHOD_NAME_IDEAL, + PAYMENT_METHOD_NAME_P24, + PAYMENT_METHOD_NAME_SEPA, + PAYMENT_METHOD_NAME_SOFORT, + PAYMENT_METHOD_NAME_CARD, + ]; +} diff --git a/client/checkout/utils/test/upe.test.js b/client/checkout/utils/test/upe.test.js index 502a07d9281..9e5ff54f56e 100644 --- a/client/checkout/utils/test/upe.test.js +++ b/client/checkout/utils/test/upe.test.js @@ -6,7 +6,15 @@ import { getCookieValue, isWCPayChosen, getPaymentIntentFromSession, + generateCheckoutEventNames, } from '../upe'; +import { getPaymentMethodsConstants } from '../../constants'; + +jest.mock( '../../constants', () => { + return { + getPaymentMethodsConstants: jest.fn(), + }; +} ); describe( 'UPE checkout utils', () => { describe( 'getTerms', () => { @@ -156,4 +164,27 @@ describe( 'UPE checkout utils', () => { ).toEqual( {} ); } ); } ); + + describe( 'generateCheckoutEventNames', () => { + it( 'should return empty string when there are no payment methods', () => { + getPaymentMethodsConstants.mockImplementation( () => [] ); + + const result = generateCheckoutEventNames(); + + expect( result ).toEqual( '' ); + } ); + + it( 'should generate correct event names when there are payment methods', () => { + getPaymentMethodsConstants.mockImplementation( () => [ + 'woocommerce_payments_bancontact', + 'woocommerce_payments_eps', + ] ); + + const result = generateCheckoutEventNames(); + + expect( result ).toEqual( + 'checkout_place_order_woocommerce_payments_bancontact checkout_place_order_woocommerce_payments_eps' + ); + } ); + } ); } ); diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js index e5cba6529ab..c1a0009b19d 100644 --- a/client/checkout/utils/upe.js +++ b/client/checkout/utils/upe.js @@ -1,3 +1,9 @@ +/** + * Internal dependencies + */ +import { getUPEConfig } from 'wcpay/utils/checkout'; +import { getPaymentMethodsConstants } from '../constants'; + /** * Generates terms parameter for UPE, with value set for reusable payment methods * @@ -64,3 +70,132 @@ export const getPaymentIntentFromSession = ( return {}; }; + +/** + * Finds selected payment gateway and returns matching Stripe payment method for gateway. + * + * @return {string} Stripe payment method type + */ +export const getSelectedUPEGatewayPaymentMethod = () => { + const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); + const gatewayCardId = getUPEConfig( 'gatewayId' ); + let selectedGatewayId = null; + + // Handle payment method selection on the Checkout page or Add Payment Method page where class names differ. + const radio = document.querySelector( + 'li.wc_payment_method input.input-radio:checked, li.woocommerce-PaymentMethod input.input-radio:checked' + ); + if ( null !== radio ) { + selectedGatewayId = radio.id; + } + + if ( 'payment_method_woocommerce_payments' === selectedGatewayId ) { + selectedGatewayId = 'payment_method_woocommerce_payments_card'; + } + + let selectedPaymentMethod = null; + + for ( const paymentMethodType in paymentMethodsConfig ) { + if ( + `payment_method_${ gatewayCardId }_${ paymentMethodType }` === + selectedGatewayId + ) { + selectedPaymentMethod = paymentMethodType; + break; + } + } + + return selectedPaymentMethod; +}; + +export const getHiddenBillingFields = ( enabledBillingFields ) => { + return { + name: + enabledBillingFields.includes( 'billing_first_name' ) || + enabledBillingFields.includes( 'billing_last_name' ) + ? 'never' + : 'auto', + email: enabledBillingFields.includes( 'billing_email' ) + ? 'never' + : 'auto', + phone: enabledBillingFields.includes( 'billing_phone' ) + ? 'never' + : 'auto', + address: { + country: enabledBillingFields.includes( 'billing_country' ) + ? 'never' + : 'auto', + line1: enabledBillingFields.includes( 'billing_address_1' ) + ? 'never' + : 'auto', + line2: enabledBillingFields.includes( 'billing_address_2' ) + ? 'never' + : 'auto', + city: enabledBillingFields.includes( 'billing_city' ) + ? 'never' + : 'auto', + state: enabledBillingFields.includes( 'billing_state' ) + ? 'never' + : 'auto', + postalCode: enabledBillingFields.includes( 'billing_postcode' ) + ? 'never' + : 'auto', + }, + }; +}; + +export const getUpeSettings = () => { + const upeSettings = {}; + if ( getUPEConfig( 'cartContainsSubscription' ) ) { + upeSettings.terms = getTerms( + getUPEConfig( 'paymentMethodsConfig' ), + 'always' + ); + } + if ( + getUPEConfig( 'isCheckout' ) && + ! ( + getUPEConfig( 'isOrderPay' ) || getUPEConfig( 'isChangingPayment' ) + ) + ) { + upeSettings.fields = { + billingDetails: getHiddenBillingFields( + getUPEConfig( 'enabledBillingFields' ) + ), + }; + } + + return upeSettings; +}; + +export const generateCheckoutEventNames = () => { + return getPaymentMethodsConstants() + .map( ( method ) => `checkout_place_order_${ method }` ) + .join( ' ' ); +}; + +export const appendPaymentMethodIdToForm = ( form, paymentMethodId ) => { + form.append( + `` + ); +}; + +/** + * Checks if the customer is using a saved payment method. + * + * @param {string} paymentMethodType Stripe payment method type ID. + * @return {boolean} Boolean indicating whether a saved payment method is being used. + */ +export function isUsingSavedPaymentMethod( paymentMethodType ) { + const prefix = '#wc-woocommerce_payments'; + const suffix = '-payment-token-new'; + const savedPaymentSelector = + 'card' === paymentMethodType + ? prefix + suffix + : prefix + '_' + paymentMethodType + suffix; + + return ( + null !== document.querySelector( savedPaymentSelector ) && + ! document.querySelector( savedPaymentSelector ).checked + ); +} diff --git a/client/checkout/platform-checkout/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js similarity index 78% rename from client/checkout/platform-checkout/email-input-iframe.js rename to client/checkout/woopay/email-input-iframe.js index 8d3148fb5e6..b425e857813 100644 --- a/client/checkout/platform-checkout/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -5,27 +5,26 @@ import { __ } from '@wordpress/i18n'; import { getConfig } from 'wcpay/utils/checkout'; import wcpayTracks from 'tracks'; import request from '../utils/request'; -import showErrorCheckout from '../utils/show-error-checkout'; import { buildAjaxURL } from '../../payment-request/utils'; import { getTargetElement, validateEmail } from './utils'; -export const handlePlatformCheckoutEmailInput = async ( +export const handleWooPayEmailInput = async ( field, api, isBlocksCheckout = false ) => { let timer; const waitTime = 500; - const platformCheckoutEmailInput = await getTargetElement( field ); + const woopayEmailInput = await getTargetElement( field ); let hasCheckedLoginSession = false; // If we can't find the input, return. - if ( ! platformCheckoutEmailInput ) { + if ( ! woopayEmailInput ) { return; } const spinner = document.createElement( 'div' ); - const parentDiv = platformCheckoutEmailInput.parentNode; + const parentDiv = woopayEmailInput.parentNode; spinner.classList.add( 'wc-block-components-spinner' ); // Make the login session iframe wrapper. @@ -39,9 +38,7 @@ export const handlePlatformCheckoutEmailInput = async ( 'WooPay Login Session', 'woocommerce-payments' ); - loginSessionIframe.classList.add( - 'platform-checkout-login-session-iframe' - ); + loginSessionIframe.classList.add( 'woopay-login-session-iframe' ); // To prevent twentytwenty.intrinsicRatioVideos from trying to resize the iframe. loginSessionIframe.classList.add( 'intrinsic-ignore' ); @@ -52,12 +49,12 @@ export const handlePlatformCheckoutEmailInput = async ( const iframeWrapper = document.createElement( 'div' ); iframeWrapper.setAttribute( 'role', 'dialog' ); iframeWrapper.setAttribute( 'aria-modal', 'true' ); - iframeWrapper.classList.add( 'platform-checkout-otp-iframe-wrapper' ); + iframeWrapper.classList.add( 'woopay-otp-iframe-wrapper' ); // Make the otp iframe. const iframe = document.createElement( 'iframe' ); iframe.title = __( 'WooPay SMS code verification', 'woocommerce-payments' ); - iframe.classList.add( 'platform-checkout-otp-iframe' ); + iframe.classList.add( 'woopay-otp-iframe' ); // To prevent twentytwenty.intrinsicRatioVideos from trying to resize the iframe. iframe.classList.add( 'intrinsic-ignore' ); @@ -76,7 +73,7 @@ export const handlePlatformCheckoutEmailInput = async ( ( 'undefined' !== typeof performance && 'back_forward' === performance.getEntriesByType( 'navigation' )[ 0 ].type ) || - 'true' === searchParams.get( 'skip_platform_checkout' ); + 'true' === searchParams.get( 'skip_woopay' ); // Track the current state of the header. This default // value should match the default state on the platform. @@ -94,7 +91,7 @@ export const handlePlatformCheckoutEmailInput = async ( action: 'setHeader', value: iframeHeaderValue, }, - getConfig( 'platformCheckoutHost' ) + getConfig( 'woopayHost' ) ); } @@ -136,7 +133,7 @@ export const handlePlatformCheckoutEmailInput = async ( const topOffset = 50; const scrollTop = document.documentElement.scrollTop + - platformCheckoutEmailInput.getBoundingClientRect().top - + woopayEmailInput.getBoundingClientRect().top - iframe.getBoundingClientRect().height / 2 - topOffset; window.scrollTo( { @@ -145,7 +142,7 @@ export const handlePlatformCheckoutEmailInput = async ( } // Get references to the iframe and input field bounding rects. - const anchorRect = platformCheckoutEmailInput.getBoundingClientRect(); + const anchorRect = woopayEmailInput.getBoundingClientRect(); const iframeRect = iframe.getBoundingClientRect(); // Set the iframe top. @@ -192,9 +189,7 @@ export const handlePlatformCheckoutEmailInput = async ( window.addEventListener( 'resize', setPopoverPosition ); iframe.classList.add( 'open' ); - wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OTP_START - ); + wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_OTP_START ); } ); // Add the iframe and iframe arrow to the wrapper. @@ -217,7 +212,7 @@ export const handlePlatformCheckoutEmailInput = async ( iframe.classList.remove( 'open' ); if ( focus ) { - platformCheckoutEmailInput.focus(); + woopayEmailInput.focus(); } document.body.style.overflow = ''; @@ -227,7 +222,7 @@ export const handlePlatformCheckoutEmailInput = async ( const openIframe = ( email ) => { // check and return if another otp iframe is already open. - if ( document.querySelector( '.platform-checkout-otp-iframe' ) ) { + if ( document.querySelector( '.woopay-otp-iframe' ) ) { return; } @@ -250,7 +245,7 @@ export const handlePlatformCheckoutEmailInput = async ( ); iframe.src = `${ getConfig( - 'platformCheckoutHost' + 'woopayHost' ) }/otp/?${ urlParams.toString() }`; // Insert the wrapper into the DOM. @@ -263,10 +258,7 @@ export const handlePlatformCheckoutEmailInput = async ( }; const showErrorMessage = () => { - parentDiv.insertBefore( - errorMessage, - platformCheckoutEmailInput.nextSibling - ); + parentDiv.insertBefore( errorMessage, woopayEmailInput.nextSibling ); }; document.addEventListener( 'keyup', ( event ) => { @@ -275,11 +267,7 @@ export const handlePlatformCheckoutEmailInput = async ( } } ); - // Store if the subscription login error is being shown - // to remove it when change the e-mail address. - let hasPlatformCheckoutSubscriptionLoginError = false; - - // Cancel platform checkout request and close iframe + // Cancel woopay request and close iframe // when user clicks Place Order before it loads. const abortController = new AbortController(); const { signal } = abortController; @@ -305,67 +293,25 @@ export const handlePlatformCheckoutEmailInput = async ( } const dispatchUserExistEvent = ( userExist ) => { - const PlatformCheckoutUserCheckEvent = new CustomEvent( - 'PlatformCheckoutUserCheck', - { - detail: { - isRegisteredUser: userExist, - }, - } - ); - window.dispatchEvent( PlatformCheckoutUserCheckEvent ); + const woopayUserCheckEvent = new CustomEvent( 'woopayUserCheck', { + detail: { + isRegisteredUser: userExist, + }, + } ); + window.dispatchEvent( woopayUserCheckEvent ); }; - const platformCheckoutLocateUser = async ( email ) => { - parentDiv.insertBefore( spinner, platformCheckoutEmailInput ); + const woopayLocateUser = async ( email ) => { + parentDiv.insertBefore( spinner, woopayEmailInput ); if ( parentDiv.contains( errorMessage ) ) { parentDiv.removeChild( errorMessage ); } - if ( hasPlatformCheckoutSubscriptionLoginError ) { - document - .querySelector( '#platform-checkout-subscriptions-login-error' ) - .remove(); - hasPlatformCheckoutSubscriptionLoginError = false; - } - - if ( getConfig( 'platformCheckoutNeedLogin' ) ) { - try { - const userExistsData = await request( - getConfig( 'userExistsEndpoint' ), - { - email, - }, - { signal } - ); - - if ( userExistsData[ 'user-exists' ] ) { - hasPlatformCheckoutSubscriptionLoginError = true; - showErrorCheckout( - userExistsData.message, - false, - false, - 'platform-checkout-subscriptions-login-error' - ); - spinner.remove(); - return; - } - } catch ( err ) { - if ( 'AbortError' !== err.name ) { - showErrorMessage(); - spinner.remove(); - } - } - } - request( - buildAjaxURL( - getConfig( 'wcAjaxUrl' ), - 'get_platform_checkout_signature' - ), + buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_signature' ), { - _ajax_nonce: getConfig( 'platformCheckoutSignatureNonce' ), + _ajax_nonce: getConfig( 'woopaySignatureNonce' ), } ) .then( ( response ) => { @@ -395,13 +341,13 @@ export const handlePlatformCheckoutEmailInput = async ( ); emailExistsQuery.append( 'blog_id', - getConfig( 'platformCheckoutMerchantId' ) + getConfig( 'woopayMerchantId' ) ); emailExistsQuery.append( 'request_signature', signature ); return fetch( `${ getConfig( - 'platformCheckoutHost' + 'woopayHost' ) }/wp-json/platform-checkout/v1/user/exists?${ emailExistsQuery.toString() }`, { signal, @@ -423,7 +369,7 @@ export const handlePlatformCheckoutEmailInput = async ( openIframe( email ); } else if ( 'rest_invalid_param' !== data.code ) { wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OFFERED + wcpayTracks.events.WOOPAY_OFFERED ); } } ) @@ -443,13 +389,13 @@ export const handlePlatformCheckoutEmailInput = async ( const closeLoginSessionIframe = () => { loginSessionIframeWrapper.remove(); loginSessionIframe.classList.remove( 'open' ); - platformCheckoutEmailInput.focus( { + woopayEmailInput.focus( { preventScroll: true, } ); // Check the initial value of the email input and trigger input validation. - if ( validateEmail( platformCheckoutEmailInput.value ) ) { - platformCheckoutLocateUser( platformCheckoutEmailInput.value ); + if ( validateEmail( woopayEmailInput.value ) ) { + woopayLocateUser( woopayEmailInput.value ); } }; @@ -457,13 +403,13 @@ export const handlePlatformCheckoutEmailInput = async ( const emailParam = new URLSearchParams(); if ( validateEmail( email ) ) { - parentDiv.insertBefore( spinner, platformCheckoutEmailInput ); + parentDiv.insertBefore( spinner, woopayEmailInput ); emailParam.append( 'email', email ); emailParam.append( 'test_mode', !! getConfig( 'testMode' ) ); } loginSessionIframe.src = `${ getConfig( - 'platformCheckoutHost' + 'woopayHost' ) }/login-session?${ emailParam.toString() }`; // Insert the wrapper into the DOM. @@ -481,10 +427,10 @@ export const handlePlatformCheckoutEmailInput = async ( }, 15000 ); }; - platformCheckoutEmailInput.addEventListener( 'input', ( e ) => { + woopayEmailInput.addEventListener( 'input', ( e ) => { if ( ! hasCheckedLoginSession && ! customerClickedBackButton ) { if ( customerClickedBackButton ) { - openLoginSessionIframe( platformCheckoutEmailInput.value ); + openLoginSessionIframe( woopayEmailInput.value ); } return; @@ -497,34 +443,39 @@ export const handlePlatformCheckoutEmailInput = async ( timer = setTimeout( () => { if ( validateEmail( email ) ) { - platformCheckoutLocateUser( email ); + woopayLocateUser( email ); } }, waitTime ); } ); window.addEventListener( 'message', ( e ) => { - if ( ! getConfig( 'platformCheckoutHost' ).startsWith( e.origin ) ) { + if ( ! getConfig( 'woopayHost' ).startsWith( e.origin ) ) { return; } switch ( e.data.action ) { case 'auto_redirect_to_platform_checkout': + case 'auto_redirect_to_woopay': hasCheckedLoginSession = true; - api.initPlatformCheckout( - '', - e.data.platformCheckoutUserSession - ) + api.initWooPay( '', e.data.platformCheckoutUserSession ) .then( ( response ) => { if ( 'success' === response.result ) { loginSessionIframeWrapper.classList.add( - 'platform-checkout-login-session-iframe-wrapper' + 'woopay-login-session-iframe-wrapper' ); loginSessionIframe.classList.add( 'open' ); wcpayTracks.recordUserEvent( - wcpayTracks.events - .PLATFORM_CHECKOUT_AUTO_REDIRECT + wcpayTracks.events.WOOPAY_AUTO_REDIRECT ); spinner.remove(); + // Do nothing if the iframe has been closed. + if ( + ! document.querySelector( + '.woopay-login-session-iframe' + ) + ) { + return; + } window.location = response.url; } else { closeLoginSessionIframe(); @@ -547,14 +498,21 @@ export const handlePlatformCheckoutEmailInput = async ( closeLoginSessionIframe(); break; case 'redirect_to_platform_checkout': + case 'redirect_to_woopay': wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OTP_COMPLETE + wcpayTracks.events.WOOPAY_OTP_COMPLETE ); - api.initPlatformCheckout( - platformCheckoutEmailInput.value, + api.initWooPay( + woopayEmailInput.value, e.data.platformCheckoutUserSession ) .then( ( response ) => { + // Do nothing if the iframe has been closed. + if ( + ! document.querySelector( '.woopay-otp-iframe' ) + ) { + return; + } if ( 'success' === response.result ) { window.location = response.url; } else { @@ -569,7 +527,7 @@ export const handlePlatformCheckoutEmailInput = async ( break; case 'otp_validation_failed': wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OTP_FAILED + wcpayTracks.events.WOOPAY_OTP_FAILED ); break; case 'close_modal': @@ -578,11 +536,11 @@ export const handlePlatformCheckoutEmailInput = async ( case 'iframe_height': if ( 300 < e.data.height ) { if ( fullScreenModalBreakpoint <= window.innerWidth ) { - // attach iframe to right side of platformCheckoutEmailInput. + // attach iframe to right side of woopayEmailInput. iframe.style.height = e.data.height + 'px'; - const inputRect = platformCheckoutEmailInput.getBoundingClientRect(); + const inputRect = woopayEmailInput.getBoundingClientRect(); // iframe top is the input top minus the iframe height. iframe.style.top = @@ -620,7 +578,7 @@ export const handlePlatformCheckoutEmailInput = async ( if ( ! customerClickedBackButton ) { // Check if user already has a WooPay login session. if ( ! hasCheckedLoginSession ) { - openLoginSessionIframe( platformCheckoutEmailInput.value ); + openLoginSessionIframe( woopayEmailInput.value ); } } else { // Dispatch an event declaring this user exists as returned via back button. Wait for the window to load. @@ -628,11 +586,9 @@ export const handlePlatformCheckoutEmailInput = async ( dispatchUserExistEvent( true ); }, 2000 ); - wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_SKIPPED - ); + wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_SKIPPED ); - searchParams.delete( 'skip_platform_checkout' ); + searchParams.delete( 'skip_woopay' ); let { pathname } = window.location; diff --git a/client/checkout/platform-checkout/express-button/express-checkout-iframe.js b/client/checkout/woopay/express-button/express-checkout-iframe.js similarity index 90% rename from client/checkout/platform-checkout/express-button/express-checkout-iframe.js rename to client/checkout/woopay/express-button/express-checkout-iframe.js index 40a9500b5d4..3fadd23ab0c 100644 --- a/client/checkout/platform-checkout/express-button/express-checkout-iframe.js +++ b/client/checkout/woopay/express-button/express-checkout-iframe.js @@ -7,7 +7,7 @@ import { getTargetElement, validateEmail } from '../utils'; import wcpayTracks from 'tracks'; export const expressCheckoutIframe = async ( api, context, emailSelector ) => { - const platformCheckoutEmailInput = await getTargetElement( emailSelector ); + const woopayEmailInput = await getTargetElement( emailSelector ); let userEmail = ''; const parentDiv = document.body; @@ -16,12 +16,12 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { const iframeWrapper = document.createElement( 'div' ); iframeWrapper.setAttribute( 'role', 'dialog' ); iframeWrapper.setAttribute( 'aria-modal', 'true' ); - iframeWrapper.classList.add( 'platform-checkout-otp-iframe-wrapper' ); + iframeWrapper.classList.add( 'woopay-otp-iframe-wrapper' ); // Make the otp iframe. const iframe = document.createElement( 'iframe' ); iframe.title = __( 'WooPay SMS code verification', 'woocommerce-payments' ); - iframe.classList.add( 'platform-checkout-otp-iframe' ); + iframe.classList.add( 'woopay-otp-iframe' ); // To prevent twentytwenty.intrinsicRatioVideos from trying to resize the iframe. iframe.classList.add( 'intrinsic-ignore' ); @@ -45,7 +45,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { action: 'setHeader', value: iframeHeaderValue, }, - getConfig( 'platformCheckoutHost' ) + getConfig( 'woopayHost' ) ); } @@ -93,9 +93,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { window.addEventListener( 'resize', setPopoverPosition ); iframe.classList.add( 'open' ); - wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OTP_START - ); + wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_OTP_START ); } ); // Add the iframe to the wrapper. @@ -122,7 +120,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { method: 'POST', body: new URLSearchParams( { action: 'woopay_express_checkout_button_show_error_notice', - _ajax_nonce: getConfig( 'platformCheckoutButtonNonce' ), + _ajax_nonce: getConfig( 'woopayButtonNonce' ), context, message: errorMessage, } ), @@ -161,7 +159,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { const openIframe = ( email = '' ) => { // check and return if another otp iframe is already open. - if ( document.querySelector( '.platform-checkout-otp-iframe' ) ) { + if ( document.querySelector( '.woopay-otp-iframe' ) ) { return; } @@ -190,7 +188,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { ); iframe.src = `${ getConfig( - 'platformCheckoutHost' + 'woopayHost' ) }/otp/?${ urlParams.toString() }`; // Insert the wrapper into the DOM. @@ -209,7 +207,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { } ); window.addEventListener( 'message', ( e ) => { - if ( ! getConfig( 'platformCheckoutHost' ).startsWith( e.origin ) ) { + if ( ! getConfig( 'woopayHost' ).startsWith( e.origin ) ) { return; } @@ -218,13 +216,18 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { userEmail = e.data.userEmail; break; case 'redirect_to_platform_checkout': + case 'redirect_to_woopay': wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OTP_COMPLETE + wcpayTracks.events.WOOPAY_OTP_COMPLETE ); - api.initPlatformCheckout( + api.initWooPay( userEmail, e.data.platformCheckoutUserSession ).then( ( response ) => { + // Do nothing if the iframe has been closed. + if ( ! document.querySelector( '.woopay-otp-iframe' ) ) { + return; + } if ( 'success' === response.result ) { window.location = response.url; } else { @@ -235,7 +238,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { break; case 'otp_validation_failed': wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_OTP_FAILED + wcpayTracks.events.WOOPAY_OTP_FAILED ); break; case 'close_modal': @@ -270,5 +273,5 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { } } ); - openIframe( platformCheckoutEmailInput?.value ); + openIframe( woopayEmailInput?.value ); }; diff --git a/client/checkout/platform-checkout/express-button/index.js b/client/checkout/woopay/express-button/index.js similarity index 64% rename from client/checkout/platform-checkout/express-button/index.js rename to client/checkout/woopay/express-button/index.js index f44bed7ae04..130e8b2b75e 100644 --- a/client/checkout/platform-checkout/express-button/index.js +++ b/client/checkout/woopay/express-button/index.js @@ -12,7 +12,7 @@ import { WoopayExpressCheckoutButton } from './woopay-express-checkout-button'; import WCPayAPI from '../../api'; import request from '../../utils/request'; -const renderPlatformCheckoutExpressButton = () => { +const renderWooPayExpressCheckoutButton = () => { // Create an API object, which will be used throughout the checkout. const api = new WCPayAPI( { @@ -24,31 +24,27 @@ const renderPlatformCheckoutExpressButton = () => { request ); - const platformCheckoutContainer = document.getElementById( - 'wcpay-platform-checkout-button' - ); + const woopayContainer = document.getElementById( 'wcpay-woopay-button' ); - if ( platformCheckoutContainer ) { + if ( woopayContainer ) { ReactDOM.render( , - platformCheckoutContainer + woopayContainer ); } }; -window.addEventListener( 'load', renderPlatformCheckoutExpressButton ); +window.addEventListener( 'load', renderWooPayExpressCheckoutButton ); jQuery( ( $ ) => { $( document.body ).on( 'updated_cart_totals', () => { - renderPlatformCheckoutExpressButton(); + renderWooPayExpressCheckoutButton(); } ); } ); diff --git a/client/checkout/platform-checkout/express-button/test/express-checkout-iframe.test.js b/client/checkout/woopay/express-button/test/express-checkout-iframe.test.js similarity index 89% rename from client/checkout/platform-checkout/express-button/test/express-checkout-iframe.test.js rename to client/checkout/woopay/express-button/test/express-checkout-iframe.test.js index b2841af1c55..9b035c3acb6 100644 --- a/client/checkout/platform-checkout/express-button/test/express-checkout-iframe.test.js +++ b/client/checkout/woopay/express-button/test/express-checkout-iframe.test.js @@ -27,9 +27,7 @@ describe( 'expressCheckoutIframe', () => { await waitFor( () => { const woopayIframe = document.querySelector( 'iframe' ); - expect( woopayIframe.className ).toContain( - 'platform-checkout-otp-iframe' - ); + expect( woopayIframe.className ).toContain( 'woopay-otp-iframe' ); expect( woopayIframe.src ).toContain( 'http://example.com/otp/' ); } ); } ); diff --git a/client/checkout/platform-checkout/express-button/test/index.test.js b/client/checkout/woopay/express-button/test/index.test.js similarity index 89% rename from client/checkout/platform-checkout/express-button/test/index.test.js rename to client/checkout/woopay/express-button/test/index.test.js index 50c08b8114c..b11d94bf1a7 100644 --- a/client/checkout/platform-checkout/express-button/test/index.test.js +++ b/client/checkout/woopay/express-button/test/index.test.js @@ -20,13 +20,10 @@ jest.mock( '../woopay-express-checkout-button', () => ( { }, } ) ); -describe( 'renderPlatformCheckoutExpressButton', () => { +describe( 'renderWooPayExpressButton', () => { // placeholder to attach react component. const expressButtonContainer = document.createElement( 'div' ); - expressButtonContainer.setAttribute( - 'id', - 'wcpay-platform-checkout-button' - ); + expressButtonContainer.setAttribute( 'id', 'wcpay-woopay-button' ); beforeEach( () => { getConfig.mockReturnValue( 'foo' ); diff --git a/client/checkout/platform-checkout/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js similarity index 98% rename from client/checkout/platform-checkout/express-button/test/woopay-express-checkout-button.test.js rename to client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js index 21c7401c9fe..3c4a0a651fc 100644 --- a/client/checkout/platform-checkout/express-button/test/woopay-express-checkout-button.test.js +++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js @@ -45,8 +45,7 @@ describe( 'WoopayExpressCheckoutButton', () => { getConfig.mockReturnValue( 'foo' ); wcpayTracks.recordUserEvent.mockReturnValue( true ); wcpayTracks.events = { - PLATFORM_CHECKOUT_EXPRESS_BUTTON_OFFERED: - 'platform_checkout_express_button_offered', + WOOPAY_EXPRESS_BUTTON_OFFERED: 'woopay_express_button_offered', }; useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, diff --git a/client/checkout/platform-checkout/express-button/use-express-checkout-product-handler.js b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js similarity index 100% rename from client/checkout/platform-checkout/express-button/use-express-checkout-product-handler.js rename to client/checkout/woopay/express-button/use-express-checkout-product-handler.js diff --git a/client/checkout/platform-checkout/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js similarity index 90% rename from client/checkout/platform-checkout/express-button/woopay-express-checkout-button.js rename to client/checkout/woopay/express-button/woopay-express-checkout-button.js index 49c11aff328..b7f2d944bd7 100644 --- a/client/checkout/platform-checkout/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -39,7 +39,7 @@ export const WoopayExpressCheckoutButton = ( { useEffect( () => { if ( ! isPreview ) { wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_EXPRESS_BUTTON_OFFERED, + wcpayTracks.events.WOOPAY_EXPRESS_BUTTON_OFFERED, { context, } @@ -47,7 +47,7 @@ export const WoopayExpressCheckoutButton = ( { } }, [ isPreview, context ] ); - const initPlatformCheckout = ( e ) => { + const initWooPay = ( e ) => { e.preventDefault(); if ( isPreview ) { @@ -55,7 +55,7 @@ export const WoopayExpressCheckoutButton = ( { } wcpayTracks.recordUserEvent( - wcpayTracks.events.PLATFORM_CHECKOUT_EXPRESS_BUTTON_CLICKED, + wcpayTracks.events.WOOPAY_EXPRESS_BUTTON_CLICKED, { context: context, } @@ -78,7 +78,7 @@ export const WoopayExpressCheckoutButton = ( { + +`; + exports[`StatusChip renders rejected status 1`] = `
{ expect( statusChip ).toMatchSnapshot(); } ); + test( 'renders pending verification status', () => { + const { container: statusChip } = renderStatusChip( + 'pending_verification' + ); + expect( statusChip ).toMatchSnapshot(); + } ); + test( 'renders unknown status', () => { const { container: statusChip } = renderStatusChip( 'foobar' ); expect( statusChip ).toMatchSnapshot(); diff --git a/client/components/amount-input/index.js b/client/components/amount-input/index.js deleted file mode 100644 index 2bcf32c38b2..00000000000 --- a/client/components/amount-input/index.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * External dependencies - */ -import React, { useCallback, useEffect } from 'react'; -import './style.scss'; - -const AmountInput = ( { - id, - prefix, - value, - placeholder, - help, - onChange = () => {}, -} ) => { - const validateInput = useCallback( ( subject ) => { - // Only allow decimals, a single dot, and more decimals (or an empty value). - return /^(\d+\.?\d*)?$/m.test( subject ); - }, [] ); - - useEffect( () => { - if ( ! validateInput( value ) ) { - onChange( '' ); - } - }, [ validateInput, value, onChange ] ); - - if ( isNaN( value ) || null === value ) value = ''; - - const handleChange = ( inputvalue ) => { - if ( validateInput( inputvalue ) ) { - onChange( inputvalue ); - } - }; - - return ( -
-
- { prefix && ( - - { prefix } - - ) } - handleChange( e.target.value ) } - className="components-text-control__input components-amount-input__input" - /> -
- { help } -
- ); -}; - -export default AmountInput; diff --git a/client/components/amount-input/index.tsx b/client/components/amount-input/index.tsx new file mode 100644 index 00000000000..62b6f312439 --- /dev/null +++ b/client/components/amount-input/index.tsx @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import React, { useCallback, useEffect } from 'react'; +import './style.scss'; + +interface AmountInputProps { + id?: string; + prefix?: string; + value: string; + placeholder?: string; + help?: string; + onChange?: ( value: string ) => void; +} + +const AmountInput: React.FC< AmountInputProps > = ( { + id, + prefix, + value, + placeholder, + help, + onChange = () => null, +} ) => { + // Only allow digits, a single dot, and more digits (or an empty value). + const validateInput = useCallback( + ( subject ) => /^(\d+\.?\d*)?$/m.test( subject ), + [] + ); + + const validatedValue = validateInput( value ) ? value : ''; + + const [ internalValue, setInternalValue ] = React.useState( + validatedValue + ); + + useEffect( () => { + if ( ! validateInput( internalValue ) ) { + onChange( '' ); + } + }, [ validateInput, internalValue, onChange ] ); + + if ( isNaN( Number( value ) ) || null === value || '0' === value ) + value = ''; + + const handleChange = ( inputValue: string ) => { + if ( validateInput( inputValue ) ) { + setInternalValue( inputValue ); + onChange( inputValue ); + } + }; + + return ( +
+
+ { prefix && ( + + { prefix } + + ) } + handleChange( e.target.value ) } + className="components-text-control__input components-amount-input__input" + /> +
+ { help } +
+ ); +}; + +export default AmountInput; diff --git a/client/components/amount-input/test/__snapshots__/index.test.js.snap b/client/components/amount-input/test/__snapshots__/index.test.tsx.snap similarity index 100% rename from client/components/amount-input/test/__snapshots__/index.test.js.snap rename to client/components/amount-input/test/__snapshots__/index.test.tsx.snap diff --git a/client/components/amount-input/test/index.test.js b/client/components/amount-input/test/index.test.tsx similarity index 57% rename from client/components/amount-input/test/index.test.js rename to client/components/amount-input/test/index.test.tsx index 941d297de04..668b1d0ed11 100644 --- a/client/components/amount-input/test/index.test.js +++ b/client/components/amount-input/test/index.test.tsx @@ -10,7 +10,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import AmountInput from '..'; describe( 'Amount input', () => { - let mockValue; + let mockValue: string; const mockOnChangeEvent = jest.fn(); beforeEach( () => { @@ -18,15 +18,17 @@ describe( 'Amount input', () => { mockOnChangeEvent.mockImplementation( ( value ) => { mockValue = value; } ); - mockValue = null; + mockValue = ''; } ); - const writeKeyByKey = ( input, text ) => { - for ( const i in text ) { + const writeKeyByKey = ( input: HTMLInputElement, text: string ) => { + const characters = text.split( '' ); + + characters.forEach( ( character: string ) => { fireEvent.change( input, { - target: { value: ( mockValue ? mockValue : '' ) + text[ i ] }, + target: { value: ( mockValue ? mockValue : '' ) + character }, } ); - } + } ); }; test( 'renders correctly', () => { @@ -34,7 +36,7 @@ describe( 'Amount input', () => { { ); expect( container ).toMatchSnapshot(); } ); - test( 'sets the id of the input', () => { - render( ); - const testInput = screen.queryByTestId( 'amount-input' ); + test( 'sets the id of the input', async () => { + render( ); + const testInput = await screen.findByTestId( 'amount-input' ); expect( testInput ).toBeInTheDocument(); expect( testInput.id ).toBe( 'test_id' ); } ); - test( 'sets the placeholder of the input', () => { - render( ); - const testInput = screen.queryByTestId( 'amount-input' ); + test( 'sets the placeholder of the input', async () => { + render( ); + const testInput = ( await screen.findByTestId( + 'amount-input' + ) ) as HTMLInputElement; expect( testInput ).toBeInTheDocument(); expect( testInput.placeholder ).toBe( 'test_id' ); } ); - test( 'sets the value of the input', () => { + test( 'sets the value of the input', async () => { render( ); - const testInput = screen.queryByTestId( 'amount-input' ); + const testInput = await screen.findByTestId( 'amount-input' ); expect( testInput ).toBeInTheDocument(); expect( testInput ).toHaveValue( '1234' ); } ); - test( 'sets a float value to the input', () => { + test( 'sets a float value to the input', async () => { render( ); - const testInput = screen.queryByTestId( 'amount-input' ); + const testInput = ( await screen.findByTestId( + 'amount-input' + ) ) as HTMLInputElement; const testContent = '123.45'; writeKeyByKey( testInput, testContent ); expect( mockValue ).toBe( '123.45' ); } ); - test( 'doesn`t set a non-float value to the input', () => { + test( 'doesn`t set a non-float value to the input', async () => { render( ); - const testInput = screen.queryByTestId( 'amount-input' ); + const testInput = ( await screen.findByTestId( + 'amount-input' + ) ) as HTMLInputElement; const testContent = 'a.123.123.323.v+'; writeKeyByKey( testInput, testContent ); expect( mockValue ).toBe( '123.123323' ); diff --git a/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap b/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap index 54f576735dc..79566321c61 100644 --- a/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap +++ b/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap @@ -26,9 +26,10 @@ exports[`Info BannerNotices renders with dismiss 1`] = ` + { message } + + + ); + } return { message }; }; diff --git a/client/components/chip/test/__snapshots__/index.js.snap b/client/components/chip/test/__snapshots__/index.js.snap index b15de338fb1..d59afdcf01d 100755 --- a/client/components/chip/test/__snapshots__/index.js.snap +++ b/client/components/chip/test/__snapshots__/index.js.snap @@ -1,5 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Chip renders a chip with a tooltip 1`] = ` +
+ +
+`; + exports[`Chip renders a light chip 1`] = `
{ expect( chip ).toMatchSnapshot(); } ); + test( 'renders a chip with a tooltip', () => { + const { container: chip } = renderChip( + 'light', + 'Light message', + 'Tooltip' + ); + expect( chip ).toMatchSnapshot(); + } ); + test( 'renders a primary chip by default', () => { const { container: chip } = renderChip( undefined, 'Message' ); expect( chip ).toMatchSnapshot(); @@ -40,7 +49,9 @@ describe( 'Chip', () => { expect( chip ).toMatchSnapshot(); } ); - function renderChip( type, message ) { - return render( ); + function renderChip( type, message, tooltip ) { + return render( + + ); } } ); diff --git a/client/components/clickable-cell/index.js b/client/components/clickable-cell/index.js index 4a4bf348228..2c9c6dfc61c 100644 --- a/client/components/clickable-cell/index.js +++ b/client/components/clickable-cell/index.js @@ -10,12 +10,13 @@ import './style.scss'; import { Link } from '@woocommerce/components'; -const ClickableCell = ( { href, children } ) => +const ClickableCell = ( { href, children, ...linkProps } ) => href ? ( { children } diff --git a/client/components/custom-select-control/index.tsx b/client/components/custom-select-control/index.tsx index cebdd0c0918..07877f31486 100644 --- a/client/components/custom-select-control/index.tsx +++ b/client/components/custom-select-control/index.tsx @@ -170,7 +170,9 @@ function CustomSelectControl< ItemType extends Item >( { ), } ) } > - { itemString || placeholder } + + { itemString || placeholder } + - Pineapple + + Pineapple + @@ -110,13 +112,16 @@ exports[`CustomSelectControl renders options with custom children 1`] = ` id="downshift-2-toggle-button" type="button" > - Pineapple + + Pineapple + @@ -214,13 +218,16 @@ exports[`CustomSelectControl renders with placeholder 1`] = ` id="downshift-4-toggle-button" type="button" > - Which fruit do you like best? + + Which fruit do you like best? +
- { title } -
-
- { value } -
-
- { children } -
- - ); -}; - -export default DepositsInformationBlock; diff --git a/client/components/deposits-information/index.tsx b/client/components/deposits-information/index.tsx deleted file mode 100644 index 55e0401e686..00000000000 --- a/client/components/deposits-information/index.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { Card, CardHeader, Flex } from '@wordpress/components'; -import { Link } from '@woocommerce/components'; -import CalendarIcon from 'gridicons/dist/calendar'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import DepositsInformationLoading from './loading'; -import { getDetailsURL } from 'components/details-link'; -import { - getBalanceDepositCount, - getDepositScheduleDescriptor, - getDepositDate, - getNextDepositLabelFormatted, -} from 'deposits/utils'; -import InstantDepositButton from 'deposits/instant-deposits'; -import DepositsInformationBlock from './block'; -import { formatCurrency, formatCurrencyName } from 'utils/currency'; -import { useAllDepositsOverviews } from 'wcpay/data'; - -import './style.scss'; - -interface OverviewProps { - overview: AccountOverview.Overview; - account: AccountOverview.Account; -} - -/** - * Renders a deposits overview - * - * @param {OverviewProps} props Deposits overview and account. - * @return {JSX.Element} Rendered element with deposits overview. - */ -const DepositsInformationOverview: React.FunctionComponent< OverviewProps > = ( - props -) => { - const { overview, account }: OverviewProps = props; - const { - currency, - pending, - nextScheduled, - lastPaid, - available, - instant, - } = overview; - - const pendingAmount = pending ? pending.amount : 0; - const pendingDepositsLink = pending?.deposits_count ? ( - - { getBalanceDepositCount( pending ) } - - ) : ( - '' - ); - - const nextScheduledAmount = nextScheduled ? nextScheduled.amount : 0; - const nextScheduledLink = nextScheduled && ( - - { getNextDepositLabelFormatted( nextScheduled ) } - - ); - - const lastPaidAmount = lastPaid ? lastPaid.amount : 0; - const lastPaidLink = lastPaid && ( - - { getDepositDate( lastPaid ) } - - ); - const depositButton = instant && ( - - ); - - const availableAmount = available ? available.amount : 0; - - const scheduleDescriptor = getDepositScheduleDescriptor( { - account, - last_deposit: lastPaid, - } ); - - return ( - - - { /* This div will be used for a proper layout next to the button. */ } -
-

- { sprintf( - __( '%s balance', 'woocommerce-payments' ), - formatCurrencyName( currency ) - ) } -

- -

- - { scheduleDescriptor } -

-
-
- - - - - - - - - -
- ); -}; - -const DepositsInformation = (): JSX.Element => { - const { - overviews, - isLoading, - } = useAllDepositsOverviews() as AccountOverview.OverviewsResponse; - - if ( isLoading ) { - return ; - } - - const { currencies, account } = overviews; - return ( - - { currencies.map( ( overview: AccountOverview.Overview ) => ( - - ) ) } - - ); -}; - -export default DepositsInformation; diff --git a/client/components/deposits-information/loading.tsx b/client/components/deposits-information/loading.tsx deleted file mode 100644 index 13943e06b6e..00000000000 --- a/client/components/deposits-information/loading.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { Card, CardHeader, CardBody } from '@wordpress/components'; -import CalendarIcon from 'gridicons/dist/calendar'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Loadable from 'components/loadable'; - -const DepositsInformationLoading = (): JSX.Element => { - return ( - - - { /* This div will be used for a proper layout next to the button. */ } -
-

- { __( 'Deposits overview', 'woocommerce-payments' ) } -

- -

- - -

-
-
- - - -
- ); -}; - -export default DepositsInformationLoading; diff --git a/client/components/deposits-information/style.scss b/client/components/deposits-information/style.scss deleted file mode 100644 index c41548aa32f..00000000000 --- a/client/components/deposits-information/style.scss +++ /dev/null @@ -1,61 +0,0 @@ -.wcpay-deposits-information-header { - @include breakpoint( '<480px' ) { - flex-wrap: wrap; - } - - &__heading { - @include breakpoint( '<480px' ) { - width: 100%; - margin-bottom: 10px; - } - } - - &__title { - margin: 0 0 7px; - font-size: 16px; - color: $gray-900; - } - - &__schedule { - margin: 0; - font-size: 13px; - color: $gray-600; - } - - &__icon { - vertical-align: middle; - margin-right: 5px; - - path { - fill: $gray-700; - } - } -} - -.wcpay-deposits-information-row { - & + & { - border-top: 1px solid $gray-200; - } -} - -.wcpay-deposits-information-block { - flex: 1; - padding: 24px 16px; - - & + & { - border-left: 1px solid $gray-200; - } - - &__title { - font-size: 12px; - line-height: 16px; - color: $gray-600; - } - - &__value { - color: $gray-900; - font-size: 20px; - line-height: 27px; - padding: 8px 0; - } -} diff --git a/client/components/deposits-information/test/__snapshots__/index.js.snap b/client/components/deposits-information/test/__snapshots__/index.js.snap deleted file mode 100644 index d20fbbded75..00000000000 --- a/client/components/deposits-information/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,673 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Deposits information renders correctly when loading 1`] = ` -
-
-
-
-

- Deposits overview -

-

- - - - - - - Deposit schedule here - -

-
-
-
- - Deposit information placeholder - -
-
-
-`; - -exports[`Deposits information renders correctly with multiple currencies 1`] = ` -
-
- -
-
-
- Pending balance -
-
- $33.43 -
- -
-
-
- Next deposit -
-
- $33.43 -
- -
-
-
-
-
- Last deposit -
-
- $31.60 -
- -
-
-
- Available balance -
-
- $20.30 -
-
-
-
-
-
-
-
-

- Euro balance -

-

- - - - - - Deposits set to every Thursday. - - Change this - - or - - learn more - - . -

-
-
-
-
-
- Pending balance -
-
- 33,43 € -
- -
-
-
- Next deposit -
-
- 33,43 € -
- -
-
-
-
-
- Last deposit -
-
- 31,60 € -
- -
-
-
- Available balance -
-
- 20,30 € -
-
-
-
-
-
-`; - -exports[`Deposits information renders instant deposit button only where applicable 1`] = ` -
-
-
-
-

- United States (US) dollar balance -

-

- - - - - - Deposits set to every Thursday. - - Change this - - or - - learn more - - . -

-
-
-
-
-
- Pending balance -
-
- $33.43 -
- -
-
-
- Next deposit -
-
- $33.43 -
- -
-
-
-
-
- Last deposit -
-
- $31.60 -
- -
-
-
- Available balance -
-
- $20.30 -
-
- -
-
-
-
-
-
-
-

- Euro balance -

-

- - - - - - Deposits set to every Thursday. - - Change this - - or - - learn more - - . -

-
-
-
-
-
- Pending balance -
-
- 33,43 € -
- -
-
-
- Next deposit -
-
- 33,43 € -
- -
-
-
-
-
- Last deposit -
-
- 31,60 € -
- -
-
-
- Available balance -
-
- 20,30 € -
-
-
-
-
-
-`; diff --git a/client/components/deposits-information/test/index.js b/client/components/deposits-information/test/index.js deleted file mode 100644 index 6e6dbd6e7a9..00000000000 --- a/client/components/deposits-information/test/index.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render } from '@testing-library/react'; -import { merge } from 'lodash'; - -/** - * Internal dependencies - */ -import DepositsInformation from '..'; -import { useAllDepositsOverviews, useInstantDeposit } from 'wcpay/data'; - -jest.mock( 'wcpay/data', () => ( { - useAllDepositsOverviews: jest.fn(), - useInstantDeposit: jest.fn(), -} ) ); - -const createMockAccount = ( account = {} ) => - merge( - { - default_currency: 'eur', - deposits_blocked: false, - deposits_disabled: false, - deposits_schedule: { - delay_days: 7, - interval: 'weekly', - weekly_anchor: 'thursday', - }, - }, - account - ); - -const createMockCurrency = ( currencyCode, extra = {} ) => - merge( - { - currency: currencyCode, - lastPaid: { - id: 'po_...', - date: 1619395200000, - amount: 3160, - }, - nextScheduled: { - id: 'wcpay_estimated_weekly_eur_1622678400', - date: 1622678400000, - amount: 3343, - }, - pending: { - amount: 3343, - deposits_count: 2, - }, - available: { - amount: 2030, - }, - }, - extra - ); - -const mockOverviews = ( currencies = null, account = null ) => { - return useAllDepositsOverviews.mockReturnValue( { - overviews: { - currencies: currencies, - account: account, - }, - isLoading: null === currencies || ! currencies.length, - } ); -}; - -useInstantDeposit.mockReturnValue( { - deposit: undefined, - isLoading: false, - submit: () => {}, -} ); - -describe( 'Deposits information', () => { - beforeEach( () => { - global.wcpaySettings = { - accountStatus: { - deposits: { - completed_waiting_period: true, - }, - }, - zeroDecimalCurrencies: [], - connect: { - country: 'FR', - }, - currencyData: { - FR: { - code: 'EUR', - symbol: '€', - symbolPosition: 'right_space', - thousandSeparator: ' ', - decimalSeparator: ',', - precision: 2, - }, - }, - }; - } ); - afterEach( () => { - jest.clearAllMocks(); - } ); - - test( 'renders correctly when loading', () => { - mockOverviews(); - const { container } = render( ); - expect( container ).toMatchSnapshot(); - } ); - - test( 'renders correctly with multiple currencies', () => { - mockOverviews( - [ createMockCurrency( 'usd' ), createMockCurrency( 'eur' ) ], - createMockAccount() - ); - - const { container } = render( ); - expect( container ).toMatchSnapshot(); - } ); - - test( 'renders correctly with all possible missing details', async () => { - // Everything should be zeros, without extra details or errors. - mockOverviews( - [ - createMockCurrency( 'usd', { - pending: null, - nextScheduled: null, - lastPaid: null, - available: null, - } ), - ], - createMockAccount() - ); - - const { findAllByText, findAllByTestId } = render( - - ); - - expect( await findAllByText( '$0.00' ) ).toHaveLength( 4 ); - ( await findAllByTestId( 'extra' ) ).forEach( ( extra ) => { - expect( extra ).toBeEmptyDOMElement(); - } ); - } ); - - test( 'renders instant deposit button only where applicable', async () => { - const currencyWithInstantDeposit = createMockCurrency( 'usd', { - instant: { - amount: 12345, - }, - } ); - - const currencyWithoutInstantDeposit = createMockCurrency( 'eur' ); - - mockOverviews( - // Only one of the currencies in the snapshot should include the instant deposit button. - [ currencyWithInstantDeposit, currencyWithoutInstantDeposit ], - createMockAccount() - ); - - const { container, findByText } = render( ); - expect( await findByText( 'Instant deposit' ) ).toBeVisible(); - expect( container ).toMatchSnapshot(); - } ); -} ); diff --git a/client/components/deposits-overview/footer.tsx b/client/components/deposits-overview/footer.tsx index a02160c4962..497412288b4 100644 --- a/client/components/deposits-overview/footer.tsx +++ b/client/components/deposits-overview/footer.tsx @@ -10,6 +10,7 @@ import { Link } from '@woocommerce/components'; */ import { getAdminUrl } from 'wcpay/utils'; import strings from './strings'; +import wcpayTracks from 'tracks'; /** * Renders the footer of the deposits overview card. @@ -34,10 +35,28 @@ const DepositsOverviewFooter: React.FC = () => { return ( - - + + wcpayTracks.recordEvent( + wcpayTracks.events + .OVERVIEW_DEPOSITS_CHANGE_SCHEDULE_CLICK + ) + } + > { strings.changeDepositSchedule } diff --git a/client/components/deposits-overview/index.tsx b/client/components/deposits-overview/index.tsx index 2030cf5afa0..59c6b762e2d 100644 --- a/client/components/deposits-overview/index.tsx +++ b/client/components/deposits-overview/index.tsx @@ -35,8 +35,15 @@ const DepositsOverview = (): JSX.Element => { currency ); + const hasNextDeposit = !! overview?.nextScheduled; + 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 <>; + } + return ( { strings.heading } diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index 3594ff51248..e677771804c 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -92,7 +92,6 @@ exports[`Deposits Overview information Component Renders 1`] = ` @@ -335,7 +332,7 @@ exports[`Suspended Deposit Notice Renders Component Renders 1`] = ` > diff --git a/client/components/deposits-status/index.js b/client/components/deposits-status/index.js index 09498db6b28..97ba19708fe 100644 --- a/client/components/deposits-status/index.js +++ b/client/components/deposits-status/index.js @@ -13,14 +13,18 @@ import { createInterpolateElement } from '@wordpress/element'; */ import 'components/account-status/shared.scss'; -const DepositsStatus = ( { status, interval, iconSize } ) => { +const DepositsStatus = ( { status, interval, accountStatus, iconSize } ) => { let className = 'account-status__info__green'; let description; let icon = ; const automaticIntervals = [ 'daily', 'weekly', 'monthly' ]; const showSuspendedNotice = 'blocked' === status; - if ( 'disabled' === status ) { + if ( 'pending_verification' === accountStatus ) { + description = __( 'Pending verification', 'woocommerce-payments' ); + className = 'account-status__info__gray'; + icon = ; + } else if ( 'disabled' === status ) { description = __( 'Disabled', 'woocommerce-payments' ); className = 'account-status__info__red'; icon = ; diff --git a/client/components/deposits-status/test/__snapshots__/index.js.snap b/client/components/deposits-status/test/__snapshots__/index.js.snap index 6dbed442e10..f5e9bd4d0f9 100644 --- a/client/components/deposits-status/test/__snapshots__/index.js.snap +++ b/client/components/deposits-status/test/__snapshots__/index.js.snap @@ -76,7 +76,7 @@ exports[`DepositsStatus renders daily status 1`] = ` > @@ -122,7 +122,7 @@ exports[`DepositsStatus renders manual status 1`] = ` > @@ -145,7 +145,7 @@ exports[`DepositsStatus renders monthly status 1`] = ` > @@ -154,6 +154,29 @@ exports[`DepositsStatus renders monthly status 1`] = `
`; +exports[`DepositsStatus renders pending verification status 1`] = ` +
+ +
+`; + exports[`DepositsStatus renders unknown status 1`] = `
@@ -191,7 +214,7 @@ exports[`DepositsStatus renders weekly status 1`] = ` > diff --git a/client/components/deposits-status/test/index.js b/client/components/deposits-status/test/index.js index b0d341f8aee..6fd03dbac97 100644 --- a/client/components/deposits-status/test/index.js +++ b/client/components/deposits-status/test/index.js @@ -71,6 +71,17 @@ describe( 'DepositsStatus', () => { expect( depositsStatus ).toMatchSnapshot(); } ); + test( 'renders pending verification status', async () => { + const { container: depositsStatus } = renderDepositsStatus( { + status: 'blocked', + accountStatus: 'pending_verification', + interval: 'daily', + iconSize: 20, + } ); + + expect( depositsStatus ).toMatchSnapshot(); + } ); + test( 'renders manual status', async () => { const { container: depositsStatus, findByText } = renderDepositsStatus( { @@ -93,10 +104,16 @@ describe( 'DepositsStatus', () => { expect( depositsStatus ).toMatchSnapshot(); } ); - function renderDepositsStatus( { status, interval, iconSize } ) { + function renderDepositsStatus( { + status, + interval, + accountStatus, + iconSize, + } ) { return render( diff --git a/client/components/details-link/test/__snapshots__/index.js.snap b/client/components/details-link/test/__snapshots__/index.js.snap index 992c2503f19..fee05f412bd 100644 --- a/client/components/details-link/test/__snapshots__/index.js.snap +++ b/client/components/details-link/test/__snapshots__/index.js.snap @@ -17,7 +17,7 @@ exports[`Details link renders dispute details with ID 1`] = ` > @@ -40,7 +40,7 @@ exports[`Details link renders transaction details with charge ID 1`] = ` > diff --git a/client/components/download-button/test/__snapshots__/index.js.snap b/client/components/download-button/test/__snapshots__/index.js.snap index e345e668b66..07531272344 100644 --- a/client/components/download-button/test/__snapshots__/index.js.snap +++ b/client/components/download-button/test/__snapshots__/index.js.snap @@ -16,7 +16,7 @@ exports[`DownloadButton renders a disabled button 1`] = ` > @@ -44,7 +44,7 @@ exports[`DownloadButton renders an active button 1`] = ` > diff --git a/client/components/file-upload/test/__snapshots__/index.tsx.snap b/client/components/file-upload/test/__snapshots__/index.tsx.snap index cbb7562d728..af4905635d4 100644 --- a/client/components/file-upload/test/__snapshots__/index.tsx.snap +++ b/client/components/file-upload/test/__snapshots__/index.tsx.snap @@ -41,7 +41,7 @@ exports[`FileUploadControl renders default file upload control 1`] = ` > @@ -102,7 +102,7 @@ exports[`FileUploadControl renders disabled state 1`] = ` > @@ -167,7 +167,7 @@ exports[`FileUploadControl renders loading state 1`] = ` > @@ -227,7 +227,7 @@ exports[`FileUploadControl renders upload done state 1`] = ` > @@ -258,7 +258,7 @@ exports[`FileUploadControl renders upload done state 1`] = ` > @@ -310,7 +310,7 @@ exports[`FileUploadControl renders upload failed state 1`] = ` > diff --git a/client/components/form/fields.tsx b/client/components/form/fields.tsx index 00749265577..0ca606bf635 100644 --- a/client/components/form/fields.tsx +++ b/client/components/form/fields.tsx @@ -12,6 +12,9 @@ import CustomSelectControl, { ControlProps, Item, } from 'components/custom-select-control'; +import PhoneNumberControl, { + PhoneNumberControlProps, +} from '../phone-number-control'; import './style.scss'; interface CommonProps { @@ -21,10 +24,11 @@ interface CommonProps { export type TextFieldProps = TextControl.Props & CommonProps; export type SelectFieldProps< ItemType > = ControlProps< ItemType > & CommonProps; +export type PhoneNumberFieldProps = PhoneNumberControlProps & CommonProps; type FieldProps< ItemType > = { - component: 'text' | 'select'; -} & ( TextFieldProps | SelectFieldProps< ItemType > ); + component: 'text' | 'select' | 'phone'; +} & ( TextFieldProps | SelectFieldProps< ItemType > | PhoneNumberFieldProps ); const Field = < ItemType extends Item >( { component, @@ -45,6 +49,10 @@ const Field = < ItemType extends Item >( { props = rest as SelectFieldProps< ItemType >; field = ; break; + case 'phone': + props = rest as PhoneNumberFieldProps; + field = ; + break; } return ( @@ -65,4 +73,8 @@ export const SelectField = < ItemType extends Item >( props: SelectFieldProps< ItemType > ): JSX.Element => ; +export const PhoneNumberField: React.FC< PhoneNumberControlProps > = ( + props +) => ; + export default Field; diff --git a/client/components/form/test/fields.tsx b/client/components/form/test/fields.tsx index e2055b38b56..a668cf658bd 100644 --- a/client/components/form/test/fields.tsx +++ b/client/components/form/test/fields.tsx @@ -7,7 +7,7 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import { TextField, SelectField } from '../fields'; +import { TextField, SelectField, PhoneNumberField } from '../fields'; describe( 'Form fields components', () => { it( 'renders TextField component with provided props', () => { @@ -38,6 +38,17 @@ describe( 'Form fields components', () => { expect( screen.getByText( 'Test Label' ) ).toBeInTheDocument(); } ); + it( 'renders PhoneNumberField component with provided props', () => { + render( + + ); + expect( screen.getByText( 'Test Label' ) ).toBeInTheDocument(); + } ); + it( 'renders TextField component with error', () => { render( = ( { return (
`; diff --git a/client/components/fraud-risk-tools-banner/style.scss b/client/components/fraud-risk-tools-banner/style.scss index 7c7653caa32..001f9774393 100644 --- a/client/components/fraud-risk-tools-banner/style.scss +++ b/client/components/fraud-risk-tools-banner/style.scss @@ -1,12 +1,13 @@ .discoverability-card { padding: 24px; - background: linear-gradient( 180deg, #f7edf7 0%, #fff 100% ); + background-color: #fff; @media ( min-width: 600px ) { - background: url( '../../../assets/images/upe_preview_illustration.svg' ) - no-repeat right, - linear-gradient( 180deg, #f7edf7 0%, #fff 100% ); - padding-right: 228px; + background: #fff + url( '../../../assets/images/fraud-protection/discoverability-banner@2x.png' ) + no-repeat center right; + background-size: 215px 236px; + padding-right: 236px; } &__new-feature-pill.wcpay-pill { diff --git a/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap index c5c2240431f..9cba7a14d16 100644 --- a/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap +++ b/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap @@ -25,19 +25,14 @@ exports[`FRTDiscoverabilityBanner renders 1`] = `

- New features have been added to WooCommerce Payments to help - - reduce fraudulent transactions - - on your store. By using a set of customizable rules to evaluate incoming orders, your store is better protected from fraudsters. + New features have been added to WooPayments to help reduce fraudulent transactions on your store. By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters.

Learn more @@ -74,19 +69,14 @@ exports[`FRTDiscoverabilityBanner renders when remindMeAt timestamp is in the pa

- New features have been added to WooCommerce Payments to help - - reduce fraudulent transactions - - on your store. By using a set of customizable rules to evaluate incoming orders, your store is better protected from fraudsters. + New features have been added to WooPayments to help reduce fraudulent transactions on your store. By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters.

Learn more @@ -123,19 +113,14 @@ exports[`FRTDiscoverabilityBanner renders with dismiss button if remindMeCount g

- New features have been added to WooCommerce Payments to help - - reduce fraudulent transactions - - on your store. By using a set of customizable rules to evaluate incoming orders, your store is better protected from fraudsters. + New features have been added to WooPayments to help reduce fraudulent transactions on your store. By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters.

Learn more @@ -172,19 +157,14 @@ exports[`FRTDiscoverabilityBanner renders without dismiss button if remindMeCoun

- New features have been added to WooCommerce Payments to help - - reduce fraudulent transactions - - on your store. By using a set of customizable rules to evaluate incoming orders, your store is better protected from fraudsters. + New features have been added to WooPayments to help reduce fraudulent transactions on your store. By using a set of rules to evaluate incoming orders, your store is better protected from fraudsters.

Learn more diff --git a/client/components/fraud-risk-tools-banner/test/index.test.tsx b/client/components/fraud-risk-tools-banner/test/index.test.tsx index 6c5bff94d53..b369ab5ec38 100644 --- a/client/components/fraud-risk-tools-banner/test/index.test.tsx +++ b/client/components/fraud-risk-tools-banner/test/index.test.tsx @@ -12,7 +12,6 @@ import FRTDiscoverabilityBanner from '..'; declare const global: { wcpaySettings: { frtDiscoverBannerSettings: string; - isFraudProtectionSettingsEnabled: boolean; }; }; @@ -33,7 +32,6 @@ describe( 'FRTDiscoverabilityBanner', () => { beforeEach( () => { global.wcpaySettings = { frtDiscoverBannerSettings: '', - isFraudProtectionSettingsEnabled: true, }; } ); @@ -45,7 +43,6 @@ describe( 'FRTDiscoverabilityBanner', () => { it( 'renders with dismiss button if remindMeCount greater than or equal to 3', () => { global.wcpaySettings = { - ...global.wcpaySettings, frtDiscoverBannerSettings: JSON.stringify( { remindMeCount: 3, remindMeAt: null, @@ -60,7 +57,6 @@ describe( 'FRTDiscoverabilityBanner', () => { it( 'renders without dismiss button if remindMeCount greater than 0 but less than 3', () => { global.wcpaySettings = { - ...global.wcpaySettings, frtDiscoverBannerSettings: JSON.stringify( { remindMeCount: 2, remindMeAt: null, @@ -81,7 +77,6 @@ describe( 'FRTDiscoverabilityBanner', () => { const remindMeAt = new Date( '2023-03-11T11:33:37.000Z' ).getTime(); global.wcpaySettings = { - ...global.wcpaySettings, frtDiscoverBannerSettings: JSON.stringify( { remindMeCount: 1, remindMeAt: remindMeAt, @@ -102,7 +97,6 @@ describe( 'FRTDiscoverabilityBanner', () => { const remindMeAt = new Date( '2023-03-15T12:33:37.000Z' ).getTime(); global.wcpaySettings = { - ...global.wcpaySettings, frtDiscoverBannerSettings: JSON.stringify( { remindMeCount: 1, remindMeAt: remindMeAt, @@ -119,7 +113,6 @@ describe( 'FRTDiscoverabilityBanner', () => { const remindMeAt = new Date( '2023-03-14T12:33:37.000Z' ).getTime(); global.wcpaySettings = { - ...global.wcpaySettings, frtDiscoverBannerSettings: JSON.stringify( { remindMeCount: 3, remindMeAt: remindMeAt, diff --git a/client/components/grouped-select-control/index.tsx b/client/components/grouped-select-control/index.tsx index fc7142088a3..79a2101c9e1 100644 --- a/client/components/grouped-select-control/index.tsx +++ b/client/components/grouped-select-control/index.tsx @@ -5,81 +5,60 @@ import React, { useRef, useState } from 'react'; import { Icon, check, chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; import { __ } from '@wordpress/i18n'; -import { useSelect } from 'downshift'; +import { useSelect, UseSelectState } from 'downshift'; /** * Internal Dependencies */ import './style.scss'; -export interface Item { +export interface ListItem { key: string; name: string; - group: string; + group?: string; context?: string; className?: string; -} - -export interface Group { - key: string; - name: string; - className?: string; -} - -interface ListItem extends Omit< Item, 'group' > { - group?: string; items?: string[]; } export interface GroupedSelectControlProps< ItemType > { label: string; options: ItemType[]; - groups: Group[]; value?: ItemType; placeholder?: string; searchable?: boolean; className?: string; - onChange: ( value?: ItemType ) => void; + onChange?: ( changes: Partial< UseSelectState< ItemType > > ) => void; } -const GroupedSelectControl = < ItemType extends Item >( { +const GroupedSelectControl = < ItemType extends ListItem >( { + className, label, - options: items, + options: listItems, + onChange: onSelectedItemChange, value, - groups, placeholder, searchable, - className, - onChange, }: GroupedSelectControlProps< ItemType > ): JSX.Element => { const searchRef = useRef< HTMLInputElement >( null ); const previousStateRef = useRef< { visibleItems: Set< string >; } >(); - const groupKeys = groups.map( ( group ) => group.key ); - const mergedList = groups.reduce( ( acc, group ) => { - const groupItems = items.filter( ( item ) => item.group === group.key ); - return [ - ...acc, - { - ...group, - items: groupItems.map( ( item ) => item.key ), - }, - ...groupItems, - ]; - }, [] as ListItem[] ); + const groupKeys = listItems + .filter( ( item ) => item.items?.length ) + .map( ( group ) => group.key ); const [ openedGroups, setOpenedGroups ] = useState( - new Set( [ groups[ 0 ]?.key ] ) + new Set( [ groupKeys[ 0 ] ] ) ); const [ visibleItems, setVisibleItems ] = useState( - new Set( [ ...groupKeys, ...( mergedList[ 0 ]?.items || [] ) ] ) + new Set( [ ...groupKeys, ...( listItems[ 0 ]?.items || [] ) ] ) ); const [ searchText, setSearchText ] = useState( '' ); - const itemsToRender = mergedList.filter( ( item ) => + const itemsToRender = listItems.filter( ( item ) => visibleItems.has( item.key ) ); @@ -95,8 +74,7 @@ const GroupedSelectControl = < ItemType extends Item >( { items: itemsToRender, itemToString: ( item ) => item.name, selectedItem: value || ( {} as ItemType ), - onSelectedItemChange: ( changes ) => - onChange( changes.selectedItem as ItemType ), + onSelectedItemChange, stateReducer: ( state, { changes, type } ) => { if ( searchable && @@ -141,12 +119,16 @@ const GroupedSelectControl = < ItemType extends Item >( { setVisibleItems( previousStateRef.current.visibleItems ); previousStateRef.current = undefined; } else { - const filteredItems = items.filter( ( item ) => - `${ item.name }${ item.context || '' }` - .toLowerCase() - .includes( target.value.toLowerCase() ) + const filteredItems = listItems.filter( + ( item ) => + item?.group && + `${ item.name } ${ item.context || '' }` + .toLowerCase() + .includes( target.value.toLowerCase() ) + ); + const filteredGroups = filteredItems.map( + ( item ): string => item?.group || '' ); - const filteredGroups = filteredItems.map( ( item ) => item.group ); const filteredVisibleItems = new Set( [ ...filteredItems.map( ( i ) => i.key ), ...filteredGroups, @@ -158,7 +140,7 @@ const GroupedSelectControl = < ItemType extends Item >( { }; const menuProps = getMenuProps( { - className: 'wcpay-component-new-select-control__list', + className: 'wcpay-component-grouped-select-control__list', 'aria-hidden': ! isOpen, onFocus: () => searchRef.current?.focus(), onBlur: ( event: any ) => { @@ -176,13 +158,13 @@ const GroupedSelectControl = < ItemType extends Item >( { return (
@@ -1121,7 +1114,6 @@ exports[`Multi-Currency enabled currencies list currency search works with curre class="search__icon" focusable="false" height="24" - role="img" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" @@ -1251,13 +1243,12 @@ exports[`Multi-Currency enabled currencies list should hide recommended currenci class="wcpay-wizard-task__icon-checkmark" focusable="false" height="24" - role="img" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" >
@@ -1308,7 +1299,6 @@ exports[`Multi-Currency enabled currencies list should hide recommended currenci class="search__icon" focusable="false" height="24" - role="img" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" diff --git a/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap b/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap index de5c605f413..fd924b0cefb 100644 --- a/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap +++ b/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap @@ -25,13 +25,12 @@ exports[`Multi-Currency store settings store settings task renders correctly: sn class="wcpay-wizard-task__icon-checkmark" focusable="false" height="24" - role="img" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" >
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 10ff9981408..7052fad6cc0 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 @@ -32,9 +32,10 @@ exports[`Multi-Currency enabled currencies list Available currencies modal rende @@ -861,9 +861,10 @@ exports[`Multi-Currency enabled currencies list Remove currency modal renders co @@ -41,7 +41,7 @@ exports[`Onboarding Requirements Company renders with directors and executives 1 > @@ -68,7 +68,7 @@ exports[`Onboarding Requirements Company renders with directors and executives 1 > @@ -95,7 +95,7 @@ exports[`Onboarding Requirements Company renders with directors and executives 1 > @@ -122,7 +122,7 @@ exports[`Onboarding Requirements Company renders with owners 1`] = ` > @@ -149,7 +149,7 @@ exports[`Onboarding Requirements Company renders with owners 1`] = ` > @@ -176,7 +176,7 @@ exports[`Onboarding Requirements Company renders with owners 1`] = ` > @@ -203,7 +203,7 @@ exports[`Onboarding Requirements Company renders with owners 1`] = ` > @@ -230,7 +230,7 @@ exports[`Onboarding Requirements Company renders without company details 1`] = ` > @@ -257,7 +257,7 @@ exports[`Onboarding Requirements Company renders without company details 1`] = ` > @@ -284,7 +284,7 @@ exports[`Onboarding Requirements Company renders without company details 1`] = ` > diff --git a/client/onboarding-experiment/requirements/test/__snapshots__/individual.tsx.snap b/client/onboarding-experiment/requirements/test/__snapshots__/individual.tsx.snap index 3dce3947dd2..9ce59a34c79 100644 --- a/client/onboarding-experiment/requirements/test/__snapshots__/individual.tsx.snap +++ b/client/onboarding-experiment/requirements/test/__snapshots__/individual.tsx.snap @@ -14,7 +14,7 @@ exports[`Onboarding Requirements Individual renders personal details 1`] = ` > @@ -41,7 +41,7 @@ exports[`Onboarding Requirements Individual renders personal details 1`] = ` > @@ -68,7 +68,7 @@ exports[`Onboarding Requirements Individual renders with tax 1`] = ` > @@ -95,7 +95,7 @@ exports[`Onboarding Requirements Individual renders with tax 1`] = ` > @@ -122,7 +122,7 @@ exports[`Onboarding Requirements Individual renders with tax 1`] = ` > diff --git a/client/onboarding-experiment/tasks/add-business-info-task/index.tsx b/client/onboarding-experiment/tasks/add-business-info-task/index.tsx index 1fdc0eff44f..7015b31936f 100644 --- a/client/onboarding-experiment/tasks/add-business-info-task/index.tsx +++ b/client/onboarding-experiment/tasks/add-business-info-task/index.tsx @@ -10,16 +10,15 @@ import { __ } from '@wordpress/i18n'; import WizardTaskItem from 'additional-methods-setup/wizard/task-item'; import WizardTaskContext from 'additional-methods-setup/wizard/task/context'; import CustomSelectControl from 'components/custom-select-control'; -import { LoadableBlock } from 'components/loadable'; -import { useBusinessTypes } from 'onboarding-experiment/hooks'; import RequiredVerificationInfo from './required-verification-info'; import strings from 'onboarding-experiment/strings'; +import { OnboardingProps } from 'onboarding-experiment/types'; +import { getBusinessTypes } from 'onboarding-prototype/utils'; import { - Country, - BusinessType, BusinessStructure, - OnboardingProps, -} from 'onboarding-experiment/types'; + BusinessType, + Country, +} from 'onboarding-prototype/types'; interface TaskProps { onChange: ( data: Partial< OnboardingProps > ) => void; @@ -27,7 +26,7 @@ interface TaskProps { const AddBusinessInfoTask = ( { onChange }: TaskProps ): JSX.Element => { const { isCompleted, setCompleted } = useContext( WizardTaskContext ); - const { countries, isLoading } = useBusinessTypes(); + const countries = getBusinessTypes(); const [ businessCountry, setBusinessCountry ] = useState< Country >(); const [ businessType, setBusinessType ] = useState< BusinessType >(); @@ -80,60 +79,54 @@ const AddBusinessInfoTask = ( { onChange }: TaskProps ): JSX.Element => {

{ strings.onboarding.description }

- + + handleBusinessCountryUpdate( selectedItem ) + } + options={ countries } + /> +

+ { strings.onboarding.countryDescription } +

+ { businessCountry && ( - handleBusinessCountryUpdate( selectedItem ) + handleBusinessTypeUpdate( selectedItem ) } - options={ countries } - /> -

- { strings.onboarding.countryDescription } -

- { businessCountry && ( - - handleBusinessTypeUpdate( selectedItem ) - } - > - { ( item ) => ( -
-
{ item.name }
-
- { item.description } -
+ > + { ( item ) => ( +
+
{ item.name }
+
+ { item.description }
- ) } - - ) } - { businessType && businessType.structures?.length > 0 && ( - - handleBusinessStructureUpdate( selectedItem ) - } - /> - ) } - - +
+ ) } + + ) } + { businessType && businessType.structures?.length > 0 && ( + + handleBusinessStructureUpdate( selectedItem ) + } + /> + ) } { businessCountry && businessType && isCompleted && (
@@ -68,13 +67,16 @@ exports[`AddBusinessInfoTask hides the structure when no structures are availabl id="downshift-23-toggle-button" type="button" > - United States + + United States + + Individual +
-
  • -
    -
    -
    -
    - 1 -
    - -
    - - Tell us more about your business - -
    -
    -

    - Preview the details we may require to verify your business and enable deposits. -

    - -

    - Block placeholder -

    -
    - -

    - Block placeholder -

    -
    -
    -
  • -
    -`; - exports[`AddBusinessInfoTask shows business type and structure from a selected country 1`] = `
    @@ -289,13 +219,16 @@ exports[`AddBusinessInfoTask shows business type and structure from a selected c id="downshift-3-toggle-button" type="button" > - United States + + United States + + Company + + Single member LLC +
    @@ -480,13 +418,16 @@ exports[`AddBusinessInfoTask shows the form 1`] = ` id="downshift-0-toggle-button" type="button" > - United States + + United States + + What type of business do you run? + @@ -100,7 +100,7 @@ exports[`RequiredVerificationInfoTask shows the requirements 1`] = ` > @@ -127,7 +127,7 @@ exports[`RequiredVerificationInfoTask shows the requirements 1`] = ` > diff --git a/client/onboarding-experiment/tasks/add-business-info-task/test/index.tsx b/client/onboarding-experiment/tasks/add-business-info-task/test/index.tsx index 51af3429410..2fbe755eb0e 100644 --- a/client/onboarding-experiment/tasks/add-business-info-task/test/index.tsx +++ b/client/onboarding-experiment/tasks/add-business-info-task/test/index.tsx @@ -10,7 +10,7 @@ import { mocked } from 'ts-jest/utils'; * Internal dependencies */ import AddBusinessInfoTask from '../'; -import { useBusinessTypes } from 'onboarding-experiment/hooks'; +import { getBusinessTypes } from 'onboarding-prototype/utils'; declare const global: { wcpaySettings: { @@ -20,8 +20,8 @@ declare const global: { }; }; -jest.mock( 'onboarding-experiment/hooks', () => ( { - useBusinessTypes: jest.fn(), +jest.mock( 'onboarding-prototype/utils', () => ( { + getBusinessTypes: jest.fn(), } ) ); const countries = [ @@ -91,21 +91,8 @@ describe( 'AddBusinessInfoTask', () => { }; } ); - it( 'shows a loadable block', () => { - mocked( useBusinessTypes ).mockReturnValue( { - countries: [], - isLoading: true, - } ); - - const { container: task } = renderTask(); - expect( task ).toMatchSnapshot(); - } ); - it( 'shows the form', () => { - mocked( useBusinessTypes ).mockReturnValue( { - countries, - isLoading: false, - } ); + mocked( getBusinessTypes ).mockReturnValue( countries ); const { container: task } = renderTask(); expect( task ).toMatchSnapshot(); @@ -117,10 +104,7 @@ describe( 'AddBusinessInfoTask', () => { country: 'FR', }, }; - mocked( useBusinessTypes ).mockReturnValue( { - countries, - isLoading: false, - } ); + mocked( getBusinessTypes ).mockReturnValue( countries ); const { container: task } = renderTask(); @@ -156,10 +140,7 @@ describe( 'AddBusinessInfoTask', () => { country: 'FR', }, }; - mocked( useBusinessTypes ).mockReturnValue( { - countries, - isLoading: false, - } ); + mocked( getBusinessTypes ).mockReturnValue( countries ); const { container: task } = renderTask(); diff --git a/client/onboarding-experiment/tasks/setup-complete-task/test/__snapshots__/index.tsx.snap b/client/onboarding-experiment/tasks/setup-complete-task/test/__snapshots__/index.tsx.snap index 01ddc6279ff..0ac934241a2 100644 --- a/client/onboarding-experiment/tasks/setup-complete-task/test/__snapshots__/index.tsx.snap +++ b/client/onboarding-experiment/tasks/setup-complete-task/test/__snapshots__/index.tsx.snap @@ -25,13 +25,12 @@ exports[`SetupCompleteTask renders page 1`] = ` class="wcpay-wizard-task__icon-checkmark" focusable="false" height="24" - role="img" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" >
    diff --git a/client/onboarding-experiment/translations/structures.tsx b/client/onboarding-experiment/translations/structures.tsx deleted file mode 100644 index 932c86e7f70..00000000000 --- a/client/onboarding-experiment/translations/structures.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** @format */ - -/* eslint-disable max-len */ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -interface StructureKeyMap { - [ key: string ]: { - [ key: string ]: string; - }; -} - -const businessStructureStrings: StructureKeyMap = { - AE: { - llc: __( 'LLC', 'woocommerce-payments' ), - sole_establishment: __( 'Sole establishment', 'woocommerce-payments' ), - free_zone_llc: __( 'Free-zone LLC', 'woocommerce-payments' ), - free_zone_establishment: __( - 'Free-zone establishment', - 'woocommerce-payments' - ), - }, - AU: { - public_corporation: __( 'Public company', 'woocommerce-payments' ), - private_corporation: __( - 'Proprietary company', - 'woocommerce-payments' - ), - sole_proprietorship: __( 'Sole trader', 'woocommerce-payments' ), - private_partnership: __( 'Partnership', 'woocommerce-payments' ), - unincorporated_association: __( - 'Unincorporated association', - 'woocommerce-payments' - ), - }, - HK: { - sole_proprietorship: __( - 'Sole proprietorship', - 'woocommerce-payments' - ), - private_company: __( 'Private company', 'woocommerce-payments' ), - private_partnership: __( - 'Private partnership', - 'woocommerce-payments' - ), - incorporated_non_profit: __( - 'Incorporated non-profit', - 'woocommerce-payments' - ), - unincorporated_non_profit: __( - 'Unincorporated non-profit', - 'woocommerce-payments' - ), - }, - NZ: { - public_corporation: __( 'Public corporation', 'woocommerce-payments' ), - private_corporation: __( 'Corporation', 'woocommerce-payments' ), - sole_proprietorship: __( 'Sole trader', 'woocommerce-payments' ), - private_partnership: __( 'Partnership', 'woocommerce-payments' ), - incorporated_non_profit: __( - 'Incorporated non-profit', - 'woocommerce-payments' - ), - unincorporated_non_profit: __( - 'Unincorporated non-profit', - 'woocommerce-payments' - ), - }, - SG: { - sole_proprietorship: __( - 'Sole proprietorship', - 'woocommerce-payments' - ), - private_company: __( 'Company', 'woocommerce-payments' ), - public_company: __( 'Public company', 'woocommerce-payments' ), - private_partnership: __( 'Partnership', 'woocommerce-payments' ), - }, - US: { - sole_proprietorship: __( - 'Sole proprietorship', - 'woocommerce-payments' - ), - single_member_llc: __( 'Single-member LLC', 'woocommerce-payments' ), - multi_member_llc: __( 'Multi-member LLC', 'woocommerce-payments' ), - private_partnership: __( - 'Private partnership', - 'woocommerce-payments' - ), - private_corporation: __( - 'Private corporation', - 'woocommerce-payments' - ), - unincorporated_association: __( - 'Unincorporated association', - 'woocommerce-payments' - ), - public_partnership: __( 'Public partnership', 'woocommerce-payments' ), - public_corporation: __( 'Public corporation', 'woocommerce-payments' ), - incorporated_non_profit: __( - 'Incorporated non-profit', - 'woocommerce-payments' - ), - unincorporated_non_profit: __( - 'Unincorporated non-profit', - 'woocommerce-payments' - ), - governmental_unit: __( 'Governmental unit', 'woocommerce-payments' ), - government_instrumentality: __( - 'Government instrumentality proprietorship', - 'woocommerce-payments' - ), - tax_exempt_government_instrumentality: __( - 'Tax-exempt government instrumentality', - 'woocommerce-payments' - ), - }, -}; - -export default businessStructureStrings; diff --git a/client/onboarding-experiment/translations/types.tsx b/client/onboarding-experiment/translations/types.tsx deleted file mode 100644 index f2cc004ed45..00000000000 --- a/client/onboarding-experiment/translations/types.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** @format */ - -/* eslint-disable max-len */ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -interface TypeKeyMap { - [ key: string ]: string; -} - -const businessTypeStrings: TypeKeyMap = { - individual: __( 'Individual', 'woocommerce-payments' ), - company: __( 'Company', 'woocommerce-payments' ), - non_profit: __( 'Non-Profit', 'woocommerce-payments' ), - government_entity: __( 'Government Entity', 'woocommerce-payments' ), -}; - -export default businessTypeStrings; diff --git a/client/onboarding-experiment/types.d.ts b/client/onboarding-experiment/types.d.ts index 23f9d49b4e8..bd7325f8b75 100644 --- a/client/onboarding-experiment/types.d.ts +++ b/client/onboarding-experiment/types.d.ts @@ -1,21 +1,3 @@ -export interface Country { - key: string; - name: string; - types: BusinessType[]; -} - -export interface BusinessType { - key: string; - name: string; - description: string; - structures: BusinessStructure[]; -} - -export interface BusinessStructure { - key: string; - name: string; -} - export interface OnboardingProps { country: string; type: string; diff --git a/client/onboarding-prototype/context.tsx b/client/onboarding-prototype/context.tsx index c1517f50e8e..ada76b45834 100644 --- a/client/onboarding-prototype/context.tsx +++ b/client/onboarding-prototype/context.tsx @@ -7,12 +7,13 @@ import { isNil, omitBy } from 'lodash'; /** * Internal dependencies */ -import { OnboardingFields } from './types'; +import { OnboardingFields, TempData } from './types'; const useContextValue = () => { const [ data, setData ] = useState( {} as OnboardingFields ); const [ errors, setErrors ] = useState( {} as OnboardingFields ); const [ touched, setTouched ] = useState( {} as OnboardingFields ); + const [ temp, setTemp ] = useState( {} as TempData ); return { data, @@ -24,6 +25,9 @@ const useContextValue = () => { touched, setTouched: ( value: Record< string, boolean > ) => setTouched( ( prev ) => ( { ...prev, ...value } ) ), + temp, + setTemp: ( value: Partial< TempData > ) => + setTemp( ( prev ) => ( { ...prev, ...value } ) ), }; }; diff --git a/client/onboarding-prototype/form.tsx b/client/onboarding-prototype/form.tsx index 5e4c90c370c..37bcbc039cb 100644 --- a/client/onboarding-prototype/form.tsx +++ b/client/onboarding-prototype/form.tsx @@ -15,20 +15,27 @@ import { TextFieldProps, SelectField, SelectFieldProps, + PhoneNumberField, + PhoneNumberFieldProps, } from 'components/form/fields'; import { useOnboardingContext } from './context'; import { OnboardingFields } from './types'; import { useValidation } from './validation'; +import { trackStepCompleted } from './tracking'; import strings from './strings'; +import GroupedSelectControl, { + ListItem, +} from 'components/grouped-select-control'; export const OnboardingForm: React.FC = ( { children } ) => { const { errors, touched, setTouched } = useOnboardingContext(); - const { nextStep } = useStepperContext(); - - const isValid = isEmpty( errors ); + const { currentStep, nextStep } = useStepperContext(); const handleContinue = () => { - if ( isValid ) return nextStep(); + if ( isEmpty( errors ) ) { + trackStepCompleted( currentStep ); + return nextStep(); + } setTouched( mapValues( touched, () => true ) ); }; @@ -40,7 +47,7 @@ export const OnboardingForm: React.FC = ( { children } ) => { } } > { children } - @@ -72,6 +79,34 @@ export const OnboardingTextField: React.FC< OnboardingTextFieldProps > = ( { ); }; +interface OnboardingPhoneNumberFieldProps + extends Partial< PhoneNumberFieldProps > { + name: keyof OnboardingFields; +} + +export const OnboardingPhoneNumberField: React.FC< OnboardingPhoneNumberFieldProps > = ( { + name, + ...rest +} ) => { + const { data, setData, temp, setTemp } = useOnboardingContext(); + const { validate, error } = useValidation( name ); + + return ( + { + setTemp( { phoneCountryCode } ); + setData( { [ name ]: value } ); + validate( value ); + } } + error={ error() } + { ...rest } + /> + ); +}; + interface OnboardingSelectFieldProps< ItemType > extends Partial< Omit< SelectFieldProps< ItemType >, 'onChange' > > { name: keyof OnboardingFields; @@ -109,3 +144,40 @@ export const OnboardingSelectField = < ItemType extends Item >( { /> ); }; + +interface OnboardingGroupedSelectFieldProps< ItemType > + extends OnboardingSelectFieldProps< ItemType > { + searchable?: boolean; +} + +export const OnboardingGroupedSelectField = < ListItemType extends ListItem >( { + name, + onChange, + ...rest +}: OnboardingGroupedSelectFieldProps< ListItemType > ): JSX.Element => { + const { data, setData } = useOnboardingContext(); + const { validate, error } = useValidation( name ); + + return ( + item.key === data[ name ] + ) } + placeholder={ + ( strings.placeholders as Record< string, string > )[ name ] + } + onChange={ ( { selectedItem } ) => { + if ( onChange ) { + onChange?.( name, selectedItem ); + } else { + setData( { [ name ]: selectedItem?.key } ); + } + validate( selectedItem?.key ); + } } + options={ [] } + error={ error() } + { ...rest } + /> + ); +}; diff --git a/client/onboarding-prototype/index.tsx b/client/onboarding-prototype/index.tsx index b3bf7d0777f..69f154e3f85 100644 --- a/client/onboarding-prototype/index.tsx +++ b/client/onboarding-prototype/index.tsx @@ -1,39 +1,37 @@ /** * External dependencies */ -import React from 'react'; +import React, { useEffect } from 'react'; /** * Internal dependencies */ import { OnboardingContextProvider } from './context'; import { Stepper } from 'components/stepper'; -import { OnboardingSteps } from './types'; import { OnboardingForm } from './form'; +import Step from './step'; import ModeChoice from './steps/mode-choice'; import PersonalDetails from './steps/personal-details'; import BusinessDetails from './steps/business-details'; import StoreDetails from './steps/store-details'; -import Loading from './steps/loading'; -import strings from './strings'; +import LoadingStep from './steps/loading'; +import { trackStarted } from './tracking'; import './style.scss'; -interface Props { - name: OnboardingSteps; -} -const Step: React.FC< Props > = ( { name, children } ) => { - return ( - <> -

    { strings.steps[ name ].heading }

    -

    { strings.steps[ name ].subheading }

    - { children } - - ); -}; - const OnboardingStepper = () => { + const handleExit = () => { + if ( + window.history.length > 1 && + document.referrer.includes( wcSettings.adminUrl ) + ) + return window.history.back(); + window.location.href = wcSettings.adminUrl; + }; + + const handleStepChange = () => window.scroll( 0, 0 ); + return ( - + @@ -52,14 +50,31 @@ const OnboardingStepper = () => { - - - + ); }; const OnboardingPrototype: React.FC = () => { + useEffect( () => { + trackStarted(); + + // Remove loading class and add those requires for full screen. + document.body.classList.remove( 'woocommerce-admin-is-loading' ); + document.body.classList.add( 'woocommerce-admin-full-screen' ); + document.body.classList.add( 'is-wp-toolbar-disabled' ); + document.body.classList.add( 'wcpay-onboarding-prototype__body' ); + + // Remove full screen classes on unmount. + return () => { + document.body.classList.remove( 'woocommerce-admin-full-screen' ); + document.body.classList.remove( 'is-wp-toolbar-disabled' ); + document.body.classList.remove( + 'wcpay-onboarding-prototype__body' + ); + }; + }, [] ); + return (
    diff --git a/client/onboarding-prototype/step.tsx b/client/onboarding-prototype/step.tsx new file mode 100644 index 00000000000..801d46ab31a --- /dev/null +++ b/client/onboarding-prototype/step.tsx @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import React from 'react'; +import { Icon, closeSmall } from '@wordpress/icons'; +import ChevronLeft from 'gridicons/dist/chevron-left'; + +/** + * Internal dependencies + */ +import { useStepperContext } from 'components/stepper'; +import { OnboardingSteps } from './types'; +import { useTrackAbandoned } from './tracking'; +import strings from './strings'; +import Logo from 'assets/images/woopayments.svg'; +import './style.scss'; + +interface Props { + name: OnboardingSteps; +} + +const Step: React.FC< Props > = ( { name, children } ) => { + const { trackAbandoned } = useTrackAbandoned(); + const { progress, prevStep, exit } = useStepperContext(); + const width = `${ progress * 100 }%`; + + const handleExit = () => { + trackAbandoned( 'exit' ); + exit(); + }; + + return ( + <> +
    +
    + + WooPayments + +
    +
    +

    + { strings.steps[ name ].heading } +

    +

    + { strings.steps[ name ].subheading } +

    +
    { children }
    +
    + + ); +}; + +export default Step; diff --git a/client/onboarding-prototype/steps/business-details.tsx b/client/onboarding-prototype/steps/business-details.tsx index 44470f9fb5a..5c53c3bdd3c 100644 --- a/client/onboarding-prototype/steps/business-details.tsx +++ b/client/onboarding-prototype/steps/business-details.tsx @@ -8,14 +8,18 @@ import React from 'react'; */ import { useOnboardingContext } from '../context'; import { Item } from 'components/custom-select-control'; -import { useBusinessTypes } from 'onboarding-experiment/hooks'; import { OnboardingFields } from '../types'; -import { BusinessType } from 'onboarding-experiment/types'; -import { OnboardingTextField, OnboardingSelectField } from '../form'; +import { + OnboardingTextField, + OnboardingSelectField, + OnboardingGroupedSelectField, +} from '../form'; +import { getBusinessTypes, getMccsFlatList } from 'onboarding-prototype/utils'; +import { BusinessType } from 'onboarding-prototype/types'; const BusinessDetails: React.FC = () => { const { data, setData } = useOnboardingContext(); - const { countries } = useBusinessTypes(); + const countries = getBusinessTypes(); const selectedCountry = countries.find( ( country ) => country.key === data.country @@ -39,6 +43,8 @@ const BusinessDetails: React.FC = () => { setData( newData ); }; + const mccsFlatList = getMccsFlatList(); + return ( <> @@ -72,11 +78,12 @@ const BusinessDetails: React.FC = () => { onChange={ handleTiedChange } /> ) } - { /* */ } + options={ mccsFlatList } + searchable + /> ); }; diff --git a/client/onboarding-prototype/steps/loading.tsx b/client/onboarding-prototype/steps/loading.tsx index a75f1d0f09e..639c5c03506 100644 --- a/client/onboarding-prototype/steps/loading.tsx +++ b/client/onboarding-prototype/steps/loading.tsx @@ -11,10 +11,19 @@ import apiFetch from '@wordpress/api-fetch'; import { useOnboardingContext } from '../context'; import { EligibleData, EligibleResult } from '../types'; import { fromDotNotation } from '../utils'; +import { trackRedirected, useTrackAbandoned } from '../tracking'; +import LoadBar from 'components/load-bar'; +import strings from '../strings'; -const Loading: React.FC = () => { +interface Props { + name: string; +} + +const LoadingStep: React.FC< Props > = () => { const { data } = useOnboardingContext(); + const { removeTrackListener } = useTrackAbandoned(); + const isEligibleForPo = async () => { if ( ! data.country || @@ -28,7 +37,7 @@ const Loading: React.FC = () => { business: { country: data.country, type: data.business_type, - mcc: 'computers_peripherals_and_software', // TODO GH-4853 add MCC from onboarding form + mcc: 'software_services', // TODO GH-4853 add MCC from onboarding form annual_revenue: data.annual_revenue, go_live_timeframe: data.go_live_timeframe, }, @@ -56,6 +65,10 @@ const Loading: React.FC = () => { prefill: fromDotNotation( data ), progressive: isEligible, } ); + + trackRedirected( isEligible ); + removeTrackListener(); + window.location.href = resultUrl; }; @@ -65,8 +78,17 @@ const Loading: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); - // TODO [GH-4746] Use LoadBar component. - return <>; + return ( +
    +

    + { strings.steps.loading.heading } +

    + +

    + { strings.steps.loading.subheading } +

    +
    + ); }; -export default Loading; +export default LoadingStep; diff --git a/client/onboarding-prototype/steps/mode-choice.tsx b/client/onboarding-prototype/steps/mode-choice.tsx index 5d3bf4acacd..3065560dbbd 100644 --- a/client/onboarding-prototype/steps/mode-choice.tsx +++ b/client/onboarding-prototype/steps/mode-choice.tsx @@ -11,6 +11,7 @@ import { addQueryArgs } from '@wordpress/url'; */ import RadioCard from 'components/radio-card'; import { useStepperContext } from 'components/stepper'; +import { trackModeSelected } from '../tracking'; import strings from '../strings'; const ModeChoice: React.FC = () => { @@ -21,6 +22,8 @@ const ModeChoice: React.FC = () => { const { nextStep } = useStepperContext(); const handleContinue = () => { + trackModeSelected( selected ); + if ( selected === 'live' ) return nextStep(); const { connectUrl } = wcpaySettings; @@ -59,7 +62,11 @@ const ModeChoice: React.FC = () => { }, ] } /> - diff --git a/client/onboarding-prototype/steps/personal-details.tsx b/client/onboarding-prototype/steps/personal-details.tsx index cf06218db32..292f68a0c71 100644 --- a/client/onboarding-prototype/steps/personal-details.tsx +++ b/client/onboarding-prototype/steps/personal-details.tsx @@ -3,12 +3,14 @@ */ import React from 'react'; import { Flex, FlexBlock } from '@wordpress/components'; +import { info } from '@wordpress/icons'; /** * Internal dependencies */ import strings from '../strings'; -import { OnboardingTextField } from '../form'; +import { OnboardingTextField, OnboardingPhoneNumberField } from '../form'; +import BannerNotice from 'components/banner-notice'; const PersonalDetails: React.FC = () => { return ( @@ -22,13 +24,10 @@ const PersonalDetails: React.FC = () => { -
    - { - // TODO: Use BannerNotice component when it's available. - strings.steps.personal.notice - } -
    - + + + { strings.steps.personal.notice } + ); }; diff --git a/client/onboarding-prototype/steps/test/business-details.tsx b/client/onboarding-prototype/steps/test/business-details.tsx index 1d7bee96a38..77b2f09e607 100644 --- a/client/onboarding-prototype/steps/test/business-details.tsx +++ b/client/onboarding-prototype/steps/test/business-details.tsx @@ -11,11 +11,12 @@ import { mocked } from 'ts-jest/utils'; */ import BusinessDetails from '../business-details'; import { OnboardingContextProvider } from '../../context'; -import { useBusinessTypes } from 'onboarding-experiment/hooks'; import strings from '../../strings'; +import { getBusinessTypes, getMccsFlatList } from 'onboarding-prototype/utils'; -jest.mock( 'onboarding-experiment/hooks', () => ( { - useBusinessTypes: jest.fn(), +jest.mock( 'onboarding-prototype/utils', () => ( { + getBusinessTypes: jest.fn(), + getMccsFlatList: jest.fn(), } ) ); const countries = [ @@ -72,10 +73,78 @@ const countries = [ }, ]; -mocked( useBusinessTypes ).mockReturnValue( { - countries, - isLoading: false, -} ); +mocked( getBusinessTypes ).mockReturnValue( countries ); + +const mccsFlatList = [ + { + key: 'most_popular', + name: 'Most popular', + items: [ + 'most_popular__software_services', + 'most_popular__clothing_and_apparel', + 'most_popular__consulting_services', + ], + }, + { + key: 'most_popular__software_services', + name: 'Popular Software', + group: 'most_popular', + context: + 'programming web website design data entry processing integrated systems', + }, + { + key: 'most_popular__clothing_and_apparel', + name: 'Clothing and accessories', + group: 'most_popular', + context: '', + }, + { + key: 'most_popular__consulting_services', + name: 'Consulting', + group: 'most_popular', + context: '', + }, + { + key: 'retail', + name: 'Retail', + items: [ + 'retail__software', + 'retail__clothing_and_apparel', + 'retail__convenience_stores', + 'retail__beauty_products', + ], + }, + { + key: 'retail__software', + name: 'Software', + group: 'retail', + context: + 'app business computer digital electronic hardware lease maintenance personal processing product program programming repair saas sell software retail', + }, + { + key: 'retail__clothing_and_apparel', + name: 'Clothing and accessories', + group: 'retail', + context: + 'accessories apparel baby children clothes clothing dress family infant men pant shirt short skirt t-shirt tee undergarment women retail', + }, + { + key: 'retail__convenience_stores', + name: 'Convenience stores', + group: 'retail', + context: + 'candy convenience dairy deli delicatessen drink fast food fruit gourmet grocery health market meal poultry preparation produce retail specialty supermarket vegetable vitamin retail', + }, + { + key: 'retail__beauty_products', + name: 'Beauty products', + group: 'retail', + context: + 'barber beauty cosmetic make make-up makeup moisture moisturizer retail serum skin skincare treatment up retail', + }, +]; + +mocked( getMccsFlatList ).mockReturnValue( mccsFlatList ); describe( 'BusinessDetails', () => { it( 'renders and updates fields data when they are changed', () => { @@ -116,7 +185,9 @@ describe( 'BusinessDetails', () => { user.click( companyStructureField ); user.click( screen.getByText( 'Single member LLC' ) ); - // TODO [GH-4853]: Add mcc field test + const mccField = screen.getByText( strings.placeholders.mcc ); + user.click( mccField ); + user.click( screen.getByText( 'Popular Software' ) ); expect( businessNameField ).toHaveValue( 'John Doe LLC' ); expect( urlField ).toHaveValue( 'https://johndoe.com' ); @@ -125,5 +196,6 @@ describe( 'BusinessDetails', () => { expect( companyStructureField ).toHaveTextContent( 'Single member LLC' ); + expect( mccField ).toHaveTextContent( 'Popular Software' ); } ); } ); diff --git a/client/onboarding-prototype/steps/test/loading.tsx b/client/onboarding-prototype/steps/test/loading.tsx index 4496574c0a0..62c5f8bda1e 100644 --- a/client/onboarding-prototype/steps/test/loading.tsx +++ b/client/onboarding-prototype/steps/test/loading.tsx @@ -31,6 +31,12 @@ jest.mock( '../../context', () => ( { } ) ), } ) ); +jest.mock( 'components/stepper', () => ( { + useStepperContext: jest.fn( () => ( { + currentStep: 'loading', + } ) ), +} ) ); + const checkLinkToContainNecessaryParams = ( link: string ) => { expect( link ).toContain( 'prefill' ); expect( link ).toContain( 'progressive' ); @@ -69,7 +75,7 @@ describe( 'Loading', () => { data = { country: 'US', business_type: 'individual', - mcc: 'computers_peripherals_and_software', + mcc: 'software_services', annual_revenue: 'less_than_250k', go_live_timeframe: 'within_1month', }; @@ -79,7 +85,7 @@ describe( 'Loading', () => { data: [], } ); - render( ); + render( ); await waitFor( () => { expect( apiFetch ).toHaveBeenCalledWith( { @@ -87,7 +93,7 @@ describe( 'Loading', () => { business: { country: 'US', type: 'individual', - mcc: 'computers_peripherals_and_software', + mcc: 'software_services', annual_revenue: 'less_than_250k', go_live_timeframe: 'within_1month', }, @@ -104,7 +110,7 @@ describe( 'Loading', () => { data = { country: 'GB', business_type: 'individual', - mcc: 'computers_peripherals_and_software', + mcc: 'software_services', annual_revenue: 'less_than_250k', go_live_timeframe: 'within_1month', }; @@ -114,7 +120,7 @@ describe( 'Loading', () => { data: [], } ); - render( ); + render( ); await waitFor( () => { expect( apiFetch ).toHaveBeenCalledWith( { @@ -122,7 +128,7 @@ describe( 'Loading', () => { business: { country: 'GB', type: 'individual', - mcc: 'computers_peripherals_and_software', + mcc: 'software_services', annual_revenue: 'less_than_250k', go_live_timeframe: 'within_1month', }, diff --git a/client/onboarding-prototype/steps/test/personal-details.tsx b/client/onboarding-prototype/steps/test/personal-details.tsx index 8ef237d0a7e..603cc8192c2 100644 --- a/client/onboarding-prototype/steps/test/personal-details.tsx +++ b/client/onboarding-prototype/steps/test/personal-details.tsx @@ -12,8 +12,18 @@ import PersonalDetails from '../personal-details'; import { OnboardingContextProvider } from '../../context'; import strings from '../../strings'; +declare const global: { + wcpaySettings: { + connect: { country: string }; + }; +}; + describe( 'PersonalDetails', () => { it( 'renders and updates fields data when they are changed', () => { + global.wcpaySettings = { + connect: { country: 'US' }, + }; + render( diff --git a/client/onboarding-prototype/strings.tsx b/client/onboarding-prototype/strings.tsx index ad841dbd09c..83cbcf3424c 100644 --- a/client/onboarding-prototype/strings.tsx +++ b/client/onboarding-prototype/strings.tsx @@ -17,7 +17,7 @@ export default { ), live: { label: __( - 'I’d like to set up payments for my store', + 'I’d like to set up payments on my own store', 'woocommerce-payments' ), note: __( @@ -27,7 +27,7 @@ export default { }, test: { label: __( - 'I’d like to set up test payments', + 'I’m building a store for someone else and would like to test payments', 'woocommerce-payments' ), note: __( @@ -37,13 +37,16 @@ export default { }, }, personal: { - heading: __( 'Tell us about yourself', 'woocommerce-payments' ), + heading: __( + 'First, you’ll need to create an account', + 'woocommerce-payments' + ), subheading: __( 'The information below should reflect that of the business owner or a significant shareholder.', 'woocommerce-payments' ), notice: __( - 'We will use this email address to contact you with any important notifications or information related to your account.', + 'We’ll use the email address to contact you with any important notifications related to your account, and the phone number will only be used to protect your account with two-factor authentication.', 'woocommerce-payments' ), }, @@ -53,27 +56,27 @@ export default { 'woocommerce-payments' ), subheading: __( - 'We will use these details to enable payments for your store.', + 'We’ll use these details to enable payments for your store.', 'woocommerce-payments' ), }, store: { heading: __( - 'Tell us more about your business', + 'Please share a few more details', 'woocommerce-payments' ), subheading: __( - 'This information will assist us in getting you set up quickly.', + 'This info will help us speed up the set up process.', 'woocommerce-payments' ), }, loading: { heading: __( - 'Let’s get you setup for payments', + 'Let’s get you set up for payments', 'woocommerce-payments' ), subheading: __( - 'All you need is to confirm your identity with our partner', + 'Confirm your identity with our partner', 'woocommerce-payments' ), }, @@ -123,7 +126,10 @@ export default { url: __( 'Please provide a valid website', 'woocommerce-payments' ), }, placeholders: { - country: __( 'Select a location', 'woocommerce-payments' ), + country: __( + 'Select the primary country of your business', + 'woocommerce-payments' + ), business_type: __( 'Select the legal structure of your business', 'woocommerce-payments' @@ -132,7 +138,10 @@ export default { 'Select the legal category of your business', 'woocommerce-payments' ), - mcc: __( 'Please select your industry', 'woocommerce-payments' ), + mcc: __( + 'Select the primary industry of your business', + 'woocommerce-payments' + ), annual_revenue: __( 'Select your annual revenue', 'woocommerce-payments' @@ -154,4 +163,5 @@ export default { more_than_6months: __( '6+ months', 'woocommerce-payments' ), }, continue: __( 'Continue', 'woocommerce-payments' ), + back: __( 'Back', 'woocommerce-payments' ), }; diff --git a/client/onboarding-prototype/style.scss b/client/onboarding-prototype/style.scss index 05d31c9a2ae..ea3accf6b34 100644 --- a/client/onboarding-prototype/style.scss +++ b/client/onboarding-prototype/style.scss @@ -1,10 +1,119 @@ -.wcpay-onboarding-prototype { - a { - text-decoration: none; - } +body.wcpay-onboarding-prototype__body { + background-color: #fff; + + .wcpay-onboarding-prototype { + a { + text-decoration: none; + } + + .stepper { + &__progress { + position: fixed; + top: 0; + left: 0; + height: 8px; + background-color: $gutenberg-blue; + z-index: 11; + transition: width 250ms; + } + + &__nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 80px; + padding-top: 8px; + display: grid; + grid-template-columns: 102px 1fr 102px; + align-items: stretch; + background-color: #fff; + z-index: 10; + + &-button { + cursor: pointer; + background-color: transparent; + border: none; + display: flex; + align-items: center; + padding: $gap-large; + font-size: 14px; + + .gridicons-chevron-left { + margin-right: 2px; + } + + &:last-child { + justify-self: end; + } + } + + &-logo { + justify-self: center; + align-self: center; + height: 28px; + } + } + + &__wrapper { + max-width: 600px; + margin: 88px auto 0; + display: flex; + flex-direction: column; + align-items: center; + } + + &__heading { + @include title-large; + color: $studio-gray-100; + text-align: center; + } + + &__subheading { + font-size: 16px; + line-height: 24px; + font-weight: 400; + text-align: center; + color: $studio-gray-60; + margin: $gap-small 0 $gap-largest; + } + + &__content { + max-width: 400px; + + @media screen and ( min-width: $break-mobile ) { + width: 400px; + } + } + + &__cta { + display: block; + width: 100%; + margin-top: $gap-larger; + } + } + + .loading-step { + max-width: 520px; + margin: 50% auto 0; + } + + .onboarding-mode__note { + background-color: $wp-blue-0; + padding: $gap-small $gap; + } + + .wcpay-banner-notice { + margin: 0; + } + + .components-base-control, + .components-custom-select-control { + margin-bottom: $gap-large; + } - .onboarding-mode__note { - background-color: $wp-blue-0; - padding: $gap-small $gap; + .components-form-field__error { + margin: -$gap 0 $gap; + } } } diff --git a/client/onboarding-prototype/test/form.tsx b/client/onboarding-prototype/test/form.tsx index 2fb13e1aff2..4e2a1d1db68 100644 --- a/client/onboarding-prototype/test/form.tsx +++ b/client/onboarding-prototype/test/form.tsx @@ -12,13 +12,23 @@ import { OnboardingForm, OnboardingTextField, OnboardingSelectField, + OnboardingPhoneNumberField, } from '../form'; +declare const global: { + wcpaySettings: { + connect: { country: string }; + }; +}; + let nextStep = jest.fn(); let data = {}; let errors = {}; +let temp = {}; + let setData = jest.fn(); let setTouched = jest.fn(); +let setTemp = jest.fn(); let validate = jest.fn(); let error = jest.fn(); @@ -26,8 +36,10 @@ jest.mock( '../context', () => ( { useOnboardingContext: jest.fn( () => ( { data, errors, + temp, setData, setTouched, + setTemp, } ) ), } ) ); @@ -49,10 +61,16 @@ describe( 'Progressive Onboarding Prototype Form', () => { nextStep = jest.fn(); data = {}; errors = {}; + temp = {}; setData = jest.fn(); setTouched = jest.fn(); + setTemp = jest.fn(); validate = jest.fn(); error = jest.fn(); + + global.wcpaySettings = { + connect: { country: 'US' }, + }; } ); it( 'calls nextStep when the form is submitted by click and there are no errors', () => { @@ -154,5 +172,40 @@ describe( 'Progressive Onboarding Prototype Form', () => { } ); expect( validate ).toHaveBeenCalledWith( 'individual' ); } ); + + describe( 'OnboardingPhoneNumberField', () => { + it( 'renders component with provided props ', () => { + data = { phone: '+123' }; + error.mockReturnValue( 'error message' ); + + render( ); + + const textField = screen.getByLabelText( + 'What’s your mobile phone number?' + ); + const errorMessage = screen.getByText( 'error message' ); + + expect( textField ).toHaveValue( '23' ); + expect( errorMessage ).toBeInTheDocument(); + } ); + + it( 'calls setTemp, setData and validate on change', () => { + render( ); + + const textField = screen.getByLabelText( + 'What’s your mobile phone number?' + ); + userEvent.type( textField, '23' ); + + expect( setTemp ).toHaveBeenCalledWith( { + phoneCountryCode: 'US', + } ); + + expect( setData ).toHaveBeenCalledWith( { + phone: '+123', + } ); + expect( validate ).toHaveBeenCalledWith( '+123' ); + } ); + } ); } ); } ); diff --git a/client/onboarding-prototype/tracking.ts b/client/onboarding-prototype/tracking.ts new file mode 100644 index 00000000000..3a083eeb64b --- /dev/null +++ b/client/onboarding-prototype/tracking.ts @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import { useEffect } from 'react'; + +/** + * Internal dependencies + */ + +import { useStepperContext } from 'components/stepper'; +import { useOnboardingContext } from './context'; +import { OnboardingFields } from './types'; +import wcpayTracks from 'tracks'; + +const trackedSteps: Set< string > = new Set(); +let startTime: number; +let stepStartTime: number; + +const elapsed = ( time: number ) => Math.round( ( Date.now() - time ) / 1000 ); +const stepElapsed = () => { + const result = elapsed( stepStartTime ); + stepStartTime = Date.now(); + return result; +}; + +export const trackStarted = (): void => { + startTime = stepStartTime = Date.now(); + + wcpayTracks.recordEvent( wcpayTracks.events.ONBOARDING_FLOW_STARTED, {} ); +}; + +export const trackModeSelected = ( mode: string ): void => { + wcpayTracks.recordEvent( wcpayTracks.events.ONBOARDING_FLOW_MODE_SELECTED, { + mode, + elapsed: stepElapsed(), + } ); +}; + +export const trackStepCompleted = ( step: string ): void => { + // We only track a completed step once. + if ( trackedSteps.has( step ) ) return; + + wcpayTracks.recordEvent( + wcpayTracks.events.ONBOARDING_FLOW_STEP_COMPLETED, + { + step, + elapsed: stepElapsed(), + } + ); + trackedSteps.add( step ); +}; + +export const trackRedirected = ( isEligible: boolean ): void => { + wcpayTracks.recordEvent( wcpayTracks.events.ONBOARDING_FLOW_REDIRECTED, { + is_po_eligible: isEligible, + elapsed: elapsed( startTime ), + } ); +}; + +export const trackEligibilityModalClosed = ( + action: 'dismiss' | 'setup_deposits' | 'enable_payments_only' +): void => + wcpayTracks.recordEvent( + wcpayTracks.events.ONBOARDING_FLOW_ELIGIBILITY_MODAL_CLOSED, + { action } + ); + +export const useTrackAbandoned = (): { + trackAbandoned: ( method: 'hide' | 'exit' ) => void; + removeTrackListener: () => void; +} => { + const { errors, touched } = useOnboardingContext(); + const { currentStep: step } = useStepperContext(); + + const trackEvent = ( method = 'hide' ) => { + const event = + method === 'hide' + ? wcpayTracks.events.ONBOARDING_FLOW_HIDDEN + : wcpayTracks.events.ONBOARDING_FLOW_EXITED; + const errored = Object.keys( errors ).filter( + ( field ) => touched[ field as keyof OnboardingFields ] + ); + + wcpayTracks.recordEvent( event, { + step, + errored, + elapsed: elapsed( startTime ), + } ); + }; + + const listener = () => { + if ( document.visibilityState === 'hidden' ) { + trackEvent(); + } + }; + + useEffect( () => { + document.addEventListener( 'visibilitychange', listener ); + return () => { + document.removeEventListener( 'visibilitychange', listener ); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ step, errors, touched ] ); + + return { + trackAbandoned: ( method: string ) => { + trackEvent( method ); + document.removeEventListener( 'visibilitychange', listener ); + }, + removeTrackListener: () => + document.removeEventListener( 'visibilitychange', listener ), + }; +}; diff --git a/client/onboarding-experiment/translations/descriptions.tsx b/client/onboarding-prototype/translations/descriptions.tsx similarity index 100% rename from client/onboarding-experiment/translations/descriptions.tsx rename to client/onboarding-prototype/translations/descriptions.tsx diff --git a/client/onboarding-prototype/types.ts b/client/onboarding-prototype/types.ts index da4abf56835..f2db38b8475 100644 --- a/client/onboarding-prototype/types.ts +++ b/client/onboarding-prototype/types.ts @@ -37,3 +37,34 @@ export interface EligibleData { go_live_timeframe: string; }; } + +export type TempData = { + phoneCountryCode?: string; +}; + +export interface Country { + key: string; + name: string; + types: BusinessType[]; +} + +export interface BusinessType { + key: string; + name: string; + description: string; + structures: BusinessStructure[]; +} + +export interface BusinessStructure { + key: string; + name: string; +} + +export interface MccsDisplayTreeItem { + id: string; + type: string; + title: string; + items?: MccsDisplayTreeItem[]; + mcc?: number; + keywords?: string[]; +} diff --git a/client/onboarding-prototype/utils.ts b/client/onboarding-prototype/utils.ts index 2403a421997..bcf9c6dc851 100644 --- a/client/onboarding-prototype/utils.ts +++ b/client/onboarding-prototype/utils.ts @@ -3,9 +3,79 @@ */ import { set, toPairs } from 'lodash'; +/** + * Internal dependencies + */ +import businessTypeDescriptionStrings from './translations/descriptions'; +import { ListItem } from 'components/grouped-select-control'; +import { Country } from './types'; + export const fromDotNotation = ( record: Record< string, unknown > ): Record< string, unknown > => toPairs( record ).reduce( ( result, [ key, value ] ) => { return value != null ? set( result, key, value ) : result; }, {} ); + +export const getBusinessTypes = (): Country[] => { + const data = wcpaySettings?.onboardingFieldsData?.business_types; + + return ( + ( data || [] ) + .map( ( country ) => ( { + ...country, + types: country.types.map( ( type ) => ( { + ...type, + description: businessTypeDescriptionStrings[ country.key ] + ? businessTypeDescriptionStrings[ country.key ][ + type.key + ] + : businessTypeDescriptionStrings.generic[ type.key ], + } ) ), + } ) ) + .sort( ( a, b ) => a.name.localeCompare( b.name ) ) || [] + ); +}; + +export const getMccsFlatList = (): ListItem[] => { + const data = wcpaySettings?.onboardingFieldsData?.mccs_display_tree; + + // Right now we support only two levels (top-level groups and items in those groups). + // For safety, we will discard anything else like top-level items or sub-groups. + const normalizedData = ( data || [] ).filter( ( group ) => { + if ( ! group?.items ) { + return false; + } + + const groupItems = + group.items?.filter( ( item ) => ! item?.items ) || []; + + return groupItems.length; + } ); + + return normalizedData.reduce( ( acc, group ): ListItem[] => { + const groupItems = + group.items?.map( + ( item ): ListItem => { + return { + key: item.id, + name: item.title, + group: group.id, + context: item?.keywords + ? item.keywords.join( ' ' ) + : '', + }; + } + ) || []; + + return [ + ...acc, + { + key: group.id, + name: group.title, + items: groupItems.map( ( item ) => item.key ), + }, + ...groupItems, + ]; + }, [] as ListItem[] ); +}; diff --git a/client/onboarding-prototype/validation.ts b/client/onboarding-prototype/validation.ts index a1c9ca330bd..d858b83e3d5 100644 --- a/client/onboarding-prototype/validation.ts +++ b/client/onboarding-prototype/validation.ts @@ -12,27 +12,49 @@ import { OnboardingFields } from './types'; const isValid = ( name: keyof OnboardingFields, value?: string ): boolean => { if ( ! value ) return false; - if ( name === 'email' ) return value.includes( '@' ); - return true; + + switch ( name ) { + case 'email': + return value.includes( '@' ); + case 'phone': + return /^\+\d{7,}$/.test( value ); + default: + return true; + } }; // TS is smart enough to infer the return type here. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const useValidation = ( name: keyof OnboardingFields ) => { - const { errors, setErrors, touched, setTouched } = useOnboardingContext(); + const { + data, + errors, + setErrors, + touched, + setTouched, + } = useOnboardingContext(); - const validate = ( value?: string ) => { + const validate = ( value: string | undefined = data[ name ] ) => { if ( ! touched[ name ] ) setTouched( { [ name ]: true } ); + const error = isValid( name, value ) ? undefined : ( strings.errors as Record< string, string > )[ name ] || strings.errors.generic; + setErrors( { [ name ]: error } ); }; useEffect( () => { + // Validate on mount. validate(); - setTouched( { [ name ]: false } ); + + // Set touched to false if the field is empty. + if ( ! data[ name ] ) setTouched( { [ name ]: false } ); + + // Clean up the error when the field is unmounted. + return () => setErrors( { [ name ]: undefined } ); + // We only want to run this once, so we disable the exhaustive deps rule. // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); diff --git a/client/overview/index.js b/client/overview/index.js index 92de64b83e8..4ebf8e1dec7 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -15,7 +15,6 @@ import Page from 'components/page'; import { TestModeNotice, topics } from 'components/test-mode-notice'; import AccountStatus from 'components/account-status'; import ActiveLoanSummary from 'components/active-loan-summary'; -import DepositsInformation from 'components/deposits-information'; import DepositsOverview from 'components/deposits-overview'; import ErrorBoundary from 'components/error-boundary'; import TaskList from './task-list'; @@ -23,11 +22,34 @@ import { getTasks, taskSort } from './task-list/tasks'; import InboxNotifications from './inbox-notifications'; import ConnectionSuccessNotice from './connection-sucess-notice'; import SetupRealPayments from './setup-real-payments'; +import ProgressiveOnboardingEligibilityModal from './modal/progressive-onboarding-eligibility'; import JetpackIdcNotice from 'components/jetpack-idc-notice'; import AccountBalances from 'components/account-balances'; import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; import { useSettings } from 'wcpay/data'; import './style.scss'; +import React from 'react'; + +const OverviewPageError = () => { + const queryParams = getQuery(); + const showLoginError = '1' === queryParams[ 'wcpay-login-error' ]; + if ( ! wcpaySettings.errorMessage && ! showLoginError ) { + return null; + } + return ( + + { wcpaySettings.errorMessage || + __( + 'There was a problem redirecting you to the account dashboard. Please try again.', + 'woocommerce-payments' + ) } + + ); +}; const OverviewPage = () => { const { @@ -35,7 +57,7 @@ const OverviewPage = () => { overviewTasksVisibility, showUpdateDetailsTask, wpcomReconnectUrl, - featureFlags: { accountOverviewTaskList, simplifyDepositsUi }, + featureFlags: { accountOverviewTaskList }, } = wcpaySettings; const numDisputesNeedingResponse = parseInt( wcpaySettings.numDisputesNeedingResponse, 10 ) || 0; @@ -55,10 +77,14 @@ const OverviewPage = () => { const showConnectionSuccess = '1' === queryParams[ 'wcpay-connection-success' ]; - const showLoginError = '1' === queryParams[ 'wcpay-login-error' ]; const showLoanOfferError = '1' === queryParams[ 'wcpay-loan-offer-error' ]; const showServerLinkError = '1' === queryParams[ 'wcpay-server-link-error' ]; + const showProgressiveOnboardingEligibilityModal = + showConnectionSuccess && + accountStatus.progressiveOnboarding.isEnabled && + ! accountStatus.progressiveOnboarding.isComplete && + 'pending_verification' !== accountStatus.status; const accountRejected = accountStatus.status && accountStatus.status.startsWith( 'rejected' ); @@ -85,18 +111,7 @@ const OverviewPage = () => { return ( - { showLoginError && ( - - { __( - 'There was a problem redirecting you to the account dashboard. Please try again.', - 'woocommerce-payments' - ) } - - ) } + @@ -120,24 +135,18 @@ const OverviewPage = () => { - { wcpaySettings.isFraudProtectionSettingsEnabled && ( - - - - ) } + + + { showConnectionSuccess && } { ! accountRejected && ( - { simplifyDepositsUi ? ( - <> - - - - ) : ( - - ) } + <> + + + ) } @@ -176,6 +185,12 @@ const OverviewPage = () => { ) } + + { showProgressiveOnboardingEligibilityModal && ( + + + + ) } ); }; diff --git a/client/overview/modal/progressive-onboarding-eligibility/index.tsx b/client/overview/modal/progressive-onboarding-eligibility/index.tsx new file mode 100644 index 00000000000..547b98e1a02 --- /dev/null +++ b/client/overview/modal/progressive-onboarding-eligibility/index.tsx @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import React, { useEffect, useState } from 'react'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; +import { Button, Modal } from '@wordpress/components'; +import { Icon, store, widget, tool } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { trackEligibilityModalClosed } from 'onboarding-prototype/tracking'; +import HeaderImg from 'assets/images/illustrations/po-eligibility.svg'; +import './style.scss'; + +const ProgressiveOnboardingEligibilityModal: React.FC = () => { + const [ modalVisible, setModalVisible ] = useState( true ); + + const handleSetup = () => { + trackEligibilityModalClosed( 'setup_deposits' ); + window.location.href = addQueryArgs( wcpaySettings.connectUrl, { + collect_payout_requirements: true, + } ); + }; + + const handlePaymentsOnly = () => { + trackEligibilityModalClosed( 'enable_payments_only' ); + setModalVisible( false ); + }; + + const handleDismiss = () => { + trackEligibilityModalClosed( 'dismiss' ); + setModalVisible( false ); + }; + + // Workaround to remove Modal header from the modal until `hideHeader` prop can be used. + useEffect( () => { + document + .querySelector( + '.wcpay-progressive-onboarding-eligibility-modal .components-modal__header-heading-container' + ) + ?.remove(); + }, [] ); + + if ( ! modalVisible ) return null; + + return ( + +
    + Header +
    +

    + { __( + 'You’re eligible to start selling now and fast-track the setup process.', + 'woocommerce-payments' + ) } +

    +

    + { __( + 'Start selling now with these benefits or continue the process to set up deposits.', + 'woocommerce-payments' + ) } +

    +
    +
    + +

    + { __( + 'Start selling instantly', + 'woocommerce-payments' + ) } +

    + { __( + 'Woo Payments enables you to start processing payments right away.', + 'woocommerce-payments' + ) } +
    +
    + +

    + { __( 'Quick and easy setup', 'woocommerce-payments' ) } +

    + { __( + 'The setup process is super simple and ensures your store is ready to accept payments.', + 'woocommerce-payments' + ) } +
    +
    + +

    + { __( 'Flexible process', 'woocommerce-payments' ) } +

    + { __( + 'You have a $5,000 balance limit or 30 days from your first transaction to verify and set up deposits in your account.', + 'woocommerce-payments' + ) } +
    +
    +
    + + +
    +
    + ); +}; + +export default ProgressiveOnboardingEligibilityModal; diff --git a/client/overview/modal/progressive-onboarding-eligibility/style.scss b/client/overview/modal/progressive-onboarding-eligibility/style.scss new file mode 100644 index 00000000000..151ae196136 --- /dev/null +++ b/client/overview/modal/progressive-onboarding-eligibility/style.scss @@ -0,0 +1,81 @@ +.wcpay-progressive-onboarding-eligibility-modal { + .components-modal__content { + box-sizing: border-box; + max-width: 700px; + margin: 0 auto; + padding: $gap-larger; + } + + .components-modal__header { + height: 0; + + .components-button { + position: absolute; + left: initial; + top: $gap-smaller; + right: $gap-smaller; + } + } + + &__image { + margin: -$gap-larger; + margin-bottom: $gap-larger; + background-color: $studio-gray-0; + + img { + display: block; + max-width: 100%; + max-height: 200px; + margin: 0 auto; + } + } + + &__heading { + @include wp-title-large; + text-align: center; + margin-bottom: 0; + } + + &__subheading { + @include wc-body-large; + text-align: center; + font-weight: normal; + color: $gray-60; + margin-bottom: $gap-largest; + } + + &__benefits { + display: grid; + grid-template-columns: repeat( 3, 1fr ); + column-gap: $gap-largest; + text-align: center; + fill: $studio-woocommerce-purple-50; + @include wp-label; + color: $gray-700; + + &__subtitle { + @include wp-subtitle; + text-align: center; + margin-bottom: $gap-smaller; + } + + @media screen and ( max-width: $break-small ) { + grid-template-columns: 1fr; + row-gap: $gap-large; + } + } + + &__footer { + text-align: center; + margin-top: $gap-large; + + & :first-child { + margin-right: $gap-smaller; + } + + button { + margin-top: $gap; + padding: $gap-smaller $gap; + } + } +} diff --git a/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx b/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx new file mode 100644 index 00000000000..712d645ae45 --- /dev/null +++ b/client/overview/modal/progressive-onboarding-eligibility/test/index.test.tsx @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import ProgressiveOnboardingEligibilityModal from '../index'; + +declare const global: { + wcpaySettings: { + connectUrl: string; + }; +}; + +describe( 'Progressive Onboarding Eligibility Modal', () => { + it( 'modal is open by default', () => { + render( ); + + const queryHeading = () => + screen.queryByRole( 'heading', { + name: + 'You’re eligible to start selling now and fast-track the setup process.', + } ); + + expect( queryHeading() ).toBeInTheDocument(); + } ); + + it( 'closes modal when enable button is clicked', () => { + render( ); + + user.click( + screen.getByRole( 'button', { + name: 'Enable payments only', + } ) + ); + + expect( + screen.queryByRole( 'heading', { + name: + 'You’re eligible to start selling now and fast-track the setup process.', + } ) + ).not.toBeInTheDocument(); + } ); + + it( 'calls `handleSetup` when 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', { + name: 'Set up payments and deposits', + } ) + ); + + expect( window.location.href ).toBe( + `https://wcpay.test/connect?collect_payout_requirements=true` + ); + } ); +} ); diff --git a/client/overview/modal/update-business-details/test/__snapshots__/index.tsx.snap b/client/overview/modal/update-business-details/test/__snapshots__/index.tsx.snap index e1e08f6c52a..c07e949b85f 100644 --- a/client/overview/modal/update-business-details/test/__snapshots__/index.tsx.snap +++ b/client/overview/modal/update-business-details/test/__snapshots__/index.tsx.snap @@ -32,9 +32,10 @@ exports[`Overview: update business details modal renders correctly when opened f
    -

    - No data to display -

    +
      +
    • +

      + March 27, 2023 +

      +
        +
      • +
        +
        +
        + + + + + + + Payment was screened by your fraud filters and blocked. + +
        + + 6:09am + +
        +
        + +

        + Block if the purchase price is not in your defined range +

        +
        +
        +
      • +
      +
      +
    • +
    diff --git a/client/payment-details/readers/test/__snapshots__/index.js.snap b/client/payment-details/readers/test/__snapshots__/index.js.snap index 7bc5b8fd0d8..0cbdaae354c 100644 --- a/client/payment-details/readers/test/__snapshots__/index.js.snap +++ b/client/payment-details/readers/test/__snapshots__/index.js.snap @@ -37,7 +37,7 @@ exports[`RenderPaymentCardReaderChargeDetails renders reader charges 1`] = ` > diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index f5ec29a324c..58f2797d740 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -379,7 +379,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
    { createInterpolateElement( __( - 'You must capture this charge within the next ', + 'You must capture this charge within the next', 'woocommerce-payments' ), { @@ -392,7 +392,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { /> ), } - ) } + ) }{ ' ' } capture - this charge within the next + this charge within the next + @@ -517,7 +518,8 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th > capture - this charge within the next + this charge within the next + diff --git a/client/payment-details/summary/test/index.tsx b/client/payment-details/summary/test/index.tsx index 3e899686625..eaee919c7e7 100755 --- a/client/payment-details/summary/test/index.tsx +++ b/client/payment-details/summary/test/index.tsx @@ -29,7 +29,6 @@ declare const global: { featureFlags: { isAuthAndCaptureEnabled: boolean; }; - isFraudProtectionSettingsEnabled: boolean; }; }; @@ -131,7 +130,6 @@ describe( 'PaymentDetailsSummary', () => { precision: 2, }, }, - isFraudProtectionSettingsEnabled: false, }; } ); @@ -257,8 +255,6 @@ describe( 'PaymentDetailsSummary', () => { } ); test( 'renders the fraud outcome buttons', () => { - global.wcpaySettings.isFraudProtectionSettingsEnabled = true; - mockUseAuthorization.mockReturnValueOnce( { authorization: { captured: false, diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index d50766fa3b0..33d14bc5ae3 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -510,8 +510,6 @@ export const composeFeeBreakdown = ( event ) => { }; const getManualFraudOutcomeTimelineItem = ( event, status ) => { - if ( ! wcpaySettings.isFraudProtectionSettingsEnabled ) return []; - const isBlock = 'block' === status; const headline = isBlock @@ -561,8 +559,6 @@ const buildAutomaticFraudOutcomeRuleset = ( event ) => { }; const getAutomaticFraudOutcomeTimelineItem = ( event, status ) => { - if ( ! wcpaySettings.isFraudProtectionSettingsEnabled ) return []; - const isBlock = 'block' === status; const headline = isBlock @@ -582,9 +578,12 @@ const getAutomaticFraudOutcomeTimelineItem = ( event, status ) => { ); return [ - getMainTimelineItem( event, headline, icon, [ - buildAutomaticFraudOutcomeRuleset( event ), - ] ), + getMainTimelineItem( + event, + headline, + icon, + buildAutomaticFraudOutcomeRuleset( event ) + ), ]; }; diff --git a/client/payment-details/timeline/test/__snapshots__/index.js.snap b/client/payment-details/timeline/test/__snapshots__/index.js.snap index 000c972b975..45efca0b2a6 100644 --- a/client/payment-details/timeline/test/__snapshots__/index.js.snap +++ b/client/payment-details/timeline/test/__snapshots__/index.js.snap @@ -47,7 +47,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -125,7 +125,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -217,7 +217,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -256,7 +256,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -334,7 +334,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -388,7 +388,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -466,7 +466,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -520,7 +520,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -559,7 +559,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -610,7 +610,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -745,7 +745,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -830,7 +830,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -890,7 +890,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -968,7 +968,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -1046,7 +1046,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -1085,7 +1085,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -1136,7 +1136,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -1175,7 +1175,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon > @@ -1332,7 +1332,7 @@ exports[`PaymentDetailsTimeline renders subscription fee correctly 1`] = ` > @@ -1410,7 +1410,7 @@ exports[`PaymentDetailsTimeline renders subscription fee correctly 1`] = ` > @@ -1471,7 +1471,7 @@ exports[`PaymentDetailsTimeline renders subscription fee correctly 1`] = ` > @@ -1510,7 +1510,7 @@ exports[`PaymentDetailsTimeline renders subscription fee correctly 1`] = ` > diff --git a/client/payment-details/timeline/test/__snapshots__/map-events.js.snap b/client/payment-details/timeline/test/__snapshots__/map-events.js.snap index 8d0733a6b44..79b611b1598 100644 --- a/client/payment-details/timeline/test/__snapshots__/map-events.js.snap +++ b/client/payment-details/timeline/test/__snapshots__/map-events.js.snap @@ -163,16 +163,16 @@ Array [ Object { "body": Array [], "date": 2020-04-04T13:51:06.000Z, - "headline": "21,64 $ USD will be deducted from a future deposit.", + "headline": "21,64 $ USD will be deducted from a future deposit.", "icon": , }, Object { "body": Array [ - "1,00 EUR → 1,20222 USD: 21,64 $ USD", + "1,00 EUR → 1,20222 USD: 21,64 $ USD", "", ], "date": 2020-04-04T13:51:06.000Z, - "headline": "A payment of 18,00 € EUR was successfully refunded.", + "headline": "A payment of 18,00 € EUR was successfully refunded.", "icon": , @@ -191,16 +191,16 @@ Array [ Object { "body": Array [], "date": 2020-04-03T18:58:01.000Z, - "headline": "6,00 $ USD will be deducted from a future deposit.", + "headline": "6,00 $ USD will be deducted from a future deposit.", "icon": , }, Object { "body": Array [ - "1,00 EUR → 1,2 USD: 6,00 $ USD", + "1,00 EUR → 1,2 USD: 6,00 $ USD", "Acquirer Reference Number (ARN) 4785767637658864", ], "date": 2020-04-03T18:58:01.000Z, - "headline": "A payment of 5,00 € EUR was successfully refunded.", + "headline": "A payment of 5,00 € EUR was successfully refunded.", "icon": , diff --git a/client/payment-methods/test/__snapshots__/activation-modal.test.js.snap b/client/payment-methods/test/__snapshots__/activation-modal.test.js.snap index b7251b32038..0890ce571d8 100644 --- a/client/payment-methods/test/__snapshots__/activation-modal.test.js.snap +++ b/client/payment-methods/test/__snapshots__/activation-modal.test.js.snap @@ -73,9 +73,10 @@ exports[`Activation Modal matches the snapshot 1`] = ` diff --git a/client/payment-methods/test/__snapshots__/delete-modal.test.js.snap b/client/payment-methods/test/__snapshots__/delete-modal.test.js.snap index 4092734f8b1..d8c3a79d929 100644 --- a/client/payment-methods/test/__snapshots__/delete-modal.test.js.snap +++ b/client/payment-methods/test/__snapshots__/delete-modal.test.js.snap @@ -73,9 +73,10 @@ exports[`Activation Modal matches the snapshot 1`] = `
    @@ -209,4 +209,4 @@ const PlatformCheckoutFileUpload: React.FunctionComponent< PlatformCheckoutFileU ); }; -export default PlatformCheckoutFileUpload; +export default WooPayFileUpload; 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 f4103dd2c37..e7be0844b03 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 @@ -23,7 +23,7 @@ import { usePaymentRequestButtonSize, usePaymentRequestButtonTheme, usePaymentRequestEnabledSettings, - usePlatformCheckoutEnabledSettings, + useWooPayEnabledSettings, } from 'wcpay/data'; const makeButtonSizeText = ( string ) => @@ -130,12 +130,10 @@ const GeneralPaymentRequestButtonSettings = ( { type } ) => { const [ buttonType, setButtonType ] = usePaymentRequestButtonType(); const [ size, setSize ] = usePaymentRequestButtonSize(); const [ theme, setTheme ] = usePaymentRequestButtonTheme(); - const [ isPlatformCheckoutEnabled ] = usePlatformCheckoutEnabledSettings(); + const [ isWooPayEnabled ] = useWooPayEnabledSettings(); const [ isPaymentRequestEnabled ] = usePaymentRequestEnabledSettings(); const { - featureFlags: { - platformCheckout: isPlatformCheckoutFeatureFlagEnabled, - }, + featureFlags: { woopay: isWooPayFeatureFlagEnabled }, } = useContext( WCPaySettingsContext ); const stripePromise = useMemo( () => { @@ -152,9 +150,9 @@ const GeneralPaymentRequestButtonSettings = ( { type } ) => { : __( 'WooPay button', 'woocommerce-payments' ); const showWarning = - isPlatformCheckoutEnabled && + isWooPayEnabled && isPaymentRequestEnabled && - isPlatformCheckoutFeatureFlagEnabled; + isWooPayFeatureFlagEnabled; return ( diff --git a/client/settings/express-checkout-settings/index.js b/client/settings/express-checkout-settings/index.js index 27c6374a33d..b6a4c476504 100644 --- a/client/settings/express-checkout-settings/index.js +++ b/client/settings/express-checkout-settings/index.js @@ -12,7 +12,7 @@ import './index.scss'; import SettingsSection from '../settings-section'; import { getPaymentSettingsUrl } from '../../utils'; import PaymentRequestSettings from './payment-request-settings'; -import PlatformCheckoutSettings from './platform-checkout-settings'; +import WooPaySettings from './woopay-settings'; import SettingsLayout from '../settings-layout'; import LoadableSettingsSection from '../loadable-settings-section'; import SaveSettingsSection from '../save-settings-section'; @@ -22,7 +22,7 @@ import ApplePay from 'assets/images/cards/apple-pay.svg?asset'; import GooglePay from 'assets/images/cards/google-pay.svg?asset'; const methods = { - platform_checkout: { + woopay: { title: 'WooPay', sections: [ { @@ -66,7 +66,7 @@ const methods = { ), }, ], - controls: ( props ) => , + controls: ( props ) => , }, payment_request: { title: 'Apple Pay / Google Pay', diff --git a/client/settings/express-checkout-settings/index.scss b/client/settings/express-checkout-settings/index.scss index 73c980dc230..7478420672e 100644 --- a/client/settings/express-checkout-settings/index.scss +++ b/client/settings/express-checkout-settings/index.scss @@ -59,7 +59,7 @@ box-sizing: border-box; } -.platform-checkout-settings { +.woopay-settings { &__custom-message-wrapper { max-width: 500px; position: relative; 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 31af7450808..61472d48c91 100644 --- a/client/settings/express-checkout-settings/payment-request-button-preview.js +++ b/client/settings/express-checkout-settings/payment-request-button-preview.js @@ -15,13 +15,13 @@ import { */ import { shouldUseGooglePayBrand } from 'payment-request/utils'; import InlineNotice from 'components/inline-notice'; -import { WoopayExpressCheckoutButton } from 'wcpay/checkout/platform-checkout/express-button/woopay-express-checkout-button'; +import { WoopayExpressCheckoutButton } from 'wcpay/checkout/woopay/express-button/woopay-express-checkout-button'; import { usePaymentRequestButtonSize, usePaymentRequestButtonTheme, usePaymentRequestButtonType, usePaymentRequestEnabledSettings, - usePlatformCheckoutEnabledSettings, + useWooPayEnabledSettings, } from '../../data'; /** @@ -68,7 +68,7 @@ const PaymentRequestButtonPreview = () => { const [ buttonType ] = usePaymentRequestButtonType(); const [ size ] = usePaymentRequestButtonSize(); const [ theme ] = usePaymentRequestButtonTheme(); - const [ isPlatformCheckoutEnabled ] = usePlatformCheckoutEnabledSettings(); + const [ isWooPayEnabled ] = useWooPayEnabledSettings(); const [ isPaymentRequestEnabled ] = usePaymentRequestEnabledSettings(); useEffect( () => { @@ -105,13 +105,13 @@ const PaymentRequestButtonPreview = () => { return ( <> - { ( isPlatformCheckoutEnabled || + { ( isWooPayEnabled || ( isPaymentRequestEnabled && paymentRequest ) ) && (
    - { isPlatformCheckoutEnabled && ( + { isWooPayEnabled && ( { ) }
    ) } - { ! isPlatformCheckoutEnabled && ! isPaymentRequestEnabled && ( + { ! isWooPayEnabled && ! isPaymentRequestEnabled && ( { __( 'To preview the express checkout buttons, ' + diff --git a/client/settings/express-checkout-settings/test/file-upload.test.js b/client/settings/express-checkout-settings/test/file-upload.test.js index 346b34d5425..08c3bb9b79f 100644 --- a/client/settings/express-checkout-settings/test/file-upload.test.js +++ b/client/settings/express-checkout-settings/test/file-upload.test.js @@ -8,13 +8,13 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import PlatformCheckoutFileUpload from '../file-upload'; +import WooPayFileUpload from '../file-upload'; jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn( () => ( { createErrorNotice: jest.fn() } ) ), } ) ); -describe( 'PlatformCheckoutFileUpload', () => { +describe( 'WooPayFileUpload', () => { beforeEach( () => { global.wcpaySettings = { restUrl: 'http://example.com/wp-json/', @@ -23,7 +23,7 @@ describe( 'PlatformCheckoutFileUpload', () => { it( 'should render replace and delete button with file', () => { const { container } = render( - { it( 'should not render replace and delete button without file', () => { const { container } = render( - ( { usePaymentRequestLocations: jest .fn() .mockReturnValue( [ [ true, true, true ], jest.fn() ] ), - usePlatformCheckoutEnabledSettings: jest - .fn() - .mockReturnValue( [ true, jest.fn() ] ), - usePlatformCheckoutCustomMessage: jest - .fn() - .mockReturnValue( [ 'test', jest.fn() ] ), - usePlatformCheckoutStoreLogo: jest - .fn() - .mockReturnValue( [ 'test', jest.fn() ] ), + useWooPayEnabledSettings: jest.fn().mockReturnValue( [ true, jest.fn() ] ), + useWooPayCustomMessage: jest.fn().mockReturnValue( [ 'test', jest.fn() ] ), + useWooPayStoreLogo: jest.fn().mockReturnValue( [ 'test', jest.fn() ] ), usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'buy' ] ), usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), - usePlatformCheckoutLocations: jest + useWooPayLocations: jest .fn() .mockReturnValue( [ [ true, true, true ], jest.fn() ] ), } ) ); @@ -126,8 +120,8 @@ describe( 'ExpressCheckoutSettings', () => { ).toBeInTheDocument(); } ); - test( 'renders platform checkout breadcrumbs', () => { - render( ); + test( 'renders woopay breadcrumbs', () => { + render( ); const linkToPayments = screen.getByRole( 'link', { name: 'WooCommerce Payments', @@ -138,8 +132,8 @@ describe( 'ExpressCheckoutSettings', () => { expect( breadcrumbs ).toContainElement( methodName ); } ); - test( 'renders platform checkout settings and confirm its checkbox label', () => { - render( ); + test( 'renders woopay settings and confirm its checkbox label', () => { + render( ); const label = screen.getByRole( 'checkbox', { name: 'Enable WooPay', @@ -148,7 +142,7 @@ describe( 'ExpressCheckoutSettings', () => { } ); test( 'renders WooPay express button appearance settings if feature flag is enabled and confirm its first heading', () => { - render( ); + render( ); expect( screen.queryByRole( 'heading', { @@ -160,7 +154,7 @@ describe( 'ExpressCheckoutSettings', () => { test( 'does not render WooPay express button appearance settings if feature flag is disabled', () => { global.wcpaySettings.featureFlags.woopayExpressCheckout = false; - render( ); + render( ); expect( screen.queryByRole( 'heading', { diff --git a/client/settings/express-checkout-settings/test/payment-request-button-preview.test.js b/client/settings/express-checkout-settings/test/payment-request-button-preview.test.js index aa2fed5cca1..3b6010566a2 100644 --- a/client/settings/express-checkout-settings/test/payment-request-button-preview.test.js +++ b/client/settings/express-checkout-settings/test/payment-request-button-preview.test.js @@ -34,7 +34,7 @@ jest.mock( 'wcpay/data', () => { return { __esModule: true, ...actual, - usePlatformCheckoutEnabledSettings: () => [ false, jest.fn() ], + useWooPayEnabledSettings: () => [ false, jest.fn() ], usePaymentRequestEnabledSettings: () => [ true, 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 23df1156aee..ded12e5de6a 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 @@ -17,7 +17,7 @@ import { usePaymentRequestButtonType, usePaymentRequestButtonSize, usePaymentRequestButtonTheme, - usePlatformCheckoutEnabledSettings, + useWooPayEnabledSettings, } from '../../../data'; jest.mock( '../../../data', () => ( { @@ -26,7 +26,7 @@ jest.mock( '../../../data', () => ( { usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'buy' ] ), usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), - usePlatformCheckoutEnabledSettings: jest.fn(), + useWooPayEnabledSettings: jest.fn(), } ) ); jest.mock( '../payment-request-button-preview' ); @@ -45,7 +45,7 @@ const getMockPaymentRequestEnabledSettings = ( updateIsPaymentRequestEnabledHandler ) => [ isEnabled, updateIsPaymentRequestEnabledHandler ]; -const getMockPlatformCheckoutEnabledSettings = ( isEnabled ) => [ isEnabled ]; +const getMockWooPayEnabledSettings = ( isEnabled ) => [ isEnabled ]; const getMockPaymentRequestLocations = ( isCheckoutEnabled, @@ -71,8 +71,8 @@ describe( 'PaymentRequestSettings', () => { getMockPaymentRequestLocations( true, true, true, jest.fn() ) ); - usePlatformCheckoutEnabledSettings.mockReturnValue( - getMockPlatformCheckoutEnabledSettings( true ) + useWooPayEnabledSettings.mockReturnValue( + getMockWooPayEnabledSettings( true ) ); } ); diff --git a/client/settings/express-checkout-settings/test/platform-checkout-settings.test.js b/client/settings/express-checkout-settings/test/woopay-settings.test.js similarity index 50% rename from client/settings/express-checkout-settings/test/platform-checkout-settings.test.js rename to client/settings/express-checkout-settings/test/woopay-settings.test.js index 9486817b54f..e8ea5b97da8 100644 --- a/client/settings/express-checkout-settings/test/platform-checkout-settings.test.js +++ b/client/settings/express-checkout-settings/test/woopay-settings.test.js @@ -9,44 +9,44 @@ import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ -import PlatformCheckoutSettings from '../platform-checkout-settings'; +import WooPaySettings from '../woopay-settings'; import { - usePlatformCheckoutEnabledSettings, - usePlatformCheckoutCustomMessage, - usePlatformCheckoutStoreLogo, + useWooPayEnabledSettings, + useWooPayCustomMessage, + useWooPayStoreLogo, usePaymentRequestButtonType, usePaymentRequestButtonSize, usePaymentRequestButtonTheme, - usePlatformCheckoutLocations, + useWooPayLocations, } from '../../../data'; jest.mock( '../../../data', () => ( { - usePlatformCheckoutEnabledSettings: jest.fn(), - usePlatformCheckoutCustomMessage: jest.fn(), - usePlatformCheckoutStoreLogo: jest.fn(), + useWooPayEnabledSettings: jest.fn(), + useWooPayCustomMessage: jest.fn(), + useWooPayStoreLogo: jest.fn(), usePaymentRequestButtonType: jest.fn(), usePaymentRequestButtonSize: jest.fn(), usePaymentRequestButtonTheme: jest.fn(), - usePlatformCheckoutLocations: jest.fn(), + useWooPayLocations: jest.fn(), } ) ); jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn( () => ( { createErrorNotice: jest.fn() } ) ), } ) ); -const getMockPlatformCheckoutEnabledSettings = ( +const getMockWooPayEnabledSettings = ( isEnabled, - updateIsPlatformCheckoutEnabledHandler -) => [ isEnabled, updateIsPlatformCheckoutEnabledHandler ]; + updateIsWooPayEnabledHandler +) => [ isEnabled, updateIsWooPayEnabledHandler ]; -const getMockPlatformCheckoutCustomMessage = ( +const getMockWooPayCustomMessage = ( message, - updatePlatformCheckoutCustomMessageHandler -) => [ message, updatePlatformCheckoutCustomMessageHandler ]; -const getMockPlatformCheckoutStoreLogo = ( + updateWooPayCustomMessageHandler +) => [ message, updateWooPayCustomMessageHandler ]; +const getMockWooPayStoreLogo = ( message, updateWooPayStoreLogoHandler ) => [ message, - updatePlatformCheckoutStoreLogoHandler -) => [ message, updatePlatformCheckoutStoreLogoHandler ]; + updateWooPayStoreLogoHandler, +]; const getMockPaymentRequestButtonType = ( message, @@ -60,23 +60,23 @@ const getMockPaymentRequestButtonTheme = ( message, updatePaymentRequestButtonThemeHandler ) => [ message, updatePaymentRequestButtonThemeHandler ]; -const getMockPlatformCheckoutLocations = ( +const getMockWooPayLocations = ( message, updateWooPayLocationsHandler ) => [ message, - updatePlatformCheckoutLocationsHandler -) => [ message, updatePlatformCheckoutLocationsHandler ]; + updateWooPayLocationsHandler, +]; -describe( 'PlatformCheckoutSettings', () => { +describe( 'WooPaySettings', () => { beforeEach( () => { - usePlatformCheckoutEnabledSettings.mockReturnValue( - getMockPlatformCheckoutEnabledSettings( true, jest.fn() ) + useWooPayEnabledSettings.mockReturnValue( + getMockWooPayEnabledSettings( true, jest.fn() ) ); - usePlatformCheckoutCustomMessage.mockReturnValue( - getMockPlatformCheckoutCustomMessage( '', jest.fn() ) + useWooPayCustomMessage.mockReturnValue( + getMockWooPayCustomMessage( '', jest.fn() ) ); - usePlatformCheckoutStoreLogo.mockReturnValue( - getMockPlatformCheckoutStoreLogo( '', jest.fn() ) + useWooPayStoreLogo.mockReturnValue( + getMockWooPayStoreLogo( '', jest.fn() ) ); usePaymentRequestButtonType.mockReturnValue( @@ -91,8 +91,8 @@ describe( 'PlatformCheckoutSettings', () => { getMockPaymentRequestButtonTheme( [ 'dark' ], jest.fn() ) ); - usePlatformCheckoutLocations.mockReturnValue( - getMockPlatformCheckoutLocations( [ true, true, true ], jest.fn() ) + useWooPayLocations.mockReturnValue( + getMockWooPayLocations( [ true, true, true ], jest.fn() ) ); global.wcpaySettings = { @@ -101,7 +101,7 @@ describe( 'PlatformCheckoutSettings', () => { } ); it( 'renders settings with defaults', () => { - render( ); + render( ); // confirm checkbox groups displayed const [ enableCheckbox ] = screen.queryAllByRole( 'checkbox' ); @@ -110,36 +110,28 @@ describe( 'PlatformCheckoutSettings', () => { } ); it( 'triggers the hooks when the enable setting is being interacted with', () => { - const updateIsPlatformCheckoutEnabledHandler = jest.fn(); + const updateIsWooPayEnabledHandler = jest.fn(); - usePlatformCheckoutEnabledSettings.mockReturnValue( - getMockPlatformCheckoutEnabledSettings( - true, - updateIsPlatformCheckoutEnabledHandler - ) + useWooPayEnabledSettings.mockReturnValue( + getMockWooPayEnabledSettings( true, updateIsWooPayEnabledHandler ) ); - render( ); + render( ); - expect( updateIsPlatformCheckoutEnabledHandler ).not.toHaveBeenCalled(); + expect( updateIsWooPayEnabledHandler ).not.toHaveBeenCalled(); userEvent.click( screen.getByLabelText( /Enable WooPay/ ) ); - expect( updateIsPlatformCheckoutEnabledHandler ).toHaveBeenCalledWith( - false - ); + expect( updateIsWooPayEnabledHandler ).toHaveBeenCalledWith( false ); } ); it( 'triggers the hooks when the custom message setting is being interacted with', () => { - const updatePlatformCheckoutCustomMessageHandler = jest.fn(); + const updateWooPayCustomMessageHandler = jest.fn(); - usePlatformCheckoutCustomMessage.mockReturnValue( - getMockPlatformCheckoutCustomMessage( - '', - updatePlatformCheckoutCustomMessageHandler - ) + useWooPayCustomMessage.mockReturnValue( + getMockWooPayCustomMessage( '', updateWooPayCustomMessageHandler ) ); - render( ); + render( ); // confirm settings headings expect( @@ -151,13 +143,11 @@ describe( 'PlatformCheckoutSettings', () => { expect( customMessageTextbox ).toBeInTheDocument(); - expect( - updatePlatformCheckoutCustomMessageHandler - ).not.toHaveBeenCalled(); + expect( updateWooPayCustomMessageHandler ).not.toHaveBeenCalled(); userEvent.type( screen.getByRole( 'textbox' ), 'test' ); - expect( - updatePlatformCheckoutCustomMessageHandler - ).toHaveBeenLastCalledWith( 'test' ); + expect( updateWooPayCustomMessageHandler ).toHaveBeenLastCalledWith( + 'test' + ); } ); } ); diff --git a/client/settings/express-checkout-settings/platform-checkout-preview.js b/client/settings/express-checkout-settings/woopay-preview.js similarity index 100% rename from client/settings/express-checkout-settings/platform-checkout-preview.js rename to client/settings/express-checkout-settings/woopay-preview.js diff --git a/client/settings/express-checkout-settings/platform-checkout-settings.js b/client/settings/express-checkout-settings/woopay-settings.js similarity index 66% rename from client/settings/express-checkout-settings/platform-checkout-settings.js rename to client/settings/express-checkout-settings/woopay-settings.js index 36d59d2285e..91d474af6b6 100644 --- a/client/settings/express-checkout-settings/platform-checkout-settings.js +++ b/client/settings/express-checkout-settings/woopay-settings.js @@ -11,65 +11,54 @@ import interpolateComponents from '@automattic/interpolate-components'; * Internal dependencies */ import CardBody from '../card-body'; -import PlatformCheckoutFileUpload from './file-upload'; -import PlatformCheckoutPreview from './platform-checkout-preview'; +import WooPayFileUpload from './file-upload'; +import WooPayPreview from './woopay-preview'; import { - usePlatformCheckoutEnabledSettings, - usePlatformCheckoutCustomMessage, - usePlatformCheckoutStoreLogo, - usePlatformCheckoutLocations, + useWooPayEnabledSettings, + useWooPayCustomMessage, + useWooPayStoreLogo, + useWooPayLocations, } from 'wcpay/data'; import GeneralPaymentRequestButtonSettings from './general-payment-request-button-settings'; const CUSTOM_MESSAGE_MAX_LENGTH = 100; -const PlatformCheckoutSettings = ( { section } ) => { +const WooPaySettings = ( { section } ) => { const [ - isPlatformCheckoutEnabled, - updateIsPlatformCheckoutEnabled, - ] = usePlatformCheckoutEnabledSettings(); + isWooPayEnabled, + updateIsWooPayEnabled, + ] = useWooPayEnabledSettings(); const [ - platformCheckoutCustomMessage, - setPlatformCheckoutCustomMessage, - ] = usePlatformCheckoutCustomMessage(); + woopayCustomMessage, + setWooPayCustomMessage, + ] = useWooPayCustomMessage(); - const [ - platformCheckoutStoreLogo, - setPlatformCheckoutStoreLogo, - ] = usePlatformCheckoutStoreLogo(); + const [ woopayStoreLogo, setWooPayStoreLogo ] = useWooPayStoreLogo(); - const [ - platformCheckoutLocations, - updatePlatformCheckoutLocations, - ] = usePlatformCheckoutLocations(); + const [ woopayLocations, updateWooPayLocations ] = useWooPayLocations(); const makeLocationChangeHandler = ( location ) => ( isChecked ) => { if ( isChecked ) { - updatePlatformCheckoutLocations( [ - ...platformCheckoutLocations, - location, - ] ); + updateWooPayLocations( [ ...woopayLocations, location ] ); } else { - updatePlatformCheckoutLocations( - platformCheckoutLocations.filter( - ( name ) => name !== location - ) + updateWooPayLocations( + woopayLocations.filter( ( name ) => name !== location ) ); } }; return ( - + { 'enable' === section && ( {
    -
    -
    - - Advanced - -
    -
    -
    - - - - -
    -
    - -
    -
    -
    +
    - Screen transactions for mismatched addresses + Block transactions for mismatched addresses

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    -
    -
    - - Advanced - -
    -
    -
    - - - - - -
    -
    - -
    -
    -
    +
    - Screen transactions for mismatched addresses + Block transactions for mismatched addresses

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/avs-mismatch.test.js.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/avs-mismatch.test.tsx.snap similarity index 50% rename from client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/avs-mismatch.test.js.snap rename to client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/avs-mismatch.test.tsx.snap index 2f5fefbb9bb..bdb776b8c9c 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/avs-mismatch.test.js.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/avs-mismatch.test.tsx.snap @@ -27,49 +27,58 @@ exports[`AVS mismatch card renders correctly when AVS check is disabled 1`] = ` class="components-card__body is-size-medium wcpay-card-body css-xmjzce-BodyUI e1q7k77g3" >
    - How does this filter protect me? + Enable filtering -

    - Buyers who can provide the street number and post code on file with the issuing bank are more likely to be the actual account holder. AVS matches, however, are not a guarantee. -

    -
    -
    -
    - - - - - -
    -
    + + + +
    + Block transactions for mismatched AVS +
    +

    + When enabled, the payment will be blocked. +

    +
    + + How does this filter protect me? + +

    + Buyers who can provide the street number and post code on file with the issuing bank are more likely to be the actual account holder. AVS matches, however, are not a guarantee. +

    +
    @@ -102,56 +111,58 @@ exports[`AVS mismatch card renders correctly when AVS check is enabled 1`] = ` class="components-card__body is-size-medium wcpay-card-body css-xmjzce-BodyUI e1q7k77g3" >
    - How does this filter protect me? + Enable filtering -

    - Buyers who can provide the street number and post code on file with the issuing bank are more likely to be the actual account holder. AVS matches, however, are not a guarantee. -

    -
    -
    +
    + + How does this filter protect me? + +

    + Buyers who can provide the street number and post code on file with the issuing bank are more likely to be the actual account holder. AVS matches, however, are not a guarantee. +

    +
    diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.js.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap similarity index 93% rename from client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.js.snap rename to client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap index 518a04bcc96..8519772aecf 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.js.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap @@ -57,7 +57,7 @@ exports[`CVC verification card renders correctly when CVC check is disabled 1`] > @@ -132,7 +132,7 @@ exports[`CVC verification card renders correctly when CVC check is enabled 1`] = > diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.js.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap similarity index 69% rename from client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.js.snap rename to client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap index 860cc3af8b5..9a8bef60883 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.js.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/international-ip-address.test.tsx.snap @@ -72,14 +72,14 @@ exports[`International IP address card renders correctly 1`] = ` class="components-toggle-control__label" for="inspector-toggle-control-0" > - Screen transactions for international IP addresses + Block transactions for international IP addresses

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    @@ -114,7 +114,7 @@ exports[`International IP address card renders correctly 1`] = ` > @@ -204,82 +204,17 @@ exports[`International IP address card renders correctly when enabled 1`] = ` class="components-toggle-control__label" for="inspector-toggle-control-1" > - Screen transactions for international IP addresses + Block transactions for international IP addresses

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    -
    -
    - - Advanced - -
    -
    -
    - - - - -
    -
    - -
    -
    -
    +
    @@ -402,97 +337,17 @@ exports[`International IP address card renders correctly when enabled and checke class="components-toggle-control__label" for="inspector-toggle-control-2" > - Screen transactions for international IP addresses + Block transactions for international IP addresses

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    -
    -
    - - Advanced - -
    -
    -
    - - - - - -
    -
    - -
    -
    -
    +
    @@ -615,14 +470,14 @@ exports[`International IP address card renders like disabled when checked, but n class="components-toggle-control__label" for="inspector-toggle-control-3" > - Screen transactions for international IP addresses + Block transactions for international IP addresses

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    @@ -657,7 +512,7 @@ exports[`International IP address card renders like disabled when checked, but n > diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/ip-address-mismatch.js.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/ip-address-mismatch.test.tsx.snap similarity index 65% rename from client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/ip-address-mismatch.js.snap rename to client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/ip-address-mismatch.test.tsx.snap index 20652119f3a..15c2ddf265b 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/ip-address-mismatch.js.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/ip-address-mismatch.test.tsx.snap @@ -73,7 +73,7 @@ exports[`International billing address card renders correctly 1`] = ` class="components-base-control__help css-1wm1a55-StyledHelp e1puf3u3" id="inspector-toggle-control-0__help" > - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    @@ -165,75 +165,10 @@ exports[`International billing address card renders correctly when enabled 1`] = class="components-base-control__help css-1wm1a55-StyledHelp e1puf3u3" id="inspector-toggle-control-1__help" > - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    -
    -
    - - Advanced - -
    -
    -
    - - - - -
    -
    - -
    -
    -
    +
    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    -
    -
    - - Advanced - -
    -
    -
    - - - - - -
    -
    - -
    -
    -
    +
    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.js.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap similarity index 73% rename from client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.js.snap rename to client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap index 75e3b116422..b91eae71d66 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.js.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/order-items-threshold.test.tsx.snap @@ -58,14 +58,14 @@ exports[`Order items threshold card renders correctly 1`] = ` class="components-toggle-control__label" for="inspector-toggle-control-0" > - Screen transactions for abnormal item counts + Block transactions for abnormal item counts

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    @@ -142,14 +142,14 @@ exports[`Order items threshold card renders correctly when enabled 1`] = ` class="components-toggle-control__label" for="inspector-toggle-control-1" > - Screen transactions for abnormal item counts + Block transactions for abnormal item counts

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    @@ -252,7 +252,7 @@ exports[`Order items threshold card renders correctly when enabled 1`] = ` > @@ -267,70 +267,6 @@ exports[`Order items threshold card renders correctly when enabled 1`] = `
    -
    - - Advanced - -
    -
    -
    - - - - -
    -
    - -
    -
    - Screen transactions for abnormal item counts + Block transactions for abnormal item counts

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    @@ -516,7 +452,7 @@ exports[`Order items threshold card renders correctly when enabled and checked 1 > @@ -531,85 +467,6 @@ exports[`Order items threshold card renders correctly when enabled and checked 1
    -
    - - Advanced - -
    -
    -
    - - - - - -
    -
    - -
    -
    - Screen transactions for abnormal item counts + Block transactions for abnormal item counts

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.js.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap similarity index 73% rename from client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.js.snap rename to client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap index b51e32549e4..27590e7ee35 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.js.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/purchase-price-threshold.test.tsx.snap @@ -58,14 +58,14 @@ exports[`Purchase price threshold card renders correctly 1`] = ` class="components-toggle-control__label" for="inspector-toggle-control-0" > - Screen transactions for abnormal purchase prices + Block transactions for abnormal purchase prices

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    @@ -142,14 +142,14 @@ exports[`Purchase price threshold card renders correctly when enabled 1`] = ` class="components-toggle-control__label" for="inspector-toggle-control-1" > - Screen transactions for abnormal purchase prices + Block transactions for abnormal purchase prices

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    @@ -254,7 +254,7 @@ exports[`Purchase price threshold card renders correctly when enabled 1`] = ` > @@ -269,70 +269,6 @@ exports[`Purchase price threshold card renders correctly when enabled 1`] = `
    -
    - - Advanced - -
    -
    -
    - - - - -
    -
    - -
    -
    - Screen transactions for abnormal purchase prices + Block transactions for abnormal purchase prices

    - When enabled, the payment method will not be charged until you review and approve the transaction + The payment will be blocked.

    @@ -520,7 +456,7 @@ exports[`Purchase price threshold card renders correctly when enabled and checke > @@ -535,85 +471,6 @@ exports[`Purchase price threshold card renders correctly when enabled and checke
    -
    - - Advanced - -
    -
    -
    - - - - - -
    -
    - -
    -
    - Screen transactions for abnormal purchase prices + Block transactions for abnormal purchase prices

    - When enabled, the payment method will not be charged until you review and approve the transaction + When enabled, the payment will be blocked.

    diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/address-mismatch.test.js b/client/settings/fraud-protection/advanced-settings/cards/test/address-mismatch.test.tsx similarity index 91% rename from client/settings/fraud-protection/advanced-settings/cards/test/address-mismatch.test.js rename to client/settings/fraud-protection/advanced-settings/cards/test/address-mismatch.test.tsx index 4a29c615d35..09c4133ae55 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/address-mismatch.test.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/address-mismatch.test.tsx @@ -9,7 +9,19 @@ import { render } from '@testing-library/react'; import AddressMismatchRuleCard from '../address-mismatch'; import FraudPreventionSettingsContext from '../../context'; +declare const global: { + wcpaySettings: { + isFRTReviewFeatureActive: boolean; + }; +}; + describe( 'Address mismatch card', () => { + beforeEach( () => { + global.wcpaySettings = { + isFRTReviewFeatureActive: false, + }; + } ); + const settings = { address_mismatch: { enabled: false, diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/avs-mismatch.test.js b/client/settings/fraud-protection/advanced-settings/cards/test/avs-mismatch.test.tsx similarity index 89% rename from client/settings/fraud-protection/advanced-settings/cards/test/avs-mismatch.test.js rename to client/settings/fraud-protection/advanced-settings/cards/test/avs-mismatch.test.tsx index b68465fe227..dd917d3ca2d 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/avs-mismatch.test.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/avs-mismatch.test.tsx @@ -9,6 +9,17 @@ import { render } from '@testing-library/react'; import FraudPreventionSettingsContext from '../../context'; import AVSMismatchRuleCard from '../avs-mismatch'; +declare const global: { + wcpaySettings: { + accountStatus: { + fraudProtection: { + declineOnAVSFailure: boolean; + }; + }; + isFRTReviewFeatureActive?: boolean; + }; +}; + describe( 'AVS mismatch card', () => { test( 'renders correctly when AVS check is enabled', () => { const settings = { @@ -23,6 +34,7 @@ describe( 'AVS mismatch card', () => { declineOnAVSFailure: true, }, }, + isFRTReviewFeatureActive: false, }; const setSettings = jest.fn(); const contextValue = { @@ -37,9 +49,6 @@ describe( 'AVS mismatch card', () => { ); expect( container ).toMatchSnapshot(); - expect( container ).toHaveTextContent( - /For security, this filter is enabled and cannot be modified/i - ); } ); test( 'renders correctly when AVS check is disabled', () => { const settings = { @@ -68,8 +77,5 @@ describe( 'AVS mismatch card', () => { ); expect( container ).toMatchSnapshot(); - expect( container ).toHaveTextContent( - /This filter is disabled, and can not be modified/i - ); } ); } ); diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/cvc-verification.test.js b/client/settings/fraud-protection/advanced-settings/cards/test/cvc-verification.test.tsx similarity index 83% rename from client/settings/fraud-protection/advanced-settings/cards/test/cvc-verification.test.js rename to client/settings/fraud-protection/advanced-settings/cards/test/cvc-verification.test.tsx index 4d8de852d60..e1b1e76d4aa 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/cvc-verification.test.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/cvc-verification.test.tsx @@ -9,6 +9,17 @@ import { render } from '@testing-library/react'; import FraudPreventionSettingsContext from '../../context'; import CVCVerificationRuleCard from '../cvc-verification'; +declare const global: { + wcpaySettings: { + accountStatus: { + fraudProtection: { + declineOnCVCFailure: boolean; + }; + }; + isFRTReviewFeatureActive?: boolean; + }; +}; + describe( 'CVC verification card', () => { test( 'renders correctly when CVC check is enabled', () => { const settings = { @@ -23,6 +34,7 @@ describe( 'CVC verification card', () => { declineOnCVCFailure: true, }, }, + isFRTReviewFeatureActive: false, }; const setSettings = jest.fn(); const contextValue = { @@ -57,8 +69,10 @@ describe( 'CVC verification card', () => { }; const setSettings = jest.fn(); const contextValue = { - advancedFraudProtectionSettings: settings, - setAdvancedFraudProtectionSettings: setSettings, + protectionSettingsUI: settings, + setProtectionSettingsUI: setSettings, + protectionSettingsChanged: false, + setProtectionSettingsChanged: jest.fn(), }; const { container } = render( diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/international-ip-address.test.js b/client/settings/fraud-protection/advanced-settings/cards/test/international-ip-address.test.tsx similarity index 85% rename from client/settings/fraud-protection/advanced-settings/cards/test/international-ip-address.test.js rename to client/settings/fraud-protection/advanced-settings/cards/test/international-ip-address.test.tsx index 6b36bb46fbd..360976b07e6 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/international-ip-address.test.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/international-ip-address.test.tsx @@ -9,6 +9,24 @@ import { render } from '@testing-library/react'; import FraudPreventionSettingsContext from '../../context'; import InternationalIPAddressRuleCard from '../international-ip-address'; +declare const global: { + wcSettings: { + admin: { + preloadSettings: { + general: { + woocommerce_allowed_countries: string; + woocommerce_all_except_countries: string[]; + woocommerce_specific_allowed_countries: string[]; + }; + }; + }; + }; + + wcpaySettings: { + isFRTReviewFeatureActive: boolean; + }; +}; + describe( 'International IP address card', () => { const settings = { international_ip_address: { @@ -34,6 +52,9 @@ describe( 'International IP address card', () => { }, }, }; + global.wcpaySettings = { + isFRTReviewFeatureActive: false, + }; test( 'renders correctly', () => { const { container } = render( diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/ip-address-mismatch.js b/client/settings/fraud-protection/advanced-settings/cards/test/ip-address-mismatch.test.tsx similarity index 85% rename from client/settings/fraud-protection/advanced-settings/cards/test/ip-address-mismatch.js rename to client/settings/fraud-protection/advanced-settings/cards/test/ip-address-mismatch.test.tsx index ebe1c2c011b..8c091a59fdc 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/ip-address-mismatch.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/ip-address-mismatch.test.tsx @@ -9,6 +9,24 @@ import { render } from '@testing-library/react'; import FraudPreventionSettingsContext from '../../context'; import IPAddressMismatchRuleCard from '../ip-address-mismatch'; +declare const global: { + wcSettings: { + admin: { + preloadSettings: { + general: { + woocommerce_allowed_countries: string; + woocommerce_all_except_countries: string[]; + woocommerce_specific_allowed_countries: string[]; + }; + }; + }; + }; + + wcpaySettings: { + isFRTReviewFeatureActive: boolean; + }; +}; + describe( 'International billing address card', () => { const settings = { ip_address_mismatch: { @@ -34,6 +52,9 @@ describe( 'International billing address card', () => { }, }, }; + global.wcpaySettings = { + isFRTReviewFeatureActive: false, + }; test( 'renders correctly', () => { const { container } = render( diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/order-items-threshold.test.js b/client/settings/fraud-protection/advanced-settings/cards/test/order-items-threshold.test.tsx similarity index 96% rename from client/settings/fraud-protection/advanced-settings/cards/test/order-items-threshold.test.js rename to client/settings/fraud-protection/advanced-settings/cards/test/order-items-threshold.test.tsx index ef27968eba0..3762aa9db47 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/order-items-threshold.test.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/order-items-threshold.test.tsx @@ -10,15 +10,28 @@ import FraudPreventionSettingsContext from '../../context'; import OrderItemsThresholdRuleCard, { OrderItemsThresholdValidation, } from '../order-items-threshold'; +import { FraudPreventionOrderItemsThresholdSetting } from 'wcpay/settings/fraud-protection/interfaces'; + +declare const global: { + wcpaySettings: { + isFRTReviewFeatureActive: boolean; + }; +}; describe( 'Order items threshold card', () => { + beforeEach( () => { + global.wcpaySettings = { + isFRTReviewFeatureActive: false, + }; + } ); + const settings = { order_items_threshold: { enabled: false, block: false, min_items: null, max_items: null, - }, + } as FraudPreventionOrderItemsThresholdSetting, }; const setSettings = jest.fn(); const contextValue = { diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/purchase-price-threshold.test.js b/client/settings/fraud-protection/advanced-settings/cards/test/purchase-price-threshold.test.tsx similarity index 94% rename from client/settings/fraud-protection/advanced-settings/cards/test/purchase-price-threshold.test.js rename to client/settings/fraud-protection/advanced-settings/cards/test/purchase-price-threshold.test.tsx index 360d500edf4..5399a7d2ba0 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/purchase-price-threshold.test.js +++ b/client/settings/fraud-protection/advanced-settings/cards/test/purchase-price-threshold.test.tsx @@ -10,6 +10,27 @@ import FraudPreventionSettingsContext from '../../context'; import PurchasePriceThresholdRuleCard, { PurchasePriceThresholdValidation, } from '../purchase-price-threshold'; +import { FraudPreventionPurchasePriceThresholdSetting } from 'wcpay/settings/fraud-protection/interfaces'; + +declare const global: { + wcpaySettings: { + storeCurrency: string; + connect: { + country: string; + }; + currencyData: { + [ key: string ]: { + code: string; + symbol: string; + symbolPosition: string; + thousandSeparator: string; + decimalSeparator: string; + precision: number; + }; + }; + isFRTReviewFeatureActive?: boolean; + }; +}; describe( 'Purchase price threshold card', () => { beforeEach( () => { @@ -28,6 +49,7 @@ describe( 'Purchase price threshold card', () => { precision: 2, }, }, + isFRTReviewFeatureActive: false, }; } ); @@ -37,7 +59,7 @@ describe( 'Purchase price threshold card', () => { block: false, min_amount: null, max_amount: null, - }, + } as FraudPreventionPurchasePriceThresholdSetting, }; const setSettings = jest.fn(); const contextValue = { @@ -214,7 +236,7 @@ describe( 'Purchase price threshold card', () => { settings.purchase_price_threshold.min_amount = 10; const setValidationError = jest.fn(); const validationResult = PurchasePriceThresholdValidation( - settings, + settings.purchase_price_threshold, setValidationError ); expect( validationResult ).toBe( true ); diff --git a/client/settings/fraud-protection/advanced-settings/constants.js b/client/settings/fraud-protection/advanced-settings/constants.ts similarity index 89% rename from client/settings/fraud-protection/advanced-settings/constants.js rename to client/settings/fraud-protection/advanced-settings/constants.ts index 586b45e7632..10849d1e22d 100644 --- a/client/settings/fraud-protection/advanced-settings/constants.js +++ b/client/settings/fraud-protection/advanced-settings/constants.ts @@ -3,6 +3,8 @@ export const ProtectionLevel = { BASIC: 'basic', ADVANCED: 'advanced', + STANDARD: 'standard', + HIGH: 'high', }; export const Outcomes = { @@ -12,6 +14,7 @@ export const Outcomes = { }; export const Rules = { + RULE_AVS_VERIFICATION: 'avs_verification', RULE_ADDRESS_MISMATCH: 'address_mismatch', RULE_INTERNATIONAL_IP_ADDRESS: 'international_ip_address', RULE_IP_ADDRESS_MISMATCH: 'ip_address_mismatch', @@ -20,6 +23,7 @@ export const Rules = { }; export const Checks = { + CHECK_AVS_MISMATCH: 'avs_mismatch', CHECK_BILLING_SHIPPING_ADDRESS_SAME: 'billing_shipping_address_same', CHECK_IP_COUNTRY: 'ip_country', CHECK_IP_BILLING_COUNTRY_SAME: 'ip_billing_country_same', diff --git a/client/settings/fraud-protection/advanced-settings/context.js b/client/settings/fraud-protection/advanced-settings/context.js deleted file mode 100644 index 644c92b0d53..00000000000 --- a/client/settings/fraud-protection/advanced-settings/context.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * External dependencies - */ -import { createContext } from 'react'; - -const FraudPreventionSettingsContext = createContext( { - protectionSettingsUI: false, - setProtectionSettingsUI: () => {}, - protectionSettingsChanged: false, - setProtectionSettingsChanged: () => {}, -} ); - -export default FraudPreventionSettingsContext; diff --git a/client/settings/fraud-protection/advanced-settings/context.ts b/client/settings/fraud-protection/advanced-settings/context.ts new file mode 100644 index 00000000000..ade59fc45ce --- /dev/null +++ b/client/settings/fraud-protection/advanced-settings/context.ts @@ -0,0 +1,14 @@ +/** + * External dependencies + */ +import { createContext } from 'react'; +import { FraudPreventionSettingsContextType } from '../interfaces'; + +const FraudPreventionSettingsContext = createContext( { + protectionSettingsUI: {}, + setProtectionSettingsUI: () => null, + protectionSettingsChanged: false, + setProtectionSettingsChanged: () => false, +} as FraudPreventionSettingsContextType ); + +export default FraudPreventionSettingsContext; diff --git a/client/settings/fraud-protection/advanced-settings/index.js b/client/settings/fraud-protection/advanced-settings/index.tsx similarity index 84% rename from client/settings/fraud-protection/advanced-settings/index.js rename to client/settings/fraud-protection/advanced-settings/index.tsx index 8431bfd47eb..4cc6e80cab9 100644 --- a/client/settings/fraud-protection/advanced-settings/index.js +++ b/client/settings/fraud-protection/advanced-settings/index.tsx @@ -37,8 +37,14 @@ import './../style.scss'; import { ProtectionLevel } from './constants'; import { readRuleset, writeRuleset } from './utils'; import wcpayTracks from 'tracks'; +import { + CurrentProtectionLevelHook, + AdvancedFraudPreventionSettingsHook, + ProtectionSettingsUI, + SettingsHook, +} from '../interfaces'; -const observerEventMapping = { +const observerEventMapping: Record< string, string > = { 'avs-mismatch-card': 'wcpay_fraud_protection_advanced_settings_card_avs_mismatch_viewed', 'cvc-verification-card': @@ -72,28 +78,32 @@ const Breadcrumb = () => ( ); -const SaveFraudProtectionSettingsButton = ( { children } ) => { +const SaveFraudProtectionSettingsButton: React.FC = ( { children } ) => { const headerElement = document.querySelector( '.woocommerce-layout__header-wrapper' ); return headerElement && ReactDOM.createPortal( children, headerElement ); }; -const FraudProtectionAdvancedSettingsPage = () => { - const { saveSettings, isLoading, isSaving } = useSettings(); +const FraudProtectionAdvancedSettingsPage: React.FC = () => { + const { saveSettings, isLoading, isSaving } = useSettings() as SettingsHook; - const cardObserver = useRef( null ); + const cardObserver = useRef< IntersectionObserver >(); const [ currentProtectionLevel, updateProtectionLevel, - ] = useCurrentProtectionLevel(); + ] = useCurrentProtectionLevel() as CurrentProtectionLevelHook; const [ advancedFraudProtectionSettings, updateAdvancedFraudProtectionSettings, - ] = useAdvancedFraudProtectionSettings(); - const [ validationError, setValidationError ] = useState( null ); - const [ protectionSettingsUI, setProtectionSettingsUI ] = useState( {} ); + ] = useAdvancedFraudProtectionSettings() as AdvancedFraudPreventionSettingsHook; + const [ validationError, setValidationError ] = useState< string | null >( + null + ); + const [ protectionSettingsUI, setProtectionSettingsUI ] = useState< + ProtectionSettingsUI + >( {} ); const [ protectionSettingsChanged, setProtectionSettingsChanged, @@ -112,12 +122,15 @@ const FraudProtectionAdvancedSettingsPage = () => { if ( saveButton ) { document .querySelector( '.woocommerce-layout__header-heading' ) - .after( saveButton ); + ?.after( saveButton ); } } ); - const validateSettings = ( fraudProtectionSettings ) => { + const validateSettings = ( + fraudProtectionSettings: ProtectionSettingsUI + ) => { setValidationError( null ); + const validators = { order_items_threshold: OrderItemsThresholdValidation, purchase_price_threshold: PurchasePriceThresholdValidation, @@ -125,7 +138,7 @@ const FraudProtectionAdvancedSettingsPage = () => { return Object.keys( validators ) .map( ( key ) => - validators[ key ]( + validators[ key as keyof typeof validators ]( fraudProtectionSettings[ key ], setValidationError ) @@ -147,6 +160,16 @@ const FraudProtectionAdvancedSettingsPage = () => { const settings = writeRuleset( protectionSettingsUI ); + // Persist the AVS verification setting until the account cache is updated locally. + if ( + wcpaySettings?.accountStatus?.fraudProtection + ?.declineOnAVSFailure + ) { + wcpaySettings.accountStatus.fraudProtection.declineOnAVSFailure = settings.some( + ( setting ) => setting.key === 'avs_verification' + ); + } + updateAdvancedFraudProtectionSettings( settings ); saveSettings(); @@ -170,27 +193,29 @@ const FraudProtectionAdvancedSettingsPage = () => { if ( wcSettingsMenuItem ) { wcSettingsMenuItem.setAttribute( 'aria-current', 'page' ); wcSettingsMenuItem.classList.add( 'current' ); - wcSettingsMenuItem.parentElement.classList.add( 'current' ); + wcSettingsMenuItem.parentElement?.classList.add( 'current' ); } }, [] ); // Intersection observer callback for tracking card viewed events. - const observerCallback = ( entries ) => { - entries.forEach( ( entry ) => { + const observerCallback = ( entries: IntersectionObserverEntry[] ) => { + entries.forEach( ( entry: IntersectionObserverEntry ) => { const { target, intersectionRatio } = entry; if ( 0 < intersectionRatio ) { - // element is at least partially visible. + // Element is at least partially visible. const { id } = target; const event = observerEventMapping[ id ] || null; if ( event ) { - wcpayTracks.recordEvent( event ); + wcpayTracks.recordEvent( event, {} ); } - cardObserver.current?.unobserve( - document.getElementById( id ) - ); + const element = document.getElementById( id ); + + if ( element ) { + cardObserver.current?.unobserve( element ); + } } } ); }; @@ -275,9 +300,6 @@ const FraudProtectionAdvancedSettingsPage = () => { - - - @@ -293,6 +315,9 @@ const FraudProtectionAdvancedSettingsPage = () => { + + +