diff --git a/.eslintrc b/.eslintrc index f1eb9c1e460..8356990a159 100644 --- a/.eslintrc +++ b/.eslintrc @@ -76,7 +76,6 @@ "definedTags": [ "format" ] } ], - "yoda": [ "error", "always" ], /* partially disable rules to get @woocommerce/eslint-plugin integration done */ "jsdoc/no-undefined-types": "off", "jsdoc/require-param": "off", @@ -138,7 +137,10 @@ "wpcalypso/jsx-classname-namespace": "off", "react/react-in-jsx-scope": "error", "no-shadow": "off", - "@typescript-eslint/no-shadow": "error" + "@typescript-eslint/no-shadow": "error", + "jsdoc/require-param-type": 0, + "jsdoc/require-returns-type": 0, + "valid-jsdoc": "off" } } ] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..c5fe332ca5a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Each line is a file pattern followed by one or more owners. + +# Harmony owns any files in the .github directory at the root of the repository and any of its subdirectories. +/.github/ @Automattic/harmony diff --git a/.github/actions/commit-push-as-bot/action.yml b/.github/actions/commit-push-as-bot/action.yml new file mode 100644 index 00000000000..82cd953c04c --- /dev/null +++ b/.github/actions/commit-push-as-bot/action.yml @@ -0,0 +1,27 @@ +name: "Commit and push as the github-actions bot" +description: "Commit and push as the github-actions bot" + +inputs: + release-version: + description: "The release version to be used in the commit message" + required: true + branch: + description: "The branch where the commit will be pushed" + required: true + +runs: + using: composite + steps: + - name: "Commit and push changes" + id: build_plugin + shell: bash + env: + RELEASE_VERSION: ${{ inputs.release-version }} + BRANCH_NAME: ${{ inputs.branch }} + run: | + git config user.name "github-actions[bot]" + # We could consider fetching the bot's ID through an API call to be future-proof. Hardcoded for now. + # See https://github.com/Automattic/woocommerce-payments/pull/5200#discussion_r1034560144 + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -am "Update version and add changelog entries for release $RELEASE_VERSION" + git push --set-upstream origin $BRANCH_NAME diff --git a/.github/actions/e2e/env-setup/action.yml b/.github/actions/e2e/env-setup/action.yml index dabaa752c3e..863ad27e75b 100644 --- a/.github/actions/e2e/env-setup/action.yml +++ b/.github/actions/e2e/env-setup/action.yml @@ -10,12 +10,8 @@ runs: run: echo -e "machine github.com\n login $E2E_GH_TOKEN" > ~/.netrc # PHP setup - - name: PHP Setup - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php # Composer setup - name: Setup Composer diff --git a/.github/actions/process-changelog/action.yml b/.github/actions/process-changelog/action.yml index 66a07c2107c..787e26416ac 100644 --- a/.github/actions/process-changelog/action.yml +++ b/.github/actions/process-changelog/action.yml @@ -22,47 +22,75 @@ outputs: runs: using: composite steps: + - name: "Verify the action type" + id: verify_action_type + if: ${{ inputs.action-type == 'generate' }} + shell: bash + env: + RELEASE_VERSION: ${{ inputs.release-version }} + RELEASE_DATE: ${{ inputs.release-date }} + ACTION_TYPE: ${{ inputs.action-type }} + run: | + FINAL_RELEASE_VERSION=$(echo "$RELEASE_VERSION" | grep -Po '\d.\d.\d(.*?)') # Keep only x.y.z from x.y.z(-test-n) + CURRENT_RELEASE_VERSION=$(jq '.version' package.json -r) + + # If the changelog directory is empty (except .gitkeep) and the final release version is already defined in package.json, we need to switch to amend + # This use case is mainly for the last test package created from the release branch, to avoid an empty changelog + if [ "$(ls -A changelog | wc -l)" -eq 1 ] && [[ "$FINAL_RELEASE_VERSION" == "$CURRENT_RELEASE_VERSION" ]]; then + echo "ACTION_TYPE=amend-version" >> $GITHUB_OUTPUT + echo "CURRENT_VERSION=$CURRENT_RELEASE_VERSION" >> $GITHUB_OUTPUT + fi + - name: "Process changelog for changelog.txt" id: process_changelog shell: bash env: - ACTION_TYPE: ${{ inputs.action-type }} + ACTION_TYPE: ${{ steps.verify_action_type.outputs.ACTION_TYPE || inputs.action-type }} + CURRENT_VERSION: ${{ steps.verify_action_type.outputs.CURRENT_VERSION }} RELEASE_VERSION: ${{ inputs.release-version }} RELEASE_DATE: ${{ inputs.release-date }} run: | - # Install this dev package globally to gather changelog entries while not including it into the release package - composer global require automattic/jetpack-changelogger:^3.0.7 - - if ${{ env.ACTION_TYPE == 'generate' }}; then - CHANGELOG_FLAG="" - echo "Generating the changelog entries." >> $GITHUB_STEP_SUMMARY + if ${{ env.ACTION_TYPE == 'amend-version' }}; then + sed -i "s/^= $CURRENT_VERSION - .* =$/= $RELEASE_VERSION - $RELEASE_DATE =/" changelog.txt else - CHANGELOG_FLAG="--amend" - echo "Amending the changelog entries." >> $GITHUB_STEP_SUMMARY - fi + # Install this dev package globally to gather changelog entries while not including it into the release package + composer global require automattic/jetpack-changelogger:^3.0.7 - ~/.composer/vendor/bin/changelogger write --use-version="$RELEASE_VERSION" --release-date="$RELEASE_DATE" $CHANGELOG_FLAG --no-interaction --yes + if ${{ env.ACTION_TYPE == 'generate' }}; then + CHANGELOG_FLAG="" + echo "Generating the changelog entries." >> $GITHUB_STEP_SUMMARY + else + CHANGELOG_FLAG="--amend" + echo "Amending the changelog entries." >> $GITHUB_STEP_SUMMARY + fi + + ~/.composer/vendor/bin/changelogger write --use-version="$RELEASE_VERSION" --release-date="$RELEASE_DATE" $CHANGELOG_FLAG --no-interaction --yes + fi - echo "Picking up changelog for version '$RELEASE_VERSION'..." CHANGELOG=$(awk '/^= / { if (p) { exit }; p=1; next } p && NF' changelog.txt) - echo "$CHANGELOG" # Escape backslash, new line and ampersand characters. The order is important. CHANGELOG=${CHANGELOG//\\/\\\\} CHANGELOG=${CHANGELOG//$'\n'/\\n} CHANGELOG=${CHANGELOG//&/\\&} + echo "CHANGELOG=$CHANGELOG" >> $GITHUB_OUTPUT - name: "Process changelog for readme.txt" shell: bash env: - ACTION_TYPE: ${{ inputs.action-type }} + ACTION_TYPE: ${{ steps.verify_action_type.outputs.ACTION_TYPE || inputs.action-type }} + CURRENT_VERSION: ${{ steps.verify_action_type.outputs.CURRENT_VERSION }} RELEASE_VERSION: ${{ inputs.release-version }} RELEASE_DATE: ${{ inputs.release-date }} CHANGELOG: ${{ steps.process_changelog.outputs.CHANGELOG }} run: | - if ${{ env.ACTION_TYPE == 'amend' }}; then - perl -i -p0e "s/= $RELEASE_VERSION.*?(\n){2}//s" readme.txt # Delete the existing changelog for the release version first - fi + if ${{ env.ACTION_TYPE == 'amend-version' }}; then + sed -i "s/^= $CURRENT_VERSION - .* =$/= $RELEASE_VERSION - $RELEASE_DATE =/" readme.txt + else + if ${{ env.ACTION_TYPE == 'amend' }}; then + perl -i -p0e "s/= $RELEASE_VERSION.*?(\n){2}//s" readme.txt # Delete the existing changelog for the release version first + fi - sed -ri "s|(== Changelog ==)|\1\n\n= $RELEASE_VERSION - $RELEASE_DATE =\n$CHANGELOG|" readme.txt + sed -ri "s|(== Changelog ==)|\1\n\n= $RELEASE_VERSION - $RELEASE_DATE =\n$CHANGELOG|" readme.txt + fi diff --git a/.github/actions/setup-php/action.yml b/.github/actions/setup-php/action.yml new file mode 100644 index 00000000000..44e797aeb6d --- /dev/null +++ b/.github/actions/setup-php/action.yml @@ -0,0 +1,19 @@ +name: "Set up PHP" +description: "Extracts the required PHP version from plugin file and uses it to build PHP." + +runs: + using: composite + steps: + - name: "Get minimum PHP version" + shell: bash + id: get_min_php_version + run: | + MIN_PHP_VERSION=$(sed -n 's/.*PHP: //p' woocommerce-payments.php) + echo "MIN_PHP_VERSION=$MIN_PHP_VERSION" >> $GITHUB_OUTPUT + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ steps.get_min_php_version.outputs.MIN_PHP_VERSION }} + tools: composer + coverage: none diff --git a/.github/actions/setup-repo/action.yml b/.github/actions/setup-repo/action.yml index 28741b60920..890fe95963f 100644 --- a/.github/actions/setup-repo/action.yml +++ b/.github/actions/setup-repo/action.yml @@ -1,29 +1,20 @@ name: "Setup WooCommerce Payments repository" description: "Handles the installation, building, and caching of the projects within the repository." -inputs: - php-version: - description: "The version of PHP that the action should set up." - default: "7.4" - runs: using: composite steps: - name: "Setup Node" uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' - cache: 'npm' + node-version-file: ".nvmrc" + cache: "npm" - name: "Enable composer dependencies caching" uses: actions/cache@v3 with: path: ~/.cache/composer/ key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} - - - name: "Setup PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ inputs.php-version }} - tools: composer - coverage: none + + - name: "Set up PHP" + uses: ./.github/actions/setup-php diff --git a/.github/workflows/build-zip-and-run-smoke-tests.yml b/.github/workflows/build-zip-and-run-smoke-tests.yml index 94599b3b88e..7afaf05a833 100644 --- a/.github/workflows/build-zip-and-run-smoke-tests.yml +++ b/.github/workflows/build-zip-and-run-smoke-tests.yml @@ -24,7 +24,7 @@ on: jobs: build-zip: name: "Build the zip file" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: "Checkout repository" uses: actions/checkout@v3 diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml index 55f7391fb90..c44b2b0191e 100644 --- a/.github/workflows/check-changelog.yml +++ b/.github/workflows/check-changelog.yml @@ -11,7 +11,7 @@ concurrency: jobs: check-changelog: name: Check changelog - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 @@ -22,11 +22,8 @@ jobs: path: ~/.cache/composer/ key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} # setup PHP, but without debug extensions for reasonable performance - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php # Install composer packages. - run: composer self-update && composer install --no-progress # Fetch the target branch before running the check. diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 5e2ef9bdcb2..6b9ade34d58 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -4,9 +4,10 @@ on: pull_request env: - WC_MIN_SUPPORTED_VERSION: '7.5.0' - WP_MIN_SUPPORTED_VERSION: '6.0' - PHP_MIN_SUPPORTED_VERSION: '7.3' + WC_MIN_SUPPORTED_VERSION: '7.7.0' + WP_MIN_SUPPORTED_VERSION: '6.1' + PHP_MIN_SUPPORTED_VERSION: '7.4' + GUTENBERG_VERSION_FOR_WP_MIN: '15.7.0' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,7 +16,7 @@ concurrency: jobs: generate-wc-compat-matrix: name: "Generate the matrix for woocommerce compatibility dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -23,13 +24,13 @@ jobs: id: generate_matrix run: | WC_VERSIONS=$( echo "[\"$WC_MIN_SUPPORTED_VERSION\", \"latest\", \"beta\"]" ) - MATRIX_INCLUDE=$( echo "[{\"woocommerce\":\"$WC_MIN_SUPPORTED_VERSION\",\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"13.6.0\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) + MATRIX_INCLUDE=$( echo "[{\"woocommerce\":\"$WC_MIN_SUPPORTED_VERSION\",\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"$GUTENBERG_VERSION_FOR_WP_MIN\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) echo "matrix={\"woocommerce\":$WC_VERSIONS,\"wordpress\":[\"latest\"],\"gutenberg\":[\"latest\"],\"php\":[\"7.4\"], \"include\":$MATRIX_INCLUDE}" >> $GITHUB_OUTPUT woocommerce-compatibility: name: "WC compatibility" needs: generate-wc-compat-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: WP_VERSION: ${{ matrix.wordpress }} WC_VERSION: ${{ matrix.woocommerce }} @@ -57,7 +58,7 @@ jobs: generate-wc-compat-beta-matrix: name: "Generate the matrix for compatibility-woocommerce-beta dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -71,7 +72,7 @@ jobs: compatibility-woocommerce-beta: name: Environment - WC beta needs: generate-wc-compat-beta-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: ${{ fromJSON(needs.generate-wc-compat-beta-matrix.outputs.matrix) }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ddc2db674bc..b1401136de9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,7 +10,7 @@ concurrency: jobs: woocommerce-coverage: name: Code coverage - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 10 diff --git a/.github/workflows/create-pre-release.yml b/.github/workflows/create-pre-release.yml index fec46604650..65c20427376 100644 --- a/.github/workflows/create-pre-release.yml +++ b/.github/workflows/create-pre-release.yml @@ -16,7 +16,7 @@ defaults: jobs: create-release: name: "Create the pre-release" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ inputs.releaseVersion }} @@ -33,24 +33,33 @@ jobs: with: version: ${{ env.RELEASE_VERSION }} - - name: "Create a test tag" - id: create_tag - uses: ./.github/actions/create-tag - with: - version: ${{ env.RELEASE_VERSION }} - - name: "Generate the changelog" id: generate_changelog uses: ./.github/actions/process-changelog with: - release-version: ${{ steps.create_tag.outputs.trimmed-version }} + release-version: ${{ steps.create_branch.outputs.trimmed-version }} - name: "Bump version header" env: - VERSION: ${{ steps.create_tag.outputs.trimmed-version }} + VERSION: ${{ steps.create_branch.outputs.trimmed-version }} run: | sed -i "s/^ \* Version: .*$/ * Version: $VERSION/" woocommerce-payments.php + - name: "Commit and push changes" + env: + BRANCH_NAME: ${{ steps.create_branch.outputs.branch-name }} + RELEASE_VERSION: ${{ steps.create_branch.outputs.trimmed-version }} + uses: ./.github/actions/commit-push-as-bot + with: + release-version: ${{ env.RELEASE_VERSION }} + branch: ${{ env.BRANCH_NAME }} + + - name: "Create a test tag" + id: create_tag + uses: ./.github/actions/create-tag + with: + version: ${{ env.RELEASE_VERSION }} + - name: "Build the plugin" id: build_plugin uses: ./.github/actions/build diff --git a/.github/workflows/e2e-pull-request.yml b/.github/workflows/e2e-pull-request.yml index 1315a484a34..ab0cd702a86 100644 --- a/.github/workflows/e2e-pull-request.yml +++ b/.github/workflows/e2e-pull-request.yml @@ -34,6 +34,7 @@ env: WCPAY_USE_BUILD_ARTIFACT: ${{ inputs.wcpay-use-build-artifact }} WCPAY_ARTIFACT_DIRECTORY: 'zipfile' NODE_ENV: 'test' + FORCE_E2E_DEPS_SETUP: true concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -41,7 +42,7 @@ concurrency: jobs: wcpay-e2e-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 1a6ca8e5fdc..ddc50e45526 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -23,13 +23,14 @@ 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.5.0' + WC_MIN_SUPPORTED_VERSION: '7.7.0' NODE_ENV: 'test' + FORCE_E2E_DEPS_SETUP: true jobs: generate-matrix: name: "Generate the matrix for subscriptions-tests dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: @@ -41,7 +42,7 @@ jobs: # Run WCPay & subscriptions tests against specific WC versions wcpay-subscriptions-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: generate-matrix strategy: fail-fast: false @@ -69,7 +70,7 @@ jobs: # Run tests against WC Checkout blocks & WC latest # [TODO] Unskip blocks tests after investigating constant failures. # blocks-tests: - # runs-on: ubuntu-20.04 + # runs-on: ubuntu-latest # name: WC - latest | blocks - shopper # env: @@ -92,7 +93,7 @@ jobs: # Run tests against WP Nightly & WC latest wp-nightly-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/i18n-weekly-release.yml b/.github/workflows/i18n-weekly-release.yml index 197213dec1c..11dd8a89705 100644 --- a/.github/workflows/i18n-weekly-release.yml +++ b/.github/workflows/i18n-weekly-release.yml @@ -6,7 +6,7 @@ on: jobs: i18n-release: name: Release - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository @@ -27,12 +27,8 @@ jobs: path: ~/.npm/ key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} # setup PHP, but without debug extensions for reasonable performance - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none - + - name: "Set up PHP" + uses: ./.github/actions/setup-php - name: Build release run: | npm ci diff --git a/.github/workflows/js-lint-test.yml b/.github/workflows/js-lint-test.yml index 4824d78ca19..fdbea1d59b0 100644 --- a/.github/workflows/js-lint-test.yml +++ b/.github/workflows/js-lint-test.yml @@ -11,7 +11,7 @@ concurrency: jobs: lint: name: JS linting - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 @@ -32,7 +32,7 @@ jobs: test: name: JS testing - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index 8c0e7d73b37..abf8413b84c 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -11,12 +11,9 @@ jobs: # Check for version-specific PHP compatibility php-compatibility: name: PHP Compatibility - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php - run: bash bin/phpcs-compat.sh diff --git a/.github/workflows/php-lint-test.yml b/.github/workflows/php-lint-test.yml index 602c7c0755e..0a55baaeb8e 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.5.0' + WC_MIN_SUPPORTED_VERSION: '7.7.0' GUTENBERG_VERSION: latest - PHP_MIN_SUPPORTED_VERSION: '7.3' + PHP_MIN_SUPPORTED_VERSION: '7.4' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -17,7 +17,7 @@ concurrency: jobs: lint: name: PHP linting - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # clone the repository - uses: actions/checkout@v3 @@ -27,30 +27,27 @@ jobs: path: ~/.cache/composer/ key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} # setup PHP, but without debug extensions for reasonable performance - - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - tools: composer - coverage: none + - name: "Set up PHP" + uses: ./.github/actions/setup-php # install dependencies and run linter - run: composer self-update && composer install --no-progress && ./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$) && ./vendor/bin/psalm generate-test-matrix: name: "Generate the matrix for php tests dynamically" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: - name: "Generate matrix" id: generate_matrix run: | - PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"7.3\", \"7.4\"]" ) + PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"8.0\", \"8.1\"]" ) echo "matrix={\"php\":$PHP_VERSIONS}" >> $GITHUB_OUTPUT test: name: PHP testing needs: generate-test-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 10 diff --git a/.github/workflows/post-release-updates.yml b/.github/workflows/post-release-updates.yml index f19be159407..141c53e0b16 100644 --- a/.github/workflows/post-release-updates.yml +++ b/.github/workflows/post-release-updates.yml @@ -11,7 +11,7 @@ defaults: jobs: get-last-released-version: name: "Get the last released version" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: releaseVersion: ${{ steps.current-version.outputs.RELEASE_VERSION }} @@ -31,7 +31,7 @@ jobs: create-gh-release: name: "Create a GH release" needs: get-last-released-version - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.get-last-released-version.outputs.releaseVersion }} @@ -75,7 +75,7 @@ jobs: merge-trunk-into-develop: name: "Merge trunk back into develop" needs: get-last-released-version - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.get-last-released-version.outputs.releaseVersion }} @@ -98,7 +98,7 @@ jobs: trigger-translations: name: "Trigger translations update for the release" needs: [ get-last-released-version, create-gh-release ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: "Checkout repository (trunk)" uses: actions/checkout@v3 @@ -114,7 +114,7 @@ jobs: update-wiki: name: "Update the wiki for the next release" needs: get-last-released-version - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.get-last-released-version.outputs.releaseVersion }} diff --git a/.github/workflows/pr-build-live-branch.yml b/.github/workflows/pr-build-live-branch.yml index c50d026fe08..1fa9742f0ea 100644 --- a/.github/workflows/pr-build-live-branch.yml +++ b/.github/workflows/pr-build-live-branch.yml @@ -10,7 +10,7 @@ concurrency: jobs: build-and-inform-zip-file: name: "Build and inform the zip file" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: "Checkout repository" uses: actions/checkout@v3 @@ -122,7 +122,7 @@ jobs: #### Option 2. Jurassic Ninja - available for logged-in A12s - :rocket: [Launch a JN site with this branch](https://jurassic.ninja/create/?jetpack-beta&shortlived&nojetpack&woocommerce&branches.woocommerce-payments=${PR_HEAD_REF}) :rocket: + :rocket: [Launch a JN site with this branch](https://jurassic.ninja/create/?jetpack-beta&shortlived&nojetpack&woocommerce&woocommerce-payments-dev-tools&branches.woocommerce-payments=${PR_HEAD_REF}) :rocket: :information_source: Install this [Tampermonkey script](https://github.com/Automattic/woocommerce-payments/tree/develop/bin/wcpay-live-branches) to get more options. diff --git a/.github/workflows/release-changelog.yml b/.github/workflows/release-changelog.yml index f20588f9cd7..4c9c7a7a8b1 100644 --- a/.github/workflows/release-changelog.yml +++ b/.github/workflows/release-changelog.yml @@ -29,7 +29,7 @@ defaults: jobs: process-changelog: name: "Process the changelog" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: CHANGELOG_ACTION: ${{ inputs.action-type }} RELEASE_VERSION: ${{ inputs.release-version }} @@ -37,7 +37,9 @@ jobs: steps: - name: "Checkout repository" uses: actions/checkout@v3 - + with: + token: ${{ SECRETS.BOTWOO_TOKEN }} + - name: "Format the release date" id: format_date run: | @@ -54,8 +56,8 @@ jobs: - name: "Commit and push the changes" run: | - git config user.name "${{ github.actor }}" - git config user.email "${{ github.actor }}@users.noreply.github.com" + git config user.name "botwoo" + git config user.email "botwoo@users.noreply.github.com" if ${{ env.CHANGELOG_ACTION == 'amend' }}; then git commit -am "Amend changelog entries for release $RELEASE_VERSION" else diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml index 84760b24ebc..7b81c24d138 100644 --- a/.github/workflows/release-code-freeze.yml +++ b/.github/workflows/release-code-freeze.yml @@ -19,7 +19,7 @@ defaults: jobs: check-code-freeze: name: "Check that today is the day of the code freeze" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: freeze: ${{ steps.check-freeze.outputs.FREEZE }} nextReleaseVersion: ${{ steps.next-version.outputs.NEXT_RELEASE_VERSION }} @@ -81,7 +81,7 @@ jobs: name: "Send notification to Slack" needs: [check-code-freeze, create-release-pr] if: ${{ ! ( inputs.skipSlackPing && needs.create-release-pr.outputs.release-pr-id ) }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.check-code-freeze.outputs.nextReleaseVersion }} RELEASE_DATE: ${{ needs.check-code-freeze.outputs.nextReleaseDate }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 52c6c9e1acf..0433e03eb51 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -35,6 +35,11 @@ on: required: false default: "next wednesday" type: string + skip-build-zip: + type: boolean + required: false + default: true + description: "Skip building the zip file" skip-smoke-tests: type: boolean required: false @@ -48,7 +53,7 @@ defaults: jobs: prepare-release: name: "Prepare a stable release" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: branch: ${{ steps.create_branch.outputs.branch-name }} release-pr-id: ${{ steps.create-pr-to-trunk.outputs.RELEASE_PR_ID }} @@ -104,13 +109,10 @@ jobs: env: RELEASE_VERSION: ${{ steps.create_branch.outputs.trimmed-version }} BRANCH_NAME: ${{ steps.create_branch.outputs.branch-name }} - run: | - git config user.name "github-actions[bot]" - # We could consider fetching the bot's ID through an API call to be future-proof. Hardcoded for now. - # See https://github.com/Automattic/woocommerce-payments/pull/5200#discussion_r1034560144 - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -am "Update version and add changelog entries for release $RELEASE_VERSION" - git push --set-upstream origin $BRANCH_NAME + uses: ./.github/actions/commit-push-as-bot + with: + release-version: ${{ env.RELEASE_VERSION }} + branch: ${{ env.BRANCH_NAME }} - name: "Create a PR to trunk" id: create-pr-to-trunk @@ -139,6 +141,7 @@ jobs: build-zip-and-run-smoke-tests: name: "Build zip & Run smoke tests" needs: prepare-release + if: ${{ ! inputs.skip-build-zip }} uses: ./.github/workflows/build-zip-and-run-smoke-tests.yml with: skip-smoke-tests: ${{ inputs.skip-smoke-tests }} diff --git a/.gitignore b/.gitignore index e5910ecfea4..9749d5a27ba 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ phpunit.xml # Composer /vendor/ /vendor-dist/ +/lib/vendor/ contributors.md # Screenshots for e2e tests failures @@ -84,4 +85,4 @@ local.env tests/e2e/screenshots # E2E Performance test results -tests/e2e/reports \ No newline at end of file +tests/e2e/reports diff --git a/.husky/post-merge b/.husky/post-merge index ce86573df84..2d66c62ad2b 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -2,7 +2,7 @@ . "$(dirname "$0")/_/husky.sh" # Load local env variables if present. -if [[ -f "$(pwd)/local.env" ]]; then +if [ -f "$(pwd)/local.env" ]; then . "$(pwd)/local.env" fi @@ -17,7 +17,7 @@ if [ ! -d $DEV_TOOLS_PLUGIN_PATH ]; then echo echo "\033[33mCouldn't find the '$DEV_TOOLS_PLUGIN_PATH' directory. Skipping the auto-update for the WCPay Dev Tools plugin...\033[0m" else - if [[ "$(cd $DEV_TOOLS_PLUGIN_PATH && git rev-parse --show-toplevel 2>/dev/null)" = "$(cd $DEV_TOOLS_PLUGIN_PATH && pwd)" ]]; then + if [ "$(cd $DEV_TOOLS_PLUGIN_PATH && git rev-parse --show-toplevel 2>/dev/null)" = "$(cd $DEV_TOOLS_PLUGIN_PATH && pwd)" ]; then echo echo "\033[32mDetermining if there is an update for the WCPay Dev Tools plugin...\033[0m" diff --git a/README.md b/README.md index dd368cef739..416c6b6a728 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ We adopt the L-2 version support policy for WordPress core strictly, and a loose ### Install dependencies & build -- `npm install` +- `npm install` - `composer install` - `npm run build:client`, or if you're developing the client you can have it auto-update when changes are made: `npm start` @@ -55,7 +55,7 @@ We currently support the following variables: ## Test account setup -For setting up a test account follow [these instructions](https://woocommerce.com/document/payments/testing/dev-mode/). +For setting up a test account follow [these instructions](https://woocommerce.com/document/woopayments/testing-and-troubleshooting/dev-mode/). You will need a externally accessible URL to set up the plugin. You can use ngrok for this. diff --git a/assets/css/admin.css b/assets/css/admin.css index 34343afd6f7..8b17d53e766 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -36,98 +36,96 @@ height: 1.25rem; width: 32px; width: 2rem; + background-size: contain; + background-repeat: no-repeat; } .payment-method__brand--amex { - background: no-repeat url( '../images/cards/amex.svg' ); + background-image: url( '../images/cards/amex.svg' ); } .payment-method__brand--diners { - background: no-repeat url( '../images/cards/diners.svg' ); + background-image: url( '../images/cards/diners.svg' ); } .payment-method__brand--discover { - background: no-repeat url( '../images/cards/discover.svg' ); + background-image: url( '../images/cards/discover.svg' ); } .payment-method__brand--jcb { - background: no-repeat url( '../images/cards/jcb.svg' ); + background-image: url( '../images/cards/jcb.svg' ); } .payment-method__brand--mastercard { - background: no-repeat url( '../images/cards/mastercard.svg' ); + background-image: url( '../images/cards/mastercard.svg' ); } .payment-method__brand--unionpay { - background: no-repeat url( '../images/cards/unionpay.svg' ); + background-image: url( '../images/cards/unionpay.svg' ); } .payment-method__brand--visa { - background: no-repeat url( '../images/cards/visa.svg' ); + background-image: url( '../images/cards/visa.svg' ); } .payment-method__brand--unknown { - background: no-repeat url( '../images/cards/unknown.svg' ); + background-image: url( '../images/cards/unknown.svg' ); } .payment-method__brand--giropay { - background: no-repeat url( '../images/payment-methods/giropay.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/giropay.svg' ); } .payment-method__brand--eps { - background: no-repeat url( '../images/payment-methods/eps.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/eps.svg' ); } .payment-method__brand--p24 { - background: no-repeat url( '../images/payment-methods/p24.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/p24.svg' ); } .payment-method__brand--sepa_debit { - background: no-repeat url( '../images/cards/sepa.svg' ); - background-size: contain; + background-image: url( '../images/cards/sepa.svg' ); } .payment-method__brand--sofort { - background: no-repeat url( '../images/payment-methods/sofort.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/sofort.svg' ); } .payment-method__brand--ideal { - background: no-repeat url( '../images/payment-methods/ideal.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/ideal.svg' ); } .payment-method__brand--google-pay { - background: no-repeat url( '../images/cards/google-pay.svg' ); - background-size: contain; + background-image: url( '../images/cards/google-pay.svg' ); } .payment-method__brand--apple-pay { - background: no-repeat url( '../images/cards/apple-pay.svg' ); - background-size: contain; + background-image: url( '../images/cards/apple-pay.svg' ); } .payment-method__brand--bancontact { - background: no-repeat url( '../images/payment-methods/bancontact.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/bancontact.svg' ); } .payment-method__brand--sepa_debit { - background: no-repeat url( '../images/payment-methods/sepa-debit.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/sepa-debit.svg' ); } .payment-method__brand--au_becs_debit { - background: no-repeat url( '../images/payment-methods/bank-debit.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/bank-debit.svg' ); } .payment-method__brand--link { - background: no-repeat url( '../images/payment-methods/link.svg' ); - background-size: contain; + background-image: url( '../images/payment-methods/link.svg' ); +} + +.payment-method__brand--afterpay_clearpay { + background-image: url( '../images/payment-methods/afterpay-icon.svg' ); +} + +.payment-method__brand--affirm { + background-image: url( '../images/payment-methods/affirm-icon.svg' ); } .wc_gateways tr[data-gateway_id='woocommerce_payments'] .payment-method__icon { diff --git a/assets/css/success.css b/assets/css/success.css index 5126138b10b..6d5200174b0 100644 --- a/assets/css/success.css +++ b/assets/css/success.css @@ -1,36 +1,11 @@ -.woocommerce-order .thankyou-notice-woopay { - background: #239328; - color: #fff; - padding: 12px; - padding-left: 66px; - border-radius: 2px; - border-left: 10px solid #156b14; - position: relative; -} - -.woocommerce-order .thankyou-notice-woopay::before { - content: url( '../images/check-circle.svg' ); - position: absolute; - left: 32px; - width: 24px; - top: 13px; -} - .wc-payment-gateway-method-name-woopay-wrapper { display: flex; align-items: center; + flex-wrap: wrap; + line-height: 1; } .wc-payment-gateway-method-name-woopay-wrapper img { - margin-top: 6px; - margin-right: 9px; -} - -.woocommerce-order-overview.woocommerce-thankyou-order-details.order_details.woopay { - margin-top: -4.324325903em; -} -.woocommerce-order-overview.woocommerce-thankyou-order-details.order_details.woopay - .woocommerce-order-overview__payment-method.method { - border-top: 1px dotted #e3e3e3; - padding-top: 1em; + margin-right: 0.5rem; + padding-top: 4px; } diff --git a/assets/images/cards/mastercard.svg b/assets/images/cards/mastercard.svg index 9e1515fb207..b77d93d3041 100644 --- a/assets/images/cards/mastercard.svg +++ b/assets/images/cards/mastercard.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/images/cards/visa.svg b/assets/images/cards/visa.svg index 4b6f8616477..04eeda0e51e 100644 --- a/assets/images/cards/visa.svg +++ b/assets/images/cards/visa.svg @@ -1 +1,7 @@ - + + + + + + + diff --git a/assets/images/check-circle.svg b/assets/images/check-circle.svg deleted file mode 100644 index 5e669a389b5..00000000000 --- a/assets/images/check-circle.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/images/fraud-protection/discoverability-banner@2x.png b/assets/images/fraud-protection/discoverability-banner@2x.png index dc209d571f7..8228cb2812c 100644 Binary files a/assets/images/fraud-protection/discoverability-banner@2x.png and b/assets/images/fraud-protection/discoverability-banner@2x.png differ diff --git a/assets/images/illustrations/connect-hero.png b/assets/images/illustrations/connect-hero.png new file mode 100644 index 00000000000..4e062d70749 Binary files /dev/null and b/assets/images/illustrations/connect-hero.png differ diff --git a/assets/images/logo.svg b/assets/images/logo.svg index f9df57e1c2d..2e406e07269 100644 --- a/assets/images/logo.svg +++ b/assets/images/logo.svg @@ -1 +1,14 @@ - + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/affirm-icon.svg b/assets/images/payment-methods/affirm-icon.svg new file mode 100644 index 00000000000..26b821a3b72 --- /dev/null +++ b/assets/images/payment-methods/affirm-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/affirm.svg b/assets/images/payment-methods/affirm.svg new file mode 100644 index 00000000000..7ad4f7063ef --- /dev/null +++ b/assets/images/payment-methods/affirm.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/afterpay-icon.svg b/assets/images/payment-methods/afterpay-icon.svg new file mode 100644 index 00000000000..e0797da52ae --- /dev/null +++ b/assets/images/payment-methods/afterpay-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/payment-methods/afterpay.svg b/assets/images/payment-methods/afterpay.svg new file mode 100644 index 00000000000..75cb2eda73b --- /dev/null +++ b/assets/images/payment-methods/afterpay.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/all_local_payments.svg b/assets/images/payment-methods/all_local_payments.svg new file mode 100644 index 00000000000..94f1b2ff2ad --- /dev/null +++ b/assets/images/payment-methods/all_local_payments.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/cc.svg b/assets/images/payment-methods/cc.svg index 556ef2d70c7..8c7fd884aa1 100644 --- a/assets/images/payment-methods/cc.svg +++ b/assets/images/payment-methods/cc.svg @@ -1 +1,26 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/jcb.svg b/assets/images/payment-methods/jcb.svg new file mode 100644 index 00000000000..473c198b249 --- /dev/null +++ b/assets/images/payment-methods/jcb.svg @@ -0,0 +1 @@ + diff --git a/assets/images/payment-methods/local_payments.svg b/assets/images/payment-methods/local_payments.svg new file mode 100644 index 00000000000..4b1d690585b --- /dev/null +++ b/assets/images/payment-methods/local_payments.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/payment-methods/woopay.svg b/assets/images/payment-methods/woopay.svg new file mode 100644 index 00000000000..a516fe11fd5 --- /dev/null +++ b/assets/images/payment-methods/woopay.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/subscriptions-empty-state-connected.svg b/assets/images/subscriptions-empty-state-connected.svg deleted file mode 100644 index cbd0cc3a3fd..00000000000 --- a/assets/images/subscriptions-empty-state-connected.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bin/class-wcpay-changelog-formatter.php b/bin/class-wcpay-changelog-formatter.php index e01beccaeb5..b686cc58a2b 100644 --- a/bin/class-wcpay-changelog-formatter.php +++ b/bin/class-wcpay-changelog-formatter.php @@ -37,7 +37,7 @@ class WCPay_Changelog_Formatter extends Parser implements FormatterPlugin { * * @var string */ - private $title = '*** WooCommerce Payments Changelog ***'; + private $title = '*** WooPayments Changelog ***'; /** * Separator used in headings and change entries. diff --git a/bin/cli.sh b/bin/cli.sh new file mode 100755 index 00000000000..86d167477da --- /dev/null +++ b/bin/cli.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +first_arg=${1} +if [ "${first_arg}" = "--as-root" ]; then + user=0 + command=${@:2} +else + user=www-data + command=${@:1} +fi + +command=${command:-bash} + +docker-compose exec -u ${user} wordpress ${command} diff --git a/bin/wcpay-live-branches/wcpay-live-branches.user.js b/bin/wcpay-live-branches/wcpay-live-branches.user.js index a11ef88b261..73df0f3abca 100644 --- a/bin/wcpay-live-branches/wcpay-live-branches.user.js +++ b/bin/wcpay-live-branches/wcpay-live-branches.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name WCPay Live Branches // @namespace https://wordpress.com/ -// @version 1.1 +// @version 1.2 // @description Adds links to PRs pointing to Jurassic Ninja sites for live-testing a changeset // @grant GM_xmlhttpRequest // @connect jurassic.ninja @@ -186,6 +186,7 @@ { label: 'WooCommerce Payments Dev Tools', name: 'woocommerce-payments-dev-tools', + checked: true, }, { label: 'WooCommerce Smooth Generator', diff --git a/changelog.txt b/changelog.txt index 7eaf6fdec12..4ea06d5753b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,274 @@ -*** WooCommerce Payments Changelog *** +*** WooPayments Changelog *** + += 6.4.1 - 2023-09-06 = +* Fix - checkout processing when fields are hidden via customizer +* Fix - Potential fatal error when viewing WooCommerce home because we try to check if store has been fully onboarded but account service is not yet initialized. +* Fix - Resolved an issue with WCPay Subscription orders being set to failed during payment processing when Woo Subscriptions plugin is active. +* Fix - Use the gateway from the Checkout class in case the main registered gateway isn't initialized for some reason. +* Dev - Revert - Bump minimum required version of WooCommerce to 8.0.0 and WP to 6.1 +* Dev - Setting the minimum required version of WooCommerce back to 7.8.0 and WP to 6.0 + += 6.4.0 - 2023-08-31 = +* Add - Added the Transactions reporting endpoint +* Add - Adjust WooPay "custom message" to be reused for T&C and privacy policy +* Add - Combine Session Initialization with User Authentication on WooPay. +* Add - Enables deferred intent UPE for existing split UPE stores and newly onboarded stores. +* Add - Onboarding flow state persistence +* Fix - Adds consistent payment token labels for saved Stripe Link payment methods across my account, shortcode checkout, and blocks checkout pages. +* Fix - Adds the possibility of continuing in progress onboarding process +* Fix - Add `is_user_connected()` and `get_connected_user_data()` methods to `WC_Payments_Http_Interface` +* Fix - Display onboarding MCC field validation error +* Fix - Ensures that Stripe Link and SEPA Debit saved payment tokens are stored and validated with correct gateway IDs for relevant feature flags enabled. +* Fix - Fixes subscription renewals with the UPE enabled. +* Fix - Fix express checkout button design issues. +* Fix - Fix phone number input widget on checkout page +* Fix - Fix the WooPay logo so that it stays scaled up and visible on the WooPay button. +* Fix - Fix zero decimal currency display in order notes +* Fix - JavaScript is now correctly loaded on admin order screens when HPOS is enabled. +* Fix - Prevent displaying "Fraud protection" menu on half-onboarded stores +* Fix - Prevent fetching disputes on WooCommerce home task when store is not connected to a WooPayments account. +* Fix - Prevent Progressive Onboarding accounts from adding APMs until completing full verification. +* Fix - Provide per active discount Terms and Conditions link in the Account details card. +* Fix - Remove precision overriding in multi-currency scenarios +* Fix - Use domestic currency, instead of default deposit currency, to check if a payment method is compatible with the presentment currency in the checkout form. +* Update - BNPLs: updated methods copy on settings page +* Update - Change Payment_Intent_Status to Intent_Status +* Update - Improve `Chip` component styles to with improved visual design and accessible contrast ratios. +* Update - Removed wcpay_empty_state_preview_mode_v5 experiment code +* Update - Set WooPay button default enabled for product/cart/checkout pages +* Update - Updated copy for credit and debit card in settings +* Update - Updated payment method tooltip in settings so that it is rendered correctly in mobile view +* Update - Updated section "Payment Methods" in page "Settings" for mobile view +* Update - Update express checkouts section in settings for mobile view +* Update - Update tooltip styles to improve readability of interactive tooltip content. +* Dev - Adding dispute object to DisputeDetails component +* Dev - Adding HooksProxy and LegacyProxy to src, used to access code outside of it (hooks, globals, functions, and static methods). +* Dev - Adding Psalm checks to function calls within `src`. +* Dev - Add interface and concrete classes for Payment Method (project reengineering payment process). +* Dev - Add LegacyContainer to `src` to allow loading classes from `includes`. +* Dev - Add TypeScript development guidelines +* Dev - Bump minimum required version of WooCommerce to 8.0.0 and WP to 6.1 +* Dev - Fixing a mistake in the doc regarding the customer service. +* Dev - Fix Tracks to record checkout view on all stores +* Dev - Ignore updating currency precision if the country is Japan +* Dev - Move Multi-Currency Order Meta Helper functionality behind url param. +* Dev - Refactor the deposit status UI element to use the `Chip` component. +* Dev - Track WooPay Save My Info checkbox usage + += 6.3.2 - 2023-08-17 = +* Fix - Revert fix WooPay Session Handler in Store API requests. + += 6.3.1 - 2023-08-14 = +* Fix - Fix AutomateWoo error on WooPay redirection. +* Fix - Fix WooPay Session Handler in Store API requests. + += 6.3.0 - 2023-08-09 = +* Add - Add constant flag to use the new payment service (project reengineering payment process). +* Add - Add JCB payment method coming soon notice +* Add - Add payment service class (project reengineering payment process). +* Add - Adds integration for Stripe Link while using split UPE with deferred intent creation. +* Add - Add support for Japan +* Add - Add support for the Enabled status from Stripe (will replace Restricted Soon in the case where there is no active deadline). +* Add - Add support for United Arab Emirates +* Add - Add Tracks events around account connect when promo is active. +* Add - Add warning to the advanced settings page for WooPay incompatible extensions +* Add - Add WooPay Gift Cards support. +* Add - Add WooPay on onboarding payment methods. +* Add - Add WooPay Points and Rewards support +* Add - Allow WooPay verified email requests +* Add - Ensure WooPay compatibility with the split UPE that has deferred intent creation implementation. +* Add - Include WooPay merchant terms on WooCommerce Payment Methods settings. +* Add - Prefill the Business Name and Country fields during WooPayments KYC. +* Fix - Adding more descriptive error messages in gradual signup +* Fix - Allow card gateway to load properly with WooPay enabled and subscription item in the cart. +* Fix - Allow only domestic payments for BNPL payment methods +* Fix - Allow only Japanese phone numbers for Japanese accounts +* Fix - Avoid creating duplicate paid orders from a single payment intent. +* Fix - Disputes listing column renamed to Respond by and will show empty when dispute does not need response or if respond by date has passed. +* Fix - Enable customers who have migrated a WCPay subscription to a tokenised subscription to pay for existing failed orders. +* Fix - Fatal error when using latest MailPoet with WooPay +* Fix - Fixed the creation of the nonce that is sent to WooPay +* Fix - Fix error while selecting product variations. Make the stripe payment messaging element load only if at least one BNPL method is active. +* Fix - Fix extra requests when clicking WooPay express checkout button. +* Fix - Fix Fraud and Risk Tools welcome tour to only show if Fraud and Risk banner learn more button is clicked and tour not previously dismissed. +* Fix - Get WooPay adapted extensions from server +* Fix - Highlight menu item when transaction details, deposit details, and disputes details page are opened. +* Fix - Improve split UPE support of WooPay with multiple payment methods enabled. +* Fix - Minor copy changes on the Set Up Real Payments modal. +* Fix - Remove daily deposits for JP merchants as it's not permitted by our payment processor +* Fix - Reverting change to the plugin name because of compatibility with iOS app. +* Fix - Send correct shipping address to Afterpay in Classic Checkout +* Fix - Send shipping address correctly to Afterpay in block checkout, when separate billing address is provided. +* Fix - Update excerpt in readme.txt to improve ranking +* Fix - Visual fixes for the Connect page hero. +* Update - Allows nulls in min/max payment ranges definitions for UPE payment methods +* Update - Minor copy fixes on the onboarding form. +* Update - Modify 'Contact WooCommerce Support' badge located on the 'Payments accepted on checkout' section of Payments > Settings. +* Update - Only show the post-onboarding congratulations message if user did not onboard in test mode. +* Update - Unify payment method icon design +* Dev - Add a dependency container for the new src directory. +* Dev - Add generic Get_Request class, and migrate current simple requests to use it +* Dev - Add PSR-4 autoloading for the src directory. +* Dev - Add unit tests to cover WooPay button eligibility. +* Dev - Add webpack script to generate RTL .css files and enqueue it for RTL languages +* Dev - Adjust coding standards to align with WC Core. +* Dev - Avoiding product-service exceptions during checkout, making debugging easier. +* Dev - Fix Husky post-merge script to conform to `sh` syntax +* Dev - Ignore updating currency precision if the country is Japan +* Dev - Introduce model class WC_Payments_API_Setup_Intention for setup intents +* Dev - Migrate certain WCPay shopper tracks to use wcpay prefix +* Dev - Migrate Chip component to TypeScript to improve code quality. +* Dev - Migrate DisputeStatusChip comp to TypeScript to improve code quality. +* Dev - Pass tracks identity to WooPay iframe +* Dev - Pass Tracks identity to WooPay to improve telemetry + += 6.2.2 - 2023-08-01 = +* Fix - Move the email title hook from the UPE class to the parent legacy gateway class, to avoid multiple callback invocations for the split UPE + += 6.2.1 - 2023-07-31 = +* Fix - Enhance query parameters validation in redirected requests. + += 6.2.0 - 2023-07-19 = +* Add - Add Android option in Device type advanced filter +* Add - Add dispute notice to the WooCommerce order screen to highlight disputes awaiting a response. +* Add - Added flag to allow us to remotely set if WooPay should be enabled or not for new merchants. +* Add - Add tooltip and ARIA labels to payment method logos in transaction list +* Add - Check for invalid extensions when one is activated or deactivated. +* Add - Make Affirm and Afterpay Stripe messaging show up correctly for variable products, especially when we change variations. +* Add - Prefill the store URL on the new Onboarding Form. +* Add - Sending preloaded_requests to WooPay to avoid waiting for external requests +* Fix - Add array_filter callback method +* Fix - Added logic to check if the recurring cart array is present before displaying the recurring totals section in the cart. +* Fix - Allow webhooks without livemode to be received. +* Fix - Ensure when a customer changes the shipping method on cart and checkout that the recurring totals correctly reflect the chosen method. +* Fix - Fix a fatal error on sites using WC Subscriptions versions below 4.0.0 +* Fix - Fix Country informed as empty for logged-out user in the BNPL site messaging configuration. +* Fix - Fixed typo in businessInfo strings in strings.tsx file +* Fix - Fix fatal errors when get_product method returns null +* Fix - Fix incorrect channel value in transaction description screen for Tap to Pay for Android transactions +* Fix - Fix issue where subscription signup fees are not converted correctly with Multi-Currency. +* Fix - Fix outdated documentation links. +* Fix - Fix Save my info section style when Payment options is not numbered. +* Fix - Remove duplicated payment method on thank you page when using WooPay. +* Fix - Resolve an issue that prevented the "Used for variations" checkbox from being enabled on the variable subscription product edit screen on WC version v7.9.0. +* Fix - Resolved an issue that caused the payment type metadata to not be included in payment requests. +* Fix - Resolved errors that occurred when activating the WC Subscriptions plugin via bulk action on the WP plugins screen or updating the plugin via the WooCommerce Extensions screen. +* Fix - Restore removed condition after naming convention. +* Fix - Reverting change to the plugin name because of compatibility with iOS app. +* Fix - When HPOS is enabled, permanently deleting a subscription related order wasn't updating the related orders cache properly. +* Fix - Wrap list of payment method logos on next line +* Update - Add incentive cache invalidation based on store context hash. +* Update - Another chunk of branding rollout: update wordpress.org assets +* Update - Check WCPay Subscriptions eligibility after store completes WooCommerce onboarding wizard. +* Update - Confirm subscription being switched is owned by customer before trying to possibly use its currency to prevent error. +* Update - Highlight the active dispute task if disputes are due within 72 hours. +* Update - Improve disputes list page by hiding the "Disputed on" column by default, add an "Action" column with clear call to action button, highlight urgent disputes' due dates, and color code disputes' statuses. +* Update - Mark an expired uncaptured order as 'Failed" instead of 'Canceled' +* Update - Refactoring and cleanup of code +* Update - Remove the Remind Me Later option from the Fraud and Risk Tools discoverability banner. +* Update - Remove WooCommerce Payments from taking over the WC core settings page +* Update - Simplify the active dispute task title when disputes are for multiple currencies to improve readability. +* Update - Update WooCommerce Payments to WooPayments across the plugin +* Update - WC Payments inbuilt subscriptions functionality is no longer enabled by default for eligible US based stores. +* Dev - Add E2E tests for Fraud & Risk tools. +* Dev - Adding a tracking property to record whether user went through the new onboarding UX or not. +* Dev - Add tool to allow merchants to fix Multi-Currency exchange rates in orders. +* Dev - Affirm&Afterpay: add new test cases to ensure the method availability on checkout +* Dev - Affirm&Afterpay: refactor subscription products detection by using existing subs API +* Dev - Extracting functionality for preventing duplicate payments into a service. +* Dev - Fix tests by ensuring the rest_pre_dispatch filter returns a WP_REST_Response +* Dev - Migrate `HorizontalList` component to TS +* Dev - Removed an old flag for a feature which is now enabled for old users. Some refactoring of the task lists code (no impact on functionality). +* Dev - Remove FRT E2E tests. +* Dev - Update subscriptions-core to 6.0.0. + += 6.1.1 - 2023-06-29 = +* Fix - Fix syntax for advanced filters in WC 7.8 and over + += 6.1.0 - 2023-06-28 = +* Add - Add additional validation in VAT controller. +* Add - Add Affirm and Afterpay to checkout block. +* Add - Add Affirm and Afterpay to classic checkout. +* Add - Add BNPL messaging to the product details page via Stripe Payment Method Messaging Element. +* Add - Added a check to disable WooPay in case incompatible extensions are found. +* Add - Adds implementation to handle deferred intent for the UPE in My account page. +* Add - Adds support for UPE with deferred intent creation on the Blocks checkout page. +* Add - Add Tap to Pay device type filter on transactions list page. +* Add - Add usage tracking to WooPay express button location updates. +* Add - Affirm and Afterpay logo support in transactions listing and transaction details. +* Add - Connect page incentive for eligible merchants. +* Add - Deposit schedule changes are disabled when an account has restrictions on it. +* Add - Ensure Affirm and Afterpay available on checkout only when the payment is in expected range. +* Add - Improve the wording and style of the "Active Disputes" task list item on the Payments → Overview screen to better communicate the urgency of resolving these disputes. +* Add - Links in subscription deactivation modal open in a new tab. +* Add - Show checkbox options for Affirm and Afterpay BNPL payment options. +* Add - Update Affirm & Afterpay logos with border and icon images. +* Add - Check for invalid extensions when one is activated or deactivated. +* Fix - Add deadline and amount to clarify disputed order note. +* Fix - Affirm&Afterpay: do not show messaging element for subscription products. +* Fix - Allow `card_` prefix when validating payment method IDs to fix failing subscription renewals. +* Fix - Check that a currency is available before adding it to the current currencies. Minor admin text string updates. Minor refactoring of MultiCurrency/RestController class. +* Fix - Corrected bug where checkbox could not be enabled when credit card was disabled. +* Fix - Fixed payment intents still getting confirmed on UPE when intent update fails. +* Fix - Fix untranslated strings on the checkout page. +* Fix - Fraudulent disputes will now show as Transaction unauthorized. +* Fix - Hide Google Pay and Apple Pay buttons when total amount is zero on item details, cart, and checkout pages. +* Fix - Improved user experience on onboarding form when validating fields if Enter key is pressed. +* Fix - Improve the logic which determines if a user is activating the WC Subscriptions plugin when determining the need to load built-in subscriptions functionality. +* Fix - Move WP hooks registration out of the core classes' constructors. +* Fix - Remove all actions on preflight check. +* Fix - Show descriptive dispute reasons in order notes. +* Fix - Updated correct link for request classes docs. +* Fix - Uses correct payment method title in order confirmation emails. +* Update - Display the "Active Disputes" task list item on the Payments → Overview screen only if there are disputes due within seven days. +* Update - Improve copy in Subscriptions deactivation modal. +* Update - Improve the wording of the "Active Disputes" task list item on the WooCommerce → Home screen to better communicate the urgency of resolving these disputes. +* Update - Moved the overview task list to the welcome greeting to improve visibility of important tasks. +* Update - Update the design for UPE settings block. +* Dev - Add support for Czech Republic, Hungary, and Sweden. +* Dev - Bump minimum required version of WooCommerce to 7.8.0. +* Dev - Comment: Add script to run QIT security tests locally. +* Dev - Gracefully handle missing payment method constants. +* Dev - minor refactor from js to tsx. +* Dev - minor tsx refactor. +* Dev - Tracking events for BNPL payment methods. + += 6.0.0 - 2023-06-08 = +* Add - Show Progressive Onboarding Express using Explat experiment +* Fix - Add a session check to avoid fatal errors. +* Fix - Add error notice when using ISK with decimals +* Fix - Adjust style of deposits icon and text in account details for progressive accounts +* Fix - Disabled subscription-related actions, webhooks, and invoice services on staging sites to avoid unintended interactions with the server and live site. +* Fix - Ensures 3DS authenticated payment methods can be saved and reused with deferred intent UPE. +* Fix - Ensures WCPay is present as default gateway on WC Settings screen when split UPE is enabled. +* Fix - Fix account status chip from restricted to pending for PO accounts +* Fix - Fixes pay for order page functionality in split and deferred intent UPE. +* Fix - Fix for PO eligible request +* Fix - Fix fraud and risk tools welcome tour copy to remove mentions of risk review. +* Fix - Fix incorrectly labelled card gateway for the enabled deferred intent creation UPE +* Fix - Fix single currency settings conversion preview for zero decimal currencies +* Fix - Fix to an API route for Progressive Onboarding feature which was previously only used in development environment. +* Fix - Just a few CSS updates for WooPay buttons. +* Fix - Progressive onboarding design fixes +* Fix - Resolves issue with multiple charge attempts during manual capture with UPE. +* Fix - Show setup deposits task for PO accounts without sales and right after acount creation +* Update - Connect account page design. +* Update - Pass the setup mode selected during Progressive Onboarding to the server onboarding init. +* Update - Update @wordpress/components to v19.8.5 +* Dev - Behind progressive onboarding feature flag – Stardardize and send PO collected merchant info to server. +* Dev - Fixes intermittently failing UPE E2E tests. +* Dev - Fix failing e2e shopper test - WC Beta +* Dev - Migrate DepositsStatus and PaymentsStatus components to Typescript +* Dev - Remove fraud and risk tools feature flag checks and tests +* Dev - Skip failing E2E refund tests. +* Dev - Tracking for account balance section on the Payments > Overview page. +* Dev - Update @woocommerce/components to v12.0.0 + += 5.9.1 - 2023-06-05 = +* Fix - Improve validation of WC analytics query filters +* Fix - Improved validation of the order key arg when redirecting to subscription's change payment method URL. +* Fix - Resolved an issue with customers being redirected to an incorrect Pay for Order URL after login. +* Dev - Update subscriptions-core to 5.7.2 = 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. diff --git a/changelog/5749-Remove-feature-flag-and-old-UI-code-for-simplify-deposits-UI b/changelog/5749-Remove-feature-flag-and-old-UI-code-for-simplify-deposits-UI deleted file mode 100644 index 5dae238eff9..00000000000 --- a/changelog/5749-Remove-feature-flag-and-old-UI-code-for-simplify-deposits-UI +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Remove the `simplifyDepositsUi` feature flag and legacy deposits UI code. diff --git a/changelog/6814-has-multi-currency-orders-query-improvement b/changelog/6814-has-multi-currency-orders-query-improvement new file mode 100644 index 00000000000..dc97afed7df --- /dev/null +++ b/changelog/6814-has-multi-currency-orders-query-improvement @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Enhanced Analytics SQL, added unit test for has_multi_currency_orders(). Improved code quality and test coverage. diff --git a/changelog/6852-get_all_customer_currencies_improvement b/changelog/6852-get_all_customer_currencies_improvement new file mode 100644 index 00000000000..09be89d6180 --- /dev/null +++ b/changelog/6852-get_all_customer_currencies_improvement @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Improved `get_all_customer_currencies` method to retrieve existing order currencies faster. diff --git a/changelog/add-5669-add-further-payment-metadata b/changelog/add-5669-add-further-payment-metadata new file mode 100644 index 00000000000..347c49daf22 --- /dev/null +++ b/changelog/add-5669-add-further-payment-metadata @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added additional meta data to payment requests diff --git a/changelog/add-5880-track-overview-balance b/changelog/add-5880-track-overview-balance deleted file mode 100644 index 6014997107b..00000000000 --- a/changelog/add-5880-track-overview-balance +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Tracking for account balance section on the Payments > Overview page. diff --git a/changelog/add-5885-deposit-detail-tracking b/changelog/add-5885-deposit-detail-tracking deleted file mode 100644 index 7fd21e8fdf7..00000000000 --- a/changelog/add-5885-deposit-detail-tracking +++ /dev/null @@ -1,3 +0,0 @@ -Significance: patch -Type: dev -Comment: Adding tracks event when downloading deposit transactions diff --git a/changelog/add-5928-po-express-explat-experiment b/changelog/add-5928-po-express-explat-experiment deleted file mode 100644 index abc88cb3f81..00000000000 --- a/changelog/add-5928-po-express-explat-experiment +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Show Progressive Onboarding Express using Explat experiment diff --git a/changelog/add-6034-feature-flag-bnlp b/changelog/add-6034-feature-flag-bnlp deleted file mode 100644 index bdbb557ddf5..00000000000 --- a/changelog/add-6034-feature-flag-bnlp +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This merge is a feature flog utilities that will be made available for masses only in later stages of the project - - diff --git a/changelog/add-6158-progressive-onboarding-feedback b/changelog/add-6158-progressive-onboarding-feedback deleted file mode 100644 index ef00aec5eab..00000000000 --- a/changelog/add-6158-progressive-onboarding-feedback +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Progressive onboarding design fixes diff --git a/changelog/add-6429-warn-about-dev-mode-on-new-onboarding b/changelog/add-6429-warn-about-dev-mode-on-new-onboarding new file mode 100644 index 00000000000..386c3f79ba7 --- /dev/null +++ b/changelog/add-6429-warn-about-dev-mode-on-new-onboarding @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Warn about dev mode enabled on new onboarding flow choice diff --git a/changelog/add-6874-add-kanji-kana b/changelog/add-6874-add-kanji-kana new file mode 100644 index 00000000000..ecd1574347c --- /dev/null +++ b/changelog/add-6874-add-kanji-kana @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Support kanji and kana statement descriptors for Japanese merchants diff --git a/changelog/add-6923-dispute-details-notice b/changelog/add-6923-dispute-details-notice new file mode 100644 index 00000000000..027a1fa2448 --- /dev/null +++ b/changelog/add-6923-dispute-details-notice @@ -0,0 +1,3 @@ +Significance: patch +Type: add +Comment: Dispute notice added to transactions screen behind a feature flag. diff --git a/changelog/add-6924-dispute-details-attributes b/changelog/add-6924-dispute-details-attributes new file mode 100644 index 00000000000..0e2dfaa37d9 --- /dev/null +++ b/changelog/add-6924-dispute-details-attributes @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Add dispute details to transaction page, hidden behind feature flag. + + diff --git a/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice b/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice new file mode 100644 index 00000000000..3b03ed5b61e --- /dev/null +++ b/changelog/add-6966-transaction-details-dispute-challenge-in-progress-notice @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Behind feature flag: add staged dispute notice to Transaction Details screen + + diff --git a/changelog/add-7048-banner-notice-component b/changelog/add-7048-banner-notice-component new file mode 100644 index 00000000000..f9ccb0504d8 --- /dev/null +++ b/changelog/add-7048-banner-notice-component @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Add BannerNotice component, with no major UI difference for merchants. + + diff --git a/changelog/add-frt-review-feature-flag b/changelog/add-frt-review-feature-flag deleted file mode 100644 index 1245fe44c18..00000000000 --- a/changelog/add-frt-review-feature-flag +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This will be updated later. - - diff --git a/changelog/add-incentive-task-badge b/changelog/add-incentive-task-badge new file mode 100644 index 00000000000..f2a30452565 --- /dev/null +++ b/changelog/add-incentive-task-badge @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add onboarding task incentive badge. diff --git a/changelog/add-new-fraud-tools b/changelog/add-new-fraud-tools deleted file mode 100644 index 58df4dbbbb8..00000000000 --- a/changelog/add-new-fraud-tools +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Enhanced fraud protection for your store. Reduce fraudulent transactions by using a set of customizable rules. diff --git a/changelog/add-pay-for-order b/changelog/add-pay-for-order new file mode 100644 index 00000000000..b44cf523113 --- /dev/null +++ b/changelog/add-pay-for-order @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add the express button on the pay for order page diff --git a/changelog/add-s2939-store-onboarding-collected-data b/changelog/add-s2939-store-onboarding-collected-data deleted file mode 100644 index ad986b3c7bd..00000000000 --- a/changelog/add-s2939-store-onboarding-collected-data +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Behind progressive onboarding feature flag – Stardardize and send PO collected merchant info to server. diff --git a/changelog/add-use-site-logo-when-no-woopay-logo-defined b/changelog/add-use-site-logo-when-no-woopay-logo-defined new file mode 100644 index 00000000000..0afbfccf655 --- /dev/null +++ b/changelog/add-use-site-logo-when-no-woopay-logo-defined @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Fall back to site logo when a custom WooPay logo has not been defined diff --git a/changelog/deferred-upe-rollout-notices b/changelog/deferred-upe-rollout-notices new file mode 100644 index 00000000000..231d4bb7942 --- /dev/null +++ b/changelog/deferred-upe-rollout-notices @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Add notice for legacy UPE users about deferred UPE upcoming, and adjust wording for non-UPE users diff --git a/changelog/dev-6311-automatically-update-wcpay-dev-tools b/changelog/dev-6311-automatically-update-wcpay-dev-tools deleted file mode 100644 index e6519aa2963..00000000000 --- a/changelog/dev-6311-automatically-update-wcpay-dev-tools +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: We auto-update the local dev clone of WCPay Dev Tools. - - diff --git a/changelog/dev-6314-fix-instant-deposits-unused-var b/changelog/dev-6314-fix-instant-deposits-unused-var deleted file mode 100644 index 7e9dda52156..00000000000 --- a/changelog/dev-6314-fix-instant-deposits-unused-var +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: No changelog required, a very minor fix for a linting issue, not user-facing - - diff --git a/changelog/dev-6441-inbox-notifications-update b/changelog/dev-6441-inbox-notifications-update new file mode 100644 index 00000000000..b93787b65af --- /dev/null +++ b/changelog/dev-6441-inbox-notifications-update @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Update inbox note logic to prevent prompt to set up payment methods from showing on not fully onboarded account. diff --git a/changelog/dev-6779-po-new-task b/changelog/dev-6779-po-new-task new file mode 100644 index 00000000000..c49c78654fa --- /dev/null +++ b/changelog/dev-6779-po-new-task @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. diff --git a/changelog/fix-5884-deposits-list-tracks b/changelog/dev-add-cli-command similarity index 50% rename from changelog/fix-5884-deposits-list-tracks rename to changelog/dev-add-cli-command index 3ec015ea22f..4451c5b4326 100644 --- a/changelog/fix-5884-deposits-list-tracks +++ b/changelog/dev-add-cli-command @@ -1,5 +1,5 @@ Significance: patch Type: dev -Comment: covered by PR #6188 +Comment: It is only dev-facing. diff --git a/changelog/dev-add-dispute-summary-row-tests b/changelog/dev-add-dispute-summary-row-tests new file mode 100644 index 00000000000..1aba1754bb0 --- /dev/null +++ b/changelog/dev-add-dispute-summary-row-tests @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Not user-facing: updates tests for DisputeDetails component only. + + diff --git a/changelog/dev-bump-min-wc-8-1-php-7-4 b/changelog/dev-bump-min-wc-8-1-php-7-4 new file mode 100644 index 00000000000..989290fa6f3 --- /dev/null +++ b/changelog/dev-bump-min-wc-8-1-php-7-4 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Bump minimum required version of WooCommerce to 7.7 and PHP to 7.4. diff --git a/changelog/dev-details-link-ts-migration b/changelog/dev-details-link-ts-migration new file mode 100644 index 00000000000..daaa601b05b --- /dev/null +++ b/changelog/dev-details-link-ts-migration @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate DetailsLink component to TypeScript to improve code quality diff --git a/changelog/dev-fix-po-eligible-route b/changelog/dev-fix-po-eligible-route deleted file mode 100644 index 6e192190665..00000000000 --- a/changelog/dev-fix-po-eligible-route +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix to an API route for Progressive Onboarding feature which was previously only used in development environment. diff --git a/changelog/dev-overview-balance-card-welcome-split b/changelog/dev-overview-balance-card-welcome-split deleted file mode 100644 index b10c0354ed9..00000000000 --- a/changelog/dev-overview-balance-card-welcome-split +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: No changelog required: insignificant change, preparation for a future user-facing change. - - diff --git a/changelog/dev-remove-v1-experiment b/changelog/dev-remove-v1-experiment new file mode 100644 index 00000000000..f4d0231167e --- /dev/null +++ b/changelog/dev-remove-v1-experiment @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Remove reference to old experiment. diff --git a/changelog/dev-skip-merchant-failing-e2e-tests b/changelog/dev-skip-merchant-failing-e2e-tests deleted file mode 100644 index eae639e2879..00000000000 --- a/changelog/dev-skip-merchant-failing-e2e-tests +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Skip failing E2E refund tests. diff --git a/changelog/dev-ubuntu-workflow b/changelog/dev-ubuntu-workflow new file mode 100644 index 00000000000..79c42cc4fc7 --- /dev/null +++ b/changelog/dev-ubuntu-workflow @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Comment: Update occurence of all ubuntu versions to ubuntu-latest diff --git a/changelog/dev-update-dependencies-doc b/changelog/dev-update-dependencies-doc deleted file mode 100644 index 45c91a8bf9f..00000000000 --- a/changelog/dev-update-dependencies-doc +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Clean up the dependencies doc - - diff --git a/changelog/dev-update-pre-release-workflow b/changelog/dev-update-pre-release-workflow deleted file mode 100644 index edee13dbd32..00000000000 --- a/changelog/dev-update-pre-release-workflow +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Minor adjustement to the create-pre-release workflow - - diff --git a/changelog/dev-update-workflows b/changelog/dev-update-workflows new file mode 100644 index 00000000000..cdab2b4fa9f --- /dev/null +++ b/changelog/dev-update-workflows @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Comment: Update GH workflows to use PHP version from plugin file. diff --git a/changelog/fix-3379-remove-review-from-frt-tour-copy b/changelog/fix-3379-remove-review-from-frt-tour-copy deleted file mode 100644 index 4364edff069..00000000000 --- a/changelog/fix-3379-remove-review-from-frt-tour-copy +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix fraud and risk tools welcome tour copy to remove mentions of risk review. diff --git a/changelog/fix-4528-wrong-multi-currency-preview-for-zero-decimal-currencies b/changelog/fix-4528-wrong-multi-currency-preview-for-zero-decimal-currencies deleted file mode 100644 index 409ae3ffda6..00000000000 --- a/changelog/fix-4528-wrong-multi-currency-preview-for-zero-decimal-currencies +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix single currency settings conversion preview for zero decimal currencies diff --git a/changelog/fix-5025-3ds-auth-checkout b/changelog/fix-5025-3ds-auth-checkout deleted file mode 100644 index 6364b0ec378..00000000000 --- a/changelog/fix-5025-3ds-auth-checkout +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Ensures 3DS authenticated payment methods can be saved and reused with deferred intent UPE. diff --git a/changelog/fix-5507-block-currency-update-on-sub-switch b/changelog/fix-5507-block-currency-update-on-sub-switch new file mode 100644 index 00000000000..9fd0d5e5019 --- /dev/null +++ b/changelog/fix-5507-block-currency-update-on-sub-switch @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches. diff --git a/changelog/fix-5780-multiple-manual-captures-with-upe b/changelog/fix-5780-multiple-manual-captures-with-upe deleted file mode 100644 index af0717ce68a..00000000000 --- a/changelog/fix-5780-multiple-manual-captures-with-upe +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Resolves issue with multiple charge attempts during manual capture with UPE. diff --git a/changelog/fix-5808-upe-pay-for-order-page b/changelog/fix-5808-upe-pay-for-order-page deleted file mode 100644 index ce2fe9a1cb0..00000000000 --- a/changelog/fix-5808-upe-pay-for-order-page +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixes pay for order page functionality in split and deferred intent UPE. diff --git a/changelog/fix-6053-overview-deposit-tooltip-keyboard-navigation b/changelog/fix-6053-overview-deposit-tooltip-keyboard-navigation deleted file mode 100644 index 014e9318829..00000000000 --- a/changelog/fix-6053-overview-deposit-tooltip-keyboard-navigation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix keyboard navigation for account balance tooltips on the Payments → Overview screen. diff --git a/changelog/fix-6183-exchange-date b/changelog/fix-6183-exchange-date new file mode 100644 index 00000000000..240f51b7e1a --- /dev/null +++ b/changelog/fix-6183-exchange-date @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Multi-currency exchange rate date format when using custom date or time settings. diff --git a/changelog/fix-6192-currency-switcher-block-on-windows b/changelog/fix-6192-currency-switcher-block-on-windows new file mode 100644 index 00000000000..ba18cbf9042 --- /dev/null +++ b/changelog/fix-6192-currency-switcher-block-on-windows @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Currency Switcher Block flag rendering on Windows platform. diff --git a/changelog/fix-6208-fatal-due-to-null-session b/changelog/fix-6208-fatal-due-to-null-session deleted file mode 100644 index b4442f65dc9..00000000000 --- a/changelog/fix-6208-fatal-due-to-null-session +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add a session check to avoid fatal errors. diff --git a/changelog/fix-6230-prevent-redundant-checkout-error b/changelog/fix-6230-prevent-redundant-checkout-error deleted file mode 100644 index 6c32b6bbccf..00000000000 --- a/changelog/fix-6230-prevent-redundant-checkout-error +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Small bug fix pertaining only to upe deferred intent work, behind feature flag that hasn't yet been enabled. - - diff --git a/changelog/fix-6232 b/changelog/fix-6232 deleted file mode 100644 index c8d5c263355..00000000000 --- a/changelog/fix-6232 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add error notice when using ISK with decimals diff --git a/changelog/fix-6279-show-wcpay-on-main-settings-screen b/changelog/fix-6279-show-wcpay-on-main-settings-screen deleted file mode 100644 index 0032548c6d6..00000000000 --- a/changelog/fix-6279-show-wcpay-on-main-settings-screen +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Ensures WCPay is present as default gateway on WC Settings screen when split UPE is enabled. diff --git a/changelog/fix-6298-remove-prototype-mention b/changelog/fix-6298-remove-prototype-mention deleted file mode 100644 index b6d6f6f4a59..00000000000 --- a/changelog/fix-6298-remove-prototype-mention +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: update -Comment: Minimal change to remove a mention to the past prototype - - diff --git a/changelog/fix-6322-update-upe-e2e-tests b/changelog/fix-6322-update-upe-e2e-tests deleted file mode 100644 index e01d7b89ecf..00000000000 --- a/changelog/fix-6322-update-upe-e2e-tests +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Fixes intermittently failing UPE E2E tests. diff --git a/changelog/fix-6429-follow-up-fix b/changelog/fix-6429-follow-up-fix new file mode 100644 index 00000000000..426b76ace57 --- /dev/null +++ b/changelog/fix-6429-follow-up-fix @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Warn about dev mode roll back to inline notice + + diff --git a/changelog/fix-6633-sar-aed-currencies-formatting b/changelog/fix-6633-sar-aed-currencies-formatting new file mode 100644 index 00000000000..160ef981508 --- /dev/null +++ b/changelog/fix-6633-sar-aed-currencies-formatting @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixes the currency formatting for AED and SAR currencies. diff --git a/changelog/fix-6951-validate-set-title-for-email b/changelog/fix-6951-validate-set-title-for-email new file mode 100644 index 00000000000..8b2a138daf0 --- /dev/null +++ b/changelog/fix-6951-validate-set-title-for-email @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Minor bug fix only adding a defensive check + + diff --git a/changelog/fix-6981-missing-onboarding-field-data b/changelog/fix-6981-missing-onboarding-field-data new file mode 100644 index 00000000000..d8170c0d31d --- /dev/null +++ b/changelog/fix-6981-missing-onboarding-field-data @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Avoid empty fields in new onboarding flow diff --git a/changelog/fix-7061-extended-requests b/changelog/fix-7061-extended-requests new file mode 100644 index 00000000000..1c62343b3c8 --- /dev/null +++ b/changelog/fix-7061-extended-requests @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Allow request classes to be extended more than once. diff --git a/changelog/fix-7067-admin-enqueue-scripts-priority b/changelog/fix-7067-admin-enqueue-scripts-priority new file mode 100644 index 00000000000..d23c2da6dd7 --- /dev/null +++ b/changelog/fix-7067-admin-enqueue-scripts-priority @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Increase admin enqueue scripts priority to avoid compatibility issues with WooCommerce Beta Tester plugin. diff --git a/changelog/fix-7135-currency-switch-widget-on-edit-post b/changelog/fix-7135-currency-switch-widget-on-edit-post new file mode 100644 index 00000000000..357711e2484 --- /dev/null +++ b/changelog/fix-7135-currency-switch-widget-on-edit-post @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Multicurrency widget error on post/page edit screen diff --git a/changelog/fix-7149-fix-cancel-authorization-flaky-error-response b/changelog/fix-7149-fix-cancel-authorization-flaky-error-response new file mode 100644 index 00000000000..27cbbbbeea7 --- /dev/null +++ b/changelog/fix-7149-fix-cancel-authorization-flaky-error-response @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Corrected an issue causing incorrect responses at the cancel authorization API endpoint. diff --git a/changelog/fix-changelog-from-pr-6225 b/changelog/fix-changelog-from-pr-6225 deleted file mode 100644 index b0dc8af92d8..00000000000 --- a/changelog/fix-changelog-from-pr-6225 +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: No need changelog entry. - - diff --git a/changelog/fix-deprecation-warning-on-blocks-checkout b/changelog/fix-deprecation-warning-on-blocks-checkout new file mode 100644 index 00000000000..ae1241fc85a --- /dev/null +++ b/changelog/fix-deprecation-warning-on-blocks-checkout @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix deprecation warnings on blocks checkout. diff --git a/changelog/fix-docs-links-part-2 b/changelog/fix-docs-links-part-2 new file mode 100644 index 00000000000..6f30cc936c2 --- /dev/null +++ b/changelog/fix-docs-links-part-2 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update outdated public documentation links on WooCommerce.com diff --git a/changelog/fix-improve-transaction-details-redirect b/changelog/fix-improve-transaction-details-redirect new file mode 100644 index 00000000000..d61f18b2f6c --- /dev/null +++ b/changelog/fix-improve-transaction-details-redirect @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Improve the transaction details redirect user-experience by using client-side routing. diff --git a/changelog/fix-incorrectly-labelled-gateway-deferred-intent-upe b/changelog/fix-incorrectly-labelled-gateway-deferred-intent-upe deleted file mode 100644 index 49d29c4cb1b..00000000000 --- a/changelog/fix-incorrectly-labelled-gateway-deferred-intent-upe +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix incorrectly labelled card gateway for the enabled deferred intent creation UPE diff --git a/changelog/fix-invalid-currency-from-store-api-request b/changelog/fix-invalid-currency-from-store-api-request new file mode 100644 index 00000000000..38f9ee6bc4b --- /dev/null +++ b/changelog/fix-invalid-currency-from-store-api-request @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix WooPay Session Handler in Store API requests. diff --git a/changelog/fix-plugin-upgrade b/changelog/fix-plugin-upgrade deleted file mode 100644 index 4f4b515c90b..00000000000 --- a/changelog/fix-plugin-upgrade +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Temporary fix to hide an error notice that's printed during the plugin upgrade - - diff --git a/changelog/fix-remove-unused-import-noticeoutlineicon b/changelog/fix-remove-unused-import-noticeoutlineicon new file mode 100644 index 00000000000..07e246aa8fc --- /dev/null +++ b/changelog/fix-remove-unused-import-noticeoutlineicon @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: **N/A** this is a fix for a code linting issue + + diff --git a/changelog/fix-request-constant-traversing b/changelog/fix-request-constant-traversing new file mode 100644 index 00000000000..0449e00a842 --- /dev/null +++ b/changelog/fix-request-constant-traversing @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix the way request params are loaded between parent and child classes. diff --git a/changelog/fix-title-task-continue-onboarding b/changelog/fix-title-task-continue-onboarding new file mode 100644 index 00000000000..84241736c04 --- /dev/null +++ b/changelog/fix-title-task-continue-onboarding @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Modify title in task to continue with onboarding diff --git a/changelog/fix-type-cast-woo-stats b/changelog/fix-woopay-appearance-width similarity index 50% rename from changelog/fix-type-cast-woo-stats rename to changelog/fix-woopay-appearance-width index 1239c2df01c..5a11ed32a87 100644 --- a/changelog/fix-type-cast-woo-stats +++ b/changelog/fix-woopay-appearance-width @@ -1,4 +1,4 @@ Significance: patch Type: fix -Fix for PO eligible request +fix checkout appearance width diff --git a/changelog/imp-7052-migrate-to-ts b/changelog/imp-7052-migrate-to-ts new file mode 100644 index 00000000000..1f1fa0afad0 --- /dev/null +++ b/changelog/imp-7052-migrate-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate link-item.js to typescript diff --git a/changelog/imp-7052-migrate-to-ts-woopay b/changelog/imp-7052-migrate-to-ts-woopay new file mode 100644 index 00000000000..f866f25573f --- /dev/null +++ b/changelog/imp-7052-migrate-to-ts-woopay @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate woopay-item to typescript diff --git a/changelog/issue-6526-schedule-subscription-migration-tool b/changelog/issue-6526-schedule-subscription-migration-tool new file mode 100644 index 00000000000..391c0a20ddd --- /dev/null +++ b/changelog/issue-6526-schedule-subscription-migration-tool @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This PR is part of a larger feature coming to WCPay and not single entry is needed for this PR. + + diff --git a/changelog/issue-7038-retry-migration b/changelog/issue-7038-retry-migration new file mode 100644 index 00000000000..62a83cfe6ef --- /dev/null +++ b/changelog/issue-7038-retry-migration @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: No changelog added. This feature is part of a bigger migration feature coming to WCPay + + diff --git a/changelog/remove-3026-fraud-and-risk-tools-feature-flag b/changelog/remove-3026-fraud-and-risk-tools-feature-flag deleted file mode 100644 index c506ba2cca0..00000000000 --- a/changelog/remove-3026-fraud-and-risk-tools-feature-flag +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Remove fraud and risk tools feature flag checks and tests diff --git a/changelog/rpp-6679-factor-flags b/changelog/rpp-6679-factor-flags new file mode 100644 index 00000000000..60d7f3c7a0a --- /dev/null +++ b/changelog/rpp-6679-factor-flags @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Adding factor flags to control when to enter the new payment process. diff --git a/changelog/rpp-6684-request-class b/changelog/rpp-6684-request-class new file mode 100644 index 00000000000..76f23856692 --- /dev/null +++ b/changelog/rpp-6684-request-class @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add payment request class for loading, sanitizing, and escaping data (reengineering payment process) diff --git a/changelog/rpp-6685-load-payment-methods b/changelog/rpp-6685-load-payment-methods new file mode 100644 index 00000000000..82d45e02d4c --- /dev/null +++ b/changelog/rpp-6685-load-payment-methods @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Load payment methods through the request class (re-engineering payment process). diff --git a/changelog/stripe-billing-migration-notices b/changelog/stripe-billing-migration-notices new file mode 100644 index 00000000000..55bfc08a088 --- /dev/null +++ b/changelog/stripe-billing-migration-notices @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. diff --git a/changelog/stripe-billing-setting b/changelog/stripe-billing-setting new file mode 100644 index 00000000000..8feb7c76c0f --- /dev/null +++ b/changelog/stripe-billing-setting @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Introduce a new setting that enables store to opt into Subscription off-site billing via Stripe Billing. diff --git a/changelog/subscriptions-core-5.7.0 b/changelog/subscriptions-core-5.7.0 deleted file mode 100644 index fc111cb6fc7..00000000000 --- a/changelog/subscriptions-core-5.7.0 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fatal error from third-party extensions using the `woocommerce_update_order` expecting the second parameter. diff --git a/changelog/subscriptions-core-5.7.0-1 b/changelog/subscriptions-core-5.7.0-1 deleted file mode 100644 index 1063c5b2041..00000000000 --- a/changelog/subscriptions-core-5.7.0-1 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Pass the subscription object as the second parameter to `woocommerce_update_subscription` hook (and `woocommerce_update_order` for backwards compatibility). diff --git a/changelog/subscriptions-core-5.7.0-2 b/changelog/subscriptions-core-5.7.0-2 deleted file mode 100644 index d04dd3e5d90..00000000000 --- a/changelog/subscriptions-core-5.7.0-2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Return a response from the WC_Subscription::set_status() function in line with the parent WC_Order::set_status() function. diff --git a/changelog/subscriptions-core-5.7.0-3 b/changelog/subscriptions-core-5.7.0-3 deleted file mode 100644 index b75b1bd8c29..00000000000 --- a/changelog/subscriptions-core-5.7.0-3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: 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. diff --git a/changelog/subscriptions-core-5.7.1-1 b/changelog/subscriptions-core-5.7.1-1 deleted file mode 100644 index 490d45e05fc..00000000000 --- a/changelog/subscriptions-core-5.7.1-1 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: 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. diff --git a/changelog/subscriptions-core-5.7.1-2 b/changelog/subscriptions-core-5.7.1-2 deleted file mode 100644 index 66956a265f2..00000000000 --- a/changelog/subscriptions-core-5.7.1-2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Enable third-party code to alter the delete payment token URL returned from flag_subscription_payment_token_deletions. diff --git a/changelog/subscriptions-core-5.7.1-3 b/changelog/subscriptions-core-5.7.1-3 deleted file mode 100644 index 8c711e21a9d..00000000000 --- a/changelog/subscriptions-core-5.7.1-3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update subscriptions-core to 5.7.1 diff --git a/changelog/subscriptions-core-6.2.0-1 b/changelog/subscriptions-core-6.2.0-1 new file mode 100644 index 00000000000..2aa534e189f --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-1 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Ensure the shipping phone number field is copied to subscriptions and their orders when copying address meta. diff --git a/changelog/subscriptions-core-6.2.0-2 b/changelog/subscriptions-core-6.2.0-2 new file mode 100644 index 00000000000..1d4cd07d2c0 --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-2 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache to improve performance. diff --git a/changelog/subscriptions-core-6.2.0-3 b/changelog/subscriptions-core-6.2.0-3 new file mode 100644 index 00000000000..0244ee6dbd1 --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-3 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Deprecated the 'woocommerce_subscriptions_not_found_label' filter. diff --git a/changelog/subscriptions-core-6.2.0-4 b/changelog/subscriptions-core-6.2.0-4 new file mode 100644 index 00000000000..9ad82198e89 --- /dev/null +++ b/changelog/subscriptions-core-6.2.0-4 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Updated subscriptions-core to 6.2.0 diff --git a/changelog/update-5508-connect-account-page b/changelog/temporarily-disable-saving-sepa similarity index 51% rename from changelog/update-5508-connect-account-page rename to changelog/temporarily-disable-saving-sepa index 6e1f3752b18..53e329f4089 100644 --- a/changelog/update-5508-connect-account-page +++ b/changelog/temporarily-disable-saving-sepa @@ -1,4 +1,4 @@ Significance: minor Type: update -Connect account page design. +Temporarily disable saving SEPA diff --git a/changelog/update-6027-fix-e2e-test-for-wcpay-dev-tools-revamp b/changelog/update-6027-fix-e2e-test-for-wcpay-dev-tools-revamp deleted file mode 100644 index 4b074efe346..00000000000 --- a/changelog/update-6027-fix-e2e-test-for-wcpay-dev-tools-revamp +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This just fixes e2e tests to work with the new WCPay Dev Tools UI markup. - - diff --git a/changelog/update-6186-mc-settings-links b/changelog/update-6186-mc-settings-links new file mode 100644 index 00000000000..3bff5d53ef6 --- /dev/null +++ b/changelog/update-6186-mc-settings-links @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Multi-currency documentation links. diff --git a/changelog/update-6378-disable-refund-button-when-disputed b/changelog/update-6378-disable-refund-button-when-disputed new file mode 100644 index 00000000000..6f9f041c7d6 --- /dev/null +++ b/changelog/update-6378-disable-refund-button-when-disputed @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Disable refund button on order edit page when there is active or lost dispute. diff --git a/changelog/update-6991-table-tooltip b/changelog/update-6991-table-tooltip new file mode 100644 index 00000000000..9c50fe720f6 --- /dev/null +++ b/changelog/update-6991-table-tooltip @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Tooltip component on ConvertedAmount. diff --git a/changelog/update-7048-inline-notice-component b/changelog/update-7048-inline-notice-component new file mode 100644 index 00000000000..debb19e3fe5 --- /dev/null +++ b/changelog/update-7048-inline-notice-component @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Mainly code refactor and component updates without noticiable UI changes. + + diff --git a/changelog/update-7098-onboarding-components b/changelog/update-7098-onboarding-components new file mode 100644 index 00000000000..3ecd11636ad --- /dev/null +++ b/changelog/update-7098-onboarding-components @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Ensure new onboarding components (behind an experiment) use admin color schema. + + diff --git a/changelog/update-base-constant-return-same-object-static-call b/changelog/update-base-constant-return-same-object-static-call new file mode 100644 index 00000000000..d15f76ec43e --- /dev/null +++ b/changelog/update-base-constant-return-same-object-static-call @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update Base_Constant to return the singleton object for same static calls. diff --git a/changelog/update-dependency-woocommerce-components b/changelog/update-dependency-woocommerce-components deleted file mode 100644 index 2bffa38e39b..00000000000 --- a/changelog/update-dependency-woocommerce-components +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Update @woocommerce/components to v12.0.0 diff --git a/changelog/update-horizontal-list-label-style-uppercase b/changelog/update-horizontal-list-label-style-uppercase new file mode 100644 index 00000000000..b7a71d2c30c --- /dev/null +++ b/changelog/update-horizontal-list-label-style-uppercase @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: No changelog required: a particularly insignificant UI change. + + diff --git a/changelog/update-stripe-billing-notice-links b/changelog/update-stripe-billing-notice-links new file mode 100644 index 00000000000..8f2ad33678b --- /dev/null +++ b/changelog/update-stripe-billing-notice-links @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: No changelog is needed given these notices are unreleased. + + diff --git a/changelog/update-wordpress-components-dependency b/changelog/update-wordpress-components-dependency deleted file mode 100644 index 95b2a657f80..00000000000 --- a/changelog/update-wordpress-components-dependency +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update @wordpress/components to v19.8.5 diff --git a/client/account-status-settings/index.js b/client/account-status-settings/index.js index 53e5dc3526a..3365c54cf26 100644 --- a/client/account-status-settings/index.js +++ b/client/account-status-settings/index.js @@ -42,12 +42,12 @@ const renderDepositsStatus = ( { deposits } ) => { const renderAccountStatusDescription = ( accountStatus ) => { const { status, currentDeadline, pastDue, accountLink } = accountStatus; - if ( 'complete' === status ) { + if ( status === 'complete' ) { return ''; } let description = ''; - if ( 'restricted_soon' === status ) { + if ( status === 'restricted_soon' ) { description = createInterpolateElement( sprintf( /* translators: %s - formatted requirements current deadline, - dashboard login URL */ @@ -63,7 +63,7 @@ const renderAccountStatusDescription = ( accountStatus ) => { // eslint-disable-next-line jsx-a11y/anchor-has-content { a: } ); - } else if ( 'restricted' === status && pastDue ) { + } else if ( status === 'restricted' && pastDue ) { description = createInterpolateElement( /* translators: - dashboard login URL */ __( @@ -73,22 +73,28 @@ const renderAccountStatusDescription = ( accountStatus ) => { // eslint-disable-next-line jsx-a11y/anchor-has-content { a: } ); - } else if ( 'restricted_partially' === status ) { + } else if ( status === 'restricted_partially' ) { description = __( 'Some payment methods and deposits are disabled for this account until all required documents are provided.', 'woocommerce-payments' ); - } else if ( 'restricted' === status ) { + } else if ( status === 'enabled' ) { + description = __( + // eslint-disable-next-line max-len + 'This account is in good standing. Additional business information might be required when a payment volume threshold is reached.', + 'woocommerce-payments' + ); + } else if ( status === 'restricted' ) { description = __( 'Payments and deposits are disabled for this account until business information is verified by the payment processor.', 'woocommerce-payments' ); - } else if ( 'rejected.fraud' === status ) { + } else if ( status === 'rejected.fraud' ) { description = __( 'This account has been rejected because of suspected fraudulent activity.', 'woocommerce-payments' ); - } else if ( 'rejected.terms_of_service' === status ) { + } else if ( status === 'rejected.terms_of_service' ) { description = __( 'This account has been rejected due to a Terms of Service violation.', 'woocommerce-payments' @@ -123,7 +129,13 @@ const AccountStatus = ( props ) => { return (
- + { renderPaymentsStatus( accountStatus.paymentsEnabled ) } { renderDepositsStatus( { deposits: accountStatus.deposits } ) }
diff --git a/client/account-status-settings/test/__snapshots__/index.js.snap b/client/account-status-settings/test/__snapshots__/index.js.snap index 8195f567ae4..2f8a5fe8a91 100644 --- a/client/account-status-settings/test/__snapshots__/index.js.snap +++ b/client/account-status-settings/test/__snapshots__/index.js.snap @@ -5,7 +5,7 @@ exports[`AccountStatus renders connected account 1`] = `
Complete @@ -66,7 +66,7 @@ exports[`AccountStatus renders manual deposits 1`] = `
Complete @@ -126,11 +126,35 @@ exports[`AccountStatus renders partially restricted account 1`] = ` Object { "asFragment": [Function], "baseElement": + +
+
Unknown @@ -188,7 +212,7 @@ Object {
Unknown @@ -300,7 +324,7 @@ exports[`AccountStatus renders rejected.fraud account 1`] = `
Rejected @@ -365,7 +389,7 @@ exports[`AccountStatus renders rejected.other account 1`] = `
Rejected @@ -430,7 +454,7 @@ exports[`AccountStatus renders rejected.terms_of_service account 1`] = `
Rejected @@ -494,11 +518,35 @@ exports[`AccountStatus renders restricted account 1`] = ` Object { "asFragment": [Function], "baseElement": + +
+
Restricted @@ -561,7 +609,7 @@ Object {
Restricted @@ -677,11 +725,35 @@ exports[`AccountStatus renders restricted account with overdue requirements 1`] Object { "asFragment": [Function], "baseElement": + +
+
Restricted @@ -749,7 +821,7 @@ Object {
Restricted @@ -871,7 +943,7 @@ exports[`AccountStatus renders restricted soon account 1`] = `
Restricted soon diff --git a/client/account-status-settings/test/index.js b/client/account-status-settings/test/index.js index f4daa6f8b2b..9b25d97aaf6 100644 --- a/client/account-status-settings/test/index.js +++ b/client/account-status-settings/test/index.js @@ -22,6 +22,10 @@ describe( 'AccountStatus', () => { status: 'enabled', interval: 'daily', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 0, accountLink: '', } ); @@ -36,6 +40,10 @@ describe( 'AccountStatus', () => { status: 'enabled', interval: 'daily', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 1583844589, accountLink: '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments&wcpay-login=1', @@ -51,6 +59,10 @@ describe( 'AccountStatus', () => { status: 'disabled', interval: '', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 1583844589, pastDue: true, accountLink: @@ -67,6 +79,10 @@ describe( 'AccountStatus', () => { status: 'disabled', interval: 'daily', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 1583844589, pastDue: false, accountLink: '', @@ -82,6 +98,10 @@ describe( 'AccountStatus', () => { status: 'disabled', interval: 'daily', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 1583844589, pastDue: false, accountLink: '', @@ -97,6 +117,10 @@ describe( 'AccountStatus', () => { status: 'disabled', interval: 'monthly', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 0, accountLink: '', } ); @@ -111,6 +135,10 @@ describe( 'AccountStatus', () => { status: 'disabled', interval: 'daily', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 0, accountLink: '', } ); @@ -125,6 +153,10 @@ describe( 'AccountStatus', () => { status: 'disabled', interval: 'daily', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 0, accountLink: '', } ); @@ -139,6 +171,10 @@ describe( 'AccountStatus', () => { status: 'enabled', interval: 'manual', }, + progressiveOnboarding: { + isEnabled: false, + isComplete: false, + }, currentDeadline: 0, accountLink: '', } ); diff --git a/client/additional-methods-setup/constants.js b/client/additional-methods-setup/constants.js index 4bd4f0999af..4f2222ea780 100644 --- a/client/additional-methods-setup/constants.js +++ b/client/additional-methods-setup/constants.js @@ -7,6 +7,9 @@ export const upeMethods = [ 'p24', 'sepa_debit', 'sofort', + 'affirm', + 'afterpay_clearpay', + 'jcb', ]; export const upeCapabilityStatuses = { diff --git a/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js b/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js index d4ffb57ec1a..b619ae96044 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/add-payment-methods-task.js @@ -9,7 +9,14 @@ import React, { useState, } from 'react'; import { __ } from '@wordpress/i18n'; -import { Button, Card, CardBody, ExternalLink } from '@wordpress/components'; +import { + Button, + Card, + CardBody, + ExternalLink, + CardDivider, + Notice, +} from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; /** @@ -32,6 +39,7 @@ import CurrencyInformationForMethods from '../../components/currency-information import { upeCapabilityStatuses, upeMethods } from '../constants'; import paymentMethodsMap from '../../payment-methods-map'; import ConfirmPaymentMethodActivationModal from 'wcpay/payment-methods/activation-modal'; +import './add-payment-methods-task.scss'; const usePaymentMethodsCheckboxState = () => { // For UPE, the card payment method is required and always active. @@ -117,7 +125,7 @@ const ContinueButton = ( { paymentMethodsState } ) => { return ( - - - ); -}; - -export default EnableUpePreviewTask; diff --git a/client/additional-methods-setup/upe-preview-methods-selector/enable-upe-preview-task.scss b/client/additional-methods-setup/upe-preview-methods-selector/enable-upe-preview-task.scss deleted file mode 100644 index 35d4e5212a9..00000000000 --- a/client/additional-methods-setup/upe-preview-methods-selector/enable-upe-preview-task.scss +++ /dev/null @@ -1,77 +0,0 @@ -.enable-upe-preview { - &__body { - &.is-active { - // there's so much text that on mobile the text overflows the max-height set on the collapsible body. - max-height: 1000px; - } - } - - &__advantages-wrapper { - display: flex; - flex-direction: column; - - @include breakpoint( '>660px' ) { - flex-direction: row; - justify-content: space-between; - align-items: stretch; - - > * { - width: 100%; - - &:not( :first-child ) { - margin-left: 8px; - } - } - } - } - - &__advantage { - overflow: hidden; - - &-color { - position: relative; - width: 100%; - height: 5px; - - &--for-you { - background: #007cba; - } - - &--for-customers { - background: #b35eb1; - } - - &::after { - position: absolute; - content: ' '; - width: 100%; - height: 2px; - bottom: 0; - left: 0; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - background: $white; - } - } - - svg { - fill: $gray-600; - } - - h3 { - margin: 12px 0; - font-size: 13px; - line-height: 20px; - } - - &-features-list { - li { - background: url( "data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234AB866' viewBox='0 0 24 24' height='auto' width='20'%3E%3Cpath d='M18.3 5.6L9.9 16.9l-4.6-3.4-.9 1.2 5.8 4.3 9.3-12.6z' /%3E%3C/svg%3E" ) - no-repeat left top; - padding: 0 0 0 24px; - font-size: 13px; - line-height: 1.5; - } - } - } -} diff --git a/client/additional-methods-setup/upe-preview-methods-selector/index.js b/client/additional-methods-setup/upe-preview-methods-selector/index.js index 3118ece1d24..541865d4ebb 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/index.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/index.js @@ -10,7 +10,6 @@ import { Card, CardBody } from '@wordpress/components'; import Wizard from '../wizard/wrapper'; import WizardTask from '../wizard/task'; import WizardTaskList from '../wizard/task-list'; -import EnableUpePreviewTask from './enable-upe-preview-task'; import SetupCompleteTask from './setup-complete-task'; import useIsUpeEnabled from '../../settings/wcpay-upe-toggle/hook'; import AddPaymentMethodsTask from './add-payment-methods-task'; @@ -33,9 +32,6 @@ const UpePreviewMethodsSelector = () => { } } > - - - diff --git a/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.js b/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.js index 629ab13dcbc..76077252764 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.js +++ b/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.js @@ -33,7 +33,7 @@ const SetupCompleteMessaging = () => { // we need to check that the type of `enableUpePreviewPayload` is an object - it can also just be `true` or `undefined` let addedPaymentMethodsCount = 0; if ( - 'object' === typeof enableUpePreviewPayload && + typeof enableUpePreviewPayload === 'object' && enableUpePreviewPayload.initialMethods ) { const { initialMethods } = enableUpePreviewPayload; @@ -41,7 +41,7 @@ const SetupCompleteMessaging = () => { } // can't just check for "0", some methods could have been disabled - if ( 0 >= addedPaymentMethodsCount ) { + if ( addedPaymentMethodsCount <= 0 ) { return __( 'Setup complete!', 'woocommerce-payments' ); } @@ -87,8 +87,8 @@ const SetupComplete = () => { return (

diff --git a/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.scss b/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.scss index d47134504f5..027431e01d5 100644 --- a/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.scss +++ b/client/additional-methods-setup/upe-preview-methods-selector/setup-complete-task.scss @@ -10,6 +10,7 @@ &__enabled-methods-list { display: flex; align-items: center; + flex-wrap: wrap; > *:not( :last-child ) { margin-right: $grid-unit-10; 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 ae4818e718b..98443119428 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 @@ -19,6 +19,7 @@ import { useCurrencies, useEnabledCurrencies, useManualCapture, + useAccountDomesticCurrency, } from '../../../data'; import WCPaySettingsContext from '../../../settings/wcpay-settings-context'; import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants'; @@ -31,6 +32,7 @@ jest.mock( '../../../data', () => ( { useEnabledCurrencies: jest.fn(), useGetPaymentMethodStatuses: jest.fn(), useManualCapture: jest.fn(), + useAccountDomesticCurrency: jest.fn(), } ) ); jest.mock( '@wordpress/a11y', () => ( { @@ -116,6 +118,7 @@ describe( 'AddPaymentMethodsTask', () => { }, } ); useManualCapture.mockReturnValue( [ false, jest.fn() ] ); + useAccountDomesticCurrency.mockReturnValue( 'usd' ); global.wcpaySettings = { accountEmail: 'admin@example.com', }; diff --git a/client/additional-methods-setup/upe-preview-methods-selector/test/enable-upe-preview-task.test.js b/client/additional-methods-setup/upe-preview-methods-selector/test/enable-upe-preview-task.test.js deleted file mode 100644 index 21d13142cfc..00000000000 --- a/client/additional-methods-setup/upe-preview-methods-selector/test/enable-upe-preview-task.test.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import WizardTaskContext from '../../wizard/task/context'; -import WcPayUpeContext from '../../../settings/wcpay-upe-toggle/context'; - -import EnableUpePreviewTask from '../enable-upe-preview-task'; - -describe( 'EnableUpePreviewTask', () => { - it( 'should enable the UPE flag when clicking the "Enable" button', async () => { - const setCompletedMock = jest.fn(); - const setIsUpeEnabledMock = jest.fn().mockResolvedValue( true ); - - render( - - - - - - ); - - expect( setCompletedMock ).not.toHaveBeenCalled(); - expect( setIsUpeEnabledMock ).not.toHaveBeenCalled(); - - userEvent.click( screen.getByText( 'Enable' ) ); - - expect( setIsUpeEnabledMock ).toHaveBeenCalledWith( true ); - await waitFor( () => expect( setIsUpeEnabledMock ).toHaveReturned() ); - expect( setCompletedMock ).toHaveBeenCalledWith( - true, - 'add-payment-methods' - ); - } ); -} ); diff --git a/client/additional-methods-setup/wizard/task-item.scss b/client/additional-methods-setup/wizard/task-item.scss index c4a2008f90a..c2b7dbb5ec6 100644 --- a/client/additional-methods-setup/wizard/task-item.scss +++ b/client/additional-methods-setup/wizard/task-item.scss @@ -121,7 +121,7 @@ &__payment-selector { &-wrapper { // Reduce the wrapper's bottom margin and move the difference on the description. - margin-bottom: $gap-smallest; + margin-bottom: $gap-large; } &-title { diff --git a/client/additional-methods-setup/wizard/task-list.js b/client/additional-methods-setup/wizard/task-list.js index f01ea9bb108..08aa296bc33 100644 --- a/client/additional-methods-setup/wizard/task-list.js +++ b/client/additional-methods-setup/wizard/task-list.js @@ -16,7 +16,7 @@ const WizardTaskList = ( { children } ) => { useLayoutEffect( () => { // set the focus on the next active heading. // but need to set the focus only after the first mount, only when the active task changes. - if ( true === isFirstMount.current ) { + if ( isFirstMount.current === true ) { isFirstMount.current = false; return; } diff --git a/client/banner/banner.scss b/client/banner/banner.scss index 4c38d67c92b..6ca7b6beb78 100644 --- a/client/banner/banner.scss +++ b/client/banner/banner.scss @@ -21,6 +21,10 @@ } } + .woocommerce-payments-banner-logo { + margin-left: -$gap-large; + } + .woocommerce-payments-banner-pill { /* adapted from https://github.com/woocommerce/woocommerce-admin/blob/main/packages/components/src/pill/style.scss */ font-size: 12px; diff --git a/client/banner/index.js b/client/banner/index.js index 8607d744867..7018ffae9df 100644 --- a/client/banner/index.js +++ b/client/banner/index.js @@ -17,7 +17,7 @@ const Banner = ( { style } ) => { logoHeight, showPill, className = 'woocommerce-payments-banner'; - if ( 'account-page' === style ) { + if ( style === 'account-page' ) { logoWidth = 196; logoHeight = 65; showPill = true; @@ -30,7 +30,11 @@ const Banner = ( { style } ) => { return ( - + { showPill && (

diff --git a/client/banner/wcpay-logo.js b/client/banner/wcpay-logo.js index 702866645fa..08afaddb4c7 100644 --- a/client/banner/wcpay-logo.js +++ b/client/banner/wcpay-logo.js @@ -8,7 +8,7 @@ export default ( props ) => ( src={ logoImg } width="241" height="64" - alt={ 'WooCommerce Payments logo' } + alt={ 'WooPayments logo' } { ...props } /> ); diff --git a/client/card-readers/list/index.tsx b/client/card-readers/list/index.tsx index 7275252ac79..2cd75d1a639 100644 --- a/client/card-readers/list/index.tsx +++ b/client/card-readers/list/index.tsx @@ -3,7 +3,7 @@ * External dependencies */ import React from 'react'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Card, CardBody, CardDivider } from '@wordpress/components'; /** @@ -19,10 +19,14 @@ const ReadersListDescription = () => ( <>

{ __( 'Connected card readers', 'woocommerce-payments' ) }

- { __( - 'Card readers are marked as active if they’ve processed one or more transactions durring the current billing cycle. ' + - 'To connect or disconnect card readers, use the WooCommerce Payments mobile application.', - 'woocommerce-payments' + { sprintf( + /* translators: %s: WooPayments */ + __( + 'Card readers are marked as active if they’ve processed one or more transactions during the current billing cycle. ' + + 'To connect or disconnect card readers, use the %s mobile application.', + 'woocommerce-payments' + ), + 'WooPayments' ) }

diff --git a/client/card-readers/settings/sections/address-details.js b/client/card-readers/settings/sections/address-details.js index 1b950bc5f5c..3646d6bbbd6 100644 --- a/client/card-readers/settings/sections/address-details.js +++ b/client/card-readers/settings/sections/address-details.js @@ -100,7 +100,7 @@ const AddressDetailsSection = () => { handleAddressPropertyChange( 'city', value ) } /> - { 0 < countryStatesOptions.length && ( + { countryStatesOptions.length > 0 && ( { }, ], }, + accountStatus: { + country: 'US', + }, }; } ); diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index d79ebfcd7de..e7fcee97a18 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -3,7 +3,7 @@ /** * Internal dependencies */ -import { getConfig } from 'utils/checkout'; +import { getConfig, getUPEConfig } from 'utils/checkout'; import { getPaymentRequestData, getPaymentRequestAjaxURL, @@ -41,6 +41,21 @@ export default class WCPayAPI { return new Stripe( publishableKey, options ); } + /** + * Overloaded method to get the Stripe object for UPE. Leverages the original getStripe method but before doing + * so, sets the forceNetworkSavedCards option to the proper value for the payment method type. + * forceNetworkSavedCards is currently the flag that among others determines whether or not to use the Stripe Platform on the checkout. + * + * @param {string} paymentMethodType The payment method type. + * @return {Object} The Stripe Object. + */ + getStripeForUPE( paymentMethodType ) { + this.options.forceNetworkSavedCards = getUPEConfig( + 'paymentMethodsConfig' + )[ paymentMethodType ].forceNetworkSavedCards; + return this.getStripe(); + } + /** * Generates a new instance of Stripe. * @@ -54,13 +69,14 @@ export default class WCPayAPI { forceNetworkSavedCards, locale, isUPEEnabled, + isUPEDeferredEnabled, isStripeLinkEnabled, } = this.options; if ( forceNetworkSavedCards && ! forceAccountRequest && - ! isUPEEnabled + ! ( isUPEEnabled && ! isUPEDeferredEnabled ) ) { if ( ! this.stripePlatform ) { this.stripePlatform = this.createStripe( @@ -143,11 +159,11 @@ export default class WCPayAPI { */ prepareValue( name, value ) { // Fall back to the value in `preparedCustomerData`. - if ( 'undefined' === typeof value || 0 === value.length ) { + if ( typeof value === 'undefined' || value.length === 0 ) { value = preparedCustomerData[ name ]; // `undefined` if not set. } - if ( 'undefined' !== typeof value && 0 < value.length ) { + if ( typeof value !== 'undefined' && value.length > 0 ) { return value; } } @@ -160,7 +176,7 @@ export default class WCPayAPI { */ setBillingDetail( name, value ) { const preparedValue = this.prepareValue( name, value ); - if ( 'undefined' !== typeof preparedValue ) { + if ( typeof preparedValue !== 'undefined' ) { this.args.billing_details[ name ] = preparedValue; } } @@ -173,7 +189,7 @@ export default class WCPayAPI { */ setAddressDetail( name, value ) { const preparedValue = this.prepareValue( name, value ); - if ( 'undefined' !== typeof preparedValue ) { + if ( typeof preparedValue !== 'undefined' ) { this.args.billing_details.address[ name ] = preparedValue; } } @@ -214,13 +230,13 @@ export default class WCPayAPI { return true; } - const isSetupIntent = 'si' === partials[ 1 ]; + const isSetupIntent = partials[ 1 ] === 'si'; let orderId = partials[ 2 ]; const clientSecret = partials[ 3 ]; const nonce = partials[ 4 ]; const orderPayIndex = redirectUrl.indexOf( 'order-pay' ); - const isOrderPage = -1 < orderPayIndex; + const isOrderPage = orderPayIndex > -1; // If we're on the Pay for Order page, get the order ID // directly from the URL instead of relying on the hash. @@ -314,7 +330,7 @@ export default class WCPayAPI { return verificationCall.then( ( response ) => { const result = - 'string' === typeof response + typeof response === 'string' ? JSON.parse( response ) : response; @@ -369,7 +385,7 @@ export default class WCPayAPI { throw response.data.error; } - if ( 'succeeded' === response.data.status ) { + if ( response.data.status === 'succeeded' ) { // No need for further authentication. return response.data; } @@ -465,7 +481,7 @@ export default class WCPayAPI { 'wcpay-fingerprint': fingerprint, } ) .then( ( response ) => { - if ( 'failure' === response.result ) { + if ( response.result === 'failure' ) { throw new Error( response.messages ); } return response; @@ -502,14 +518,14 @@ export default class WCPayAPI { if ( paymentIntentSecret && confirmPaymentResult.error && - 'lock_timeout' === confirmPaymentResult.error.code + confirmPaymentResult.error.code === 'lock_timeout' ) { const paymentIntentResult = await stripe.retrievePaymentIntent( decryptClientSecret( paymentIntentSecret ) ); if ( ! paymentIntentResult.error && - 'succeeded' === paymentIntentResult.paymentIntent.status + paymentIntentResult.paymentIntent.status === 'succeeded' ) { window.location.href = confirmParams.redirect_url; return paymentIntentResult; //To prevent returning an error during the redirection. @@ -567,7 +583,7 @@ export default class WCPayAPI { } ) .then( ( response ) => { - if ( 'failure' === response.result ) { + if ( response.result === 'failure' ) { throw new Error( response.messages ); } return response; diff --git a/client/checkout/blocks/confirm-card-payment.js b/client/checkout/blocks/confirm-card-payment.js index 4558ef9351f..f5235427fc6 100644 --- a/client/checkout/blocks/confirm-card-payment.js +++ b/client/checkout/blocks/confirm-card-payment.js @@ -22,7 +22,7 @@ export default async function confirmCardPayment( ); // `true` means there is no intent to confirm. - if ( true === confirmation ) { + if ( confirmation === true ) { return { type: 'success', redirectUrl: redirect, diff --git a/client/checkout/blocks/confirm-upe-payment.js b/client/checkout/blocks/confirm-upe-payment.js index c39d80d1b21..4746b37d3f9 100644 --- a/client/checkout/blocks/confirm-upe-payment.js +++ b/client/checkout/blocks/confirm-upe-payment.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; + /** * Handles the confirmation of card payments (3DSv2 modals/SCA challenge). * @@ -7,8 +12,10 @@ * @param {string|null} paymentIntentSecret Payment Intent Secret used to validate payment on rate limit error. * @param {Object} elements Reference to the UPE elements mounted on the page. * @param {Object} billingData An object containing the customer's billing data. + * @param {Object} shippingData An object containing the customer's shipping data, needed for Afterpay. * @param {Object} emitResponse Various helpers for usage with observer response objects. - * @return {Object} An object, which contains the result from the action. + * @param {string} selectedUPEPaymentType The selected UPE payment type. + * @return {Object} An object, which contains the result from the action. */ export default async function confirmUPEPayment( api, @@ -17,7 +24,9 @@ export default async function confirmUPEPayment( paymentIntentSecret, elements, billingData, - emitResponse + shippingData, + emitResponse, + selectedUPEPaymentType ) { const name = `${ billingData.first_name } ${ billingData.last_name }`.trim() || '-'; @@ -29,7 +38,7 @@ export default async function confirmUPEPayment( billing_details: { name, email: - 'string' === typeof billingData.email + typeof billingData.email === 'string' ? billingData.email.trim() : '-', phone: billingData.phone || '-', @@ -45,6 +54,23 @@ export default async function confirmUPEPayment( }, }; + // Afterpay requires shipping details to be passed. Not needed by other payment methods. + if ( PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY === selectedUPEPaymentType ) { + confirmParams.shipping = { + name: + `${ shippingData.shippingAddress.first_name } ${ shippingData.shippingAddress.last_name }`.trim() || + '-', + address: { + country: shippingData.shippingAddress.country || '_', + postal_code: shippingData.shippingAddress.postcode || '-', + state: shippingData.shippingAddress.state || '-', + city: shippingData.shippingAddress.city || '-', + line1: shippingData.shippingAddress.address_1 || '-', + line2: shippingData.shippingAddress.address_2 || '-', + }, + }; + } + if ( paymentNeeded ) { const { error } = await api.handlePaymentConfirmation( elements, diff --git a/client/checkout/blocks/fields.js b/client/checkout/blocks/fields.js index d3de01e5e46..5f47df5d286 100644 --- a/client/checkout/blocks/fields.js +++ b/client/checkout/blocks/fields.js @@ -23,10 +23,7 @@ const WCPayFields = ( { stripe, elements, billing: { billingData }, - eventRegistration: { - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - }, + eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, emitResponse, shouldSavePayment, } ) => { @@ -36,7 +33,7 @@ const WCPayFields = ( {

Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC, or any test card numbers listed{ ' ' } - + here . @@ -87,7 +84,7 @@ const WCPayFields = ( { api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, shouldSavePayment ); diff --git a/client/checkout/blocks/hooks.js b/client/checkout/blocks/hooks.js index fd52b16c93b..34a7a6f504d 100644 --- a/client/checkout/blocks/hooks.js +++ b/client/checkout/blocks/hooks.js @@ -16,21 +16,20 @@ export const usePaymentCompleteHandler = ( api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, shouldSavePayment ) => { // Once the server has completed payment processing, confirm the intent of necessary. useEffect( () => - onCheckoutAfterProcessingWithSuccess( - ( { processingResponse: { paymentDetails } } ) => - confirmCardPayment( - api, - paymentDetails, - emitResponse, - shouldSavePayment - ) + onCheckoutSuccess( ( { processingResponse: { paymentDetails } } ) => + confirmCardPayment( + api, + paymentDetails, + emitResponse, + shouldSavePayment + ) ), // not sure if we need to disable this, but kept it as-is to ensure nothing breaks. Please consider passing all the deps. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/client/checkout/blocks/saved-token-handler.js b/client/checkout/blocks/saved-token-handler.js index a2e2b0ea339..2ec311c8d5e 100644 --- a/client/checkout/blocks/saved-token-handler.js +++ b/client/checkout/blocks/saved-token-handler.js @@ -7,7 +7,7 @@ export const SavedTokenHandler = ( { api, stripe, elements, - eventRegistration: { onCheckoutAfterProcessingWithSuccess }, + eventRegistration: { onCheckoutSuccess }, emitResponse, } ) => { // Once the server has completed payment processing, confirm the intent of necessary. @@ -15,7 +15,7 @@ export const SavedTokenHandler = ( { api, stripe, elements, - onCheckoutAfterProcessingWithSuccess, + onCheckoutSuccess, emitResponse, false // No need to save a payment that has already been saved. ); diff --git a/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js b/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js new file mode 100644 index 00000000000..36abca3728f --- /dev/null +++ b/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js @@ -0,0 +1,85 @@ +/** + * Internal dependencies + */ +import { getAppearance } from 'wcpay/checkout/upe-styles'; +import { getUPEConfig } from 'wcpay/utils/checkout'; +import { useFingerprint } from '../hooks'; +import { LoadableBlock } from 'wcpay/components/loadable'; +import { Elements } from '@stripe/react-stripe-js'; +import { useEffect, useState } from 'react'; +import PaymentProcessor from './payment-processor'; +import { getPaymentMethodTypes } from 'wcpay/checkout/utils/upe'; + +const PaymentElements = ( { api, ...props } ) => { + const stripe = api.getStripeForUPE( props.paymentMethodId ); + const [ errorMessage, setErrorMessage ] = useState( null ); + const [ appearance, setAppearance ] = useState( + getUPEConfig( 'wcBlocksUPEAppearance' ) + ); + const [ fingerprint, fingerprintErrorMessage ] = useFingerprint(); + const amount = Number( getUPEConfig( 'cartTotal' ) ); + const currency = getUPEConfig( 'currency' ).toLowerCase(); + const paymentMethodTypes = getPaymentMethodTypes( props.paymentMethodId ); + + useEffect( () => { + async function generateUPEAppearance() { + // Generate UPE input styles. + const upeAppearance = getAppearance( true ); + await api.saveUPEAppearance( upeAppearance, 'true' ); + setAppearance( upeAppearance ); + } + + if ( ! appearance ) { + generateUPEAppearance(); + } + + if ( fingerprintErrorMessage ) { + setErrorMessage( fingerprintErrorMessage ); + } + }, [ + api, + appearance, + fingerprint, + fingerprintErrorMessage, + props.paymentMethodId, + ] ); + + return ( + + + + + + ); +}; + +export const getDeferredIntentCreationUPEFields = ( + upeName, + upeMethods, + api, + testingInstructions +) => { + return ( + + ); +}; diff --git a/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js b/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js new file mode 100644 index 00000000000..989773e8400 --- /dev/null +++ b/client/checkout/blocks/upe-deferred-intent-creation/payment-processor.js @@ -0,0 +1,260 @@ +/** + * External dependencies + */ +import { + PaymentElement, + useElements, + useStripe, +} from '@stripe/react-stripe-js'; +import { + getPaymentMethods, + // eslint-disable-next-line import/no-unresolved +} from '@woocommerce/blocks-registry'; +import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from 'react'; + +/** + * Internal dependencies + */ +import { usePaymentCompleteHandler } from '../hooks'; +import { + getStripeElementOptions, + useCustomerData, + blocksShowLinkButtonHandler, + getBlocksEmailValue, + isLinkEnabled, +} from 'wcpay/checkout/utils/upe'; +import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link'; +import { getUPEConfig } from 'wcpay/utils/checkout'; +import { validateElements } from 'wcpay/checkout/classic/upe-deferred-intent-creation/payment-processing'; +import { + BLOCKS_SHIPPING_ADDRESS_FIELDS, + BLOCKS_BILLING_ADDRESS_FIELDS, +} from '../../constants'; + +const getBillingDetails = ( billingData ) => { + return { + name: `${ billingData.first_name } ${ billingData.last_name }`.trim(), + email: billingData.email, + phone: billingData.phone, + address: { + city: billingData.city, + country: billingData.country, + + line1: billingData.address_1, + line2: billingData.address_2, + postal_code: billingData.postcode, + state: billingData.state, + }, + }; +}; + +const getFraudPreventionToken = () => { + return document + .querySelector( '#wcpay-fraud-prevention-token' ) + ?.getAttribute( 'value' ); +}; + +const PaymentProcessor = ( { + api, + activePaymentMethod, + testingInstructions, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, + emitResponse, + paymentMethodId, + upeMethods, + errorMessage, + shouldSavePayment, + fingerprint, +} ) => { + const stripe = useStripe(); + const elements = useElements(); + const [ isPaymentElementComplete, setIsPaymentElementComplete ] = useState( + false + ); + + const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); + const isTestMode = getUPEConfig( 'testMode' ); + const testingInstructionsIfAppropriate = isTestMode + ? testingInstructions + : ''; + const gatewayConfig = getPaymentMethods()[ upeMethods[ paymentMethodId ] ]; + const customerData = useCustomerData(); + const billingData = customerData.billingAddress; + + useEffect( () => { + if ( isLinkEnabled( paymentMethodsConfig ) ) { + enableStripeLinkPaymentMethod( { + api: api, + elements: elements, + emailId: 'email', + fill_field_method: ( address, nodeId, key ) => { + const setAddress = + BLOCKS_SHIPPING_ADDRESS_FIELDS[ key ] === nodeId + ? customerData.setShippingAddress + : customerData.setBillingData || + customerData.setBillingAddress; + const customerAddress = + BLOCKS_SHIPPING_ADDRESS_FIELDS[ key ] === nodeId + ? customerData.shippingAddress + : customerData.billingData || + customerData.billingAddress; + + if ( key === 'line1' ) { + customerAddress.address_1 = address.address[ key ]; + } else if ( key === 'line2' ) { + customerAddress.address_2 = address.address[ key ]; + } else if ( key === 'postal_code' ) { + customerAddress.postcode = address.address[ key ]; + } else { + customerAddress[ key ] = address.address[ key ]; + } + + setAddress( customerAddress ); + + if ( customerData.billingData ) { + customerData.billingData.email = getBlocksEmailValue(); + customerData.setBillingData( customerData.billingData ); + } else { + customerData.billingAddress.email = getBlocksEmailValue(); + customerData.setBillingAddress( + customerData.billingAddress + ); + } + }, + show_button: blocksShowLinkButtonHandler, + shipping_fields: BLOCKS_SHIPPING_ADDRESS_FIELDS, + billing_fields: BLOCKS_BILLING_ADDRESS_FIELDS, + complete_shipping: () => { + return ( + document.getElementById( 'shipping-address_1' ) !== null + ); + }, + complete_billing: () => { + return ( + document.getElementById( 'billing-address_1' ) !== null + ); + }, + } ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ elements ] ); + + useEffect( + () => + onPaymentSetup( () => { + async function handlePaymentProcessing() { + if ( + upeMethods[ paymentMethodId ] !== activePaymentMethod + ) { + return; + } + + if ( ! isPaymentElementComplete ) { + return { + type: 'error', + message: __( + 'Your payment information is incomplete.', + 'woocommerce-payments' + ), + }; + } + + if ( errorMessage ) { + return { + type: 'error', + message: errorMessage, + }; + } + + if ( + gatewayConfig.supports.showSaveOption && + shouldSavePayment && + ! paymentMethodsConfig[ paymentMethodId ].isReusable + ) { + return { + type: 'error', + message: + 'This payment method cannot be saved for future use.', + }; + } + + await validateElements( elements ); + + const paymentMethodObject = await api + .getStripeForUPE( paymentMethodId ) + .createPaymentMethod( { + elements, + params: { + billing_details: getBillingDetails( + billingData + ), + }, + } ); + + return { + type: 'success', + meta: { + paymentMethodData: { + payment_method: upeMethods[ paymentMethodId ], + 'wcpay-payment-method': + paymentMethodObject.paymentMethod.id, + 'wcpay-fraud-prevention-token': getFraudPreventionToken(), + 'wcpay-fingerprint': fingerprint, + }, + }, + }; + } + return handlePaymentProcessing(); + } ), + [ + activePaymentMethod, + api, + elements, + fingerprint, + gatewayConfig, + paymentMethodId, + paymentMethodsConfig, + shouldSavePayment, + upeMethods, + errorMessage, + onPaymentSetup, + billingData, + isPaymentElementComplete, + ] + ); + + usePaymentCompleteHandler( + api, + stripe, + elements, + onCheckoutSuccess, + emitResponse, + shouldSavePayment + ); + + const updatePaymentElementCompletionStatus = ( event ) => { + setIsPaymentElementComplete( event.complete ); + }; + + return ( + <> +

+ + + ); +}; + +export default PaymentProcessor; diff --git a/client/checkout/blocks/upe-fields.js b/client/checkout/blocks/upe-fields.js index 02444947c54..9f1ea7d67ee 100644 --- a/client/checkout/blocks/upe-fields.js +++ b/client/checkout/blocks/upe-fields.js @@ -1,5 +1,3 @@ -/* global jQuery */ - /** * External dependencies */ @@ -14,7 +12,6 @@ import { // eslint-disable-next-line import/no-unresolved } from '@woocommerce/blocks-registry'; import { useEffect, useState } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; /** @@ -23,48 +20,28 @@ import { __ } from '@wordpress/i18n'; import './style.scss'; import confirmUPEPayment from './confirm-upe-payment.js'; import { getConfig } from 'utils/checkout'; -import { getTerms } from '../utils/upe'; +import { + getStripeElementOptions, + useCustomerData, + isLinkEnabled, + blocksShowLinkButtonHandler, +} from '../utils/upe'; import { decryptClientSecret } from '../utils/encryption'; -import { PAYMENT_METHOD_NAME_CARD, WC_STORE_CART } from '../constants.js'; +import { PAYMENT_METHOD_NAME_CARD } from '../constants.js'; import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link'; import { getAppearance, getFontRulesFromPage } from '../upe-styles'; import { useFingerprint } from './hooks'; - -const useCustomerData = () => { - const { customerData, isInitialized } = useSelect( ( select ) => { - const store = select( WC_STORE_CART ); - return { - customerData: store.getCustomerData(), - isInitialized: store.hasFinishedResolution( 'getCartData' ), - }; - } ); - const { - setShippingAddress, - setBillingData, - setBillingAddress, - } = useDispatch( WC_STORE_CART ); - - return { - isInitialized, - billingData: customerData.billingData, - // Backward compatibility billingData/billingAddress - billingAddress: customerData.billingAddress, - shippingAddress: customerData.shippingAddress, - setBillingData, - // Backward compatibility setBillingData/setBillingAddress - setBillingAddress, - setShippingAddress, - }; -}; +import { + BLOCKS_SHIPPING_ADDRESS_FIELDS, + BLOCKS_BILLING_ADDRESS_FIELDS, +} from '../constants'; const WCPayUPEFields = ( { api, activePaymentMethod, billing: { billingData }, - eventRegistration: { - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - }, + shippingData, + eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, emitResponse, paymentIntentId, paymentIntentSecret, @@ -94,52 +71,28 @@ const WCPayUPEFields = ( { const customerData = useCustomerData(); useEffect( () => { - if ( - paymentMethodsConfig.link !== undefined && - paymentMethodsConfig.card !== undefined - ) { - const shippingAddressFields = { - line1: 'shipping-address_1', - line2: 'shipping-address_2', - city: 'shipping-city', - state: 'components-form-token-input-1', - postal_code: 'shipping-postcode', - country: 'components-form-token-input-0', - first_name: 'shipping-first_name', - last_name: 'shipping-last_name', - }; - const billingAddressFields = { - line1: 'billing-address_1', - line2: 'billing-address_2', - city: 'billing-city', - state: 'components-form-token-input-3', - postal_code: 'billing-postcode', - country: 'components-form-token-input-2', - first_name: 'billing-first_name', - last_name: 'billing-last_name', - }; - + if ( isLinkEnabled( paymentMethodsConfig ) ) { enableStripeLinkPaymentMethod( { api: api, elements: elements, emailId: 'email', fill_field_method: ( address, nodeId, key ) => { const setAddress = - shippingAddressFields[ key ] === nodeId + BLOCKS_SHIPPING_ADDRESS_FIELDS[ key ] === nodeId ? customerData.setShippingAddress : customerData.setBillingData || customerData.setBillingAddress; const customerAddress = - shippingAddressFields[ key ] === nodeId + BLOCKS_SHIPPING_ADDRESS_FIELDS[ key ] === nodeId ? customerData.shippingAddress : customerData.billingData || customerData.billingAddress; - if ( 'line1' === key ) { + if ( key === 'line1' ) { customerAddress.address_1 = address.address[ key ]; - } else if ( 'line2' === key ) { + } else if ( key === 'line2' ) { customerAddress.address_2 = address.address[ key ]; - } else if ( 'postal_code' === key ) { + } else if ( key === 'postal_code' ) { customerAddress.postcode = address.address[ key ]; } else { customerAddress[ key ] = address.address[ key ]; @@ -161,38 +114,17 @@ const WCPayUPEFields = ( { ); } }, - show_button: ( linkAutofill ) => { - jQuery( '#email' ) - .parent() - .append( - '' - ); - if ( '' !== jQuery( '#email' ).val() ) { - jQuery( '.wcpay-stripelink-modal-trigger' ).show(); - } - - //Handle StripeLink button click. - jQuery( '.wcpay-stripelink-modal-trigger' ).on( - 'click', - ( event ) => { - event.preventDefault(); - // Trigger modal. - linkAutofill.launch( { - email: jQuery( '#email' ).val(), - } ); - } - ); - }, + show_button: blocksShowLinkButtonHandler, complete_shipping: () => { return ( - null !== document.getElementById( 'shipping-address_1' ) + document.getElementById( 'shipping-address_1' ) !== null ); }, - shipping_fields: shippingAddressFields, - billing_fields: billingAddressFields, + shipping_fields: BLOCKS_SHIPPING_ADDRESS_FIELDS, + billing_fields: BLOCKS_BILLING_ADDRESS_FIELDS, complete_billing: () => { return ( - null !== document.getElementById( 'billing-address_1' ) + document.getElementById( 'billing-address_1' ) !== null ); }, } ); @@ -232,8 +164,10 @@ const WCPayUPEFields = ( { ) { return { type: 'error', - message: + message: __( 'This payment method can not be saved for future use.', + 'woocommerce-payments' + ), }; } @@ -269,7 +203,7 @@ const WCPayUPEFields = ( { // Once the server has completed payment processing, confirm the intent if necessary. useEffect( () => - onCheckoutAfterProcessingWithSuccess( + onCheckoutSuccess( ( { orderId, processingResponse: { paymentDetails } } ) => { async function updateIntent() { if ( api.handleDuplicatePayments( paymentDetails ) ) { @@ -292,7 +226,9 @@ const WCPayUPEFields = ( { paymentIntentSecret, elements, billingData, - emitResponse + shippingData, + emitResponse, + selectedUPEPaymentType ); } @@ -323,39 +259,14 @@ const WCPayUPEFields = ( { setPaymentCountry( event.value.country ); }; - const elementOptions = { - fields: { - billingDetails: { - name: 'never', - email: 'never', - phone: 'never', - address: { - country: 'never', - line1: 'never', - line2: 'never', - city: 'never', - state: 'never', - postalCode: 'never', - }, - }, - }, - wallets: { - applePay: 'never', - googlePay: 'never', - }, - }; - - const showTerms = - shouldSavePayment || getConfig( 'cartContainsSubscription' ) - ? 'always' - : 'never'; - elementOptions.terms = getTerms( paymentMethodsConfig, showTerms ); - return ( <> { testMode ? testCopy : '' } diff --git a/client/checkout/blocks/upe-split-fields.js b/client/checkout/blocks/upe-split-fields.js index aad6d6af803..07b9f720da1 100644 --- a/client/checkout/blocks/upe-split-fields.js +++ b/client/checkout/blocks/upe-split-fields.js @@ -1,5 +1,3 @@ -/* global jQuery */ - /** * External dependencies */ @@ -14,7 +12,6 @@ import { // eslint-disable-next-line import/no-unresolved } from '@woocommerce/blocks-registry'; import { useEffect, useState } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; /** @@ -24,53 +21,31 @@ import './style.scss'; import confirmUPEPayment from './confirm-upe-payment.js'; import { getUPEConfig } from 'utils/checkout'; import { - getTerms, getPaymentIntentFromSession, getCookieValue, + getStripeElementOptions, + getBlocksEmailValue, + blocksShowLinkButtonHandler, + useCustomerData, + isLinkEnabled, } from '../utils/upe'; -import { WC_STORE_CART } from '../constants.js'; import { decryptClientSecret } from '../utils/encryption'; import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link'; import { getAppearance, getFontRulesFromPage } from '../upe-styles'; import { useFingerprint } from './hooks'; import { LoadableBlock } from '../../components/loadable'; - -const useCustomerData = () => { - const { customerData, isInitialized } = useSelect( ( select ) => { - const store = select( WC_STORE_CART ); - return { - customerData: store.getCustomerData(), - isInitialized: store.hasFinishedResolution( 'getCartData' ), - }; - } ); - const { - setShippingAddress, - setBillingData, - setBillingAddress, - } = useDispatch( WC_STORE_CART ); - - return { - isInitialized, - billingData: customerData.billingData, - // Backward compatibility billingData/billingAddress - billingAddress: customerData.billingAddress, - shippingAddress: customerData.shippingAddress, - setBillingData, - // Backward compatibility setBillingData/setBillingAddress - setBillingAddress, - setShippingAddress, - }; -}; +import { + BLOCKS_SHIPPING_ADDRESS_FIELDS, + BLOCKS_BILLING_ADDRESS_FIELDS, +} from '../constants'; const WCPayUPEFields = ( { api, activePaymentMethod, testingInstructions, billing: { billingData }, - eventRegistration: { - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - }, + shippingData, + eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, emitResponse, paymentMethodId, upeMethods, @@ -98,52 +73,28 @@ const WCPayUPEFields = ( { const customerData = useCustomerData(); useEffect( () => { - if ( - paymentMethodsConfig.link !== undefined && - paymentMethodsConfig.card !== undefined - ) { - const shippingAddressFields = { - line1: 'shipping-address_1', - line2: 'shipping-address_2', - city: 'shipping-city', - state: 'components-form-token-input-1', - postal_code: 'shipping-postcode', - country: 'components-form-token-input-0', - first_name: 'shipping-first_name', - last_name: 'shipping-last_name', - }; - const billingAddressFields = { - line1: 'billing-address_1', - line2: 'billing-address_2', - city: 'billing-city', - state: 'components-form-token-input-3', - postal_code: 'billing-postcode', - country: 'components-form-token-input-2', - first_name: 'billing-first_name', - last_name: 'billing-last_name', - }; - + if ( isLinkEnabled( paymentMethodsConfig ) ) { enableStripeLinkPaymentMethod( { api: api, elements: elements, emailId: 'email', fill_field_method: ( address, nodeId, key ) => { const setAddress = - shippingAddressFields[ key ] === nodeId + BLOCKS_SHIPPING_ADDRESS_FIELDS[ key ] === nodeId ? customerData.setShippingAddress : customerData.setBillingData || customerData.setBillingAddress; const customerAddress = - shippingAddressFields[ key ] === nodeId + BLOCKS_SHIPPING_ADDRESS_FIELDS[ key ] === nodeId ? customerData.shippingAddress : customerData.billingData || customerData.billingAddress; - if ( 'line1' === key ) { + if ( key === 'line1' ) { customerAddress.address_1 = address.address[ key ]; - } else if ( 'line2' === key ) { + } else if ( key === 'line2' ) { customerAddress.address_2 = address.address[ key ]; - } else if ( 'postal_code' === key ) { + } else if ( key === 'postal_code' ) { customerAddress.postcode = address.address[ key ]; } else { customerAddress[ key ] = address.address[ key ]; @@ -151,52 +102,27 @@ const WCPayUPEFields = ( { setAddress( customerAddress ); - function getEmail() { - return document.getElementById( 'email' ).value; - } - if ( customerData.billingData ) { - customerData.billingData.email = getEmail(); + customerData.billingData.email = getBlocksEmailValue(); customerData.setBillingData( customerData.billingData ); } else { - customerData.billingAddress.email = getEmail(); + customerData.billingAddress.email = getBlocksEmailValue(); customerData.setBillingAddress( customerData.billingAddress ); } }, - show_button: ( linkAutofill ) => { - jQuery( '#email' ) - .parent() - .append( - '' - ); - if ( '' !== jQuery( '#email' ).val() ) { - jQuery( '.wcpay-stripelink-modal-trigger' ).show(); - } - - //Handle StripeLink button click. - jQuery( '.wcpay-stripelink-modal-trigger' ).on( - 'click', - ( event ) => { - event.preventDefault(); - // Trigger modal. - linkAutofill.launch( { - email: jQuery( '#email' ).val(), - } ); - } - ); - }, + show_button: blocksShowLinkButtonHandler, complete_shipping: () => { return ( - null !== document.getElementById( 'shipping-address_1' ) + document.getElementById( 'shipping-address_1' ) !== null ); }, - shipping_fields: shippingAddressFields, - billing_fields: billingAddressFields, + shipping_fields: BLOCKS_SHIPPING_ADDRESS_FIELDS, + billing_fields: BLOCKS_BILLING_ADDRESS_FIELDS, complete_billing: () => { return ( - null !== document.getElementById( 'billing-address_1' ) + document.getElementById( 'billing-address_1' ) !== null ); }, } ); @@ -236,8 +162,10 @@ const WCPayUPEFields = ( { ) { return { type: 'error', - message: + message: __( 'This payment method can not be saved for future use.', + 'woocommerce-payments' + ), }; } @@ -273,7 +201,7 @@ const WCPayUPEFields = ( { // Once the server has completed payment processing, confirm the intent if necessary. useEffect( () => - onCheckoutAfterProcessingWithSuccess( + onCheckoutSuccess( ( { orderId, processingResponse: { paymentDetails } } ) => { async function updateIntent() { if ( api.handleDuplicatePayments( paymentDetails ) ) { @@ -296,7 +224,9 @@ const WCPayUPEFields = ( { paymentIntentSecret, elements, billingData, - emitResponse + shippingData, + emitResponse, + selectedUPEPaymentType ); } @@ -320,7 +250,7 @@ const WCPayUPEFields = ( { const upeOnChange = ( event ) => { // Update WC Blocks gateway config based on selected UPE payment method. const paymentType = - 'link' !== event.value.type ? event.value.type : 'card'; + event.value.type !== 'link' ? event.value.type : 'card'; gatewayConfig.supports.showSaveOption = paymentMethodsConfig[ paymentType ].showSaveOption; @@ -329,34 +259,6 @@ const WCPayUPEFields = ( { setPaymentCountry( event.value.country ); }; - const elementOptions = { - fields: { - billingDetails: { - name: 'never', - email: 'never', - phone: 'never', - address: { - country: 'never', - line1: 'never', - line2: 'never', - city: 'never', - state: 'never', - postalCode: 'never', - }, - }, - }, - wallets: { - applePay: 'never', - googlePay: 'never', - }, - }; - - const showTerms = - shouldSavePayment || getUPEConfig( 'cartContainsSubscription' ) - ? 'always' - : 'never'; - elementOptions.terms = getTerms( paymentMethodsConfig, showTerms ); - return ( <>

@@ -514,4 +419,18 @@ const ConsumableWCPayFields = ( { api, ...props } ) => { ); }; -export default ConsumableWCPayFields; +export const getSplitUPEFields = ( + upeName, + upeMethods, + api, + testingInstructions +) => { + return ( + + ); +}; diff --git a/client/checkout/blocks/upe-split.js b/client/checkout/blocks/upe-split.js index ff84c7d2df9..ab2463aadc7 100644 --- a/client/checkout/blocks/upe-split.js +++ b/client/checkout/blocks/upe-split.js @@ -1,8 +1,6 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; - // Handled as an external dependency: see '/webpack.config.js:83' import { registerPaymentMethod, @@ -14,8 +12,8 @@ import { * Internal dependencies */ import { getUPEConfig } from 'utils/checkout'; +import { isLinkEnabled } from '../utils/upe'; import WCPayAPI from './../api'; -import WCPayUPEFields from './upe-split-fields.js'; import { SavedTokenHandler } from './saved-token-handler'; import request from '../utils/request'; import enqueueFraudScripts from 'fraud-scripts'; @@ -30,7 +28,11 @@ import { PAYMENT_METHOD_NAME_P24, PAYMENT_METHOD_NAME_SEPA, PAYMENT_METHOD_NAME_SOFORT, + PAYMENT_METHOD_NAME_AFFIRM, + PAYMENT_METHOD_NAME_AFTERPAY, } from '../constants.js'; +import { getSplitUPEFields } from './upe-split-fields'; +import { getDeferredIntentCreationUPEFields } from './upe-deferred-intent-creation/payment-elements'; const upeMethods = { card: PAYMENT_METHOD_NAME_CARD, @@ -42,12 +44,12 @@ const upeMethods = { p24: PAYMENT_METHOD_NAME_P24, sepa_debit: PAYMENT_METHOD_NAME_SEPA, sofort: PAYMENT_METHOD_NAME_SOFORT, + affirm: PAYMENT_METHOD_NAME_AFFIRM, + afterpay_clearpay: PAYMENT_METHOD_NAME_AFTERPAY, }; const enabledPaymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); -const isStripeLinkEnabled = - enabledPaymentMethodsConfig.link !== undefined && - enabledPaymentMethodsConfig.card !== undefined; +const isStripeLinkEnabled = isLinkEnabled( enabledPaymentMethodsConfig ); // Create an API object, which will be used throughout the checkout. const api = new WCPayAPI( @@ -58,33 +60,42 @@ const api = new WCPayAPI( locale: getUPEConfig( 'locale' ), isUPEEnabled: getUPEConfig( 'isUPEEnabled' ), isUPESplitEnabled: getUPEConfig( 'isUPESplitEnabled' ), + isUPEDeferredEnabled: getUPEConfig( 'isUPEDeferredEnabled' ), isStripeLinkEnabled, }, request ); +const getUPEFields = getUPEConfig( 'isUPEDeferredEnabled' ) + ? getDeferredIntentCreationUPEFields + : getSplitUPEFields; Object.entries( enabledPaymentMethodsConfig ) - .filter( ( [ upeName ] ) => 'link' !== upeName ) - .map( ( [ upeName, upeConfig ] ) => + .filter( ( [ upeName ] ) => upeName !== 'link' ) + .forEach( ( [ upeName, upeConfig ] ) => { registerPaymentMethod( { name: upeMethods[ upeName ], - content: ( - + content: getUPEFields( + upeName, + upeMethods, + api, + upeConfig.testingInstructions ), - edit: ( - + edit: getUPEFields( + upeName, + upeMethods, + api, + upeConfig.testingInstructions ), savedTokenComponent: , - canMakePayment: () => !! api.getStripe(), + canMakePayment: ( cartData ) => { + const billingCountry = cartData.billingAddress.country; + const isRestrictedInAnyCountry = !! upeConfig.countries.length; + const isAvailableInTheCountry = + ! isRestrictedInAnyCountry || + upeConfig.countries.includes( billingCountry ); + return ( + isAvailableInTheCountry && !! api.getStripeForUPE( upeName ) + ); + }, paymentMethodId: upeMethods[ upeName ], // see .wc-block-checkout__payment-method styles in blocks/style.scss label: ( @@ -95,14 +106,14 @@ Object.entries( enabledPaymentMethodsConfig ) ), - ariaLabel: __( 'WooCommerce Payments', 'woocommerce-payments' ), + ariaLabel: 'WooPayments', supports: { showSavedCards: getUPEConfig( 'isSavedCardsEnabled' ) ?? false, showSaveOption: upeConfig.showSaveOption ?? false, features: getUPEConfig( 'features' ), }, - } ) - ); + } ); + } ); registerExpressPaymentMethod( paymentRequestPaymentMethod( api ) ); window.addEventListener( 'load', () => { diff --git a/client/checkout/blocks/upe.js b/client/checkout/blocks/upe.js index c4ac0904230..c0b1e28ad65 100644 --- a/client/checkout/blocks/upe.js +++ b/client/checkout/blocks/upe.js @@ -1,8 +1,6 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; - // Handled as an external dependency: see '/webpack.config.js:83' import { registerPaymentMethod, @@ -21,11 +19,10 @@ import { SavedTokenHandler } from './saved-token-handler'; import request from '../utils/request'; import enqueueFraudScripts from 'fraud-scripts'; import paymentRequestPaymentMethod from '../../payment-request/blocks'; +import { isLinkEnabled } from '../utils/upe.js'; const paymentMethodsConfig = getConfig( 'paymentMethodsConfig' ); -const isStripeLinkEnabled = - paymentMethodsConfig.link !== undefined && - paymentMethodsConfig.card !== undefined; +const isStripeLinkEnabled = isLinkEnabled( paymentMethodsConfig ); // Create an API object, which will be used throughout the checkout. const api = new WCPayAPI( @@ -48,7 +45,7 @@ registerPaymentMethod( { canMakePayment: () => !! api.getStripe(), paymentMethodId: PAYMENT_METHOD_NAME_CARD, label: getCustomGatewayTitle( getConfig( 'paymentMethodsConfig' ) ), - ariaLabel: __( 'WooCommerce Payments', 'woocommerce-payments' ), + ariaLabel: 'WooPayments', supports: { showSavedCards: getConfig( 'isSavedCardsEnabled' ) ?? false, showSaveOption: diff --git a/client/checkout/classic/index.js b/client/checkout/classic/index.js index 68452b243a7..f91fba53a72 100644 --- a/client/checkout/classic/index.js +++ b/client/checkout/classic/index.js @@ -48,7 +48,7 @@ jQuery( function ( $ ) { // Customer information for Pay for Order and Save Payment method. /* global wcpayCustomerData */ const preparedCustomerData = - 'undefined' !== typeof wcpayCustomerData ? wcpayCustomerData : {}; + typeof wcpayCustomerData !== 'undefined' ? wcpayCustomerData : {}; // Create a card element. const cardElement = elements.create( 'card', { @@ -292,7 +292,11 @@ jQuery( function ( $ ) { .catch( function ( error ) { paymentMethodGenerated = null; $form.removeClass( 'processing' ).unblock(); - showError( error.message ); + if ( error.responseJSON && ! error.responseJSON.success ) { + showError( error.responseJSON.data.error.message ); + } else if ( error.message ) { + showError( error.message ); + } } ); }; @@ -409,7 +413,7 @@ jQuery( function ( $ ) { ); // Boolean `true` means that there is nothing to confirm. - if ( true === confirmation ) { + if ( confirmation === true ) { return; } @@ -474,7 +478,7 @@ jQuery( function ( $ ) { ); } - // Handle the checkout form when WooCommerce Payments is chosen. + // Handle the checkout form when WooPayments is chosen. const wcpayPaymentMethods = [ PAYMENT_METHOD_NAME_CARD ]; const checkoutEvents = wcpayPaymentMethods .map( ( method ) => `checkout_place_order_${ method }` ) @@ -492,7 +496,7 @@ jQuery( function ( $ ) { } } ); - // Handle the Pay for Order form if WooCommerce Payments is chosen. + // Handle the Pay for Order form if WooPayments is chosen. $( '#order_review' ).on( 'submit', () => { if ( isUsingSavedPaymentMethod() || @@ -508,13 +512,12 @@ jQuery( function ( $ ) { ); } ); - // Handle the add payment method form for WooCommerce Payments. + // Handle the add payment method form for WooPayments. $( 'form#add_payment_method' ).on( 'submit', function () { if ( - 'woocommerce_payments' !== $( "#add_payment_method input:checked[name='payment_method']" - ).val() + ).val() !== 'woocommerce_payments' ) { return; } diff --git a/client/checkout/classic/test/upe-split.test.js b/client/checkout/classic/test/upe-split.test.js index 5c38cf14c10..0b8434bb7d3 100644 --- a/client/checkout/classic/test/upe-split.test.js +++ b/client/checkout/classic/test/upe-split.test.js @@ -112,10 +112,10 @@ describe( 'UPE split checkout', () => { beforeEach( () => { const spy = jest.spyOn( CheckoutUtils, 'getUPEConfig' ); spy.mockImplementation( ( param ) => { - if ( 'paymentMethodsConfig' === param ) { + if ( param === 'paymentMethodsConfig' ) { return { card: {}, bancontact: {} }; } - if ( 'gatewayId' === param ) { + if ( param === 'gatewayId' ) { return 'woocommerce_payments'; } } ); 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 index 11ec05fc5ac..28171ec7207 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/3ds-flow-handling.js +++ b/client/checkout/classic/upe-deferred-intent-creation/3ds-flow-handling.js @@ -32,7 +32,7 @@ export const showAuthenticationModalIfRequired = ( api ) => { ); // Boolean `true` means that there is nothing to confirm. - if ( true === confirmation ) { + if ( confirmation === true ) { return; } diff --git a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js index 4ebe3c84852..c174a861ca3 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js +++ b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js @@ -7,17 +7,22 @@ import { getUPEConfig } from 'wcpay/utils/checkout'; import { generateCheckoutEventNames, getSelectedUPEGatewayPaymentMethod, + isLinkEnabled, isUsingSavedPaymentMethod, } from '../../utils/upe'; import { - checkout, + processPayment, mountStripePaymentElement, renderTerms, -} from './stripe-checkout'; + createAndConfirmSetupIntent, + maybeEnableStripeLink, +} from './payment-processing'; import enqueueFraudScripts from 'fraud-scripts'; import { showAuthenticationModalIfRequired } from './3ds-flow-handling'; import WCPayAPI from 'wcpay/checkout/api'; import apiRequest from '../../utils/request'; +import { handleWooPayEmailInput } from 'wcpay/checkout/woopay/email-input-iframe'; +import { isPreviewing } from 'wcpay/checkout/preview'; jQuery( function ( $ ) { enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); @@ -27,29 +32,22 @@ jQuery( function ( $ ) { accountId: getUPEConfig( 'accountId' ), forceNetworkSavedCards: getUPEConfig( 'forceNetworkSavedCards' ), locale: getUPEConfig( 'locale' ), + isUPEEnabled: getUPEConfig( 'isUPEEnabled' ), + isStripeLinkEnabled: isLinkEnabled( + getUPEConfig( 'paymentMethodsConfig' ) + ), + isUPEDeferredEnabled: getUPEConfig( 'isUPEDeferredEnabled' ), }, 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 ) - ); - } + maybeMountStripePaymentElement(); } ); $( 'form.checkout' ).on( generateCheckoutEventNames(), function () { - const paymentMethodType = getSelectedUPEGatewayPaymentMethod(); - if ( ! isUsingSavedPaymentMethod( paymentMethodType ) ) { - return checkout( api, jQuery( this ), paymentMethodType ); - } + return processPaymentIfNotUsingSavedMethod( $( this ) ); } ); window.addEventListener( 'hashchange', () => { @@ -61,9 +59,58 @@ jQuery( function ( $ ) { document.addEventListener( 'change', function ( event ) { if ( event.target && - 'wc-woocommerce_payments-new-payment-method' === event.target.id + event.target.id === 'wc-woocommerce_payments-new-payment-method' ) { renderTerms( event ); } } ); + + if ( + $( 'form#add_payment_method' ).length || + $( 'form#order_review' ).length + ) { + if ( getUPEConfig( 'isUPEEnabled' ) ) { + maybeMountStripePaymentElement(); + } + } + + $( 'form#add_payment_method' ).on( 'submit', function () { + // WC core calls block() when add_payment_method form is submitted, so we need to enable the ignore flag here to avoid + // the overlay blink when the form is blocked twice. + $.blockUI.defaults.ignoreIfBlocked = true; + + return processPayment( + api, + $( 'form#add_payment_method' ), + getSelectedUPEGatewayPaymentMethod(), + createAndConfirmSetupIntent + ); + } ); + + $( 'form#order_review' ).on( 'submit', function () { + return processPaymentIfNotUsingSavedMethod( $( 'form#order_review' ) ); + } ); + + if ( getUPEConfig( 'isWooPayEnabled' ) && ! isPreviewing() ) { + handleWooPayEmailInput( '#billing_email', api ); + } + + function processPaymentIfNotUsingSavedMethod( $form ) { + const paymentMethodType = getSelectedUPEGatewayPaymentMethod(); + if ( ! isUsingSavedPaymentMethod( paymentMethodType ) ) { + return processPayment( api, $form, paymentMethodType ); + } + } + + async function maybeMountStripePaymentElement() { + if ( + $( '.wcpay-upe-element' ).length && + ! $( '.wcpay-upe-element' ).children().length + ) { + for ( const upeElement of $( '.wcpay-upe-element' ).toArray() ) { + await mountStripePaymentElement( api, upeElement ); + } + maybeEnableStripeLink( api ); + } + } } ); diff --git a/client/checkout/classic/upe-deferred-intent-creation/stripe-checkout.js b/client/checkout/classic/upe-deferred-intent-creation/payment-processing.js similarity index 62% rename from client/checkout/classic/upe-deferred-intent-creation/stripe-checkout.js rename to client/checkout/classic/upe-deferred-intent-creation/payment-processing.js index a9591128034..dc4dc62c5d6 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/stripe-checkout.js +++ b/client/checkout/classic/upe-deferred-intent-creation/payment-processing.js @@ -10,10 +10,17 @@ import { } from 'wcpay/checkout/utils/fingerprint'; import { appendPaymentMethodIdToForm, + getPaymentMethodTypes, getSelectedUPEGatewayPaymentMethod, getTerms, getUpeSettings, + isLinkEnabled, } from 'wcpay/checkout/utils/upe'; +import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link'; +import { + SHORTCODE_SHIPPING_ADDRESS_FIELDS, + SHORTCODE_BILLING_ADDRESS_FIELDS, +} from '../../constants'; const gatewayUPEComponents = {}; let fingerprint = null; @@ -65,7 +72,7 @@ function blockUI( jQueryForm ) { * @param {Object} elements The Stripe elements object to be validated. * @return {Promise} Promise for the checkout submission. */ -function validateElements( elements ) { +export function validateElements( elements ) { return elements.submit().then( ( result ) => { if ( result.error ) { throw new Error( result.error.message ); @@ -88,35 +95,50 @@ function submitForm( jQueryForm ) { * * @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. + * @param {Object} jQueryForm The jQuery object for the form being submitted. + * @param {string} paymentMethodType The type of Stripe payment method to create. * @return {Promise} A promise that resolves with the created Stripe payment method. */ -function createStripePaymentMethod( api, elements ) { - return api.getStripe().createPaymentMethod( { - elements, - params: { +function createStripePaymentMethod( + api, + elements, + jQueryForm, + paymentMethodType +) { + let params = {}; + if ( jQueryForm.attr( 'name' ) === 'checkout' ) { + params = { billing_details: { name: document.querySelector( '#billing_first_name' ) ? ( document.querySelector( '#billing_first_name' ) - .value + + ?.value + ' ' + - document.querySelector( '#billing_last_name' ).value + document.querySelector( '#billing_last_name' ) + ?.value ).trim() : undefined, - email: document.querySelector( '#billing_email' ).value, - phone: document.querySelector( '#billing_phone' ).value, + 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, + 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, + ?.value, + state: document.querySelector( '#billing_state' )?.value, }, }, - }, - } ); + }; + } + + return api + .getStripeForUPE( paymentMethodType ) + .createPaymentMethod( { elements, params: params } ); } /** @@ -130,16 +152,19 @@ function createStripePaymentMethod( api, elements ) { */ async function createStripePaymentElement( api, paymentMethodType ) { const amount = Number( getUPEConfig( 'cartTotal' ) ); + const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType ); const options = { - mode: 1 > amount ? 'setup' : 'payment', + mode: amount < 1 ? 'setup' : 'payment', currency: getUPEConfig( 'currency' ).toLowerCase(), amount: amount, paymentMethodCreation: 'manual', - paymentMethodTypes: [ paymentMethodType ], + paymentMethodTypes: paymentMethodTypes, appearance: initializeAppearance( api ), }; - const elements = api.getStripe().elements( options ); + const elements = api + .getStripeForUPE( paymentMethodType ) + .elements( options ); const createdStripePaymentElement = elements.create( 'payment', { ...getUpeSettings(), wallets: { @@ -155,6 +180,52 @@ async function createStripePaymentElement( api, paymentMethodType ) { return createdStripePaymentElement; } +/** + * Appends a hidden input field with the confirmed setup intent ID to the provided form. + * + * @param {HTMLElement} form The HTML form element to which the input field will be appended. + * @param {Object} confirmedIntent The confirmed setup intent object containing the ID to be stored in the input field. + */ +function appendSetupIntentToForm( form, confirmedIntent ) { + const input = document.createElement( 'input' ); + input.type = 'hidden'; + input.id = 'wcpay-setup-intent'; + input.name = 'wcpay-setup-intent'; + input.value = confirmedIntent.id; + + form.append( input ); +} + +/** + * If Link is enabled, add event listeners and handlers. + * + * @param {Object} api WCPayAPI instance. + */ +export function maybeEnableStripeLink( api ) { + if ( isLinkEnabled( getUPEConfig( 'paymentMethodsConfig' ) ) ) { + enableStripeLinkPaymentMethod( { + api: api, + elements: gatewayUPEComponents.card.elements, + emailId: 'billing_email', + complete_billing: () => { + return true; + }, + complete_shipping: () => { + return ( + document.getElementById( + 'ship-to-different-address-checkbox' + ) && + document.getElementById( + 'ship-to-different-address-checkbox' + ).checked + ); + }, + shipping_fields: SHORTCODE_SHIPPING_ADDRESS_FIELDS, + billing_fields: SHORTCODE_BILLING_ADDRESS_FIELDS, + } ); + } +} + /** * 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. @@ -172,13 +243,65 @@ export async function mountStripePaymentElement( api, domElement ) { showErrorCheckout( error.message ); return; } + + /* + * Trigger this event to ensure the tokenization-form.js init + * is executed. + * + * This script handles the radio input interaction when toggling + * between the user's saved card / entering new card details. + * + * Ref: https://github.com/woocommerce/woocommerce/blob/2429498/assets/js/frontend/tokenization-form.js#L109 + */ + const event = new Event( 'wc-credit-card-form-init' ); + document.body.dispatchEvent( event ); + const paymentMethodType = domElement.dataset.paymentMethodType; + if ( ! gatewayUPEComponents[ paymentMethodType ] ) { + return; + } + const upeElement = gatewayUPEComponents[ paymentMethodType ].upeElement || ( await createStripePaymentElement( api, paymentMethodType ) ); upeElement.mount( domElement ); } +/** + * Creates and confirms a setup intent using the provided ID, then appends the confirmed setup intent to the given jQuery form. + * + * @param {Object} id Payment method object ID. + * @param {Object} $form The jQuery object for the form to which the confirmed setup intent will be appended. + * @param {Object} api The API object with the setupIntent method to create and confirm the setup intent. + * + * @return {Promise} A promise that resolves when the setup intent is confirmed and appended to the form. + */ +export const createAndConfirmSetupIntent = ( { id }, $form, api ) => { + return api.setupIntent( id ).then( function ( confirmedSetupIntent ) { + appendSetupIntentToForm( $form, confirmedSetupIntent ); + } ); +}; + +/** + * 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 ), + } ); + } +} + /** * 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 @@ -191,7 +314,12 @@ export async function mountStripePaymentElement( api, domElement ) { * @return {boolean} return false to prevent the default form submission from WC Core. */ let hasCheckoutCompleted; -export const checkout = ( api, jQueryForm, paymentMethodType ) => { +export const processPayment = ( + api, + jQueryForm, + paymentMethodType, + additionalActionsHandler = () => {} +) => { if ( hasCheckoutCompleted ) { hasCheckoutCompleted = false; return; @@ -206,16 +334,24 @@ export const checkout = ( api, jQueryForm, paymentMethodType ) => { await validateElements( elements ); const paymentMethodObject = await createStripePaymentMethod( api, - elements + elements, + jQueryForm, + paymentMethodType ); appendFingerprintInputToForm( jQueryForm, fingerprint ); appendPaymentMethodIdToForm( jQueryForm, paymentMethodObject.paymentMethod.id ); + await additionalActionsHandler( + paymentMethodObject.paymentMethod, + jQueryForm, + api + ); hasCheckoutCompleted = true; submitForm( jQueryForm ); } catch ( err ) { + hasCheckoutCompleted = false; jQueryForm.removeClass( 'processing' ).unblock(); showErrorCheckout( err.message ); } @@ -224,23 +360,3 @@ export const checkout = ( api, jQueryForm, paymentMethodType ) => { // 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/stripe-checkout.test.js b/client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js similarity index 59% rename from client/checkout/classic/upe-deferred-intent-creation/test/stripe-checkout.test.js rename to client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js index da3084c4a87..87b258e8a2f 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/test/stripe-checkout.test.js +++ b/client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js @@ -2,10 +2,11 @@ * Internal dependencies */ import { - checkout, + createAndConfirmSetupIntent, mountStripePaymentElement, + processPayment, renderTerms, -} from '../stripe-checkout'; +} from '../payment-processing'; import { getAppearance } from '../../../upe-styles'; import { getUPEConfig } from 'wcpay/utils/checkout'; import { getFingerprint } from 'wcpay/checkout/utils/fingerprint'; @@ -20,24 +21,28 @@ jest.mock( 'wcpay/checkout/utils/upe' ); jest.mock( 'wcpay/utils/checkout', () => { return { getUPEConfig: jest.fn( ( argument ) => { - if ( 'paymentMethodsConfig' === argument ) { + if ( argument === 'paymentMethodsConfig' ) { return { card: { label: 'Card', + forceNetworkSavedCards: true, }, giropay: { label: 'Giropay', + forceNetworkSavedCards: false, }, ideal: { label: 'iDEAL', + forceNetworkSavedCards: false, }, sepa: { label: 'SEPA', + forceNetworkSavedCards: false, }, }; } - if ( 'currency' === argument ) { + if ( argument === 'currency' ) { return 'eur'; } } ), @@ -91,7 +96,7 @@ const mockCreatePaymentMethod = jest.fn( () => { }; } ); -const mockGetStripe = jest.fn( () => { +const mockGetStripeForUPE = jest.fn( () => { return { elements: mockElements, createPaymentMethod: mockCreatePaymentMethod, @@ -100,24 +105,41 @@ const mockGetStripe = jest.fn( () => { const saveUPEAppearanceMock = jest.fn(); +const mockSetupIntentThen = jest.fn(); +const setupIntentMock = jest.fn( () => { + return { + then: mockSetupIntentThen, + }; +} ); + const apiMock = { saveUPEAppearance: saveUPEAppearanceMock, - getStripe: mockGetStripe, + getStripeForUPE: mockGetStripeForUPE, + setupIntent: setupIntentMock, }; -describe( 'Mount Stripe Payment Element', () => { +describe( 'Stripe Payment Element mounting', () => { + let mockDomElement; + + beforeEach( () => { + mockDomElement = document.createElement( 'div' ); + } ); + afterEach( () => { jest.clearAllMocks(); } ); test( 'initializes the appearance when it is not set and saves it', async () => { + // Create a mock function to track the event dispatch for tokenization-form.js execution + const dispatchMock = jest.fn(); + document.body.dispatchEvent = dispatchMock; + const appearanceMock = { backgroundColor: '#fff' }; getAppearance.mockReturnValue( appearanceMock ); getFingerprint.mockImplementation( () => { return 'fingerprint'; } ); - const mockDomElement = document.createElement( 'div' ); mockDomElement.dataset.paymentMethodType = 'giropay'; mountStripePaymentElement( apiMock, mockDomElement ); @@ -127,6 +149,7 @@ describe( 'Mount Stripe Payment Element', () => { expect( apiMock.saveUPEAppearance ).toHaveBeenCalledWith( appearanceMock ); + expect( dispatchMock ).toHaveBeenCalled(); } ); } ); @@ -137,19 +160,39 @@ describe( 'Mount Stripe Payment Element', () => { return 'fingerprint'; } ); getUPEConfig.mockImplementation( ( argument ) => { - if ( 'currency' === argument ) { + if ( argument === 'currency' ) { return 'eur'; } - if ( 'upeAppearance' === argument ) { + if ( argument === 'upeAppearance' ) { return { backgroundColor: '#fff', }; } + + if ( argument === 'paymentMethodsConfig' ) { + return { + ideal: { + label: 'iDEAL', + forceNetworkSavedCards: false, + }, + card: { + label: 'Card', + forceNetworkSavedCards: true, + }, + giropay: { + label: 'Giropay', + forceNetworkSavedCards: false, + }, + sepa: { + label: 'SEPA', + forceNetworkSavedCards: false, + }, + }; + } } ); - const mockDomElement = document.createElement( 'div' ); - mockDomElement.dataset.paymentMethodType = 'ideal'; + mockDomElement.dataset.paymentMethodType = 'giropay'; mountStripePaymentElement( apiMock, mockDomElement ); @@ -170,7 +213,7 @@ describe( 'Mount Stripe Payment Element', () => { expect( showErrorCheckout ).toHaveBeenCalledWith( 'No fingerprint' ); - expect( apiMock.getStripe ).not.toHaveBeenCalled(); + expect( apiMock.getStripeForUPE ).not.toHaveBeenCalled(); expect( mockElements ).not.toHaveBeenCalled(); expect( mockCreateFunction ).not.toHaveBeenCalled(); } ); @@ -181,13 +224,12 @@ describe( 'Mount Stripe Payment Element', () => { return 'fingerprint'; } ); - const mockDomElement = document.createElement( 'div' ); mockDomElement.dataset.paymentMethodType = 'card'; mountStripePaymentElement( apiMock, mockDomElement ); await waitFor( () => { - expect( apiMock.getStripe ).toHaveBeenCalled(); + expect( apiMock.getStripeForUPE ).toHaveBeenCalled(); expect( mockElements ).toHaveBeenCalled(); expect( mockCreateFunction ).toHaveBeenCalled(); expect( mockMountFunction ).toHaveBeenCalled(); @@ -201,11 +243,11 @@ describe( 'Mount Stripe Payment Element', () => { }, }; getUPEConfig.mockImplementation( ( argument ) => { - if ( 'currency' === argument ) { + if ( argument === 'currency' ) { return 'eur'; } - if ( 'isUPEEnabled' === argument ) { + if ( argument === 'isUPEEnabled' ) { return true; } } ); @@ -233,25 +275,55 @@ describe( 'Mount Stripe Payment Element', () => { return 'fingerprint'; } ); - const mockDomElement = document.createElement( 'div' ); + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'currency' ) { + return 'eur'; + } + + if ( argument === 'paymentMethodsConfig' ) { + return { + sepa: { + label: 'SEPA', + forceNetworkSavedCards: false, + }, + }; + } + } ); + mockDomElement.dataset.paymentMethodType = 'sepa'; mountStripePaymentElement( apiMock, mockDomElement ); mountStripePaymentElement( apiMock, mockDomElement ); await waitFor( () => { - expect( apiMock.getStripe ).toHaveBeenCalled(); + expect( apiMock.getStripeForUPE ).toHaveBeenCalled(); expect( mockElements ).toHaveBeenCalledTimes( 1 ); } ); } ); } ); -describe( 'Checkout', () => { +describe( 'Payment processing', () => { + beforeEach( () => { + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'currency' ) { + return 'eur'; + } + + if ( argument === 'paymentMethodsConfig' ) { + return { + card: { + label: 'card', + forceNetworkSavedCards: false, + }, + }; + } + } ); + } ); afterEach( () => { jest.clearAllMocks(); } ); - test( 'Successful checkout', async () => { + test( 'Successful payment processing', async () => { setupBillingDetailsFields(); getFingerprint.mockImplementation( () => { return 'fingerprint'; @@ -264,16 +336,20 @@ describe( 'Checkout', () => { const mockJqueryForm = { submit: jest.fn(), - addClass: jest.fn( () => { - return { - block: jest.fn(), - }; - } ), - removeClass: jest.fn(), - unblock: jest.fn(), + addClass: jest.fn( () => ( { + block: jest.fn(), + } ) ), + removeClass: jest.fn( () => ( { + unblock: jest.fn(), + } ) ), + attr: jest.fn().mockReturnValue( 'checkout' ), }; - const checkoutResult = checkout( apiMock, mockJqueryForm, 'card' ); + const checkoutResult = processPayment( + apiMock, + mockJqueryForm, + 'card' + ); expect( mockJqueryForm.addClass ).toHaveBeenCalledWith( 'processing' ); expect( mockJqueryForm.removeClass ).not.toHaveBeenCalledWith( @@ -287,6 +363,102 @@ describe( 'Checkout', () => { expect( checkoutResult ).toBe( false ); } ); + test( 'Payment processing adds billing details for checkout', async () => { + setupBillingDetailsFields(); + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'card'; + + await mountStripePaymentElement( apiMock, mockDomElement ); + + const checkoutForm = { + submit: jest.fn(), + addClass: jest.fn( () => ( { + block: jest.fn(), + } ) ), + removeClass: jest.fn( () => ( { + unblock: jest.fn(), + } ) ), + attr: jest.fn().mockReturnValue( 'checkout' ), + }; + + await processPayment( apiMock, checkoutForm, 'card' ); + + expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( { + elements: expect.any( Object ), + params: { + billing_details: expect.any( Object ), + }, + } ); + } ); + + test( 'Payment processing does not create error when some fields are hidden via customizer', async () => { + setupBillingDetailsFields(); + // pretending that the customizer removed the billing phone field + document.body.removeChild( document.getElementById( 'billing_phone' ) ); + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'card'; + + await mountStripePaymentElement( apiMock, mockDomElement ); + + const checkoutForm = { + submit: jest.fn(), + addClass: jest.fn( () => ( { + block: jest.fn(), + } ) ), + removeClass: jest.fn( () => ( { + unblock: jest.fn(), + } ) ), + attr: jest.fn().mockReturnValue( 'checkout' ), + }; + + await processPayment( apiMock, checkoutForm, 'card' ); + + expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( { + elements: expect.any( Object ), + params: { + billing_details: expect.any( Object ), + }, + } ); + } ); + + test( 'Payment processing does not add billing details for non-checkout forms', async () => { + setupBillingDetailsFields(); + getFingerprint.mockImplementation( () => { + return 'fingerprint'; + } ); + + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'card'; + + await mountStripePaymentElement( apiMock, mockDomElement ); + + const addPaymentMethodForm = { + submit: jest.fn(), + addClass: jest.fn( () => ( { + block: jest.fn(), + } ) ), + removeClass: jest.fn( () => ( { + unblock: jest.fn(), + } ) ), + attr: jest.fn().mockReturnValue( 'add_payment_method' ), + }; + + await processPayment( apiMock, addPaymentMethodForm, 'card' ); + + expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( { + elements: expect.any( Object ), + params: {}, + } ); + } ); + function setupBillingDetailsFields() { // Create DOM elements for the test const firstNameInput = document.createElement( 'input' ); @@ -342,3 +514,30 @@ describe( 'Checkout', () => { document.body.appendChild( stateInput ); } } ); + +describe( 'Setup intent creation and confirmation', () => { + test( 'Setup intent is created and confirmed', async () => { + const mockDomElement = document.createElement( 'div' ); + mockDomElement.dataset.paymentMethodType = 'card'; + + const mockJqueryForm = { + submit: jest.fn(), + addClass: jest.fn( () => { + return { + block: jest.fn(), + }; + } ), + removeClass: jest.fn(), + unblock: jest.fn(), + attr: jest.fn().mockReturnValue( 'add_payment_method' ), + }; + + await createAndConfirmSetupIntent( + mockJqueryForm, + { id: 'si123xyz' }, + apiMock + ); + + expect( setupIntentMock ).toHaveBeenCalled(); + } ); +} ); diff --git a/client/checkout/classic/upe-split.js b/client/checkout/classic/upe-split.js index 38026f3ce14..87edf7333ff 100644 --- a/client/checkout/classic/upe-split.js +++ b/client/checkout/classic/upe-split.js @@ -19,7 +19,11 @@ import { PAYMENT_METHOD_NAME_P24, PAYMENT_METHOD_NAME_SEPA, PAYMENT_METHOD_NAME_SOFORT, -} from '../constants.js'; + PAYMENT_METHOD_NAME_AFFIRM, + PAYMENT_METHOD_NAME_AFTERPAY, + SHORTCODE_SHIPPING_ADDRESS_FIELDS, + SHORTCODE_BILLING_ADDRESS_FIELDS, +} from '../constants'; import { getUPEConfig } from 'utils/checkout'; import WCPayAPI from '../api'; import enqueueFraudScripts from 'fraud-scripts'; @@ -28,8 +32,11 @@ import { getTerms, getPaymentIntentFromSession, getSelectedUPEGatewayPaymentMethod, + getBillingDetails, + getShippingDetails, getUpeSettings, isUsingSavedPaymentMethod, + isLinkEnabled, } from '../utils/upe'; import { decryptClientSecret } from '../utils/encryption'; import enableStripeLinkPaymentMethod from '../stripe-link'; @@ -39,6 +46,7 @@ import { getFingerprint, appendFingerprintInputToForm, } from '../utils/fingerprint'; +import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; jQuery( function ( $ ) { enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); @@ -48,9 +56,7 @@ jQuery( function ( $ ) { const isUPEEnabled = getUPEConfig( 'isUPEEnabled' ); const isUPESplitEnabled = getUPEConfig( 'isUPESplitEnabled' ); const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' ); - const isStripeLinkEnabled = - paymentMethodsConfig.link !== undefined && - paymentMethodsConfig.card !== undefined; + const isStripeLinkEnabled = isLinkEnabled( paymentMethodsConfig ); const gatewayUPEComponents = {}; for ( const paymentMethodType in paymentMethodsConfig ) { @@ -108,33 +114,6 @@ jQuery( function ( $ ) { $form.removeClass( 'processing' ).unblock(); }; - /** - * Converts form fields object into Stripe `billing_details` object. - * - * @param {Object} fields Object mapping checkout billing fields to values. - * @return {Object} Stripe formatted `billing_details` object. - */ - const getBillingDetails = ( fields ) => { - return { - name: - `${ fields.billing_first_name } ${ fields.billing_last_name }`.trim() || - '-', - email: - 'string' === typeof fields.billing_email - ? fields.billing_email.trim() - : '-', - phone: fields.billing_phone || '-', - address: { - country: fields.billing_country || '-', - line1: fields.billing_address_1 || '-', - line2: fields.billing_address_2 || '-', - city: fields.billing_city || '-', - state: fields.billing_state || '-', - postal_code: fields.billing_postcode || '-', - }, - }; - }; - /** * Mounts Stripe UPE element if feature is enabled. * @@ -209,10 +188,12 @@ jQuery( function ( $ ) { } catch ( error ) { unblockUI( $upeContainer ); showErrorCheckout( error.message ); - const gatewayErrorMessage = - '
An error was encountered when preparing the payment form. Please try again later.
'; + const gatewayErrorMessage = __( + 'An error was encountered when preparing the payment form. Please try again later.', + 'woocommerce-payments' + ); $( '.payment_box.payment_method_woocommerce_payments' ).html( - gatewayErrorMessage + `
${ gatewayErrorMessage }
` ); } } @@ -258,26 +239,8 @@ jQuery( function ( $ ) { ).checked ); }, - shipping_fields: { - line1: 'shipping_address_1', - line2: 'shipping_address_2', - city: 'shipping_city', - state: 'shipping_state', - postal_code: 'shipping_postcode', - country: 'shipping_country', - first_name: 'shipping_first_name', - last_name: 'shipping_last_name', - }, - billing_fields: { - line1: 'billing_address_1', - line2: 'billing_address_2', - city: 'billing_city', - state: 'billing_state', - postal_code: 'billing_postcode', - country: 'billing_country', - first_name: 'billing_first_name', - last_name: 'billing_last_name', - }, + shipping_fields: SHORTCODE_SHIPPING_ADDRESS_FIELDS, + billing_fields: SHORTCODE_BILLING_ADDRESS_FIELDS, } ); } @@ -292,7 +255,7 @@ jQuery( function ( $ ) { unblockUI( $upeContainer ); upeElement.on( 'change', ( event ) => { const selectedUPEPaymentType = - 'link' !== event.value.type ? event.value.type : 'card'; + event.value.type !== 'link' ? event.value.type : 'card'; gatewayUPEComponents[ selectedUPEPaymentType ].country = event.value.country; gatewayUPEComponents[ selectedUPEPaymentType ].isUPEComplete = @@ -301,6 +264,41 @@ jQuery( function ( $ ) { gatewayUPEComponents[ paymentMethodType ].upeElement = upeElement; }; + function togglePaymentMethodForCountry( + paymentMethodType, + supportedCountries + ) { + const billingCountry = document.querySelector( '#billing_country' ) + .value; + const upeContainer = document.querySelector( + '#payment_method_woocommerce_payments_' + paymentMethodType + ).parentElement; + if ( supportedCountries.includes( billingCountry ) ) { + upeContainer.style.display = 'block'; + } else { + upeContainer.style.display = 'none'; + } + } + + function restrictPaymentMethodToLocation( paymentMethodType ) { + const isRestrictedInAnyCountry = !! paymentMethodsConfig[ + paymentMethodType + ].countries.length; + + if ( isRestrictedInAnyCountry ) { + togglePaymentMethodForCountry( + paymentMethodType, + paymentMethodsConfig[ paymentMethodType ].countries + ); + $( '#billing_country' ).on( 'change', function () { + togglePaymentMethodForCountry( + paymentMethodType, + paymentMethodsConfig[ paymentMethodType ].countries + ); + } ); + } + } + // Only attempt to mount the card element once that section of the page has loaded. We can use the updated_checkout // event for this. This part of the page can also reload based on changes to checkout details, so we call unmount // first to ensure the card element is re-mounted correctly. @@ -326,6 +324,8 @@ jQuery( function ( $ ) { } else { mountUPEElement( paymentMethodType, upeDOMElement ); } + + restrictPaymentMethodToLocation( paymentMethodType ); } } } ); @@ -445,6 +445,10 @@ jQuery( function ( $ ) { ); if ( updateResponse.data ) { + if ( updateResponse.data.error ) { + throw updateResponse.data.error; + } + if ( api.handleDuplicatePayments( updateResponse.data ) ) { return; } @@ -539,6 +543,12 @@ jQuery( function ( $ ) { }, }, }; + // Afterpay requires shipping details to be passed. Not needed by other payment methods. + if ( PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY === paymentMethodType ) { + upeConfig.confirmParams.shipping = getShippingDetails( + formFields + ); + } let error; if ( response.payment_needed ) { ( { error } = await api.handlePaymentConfirmation( @@ -577,7 +587,7 @@ jQuery( function ( $ ) { ); // Boolean `true` means that there is nothing to confirm. - if ( true === confirmation ) { + if ( confirmation === true ) { return; } @@ -635,7 +645,7 @@ jQuery( function ( $ ) { return clientSecret ? clientSecret : null; } - // Handle the checkout form when WooCommerce Payments is chosen. + // Handle the checkout form when WooPayments is chosen. const wcpayPaymentMethods = [ PAYMENT_METHOD_NAME_BANCONTACT, PAYMENT_METHOD_NAME_BECS, @@ -645,6 +655,8 @@ jQuery( function ( $ ) { PAYMENT_METHOD_NAME_P24, PAYMENT_METHOD_NAME_SEPA, PAYMENT_METHOD_NAME_SOFORT, + PAYMENT_METHOD_NAME_AFFIRM, + PAYMENT_METHOD_NAME_AFTERPAY, paymentMethodsConfig.card !== undefined && PAYMENT_METHOD_NAME_CARD, ].filter( Boolean ); const checkoutEvents = wcpayPaymentMethods @@ -664,15 +676,14 @@ jQuery( function ( $ ) { appendFingerprintInputToForm( $( this ), fingerprint ); } ); - // Handle the add payment method form for WooCommerce Payments. + // Handle the add payment method form for WooPayments. $( 'form#add_payment_method' ).on( 'submit', function () { // Skip adding legacy cards as UPE payment methods. if ( - 'woocommerce_payments' === - $( - "#add_payment_method input:checked[name='payment_method']" - ).val() && - '0' === isUPESplitEnabled + $( + "#add_payment_method input:checked[name='payment_method']" + ).val() === 'woocommerce_payments' && + isUPESplitEnabled === '0' ) { return; } @@ -687,12 +698,12 @@ jQuery( function ( $ ) { } } ); - // Handle the Pay for Order form if WooCommerce Payments is chosen. + // Handle the Pay for Order form if WooPayments is chosen. $( '#order_review' ).on( 'submit', () => { const paymentMethodType = getSelectedUPEGatewayPaymentMethod(); if ( ! isUsingSavedPaymentMethod( paymentMethodType ) && - null !== paymentMethodType + paymentMethodType !== null ) { if ( isChangingPayment ) { handleUPEAddPayment( $( '#order_review' ) ); diff --git a/client/checkout/classic/upe.js b/client/checkout/classic/upe.js index 749d443a93f..519a6f39b80 100644 --- a/client/checkout/classic/upe.js +++ b/client/checkout/classic/upe.js @@ -12,12 +12,21 @@ import './style.scss'; import { PAYMENT_METHOD_NAME_CARD, PAYMENT_METHOD_NAME_UPE, -} from '../constants.js'; + SHORTCODE_SHIPPING_ADDRESS_FIELDS, + SHORTCODE_BILLING_ADDRESS_FIELDS, +} from '../constants'; import { getConfig, getCustomGatewayTitle } from 'utils/checkout'; import WCPayAPI from '../api'; import enqueueFraudScripts from 'fraud-scripts'; import { getFontRulesFromPage, getAppearance } from '../upe-styles'; -import { getTerms, getCookieValue, isWCPayChosen } from '../utils/upe'; +import { + getTerms, + getCookieValue, + isWCPayChosen, + isLinkEnabled, + getBillingDetails, + getShippingDetails, +} from '../utils/upe'; import { decryptClientSecret } from '../utils/encryption'; import enableStripeLinkPaymentMethod from '../stripe-link'; import apiRequest from '../utils/request'; @@ -26,6 +35,7 @@ import { getFingerprint, appendFingerprintInputToForm, } from '../utils/fingerprint'; +import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; jQuery( function ( $ ) { enqueueFraudScripts( getConfig( 'fraudServices' ) ); @@ -37,9 +47,7 @@ jQuery( function ( $ ) { const enabledBillingFields = getConfig( 'enabledBillingFields' ); const upePaymentIntentData = getConfig( 'upePaymentIntentData' ); const upeSetupIntentData = getConfig( 'upeSetupIntentData' ); - const isStripeLinkEnabled = - paymentMethodsConfig.link !== undefined && - paymentMethodsConfig.card !== undefined; + const isStripeLinkEnabled = isLinkEnabled( paymentMethodsConfig ); if ( ! publishableKey ) { // If no configuration is present, probably this is not the checkout page. @@ -145,38 +153,16 @@ jQuery( function ( $ ) { $( '#wcpay_selected_upe_payment_type' ).val( paymentType ); }; + // Get the selected UPE payment type field + const getSelectedUPEPaymentType = () => { + return $( '#wcpay_selected_upe_payment_type' ).val(); + }; + // Set the payment country field const setPaymentCountry = ( country ) => { $( '#wcpay_payment_country' ).val( country ); }; - /** - * Converts form fields object into Stripe `billing_details` object. - * - * @param {Object} fields Object mapping checkout billing fields to values. - * @return {Object} Stripe formatted `billing_details` object. - */ - const getBillingDetails = ( fields ) => { - return { - name: - `${ fields.billing_first_name } ${ fields.billing_last_name }`.trim() || - '-', - email: - 'string' === typeof fields.billing_email - ? fields.billing_email.trim() - : '-', - phone: fields.billing_phone || '-', - address: { - country: fields.billing_country || '-', - line1: fields.billing_address_1 || '-', - line2: fields.billing_address_2 || '-', - city: fields.billing_city || '-', - state: fields.billing_state || '-', - postal_code: fields.billing_postcode || '-', - }, - }; - }; - /** * Mounts Stripe UPE element if feature is enabled. * @@ -236,10 +222,12 @@ jQuery( function ( $ ) { } catch ( error ) { unblockUI( $upeContainer ); showErrorCheckout( error.message ); - const gatewayErrorMessage = - '
An error was encountered when preparing the payment form. Please try again later.
'; + const gatewayErrorMessage = __( + 'An error was encountered when preparing the payment form. Please try again later.', + 'woocommerce-payments' + ); $( '.payment_box.payment_method_woocommerce_payments' ).html( - gatewayErrorMessage + `
${ gatewayErrorMessage }
` ); } } @@ -285,26 +273,8 @@ jQuery( function ( $ ) { ).checked ); }, - shipping_fields: { - line1: 'shipping_address_1', - line2: 'shipping_address_2', - city: 'shipping_city', - state: 'shipping_state', - postal_code: 'shipping_postcode', - country: 'shipping_country', - first_name: 'shipping_first_name', - last_name: 'shipping_last_name', - }, - billing_fields: { - line1: 'billing_address_1', - line2: 'billing_address_2', - city: 'billing_city', - state: 'billing_state', - postal_code: 'billing_postcode', - country: 'billing_country', - first_name: 'billing_first_name', - last_name: 'billing_last_name', - }, + shipping_fields: SHORTCODE_SHIPPING_ADDRESS_FIELDS, + billing_fields: SHORTCODE_BILLING_ADDRESS_FIELDS, } ); } @@ -457,11 +427,15 @@ jQuery( function ( $ ) { paymentIntentId, orderId, savePaymentMethod, - $( '#wcpay_selected_upe_payment_type' ).val(), + getSelectedUPEPaymentType(), $( '#wcpay_payment_country' ).val() ); if ( updateResponse.data ) { + if ( updateResponse.data.error ) { + throw updateResponse.data.error; + } + if ( api.handleDuplicatePayments( updateResponse.data ) ) { return; } @@ -556,6 +530,13 @@ jQuery( function ( $ ) { }, }, }; + const paymentMethodType = getSelectedUPEPaymentType(); + // Afterpay requires shipping details to be passed. Not needed by other payment methods. + if ( PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY === paymentMethodType ) { + upeConfig.confirmParams.shipping = getShippingDetails( + formFields + ); + } let error; if ( response.payment_needed ) { ( { error } = await api.handlePaymentConfirmation( @@ -594,7 +575,7 @@ jQuery( function ( $ ) { ); // Boolean `true` means that there is nothing to confirm. - if ( true === confirmation ) { + if ( confirmation === true ) { return; } @@ -683,7 +664,7 @@ jQuery( function ( $ ) { return clientSecret ? clientSecret : null; } - // Handle the checkout form when WooCommerce Payments is chosen. + // Handle the checkout form when WooPayments is chosen. const wcpayPaymentMethods = [ PAYMENT_METHOD_NAME_CARD, PAYMENT_METHOD_NAME_UPE, @@ -702,13 +683,12 @@ jQuery( function ( $ ) { appendFingerprintInputToForm( $( this ), fingerprint ); } ); - // Handle the add payment method form for WooCommerce Payments. + // Handle the add payment method form for WooPayments. $( 'form#add_payment_method' ).on( 'submit', function () { if ( - 'woocommerce_payments' !== $( "#add_payment_method input:checked[name='payment_method']" - ).val() + ).val() !== 'woocommerce_payments' ) { return; } @@ -721,7 +701,7 @@ jQuery( function ( $ ) { } } ); - // Handle the Pay for Order form if WooCommerce Payments is chosen. + // Handle the Pay for Order form if WooPayments is chosen. $( '#order_review' ).on( 'submit', () => { if ( ! isUsingSavedPaymentMethod() && isWCPayChosen() ) { if ( isChangingPayment ) { @@ -770,10 +750,9 @@ jQuery( function ( $ ) { */ export function isUsingSavedPaymentMethod() { return ( - null !== - document.querySelector( - '#wc-woocommerce_payments-payment-token-new' - ) && + document.querySelector( + '#wc-woocommerce_payments-payment-token-new' + ) !== null && ! document.querySelector( '#wc-woocommerce_payments-payment-token-new' ) .checked ); diff --git a/client/checkout/constants.js b/client/checkout/constants.js index cd4bd2f942a..5b6c5468905 100644 --- a/client/checkout/constants.js +++ b/client/checkout/constants.js @@ -7,6 +7,9 @@ export const PAYMENT_METHOD_NAME_IDEAL = 'woocommerce_payments_ideal'; export const PAYMENT_METHOD_NAME_P24 = 'woocommerce_payments_p24'; export const PAYMENT_METHOD_NAME_SEPA = 'woocommerce_payments_sepa_debit'; export const PAYMENT_METHOD_NAME_SOFORT = 'woocommerce_payments_sofort'; +export const PAYMENT_METHOD_NAME_AFFIRM = 'woocommerce_payments_affirm'; +export const PAYMENT_METHOD_NAME_AFTERPAY = + 'woocommerce_payments_afterpay_clearpay'; export const PAYMENT_METHOD_NAME_UPE = 'woocommerce_payments_upe'; export const PAYMENT_METHOD_NAME_PAYMENT_REQUEST = 'woocommerce_payments_payment_request'; @@ -24,6 +27,49 @@ export function getPaymentMethodsConstants() { PAYMENT_METHOD_NAME_P24, PAYMENT_METHOD_NAME_SEPA, PAYMENT_METHOD_NAME_SOFORT, + PAYMENT_METHOD_NAME_AFFIRM, + PAYMENT_METHOD_NAME_AFTERPAY, PAYMENT_METHOD_NAME_CARD, ]; } + +export const BLOCKS_SHIPPING_ADDRESS_FIELDS = { + line1: 'shipping-address_1', + line2: 'shipping-address_2', + city: 'shipping-city', + state: 'components-form-token-input-1', + postal_code: 'shipping-postcode', + country: 'components-form-token-input-0', + first_name: 'shipping-first_name', + last_name: 'shipping-last_name', +}; +export const BLOCKS_BILLING_ADDRESS_FIELDS = { + line1: 'billing-address_1', + line2: 'billing-address_2', + city: 'billing-city', + state: 'components-form-token-input-3', + postal_code: 'billing-postcode', + country: 'components-form-token-input-2', + first_name: 'billing-first_name', + last_name: 'billing-last_name', +}; +export const SHORTCODE_SHIPPING_ADDRESS_FIELDS = { + line1: 'shipping_address_1', + line2: 'shipping_address_2', + city: 'shipping_city', + state: 'shipping_state', + postal_code: 'shipping_postcode', + country: 'shipping_country', + first_name: 'shipping_first_name', + last_name: 'shipping_last_name', +}; +export const SHORTCODE_BILLING_ADDRESS_FIELDS = { + line1: 'billing_address_1', + line2: 'billing_address_2', + city: 'billing_city', + state: 'billing_state', + postal_code: 'billing_postcode', + country: 'billing_country', + first_name: 'billing_first_name', + last_name: 'billing_last_name', +}; diff --git a/client/checkout/express-checkout-buttons.scss b/client/checkout/express-checkout-buttons.scss new file mode 100644 index 00000000000..ef813873378 --- /dev/null +++ b/client/checkout/express-checkout-buttons.scss @@ -0,0 +1,16 @@ +.wcpay-payment-request-wrapper { + margin-top: 1em; + width: 100%; + + &:first-child { + margin-top: 0; + } +} + +.woocommerce-checkout .wcpay-payment-request-wrapper { + margin-bottom: 1.5em; +} + +.wcpay-payment-request-wrapper > div:not( :last-child ) { + margin-bottom: 1em; +} diff --git a/client/checkout/preview.js b/client/checkout/preview.js index e08721afa91..022bbe7ab84 100644 --- a/client/checkout/preview.js +++ b/client/checkout/preview.js @@ -15,7 +15,7 @@ export const isPreviewing = () => { // Check for the URL parameter used in the iframe of the customize.php page // and for the is_preview() value for posts. return ( - null !== searchParams.get( 'customize_messenger_channel' ) || + searchParams.get( 'customize_messenger_channel' ) !== null || getConfig( 'isPreview' ) ); }; diff --git a/client/checkout/stripe-link/index.js b/client/checkout/stripe-link/index.js index 7ae0adde474..08ba006f814 100644 --- a/client/checkout/stripe-link/index.js +++ b/client/checkout/stripe-link/index.js @@ -1,7 +1,7 @@ const showLinkButton = ( linkAutofill ) => { // Display StripeLink button if email field is prefilled. const billingEmailInput = document.getElementById( 'billing_email' ); - if ( '' !== billingEmailInput.value ) { + if ( billingEmailInput.value !== '' ) { const linkButtonTop = billingEmailInput.offsetTop + ( billingEmailInput.offsetHeight - 40 ) / 2; @@ -28,7 +28,7 @@ export const autofill = ( event, options ) => { const fillWith = options.fill_field_method ? options.fill_field_method : ( address, nodeId, key ) => { - if ( null !== document.getElementById( nodeId ) ) { + if ( document.getElementById( nodeId ) !== null ) { document.getElementById( nodeId ).value = address.address[ key ]; } diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js index 9787a00660b..3ab91f23211 100644 --- a/client/checkout/upe-styles/index.js +++ b/client/checkout/upe-styles/index.js @@ -259,13 +259,13 @@ export const getFieldStyles = ( selector, upeElement, focus = false ) => { } } - if ( '.Input' === upeElement || '.Tab--selected' === upeElement ) { + if ( upeElement === '.Input' || upeElement === '.Tab--selected' ) { const outline = generateOutlineStyle( filteredStyles.outlineWidth, filteredStyles.outlineStyle, filteredStyles.outlineColor ); - if ( '' !== outline ) { + if ( outline !== '' ) { filteredStyles.outline = outline; } delete filteredStyles.outlineWidth; @@ -277,9 +277,9 @@ export const getFieldStyles = ( selector, upeElement, focus = false ) => { //since Stripe doesn't support text-indents. const textIndent = styles.getPropertyValue( 'text-indent' ); if ( - '0px' !== textIndent && - '0px' === filteredStyles.paddingLeft && - '0px' === filteredStyles.paddingRight + textIndent !== '0px' && + filteredStyles.paddingLeft === '0px' && + filteredStyles.paddingRight === '0px' ) { filteredStyles.paddingLeft = textIndent; filteredStyles.paddingRight = textIndent; @@ -302,7 +302,7 @@ export const getFontRulesFromPage = () => { continue; } const url = new URL( sheets[ i ].href ); - if ( -1 !== fontDomains.indexOf( url.hostname ) ) { + if ( fontDomains.indexOf( url.hostname ) !== -1 ) { fontRules.push( { cssSrc: sheets[ i ].href, } ); diff --git a/client/checkout/upe-styles/utils.js b/client/checkout/upe-styles/utils.js index 36742d84b01..dc7878b4898 100644 --- a/client/checkout/upe-styles/utils.js +++ b/client/checkout/upe-styles/utils.js @@ -29,7 +29,7 @@ export const generateHoverColors = ( backgroundColor, color ) => { // Darken if brightness > 50 (Storefront Button 51 ), else lighten const newBackgroundColor = - 50 < tinyBackgroundColor.getBrightness() + tinyBackgroundColor.getBrightness() > 50 ? tinycolor( tinyBackgroundColor ).darken( 7 ) : tinycolor( tinyBackgroundColor ).lighten( 7 ); diff --git a/client/checkout/utils/encryption.js b/client/checkout/utils/encryption.js index d980225f9d7..1bb59e94ed5 100644 --- a/client/checkout/utils/encryption.js +++ b/client/checkout/utils/encryption.js @@ -12,9 +12,9 @@ export const decryptClientSecret = function ( ) { if ( getConfig( 'isClientEncryptionEnabled' ) && - 3 < encryptedValue.length && - 'pi_' !== encryptedValue.slice( 0, 3 ) && - 'seti_' !== encryptedValue.slice( 0, 5 ) + encryptedValue.length > 3 && + encryptedValue.slice( 0, 3 ) !== 'pi_' && + encryptedValue.slice( 0, 5 ) !== 'seti_' ) { stripeAccountId = stripeAccountId || getConfig( 'accountId' ); return Utf8.stringify( diff --git a/client/checkout/utils/fingerprint.js b/client/checkout/utils/fingerprint.js index 101a177df10..d4b5f1f4087 100644 --- a/client/checkout/utils/fingerprint.js +++ b/client/checkout/utils/fingerprint.js @@ -1,11 +1,18 @@ /** @format */ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ import FingerprintJS from '@fingerprintjs/fingerprintjs'; -export const FINGERPRINT_GENERIC_ERROR = - 'An error was encountered when preparing the payment form. Please try again later.'; +export const FINGERPRINT_GENERIC_ERROR = __( + 'An error was encountered when preparing the payment form. Please try again later.', + 'woocommerce-payments' +); export const getFingerprint = async () => { const agent = await FingerprintJS.load( { monitoring: false } ); diff --git a/client/checkout/utils/request.js b/client/checkout/utils/request.js index 61c8478e4d8..0f50e1184bc 100644 --- a/client/checkout/utils/request.js +++ b/client/checkout/utils/request.js @@ -3,9 +3,9 @@ function createFormData( obj, subKeyStr = '', formData ) { const value = obj[ key ]; const subKeyStrTrans = subKeyStr ? subKeyStr + '[' + key + ']' : key; - if ( 'string' === typeof value || 'number' === typeof value ) { + if ( typeof value === 'string' || typeof value === 'number' ) { formData.append( subKeyStrTrans, value ); - } else if ( 'object' === typeof value ) { + } else if ( typeof value === 'object' ) { createFormData( value, subKeyStrTrans, formData ); } } diff --git a/client/checkout/utils/test/upe.test.js b/client/checkout/utils/test/upe.test.js index 9e5ff54f56e..6251b97bb82 100644 --- a/client/checkout/utils/test/upe.test.js +++ b/client/checkout/utils/test/upe.test.js @@ -7,8 +7,14 @@ import { isWCPayChosen, getPaymentIntentFromSession, generateCheckoutEventNames, + getUpeSettings, + getStripeElementOptions, + blocksShowLinkButtonHandler, } from '../upe'; import { getPaymentMethodsConstants } from '../../constants'; +import { getUPEConfig } from 'wcpay/utils/checkout'; + +jest.mock( 'wcpay/utils/checkout' ); jest.mock( '../../constants', () => { return { @@ -165,6 +171,96 @@ describe( 'UPE checkout utils', () => { } ); } ); + describe( 'getUPESettings', () => { + afterEach( () => { + const checkboxElement = document.getElementById( + 'wc-woocommerce_payments-new-payment-method' + ); + if ( checkboxElement ) { + checkboxElement.remove(); + } + } ); + + it( 'should not provide terms when cart does not contain subscriptions and the saving checkbox is unchecked', () => { + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'paymentMethodsConfig' ) { + return { + card: { + label: 'Card', + isReusable: true, + }, + }; + } + + if ( argument === 'cartContainsSubscription' ) { + return false; + } + } ); + + createCheckboxElementWhich( false ); + + const upeSettings = getUpeSettings(); + + expect( upeSettings.terms.card ).toEqual( 'never' ); + } ); + + it( 'should provide terms when cart does not contain subscriptions but the saving checkbox is checked', () => { + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'paymentMethodsConfig' ) { + return { + card: { + label: 'Card', + isReusable: true, + }, + }; + } + + if ( argument === 'cartContainsSubscription' ) { + return false; + } + } ); + + createCheckboxElementWhich( true ); + + const upeSettings = getUpeSettings(); + + // console.log(result); + expect( upeSettings.terms.card ).toEqual( 'always' ); + } ); + + it( 'should provide terms when cart contains subscriptions but the saving checkbox is unchecked', () => { + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'paymentMethodsConfig' ) { + return { + card: { + label: 'Card', + isReusable: true, + }, + }; + } + + if ( argument === 'cartContainsSubscription' ) { + return true; + } + } ); + + createCheckboxElementWhich( false ); + const upeSettings = getUpeSettings(); + + expect( upeSettings.terms.card ).toEqual( 'always' ); + } ); + + function createCheckboxElementWhich( isChecked ) { + // Create the checkbox element + const checkboxElement = document.createElement( 'input' ); + checkboxElement.type = 'checkbox'; + checkboxElement.checked = isChecked; + checkboxElement.id = 'wc-woocommerce_payments-new-payment-method'; + + document.body.appendChild( checkboxElement ); + } + } ); + describe( 'generateCheckoutEventNames', () => { it( 'should return empty string when there are no payment methods', () => { getPaymentMethodsConstants.mockImplementation( () => [] ); @@ -188,3 +284,186 @@ describe( 'UPE checkout utils', () => { } ); } ); } ); + +describe( 'getStripeElementOptions', () => { + test( 'should return options with "always" terms for cart containing subscription', () => { + const shouldSavePayment = false; + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'cartContainsSubscription' ) { + return true; + } + } ); + const paymentMethodsConfig = { + card: { + isReusable: true, + }, + bancontact: { + isReusable: true, + }, + eps: { + isReusable: true, + }, + giropay: { + isReusable: false, + }, + }; + + const options = getStripeElementOptions( + shouldSavePayment, + paymentMethodsConfig + ); + + expect( options ).toEqual( { + fields: { + billingDetails: { + address: { + city: 'never', + country: 'never', + line1: 'never', + line2: 'never', + postalCode: 'never', + state: 'never', + }, + email: 'never', + name: 'never', + phone: 'never', + }, + }, + terms: { bancontact: 'always', card: 'always', eps: 'always' }, + wallets: { applePay: 'never', googlePay: 'never' }, + } ); + } ); + + test( 'should return options with "always" terms when checkbox to save payment method is checked', () => { + const shouldSavePayment = true; + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'cartContainsSubscription' ) { + return false; + } + } ); + const paymentMethodsConfig = { + card: { + isReusable: true, + }, + bancontact: { + isReusable: true, + }, + eps: { + isReusable: true, + }, + giropay: { + isReusable: false, + }, + }; + + const options = getStripeElementOptions( + shouldSavePayment, + paymentMethodsConfig + ); + + expect( options ).toEqual( { + fields: { + billingDetails: { + address: { + city: 'never', + country: 'never', + line1: 'never', + line2: 'never', + postalCode: 'never', + state: 'never', + }, + email: 'never', + name: 'never', + phone: 'never', + }, + }, + terms: { bancontact: 'always', card: 'always', eps: 'always' }, + wallets: { applePay: 'never', googlePay: 'never' }, + } ); + } ); + + test( 'should return options with "never" for terms when shouldSavePayment is false and no subscription in cart', () => { + const shouldSavePayment = false; + const paymentMethodsConfig = { + card: { + isReusable: true, + }, + }; + + getUPEConfig.mockImplementation( ( argument ) => { + if ( argument === 'cartContainsSubscription' ) { + return false; + } + } ); + + const options = getStripeElementOptions( + shouldSavePayment, + paymentMethodsConfig + ); + + expect( options ).toEqual( { + fields: { + billingDetails: { + address: { + city: 'never', + country: 'never', + line1: 'never', + line2: 'never', + postalCode: 'never', + state: 'never', + }, + email: 'never', + name: 'never', + phone: 'never', + }, + }, + terms: { card: 'never' }, + wallets: { applePay: 'never', googlePay: 'never' }, + } ); + } ); +} ); + +describe( 'blocksShowLinkButtonHandler', () => { + let container; + const autofill = { + launch: ( props ) => { + return props.email; + }, + }; + + beforeEach( () => { + container = document.createElement( 'div' ); + container.innerHTML = ` + + + `; + document.body.appendChild( container ); + } ); + + afterEach( () => { + document.body.removeChild( container ); + container = null; + } ); + + test( 'should hide link button if email input is empty', () => { + blocksShowLinkButtonHandler( autofill ); + + const stripeLinkButton = document.querySelector( + '.wcpay-stripelink-modal-trigger' + ); + expect( stripeLinkButton ).toBeDefined(); + expect( stripeLinkButton.style.display ).toEqual( 'none' ); + } ); + + test( 'should show link button if email input is present', () => { + document.getElementById( 'email' ).value = 'admin@example.com'; + + blocksShowLinkButtonHandler( autofill ); + + const stripeLinkButton = document.querySelector( + '.wcpay-stripelink-modal-trigger' + ); + expect( stripeLinkButton ).toBeDefined(); + expect( stripeLinkButton.style.display ).toEqual( 'inline-block' ); + } ); +} ); diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js index 0a16b7def0c..da11bc2bfd7 100644 --- a/client/checkout/utils/upe.js +++ b/client/checkout/utils/upe.js @@ -2,7 +2,8 @@ * Internal dependencies */ import { getUPEConfig } from 'wcpay/utils/checkout'; -import { getPaymentMethodsConstants } from '../constants'; +import { WC_STORE_CART, getPaymentMethodsConstants } from '../constants'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Generates terms parameter for UPE, with value set for reusable payment methods @@ -85,11 +86,11 @@ export const getSelectedUPEGatewayPaymentMethod = () => { const radio = document.querySelector( 'li.wc_payment_method input.input-radio:checked, li.woocommerce-PaymentMethod input.input-radio:checked' ); - if ( null !== radio ) { + if ( radio !== null ) { selectedGatewayId = radio.id; } - if ( 'payment_method_woocommerce_payments' === selectedGatewayId ) { + if ( selectedGatewayId === 'payment_method_woocommerce_payments' ) { selectedGatewayId = 'payment_method_woocommerce_payments_card'; } @@ -146,12 +147,13 @@ export const getHiddenBillingFields = ( enabledBillingFields ) => { export const getUpeSettings = () => { const upeSettings = {}; - if ( getUPEConfig( 'cartContainsSubscription' ) ) { - upeSettings.terms = getTerms( - getUPEConfig( 'paymentMethodsConfig' ), - 'always' - ); - } + const showTerms = shouldIncludeTerms() ? 'always' : 'never'; + + upeSettings.terms = getTerms( + getUPEConfig( 'paymentMethodsConfig' ), + showTerms + ); + if ( getUPEConfig( 'isCheckout' ) && ! ( @@ -168,6 +170,24 @@ export const getUpeSettings = () => { return upeSettings; }; +function shouldIncludeTerms() { + if ( getUPEConfig( 'cartContainsSubscription' ) ) { + return true; + } + + const savePaymentMethodCheckbox = document.getElementById( + 'wc-woocommerce_payments-new-payment-method' + ); + if ( + savePaymentMethodCheckbox !== null && + savePaymentMethodCheckbox.checked + ) { + return true; + } + + return false; +} + export const generateCheckoutEventNames = () => { return getPaymentMethodsConstants() .map( ( method ) => `checkout_place_order_${ method }` ) @@ -190,12 +210,214 @@ export function isUsingSavedPaymentMethod( paymentMethodType ) { const prefix = '#wc-woocommerce_payments'; const suffix = '-payment-token-new'; const savedPaymentSelector = - 'card' === paymentMethodType + paymentMethodType === 'card' || paymentMethodType === 'link' ? prefix + suffix : prefix + '_' + paymentMethodType + suffix; return ( - null !== document.querySelector( savedPaymentSelector ) && + document.querySelector( savedPaymentSelector ) !== null && ! document.querySelector( savedPaymentSelector ).checked ); } + +/** + * + * Custom React hook that provides customer data and related functions for managing customer information. + * The hook retrieves customer data from the WC_STORE_CART selector and dispatches actions to modify billing and shipping addresses. + * + * @return {Object} An object containing customer data and functions for managing customer information. + */ +export const useCustomerData = () => { + const { customerData, isInitialized } = useSelect( ( select ) => { + const store = select( WC_STORE_CART ); + return { + customerData: store.getCustomerData(), + isInitialized: store.hasFinishedResolution( 'getCartData' ), + }; + } ); + const { + setShippingAddress, + setBillingData, + setBillingAddress, + } = useDispatch( WC_STORE_CART ); + + return { + isInitialized, + billingData: customerData.billingData, + // Backward compatibility billingData/billingAddress + billingAddress: customerData.billingAddress, + shippingAddress: customerData.shippingAddress, + setBillingData, + // Backward compatibility setBillingData/setBillingAddress + setBillingAddress, + setShippingAddress, + }; +}; + +/** + * Returns the prepared set of options needed to initialize the Stripe elements for UPE in Block Checkout. + * The initial options have all the fields set to 'never' to hide them from the UPE, because all the + * information is already collected in the checkout form. Additionally, the options are updated with + * the terms text if needed. + * + * @param {boolean} shouldSavePayment Whether the payment method should be saved. + * @param {Object} paymentMethodsConfig The payment methods config object. + * + * @return {Object} The options object for the Stripe elements. + */ +export const getStripeElementOptions = ( + shouldSavePayment, + paymentMethodsConfig +) => { + const options = { + fields: { + billingDetails: { + name: 'never', + email: 'never', + phone: 'never', + address: { + country: 'never', + line1: 'never', + line2: 'never', + city: 'never', + state: 'never', + postalCode: 'never', + }, + }, + }, + wallets: { + applePay: 'never', + googlePay: 'never', + }, + }; + + const showTerms = + shouldSavePayment || getUPEConfig( 'cartContainsSubscription' ) + ? 'always' + : 'never'; + + options.terms = getTerms( paymentMethodsConfig, showTerms ); + + return options; +}; + +/** + * Check whether Stripe Link is enabled. + * + * @param {Object} paymentMethodsConfig Checkout payment methods configuration settings object. + * @return {boolean} True, if enabled; false otherwise. + */ +export const isLinkEnabled = ( paymentMethodsConfig ) => { + return ( + paymentMethodsConfig.link !== undefined && + paymentMethodsConfig.card !== undefined + ); +}; + +/** + * Get array of payment method types to use with intent. + * + * @param {string} paymentMethodType Payment method type Stripe ID. + * @return {Array} Array of payment method types to use with intent. + */ +export const getPaymentMethodTypes = ( paymentMethodType ) => { + const paymentMethodTypes = [ paymentMethodType ]; + if ( + paymentMethodType === 'card' && + isLinkEnabled( getUPEConfig( 'paymentMethodsConfig' ) ) + ) { + paymentMethodTypes.push( 'link' ); + } + return paymentMethodTypes; +}; + +/** + * Returns the value of the email input on the blocks checkout page. + * + * @return {string} The value of email input. + */ +export const getBlocksEmailValue = () => { + return document.getElementById( 'email' ).value; +}; + +/** + * Function to initialise Stripe Link button on email input field. + * + * @param {Object} linkAutofill Stripe Link Autofill instance. + */ +export const blocksShowLinkButtonHandler = ( linkAutofill ) => { + const emailInput = document.getElementById( 'email' ); + + const stripeLinkButton = document.createElement( 'button' ); + stripeLinkButton.setAttribute( 'class', 'wcpay-stripelink-modal-trigger' ); + stripeLinkButton.style.display = emailInput.value ? 'inline-block' : 'none'; + stripeLinkButton.addEventListener( 'click', ( event ) => { + event.preventDefault(); + linkAutofill.launch( { + email: document.getElementById( 'email' ).value, + } ); + } ); + + emailInput.parentNode.appendChild( stripeLinkButton ); +}; + +/** + * Converts form fields object into Stripe `billing_details` object. + * + * @param {Object} fields Object mapping checkout billing fields to values. + * @return {Object} Stripe formatted `billing_details` object. + */ +export const getBillingDetails = ( fields ) => { + return { + name: + `${ fields.billing_first_name } ${ fields.billing_last_name }`.trim() || + '-', + email: + typeof fields.billing_email === 'string' + ? fields.billing_email.trim() + : '-', + phone: fields.billing_phone || '-', + address: { + country: fields.billing_country || '-', + line1: fields.billing_address_1 || '-', + line2: fields.billing_address_2 || '-', + city: fields.billing_city || '-', + state: fields.billing_state || '-', + postal_code: fields.billing_postcode || '-', + }, + }; +}; + +/** + * Converts form fields object into Stripe `shipping` object. + * + * @param {Object} fields Object mapping checkout shipping fields to values. + * @return {Object} Stripe formatted `shipping` object. + */ +export const getShippingDetails = ( fields ) => { + // Shipping address is needed by Afterpay. If available, use shipping address, else fallback to billing address. + if ( + fields.ship_to_different_address && + fields.ship_to_different_address === '1' + ) { + return { + name: + `${ fields.shipping_first_name } ${ fields.shipping_last_name }`.trim() || + '-', + address: { + country: fields.shipping_country || '-', + line1: fields.shipping_address_1 || '-', + line2: fields.shipping_address_2 || '-', + city: fields.shipping_city || '-', + state: fields.shipping_state || '-', + postal_code: fields.shipping_postcode || '-', + }, + }; + } + + const billingAsShippingAddress = getBillingDetails( fields ); + delete billingAsShippingAddress.email; + delete billingAsShippingAddress.phone; + + return billingAsShippingAddress; +}; diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index b425e857813..47ae94dff85 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -6,7 +6,11 @@ import { getConfig } from 'wcpay/utils/checkout'; import wcpayTracks from 'tracks'; import request from '../utils/request'; import { buildAjaxURL } from '../../payment-request/utils'; -import { getTargetElement, validateEmail } from './utils'; +import { + getTargetElement, + validateEmail, + appendRedirectionParams, +} from './utils'; export const handleWooPayEmailInput = async ( field, @@ -70,10 +74,10 @@ export const handleWooPayEmailInput = async ( //Checks if customer has clicked the back button to prevent auto redirect const searchParams = new URLSearchParams( window.location.search ); const customerClickedBackButton = - ( 'undefined' !== typeof performance && - 'back_forward' === - performance.getEntriesByType( 'navigation' )[ 0 ].type ) || - 'true' === searchParams.get( 'skip_woopay' ); + ( typeof performance !== 'undefined' && + performance.getEntriesByType( 'navigation' )[ 0 ].type === + 'back_forward' ) || + searchParams.get( 'skip_woopay' ) === 'true'; // Track the current state of the header. This default // value should match the default state on the platform. @@ -124,11 +128,11 @@ export const handleWooPayEmailInput = async ( * scroll the window so the iframe is in view. */ if ( - 0 >= iframe.getBoundingClientRect().top || - 0 >= - window.innerHeight - - ( iframe.getBoundingClientRect().height + - iframe.getBoundingClientRect().top ) + iframe.getBoundingClientRect().top <= 0 || + window.innerHeight - + ( iframe.getBoundingClientRect().height + + iframe.getBoundingClientRect().top ) <= + 0 ) { const topOffset = 50; const scrollTop = @@ -163,8 +167,8 @@ export const handleWooPayEmailInput = async ( // Check if the iframe is off the right edge of the screen. If so, stick it to the right edge of the window. if ( - 50 >= - window.innerWidth - ( anchorRect.right + iframeRect.width ) + window.innerWidth - ( anchorRect.right + iframeRect.width ) <= + 50 ) { iframe.style.left = 'auto'; iframeArrow.style.left = 'auto'; @@ -182,6 +186,23 @@ export const handleWooPayEmailInput = async ( // Set the initial value. iframeHeaderValue = true; + request( + buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ), + { + _ajax_nonce: getConfig( 'woopaySessionNonce' ), + } + ).then( ( response ) => { + if ( response?.data?.session ) { + iframe.contentWindow.postMessage( + { + action: 'setSessionData', + value: response, + }, + getConfig( 'woopayHost' ) + ); + } + } ); + getWindowSize(); window.addEventListener( 'resize', getWindowSize ); @@ -189,7 +210,11 @@ export const handleWooPayEmailInput = async ( window.addEventListener( 'resize', setPopoverPosition ); iframe.classList.add( 'open' ); - wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_OTP_START ); + wcpayTracks.recordUserEvent( + wcpayTracks.events.WOOPAY_OTP_START, + [], + true + ); } ); // Add the iframe and iframe arrow to the wrapper. @@ -243,6 +268,10 @@ export const handleWooPayEmailInput = async ( 'viewport', `${ viewportWidth }x${ viewportHeight }` ); + urlParams.append( + 'tracksUserIdentity', + JSON.stringify( getConfig( 'tracksUserIdentity' ) ) + ); iframe.src = `${ getConfig( 'woopayHost' @@ -262,7 +291,7 @@ export const handleWooPayEmailInput = async ( }; document.addEventListener( 'keyup', ( event ) => { - if ( 'Escape' === event.key && closeIframe() ) { + if ( event.key === 'Escape' && closeIframe() ) { event.stopPropagation(); } } ); @@ -355,7 +384,7 @@ export const handleWooPayEmailInput = async ( ); } ) .then( ( response ) => { - if ( 200 !== response.status ) { + if ( response.status !== 200 ) { showErrorMessage(); } @@ -367,9 +396,11 @@ export const handleWooPayEmailInput = async ( if ( data[ 'user-exists' ] ) { openIframe( email ); - } else if ( 'rest_invalid_param' !== data.code ) { + } else if ( data.code !== 'rest_invalid_param' ) { wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_OFFERED + wcpayTracks.events.WOOPAY_OFFERED, + [], + true ); } } ) @@ -377,7 +408,7 @@ export const handleWooPayEmailInput = async ( // Only show the error if it's not an AbortError, // it occur when the fetch request is aborted because user // clicked the Place Order button while loading. - if ( 'AbortError' !== err.name ) { + if ( err.name !== 'AbortError' ) { showErrorMessage(); } } ) @@ -457,9 +488,12 @@ export const handleWooPayEmailInput = async ( case 'auto_redirect_to_platform_checkout': case 'auto_redirect_to_woopay': hasCheckedLoginSession = true; - api.initWooPay( '', e.data.platformCheckoutUserSession ) + api.initWooPay( + e.data.userEmail, + e.data.platformCheckoutUserSession + ) .then( ( response ) => { - if ( 'success' === response.result ) { + if ( response.result === 'success' ) { loginSessionIframeWrapper.classList.add( 'woopay-login-session-iframe-wrapper' ); @@ -485,7 +519,7 @@ export const handleWooPayEmailInput = async ( // Only show the error if it's not an AbortError, // it occurs when the fetch request is aborted because user // clicked the Place Order button while loading. - if ( 'AbortError' !== err.name ) { + if ( err.name !== 'AbortError' ) { showErrorMessage(); } } ) @@ -497,10 +531,24 @@ export const handleWooPayEmailInput = async ( hasCheckedLoginSession = true; closeLoginSessionIframe(); break; + case 'redirect_to_woopay_skip_session_init': + wcpayTracks.recordUserEvent( + wcpayTracks.events.WOOPAY_OTP_COMPLETE, + [], + true + ); + if ( e.data.redirectUrl ) { + window.location = appendRedirectionParams( + e.data.redirectUrl + ); + } + break; case 'redirect_to_platform_checkout': case 'redirect_to_woopay': wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_OTP_COMPLETE + wcpayTracks.events.WOOPAY_OTP_COMPLETE, + [], + true ); api.initWooPay( woopayEmailInput.value, @@ -513,7 +561,7 @@ export const handleWooPayEmailInput = async ( ) { return; } - if ( 'success' === response.result ) { + if ( response.result === 'success' ) { window.location = response.url; } else { showErrorMessage(); @@ -527,14 +575,16 @@ export const handleWooPayEmailInput = async ( break; case 'otp_validation_failed': wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_OTP_FAILED + wcpayTracks.events.WOOPAY_OTP_FAILED, + [], + true ); break; case 'close_modal': closeIframe(); break; case 'iframe_height': - if ( 300 < e.data.height ) { + if ( e.data.height > 300 ) { if ( fullScreenModalBreakpoint <= window.innerWidth ) { // attach iframe to right side of woopayEmailInput. @@ -586,13 +636,17 @@ export const handleWooPayEmailInput = async ( dispatchUserExistEvent( true ); }, 2000 ); - wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_SKIPPED ); + wcpayTracks.recordUserEvent( + wcpayTracks.events.WOOPAY_SKIPPED, + [], + true + ); searchParams.delete( 'skip_woopay' ); let { pathname } = window.location; - if ( '' !== searchParams.toString() ) { + if ( searchParams.toString() !== '' ) { pathname += '?' + searchParams.toString(); } diff --git a/client/checkout/woopay/express-button/express-checkout-iframe.js b/client/checkout/woopay/express-button/express-checkout-iframe.js index 3fadd23ab0c..95b49e62091 100644 --- a/client/checkout/woopay/express-button/express-checkout-iframe.js +++ b/client/checkout/woopay/express-button/express-checkout-iframe.js @@ -3,7 +3,13 @@ */ import { __ } from '@wordpress/i18n'; import { getConfig } from 'utils/checkout'; -import { getTargetElement, validateEmail } from '../utils'; +import request from 'wcpay/checkout/utils/request'; +import { buildAjaxURL } from 'wcpay/payment-request/utils'; +import { + getTargetElement, + validateEmail, + appendRedirectionParams, +} from '../utils'; import wcpayTracks from 'tracks'; export const expressCheckoutIframe = async ( api, context, emailSelector ) => { @@ -86,6 +92,23 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { // Set the initial value. iframeHeaderValue = true; + request( + buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ), + { + _ajax_nonce: getConfig( 'woopaySessionNonce' ), + } + ).then( ( response ) => { + if ( response?.data?.session ) { + iframe.contentWindow.postMessage( + { + action: 'setSessionData', + value: response, + }, + getConfig( 'woopayHost' ) + ); + } + } ); + getWindowSize(); window.addEventListener( 'resize', getWindowSize ); @@ -93,7 +116,11 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { window.addEventListener( 'resize', setPopoverPosition ); iframe.classList.add( 'open' ); - wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_OTP_START ); + wcpayTracks.recordUserEvent( + wcpayTracks.events.WOOPAY_OTP_START, + [], + true + ); } ); // Add the iframe to the wrapper. @@ -107,7 +134,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { ); // Handle Blocks Cart and Checkout notices. - if ( wcSettings.wcBlocksConfig && 'product' !== context ) { + if ( wcSettings.wcBlocksConfig && context !== 'product' ) { // This handles adding the error notice to the cart page. wp.data .dispatch( 'core/notices' ) @@ -148,6 +175,9 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { const closeIframe = () => { window.removeEventListener( 'resize', getWindowSize ); window.removeEventListener( 'resize', setPopoverPosition ); + window.removeEventListener( 'pageshow', onPageShow ); + window.removeEventListener( 'message', onMessage ); + document.removeEventListener( 'keyup', onKeyUp ); iframeWrapper.remove(); iframe.classList.remove( 'open' ); @@ -163,6 +193,10 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { return; } + window.addEventListener( 'pageshow', onPageShow ); + window.addEventListener( 'message', onMessage ); + document.addEventListener( 'keyup', onKeyUp ); + const viewportWidth = window.document.documentElement.clientWidth; const viewportHeight = window.document.documentElement.clientHeight; @@ -186,6 +220,10 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { 'viewport', `${ viewportWidth }x${ viewportHeight }` ); + urlParams.append( + 'tracksUserIdentity', + JSON.stringify( getConfig( 'tracksUserIdentity' ) ) + ); iframe.src = `${ getConfig( 'woopayHost' @@ -200,13 +238,7 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { iframe.focus(); }; - document.addEventListener( 'keyup', ( event ) => { - if ( 'Escape' === event.key && closeIframe() ) { - event.stopPropagation(); - } - } ); - - window.addEventListener( 'message', ( e ) => { + function onMessage( e ) { if ( ! getConfig( 'woopayHost' ).startsWith( e.origin ) ) { return; } @@ -215,21 +247,37 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { case 'otp_email_submitted': userEmail = e.data.userEmail; break; + case 'redirect_to_woopay_skip_session_init': + wcpayTracks.recordUserEvent( + wcpayTracks.events.WOOPAY_OTP_COMPLETE, + [], + true + ); + if ( e.data.redirectUrl ) { + window.location = appendRedirectionParams( + e.data.redirectUrl + ); + } + break; case 'redirect_to_platform_checkout': case 'redirect_to_woopay': wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_OTP_COMPLETE + wcpayTracks.events.WOOPAY_OTP_COMPLETE, + [], + true ); api.initWooPay( - userEmail, + userEmail || e.data.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; + if ( response.result === 'success' ) { + window.location = appendRedirectionParams( + response.url + ); } else { showErrorMessage(); closeIframe( false ); @@ -238,14 +286,16 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { break; case 'otp_validation_failed': wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_OTP_FAILED + wcpayTracks.events.WOOPAY_OTP_FAILED, + [], + true ); break; case 'close_modal': closeIframe(); break; case 'iframe_height': - if ( 300 < e.data.height ) { + if ( e.data.height > 300 ) { if ( fullScreenModalBreakpoint <= window.innerWidth ) { // set height to given value iframe.style.height = e.data.height + 'px'; @@ -264,14 +314,20 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { default: // do nothing, only respond to expected actions. } - } ); + } - window.addEventListener( 'pageshow', function ( event ) { + function onPageShow( event ) { if ( event.persisted ) { // Safari needs to close iframe with this. closeIframe( false ); } - } ); + } + + function onKeyUp( event ) { + if ( event.key === 'Escape' && closeIframe() ) { + event.stopPropagation(); + } + } openIframe( woopayEmailInput?.value ); }; diff --git a/client/checkout/woopay/express-button/index.js b/client/checkout/woopay/express-button/index.js index 130e8b2b75e..8db44bea906 100644 --- a/client/checkout/woopay/express-button/index.js +++ b/client/checkout/woopay/express-button/index.js @@ -11,6 +11,7 @@ import { getConfig } from 'utils/checkout'; import { WoopayExpressCheckoutButton } from './woopay-express-checkout-button'; import WCPayAPI from '../../api'; import request from '../../utils/request'; +import '../../express-checkout-buttons.scss'; const renderWooPayExpressCheckoutButton = () => { // Create an API object, which will be used throughout the checkout. diff --git a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js index 3c4a0a651fc..7b67433e342 100644 --- a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js +++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js @@ -158,6 +158,7 @@ describe( 'WoopayExpressCheckoutButton', () => { test( 'call `addToCart` and `expressCheckoutIframe` on express button click on product page', async () => { useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, + getProductData: jest.fn().mockReturnValue( {} ), isAddToCartDisabled: false, } ) ); render( @@ -186,5 +187,34 @@ describe( 'WoopayExpressCheckoutButton', () => { ); } ); } ); + + test( 'do not call `addToCart` on express button click on product page when validation fails', async () => { + useExpressCheckoutProductHandler.mockImplementation( () => ( { + addToCart: mockAddToCart, + getProductData: jest.fn().mockReturnValue( false ), + isAddToCartDisabled: false, + } ) ); + render( + + ); + + const expressButton = screen.queryByRole( 'button', { + name: 'WooPay', + } ); + + userEvent.click( expressButton ); + + expect( mockAddToCart ).not.toHaveBeenCalled(); + + await waitFor( () => { + expect( expressCheckoutIframe ).not.toHaveBeenCalled(); + } ); + } ); } ); } ); diff --git a/client/checkout/woopay/express-button/use-express-checkout-product-handler.js b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js index 6f503cc7fea..f9a5bb842b0 100644 --- a/client/checkout/woopay/express-button/use-express-checkout-product-handler.js +++ b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js @@ -2,6 +2,7 @@ * External dependencies */ import { useEffect, useState } from 'react'; +import validator from 'validator'; const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { const [ isAddToCartDisabled, setIsAddToCartDisabled ] = useState( false ); @@ -24,7 +25,45 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { return attributes; }; - const addToCart = () => { + const validateGiftCardFields = ( data ) => { + const requiredFields = [ + 'wc_gc_giftcard_to', + 'wc_gc_giftcard_from', + 'wc_gc_giftcard_to_multiple', + ]; + + for ( const requiredField of requiredFields ) { + if ( + data.hasOwnProperty( requiredField ) && + ! data[ requiredField ] + ) { + alert( 'Please fill out all required fields' ); + return false; + } + } + + if ( data.hasOwnProperty( 'wc_gc_giftcard_to_multiple' ) ) { + if ( + ! data.wc_gc_giftcard_to_multiple + .split( ',' ) + .every( ( email ) => validator.isEmail( email.trim() ) ) + ) { + alert( 'Please type only valid emails' ); + return false; + } + } + + if ( data.hasOwnProperty( 'wc_gc_giftcard_to' ) ) { + if ( ! validator.isEmail( data.wc_gc_giftcard_to ) ) { + alert( 'Please type only valid emails' ); + return false; + } + } + + return true; + }; + + const getProductData = () => { let productId = document.querySelector( '.single_add_to_cart_button' ) .value; @@ -49,9 +88,13 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { const formData = new FormData( addOnForm ); formData.forEach( ( value, name ) => { - if ( /^addon-/.test( name ) ) { + if ( + /^addon-/.test( name ) || + /^wc_gc_giftcard_/.test( name ) + ) { if ( /\[\]$/.test( name ) ) { const fieldName = name.substring( 0, name.length - 2 ); + if ( data[ fieldName ] ) { data[ fieldName ].push( value ); } else { @@ -62,8 +105,16 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { } } } ); + + if ( ! validateGiftCardFields( data ) ) { + return false; + } } + return data; + }; + + const addToCart = ( data ) => { return api.expressCheckoutAddToCart( data ); }; @@ -105,6 +156,7 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { return { addToCart: addToCart, + getProductData: getProductData, isAddToCartDisabled: isAddToCartDisabled, }; }; diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index b7f2d944bd7..76477a4e0b1 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -2,7 +2,7 @@ * External dependencies */ import { sprintf, __ } from '@wordpress/i18n'; -import { useEffect } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; /** * Internal dependencies @@ -20,28 +20,50 @@ export const WoopayExpressCheckoutButton = ( { isProductPage = false, emailSelector = '#email', } ) => { + const buttonWidthTypes = { + narrow: 'narrow', + wide: 'wide', + }; + const buttonRef = useRef(); const { type: buttonType, height, size, theme, context } = buttonSettings; + const [ buttonWidthType, setButtonWidthType ] = useState( + buttonWidthTypes.wide + ); + const text = - 'default' !== buttonType + buttonType !== 'default' ? sprintf( __( `%s with`, 'woocommerce-payments' ), buttonType.charAt( 0 ).toUpperCase() + buttonType.slice( 1 ).toLowerCase() ) : ''; - const ThemedWooPayIcon = 'dark' === theme ? WoopayIcon : WoopayIconLight; + const ThemedWooPayIcon = theme === 'dark' ? WoopayIcon : WoopayIconLight; - const { addToCart, isAddToCartDisabled } = useExpressCheckoutProductHandler( - api, - isProductPage - ); + const { + addToCart, + getProductData, + isAddToCartDisabled, + } = useExpressCheckoutProductHandler( api, isProductPage ); + + useEffect( () => { + if ( ! buttonRef.current ) { + return; + } + + const buttonWidth = buttonRef.current.getBoundingClientRect().width; + const isButtonWide = buttonWidth > 140; + setButtonWidthType( + isButtonWide ? buttonWidthTypes.wide : buttonWidthTypes.narrow + ); + }, [ buttonWidthTypes.narrow, buttonWidthTypes.wide ] ); useEffect( () => { if ( ! isPreview ) { wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_EXPRESS_BUTTON_OFFERED, + wcpayTracks.events.WOOPAY_BUTTON_LOAD, { - context, + source: context, } ); } @@ -54,15 +76,18 @@ export const WoopayExpressCheckoutButton = ( { return; // eslint-disable-line no-useless-return } - wcpayTracks.recordUserEvent( - wcpayTracks.events.WOOPAY_EXPRESS_BUTTON_CLICKED, - { - context: context, - } - ); + wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_BUTTON_CLICK, { + source: context, + } ); if ( isProductPage ) { - addToCart() + const productData = getProductData(); + + if ( ! productData ) { + return; + } + + addToCart( productData ) .then( () => { expressCheckoutIframe( api, context, emailSelector ); } ) @@ -76,14 +101,16 @@ export const WoopayExpressCheckoutButton = ( { return ( + +`; + +exports[`StatusChip renders pending verification status for po 1`] = ` +
+ - ); - } ); - - // We'll render the actions ourselves so we need to remove them from the props sent to the notice component. - delete noticeProps.actions; - } + const handleRemove = () => onRemove?.(); return ( - - - { icon && ( - - - +
+ { iconToDisplay && ( + + ) } +
+ { children } + { actions.length > 0 && ( +
+ { actions.map( + ( + { + className: buttonCustomClasses, + label, + variant, + onClick, + url, + }, + index + ) => { + let computedVariant = variant; + if ( variant !== 'primary' ) { + computedVariant = ! url + ? 'secondary' + : 'link'; + } + + return ( + + ); + } + ) } +
) } - - { noticeProps.children } - { actions && ( - - { actions } - +
+ { isDismissible && ( +
); -} +}; export default BannerNotice; diff --git a/client/components/banner-notice/style.scss b/client/components/banner-notice/style.scss new file mode 100644 index 00000000000..4ab24170454 --- /dev/null +++ b/client/components/banner-notice/style.scss @@ -0,0 +1,65 @@ +.wcpay-banner-notice { + display: flex; + font-family: $default-font; + font-size: $default-font-size; + background-color: $white; + border-left: $gap-smallest solid $components-color-accent; + fill: $components-color-accent; + margin: $gap-large 0; + padding: $gap-small; + box-shadow: 0 2px 6px 0 rgba( 0, 0, 0, 0.05 ); + + &.is-success { + border-left-color: $alert-green; + fill: $alert-green; + } + + &.is-warning { + border-left-color: $alert-yellow; + fill: $alert-yellow; + } + + &.is-error { + border-left-color: $alert-red; + fill: $alert-red; + } + + &.is-warning &__icon, + &.is-error &__icon { + height: 21px; // Adjust gridicon height to match other icons + } + + &__icon { + flex-shrink: 0; + margin-right: $gap-small; + } + + &__content { + flex-grow: 1; + } + + &__actions { + display: grid; + grid-auto-flow: column; + grid-auto-columns: min-content; + column-gap: $gap-small; + margin-top: $gap-small; + } + + &__dismiss.components-button.has-icon { + flex-shrink: 0; + padding: 0; + min-width: 24px; + height: 24px; + + svg { + width: 20px; + } + } + + /* Margin exceptions */ + & + &, + &:first-child { + margin-top: 0; + } +} diff --git a/client/components/banner-notice/styles.scss b/client/components/banner-notice/styles.scss deleted file mode 100644 index e025a9f2e2f..00000000000 --- a/client/components/banner-notice/styles.scss +++ /dev/null @@ -1,125 +0,0 @@ -$is-info: #007cba; -$is-info-hover: #006ba1; -$is-warning: #bd8600; -$is-warning-hover: #a16f00; -$is-error: #cc1818; -$is-error-hover: #b30f0f; -$is-success: #00a32a; -$is-success-hover: #00982a; - -.wcpay-banner-notice.components-notice { - padding: 11px 0 11px 17px; - border-left: none; - border-radius: 2px; - justify-content: flex-start; - - /* Shared styles for all variants */ - .wcpay-banner-notice__icon { - display: flex; - align-items: center; - align-self: flex-start; - min-height: auto; - min-width: auto; - margin-right: 5px; - svg { - width: 22px; - height: 22px; - } - } - .components-notice__content { - margin-top: 2px; - margin-bottom: 2px; - } - .wcpay-banner-notice__content__actions { - padding-top: 12px; - } - a.wcpay-banner-notice__action { - text-decoration: none; - } - &.is-dismissible { - padding-right: 12px; - } - .components-notice__dismiss { - height: 24px; - width: 24px; - svg { - width: 15px; - height: 15px; - fill: $gray-900; - } - } - - /* Specific styles for each variant */ - &.is-info { - background-color: #f0f6fc; - .wcpay-banner-notice__icon svg { - fill: $is-info; - } - button.wcpay-banner-notice__action { - box-shadow: inset 0 0 0 1px $is-info; - &:hover { - box-shadow: inset 0 0 0 1px $is-info-hover; - } - } - .wcpay-banner-notice__action { - color: $is-info; - &:hover { - color: $is-info-hover; - } - } - } - &.is-warning { - background-color: #fcf9e8; - .wcpay-banner-notice__icon svg { - fill: $is-warning; - } - button.wcpay-banner-notice__action { - box-shadow: inset 0 0 0 1px $is-warning; - &:hover { - box-shadow: inset 0 0 0 1px $is-warning-hover; - } - } - .wcpay-banner-notice__action { - color: $is-warning; - &:hover { - color: $is-warning-hover; - } - } - } - &.is-error { - background-color: #fcf0f1; - .wcpay-banner-notice__icon svg { - fill: $is-error; - } - button.wcpay-banner-notice__action { - box-shadow: inset 0 0 0 1px $is-error; - &:hover { - box-shadow: inset 0 0 0 1px $is-error-hover; - } - } - .wcpay-banner-notice__action { - color: $is-error; - &:hover { - color: $is-error-hover; - } - } - } - &.is-success { - background-color: #edfaef; - .wcpay-banner-notice__icon svg { - fill: $is-success; - } - button.wcpay-banner-notice__action { - box-shadow: inset 0 0 0 1px $is-success; - &:hover { - box-shadow: inset 0 0 0 1px $is-success-hover; - } - } - .wcpay-banner-notice__action { - color: $is-success; - &:hover { - color: $is-success-hover; - } - } - } -} 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 c7b2599f363..32e6eb3fdf7 100644 --- a/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap +++ b/client/components/banner-notice/tests/__snapshots__/index.test.tsx.snap @@ -1,198 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Info BannerNotices renders with dismiss 1`] = ` +exports[`BannerNotice should match snapshot 1`] = `
-
-
-
- Test notice content -
-
-
-
- -
-
-`; - -exports[`Info BannerNotices renders with dismiss and icon 1`] = ` -
-
+ Custom Icon +
+ Example
-
- -
-
+
-
-
-
- -
-
-`; - -exports[`Info BannerNotices renders with dismiss and icon and actions 1`] = ` -
-
-
-
-
+ - - URL - -
-
+ Submit +
-
`; - -exports[`Info BannerNotices renders without dismiss and icon 1`] = ` -
-
-
-
-
- Test notice content -
-
-
-
-
-
-`; diff --git a/client/components/banner-notice/tests/index.test.tsx b/client/components/banner-notice/tests/index.test.tsx index 3a98e3363b3..819a9bf935e 100644 --- a/client/components/banner-notice/tests/index.test.tsx +++ b/client/components/banner-notice/tests/index.test.tsx @@ -2,145 +2,111 @@ * External dependencies */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; +import { mocked } from 'ts-jest/utils'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies */ import BannerNotice from '../'; -describe( 'Info BannerNotices renders', () => { - test( 'with dismiss', () => { - const { container } = render( - - ); - expect( container ).toMatchSnapshot(); - } ); +jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) ); - test( 'with dismiss and icon', () => { - const { container } = render( - - ); - expect( container ).toMatchSnapshot(); +describe( 'BannerNotice', () => { + beforeEach( () => { + mocked( speak ).mockClear(); } ); - test( 'with dismiss and icon and actions', () => { + it( 'should match snapshot', () => { + const onClick = jest.fn(); const { container } = render( Custom Icon } actions={ [ - { - label: 'Button', - onClick: jest.fn(), - }, - { - label: 'URL', - url: 'https://wordpress.com', - }, + { label: 'More information', url: 'https://example.com' }, + { label: 'Cancel', onClick }, + { label: 'Submit', onClick, variant: 'primary' }, ] } - /> + > + Example + ); expect( container ).toMatchSnapshot(); } ); - test( 'without dismiss and icon', () => { - const { container } = render( - - ); - expect( container ).toMatchSnapshot(); + it( 'should default to info status', () => { + const { + container: { firstChild }, + } = render( FYI ); + + expect( firstChild ).toHaveClass( 'is-info' ); } ); -} ); -describe( 'Action click triggers callback', () => { - test( 'with dismiss and icon and actions', () => { - const onClickMock = jest.fn(); - const { getByText } = render( - + /***************** */ + + it( 'calls action onClick when clicked', () => { + const onClick = jest.fn(); + render( + + Notice with Action + ); - fireEvent.click( getByText( 'Button' ) ); - expect( onClickMock ).toHaveBeenCalled(); + user.click( screen.getByText( 'Action' ) ); + + expect( onClick ).toHaveBeenCalled(); } ); - test( 'With icon and multiple button actions', () => { - const onButtonClickOne = jest.fn(); - const onButtonClickTwo = jest.fn(); - const { getByText } = render( - + it( 'calls onRemove when dismiss button is clicked', () => { + const onRemove = jest.fn(); + render( + + Dismissible Notice + ); - expect( onButtonClickOne ).not.toHaveBeenCalled(); - expect( onButtonClickTwo ).not.toHaveBeenCalled(); + user.click( screen.getByLabelText( 'Dismiss this notice' ) ); - // Click Button 1 - fireEvent.click( getByText( 'Button one' ) ); - expect( onButtonClickOne ).toHaveBeenCalled(); - expect( onButtonClickTwo ).not.toHaveBeenCalled(); - - // Click Button 1 - fireEvent.click( getByText( 'Button two' ) ); - expect( onButtonClickTwo ).toHaveBeenCalled(); + expect( onRemove ).toHaveBeenCalled(); } ); -} ); -describe( 'Dismiss click triggers callback', () => { - test( 'with dismiss and icon and actions', () => { - const onDismissMock = jest.fn(); - const { getByLabelText } = render( - - ); + describe( 'useSpokenMessage', () => { + it( 'should speak the given message', () => { + render( FYI ); + + expect( speak ).toHaveBeenCalledWith( 'FYI', 'polite' ); + } ); + + it( 'should speak the given message by implicit politeness by status', () => { + render( Uh oh! ); + + expect( speak ).toHaveBeenCalledWith( 'Uh oh!', 'assertive' ); + } ); + + it( 'should coerce a message to a string', () => { + render( + + With emphasis this time. + + ); + + expect( speak ).toHaveBeenCalledWith( + 'With emphasis this time.', + 'polite' + ); + } ); + + it( 'should not re-speak an effectively equivalent element message', () => { + const { rerender } = render( + Duplicated notice message. + ); + rerender( Duplicated notice message. ); - fireEvent.click( getByLabelText( 'Dismiss this notice' ) ); - expect( onDismissMock ).toHaveBeenCalled(); + expect( speak ).toHaveBeenCalledTimes( 1 ); + } ); } ); } ); diff --git a/client/components/chip/index.js b/client/components/chip/index.tsx similarity index 51% rename from client/components/chip/index.js rename to client/components/chip/index.tsx index a06cb5e1fc0..33faa70ccc8 100755 --- a/client/components/chip/index.js +++ b/client/components/chip/index.tsx @@ -1,22 +1,31 @@ /** @format **/ +/** + * External dependencies + */ +import React from 'react'; + /** * Internal dependencies */ import './style.scss'; import { HoverTooltip } from 'wcpay/components/tooltip'; -const types = [ 'primary', 'light', 'warning', 'alert' ]; - -const Chip = ( props ) => { - const { message, type, isCompact, className, tooltip } = props; +export type ChipType = 'primary' | 'success' | 'light' | 'warning' | 'alert'; - const classNames = [ - 'chip', - `chip-${ types.find( ( t ) => t === type ) || 'primary' }`, - isCompact ? 'is-compact' : '', - className ?? '', - ]; +interface Props { + message: string; + type?: ChipType; + className?: string; + tooltip?: string; +} +const Chip: React.FC< Props > = ( { + message, + type = 'primary', + className, + tooltip, +} ) => { + const classNames = [ 'chip', `chip-${ type }`, className ?? '' ]; if ( tooltip ) { return ( diff --git a/client/components/chip/style.scss b/client/components/chip/style.scss index 61eebcfd47c..4d2c020a38e 100755 --- a/client/components/chip/style.scss +++ b/client/components/chip/style.scss @@ -1,33 +1,38 @@ /** @format */ .chip { - @include font-size( 14 ); - display: inline-block; - padding: 0.25rem 0.75rem; - border-radius: 1rem; + @include font-size( 13 ); + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 300; + padding: 0.25rem 0.5rem; + border-radius: 0.2rem; + line-height: 1.5; white-space: nowrap; &.chip-primary { - background: $studio-blue-5; - color: $studio-blue-80; + background: $wp-blue-0; + color: $wp-blue-70; + } + + &.chip-success { + background: $wp-green-0; + color: $wp-green-70; } &.chip-light { - background: $studio-gray-5; - color: $studio-gray-80; + background: $wp-gray-0; + color: $wp-gray-80; } &.chip-alert { - background: $studio-red-5; - color: $studio-red-80; + background: $wp-red-0; + color: $wp-red-70; } &.chip-warning { - background: $studio-yellow-5; - color: $studio-yellow-50; - } - - &.is-compact { - padding: 0.06rem 0.65rem 0.1rem; + background: $wp-yellow-0; + color: $wp-yellow-70; } } diff --git a/client/components/chip/test/__snapshots__/index.js.snap b/client/components/chip/test/__snapshots__/index.test.tsx.snap old mode 100755 new mode 100644 similarity index 88% rename from client/components/chip/test/__snapshots__/index.js.snap rename to client/components/chip/test/__snapshots__/index.test.tsx.snap index d59afdcf01d..91f92711cac --- a/client/components/chip/test/__snapshots__/index.js.snap +++ b/client/components/chip/test/__snapshots__/index.test.tsx.snap @@ -68,13 +68,3 @@ exports[`Chip renders an alert chip 1`] = `
`; - -exports[`Chip renders default if type is invalid 1`] = ` -
- - Message - -
-`; diff --git a/client/components/chip/test/index.js b/client/components/chip/test/index.js deleted file mode 100755 index e33251dbf3a..00000000000 --- a/client/components/chip/test/index.js +++ /dev/null @@ -1,57 +0,0 @@ -/** @format */ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import Chip from '../'; - -describe( 'Chip', () => { - test( 'renders an alert chip', () => { - const { container: chip } = renderChip( 'alert', 'Alert message' ); - expect( chip ).toMatchSnapshot(); - } ); - - test( 'renders a primary chip', () => { - const { container: chip } = renderChip( 'primary', 'Primary message' ); - expect( chip ).toMatchSnapshot(); - } ); - - test( 'renders a light chip', () => { - const { container: chip } = renderChip( 'light', 'Light message' ); - 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(); - } ); - - test( 'renders a warning chip', () => { - const { container: chip } = renderChip( 'warning', 'Alert message' ); - expect( chip ).toMatchSnapshot(); - } ); - - test( 'renders default if type is invalid', () => { - const { container: chip } = renderChip( 'invalidtype', 'Message' ); - expect( chip ).toMatchSnapshot(); - } ); - - function renderChip( type, message, tooltip ) { - return render( - - ); - } -} ); diff --git a/client/components/chip/test/index.test.tsx b/client/components/chip/test/index.test.tsx new file mode 100755 index 00000000000..600374dcd57 --- /dev/null +++ b/client/components/chip/test/index.test.tsx @@ -0,0 +1,60 @@ +/** @format */ + +/** + * External dependencies + */ +import React from 'react'; + +/** + * External dependencies + */ +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import Chip from '../'; + +describe( 'Chip', () => { + test( 'renders an alert chip', () => { + const { container: chip } = render( + + ); + expect( chip ).toMatchSnapshot(); + } ); + + test( 'renders a primary chip', () => { + const { container: chip } = render( + + ); + expect( chip ).toMatchSnapshot(); + } ); + + test( 'renders a light chip', () => { + const { container: chip } = render( + + ); + expect( chip ).toMatchSnapshot(); + } ); + + test( 'renders a chip with a tooltip', () => { + const { container: chip } = render( + + ); + expect( chip ).toMatchSnapshot(); + } ); + + test( 'renders a primary chip by default', () => { + const { container: chip } = render( + + ); + expect( chip ).toMatchSnapshot(); + } ); + + test( 'renders a warning chip', () => { + const { container: chip } = render( + + ); + expect( chip ).toMatchSnapshot(); + } ); +} ); diff --git a/client/components/currency-information-for-methods/index.js b/client/components/currency-information-for-methods/index.js index 3e9c2feb1c2..38ca0b133cf 100644 --- a/client/components/currency-information-for-methods/index.js +++ b/client/components/currency-information-for-methods/index.js @@ -11,13 +11,13 @@ import interpolateComponents from '@automattic/interpolate-components'; */ import { useCurrencies, useEnabledCurrencies } from '../../data'; import WCPaySettingsContext from '../../settings/wcpay-settings-context'; -import InlineNotice from '../inline-notice'; +import InlineNotice from 'components/inline-notice'; import PaymentMethodsMap from '../../payment-methods-map'; const ListToCommaSeparatedSentencePartConverter = ( items ) => { - if ( 1 === items.length ) { + if ( items.length === 1 ) { return items[ 0 ]; - } else if ( 2 === items.length ) { + } else if ( items.length === 2 ) { return items.join( ' ' + __( 'and', 'woocommerce-payments' ) + ' ' ); } const lastItem = items.pop(); @@ -49,7 +49,7 @@ const CurrencyInformationForMethods = ( { selectedMethods } ) => { const missingCurrencies = []; selectedMethods.map( ( paymentMethod ) => { - if ( 'undefined' !== typeof PaymentMethodsMap[ paymentMethod ] ) { + if ( typeof PaymentMethodsMap[ paymentMethod ] !== 'undefined' ) { PaymentMethodsMap[ paymentMethod ].currencies.map( ( currency ) => { if ( ! enabledCurrenciesIds.includes( currency.toLowerCase() ) @@ -66,7 +66,7 @@ const CurrencyInformationForMethods = ( { selectedMethods } ) => { currencyInfo.available[ currency ]; const missingCurrencyLabel = - null != missingCurrencyInfo + missingCurrencyInfo != null ? missingCurrencyInfo.name + ' (' + ( undefined !== missingCurrencyInfo.symbol @@ -88,9 +88,9 @@ const CurrencyInformationForMethods = ( { selectedMethods } ) => { paymentMethodsWithMissingCurrencies ); - if ( 0 < missingCurrencyLabels.length ) { + if ( missingCurrencyLabels.length > 0 ) { return ( - + { interpolateComponents( { mixedString: sprintf( __( @@ -107,7 +107,7 @@ const CurrencyInformationForMethods = ( { selectedMethods } ) => { paymentMethodsWithMissingCurrencies.length, 'woocommerce-payments' ), - 1 === missingCurrencyLabels.length ? 'an' : '', + missingCurrencyLabels.length === 1 ? 'an' : '', _n( 'currency', 'currencies', diff --git a/client/components/deposit-status-chip/index.tsx b/client/components/deposit-status-chip/index.tsx new file mode 100644 index 00000000000..e0e7ca10050 --- /dev/null +++ b/client/components/deposit-status-chip/index.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import * as React from 'react'; + +/** + * Internal dependencies + */ +import { displayStatus } from 'deposits/strings'; +import Chip, { ChipType } from 'components/chip'; +import type { DepositStatus } from 'wcpay/types/deposits'; + +/** + * Maps a DepositStatus to a ChipType. + */ +const mappings: Record< DepositStatus, ChipType > = { + estimated: 'light', + pending: 'warning', + in_transit: 'success', + paid: 'success', + failed: 'alert', + canceled: 'alert', +}; + +/** + * Renders a deposits status chip. + * + * @return {JSX.Element} Deposit status chip. + */ +const DepositStatusChip: React.FC< { + status: DepositStatus; +} > = ( { status } ): JSX.Element => ( + +); + +export default DepositStatusChip; diff --git a/client/components/deposit-status-chip/test/index.test.tsx b/client/components/deposit-status-chip/test/index.test.tsx new file mode 100644 index 00000000000..ee1575e1d85 --- /dev/null +++ b/client/components/deposit-status-chip/test/index.test.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import DepositStatusChip from '..'; + +describe( 'Deposits status chip renders', () => { + test( 'Renders In Transit status chip.', () => { + const { getByText } = render( + + ); + expect( getByText( 'Estimated' ) ).toBeTruthy(); + } ); + + test( 'Renders In Transit status chip.', () => { + const { getByText } = render( ); + expect( getByText( 'Pending' ) ).toBeTruthy(); + } ); + + test( 'Renders In Transit status chip.', () => { + const { getByText } = render( ); + expect( getByText( 'Paid' ) ).toBeTruthy(); + } ); + + test( 'Renders In Transit status chip.', () => { + const { getByText } = render( + + ); + expect( getByText( 'In transit' ) ).toBeTruthy(); + } ); +} ); diff --git a/client/components/deposit-status-pill/index.tsx b/client/components/deposit-status-pill/index.tsx deleted file mode 100644 index 7f5dc2c74c9..00000000000 --- a/client/components/deposit-status-pill/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { displayStatus } from 'deposits/strings'; -import Pill from 'components/pill'; - -const mappings: { - [ key: string ]: 'primary' | 'success' | 'alert' | 'danger' | 'light'; -} = { - estimated: 'light', - pending: 'alert', - in_transit: 'success', - paid: 'success', - failed: 'danger', - canceled: 'danger', -}; - -/** - * Renders a deposits status pill. - * Based off of the Pill component found components/pill. - * - * @return {JSX.Element} Deposit status pill. - */ -const DepositStatusPill: React.FC< { - status: string; -} > = ( { status } ): JSX.Element => { - const label = displayStatus[ status as keyof typeof displayStatus ] - ? displayStatus[ status as keyof typeof displayStatus ] - : __( 'Unknown', 'woocommerce-payments' ); - - const type = status && mappings[ status ] ? mappings[ status ] : 'light'; - - return { label }; -}; - -export default DepositStatusPill; diff --git a/client/components/deposit-status-pill/test/index.test.tsx b/client/components/deposit-status-pill/test/index.test.tsx deleted file mode 100644 index 67160025981..00000000000 --- a/client/components/deposit-status-pill/test/index.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import DepositStatusPill from '..'; - -describe( 'Deposits status pill renders', () => { - test( 'Renders default status pill "unknown" when unknown/invalid status is given.', () => { - const { getByText } = render( ); - - expect( getByText( 'Unknown' ) ).toBeTruthy(); - } ); - - test( 'Renders In Transit status pill.', () => { - const { getByText } = render( - - ); - expect( getByText( 'Estimated' ) ).toBeTruthy(); - } ); - - test( 'Renders In Transit status pill.', () => { - const { getByText } = render( ); - expect( getByText( 'Pending' ) ).toBeTruthy(); - } ); - - test( 'Renders In Transit status pill.', () => { - const { getByText } = render( ); - expect( getByText( 'Paid' ) ).toBeTruthy(); - } ); - - test( 'Renders In Transit status pill.', () => { - const { getByText } = render( - - ); - expect( getByText( 'In transit' ) ).toBeTruthy(); - } ); -} ); diff --git a/client/components/deposits-overview/deposit-schedule.tsx b/client/components/deposits-overview/deposit-schedule.tsx index 4d2b4c9135c..86c9493bfd0 100644 --- a/client/components/deposits-overview/deposit-schedule.tsx +++ b/client/components/deposits-overview/deposit-schedule.tsx @@ -10,6 +10,7 @@ import moment from 'moment'; * Internal dependencies */ import { getDepositMonthlyAnchorLabel } from 'wcpay/deposits/utils'; +import type * as AccountOverview from 'wcpay/types/account-overview'; /** * The type of the props for the DepositScheduleDescription component. diff --git a/client/components/deposits-overview/next-deposit.tsx b/client/components/deposits-overview/next-deposit.tsx index 5406d7372ff..cf9a08b23fd 100644 --- a/client/components/deposits-overview/next-deposit.tsx +++ b/client/components/deposits-overview/next-deposit.tsx @@ -10,21 +10,20 @@ import { Icon, } from '@wordpress/components'; import { calendar } from '@wordpress/icons'; -import InfoOutlineIcon from 'gridicons/dist/info-outline'; -import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; import interpolateComponents from '@automattic/interpolate-components'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies. */ import Loadable from 'components/loadable'; import { getNextDeposit } from './utils'; -import DepositStatusPill from 'components/deposit-status-pill'; +import DepositStatusChip from 'components/deposit-status-chip'; import { getDepositDate } from 'deposits/utils'; import { useAllDepositsOverviews, useDepositIncludesLoan } from 'wcpay/data'; -import BannerNotice from 'wcpay/components/banner-notice'; +import InlineNotice from 'components/inline-notice'; import { useSelectedCurrency } from 'wcpay/overview/hooks'; +import type * as AccountOverview from 'wcpay/types/account-overview'; type NextDepositProps = { isLoading: boolean; @@ -32,11 +31,7 @@ type NextDepositProps = { }; const DepositIncludesLoanPayoutNotice = () => ( - } - isDismissible={ false } - > + { interpolateComponents( { mixedString: __( 'This deposit will include funds from your WooCommerce Capital loan. {{learnMoreLink}}Learn more{{/learnMoreLink}}', @@ -48,7 +43,7 @@ const DepositIncludesLoanPayoutNotice = () => ( // eslint-disable-next-line jsx-a11y/anchor-has-content ( ), }, } ) } - + ); const NewAccountWaitingPeriodNotice = () => ( - } + icon className="new-account-waiting-period-notice" isDismissible={ false } > @@ -78,25 +73,29 @@ const NewAccountWaitingPeriodNotice = () => ( ), }, } ) } - + ); const NegativeBalanceDepositsPausedNotice = () => ( - } + icon className="negative-balance-deposits-paused-notice" isDismissible={ false } > { interpolateComponents( { - mixedString: __( - 'Deposits may be interrupted while your WooCommerce Payments balance remains negative. {{whyLink}}Why?{{/whyLink}}', - 'woocommerce-payments' + mixedString: sprintf( + /* translators: %s: WooPayments */ + __( + 'Deposits may be interrupted while your %s balance remains negative. {{whyLink}}Why?{{/whyLink}}', + 'woocommerce-payments' + ), + 'WooPayments' ), components: { whyLink: ( @@ -105,12 +104,12 @@ const NegativeBalanceDepositsPausedNotice = () => ( ), }, } ) } - + ); /** @@ -197,7 +196,7 @@ const NextDepositDetails: React.FC< NextDepositProps > = ( { isLoading={ isLoading } placeholder="Estimated" children={ - } diff --git a/client/components/deposits-overview/recent-deposits-list.tsx b/client/components/deposits-overview/recent-deposits-list.tsx index d048de64b4d..a0cbfea5fc2 100644 --- a/client/components/deposits-overview/recent-deposits-list.tsx +++ b/client/components/deposits-overview/recent-deposits-list.tsx @@ -11,7 +11,6 @@ import { } from '@wordpress/components'; import { calendar } from '@wordpress/icons'; import { Link } from '@woocommerce/components'; -import InfoOutlineIcon from 'gridicons/dist/info-outline'; import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -19,12 +18,12 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies. */ import './style.scss'; -import DepositStatusPill from 'components/deposit-status-pill'; +import DepositStatusChip from 'components/deposit-status-chip'; import { getDepositDate } from 'deposits/utils'; import { CachedDeposit } from 'wcpay/types/deposits'; import { formatCurrency } from 'wcpay/utils/currency'; import { getDetailsURL } from 'wcpay/components/details-link'; -import BannerNotice from '../banner-notice'; +import InlineNotice from '../inline-notice'; interface DepositRowProps { deposit: CachedDeposit; @@ -53,7 +52,7 @@ const DepositTableRow: React.FC< DepositRowProps > = ( { - + { formatCurrency( deposit.amount, deposit.currency ) } @@ -89,10 +88,10 @@ const RecentDepositsList: React.FC< RecentDepositsProps > = ( { { deposit.id === oldestPendingDepositId && ( - } + icon children={ 'Deposits pending or in-transit may take 1-2 business days to appear in your bank account once dispatched' } diff --git a/client/components/deposits-overview/style.scss b/client/components/deposits-overview/style.scss index 7be1679ced9..9b2c500af31 100644 --- a/client/components/deposits-overview/style.scss +++ b/client/components/deposits-overview/style.scss @@ -19,7 +19,7 @@ } } } - .wcpay-banner-notice.components-notice { + .wcpay-inline-notice.components-notice { margin: 0; } @@ -27,7 +27,7 @@ // in the notices container and to the business delay // notice if it's the last child of the Deposit history table. &__notices__container - > .wcpay-banner-notice.components-notice:not( :last-child ), + > .wcpay-inline-notice.components-notice:not( :last-child ), .wcpay-deposits-overview__business-day-delay-notice:last-child { margin-bottom: 16px; } diff --git a/client/components/deposits-overview/suspended-deposit-notice.tsx b/client/components/deposits-overview/suspended-deposit-notice.tsx index 17907dcf918..de5aa05fca3 100644 --- a/client/components/deposits-overview/suspended-deposit-notice.tsx +++ b/client/components/deposits-overview/suspended-deposit-notice.tsx @@ -9,8 +9,7 @@ import { Link } from '@woocommerce/components'; /** * Internal dependencies */ -import BannerNotice from 'components/banner-notice'; -import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import InlineNotice from 'components/inline-notice'; /** * Renders a notice informing the user that their deposits are suspended. @@ -19,9 +18,9 @@ import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; */ function SuspendedDepositNotice(): JSX.Element { return ( - } + icon isDismissible={ false } status="warning" > @@ -36,13 +35,13 @@ function SuspendedDepositNotice(): JSX.Element { suspendLink: ( ), }, } ) } - + ); } diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index 2dee50c0b52..46810596530 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -141,9 +141,7 @@ exports[`Deposits Overview information Component Renders 1`] = ` data-wp-component="FlexItem" > Estimated @@ -265,9 +263,7 @@ exports[`Deposits Overview information Component Renders 1`] = ` data-wp-component="FlexItem" > Paid @@ -315,9 +311,7 @@ exports[`Deposits Overview information Component Renders 1`] = ` data-wp-component="FlexItem" > Pending @@ -331,7 +325,7 @@ exports[`Deposits Overview information Component Renders 1`] = `
@@ -361,7 +355,7 @@ exports[`Deposits Overview information Component Renders 1`] = `
@@ -418,7 +412,7 @@ exports[`Deposits Overview information Component Renders 1`] = ` exports[`Suspended Deposit Notice Renders Component Renders 1`] = `
@@ -448,7 +442,7 @@ exports[`Suspended Deposit Notice Renders Component Renders 1`] = `
@@ -459,7 +453,7 @@ exports[`Suspended Deposit Notice Renders Component Renders 1`] = ` . Learn more diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index c4e092d55f9..a69aa3408cb 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -9,7 +9,6 @@ import { render } from '@testing-library/react'; */ import DepositsOverview from '..'; import NextDepositDetails from '../next-deposit'; -import { CachedDeposit } from 'wcpay/types/deposits'; import RecentDepositsList from '../recent-deposits-list'; import DepositsOverviewFooter from '../footer'; import DepositSchedule from '../deposit-schedule'; @@ -23,6 +22,8 @@ import { useDeposits, useAllDepositsOverviews, } from 'wcpay/data'; +import type { CachedDeposit, DepositStatus } from 'wcpay/types/deposits'; +import type * as AccountOverview from 'wcpay/types/account-overview'; jest.mock( 'wcpay/data', () => ( { useDepositIncludesLoan: jest.fn(), @@ -90,7 +91,7 @@ const createMockOverview = ( currencyCode: string, depositAmount: number, depositDate: number, - depositStatus: string + depositStatus: DepositStatus ): AccountOverview.Overview => { return { currency: currencyCode, @@ -321,21 +322,6 @@ describe( 'Deposits Overview information', () => { expect( getByText( 'October 1, 2021' ) ).toBeTruthy(); } ); - test( 'Confirm next deposit default status and date', () => { - const overview = createMockOverview( 'usd', 100, 0, 'rubbish' ); - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); - mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'eur', - setSelectedCurrency: mockSetSelectedCurrency, - } ); - - const { getByText } = render( - - ); - expect( getByText( 'Unknown' ) ).toBeTruthy(); - expect( getByText( '—' ) ).toBeTruthy(); - } ); - test( 'Confirm recent deposits renders ', () => { const { getByText } = render( @@ -350,7 +336,7 @@ describe( 'Deposits Overview information', () => { } ); test( 'Renders capital loan notice if deposit includes financing payout', () => { - const overview = createMockOverview( 'usd', 100, 0, 'rubbish' ); + const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: true, isLoading: false, @@ -378,12 +364,12 @@ describe( 'Deposits Overview information', () => { } ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/stripe-capital/overview' + 'https://woocommerce.com/document/woopayments/stripe-capital/overview/' ); } ); test( `Doesn't render capital loan notice if deposit does not include financing payout`, () => { - const overview = createMockOverview( 'usd', 100, 0, 'rubbish' ); + const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: false, isLoading: false, @@ -442,7 +428,7 @@ describe( 'Deposits Overview information', () => { } ); expect( getByRole( 'link', { name: /Why\?/ } ) ).toHaveAttribute( 'href', - 'https://woocommerce.com/document/woocommerce-payments/deposits/deposit-schedule/#section-1' + 'https://woocommerce.com/document/woopayments/deposits/deposit-schedule/#new-accounts' ); } ); } ); @@ -560,7 +546,7 @@ describe( 'Paused Deposit notice Renders', () => { ); getByText( - 'Deposits may be interrupted while your WooCommerce Payments balance remains negative. Why?' + 'Deposits may be interrupted while your WooPayments balance remains negative. Why?' ); } ); test( 'When available balance is positive', () => { @@ -584,7 +570,7 @@ describe( 'Paused Deposit notice Renders', () => { ); expect( queryByText( - 'Deposits may be interrupted while your WooCommerce Payments balance remains negative. Why?' + 'Deposits may be interrupted while your WooPayments balance remains negative. Why?' ) ).toBeFalsy(); } ); diff --git a/client/components/deposits-overview/utils.ts b/client/components/deposits-overview/utils.ts index 233f88751ce..e3c93323fd0 100644 --- a/client/components/deposits-overview/utils.ts +++ b/client/components/deposits-overview/utils.ts @@ -2,11 +2,13 @@ * Internal dependencies */ import { formatCurrency } from 'utils/currency'; +import type { DepositStatus } from 'wcpay/types/deposits'; +import type * as AccountOverview from 'wcpay/types/account-overview'; type NextDepositTableData = { id?: string; date: number; - status: string; + status: DepositStatus; amount: string; }; diff --git a/client/components/deposits-status/index.js b/client/components/deposits-status/index.tsx similarity index 64% rename from client/components/deposits-status/index.js rename to client/components/deposits-status/index.tsx index 97ba19708fe..2d40bd06b48 100644 --- a/client/components/deposits-status/index.js +++ b/client/components/deposits-status/index.tsx @@ -7,17 +7,42 @@ import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle'; import GridiconNotice from 'gridicons/dist/notice'; import { __ } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; +import React from 'react'; /** * Internal dependencies */ import 'components/account-status/shared.scss'; +import type { AccountStatus } from 'wcpay/types/account/account-status'; -const DepositsStatus = ( { status, interval, accountStatus, iconSize } ) => { +type DepositsStatus = 'enabled' | 'disabled' | 'blocked'; +type DepositsIntervals = 'daily' | 'weekly' | 'monthly' | 'manual'; + +interface Props { + status: DepositsStatus; + interval: DepositsIntervals; + accountStatus: AccountStatus; + poEnabled: boolean; + poComplete: boolean; + iconSize: number; +} + +const DepositsStatus: React.FC< Props > = ( { + status, + interval, + accountStatus, + poEnabled, + poComplete, + iconSize, +} ) => { let className = 'account-status__info__green'; let description; let icon = ; - const automaticIntervals = [ 'daily', 'weekly', 'monthly' ]; + const automaticIntervals: DepositsIntervals[] = [ + 'daily', + 'weekly', + 'monthly', + ]; const showSuspendedNotice = 'blocked' === status; if ( 'pending_verification' === accountStatus ) { @@ -25,12 +50,18 @@ const DepositsStatus = ( { status, interval, accountStatus, iconSize } ) => { className = 'account-status__info__gray'; icon = ; } else if ( 'disabled' === status ) { - description = __( 'Disabled', 'woocommerce-payments' ); - className = 'account-status__info__red'; + description = + poEnabled && ! poComplete + ? __( 'Not connected', 'woocommerce-payments' ) + : __( 'Disabled', 'woocommerce-payments' ); + className = + poEnabled && ! poComplete + ? 'account-status__info__gray' + : 'account-status__info__red'; icon = ; } else if ( showSuspendedNotice ) { const learnMoreHref = - 'https://woocommerce.com/document/payments/faq/deposits-suspended/'; + 'https://woocommerce.com/document/woopayments/deposits/why-deposits-suspended/'; description = createInterpolateElement( /* translators: - suspended accounts FAQ URL */ __( diff --git a/client/components/deposits-status/test/__snapshots__/index.js.snap b/client/components/deposits-status/test/__snapshots__/index.js.snap index f5e9bd4d0f9..b5853f51013 100644 --- a/client/components/deposits-status/test/__snapshots__/index.js.snap +++ b/client/components/deposits-status/test/__snapshots__/index.js.snap @@ -20,7 +20,7 @@ exports[`DepositsStatus renders blocked status 1`] = ` Temporarily suspended ( @@ -51,7 +51,7 @@ exports[`DepositsStatus renders blocked status 2`] = ` Temporarily suspended ( diff --git a/client/components/deposits-status/test/index.js b/client/components/deposits-status/test/index.js index 6fd03dbac97..959ccd07c9a 100644 --- a/client/components/deposits-status/test/index.js +++ b/client/components/deposits-status/test/index.js @@ -108,6 +108,8 @@ describe( 'DepositsStatus', () => { status, interval, accountStatus, + poEnabled = false, + poComplete = false, iconSize, } ) { return render( @@ -115,6 +117,8 @@ describe( 'DepositsStatus', () => { status={ status } accountStatus={ accountStatus } interval={ interval } + poEnabled={ poEnabled } + poComplete={ poComplete } iconSize={ iconSize } /> ); diff --git a/client/components/details-link/index.js b/client/components/details-link/index.js deleted file mode 100644 index 1571f5be74c..00000000000 --- a/client/components/details-link/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @format **/ - -/** - * External dependencies - */ -import InfoOutlineIcon from 'gridicons/dist/info-outline'; -import { Link } from '@woocommerce/components'; -import { getAdminUrl } from 'wcpay/utils'; - -export const getDetailsURL = ( id, parentSegment ) => - getAdminUrl( { - page: 'wc-admin', - path: `/payments/${ parentSegment }/details`, - id, - } ); - -const DetailsLink = ( { id, parentSegment } ) => - id ? ( - - - - ) : null; - -export default DetailsLink; diff --git a/client/components/details-link/index.tsx b/client/components/details-link/index.tsx new file mode 100644 index 00000000000..599247ce6c5 --- /dev/null +++ b/client/components/details-link/index.tsx @@ -0,0 +1,53 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import InfoOutlineIcon from 'gridicons/dist/info-outline'; +import { Link } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { getAdminUrl } from 'wcpay/utils'; + +/** + * The parent segment is the first part of the URL after the /payments/ path. + */ +type ParentSegment = 'deposits' | 'transactions' | 'disputes'; + +export const getDetailsURL = ( + /** + * The ID of the object to link to. + */ + id: string, + /** + * The parent segment is the first part of the URL after the /payments/ path. + */ + parentSegment: ParentSegment +): string => + getAdminUrl( { + page: 'wc-admin', + path: `/payments/${ parentSegment }/details`, + id, + } ); + +interface DetailsLinkProps { + /** + * The ID of the object to link to. + */ + id?: string; + /** + * The parent segment is the first part of the URL after the /payments/ path. + */ + parentSegment: ParentSegment; +} +const DetailsLink: React.FC< DetailsLinkProps > = ( { id, parentSegment } ) => + id ? ( + + + + ) : null; + +export default DetailsLink; diff --git a/client/components/details-link/test/__snapshots__/index.js.snap b/client/components/details-link/test/__snapshots__/index.test.tsx.snap similarity index 100% rename from client/components/details-link/test/__snapshots__/index.js.snap rename to client/components/details-link/test/__snapshots__/index.test.tsx.snap diff --git a/client/components/details-link/test/index.js b/client/components/details-link/test/index.test.tsx similarity index 96% rename from client/components/details-link/test/index.js rename to client/components/details-link/test/index.test.tsx index 6c39ea4d343..172eeac2454 100644 --- a/client/components/details-link/test/index.js +++ b/client/components/details-link/test/index.test.tsx @@ -3,6 +3,7 @@ /** * External dependencies */ +import React from 'react'; import { render } from '@testing-library/react'; /** diff --git a/client/components/dispute-status-chip/index.js b/client/components/dispute-status-chip/index.js deleted file mode 100644 index af7c661ab31..00000000000 --- a/client/components/dispute-status-chip/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** @format **/ - -/** - * Internal dependencies - */ -import Chip from '../chip'; -import displayStatus from './mappings'; -import { formatStringValue } from 'utils'; - -const DisputeStatusChip = ( { status } ) => { - const mapping = displayStatus[ status ] || {}; - const message = mapping.message || formatStringValue( status ); - const type = mapping.type || 'light'; - - return ; -}; - -export default DisputeStatusChip; diff --git a/client/components/dispute-status-chip/index.tsx b/client/components/dispute-status-chip/index.tsx new file mode 100644 index 00000000000..ccb9e11a553 --- /dev/null +++ b/client/components/dispute-status-chip/index.tsx @@ -0,0 +1,41 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; + +/** + * Internal dependencies + */ +import Chip from '../chip'; +import displayStatus from './mappings'; +import { formatStringValue } from 'utils'; +import { isAwaitingResponse, isDueWithin } from 'wcpay/disputes/utils'; +import type { + CachedDispute, + DisputeStatus, + EvidenceDetails, +} from 'wcpay/types/disputes'; + +interface Props { + status: DisputeStatus | string; + dueBy?: CachedDispute[ 'due_by' ] | EvidenceDetails[ 'due_by' ]; +} +const DisputeStatusChip: React.FC< Props > = ( { status, dueBy } ) => { + const mapping = displayStatus[ status ] || {}; + const message = mapping.message || formatStringValue( status ); + + const needsResponse = isAwaitingResponse( status ); + const isUrgent = + needsResponse && dueBy && isDueWithin( { dueBy, days: 3 } ); + + let type = mapping.type || 'light'; + if ( isUrgent ) { + type = 'alert'; + } + + return ; +}; + +export default DisputeStatusChip; diff --git a/client/components/dispute-status-chip/mappings.ts b/client/components/dispute-status-chip/mappings.ts index 2a7a3fce33b..043656d7fc8 100644 --- a/client/components/dispute-status-chip/mappings.ts +++ b/client/components/dispute-status-chip/mappings.ts @@ -4,19 +4,20 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import type { ChipType } from '../chip'; const status: { [ key: string ]: { - type: string; + type: ChipType; message: string; }; } = { warning_needs_response: { - type: 'primary', + type: 'warning', message: __( 'Inquiry: Needs response', 'woocommerce-payments' ), }, warning_under_review: { - type: 'light', + type: 'primary', message: __( 'Inquiry: Under review', 'woocommerce-payments' ), }, warning_closed: { @@ -24,11 +25,11 @@ const status: { message: __( 'Inquiry: Closed', 'woocommerce-payments' ), }, needs_response: { - type: 'primary', + type: 'warning', message: __( 'Needs response', 'woocommerce-payments' ), }, under_review: { - type: 'light', + type: 'primary', message: __( 'Under review', 'woocommerce-payments' ), }, charge_refunded: { @@ -36,7 +37,7 @@ const status: { message: __( 'Charge refunded', 'woocommerce-payments' ), }, won: { - type: 'light', + type: 'success', message: __( 'Won', 'woocommerce-payments' ), }, lost: { diff --git a/client/components/dispute-status-chip/test/__snapshots__/index.js.snap b/client/components/dispute-status-chip/test/__snapshots__/index.test.tsx.snap similarity index 76% rename from client/components/dispute-status-chip/test/__snapshots__/index.js.snap rename to client/components/dispute-status-chip/test/__snapshots__/index.test.tsx.snap index 0f54e7de18f..63540374f44 100644 --- a/client/components/dispute-status-chip/test/__snapshots__/index.js.snap +++ b/client/components/dispute-status-chip/test/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`DisputeStatusChip renders charge_refunded status 1`] = `
Charge refunded @@ -13,7 +13,7 @@ exports[`DisputeStatusChip renders charge_refunded status 1`] = ` exports[`DisputeStatusChip renders default appearance if status is unrecognized 1`] = `
Mock status @@ -23,7 +23,7 @@ exports[`DisputeStatusChip renders default appearance if status is unrecognized exports[`DisputeStatusChip renders lost status 1`] = `
Lost @@ -33,7 +33,7 @@ exports[`DisputeStatusChip renders lost status 1`] = ` exports[`DisputeStatusChip renders needs_response status 1`] = `
Needs response @@ -43,7 +43,7 @@ exports[`DisputeStatusChip renders needs_response status 1`] = ` exports[`DisputeStatusChip renders under_review status 1`] = `
Under review @@ -53,7 +53,7 @@ exports[`DisputeStatusChip renders under_review status 1`] = ` exports[`DisputeStatusChip renders warning_closed status 1`] = `
Inquiry: Closed @@ -63,7 +63,7 @@ exports[`DisputeStatusChip renders warning_closed status 1`] = ` exports[`DisputeStatusChip renders warning_needs_response status 1`] = `
Inquiry: Needs response @@ -73,7 +73,7 @@ exports[`DisputeStatusChip renders warning_needs_response status 1`] = ` exports[`DisputeStatusChip renders warning_under_review status 1`] = `
Inquiry: Under review @@ -83,7 +83,7 @@ exports[`DisputeStatusChip renders warning_under_review status 1`] = ` exports[`DisputeStatusChip renders won status 1`] = `
Won diff --git a/client/components/dispute-status-chip/test/index.js b/client/components/dispute-status-chip/test/index.test.tsx similarity index 76% rename from client/components/dispute-status-chip/test/index.js rename to client/components/dispute-status-chip/test/index.test.tsx index 613882e5022..9087f5d35c4 100644 --- a/client/components/dispute-status-chip/test/index.js +++ b/client/components/dispute-status-chip/test/index.test.tsx @@ -3,12 +3,20 @@ * External dependencies */ import { render } from '@testing-library/react'; +import React from 'react'; /** * Internal dependencies */ import DisputeStatusChip from '../'; +function renderDisputeStatus( status: string, dueBy = undefined ) { + const { container } = render( + + ); + return container; +} + describe( 'DisputeStatusChip', () => { test( 'renders default appearance if status is unrecognized', () => { expect( renderDisputeStatus( 'mock_status' ) ).toMatchSnapshot(); @@ -28,9 +36,4 @@ describe( 'DisputeStatusChip', () => { test.each( statuses )( 'renders %s status', ( status ) => { expect( renderDisputeStatus( status ) ).toMatchSnapshot(); } ); - - function renderDisputeStatus( status ) { - const { container } = render( ); - return container; - } } ); diff --git a/client/components/download-button/index.js b/client/components/download-button/index.tsx similarity index 74% rename from client/components/download-button/index.js rename to client/components/download-button/index.tsx index e3c0488a63f..2bb80c3c1fa 100644 --- a/client/components/download-button/index.js +++ b/client/components/download-button/index.tsx @@ -13,7 +13,15 @@ import CloudDownloadIcon from 'gridicons/dist/cloud-download'; */ import './style.scss'; -const DownloadButton = ( { isDisabled, onClick } ) => ( +interface DownloadButtonProps { + isDisabled: boolean; + onClick: ( event: any ) => void; +} + +const DownloadButton: React.FunctionComponent< DownloadButtonProps > = ( { + isDisabled, + onClick, +} ) => ( - { 3 > remindMeCount ? ( - - ) : ( - - ) } +
); }; diff --git a/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap index dfedf34c7dc..7f2fd80d510 100644 --- a/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap +++ b/client/components/fraud-risk-tools-banner/components/banner-actions/test/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BannerActions renders with dismiss button when remindMeCount greater than or equal to 3 1`] = ` +exports[`BannerActions renders 1`] = `
`; - -exports[`BannerActions renders without dismiss button when remindMeCount less than 3 1`] = ` -
-
- - Learn more - - -
-
-`; diff --git a/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx b/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx index 3dd75e90fd1..7fa3df47b61 100644 --- a/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx +++ b/client/components/fraud-risk-tools-banner/components/banner-actions/test/index.test.tsx @@ -9,27 +9,12 @@ import { render } from '@testing-library/react'; */ import BannerActions from '..'; -const mockHandleRemindOnClick = jest.fn(); const mockHandleDontShowAgainOnClick = jest.fn(); describe( 'BannerActions', () => { - it( 'renders without dismiss button when remindMeCount less than 3', () => { + it( 'renders', () => { const { container: bannerActionsComponent } = render( - ); - - expect( bannerActionsComponent ).toMatchSnapshot(); - } ); - - it( 'renders with dismiss button when remindMeCount greater than or equal to 3', () => { - const { container: bannerActionsComponent } = render( - ); diff --git a/client/components/fraud-risk-tools-banner/index.tsx b/client/components/fraud-risk-tools-banner/index.tsx index 231d6fa6c41..09def8bd77c 100644 --- a/client/components/fraud-risk-tools-banner/index.tsx +++ b/client/components/fraud-risk-tools-banner/index.tsx @@ -11,13 +11,10 @@ import { useDispatch } from '@wordpress/data'; */ import { BannerBody, NewPill, BannerActions } from './components'; import './style.scss'; -import { TIME } from '../../constants'; import wcpayTracks from 'tracks'; interface BannerSettings { - remindMeAt: number; dontShowAgain: boolean; - remindMeCount: number; } const FRTDiscoverabilityBanner: React.FC = () => { @@ -27,32 +24,14 @@ const FRTDiscoverabilityBanner: React.FC = () => { try { return JSON.parse( frtDiscoverBannerSettings ); } catch ( e ) { - return { remindMeCount: 0, remindMeAt: null, dontShowAgain: false }; + return { dontShowAgain: false }; } } ); - const showBanner = - ! settings.dontShowAgain && - ( null === settings.remindMeAt || Date.now() > settings.remindMeAt ); - - const setReminder = () => { - const nowTimestamp = Date.now(); - setSettings( ( prevSettings ) => { - return { - ...prevSettings, - remindMeCount: prevSettings.remindMeCount + 1, - remindMeAt: nowTimestamp + 3 * TIME.DAY_IN_MS, - }; - } ); - }; + const showBanner = ! settings.dontShowAgain; const setDontShowAgain = () => { - setSettings( ( prevSettings ) => { - return { - ...prevSettings, - dontShowAgain: true, - }; - } ); + setSettings( { dontShowAgain: true } ); }; useEffect( () => { @@ -67,14 +46,6 @@ const FRTDiscoverabilityBanner: React.FC = () => { wcpaySettings.frtDiscoverBannerSettings = stringifiedSettings; }, [ frtDiscoverBannerSettings, settings, updateOptions ] ); - const handleRemindOnClick = () => { - wcpayTracks.recordEvent( - 'wcpay_fraud_protection_banner_remind_later_button_clicked', - {} - ); - setReminder(); - }; - const handleDontShowAgainOnClick = () => { setDontShowAgain(); }; @@ -95,8 +66,6 @@ const FRTDiscoverabilityBanner: React.FC = () => {
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 aa707519703..fc16ede2d75 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 @@ -2,133 +2,7 @@ exports[`FRTDiscoverabilityBanner does not render when dontShowAgain is true 1`] = `
`; -exports[`FRTDiscoverabilityBanner does not render when remindMeAt timestamp is in the future 1`] = `
`; - exports[`FRTDiscoverabilityBanner renders 1`] = ` -
-
- - -`; - -exports[`FRTDiscoverabilityBanner renders when remindMeAt timestamp is in the past 1`] = ` -
-
-
-
- - New - -

- Enhanced fraud protection for your store -

-

- 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 - - -
-
-
- -`; - -exports[`FRTDiscoverabilityBanner renders with dismiss button if remindMeCount greater than or equal to 3 1`] = `
`; - -exports[`FRTDiscoverabilityBanner renders without dismiss button if remindMeCount greater than 0 but less than 3 1`] = ` -
-
-
-
- - New - -

- Enhanced fraud protection for your store -

-

- 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 b369ab5ec38..2c808a0201e 100644 --- a/client/components/fraud-risk-tools-banner/test/index.test.tsx +++ b/client/components/fraud-risk-tools-banner/test/index.test.tsx @@ -41,81 +41,9 @@ describe( 'FRTDiscoverabilityBanner', () => { expect( frtBanner ).toMatchSnapshot(); } ); - it( 'renders with dismiss button if remindMeCount greater than or equal to 3', () => { - global.wcpaySettings = { - frtDiscoverBannerSettings: JSON.stringify( { - remindMeCount: 3, - remindMeAt: null, - dontShowAgain: false, - } ), - }; - - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); - - it( 'renders without dismiss button if remindMeCount greater than 0 but less than 3', () => { - global.wcpaySettings = { - frtDiscoverBannerSettings: JSON.stringify( { - remindMeCount: 2, - remindMeAt: null, - dontShowAgain: false, - } ), - }; - - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); - - it( 'renders when remindMeAt timestamp is in the past', () => { - Date.now = jest.fn( () => - new Date( '2023-03-14T12:33:37.000Z' ).getTime() - ); - - const remindMeAt = new Date( '2023-03-11T11:33:37.000Z' ).getTime(); - - global.wcpaySettings = { - frtDiscoverBannerSettings: JSON.stringify( { - remindMeCount: 1, - remindMeAt: remindMeAt, - dontShowAgain: false, - } ), - }; - - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); - - it( 'does not render when remindMeAt timestamp is in the future', () => { - Date.now = jest.fn( () => - new Date( '2023-03-14T12:33:37.000Z' ).getTime() - ); - - const remindMeAt = new Date( '2023-03-15T12:33:37.000Z' ).getTime(); - - global.wcpaySettings = { - frtDiscoverBannerSettings: JSON.stringify( { - remindMeCount: 1, - remindMeAt: remindMeAt, - dontShowAgain: false, - } ), - }; - - const { container: frtBanner } = render( ); - - expect( frtBanner ).toMatchSnapshot(); - } ); - it( 'does not render when dontShowAgain is true', () => { - const remindMeAt = new Date( '2023-03-14T12:33:37.000Z' ).getTime(); - global.wcpaySettings = { frtDiscoverBannerSettings: JSON.stringify( { - remindMeCount: 3, - remindMeAt: remindMeAt, dontShowAgain: true, } ), }; diff --git a/client/components/horizontal-list/index.js b/client/components/horizontal-list/index.js deleted file mode 100755 index b46156ed932..00000000000 --- a/client/components/horizontal-list/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** @format **/ - -/** - * External dependencies - */ -import { List } from '@woocommerce/components'; - -/** - * Internal dependencies. - */ -import './style.scss'; - -const HorizontalList = ( props ) => { - const { items } = props; - return ; -}; - -export default HorizontalList; diff --git a/client/components/horizontal-list/index.tsx b/client/components/horizontal-list/index.tsx new file mode 100755 index 00000000000..6db7d19be54 --- /dev/null +++ b/client/components/horizontal-list/index.tsx @@ -0,0 +1,35 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import { List } from '@woocommerce/components'; + +/** + * Internal dependencies. + */ +import './style.scss'; + +export interface HorizontalListItem { + /** + * The title for the item, displayed above the content. + */ + title: string; + /** + * The content that will be displayed in the list for this item. + */ + content: string | React.ReactNode; +} + +interface Props { + /** + * The items to display in the list. + */ + items: HorizontalListItem[]; +} + +export const HorizontalList: React.FunctionComponent< Props > = ( props ) => { + const { items } = props; + return ; +}; diff --git a/client/components/horizontal-list/style.scss b/client/components/horizontal-list/style.scss index ba52172618f..f133cae1da1 100755 --- a/client/components/horizontal-list/style.scss +++ b/client/components/horizontal-list/style.scss @@ -45,10 +45,14 @@ @include font-size( 14 ); } .woocommerce-list__item-title { - color: $studio-gray-60; + text-transform: uppercase; + color: $gray-700; + font-size: 11px; + font-weight: 600; } .woocommerce-list__item-content { color: $studio-gray-80; + display: flex; } } .woocommerce-list__item:first-child { diff --git a/client/components/horizontal-list/test/__snapshots__/index.js.snap b/client/components/horizontal-list/test/__snapshots__/index.tsx.snap similarity index 100% rename from client/components/horizontal-list/test/__snapshots__/index.js.snap rename to client/components/horizontal-list/test/__snapshots__/index.tsx.snap diff --git a/client/components/horizontal-list/test/index.js b/client/components/horizontal-list/test/index.tsx similarity index 84% rename from client/components/horizontal-list/test/index.js rename to client/components/horizontal-list/test/index.tsx index 278446108f4..f589a9372f7 100755 --- a/client/components/horizontal-list/test/index.js +++ b/client/components/horizontal-list/test/index.tsx @@ -3,14 +3,18 @@ * External dependencies */ import { render } from '@testing-library/react'; +import React from 'react'; /** * Internal dependencies */ -import HorizontalList from '../'; +import { HorizontalListItem, HorizontalList } from '..'; describe( 'HorizontalList', () => { - let horizontalList; + function renderHorizontalList( items: HorizontalListItem[] ) { + return render( ); + } + let horizontalList: HTMLElement; beforeEach( () => { const items = [ { title: 'Item 1', content: 'Item 1 content' }, @@ -28,8 +32,4 @@ describe( 'HorizontalList', () => { 'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.' ); } ); - - function renderHorizontalList( items ) { - return render( ); - } } ); diff --git a/client/components/icons/warning.tsx b/client/components/icons/warning.tsx new file mode 100644 index 00000000000..3f400fa5ad9 --- /dev/null +++ b/client/components/icons/warning.tsx @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import React from 'react'; +import { SVG, Path } from '@wordpress/primitives'; + +export default ( + + + + + +); diff --git a/client/components/inline-notice/index.js b/client/components/inline-notice/index.js deleted file mode 100644 index 1f501a677e3..00000000000 --- a/client/components/inline-notice/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { Notice } from '@wordpress/components'; -import classNames from 'classnames'; - -/** - * Internal dependencies - */ -import './style.scss'; - -const InlineNotice = ( { className, ...restProps } ) => ( - -); - -export default InlineNotice; diff --git a/client/components/inline-notice/index.tsx b/client/components/inline-notice/index.tsx new file mode 100644 index 00000000000..d0c59812e96 --- /dev/null +++ b/client/components/inline-notice/index.tsx @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +import * as React from 'react'; +import { Flex, FlexItem, Icon, Notice, Button } from '@wordpress/components'; +import classNames from 'classnames'; +import CheckmarkIcon from 'gridicons/dist/checkmark'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import InfoOutlineIcon from 'gridicons/dist/info-outline'; + +/** + * Internal dependencies. + */ +import './styles.scss'; + +interface InlineNoticeProps extends Notice.Props { + /** + * Whether to display the default icon based on status prop or the icon to display. + * Supported values are: boolean, JSX.Element and `undefined`. + * + * @default undefined + */ + icon?: boolean | JSX.Element; +} + +/** + * Renders a banner notice. + */ +function InlineNotice( props: InlineNoticeProps ): JSX.Element { + const { icon, actions, children, ...noticeProps } = props; + + // Add the default class name to the notice. + noticeProps.className = classNames( + 'wcpay-inline-notice', + `wcpay-inline-${ noticeProps.status }-notice`, + noticeProps.className + ); + + // Use default icon based on status if icon === true. + let iconToDisplay = icon; + if ( iconToDisplay === true ) { + switch ( noticeProps.status ) { + case 'success': + iconToDisplay = ; + break; + case 'error': + case 'warning': + iconToDisplay = ; + break; + case 'info': + default: + iconToDisplay = ; + break; + } + } + + // Convert the notice actions to buttons or link elements. + const actionClass = 'wcpay-inline-notice__action'; + const mappedActions = actions?.map( ( action, index ) => { + // Actions that contain a URL will be rendered as a link. + // This matches WP Notice component behavior. + if ( 'url' in action ) { + return ( + + { action.label } + + ); + } + + return ( + + ); + } ); + + return ( + + + { iconToDisplay && ( + + + + ) } + + { children } + { mappedActions && ( + + { mappedActions } + + ) } + + + + ); +} + +export default InlineNotice; diff --git a/client/components/inline-notice/style.scss b/client/components/inline-notice/style.scss deleted file mode 100644 index 5f2f855d2cc..00000000000 --- a/client/components/inline-notice/style.scss +++ /dev/null @@ -1,11 +0,0 @@ -.wcpay-inline-notice { - // increasing the specificity of the styles to override the Gutenberg ones - &#{&} { - margin: 0; - margin-bottom: $grid-unit-30; - } - - &.is-info { - background: #def1f7; - } -} diff --git a/client/components/inline-notice/styles.scss b/client/components/inline-notice/styles.scss new file mode 100644 index 00000000000..6c4059d7273 --- /dev/null +++ b/client/components/inline-notice/styles.scss @@ -0,0 +1,103 @@ +.wcpay-inline-notice.components-notice { + margin: $gap-large 0; + padding: 11px 0 11px 17px; + border-left: none; + border-radius: 2px; + justify-content: flex-start; + + /* Margin exceptions */ + @at-root .components-modal__header + &, + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + /* Shared styles for all variants */ + .wcpay-inline-notice__icon { + display: flex; + align-items: center; + align-self: flex-start; + min-height: auto; + min-width: auto; + margin-right: 5px; + svg { + width: 22px; + height: 22px; + } + } + .components-notice__content { + margin-top: 2px; + margin-bottom: 2px; + } + .wcpay-inline-notice__content__actions { + padding-top: 12px; + } + a.wcpay-inline-notice__action { + text-decoration: none; + } + &.is-dismissible { + padding-right: 12px; + } + .components-notice__dismiss { + height: 24px; + width: 24px; + svg { + width: 15px; + height: 15px; + fill: $gray-900; + } + } + + /* Specific styles for each variant */ + &.is-info { + background-color: $wp-blue-0; + .wcpay-inline-notice__icon svg { + fill: $wp-blue-70; + } + button.wcpay-inline-notice__action { + box-shadow: inset 0 0 0 1px $wp-blue-70; + } + .wcpay-inline-notice__action { + color: $wp-blue-70; + } + } + &.is-warning { + background-color: #fcf9e8; + .wcpay-inline-notice__icon svg { + fill: $wp-yellow-70; + } + button.wcpay-inline-notice__action { + box-shadow: inset 0 0 0 1px $wp-yellow-70; + } + .wcpay-inline-notice__action { + color: $wp-yellow-70; + } + } + &.is-error { + background-color: $wp-red-0; + .wcpay-inline-notice__icon svg { + fill: $wp-red-70; + } + button.wcpay-inline-notice__action { + box-shadow: inset 0 0 0 1px $wp-red-70; + } + .wcpay-inline-notice__action { + color: $wp-red-70; + } + } + &.is-success { + background-color: #edfaef; + .wcpay-inline-notice__icon svg { + fill: $wp-green-70; + } + button.wcpay-inline-notice__action { + box-shadow: inset 0 0 0 1px $wp-green-70; + } + .wcpay-inline-notice__action { + color: $wp-green-70; + } + } +} diff --git a/client/components/inline-notice/tests/__snapshots__/index.test.tsx.snap b/client/components/inline-notice/tests/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..ed2038c16b5 --- /dev/null +++ b/client/components/inline-notice/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,293 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Info InlineNotices renders with dismiss 1`] = ` +
+
+
+
+
+ Test notice content +
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders with dismiss and icon 1`] = ` +
+
+
+
+
+ + + + + +
+
+ Test notice content +
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders with dismiss and icon and actions 1`] = ` +
+
+
+
+
+ + + + + +
+
+ Test notice content +
+ + + URL + +
+
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders with no status and custom icon 1`] = ` +
+
+
+
+
+ + + + + +
+
+ Test notice content +
+
+
+
+ +
+
+`; + +exports[`Info InlineNotices renders without dismiss and icon 1`] = ` +
+
+
+
+
+ Test notice content +
+
+
+
+
+
+`; diff --git a/client/components/inline-notice/tests/index.test.tsx b/client/components/inline-notice/tests/index.test.tsx new file mode 100644 index 00000000000..23c26860cc8 --- /dev/null +++ b/client/components/inline-notice/tests/index.test.tsx @@ -0,0 +1,158 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import AddIcon from 'gridicons/dist/add'; + +/** + * Internal dependencies + */ +import InlineNotice from '..'; + +describe( 'Info InlineNotices renders', () => { + test( 'with dismiss', () => { + const { container } = render( + + ); + expect( container ).toMatchSnapshot(); + } ); + + test( 'with dismiss and icon', () => { + const { container } = render( + + ); + expect( container ).toMatchSnapshot(); + } ); + + test( 'with dismiss and icon and actions', () => { + const { container } = render( + + ); + + expect( container ).toMatchSnapshot(); + } ); + + test( 'without dismiss and icon', () => { + const { container } = render( + + ); + expect( container ).toMatchSnapshot(); + } ); + + test( 'with no status and custom icon', () => { + const { container } = render( + } + children={ 'Test notice content' } + /> + ); + expect( container ).toMatchSnapshot(); + } ); +} ); + +describe( 'Action click triggers callback', () => { + test( 'with dismiss and icon and actions', () => { + const onClickMock = jest.fn(); + const { getByText } = render( + + ); + + fireEvent.click( getByText( 'Button' ) ); + expect( onClickMock ).toHaveBeenCalled(); + } ); + + test( 'With icon and multiple button actions', () => { + const onButtonClickOne = jest.fn(); + const onButtonClickTwo = jest.fn(); + const { getByText } = render( + + ); + + expect( onButtonClickOne ).not.toHaveBeenCalled(); + expect( onButtonClickTwo ).not.toHaveBeenCalled(); + + // Click Button 1 + fireEvent.click( getByText( 'Button one' ) ); + expect( onButtonClickOne ).toHaveBeenCalled(); + expect( onButtonClickTwo ).not.toHaveBeenCalled(); + + // Click Button 1 + fireEvent.click( getByText( 'Button two' ) ); + expect( onButtonClickTwo ).toHaveBeenCalled(); + } ); +} ); + +describe( 'Dismiss click triggers callback', () => { + test( 'with dismiss and icon and actions', () => { + const onDismissMock = jest.fn(); + const { getByLabelText } = render( + + ); + + fireEvent.click( getByLabelText( 'Dismiss this notice' ) ); + expect( onDismissMock ).toHaveBeenCalled(); + } ); +} ); diff --git a/client/components/load-bar/style.scss b/client/components/load-bar/style.scss index 1329a0cce65..f3f7fb86d7f 100644 --- a/client/components/load-bar/style.scss +++ b/client/components/load-bar/style.scss @@ -8,7 +8,7 @@ display: block; height: 100%; width: 100%; - background-color: $blue-50; + background-color: var( --wp-admin-theme-color ); animation: wcpay-component-load-bar 3s ease-in-out infinite; transform-origin: 0 0; } diff --git a/client/components/loadable-checkbox/index.js b/client/components/loadable-checkbox/index.js index 2a17d908451..31ac7c60d70 100644 --- a/client/components/loadable-checkbox/index.js +++ b/client/components/loadable-checkbox/index.js @@ -7,7 +7,6 @@ import { __, sprintf } from '@wordpress/i18n'; import React, { useEffect, useState } from 'react'; import { CheckboxControl, VisuallyHidden } from '@wordpress/components'; import classNames from 'classnames'; -import { Icon, warning } from '@wordpress/icons'; /** * Internal dependencies @@ -15,6 +14,7 @@ import { Icon, warning } from '@wordpress/icons'; import { useManualCapture } from 'wcpay/data'; import { HoverTooltip } from 'components/tooltip'; import './style.scss'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; const LoadableCheckboxControl = ( { label, @@ -23,6 +23,8 @@ const LoadableCheckboxControl = ( { onChange, hideLabel = false, isAllowingManualCapture = false, + isSetupRequired = false, + setupTooltip = '', delayMsOnCheck = 0, delayMsOnUncheck = 0, } ) => { @@ -32,7 +34,7 @@ const LoadableCheckboxControl = ( { const handleOnChange = ( status ) => { const timeout = status ? delayMsOnCheck : delayMsOnUncheck; - if ( 0 < timeout ) { + if ( timeout > 0 ) { setLoading( true ); setTimeout( () => { onChange( status ); @@ -84,36 +86,53 @@ const LoadableCheckboxControl = ( {
) } - { isManualCaptureEnabled && ! isAllowingManualCapture ? ( - -
- -
- - { sprintf( - /* translators: %s: a payment method name. */ - __( - '%s cannot be enabled at checkout. Click to expand.', - 'woocommerce-payments' - ), - label - ) } - + +
+ +
+ + { sprintf( + /* translators: %s: a payment method name. */ + __( + '%s cannot be enabled at checkout. Click to expand.', + 'woocommerce-payments' + ), + label + ) } + +
-
- + +
) : ( { { paymentMethod.iban_last4 } ); + case 'affirm': + case 'afterpay_clearpay': default: return ; } @@ -50,7 +54,7 @@ const PaymentMethodDetails = ( props ) => { const { payment } = props; const paymentMethod = payment ? payment[ payment.type ] : null; - if ( ! paymentMethod && ( ! payment || 'link' !== payment.type ) ) { + if ( ! paymentMethod && ( ! payment || payment.type !== 'link' ) ) { return ; } @@ -61,9 +65,16 @@ const PaymentMethodDetails = ( props ) => { const details = formatDetails( payment ); return ( - + + + { details } ); diff --git a/client/components/payment-method-details/style.scss b/client/components/payment-method-details/style.scss index f2a97bf9071..a7d51d50d02 100644 --- a/client/components/payment-method-details/style.scss +++ b/client/components/payment-method-details/style.scss @@ -11,3 +11,9 @@ margin-top: 1px; } } + +@media screen and ( max-width: 782px ) { + .payment-method-details__brand-tooltip { + display: none; + } +} diff --git a/client/components/payment-method-details/test/__snapshots__/index.js.snap b/client/components/payment-method-details/test/__snapshots__/index.js.snap index 0e0d4647d57..72a69b2596d 100755 --- a/client/components/payment-method-details/test/__snapshots__/index.js.snap +++ b/client/components/payment-method-details/test/__snapshots__/index.js.snap @@ -13,9 +13,19 @@ exports[`PaymentMethodDetails renders a valid card brand and last 4 digits 1`] = - +  ••••  4242 diff --git a/client/components/payment-method-disabled-tooltip/index.tsx b/client/components/payment-method-disabled-tooltip/index.tsx new file mode 100644 index 00000000000..8df2e14f594 --- /dev/null +++ b/client/components/payment-method-disabled-tooltip/index.tsx @@ -0,0 +1,77 @@ +/** @format */ +/** + * External dependencies + */ +import interpolateComponents from '@automattic/interpolate-components'; +import { __ } from '@wordpress/i18n'; +import React from 'react'; + +/** + * Internal dependencies + */ +import { HoverTooltip } from 'components/tooltip'; +import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; + +export const DocumentationUrlForDisabledPaymentMethod = { + DEFAULT: + 'https://woocommerce.com/document/woopayments/payment-methods/additional-payment-methods/#method-cant-be-enabled', + BNPLS: + 'https://woocommerce.com/document/woopayments/payment-methods/buy-now-pay-later/#contact-support', +}; + +export const getDocumentationUrlForDisabledPaymentMethod = ( + paymentMethodId: string +): string => { + let url; + switch ( paymentMethodId ) { + case PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY: + case PAYMENT_METHOD_IDS.AFFIRM: + url = DocumentationUrlForDisabledPaymentMethod.BNPLS; + break; + default: + url = DocumentationUrlForDisabledPaymentMethod.DEFAULT; + } + return url; +}; + +const PaymentMethodDisabledTooltip = ( { + id, + children, +}: { + id: string; + children: React.ReactNode; +} ): React.ReactElement => { + return ( + + ), + }, + } ) } + > + { children } + + ); +}; + +export default PaymentMethodDisabledTooltip; diff --git a/client/components/payment-method-disabled-tooltip/test/index.test.tsx b/client/components/payment-method-disabled-tooltip/test/index.test.tsx new file mode 100644 index 00000000000..773c9f62cf0 --- /dev/null +++ b/client/components/payment-method-disabled-tooltip/test/index.test.tsx @@ -0,0 +1,58 @@ +/** @format */ +/** + * External dependencies + */ +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +/** + * Internal dependencies + */ +import PAYMENT_METHOD_IDS from 'wcpay/payment-methods/constants'; +import PaymentMethodDisabledTooltip, { + DocumentationUrlForDisabledPaymentMethod, + getDocumentationUrlForDisabledPaymentMethod, +} from '../index'; + +describe( 'PaymentMethodDisabledTooltip', () => { + test.each( [ + [ + PAYMENT_METHOD_IDS.AFTERPAY_CLEARPAY, + DocumentationUrlForDisabledPaymentMethod.BNPLS, + ], + [ 'default-method', DocumentationUrlForDisabledPaymentMethod.DEFAULT ], + ] )( + 'renders tooltip with correct learn more link for %s', + ( tooltipId, expectedUrl ) => { + render( + + Test children + + ); + + const element = screen.getByText( 'Test children' ); + expect( element ).toBeInTheDocument(); + + fireEvent.mouseOver( element ); + + const tooltip = screen.getByRole( 'tooltip' ); + expect( tooltip ).toBeInTheDocument(); + expect( tooltip ).toHaveTextContent( + 'We need more information from you to enable this method. Learn more.' + ); + + const learnMoreLink = screen.getByRole( 'link', { + name: 'Learn more.', + } ); + expect( learnMoreLink ).toHaveAttribute( + 'href', + getDocumentationUrlForDisabledPaymentMethod( tooltipId ) + ); + expect( learnMoreLink.getAttribute( 'href' ) ).toEqual( + expectedUrl + ); + expect( learnMoreLink ).toHaveAttribute( 'target', '_blank' ); + expect( learnMoreLink ).toHaveAttribute( 'rel', 'noreferrer' ); + } + ); +} ); diff --git a/client/components/payment-methods-checkboxes/index.js b/client/components/payment-methods-checkboxes/index.js deleted file mode 100644 index 1918df7d7a8..00000000000 --- a/client/components/payment-methods-checkboxes/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @format */ -/** - * External dependencies - */ -import React from 'react'; - -const PaymentMethodsSelector = ( { children } ) => { - return
    { children }
; -}; - -export default PaymentMethodsSelector; diff --git a/client/components/payment-methods-checkboxes/index.tsx b/client/components/payment-methods-checkboxes/index.tsx new file mode 100644 index 00000000000..439f1a2ae94 --- /dev/null +++ b/client/components/payment-methods-checkboxes/index.tsx @@ -0,0 +1,15 @@ +/** @format */ +/** + * External dependencies + */ +import React, { ReactNode } from 'react'; + +const PaymentMethodsSelector = ( { + children, +}: { + children: ReactNode; +} ): React.ReactElement => { + return
    { children }
; +}; + +export default PaymentMethodsSelector; diff --git a/client/components/payment-methods-checkboxes/payment-method-checkbox.scss b/client/components/payment-methods-checkboxes/payment-method-checkbox.scss index 06799e7a25e..6c6d35b9d62 100644 --- a/client/components/payment-methods-checkboxes/payment-method-checkbox.scss +++ b/client/components/payment-methods-checkboxes/payment-method-checkbox.scss @@ -122,8 +122,8 @@ } &.payment-status-inactive { border: 0 solid transparent; - background: #cc1818; - color: #fff; + background: $studio-yellow-5; + color: $studio-yellow-50; } } } diff --git a/client/components/payment-methods-checkboxes/payment-method-checkbox.js b/client/components/payment-methods-checkboxes/payment-method-checkbox.tsx similarity index 80% rename from client/components/payment-methods-checkboxes/payment-method-checkbox.js rename to client/components/payment-methods-checkboxes/payment-method-checkbox.tsx index c1636953e2e..60c8b9b59e9 100644 --- a/client/components/payment-methods-checkboxes/payment-method-checkbox.js +++ b/client/components/payment-methods-checkboxes/payment-method-checkbox.tsx @@ -2,30 +2,44 @@ /** * External dependencies */ -import React, { useContext, useEffect } from 'react'; import { Icon, VisuallyHidden } from '@wordpress/components'; import { useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import classNames from 'classnames'; +import React, { useContext, useEffect } from 'react'; /** * Internal dependencies */ +import LoadableCheckboxControl from 'components/loadable-checkbox'; +import { HoverTooltip } from 'components/tooltip'; +import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants'; +import { useManualCapture, useAccountDomesticCurrency } from 'wcpay/data'; +import { FeeStructure } from 'wcpay/types/fees'; +import PaymentMethodsMap from '../../payment-methods-map'; import WCPaySettingsContext from '../../settings/wcpay-settings-context'; import { formatMethodFeesDescription, formatMethodFeesTooltip, } from '../../utils/account-fees'; -import LoadableCheckboxControl from 'components/loadable-checkbox'; -import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants'; -import PaymentMethodsMap from '../../payment-methods-map'; +import PaymentMethodDisabledTooltip from '../payment-method-disabled-tooltip'; import Pill from '../pill'; -import { HoverTooltip } from 'components/tooltip'; +import { getPaymentMethodDescription } from 'wcpay/utils/payment-methods'; import './payment-method-checkbox.scss'; -import { useManualCapture } from 'wcpay/data'; -const PaymentMethodDescription = ( { name } ) => { - const description = PaymentMethodsMap[ name ]?.description; +type PaymentMethodProps = { + name: string; +}; + +const PaymentMethodDescription: React.FC< PaymentMethodProps > = ( { + name, +} ) => { + const [ stripeAccountDomesticCurrency ] = useAccountDomesticCurrency(); + const description = getPaymentMethodDescription( + name, + stripeAccountDomesticCurrency as string + ); + if ( ! description ) return null; return ( @@ -43,7 +57,17 @@ const PaymentMethodDescription = ( { name } ) => { ); }; -const PaymentMethodCheckbox = ( { +type PaymentMethodCheckboxProps = { + onChange: ( name: string, enabled: boolean ) => void; + name: string; + checked: boolean; + fees: string; + status: string; + required: boolean; + locked: boolean; +}; + +const PaymentMethodCheckbox: React.FC< PaymentMethodCheckboxProps > = ( { onChange, name, checked, @@ -52,7 +76,11 @@ const PaymentMethodCheckbox = ( { required, locked, } ) => { - const { accountFees } = useContext( WCPaySettingsContext ); + const { + accountFees, + }: { accountFees: Record< string, FeeStructure > } = useContext( + WCPaySettingsContext + ); const handleChange = useCallback( ( enabled ) => { @@ -90,7 +118,7 @@ const PaymentMethodCheckbox = ( { label={ paymentMethod.label } checked={ checked } disabled={ disabled || locked } - onChange={ ( state ) => { + onChange={ ( state: boolean ) => { handleChange( state ); } } delayMsOnCheck={ 1500 } @@ -99,7 +127,7 @@ const PaymentMethodCheckbox = ( { isAllowingManualCapture={ paymentMethod.allows_manual_capture } />
- { paymentMethod.icon() } + { paymentMethod.icon( {} ) }
@@ -153,22 +181,14 @@ const PaymentMethodCheckbox = ( { ) } { disabled && ( - + { __( - 'Contact WooCommerce Support', + 'More information needed', 'woocommerce-payments' ) } - + ) }
diff --git a/client/components/payment-methods-checkboxes/test/index.js b/client/components/payment-methods-checkboxes/test/index.test.tsx similarity index 84% rename from client/components/payment-methods-checkboxes/test/index.js rename to client/components/payment-methods-checkboxes/test/index.test.tsx index 8740ac3829a..59b231190e0 100644 --- a/client/components/payment-methods-checkboxes/test/index.js +++ b/client/components/payment-methods-checkboxes/test/index.test.tsx @@ -2,7 +2,7 @@ /** * External dependencies */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -16,7 +16,13 @@ import { act } from 'react-dom/test-utils'; jest.mock( '@woocommerce/components', () => { return { - Pill: ( { className, children } ) => ( + Pill: ( { + className, + children, + }: { + className: string; + children: ReactNode; + } ): React.ReactElement => ( { children } ), }; @@ -40,11 +46,14 @@ describe( 'PaymentMethodsCheckboxes', () => { { upeMethods.map( ( key ) => ( ) ) } @@ -101,9 +110,12 @@ describe( 'PaymentMethodsCheckboxes', () => { ); @@ -123,16 +135,18 @@ describe( 'PaymentMethodsCheckboxes', () => { } ); it( 'shows the required label on payment methods which are required', () => { - const handleChange = () => {}; + const handleChange = jest.fn(); const page = render( ); @@ -143,21 +157,24 @@ describe( 'PaymentMethodsCheckboxes', () => { } ); it( 'shows the disabled notice pill on payment methods with disabled statuses', () => { - const handleChange = () => {}; + const handleChange = jest.fn(); const page = render( ); expect( page.container ).toContainHTML( - 'Contact WooCommerce Support' + 'More information needed' ); } ); @@ -168,15 +185,17 @@ describe( 'PaymentMethodsCheckboxes', () => { ); const cardCheckbox = screen.getByRole( 'checkbox', { - name: 'Credit card / debit card', + name: 'Credit / Debit card', } ); expect( cardCheckbox ).not.toBeChecked(); userEvent.click( cardCheckbox ); @@ -191,9 +210,12 @@ describe( 'PaymentMethodsCheckboxes', () => { ); @@ -206,23 +228,29 @@ describe( 'PaymentMethodsCheckboxes', () => { expect( sofortCheckbox ).not.toBeChecked(); } ); - it( 'doesnt show the disabled notice pill on payment methods with active and unrequested statuses', () => { - const handleChange = () => {}; + it( "doesn't show the disabled notice pill on payment methods with active and unrequested statuses", () => { + const handleChange = jest.fn(); render( ); diff --git a/client/components/payment-methods-list/index.js b/client/components/payment-methods-list/index.tsx similarity index 62% rename from client/components/payment-methods-list/index.js rename to client/components/payment-methods-list/index.tsx index acf3e181699..cbb424ede38 100644 --- a/client/components/payment-methods-list/index.js +++ b/client/components/payment-methods-list/index.tsx @@ -2,7 +2,7 @@ /** * External dependencies */ -import React from 'react'; +import React, { ReactNode } from 'react'; import classNames from 'classnames'; /** @@ -10,7 +10,13 @@ import classNames from 'classnames'; */ import './style.scss'; -const PaymentMethodsList = ( { className, children } ) => { +const PaymentMethodsList = ( { + className, + children, +}: { + className?: string; + children: ReactNode; +} ): React.ReactElement => { return (

{ interpolateComponents( { - mixedString: __( - '{{strong}}Need help?{{/strong}} ' + - 'Learn more about {{wooCommercePaymentsLink}}WooCommerce Payments{{/wooCommercePaymentsLink}} or ' + - '{{contactSupportLink}}contact WooCommerce Support{{/contactSupportLink}}.', - 'woocommerce-payments' + mixedString: sprintf( + /* translators: %s: WooPayments */ + __( + '{{strong}}Need help?{{/strong}} ' + + 'Learn more about {{wooCommercePaymentsLink}}%s{{/wooCommercePaymentsLink}} or ' + + '{{contactSupportLink}}contact WooCommerce Support{{/contactSupportLink}}.', + 'woocommerce-payments' + ), + 'WooPayments' ), components: { strong: , wooCommercePaymentsLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content - + ), contactSupportLink: ( // eslint-disable-next-line jsx-a11y/anchor-has-content diff --git a/client/payment-gateways/payment-gateways-confirmation.js b/client/payment-gateways/payment-gateways-confirmation.js index 7c61fd7f5a9..eea42b86f3f 100644 --- a/client/payment-gateways/payment-gateways-confirmation.js +++ b/client/payment-gateways/payment-gateways-confirmation.js @@ -35,7 +35,7 @@ const PaymentGatewaysConfirmation = () => { useEffect( () => { const handler = ( event, request, settings ) => { - if ( true === hasUserConfirmedDeactivation.current ) { + if ( hasUserConfirmedDeactivation.current === true ) { // if the user does an "deactivate > confirm > activate > deactivate" step, we need to show the dialog again hasUserConfirmedDeactivation.current = false; return; @@ -57,10 +57,9 @@ const PaymentGatewaysConfirmation = () => { // Is the user trying to enable it or disable it? // if they're trying to enable it (i.e.: it's currently disabled), no need to show the modal if ( - 1 === jQuery( 'tr[data-gateway_id="woocommerce_payments"] .woocommerce-input-toggle--disabled' - ).length + ).length === 1 ) { return; } diff --git a/client/payment-methods-map.tsx b/client/payment-methods-map.tsx index 89ef891bc45..4ffc49befb8 100644 --- a/client/payment-methods-map.tsx +++ b/client/payment-methods-map.tsx @@ -16,6 +16,9 @@ import SepaIcon from 'assets/images/payment-methods/sepa-debit.svg?asset'; import P24Icon from 'assets/images/payment-methods/p24.svg?asset'; import IdealIcon from 'assets/images/payment-methods/ideal.svg?asset'; import BankDebitIcon from 'assets/images/payment-methods/bank-debit.svg?asset'; +import AffirmIcon from 'assets/images/payment-methods/affirm.svg?asset'; +import AfterpayIcon from 'assets/images/payment-methods/afterpay.svg?asset'; +import JCBIcon from 'assets/images/payment-methods/jcb.svg?asset'; const iconComponent = ( src: string, alt: string ): ReactImgFuncComponent => ( props @@ -24,11 +27,15 @@ const iconComponent = ( src: string, alt: string ): ReactImgFuncComponent => ( export interface PaymentMethodMapEntry { id: string; label: string; + brandTitles: Record< string, string >; description: string; icon: ReactImgFuncComponent; currencies: string[]; stripe_key: string; allows_manual_capture: boolean; + allows_pay_later: boolean; + setup_required?: boolean; + setup_tooltip?: string; } const PaymentMethodInformationObject: Record< @@ -37,7 +44,16 @@ const PaymentMethodInformationObject: Record< > = { card: { id: 'card', - label: __( 'Credit card / debit card', 'woocommerce-payments' ), + label: __( 'Credit / Debit card', 'woocommerce-payments' ), + brandTitles: { + amex: __( 'American Express', 'woocommerce-payments' ), + diners: __( 'Diners Club', 'woocommerce-payments' ), + discover: __( 'Discover', 'woocommerce-payments' ), + jcb: __( 'JCB', 'woocommerce-payments' ), + mastercard: __( 'Mastercard', 'woocommerce-payments' ), + unionpay: __( 'UnionPay', 'woocommerce-payments' ), + visa: __( 'Visa', 'woocommerce-payments' ), + }, description: __( 'Let your customers pay with major credit and debit cards without leaving your store.', 'woocommerce-payments' @@ -46,10 +62,14 @@ const PaymentMethodInformationObject: Record< currencies: [], stripe_key: 'card_payments', allows_manual_capture: true, + allows_pay_later: false, }, au_becs_debit: { id: 'au_becs_debit', label: __( 'BECS Direct Debit', 'woocommerce-payments' ), + brandTitles: { + au_becs_debit: __( 'BECS Direct Debit', 'woocommerce-payments' ), + }, description: __( 'Bulk Electronic Clearing System — Accept secure bank transfer from Australia.', 'woocommerce-payments' @@ -58,10 +78,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'AUD' ], stripe_key: 'au_becs_debit_payments', allows_manual_capture: false, + allows_pay_later: false, }, bancontact: { id: 'bancontact', label: __( 'Bancontact', 'woocommerce-payments' ), + brandTitles: { + bancontact: __( 'Bancontact', 'woocommerce-payments' ), + }, description: __( 'Bancontact is a bank redirect payment method offered by more than 80% of online businesses in Belgium.', 'woocommerce-payments' @@ -70,10 +94,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR' ], stripe_key: 'bancontact_payments', allows_manual_capture: false, + allows_pay_later: false, }, eps: { id: 'eps', label: __( 'EPS', 'woocommerce-payments' ), + brandTitles: { + eps: __( 'EPS', 'woocommerce-payments' ), + }, description: __( 'Accept your payment with EPS — a common payment method in Austria.', 'woocommerce-payments' @@ -82,10 +110,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR' ], stripe_key: 'eps_payments', allows_manual_capture: false, + allows_pay_later: false, }, giropay: { id: 'giropay', label: __( 'giropay', 'woocommerce-payments' ), + brandTitles: { + giropay: __( 'giropay', 'woocommerce-payments' ), + }, description: __( 'Expand your business with giropay — Germany’s second most popular payment system.', 'woocommerce-payments' @@ -94,10 +126,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR' ], stripe_key: 'giropay_payments', allows_manual_capture: false, + allows_pay_later: false, }, ideal: { id: 'ideal', label: __( 'iDEAL', 'woocommerce-payments' ), + brandTitles: { + ideal: __( 'iDEAL', 'woocommerce-payments' ), + }, description: __( 'Expand your business with iDEAL — Netherlands’s most popular payment method.', 'woocommerce-payments' @@ -106,10 +142,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR' ], stripe_key: 'ideal_payments', allows_manual_capture: false, + allows_pay_later: false, }, p24: { id: 'p24', label: __( 'Przelewy24 (P24)', 'woocommerce-payments' ), + brandTitles: { + p24: __( 'Przelewy24 (P24)', 'woocommerce-payments' ), + }, description: __( 'Accept payments with Przelewy24 (P24), the most popular payment method in Poland.', 'woocommerce-payments' @@ -118,10 +158,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR', 'PLN' ], stripe_key: 'p24_payments', allows_manual_capture: false, + allows_pay_later: false, }, sepa_debit: { id: 'sepa_debit', label: __( 'SEPA Direct Debit', 'woocommerce-payments' ), + brandTitles: { + sepa_debit: __( 'SEPA Direct Debit', 'woocommerce-payments' ), + }, description: __( 'Reach 500 million customers and over 20 million businesses across the European Union.', 'woocommerce-payments' @@ -130,10 +174,14 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR' ], stripe_key: 'sepa_debit_payments', allows_manual_capture: false, + allows_pay_later: false, }, sofort: { id: 'sofort', label: __( 'Sofort', 'woocommerce-payments' ), + brandTitles: { + sofort: __( 'Sofort', 'woocommerce-payments' ), + }, description: __( 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.', 'woocommerce-payments' @@ -142,6 +190,62 @@ const PaymentMethodInformationObject: Record< currencies: [ 'EUR' ], stripe_key: 'sofort_payments', allows_manual_capture: false, + allows_pay_later: false, + }, + affirm: { + id: 'affirm', + label: __( 'Affirm', 'woocommerce-payments' ), + brandTitles: { + affirm: __( 'Affirm', 'woocommerce-payments' ), + }, + description: __( + // translators: %s is the store currency. + 'Allow customers to pay over time with Affirm. Available to all customers paying in %s.', + 'woocommerce-payments' + ), + icon: iconComponent( AffirmIcon, 'Affirm' ), + currencies: [ 'USD', 'CAD' ], + stripe_key: 'affirm_payments', + allows_manual_capture: false, + allows_pay_later: true, + }, + afterpay_clearpay: { + id: 'afterpay_clearpay', + label: __( 'Afterpay', 'woocommerce-payments' ), + brandTitles: { + afterpay_clearpay: __( 'Afterpay', 'woocommerce-payments' ), + }, + description: __( + // translators: %s is the store currency. + 'Allow customers to pay over time with Afterpay. Available to all customers paying in %s.', + 'woocommerce-payments' + ), + icon: iconComponent( AfterpayIcon, 'Afterpay' ), + currencies: [ 'USD', 'AUD', 'CAD', 'NZD', 'GBP', 'EUR' ], + stripe_key: 'afterpay_clearpay_payments', + allows_manual_capture: false, + allows_pay_later: true, + }, + jcb: { + id: 'jcb', + label: __( 'JCB', 'woocommerce-payments' ), + brandTitles: { + jcb: __( 'JCB', 'woocommerce-payments' ), + }, + description: __( + 'Let your customers pay with JCB, the only international payment brand based in Japan.', + 'woocommerce-payments' + ), + icon: iconComponent( JCBIcon, 'JCB' ), + currencies: [ 'JPY' ], + stripe_key: 'card_payments', + allows_manual_capture: false, + allows_pay_later: false, + setup_required: true, + setup_tooltip: __( + 'JCB is coming soon to your country.', + 'woocommerce-payments' + ), }, }; diff --git a/client/payment-methods/constants.ts b/client/payment-methods/constants.ts new file mode 100644 index 00000000000..2facf48e83c --- /dev/null +++ b/client/payment-methods/constants.ts @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +enum PAYMENT_METHOD_IDS { + AFFIRM = 'affirm', + AFTERPAY_CLEARPAY = 'afterpay_clearpay', + AU_BECS_DEBIT = 'au_becs_debit', + BANCONTACT = 'bancontact', + CARD = 'card', + CARD_PRESENT = 'card_present', + EPS = 'eps', + GIROPAY = 'giropay', + IDEAL = 'ideal', + LINK = 'link', + P24 = 'p24', + SEPA_DEBIT = 'sepa_debit', + SOFORT = 'sofort', + JCB = 'jcb', +} + +// This constant is used for rendering tooltip titles for payment methods in transaction list and details pages. +// eslint-disable-next-line @typescript-eslint/naming-convention +export const PAYMENT_METHOD_TITLES = { + ach_credit_transfer: __( 'ACH Credit Transfer', 'woocommerce-payments' ), + ach_debit: __( 'ACH Debit', 'woocommerce-payments' ), + acss_debit: __( 'ACSS Debit', 'woocommerce-payments' ), + affirm: __( 'Affirm', 'woocommerce-payments' ), + afterpay_clearpay: __( 'Afterpay', 'woocommerce-payments' ), + alipay: __( 'Alipay', 'woocommerce-payments' ), + amex: __( 'American Express', 'woocommerce-payments' ), + au_becs_debit: __( 'AU BECS Debit', 'woocommerce-payments' ), + bancontact: __( 'Bancontact', 'woocommerce-payments' ), + card: __( 'Card Payment', 'woocommerce-payments' ), + card_present: __( 'In-Person Card Payment', 'woocommerce-payments' ), + diners: __( 'Diners Club', 'woocommerce-payments' ), + discover: __( 'Discover', 'woocommerce-payments' ), + eps: __( 'EPS', 'woocommerce-payments' ), + giropay: __( 'giropay', 'woocommerce-payments' ), + ideal: __( 'iDEAL', 'woocommerce-payments' ), + jcb: __( 'JCB', 'woocommerce-payments' ), + klarna: __( 'Klarna', 'woocommerce-payments' ), + link: __( 'Link', 'woocommerce-payments' ), + mastercard: __( 'Mastercard', 'woocommerce-payments' ), + multibanco: __( 'Multibanco', 'woocommerce-payments' ), + p24: __( 'P24', 'woocommerce-payments' ), + sepa_debit: __( 'SEPA Debit', 'woocommerce-payments' ), + sofort: __( 'SOFORT', 'woocommerce-payments' ), + stripe_account: __( 'Stripe Account', 'woocommerce-payments' ), + unionpay: __( 'Union Pay', 'woocommerce-payments' ), + visa: __( 'Visa', 'woocommerce-payments' ), + wechat: __( 'WeChat', 'woocommerce-payments' ), +}; + +export default PAYMENT_METHOD_IDS; diff --git a/client/payment-methods/delete-modal.tsx b/client/payment-methods/delete-modal.tsx index dc76faf8d70..dfc88d433ed 100644 --- a/client/payment-methods/delete-modal.tsx +++ b/client/payment-methods/delete-modal.tsx @@ -71,10 +71,7 @@ const ConfirmPaymentMethodDeleteModal: React.FunctionComponent< { components: { wooCommercePaymentsLink: ( - { __( - 'WooCommerce Payments', - 'woocommerce-payments' - ) } + { 'WooPayments' } ), }, diff --git a/client/payment-methods/index.js b/client/payment-methods/index.js index 01dacbe2810..d47b2ddb9e1 100644 --- a/client/payment-methods/index.js +++ b/client/payment-methods/index.js @@ -8,7 +8,6 @@ import { __ } from '@wordpress/i18n'; import { Button, Card, - CardDivider, CardHeader, DropdownMenu, ExternalLink, @@ -26,10 +25,12 @@ import { useGetPaymentMethodStatuses, useSelectedPaymentMethod, useUnselectedPaymentMethod, + useAccountDomesticCurrency, } from 'wcpay/data'; import useIsUpeEnabled from '../settings/wcpay-upe-toggle/hook.js'; import WcPayUpeContext from '../settings/wcpay-upe-toggle/context'; +import PAYMENT_METHOD_IDS from './constants'; // Survey modal imports. import WcPaySurveyContextProvider from '../settings/survey-modal/provider'; @@ -45,6 +46,9 @@ import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants' import ConfirmPaymentMethodActivationModal from './activation-modal'; import ConfirmPaymentMethodDeleteModal from './delete-modal'; import { getAdminUrl } from 'wcpay/utils'; +import { getPaymentMethodDescription } from 'wcpay/utils/payment-methods'; +import InlineNotice from 'wcpay/components/inline-notice'; +import interpolateComponents from '@automattic/interpolate-components'; const PaymentMethodsDropdownMenu = ( { setOpenModal } ) => { return ( @@ -79,32 +83,35 @@ const UpeSetupBanner = () => { return ( <> - - +

{ __( - 'Enable the new WooCommerce Payments checkout experience', + 'Enable the new WooPayments checkout experience, which will become the default on November 1, 2023', 'woocommerce-payments' ) }

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

- - + { __( 'Learn more', 'woocommerce-payments' ) }
@@ -121,9 +128,31 @@ const PaymentMethods = () => { const availablePaymentMethodIds = useGetAvailablePaymentMethodIds(); // We filter link payment method since this will be displayed in other section (express checkout). - const availableMethods = availablePaymentMethodIds - .filter( ( id ) => 'link' !== id ) - .map( ( methodId ) => methodsConfiguration[ methodId ] ); + // We further split the available methods into pay later and non-pay later methods to sort them in the required order later. + const availableNonPayLaterMethods = availablePaymentMethodIds.filter( + ( id ) => + PAYMENT_METHOD_IDS.LINK !== id && + PAYMENT_METHOD_IDS.CARD !== id && + ! methodsConfiguration[ id ].allows_pay_later + ); + + const availablePayLaterMethods = availablePaymentMethodIds.filter( + ( id ) => + PAYMENT_METHOD_IDS.LINK !== id && + methodsConfiguration[ id ].allows_pay_later + ); + + const orderedAvailablePaymentMethodIds = [ + PAYMENT_METHOD_IDS.CARD, + ...availablePayLaterMethods, + ...availableNonPayLaterMethods, + ]; + + const availableMethods = orderedAvailablePaymentMethodIds.map( + ( methodId ) => methodsConfiguration[ methodId ] + ); + + const isCreditCardEnabled = enabledMethodIds.includes( 'card' ); const [ activationModalParams, handleActivationModalOpen ] = useState( null @@ -132,6 +161,8 @@ const PaymentMethods = () => { const [ , updateSelectedPaymentMethod ] = useSelectedPaymentMethod(); + const [ stripeAccountDomesticCurrency ] = useAccountDomesticCurrency(); + const completeActivation = ( itemId ) => { updateSelectedPaymentMethod( itemId ); handleActivationModalOpen( null ); @@ -162,8 +193,8 @@ const PaymentMethods = () => { const handleCheckClick = ( itemId ) => { const statusAndRequirements = getStatusAndRequirements( itemId ); if ( - 'unrequested' === statusAndRequirements.status && - 0 < statusAndRequirements.requirements.length + statusAndRequirements.status === 'unrequested' && + statusAndRequirements.requirements.length > 0 ) { handleActivationModalOpen( { id: itemId, @@ -177,7 +208,7 @@ const PaymentMethods = () => { const handleUncheckClick = ( itemId ) => { const methodConfig = methodsConfiguration[ itemId ]; const statusAndRequirements = getStatusAndRequirements( itemId ); - if ( methodConfig && 'active' === statusAndRequirements.status ) { + if ( methodConfig && statusAndRequirements.status === 'active' ) { handleDeleteModalOpen( { id: itemId, label: methodConfig.label, @@ -197,7 +228,7 @@ const PaymentMethods = () => { return ( <> - { 'disable' === openModalIdentifier ? ( + { openModalIdentifier === 'disable' ? ( @@ -205,7 +236,7 @@ const PaymentMethods = () => { } /> ) : null } - { 'survey' === openModalIdentifier ? ( + { openModalIdentifier === 'survey' ? ( { { isUpeEnabled && ( @@ -229,7 +260,7 @@ const PaymentMethods = () => { 'woocommerce-payments' ) } - { 'split' !== upeType && ( + { upeType !== 'split' && ( <> { ' ' } @@ -247,21 +278,49 @@ const PaymentMethods = () => { ) } + { isUpeEnabled && upeType === 'legacy' && ( + + + { interpolateComponents( { + mixedString: __( + 'The new WooPayments checkout experience will become the default on October 11, 2023.' + + ' {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + // eslint-disable-next-line max-len + + ), + }, + } ) } + + + ) } + { availableMethods.map( ( { id, label, - description, icon: Icon, allows_manual_capture: isAllowingManualCapture, + setup_required: isSetupRequired, + setup_tooltip: setupTooltip, } ) => ( { .status } // The card payment method is required when UPE is active, and it can't be disabled/unchecked. - required={ 'card' === id && isUpeEnabled } - locked={ 'card' === id && isUpeEnabled } + required={ + PAYMENT_METHOD_IDS.CARD === id && + isUpeEnabled + } + locked={ + PAYMENT_METHOD_IDS.CARD === id && + isCreditCardEnabled && + isUpeEnabled + } Icon={ Icon } status={ getStatusAndRequirements( id ).status } + isSetupRequired={ isSetupRequired } + setupTooltip={ setupTooltip } isAllowingManualCapture={ isAllowingManualCapture } @@ -284,15 +352,34 @@ const PaymentMethods = () => { onCheckClick={ () => { handleCheckClick( id ); } } + isPoEnabled={ + wcpaySettings?.progressiveOnboarding + ?.isEnabled + } + isPoComplete={ + wcpaySettings?.progressiveOnboarding + ?.isComplete + } /> ) ) } - { isUpeSettingsPreviewEnabled && ! isUpeEnabled && ( - - ) } + + { isUpeSettingsPreviewEnabled && ! isUpeEnabled && ( + <> +
+ + + + + ) } + { activationModalParams && ( { diff --git a/client/payment-methods/style.scss b/client/payment-methods/style.scss index 9f01aed81c1..253daa09344 100644 --- a/client/payment-methods/style.scss +++ b/client/payment-methods/style.scss @@ -5,6 +5,11 @@ padding: 0; } + .components-popover__content { + position: relative; + right: 120px; + } + &__header { justify-content: space-between; @@ -38,15 +43,29 @@ } &__express-checkouts { - background: url( 'assets/images/upe_preview_illustration.svg?asset' ) + background: url( 'assets/images/payment-methods/all_local_payments.svg?asset' ) no-repeat right; - background-color: #f7edf7; + &.background-local-payment-methods { + background: url( 'assets/images/payment-methods/local_payments.svg?asset' ) + no-repeat right; + @media ( max-width: 1024px ) { + h3, + p { + padding-right: 0; + } + background-image: none; + } + } background-size: auto; + margin-right: 10px; - h3, - p { + h3 { + font-size: 14px; padding-right: 215px; } + p { + padding-right: 230px; + } &-get-started { margin-right: 16px; @@ -93,3 +112,16 @@ position: fixed; } } + +// RTL - hide flags dropdown for phone input +// @TODO find a way to make the dropdown working for LTR +.rtl { + .iti { + &__country-list { + display: none; + } + &__arrow { + display: none; + } + } +} 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 db5844304d7..197b0e6a555 100644 --- a/client/payment-methods/test/__snapshots__/activation-modal.test.js.snap +++ b/client/payment-methods/test/__snapshots__/activation-modal.test.js.snap @@ -55,7 +55,7 @@ exports[`Activation Modal matches the snapshot 1`] = ` class="components-modal__header-heading" id="components-modal-header-0" > - One more step to enable Credit card / debit card + One more step to enable Credit / Debit card

- You need to provide more information to enable Credit card / debit card on your checkout: + You need to provide more information to enable Credit / Debit card on your checkout:

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

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

+ + +
+ ); +}; + +export default StripeBillingSection; diff --git a/client/settings/advanced-settings/stripe-billing-toggle.tsx b/client/settings/advanced-settings/stripe-billing-toggle.tsx new file mode 100644 index 00000000000..4f8dea69584 --- /dev/null +++ b/client/settings/advanced-settings/stripe-billing-toggle.tsx @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import React, { useContext } from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import { CheckboxControl, ExternalLink } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; + +/** + * Internal dependencies + */ +import StripeBillingMigrationNoticeContext from './stripe-billing-notices/context'; + +interface Props { + /** + * The function to run when the checkbox is changed. + */ + onChange: ( enabled: boolean ) => void; +} + +/** + * Renders the Stripe Billing toggle. + * + * @return {JSX.Element} Rendered Stripe Billing toggle. + */ +const StripeBillingToggle: React.FC< Props > = ( { onChange } ) => { + const context = useContext( StripeBillingMigrationNoticeContext ); + + return ( + + ), + }, + } ) } + /> + ); +}; + +export default StripeBillingToggle; diff --git a/client/settings/advanced-settings/test/debug-mode.test.js b/client/settings/advanced-settings/test/debug-mode.test.js index abd59036994..54877787745 100644 --- a/client/settings/advanced-settings/test/debug-mode.test.js +++ b/client/settings/advanced-settings/test/debug-mode.test.js @@ -22,12 +22,6 @@ describe( 'DebugMode', () => { jest.clearAllMocks(); } ); - it( 'sets the heading as focused after rendering', () => { - render( ); - - expect( screen.getByText( 'Debug mode' ) ).toHaveFocus(); - } ); - it( 'toggles the logging checkbox', () => { const setDebugLogMock = jest.fn(); useDebugLog.mockReturnValue( [ false, setDebugLogMock ] ); diff --git a/client/settings/advanced-settings/test/index.test.js b/client/settings/advanced-settings/test/index.test.js index 05be0739f75..517f237969e 100644 --- a/client/settings/advanced-settings/test/index.test.js +++ b/client/settings/advanced-settings/test/index.test.js @@ -4,44 +4,59 @@ * External dependencies */ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ import AdvancedSettings from '..'; +import { + useMultiCurrency, + useWCPaySubscriptions, + useDevMode, + useDebugLog, + useClientSecretEncryption, +} from 'wcpay/data'; + +jest.mock( '../../../data', () => ( { + useSettings: jest.fn(), + useMultiCurrency: jest.fn(), + useWCPaySubscriptions: jest.fn(), + useDevMode: jest.fn(), + useDebugLog: jest.fn(), + useClientSecretEncryption: jest.fn(), +} ) ); describe( 'AdvancedSettings', () => { - it( 'toggles the advanced settings section', () => { + beforeEach( () => { + useMultiCurrency.mockReturnValue( [ false, jest.fn() ] ); + useWCPaySubscriptions.mockReturnValue( [ false, jest.fn() ] ); + useDevMode.mockReturnValue( false ); + useDebugLog.mockReturnValue( [ false, jest.fn() ] ); + useClientSecretEncryption.mockReturnValue( [ false, jest.fn() ] ); + } ); + test( 'toggles the advanced settings section', () => { global.wcpaySettings = { isClientEncryptionEligible: true, }; - render( ); - expect( screen.queryByText( 'Debug mode' ) ).not.toBeInTheDocument(); - - userEvent.click( screen.getByText( 'Advanced settings' ) ); + render( ); + // The advanced settings section is expanded by default. expect( screen.queryByText( 'Enable Public Key Encryption' ) ).toBeInTheDocument(); expect( screen.queryByText( 'Debug mode' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Debug mode' ) ).toHaveFocus(); } ); - it( 'hides the client encryption toggle when not eligible', () => { + test( 'hides the client encryption toggle when not eligible', () => { global.wcpaySettings = { isClientEncryptionEligible: false, }; - render( ); - - expect( screen.queryByText( 'Debug mode' ) ).not.toBeInTheDocument(); - userEvent.click( screen.getByText( 'Advanced settings' ) ); + render( ); expect( screen.queryByText( 'Enable Public Key Encryption' ) ).not.toBeInTheDocument(); expect( screen.queryByText( 'Debug mode' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Debug mode' ) ).toHaveFocus(); } ); } ); diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index f714617f95c..0f1f3f2937b 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -2,7 +2,7 @@ * External dependencies */ import { CheckboxControl, ExternalLink } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { useEffect, useRef } from '@wordpress/element'; /** @@ -15,7 +15,6 @@ const WCPaySubscriptionsToggle = () => { const [ isWCPaySubscriptionsEnabled, isWCPaySubscriptionsEligible, - isSubscriptionsPluginActive, updateIsWCPaySubscriptionsEnabled, ] = useWCPaySubscriptions(); @@ -31,22 +30,31 @@ const WCPaySubscriptionsToggle = () => { updateIsWCPaySubscriptionsEnabled( value ); }; - return ! isSubscriptionsPluginActive && + /** + * Only show the toggle if the site doesn't have WC Subscriptions active and is eligible + * for wcpay subscriptions or if wcpay subscriptions are already enabled. + */ + return ! wcpaySettings.isSubscriptionsActive && ( isWCPaySubscriptionsEligible || isWCPaySubscriptionsEnabled ) ? ( + ), }, } ) } diff --git a/client/settings/deposits/index.js b/client/settings/deposits/index.js index ab0fa481c73..5c633a390e0 100644 --- a/client/settings/deposits/index.js +++ b/client/settings/deposits/index.js @@ -2,6 +2,7 @@ * External dependencies */ import React, { useContext } from 'react'; +import { select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { Card, @@ -10,6 +11,7 @@ import { Notice, } from '@wordpress/components'; import HelpOutlineIcon from 'gridicons/dist/help-outline'; +import { STORE_NAME } from 'wcpay/data/constants'; /** * Internal dependencies @@ -23,6 +25,7 @@ import { useDepositScheduleMonthlyAnchor, useDepositStatus, useCompletedWaitingPeriod, + useDepositRestrictions, } from '../../data'; import './style.scss'; import wcpayTracks from 'wcpay/tracks'; @@ -61,6 +64,8 @@ const CustomizeDepositSchedule = () => { setDepositScheduleMonthlyAnchor, ] = useDepositScheduleMonthlyAnchor(); + const settings = select( STORE_NAME ).getSettings(); + const handleIntervalChange = ( newInterval ) => { switch ( newInterval ) { case 'weekly': @@ -79,6 +84,26 @@ const CustomizeDepositSchedule = () => { setDepositScheduleInterval( newInterval ); }; + let depositIntervalsOptions = [ + { + value: 'daily', + label: __( 'Daily', 'woocommerce-payments' ), + }, + { + value: 'weekly', + label: __( 'Weekly', 'woocommerce-payments' ), + }, + { + value: 'monthly', + label: __( 'Monthly', 'woocommerce-payments' ), + }, + ]; + + if ( settings.account_country === 'JP' ) { + // Japanese accounts can't have daily payouts. + depositIntervalsOptions = depositIntervalsOptions.slice( 1 ); + } + return ( <>
@@ -86,22 +111,9 @@ const CustomizeDepositSchedule = () => { label={ __( 'Frequency', 'woocommerce-payments' ) } value={ depositScheduleInterval } onChange={ handleIntervalChange } - options={ [ - { - value: 'daily', - label: __( 'Daily', 'woocommerce-payments' ), - }, - { - value: 'weekly', - label: __( 'Weekly', 'woocommerce-payments' ), - }, - { - value: 'monthly', - label: __( 'Monthly', 'woocommerce-payments' ), - }, - ] } + options={ depositIntervalsOptions } /> - { 'monthly' === depositScheduleInterval && ( + { depositScheduleInterval === 'monthly' && ( { options={ monthlyAnchors } /> ) } - { 'weekly' === depositScheduleInterval && ( + { depositScheduleInterval === 'weekly' && ( { ) }

- { 'monthly' === depositScheduleInterval && + { depositScheduleInterval === 'monthly' && __( 'Deposits scheduled on a weekend will be sent on the next business day.', 'woocommerce-payments' ) } - { 'weekly' === depositScheduleInterval && + { depositScheduleInterval === 'weekly' && __( 'Deposits that fall on a holiday will initiate on the next business day.', 'woocommerce-payments' ) } - { 'daily' === depositScheduleInterval && + { depositScheduleInterval === 'daily' && __( 'Deposits will occur every business day.', 'woocommerce-payments' @@ -140,9 +152,13 @@ const CustomizeDepositSchedule = () => { }; const DepositsSchedule = () => { const depositStatus = useDepositStatus(); + const depositRestrictions = useDepositRestrictions(); const completedWaitingPeriod = useCompletedWaitingPeriod(); - if ( 'enabled' !== depositStatus ) { + if ( + depositStatus !== 'enabled' || + depositRestrictions === 'schedule_restricted' + ) { return ( { 'Learn more about deposit scheduling.', 'woocommerce-payments' ) } - href="https://woocommerce.com/document/payments/faq/deposit-schedule/" + href="https://woocommerce.com/document/woopayments/deposits/deposit-schedule/" target="_blank" rel="external noreferrer noopener" > @@ -169,7 +185,7 @@ const DepositsSchedule = () => { ); } - if ( true !== completedWaitingPeriod ) { + if ( completedWaitingPeriod !== true ) { return ( { 'Learn more about deposit scheduling.', 'woocommerce-payments' ) } - href="https://woocommerce.com/document/payments/faq/deposit-schedule/" + href="https://woocommerce.com/document/woopayments/deposits/deposit-schedule/" target="_blank" rel="external noreferrer noopener" > diff --git a/client/settings/deposits/test/index.test.js b/client/settings/deposits/test/index.test.js index a8a0ea9f848..6a81fd73018 100644 --- a/client/settings/deposits/test/index.test.js +++ b/client/settings/deposits/test/index.test.js @@ -2,6 +2,7 @@ * External dependencies */ import { render, screen, within } from '@testing-library/react'; +import { select } from '@wordpress/data'; /** * Internal dependencies @@ -10,12 +11,15 @@ import Deposits from '..'; import WCPaySettingsContext from '../../wcpay-settings-context'; import { useDepositStatus, + useDepositRestrictions, useCompletedWaitingPeriod, useDepositScheduleInterval, useDepositScheduleWeeklyAnchor, useDepositScheduleMonthlyAnchor, } from 'wcpay/data'; +jest.mock( '@wordpress/data' ); + jest.mock( 'wcpay/data', () => ( { useAccountStatementDescriptor: jest.fn(), useManualCapture: jest.fn(), @@ -23,6 +27,7 @@ jest.mock( 'wcpay/data', () => ( { useSavedCards: jest.fn(), useCardPresentEligible: jest.fn(), useDepositStatus: jest.fn(), + useDepositRestrictions: jest.fn(), useCompletedWaitingPeriod: jest.fn(), useDepositScheduleInterval: jest.fn(), useDepositScheduleWeeklyAnchor: jest.fn(), @@ -36,6 +41,7 @@ describe( 'Deposits', () => { beforeEach( () => { useDepositStatus.mockReturnValue( 'enabled' ); + useDepositRestrictions.mockReturnValue( 'deposits_unrestricted' ); useCompletedWaitingPeriod.mockReturnValue( true ); useDepositScheduleInterval.mockReturnValue( [ 'daily', jest.fn() ] ); useDepositScheduleMonthlyAnchor.mockReturnValue( [ '1', jest.fn() ] ); @@ -43,6 +49,11 @@ describe( 'Deposits', () => { 'monday', jest.fn(), ] ); + select.mockImplementation( () => ( { + getSettings: jest.fn().mockReturnValue( { + account_country: 'US', + } ), + } ) ); } ); it( 'renders', () => { @@ -79,6 +90,26 @@ describe( 'Deposits', () => { expect( depositsMessage ).toBeInTheDocument(); } ); + it( 'renders the deposits blocked message when deposits are not blocked, but restricted', () => { + useDepositStatus.mockReturnValue( 'enabled' ); + useDepositRestrictions.mockReturnValue( 'schedule_restricted' ); + useCompletedWaitingPeriod.mockReturnValue( true ); + + render( + + + + ); + + const depositsMessage = screen.getByText( + /Deposit scheduling is currently unavailable for your store/, + { + ignore: '.a11y-speak-region', + } + ); + expect( depositsMessage ).toBeInTheDocument(); + } ); + it( 'renders the deposits blocked message for an unkown deposit status', () => { useDepositStatus.mockReturnValue( 'asdf' ); @@ -133,6 +164,27 @@ describe( 'Deposits', () => { within( frequencySelect ).getByRole( 'option', { name: /Monthly/ } ); } ); + it( 'renders the frequency select without daily for Japan', () => { + useDepositScheduleInterval.mockReturnValue( [ 'daily', jest.fn() ] ); + + select.mockImplementation( () => ( { + getSettings: jest.fn().mockReturnValue( { + account_country: 'JP', + } ), + } ) ); + render( + + + + ); + + const frequencySelect = screen.getByLabelText( /Frequency/ ); + expect( frequencySelect ).toHaveValue( 'weekly' ); + + within( frequencySelect ).getByRole( 'option', { name: /Weekly/ } ); + within( frequencySelect ).getByRole( 'option', { name: /Monthly/ } ); + } ); + it( 'renders the weekly offset select', () => { useDepositScheduleInterval.mockReturnValue( [ 'weekly', jest.fn() ] ); useDepositScheduleWeeklyAnchor.mockReturnValue( [ diff --git a/client/settings/disable-upe-modal/index.js b/client/settings/disable-upe-modal/index.js index 24048612362..4dc089783f3 100644 --- a/client/settings/disable-upe-modal/index.js +++ b/client/settings/disable-upe-modal/index.js @@ -2,7 +2,7 @@ * External dependencies */ import React, { useContext, useEffect } from 'react'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { dispatch } from '@wordpress/data'; import { Button, ExternalLink } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; @@ -14,13 +14,13 @@ import './style.scss'; import ConfirmationModal from 'components/confirmation-modal'; import useIsUpeEnabled from 'settings/wcpay-upe-toggle/hook'; import WcPayUpeContext from 'settings/wcpay-upe-toggle/context'; -import InlineNotice from '../../components/inline-notice'; +import InlineNotice from 'components/inline-notice'; import { useEnabledPaymentMethodIds } from '../../data'; import PaymentMethodIcon from '../payment-method-icon'; const NeedHelpBarSection = () => { return ( - + { interpolateComponents( { mixedString: __( 'Need help? Visit {{ docsLink /}} or {{supportLink /}}.', @@ -29,10 +29,11 @@ const NeedHelpBarSection = () => { components: { docsLink: ( // eslint-disable-next-line max-len - - { __( - 'WooCommerce Payments docs', - 'woocommerce-payments' + + { sprintf( + /* translators: %s: WooPayments */ + __( '%s docs', 'woocommerce-payments' ), + 'WooPayments' ) } ), @@ -51,7 +52,7 @@ const NeedHelpBarSection = () => { const DisableUpeModalBody = () => { const [ enabledPaymentMethodIds ] = useEnabledPaymentMethodIds(); const upePaymentMethods = enabledPaymentMethodIds.filter( - ( method ) => 'card' !== method + ( method ) => method !== 'card' ); return ( @@ -63,7 +64,7 @@ const DisableUpeModalBody = () => { 'woocommerce-payments' ) }

- { 0 < upePaymentMethods.length ? ( + { upePaymentMethods.length > 0 ? ( <>

{ __( @@ -97,7 +98,7 @@ const DisableUpeModal = ( { setOpenModal, triggerAfterDisable } ) => { }, [ isUpeEnabled, setOpenModal, triggerAfterDisable ] ); useEffect( () => { - if ( 'error' === status ) { + if ( status === 'error' ) { dispatch( 'core/notices' ).createErrorNotice( __( 'There was an error disabling the new payment methods.', @@ -120,7 +121,7 @@ const DisableUpeModal = ( { setOpenModal, triggerAfterDisable } ) => { <>

) } { ! isWooPayEnabled && ! isPaymentRequestEnabled && ( - + { __( 'To preview the express checkout buttons, ' + 'activate at least one express checkout.', @@ -161,7 +161,7 @@ const PaymentRequestButtonPreview = () => { ) } { isPaymentRequestEnabled && ! isLoading && ! paymentRequest && ( - + { __( 'To preview the Apple Pay and Google Pay buttons, ' + 'ensure your device is configured to accept Apple Pay or Google Pay, ' + diff --git a/client/settings/express-checkout-settings/payment-request-settings.js b/client/settings/express-checkout-settings/payment-request-settings.js index 27c0e727b5f..a676eba6bbe 100644 --- a/client/settings/express-checkout-settings/payment-request-settings.js +++ b/client/settings/express-checkout-settings/payment-request-settings.js @@ -42,7 +42,7 @@ const PaymentRequestSettings = ( { section } ) => { return ( - { 'enable' === section && ( + { section === 'enable' && ( { ) } - { 'general' === section && ( + { section === 'general' && ( ) } diff --git a/client/settings/express-checkout-settings/test/index.js b/client/settings/express-checkout-settings/test/index.js index b6fa3f76058..74c726d7c42 100644 --- a/client/settings/express-checkout-settings/test/index.js +++ b/client/settings/express-checkout-settings/test/index.js @@ -28,6 +28,7 @@ jest.mock( '../../../data', () => ( { useWooPayLocations: jest .fn() .mockReturnValue( [ [ true, true, true ], jest.fn() ] ), + useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), } ) ); jest.mock( '@wordpress/data', () => ( { @@ -52,6 +53,14 @@ jest.mock( 'payment-request/utils', () => ( { } ), } ) ); +jest.mock( '@woocommerce/components', () => ( { + Link: jest + .fn() + .mockImplementation( ( { href, children } ) => ( + { children } + ) ), +} ) ); + describe( 'ExpressCheckoutSettings', () => { beforeEach( () => { global.wcpaySettings = { @@ -65,7 +74,7 @@ describe( 'ExpressCheckoutSettings', () => { test( 'renders banner at the top', () => { render( ); - const banner = screen.queryByAltText( 'WooCommerce Payments logo' ); + const banner = screen.queryByAltText( 'WooPayments logo' ); expect( banner ).toBeInTheDocument(); } ); @@ -82,7 +91,7 @@ describe( 'ExpressCheckoutSettings', () => { render( ); const linkToPayments = screen.getByRole( 'link', { - name: 'WooCommerce Payments', + name: 'WooPayments', } ); const breadcrumbs = linkToPayments.closest( 'h2' ); @@ -124,7 +133,7 @@ describe( 'ExpressCheckoutSettings', () => { render( ); const linkToPayments = screen.getByRole( 'link', { - name: 'WooCommerce Payments', + name: 'WooPayments', } ); const breadcrumbs = linkToPayments.closest( 'h2' ); 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 ded12e5de6a..d0d4360b6b1 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 @@ -27,6 +27,7 @@ jest.mock( '../../../data', () => ( { usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayEnabledSettings: jest.fn(), + useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), } ) ); jest.mock( '../payment-request-button-preview' ); diff --git a/client/settings/express-checkout-settings/test/woopay-settings.test.js b/client/settings/express-checkout-settings/test/woopay-settings.test.js index e8ea5b97da8..fef25431a56 100644 --- a/client/settings/express-checkout-settings/test/woopay-settings.test.js +++ b/client/settings/express-checkout-settings/test/woopay-settings.test.js @@ -18,6 +18,7 @@ import { usePaymentRequestButtonSize, usePaymentRequestButtonTheme, useWooPayLocations, + useWooPayShowIncompatibilityNotice, } from '../../../data'; jest.mock( '../../../data', () => ( { @@ -28,12 +29,21 @@ jest.mock( '../../../data', () => ( { usePaymentRequestButtonSize: jest.fn(), usePaymentRequestButtonTheme: jest.fn(), useWooPayLocations: jest.fn(), + useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), } ) ); jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn( () => ( { createErrorNotice: jest.fn() } ) ), } ) ); +jest.mock( '@woocommerce/components', () => ( { + Link: jest + .fn() + .mockImplementation( ( { href, children } ) => ( + { children } + ) ), +} ) ); + const getMockWooPayEnabledSettings = ( isEnabled, updateIsWooPayEnabledHandler @@ -135,7 +145,9 @@ describe( 'WooPaySettings', () => { // confirm settings headings expect( - screen.queryByRole( 'heading', { name: 'Custom message' } ) + screen.queryByRole( 'heading', { + name: 'Policies and custom text', + } ) ).toBeInTheDocument(); // confirm radio button groups displayed @@ -150,4 +162,28 @@ describe( 'WooPaySettings', () => { 'test' ); } ); + + it( 'triggers the hooks when the enable setting is being interacted with', () => { + useWooPayShowIncompatibilityNotice.mockReturnValue( true ); + + render( ); + + expect( + screen.queryByText( + 'One or more of your extensions are incompatible with WooPay.' + ) + ).toBeInTheDocument(); + } ); + + it( 'triggers the hooks when the enable setting is being interacted with', () => { + useWooPayShowIncompatibilityNotice.mockReturnValue( false ); + + render( ); + + expect( + screen.queryByText( + 'One or more of your extensions are incompatible with WooPay.' + ) + ).not.toBeInTheDocument(); + } ); } ); diff --git a/client/settings/express-checkout-settings/woopay-settings.js b/client/settings/express-checkout-settings/woopay-settings.js index 91d474af6b6..5e54975dd63 100644 --- a/client/settings/express-checkout-settings/woopay-settings.js +++ b/client/settings/express-checkout-settings/woopay-settings.js @@ -4,8 +4,9 @@ */ import React from 'react'; import { __ } from '@wordpress/i18n'; -import { Card, CheckboxControl, TextControl } from '@wordpress/components'; +import { Card, CheckboxControl, TextareaControl } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; /** * Internal dependencies @@ -18,10 +19,10 @@ import { useWooPayCustomMessage, useWooPayStoreLogo, useWooPayLocations, + useWooPayShowIncompatibilityNotice, } from 'wcpay/data'; import GeneralPaymentRequestButtonSettings from './general-payment-request-button-settings'; - -const CUSTOM_MESSAGE_MAX_LENGTH = 100; +import WooPayIncompatibilityNotice from '../settings-warnings/incompatibility-notice'; const WooPaySettings = ( { section } ) => { const [ @@ -48,10 +49,15 @@ const WooPaySettings = ( { section } ) => { } }; + const showIncompatibilityNotice = useWooPayShowIncompatibilityNotice(); + return ( - { 'enable' === section && ( + { section === 'enable' && ( + { showIncompatibilityNotice && ( + + ) } { ) } - { 'appearance' === section && ( + { section === 'appearance' && (
{

- { __( 'Custom message', 'woocommerce-payments' ) } -

- + : + , + termsLink: window.wcSettings?.storePages?.terms?.permalink ? + : + , + /* eslint-enable prettier/prettier */ + } + } ) } value={ woopayCustomMessage } onChange={ setWooPayCustomMessage } - maxLength={ CUSTOM_MESSAGE_MAX_LENGTH } /> -
) } - { 'general' === section && ( + { section === 'general' && ( ) }
diff --git a/client/settings/express-checkout/apple-google-pay-item.tsx b/client/settings/express-checkout/apple-google-pay-item.tsx new file mode 100644 index 00000000000..871bbf6ee22 --- /dev/null +++ b/client/settings/express-checkout/apple-google-pay-item.tsx @@ -0,0 +1,167 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CheckboxControl } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; +import React from 'react'; + +/** + * Internal dependencies + */ +import { getPaymentMethodSettingsUrl } from '../../utils'; +import ApplePay from 'assets/images/cards/apple-pay.svg?asset'; +import GooglePay from 'assets/images/cards/google-pay.svg?asset'; +import { usePaymentRequestEnabledSettings } from 'wcpay/data'; +import { PaymentRequestEnabledSettingsHook } from './interfaces'; + +const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => { + const [ + isPaymentRequestEnabled, + updateIsPaymentRequestEnabled, + ] = usePaymentRequestEnabledSettings() as PaymentRequestEnabledSettingsHook; + + return ( +
  • +
    +
    + +
    +
    +
    +
    +
    + Apple Pay +
    +
    + { __( 'Apple Pay', 'woocommerce-payments' ) } +
    +
    +
    + { __( + 'Apple Pay', + 'woocommerce-payments' + ) } +
    +
    + { + /* eslint-disable jsx-a11y/anchor-has-content */ + isPaymentRequestEnabled + ? __( + 'Apple Pay is an easy and secure way for customers to pay on your store.', + 'woocommerce-payments' + ) + : interpolateComponents( { + mixedString: __( + /* eslint-disable-next-line max-len */ + 'Apple Pay is an easy and secure way for customers to pay on your store. ' + + /* eslint-disable-next-line max-len */ + 'By enabling this feature, you agree to {{stripeLink}}Stripe{{/stripeLink}} and' + + "{{appleLink}} Apple{{/appleLink}}'s terms of use.", + 'woocommerce-payments' + ), + components: { + stripeLink: ( + + ), + appleLink: ( + + ), + br:
    , + }, + } ) + /* eslint-enable jsx-a11y/anchor-has-content */ + } +
    +
    +
    +
    +
    + Google Pay +
    +
    + { __( 'Google Pay', 'woocommerce-payments' ) } +
    +
    +
    +
    + +
    +
    +
  • + ); +}; + +export default AppleGooglePayExpressCheckoutItem; diff --git a/client/settings/express-checkout/index.js b/client/settings/express-checkout/index.js index 26dc2e405d1..797ae2a0241 100644 --- a/client/settings/express-checkout/index.js +++ b/client/settings/express-checkout/index.js @@ -3,440 +3,25 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { Card, CheckboxControl, VisuallyHidden } from '@wordpress/components'; -import interpolateComponents from '@automattic/interpolate-components'; -import { useContext } from '@wordpress/element'; -import { Icon, warning } from '@wordpress/icons'; +import { Card } from '@wordpress/components'; /** * Internal dependencies */ -import { getPaymentMethodSettingsUrl } from '../../utils'; -import { - useEnabledPaymentMethodIds, - useGetAvailablePaymentMethodIds, - usePaymentRequestEnabledSettings, - useWooPayEnabledSettings, -} from 'wcpay/data'; import CardBody from '../card-body'; import './style.scss'; -import WCPaySettingsContext from '../wcpay-settings-context'; -import { HoverTooltip } from 'components/tooltip'; -import ApplePay from 'assets/images/cards/apple-pay.svg?asset'; -import GooglePay from 'assets/images/cards/google-pay.svg?asset'; -import LinkIcon from 'assets/images/payment-methods/link.svg?asset'; -import WooIcon from 'assets/images/payment-methods/woo.svg?asset'; +import WooPayExpressCheckoutItem from './woopay-item'; +import AppleGooglePayExpressCheckoutItem from './apple-google-pay-item'; +import LinkExpressCheckoutItem from './link-item'; const ExpressCheckout = () => { - const [ - isPaymentRequestEnabled, - updateIsPaymentRequestEnabled, - ] = usePaymentRequestEnabledSettings(); - - const [ - isWooPayEnabled, - updateIsWooPayEnabled, - ] = useWooPayEnabledSettings(); - - const availablePaymentMethodIds = useGetAvailablePaymentMethodIds(); - - const [ - enabledMethodIds, - updateEnabledMethodIds, - ] = useEnabledPaymentMethodIds(); - - const updateStripeLinkCheckout = ( isEnabled ) => { - //this handles the link payment method checkbox. If it's enable we should add link to the rest of the - //enabled payment method. - // If false - we should remove link payment method from the enabled payment methods - if ( isEnabled ) { - updateEnabledMethodIds( [ - ...new Set( [ ...enabledMethodIds, 'link' ] ), - ] ); - } else { - updateEnabledMethodIds( [ - ...enabledMethodIds.filter( ( id ) => 'link' !== id ), - ] ); - } - }; - - const displayLinkPaymentMethod = - enabledMethodIds.includes( 'card' ) && - availablePaymentMethodIds.includes( 'link' ); - const isStripeLinkEnabled = enabledMethodIds.includes( 'link' ); - - const { - featureFlags: { woopay: isWooPayFeatureFlagEnabled }, - } = useContext( WCPaySettingsContext ); - return ( diff --git a/client/settings/express-checkout/interfaces.ts b/client/settings/express-checkout/interfaces.ts new file mode 100644 index 00000000000..bf32c983829 --- /dev/null +++ b/client/settings/express-checkout/interfaces.ts @@ -0,0 +1,15 @@ +/** + * Interface exports + */ + +export type PaymentRequestEnabledSettingsHook = [ + boolean, + ( value: boolean ) => void +]; + +export type EnabledMethodIdsHook = [ + Array< string >, + ( value: Array< string > ) => void +]; + +export type WooPayEnabledSettingsHook = [ boolean, ( value: boolean ) => void ]; diff --git a/client/settings/express-checkout/link-item.tsx b/client/settings/express-checkout/link-item.tsx new file mode 100644 index 00000000000..fcfdb41a554 --- /dev/null +++ b/client/settings/express-checkout/link-item.tsx @@ -0,0 +1,186 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { CheckboxControl, VisuallyHidden } from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; + +/** + * Internal dependencies + */ +import { + useEnabledPaymentMethodIds, + useGetAvailablePaymentMethodIds, + useWooPayEnabledSettings, +} from 'wcpay/data'; +import './style.scss'; +import { HoverTooltip } from 'components/tooltip'; +import LinkIcon from 'assets/images/payment-methods/link.svg?asset'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import { EnabledMethodIdsHook } from './interfaces'; + +const LinkExpressCheckoutItem = (): React.ReactElement => { + const availablePaymentMethodIds = useGetAvailablePaymentMethodIds() as Array< + string + >; + + const [ isWooPayEnabled ] = useWooPayEnabledSettings(); + + const [ + enabledMethodIds, + updateEnabledMethodIds, + ] = useEnabledPaymentMethodIds() as EnabledMethodIdsHook; + + const updateStripeLinkCheckout = ( isEnabled: boolean ) => { + //this handles the link payment method checkbox. If it's enable we should add link to the rest of the + //enabled payment method. + // If false - we should remove link payment method from the enabled payment methods + if ( isEnabled ) { + updateEnabledMethodIds( [ + ...new Set( [ ...enabledMethodIds, 'link' ] ), + ] ); + } else { + updateEnabledMethodIds( [ + ...enabledMethodIds.filter( ( id ) => id !== 'link' ), + ] ); + } + }; + + const displayLinkPaymentMethod = + enabledMethodIds.includes( 'card' ) && + availablePaymentMethodIds.includes( 'link' ); + const isStripeLinkEnabled = enabledMethodIds.includes( 'link' ); + + return ( + <> + { displayLinkPaymentMethod && ( + + ) } + + ); +}; + +export default LinkExpressCheckoutItem; diff --git a/client/settings/express-checkout/style.scss b/client/settings/express-checkout/style.scss index 30ce663b297..8116bc0be7e 100644 --- a/client/settings/express-checkout/style.scss +++ b/client/settings/express-checkout/style.scss @@ -5,23 +5,51 @@ .express-checkouts-list { .express-checkout { - display: flex; margin: 0; - padding: 16px 24px 14px 24px; + padding: 24px; background: #fff; - flex-wrap: wrap; - justify-content: flex-start; - align-items: center; - @include breakpoint( '>660px' ) { - padding: 24px 24px 24px 24px; + .gridicons-notice-outline { + fill: #f0b849; + margin-bottom: -5px; + margin-right: 16px; + } + + &__label-container { + display: flex; + flex-wrap: wrap; + } + + &__text-container { + display: flex; + + @include breakpoint( '<660px' ) { + flex-direction: column; + } + } + + &__row { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; flex-wrap: nowrap; } + &__notice { + margin-top: 16px; + } + &__description { color: #757575; font-size: 12px; padding: 1px; + margin-right: 17px; + line-height: 18px; + + @include breakpoint( '<660px' ) { + margin-top: 10px; + } } &:not( :last-child ) { @@ -40,11 +68,15 @@ .components-base-control__field { margin: 0 4px 0 0; } + + @include breakpoint( '<660px' ) { + align-self: baseline; + margin-top: 3px; + } } &__icon { border-radius: 2px; - display: none; flex: 0 0 63.69px; height: 40px; margin: 1px 17px 1px 1px; // 1px to accommodate for box-shadow @@ -53,6 +85,15 @@ @include breakpoint( '>660px' ) { display: flex; } + + @include breakpoint( '<660px' ) { + height: 30px; + flex: 0; + margin: 0 10px 0 0; + img { + height: 30px; + } + } } &__icons { @@ -78,17 +119,38 @@ margin-bottom: 4px; } + &__label-mobile { + display: none; + @include breakpoint( '<660px' ) { + display: block; + align-self: flex-end; + } + } + + &__label-desktop { + display: none; + @include breakpoint( '>660px' ) { + display: block; + } + } + &__link { - margin-left: auto; padding: 12px; border: 1px solid #007cba; border-radius: 2px; font-size: 12px; + height: 18px; + align-self: center; a { text-decoration: none; white-space: nowrap; } + + @include breakpoint( '<660px' ) { + align-self: flex-start; + margin-top: 20px; + } } &__subgroup { display: flex; @@ -96,6 +158,11 @@ &:not( :last-child ) { margin-bottom: 1.4em; } + + @include breakpoint( '<660px' ) { + align-items: flex-start; + flex-wrap: wrap; + } } } } diff --git a/client/settings/express-checkout/test/index.test.js b/client/settings/express-checkout/test/index.test.js index b087e1b9778..19c0b38660d 100644 --- a/client/settings/express-checkout/test/index.test.js +++ b/client/settings/express-checkout/test/index.test.js @@ -15,6 +15,7 @@ import { useGetAvailablePaymentMethodIds, usePaymentRequestEnabledSettings, useWooPayEnabledSettings, + useWooPayShowIncompatibilityNotice, } from 'wcpay/data'; import WCPaySettingsContext from '../../wcpay-settings-context'; @@ -23,6 +24,7 @@ jest.mock( 'wcpay/data', () => ( { useWooPayEnabledSettings: jest.fn(), useEnabledPaymentMethodIds: jest.fn(), useGetAvailablePaymentMethodIds: jest.fn(), + useWooPayShowIncompatibilityNotice: jest.fn(), } ) ); const getMockPaymentRequestEnabledSettings = ( @@ -43,6 +45,8 @@ describe( 'ExpressCheckout', () => { useWooPayEnabledSettings.mockReturnValue( getMockWooPayEnabledSettings( false, jest.fn() ) ); + + useWooPayShowIncompatibilityNotice.mockReturnValue( false ); } ); it( 'should dispatch enabled status update if express checkout is being toggled', async () => { @@ -186,4 +190,28 @@ describe( 'ExpressCheckout', () => { ).not.toBeInTheDocument(); expect( screen.getByLabelText( 'Link by Stripe' ) ).toBeChecked(); } ); + + it( 'should show incompatibility warning', async () => { + const updateIsWooPayEnabledHandler = jest.fn(); + useWooPayEnabledSettings.mockReturnValue( + getMockWooPayEnabledSettings( false, updateIsWooPayEnabledHandler ) + ); + const context = { featureFlags: { woopay: true } }; + useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); + useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); + + useWooPayShowIncompatibilityNotice.mockReturnValue( true ); + + render( + + + + ); + + expect( + screen.queryByText( + 'One or more of your extensions are incompatible with WooPay.' + ) + ).toBeInTheDocument(); + } ); } ); diff --git a/client/settings/express-checkout/woopay-item.tsx b/client/settings/express-checkout/woopay-item.tsx new file mode 100644 index 00000000000..d0fbc404101 --- /dev/null +++ b/client/settings/express-checkout/woopay-item.tsx @@ -0,0 +1,187 @@ +/** + * External dependencies + */ + +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { CheckboxControl, VisuallyHidden } from '@wordpress/components'; +import WooIcon from 'assets/images/payment-methods/woo.svg?asset'; +import interpolateComponents from '@automattic/interpolate-components'; +import { getPaymentMethodSettingsUrl } from '../../utils'; +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { HoverTooltip } from 'components/tooltip'; +import { + useEnabledPaymentMethodIds, + useWooPayEnabledSettings, + useWooPayShowIncompatibilityNotice, +} from 'wcpay/data'; +import WCPaySettingsContext from '../wcpay-settings-context'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import WooPayIncompatibilityNotice from '../settings-warnings/incompatibility-notice'; + +import { WooPayEnabledSettingsHook } from './interfaces'; + +const WooPayExpressCheckoutItem = (): React.ReactElement => { + const [ enabledMethodIds ] = useEnabledPaymentMethodIds() as Array< + string + >; + + const [ + isWooPayEnabled, + updateIsWooPayEnabled, + ] = useWooPayEnabledSettings() as WooPayEnabledSettingsHook; + + const showIncompatibilityNotice = useWooPayShowIncompatibilityNotice(); + + const isStripeLinkEnabled = enabledMethodIds.includes( 'link' ); + + const { + featureFlags: { woopay: isWooPayFeatureFlagEnabled }, + } = useContext( WCPaySettingsContext ); + + return ( + <> + { isWooPayFeatureFlagEnabled && ( +
  • +
    +
    + { isStripeLinkEnabled ? ( + +
    + +
    + + { __( + 'WooPay cannot be enabled at checkout. Click to expand.', + 'woocommerce-payments' + ) } + +
    +
    +
    + ) : ( + + ) } +
    +
    +
    +
    +
    + WooPay +
    +
    + { __( + 'WooPay', + 'woocommerce-payments' + ) } +
    +
    +
    + { __( + 'WooPay', + 'woocommerce-payments' + ) } +
    +
    + { + /* eslint-disable jsx-a11y/anchor-has-content */ + isWooPayEnabled + ? __( + 'Boost conversion and customer loyalty by' + + ' offering a single click, secure way to pay.', + 'woocommerce-payments' + ) + : interpolateComponents( { + mixedString: __( + /* eslint-disable-next-line max-len */ + 'Boost conversion and customer loyalty by offering a single click, secure way to pay. ' + + 'In order to use {{wooPayLink}}WooPay{{/wooPayLink}},' + + ' you must agree to our ' + + '{{tosLink}}WooCommerce Terms of Service{{/tosLink}} ' + + 'and {{privacyLink}}Privacy Policy{{/privacyLink}}. ' + + '{{trackingLink}}Click here{{/trackingLink}} to learn more about the ' + + 'data you will be sharing and opt-out options.', + 'woocommerce-payments' + ), + components: { + wooPayLink: ( + + ), + tosLink: ( + + ), + privacyLink: ( + + ), + trackingLink: ( + + ), + }, + } ) + /* eslint-enable jsx-a11y/anchor-has-content */ + } +
    +
    +
    +
    + + +
    +
    + { showIncompatibilityNotice && ( + + ) } +
  • + ) } + + ); +}; + +export default WooPayExpressCheckoutItem; diff --git a/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx b/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx index 393d058abc1..e3d1826640b 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx +++ b/client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx @@ -47,7 +47,7 @@ const CVCVerificationRuleCard: React.FC = () => { target="_blank" type="external" // eslint-disable-next-line max-len - href="https://woocommerce.com/document/woocommerce-payments/fraud-and-disputes/fraud-protection/#advanced-configuration" + href="https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/#advanced-configuration" /> ), }, diff --git a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap index 492f9cde67e..2d9cdd41659 100644 --- a/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/cards/test/__snapshots__/cvc-verification.test.tsx.snap @@ -46,7 +46,7 @@ exports[`CVC verification card renders correctly when CVC check is disabled 1`]

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

    @@ -182,14 +182,14 @@ exports[`CVC verification card renders correctly when CVC check is enabled 1`] =
    @@ -133,7 +133,7 @@ exports[`International IP address card renders correctly 1`] = `
    @@ -267,7 +267,7 @@ exports[`International IP address card renders correctly when enabled 1`] = `

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

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

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

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

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

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

    @@ -528,7 +528,7 @@ exports[`Purchase price threshold card renders correctly when enabled and checke
    diff --git a/client/settings/fraud-protection/advanced-settings/index.tsx b/client/settings/fraud-protection/advanced-settings/index.tsx index 4cc6e80cab9..0ddef09a041 100644 --- a/client/settings/fraud-protection/advanced-settings/index.tsx +++ b/client/settings/fraud-protection/advanced-settings/index.tsx @@ -71,7 +71,7 @@ const Breadcrumb = () => ( section: 'woocommerce_payments', } ) } > - { __( 'WooCommerce Payments', 'woocommerce-payments' ) } + { 'WooPayments' }  >  { __( 'Advanced fraud protection', 'woocommerce-payments' ) } @@ -185,17 +185,17 @@ const FraudProtectionAdvancedSettingsPage: React.FC = () => { } }; - // Hack to make "WooCommerce > Settings" the active selected menu item. + // Hack to make "Payments > Settings" the active selected menu item. useEffect( () => { const wcSettingsMenuItem = document.querySelector( - '#toplevel_page_woocommerce a[href="admin.php?page=wc-settings"]' + '#toplevel_page_wc-admin-path--payments-overview a[href$="section=woocommerce_payments"]' ); if ( wcSettingsMenuItem ) { wcSettingsMenuItem.setAttribute( 'aria-current', 'page' ); wcSettingsMenuItem.classList.add( 'current' ); wcSettingsMenuItem.parentElement?.classList.add( 'current' ); } - }, [] ); + }, [ isLoading ] ); // Intersection observer callback for tracking card viewed events. const observerCallback = ( entries: IntersectionObserverEntry[] ) => { diff --git a/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx b/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx index 2c523993c28..30d2f6db72b 100644 --- a/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx +++ b/client/settings/fraud-protection/advanced-settings/rule-card-notice.tsx @@ -8,7 +8,7 @@ import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; * Internal dependencies */ import './../style.scss'; -import BannerNotice from 'wcpay/components/banner-notice'; +import InlineNotice from 'components/inline-notice'; import { TipIcon } from 'wcpay/icons'; const supportedTypes = [ 'error', 'warning', 'info' ] as const; @@ -31,7 +31,7 @@ const FraudProtectionRuleCardNotice: React.FC< FraudProtectionRuleCardNoticeProp const icon = 'info' === type ? : ; return ( -
    @@ -61,7 +61,7 @@ Object {
    @@ -77,7 +77,7 @@ Object { , "container":
    @@ -107,7 +107,7 @@ Object {
    @@ -205,7 +205,7 @@ Object {
    @@ -234,7 +234,7 @@ Object {
    @@ -253,7 +253,7 @@ Object { , "container":
    @@ -282,7 +282,7 @@ Object {
    @@ -383,7 +383,7 @@ Object {
    @@ -412,7 +412,7 @@ Object {
    @@ -431,7 +431,7 @@ Object { , "container":
    @@ -460,7 +460,7 @@ Object {
    @@ -561,7 +561,7 @@ Object {
    @@ -590,7 +590,7 @@ Object {
    @@ -609,7 +609,7 @@ Object { , "container":
    @@ -638,7 +638,7 @@ Object {
    @@ -739,7 +739,7 @@ Object {
    @@ -768,7 +768,7 @@ Object {
    @@ -787,7 +787,7 @@ Object { , "container":
    @@ -816,7 +816,7 @@ Object {
    diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap index 4243f1a8d98..b6af7f8078a 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap @@ -61,7 +61,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -302,7 +302,7 @@ Object {

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

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

    @@ -972,14 +972,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -1059,7 +1059,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -1300,7 +1300,7 @@ Object {

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

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

    @@ -1970,14 +1970,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -2140,7 +2140,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -2363,7 +2363,7 @@ Object {

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

    @@ -2906,14 +2906,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -2995,7 +2995,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -3218,7 +3218,7 @@ Object {

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

    @@ -3761,14 +3761,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -3913,7 +3913,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -4120,7 +4120,7 @@ Object {

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

    @@ -4663,14 +4663,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -4731,7 +4731,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -4938,7 +4938,7 @@ Object {

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

    @@ -5481,14 +5481,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -5649,7 +5649,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -5856,7 +5856,7 @@ Object {

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

    @@ -6480,14 +6480,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more @@ -6567,7 +6567,7 @@ Object { data-link-type="wp-admin" href="admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments" > - WooCommerce Payments + WooPayments  >  Advanced fraud protection @@ -6774,7 +6774,7 @@ Object {

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

    @@ -7398,14 +7398,14 @@ Object {
    For security, this filter is enabled and cannot be modified. Payments failing CVC verification will be blocked. Learn more diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap index a95aa63bc28..eebe1c2ea74 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/rule-card-notice.test.tsx.snap @@ -31,7 +31,7 @@ Object { />
    @@ -61,7 +61,7 @@ Object {
    @@ -77,7 +77,7 @@ Object { , "container":
    @@ -107,7 +107,7 @@ Object {
    @@ -205,7 +205,7 @@ Object {
    @@ -234,7 +234,7 @@ Object {
    @@ -250,7 +250,7 @@ Object { , "container":
    @@ -279,7 +279,7 @@ Object {
    @@ -377,7 +377,7 @@ Object {
    @@ -407,7 +407,7 @@ Object {
    @@ -423,7 +423,7 @@ Object { , "container":
    @@ -453,7 +453,7 @@ Object {
    diff --git a/client/settings/fraud-protection/components/protection-levels/index.tsx b/client/settings/fraud-protection/components/protection-levels/index.tsx index 0eb2e86f74a..b8597a68832 100644 --- a/client/settings/fraud-protection/components/protection-levels/index.tsx +++ b/client/settings/fraud-protection/components/protection-levels/index.tsx @@ -17,7 +17,7 @@ import { import { FraudProtectionHelpText, BasicFraudProtectionModal } from '../index'; import { getAdminUrl } from 'wcpay/utils'; import { ProtectionLevel } from '../../advanced-settings/constants'; -import InlineNotice from '../../../../components/inline-notice'; +import InlineNotice from 'components/inline-notice'; import wcpayTracks from 'tracks'; import { CurrentProtectionLevelHook } from '../../interfaces'; @@ -57,6 +57,7 @@ const ProtectionLevels: React.FC = () => { <> { 'error' === advancedFraudProtectionSettings && (
    - There was an error retrieving your fraud protection settings. Please refresh the page to try again. +
    +
    + + + + + +
    +
    + There was an error retrieving your fraud protection settings. Please refresh the page to try again. +
    +
    diff --git a/client/settings/fraud-protection/style.scss b/client/settings/fraud-protection/style.scss index 614267a6050..14be0e0899b 100644 --- a/client/settings/fraud-protection/style.scss +++ b/client/settings/fraud-protection/style.scss @@ -249,10 +249,6 @@ border-bottom: 1px solid #e0e0e0; border-top: 0; } - .wcpay-banner-notice.fraud-protection-rule-card-notice { - margin-left: 0; - margin-right: 0; - } } &__help-icon { diff --git a/client/settings/fraud-protection/test/index.test.tsx b/client/settings/fraud-protection/test/index.test.tsx index d5aaca31f48..53863e26efb 100644 --- a/client/settings/fraud-protection/test/index.test.tsx +++ b/client/settings/fraud-protection/test/index.test.tsx @@ -100,6 +100,15 @@ describe( 'FraudProtection', () => { } ); it( 'should render correctly', () => { + Object.defineProperty( window, 'location', { + configurable: true, + enumerable: true, + value: { + search: + '?page=wc-settings&tab=checkout&anchor=%23fp-settings§ion=woocommerce_payments/', + }, + } ); + const { container: fraudProtectionSettings } = render( `; +exports[`FraudProtectionTour should not render the tour component if settings page accessed directly 1`] = ` + +
    + +`; + exports[`FraudProtectionTour should render the tour component correctly 1`] = `
    diff --git a/client/settings/fraud-protection/tour/index.test.tsx b/client/settings/fraud-protection/tour/index.test.tsx index 3b767197cce..4621192f5fb 100644 --- a/client/settings/fraud-protection/tour/index.test.tsx +++ b/client/settings/fraud-protection/tour/index.test.tsx @@ -46,6 +46,15 @@ describe( 'FraudProtectionTour', () => { } ); it( 'should render the tour component correctly', () => { + Reflect.defineProperty( window, 'location', { + configurable: true, + enumerable: true, + value: { + search: + '?page=wc-settings&tab=checkout&anchor=%23fp-settings§ion=woocommerce_payments/', + }, + } ); + const { baseElement } = render( ); expect( baseElement ).toMatchSnapshot(); @@ -62,4 +71,20 @@ describe( 'FraudProtectionTour', () => { expect( baseElement ).toMatchSnapshot(); } ); + + it( 'should not render the tour component if settings page accessed directly', () => { + Reflect.deleteProperty( window, 'location' ); + Reflect.defineProperty( window, 'location', { + configurable: true, + enumerable: true, + value: { + search: + '?page=wc-settings&tab=checkout§ion=woocommerce_payments/', + }, + } ); + + const { baseElement } = render( ); + + expect( baseElement ).toMatchSnapshot(); + } ); } ); diff --git a/client/settings/fraud-protection/tour/index.tsx b/client/settings/fraud-protection/tour/index.tsx index 3169d04d7ce..bd9dad348ad 100644 --- a/client/settings/fraud-protection/tour/index.tsx +++ b/client/settings/fraud-protection/tour/index.tsx @@ -45,15 +45,19 @@ const options = { const FraudProtectionTour: React.FC = () => { const { isWelcomeTourDismissed } = wcpaySettings.fraudProtection; + const searchParams = new URLSearchParams( window.location.search ); + const anchorParam = searchParams.get( 'anchor' ); + const isTourParam = '#fp-settings' === anchorParam; + const { isLoading } = useSettings(); const { updateOptions } = useDispatch( 'wc/admin/options' ); const [ showTour, setShowTour ] = useState( false ); useEffect( () => { - if ( ! isLoading && ! isWelcomeTourDismissed ) { + if ( ! isLoading && ! isWelcomeTourDismissed && isTourParam ) { setShowTour( true ); } - }, [ isLoading, isWelcomeTourDismissed ] ); + }, [ isLoading, isWelcomeTourDismissed, isTourParam ] ); const handleTourEnd = ( stepList: any[], diff --git a/client/settings/general-settings/index.js b/client/settings/general-settings/index.js index 69823bf0f33..e9f554620ea 100644 --- a/client/settings/general-settings/index.js +++ b/client/settings/general-settings/index.js @@ -2,7 +2,7 @@ * External dependencies */ import React from 'react'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Card, CheckboxControl } from '@wordpress/components'; import interpolateComponents from '@automattic/interpolate-components'; @@ -23,13 +23,18 @@ const GeneralSettings = () => {

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

    @@ -58,7 +63,8 @@ const GeneralSettings = () => { ), learnMoreLink: ( @@ -66,7 +72,7 @@ const GeneralSettings = () => { ), }, diff --git a/client/settings/general-settings/test/general-settings.test.js b/client/settings/general-settings/test/general-settings.test.js index b862d33e95b..93f27d56b2a 100644 --- a/client/settings/general-settings/test/general-settings.test.js +++ b/client/settings/general-settings/test/general-settings.test.js @@ -26,7 +26,7 @@ describe( 'GeneralSettings', () => { render( ); expect( - screen.queryByText( 'Enable WooCommerce Payments' ) + screen.queryByText( 'Enable WooPayments' ) ).toBeInTheDocument(); expect( screen.queryByText( 'Enable test mode' ) ).toBeInTheDocument(); } ); @@ -39,7 +39,7 @@ describe( 'GeneralSettings', () => { render( ); const enableWCPayCheckbox = screen.getByLabelText( - 'Enable WooCommerce Payments' + 'Enable WooPayments' ); let expectation = expect( enableWCPayCheckbox ); @@ -62,7 +62,7 @@ describe( 'GeneralSettings', () => { render( ); const enableWCPayCheckbox = screen.getByLabelText( - 'Enable WooCommerce Payments' + 'Enable WooPayments' ); fireEvent.click( enableWCPayCheckbox ); diff --git a/client/settings/payment-method-icon/index.js b/client/settings/payment-method-icon/index.js index 17dc417b4d2..cbce74a0892 100644 --- a/client/settings/payment-method-icon/index.js +++ b/client/settings/payment-method-icon/index.js @@ -24,7 +24,7 @@ const PaymentMethodIcon = ( { name, showName } ) => { diff --git a/client/settings/phone-input/index.tsx b/client/settings/phone-input/index.tsx index cd99042411a..75c1442d713 100644 --- a/client/settings/phone-input/index.tsx +++ b/client/settings/phone-input/index.tsx @@ -70,14 +70,33 @@ const PhoneNumberInput = ( { } }; + let phoneCountries = { + initialCountry: 'US', + onlyCountries: [], + }; + + //if in admin panel + if ( 'undefined' !== typeof wcpaySettings ) { + const accountCountry = wcpaySettings?.accountStatus?.country ?? ''; + // Special case for Japan: Only Japanese phone numbers are accepted by Stripe + if ( accountCountry === 'JP' ) { + phoneCountries = { + initialCountry: 'JP', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + onlyCountries: [ 'JP' ], + }; + } + } + if ( currentRef ) { iti = intlTelInput( currentRef, { - initialCountry: 'US', customPlaceholder: () => '', separateDialCode: true, hiddenInput: 'full', utilsScript: utils, dropdownContainer: document.body, + ...phoneCountries, } ); setInputInstance( iti ); diff --git a/client/settings/phone-input/test/phone-input.test.js b/client/settings/phone-input/test/phone-input.test.js index 712b98dfdfa..e6aee7481fc 100644 --- a/client/settings/phone-input/test/phone-input.test.js +++ b/client/settings/phone-input/test/phone-input.test.js @@ -13,6 +13,14 @@ describe( 'PhoneNumberInput', () => { const handlePhoneNumberChangeMock = jest.fn(); const handlePhoneValidationChangeMock = jest.fn(); + beforeEach( () => { + window.wcpaySettings = { + accountStatus: { + country: 'US', + }, + }; + } ); + it( 'should render phone number input', () => { render( { ] = useState( null ); if ( - null === initialIsPaymentRequestEnabled && + initialIsPaymentRequestEnabled === null && settings && - 'undefined' !== typeof settings.is_payment_request_enabled + typeof settings.is_payment_request_enabled !== 'undefined' ) { setInitialIsPaymentRequestEnabled( settings.is_payment_request_enabled diff --git a/client/settings/settings-manager/index.js b/client/settings/settings-manager/index.js index 7478a1bd8c5..9f8c05172d2 100644 --- a/client/settings/settings-manager/index.js +++ b/client/settings/settings-manager/index.js @@ -52,7 +52,7 @@ const ExpressCheckoutDescription = () => ( 'woocommerce-payments' ) }

    - + { __( 'Learn more', 'woocommerce-payments' ) } @@ -62,9 +62,13 @@ const GeneralSettingsDescription = () => ( <>

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

    - { __( - 'Enable or disable WooCommerce Payments on your store and turn on test mode to simulate transactions.', - 'woocommerce-payments' + { sprintf( + /* translators: %s: WooPayments */ + __( + 'Enable or disable %s on your store and turn on test mode to simulate transactions.', + 'woocommerce-payments' + ), + 'WooPayments' ) }

    @@ -79,8 +83,8 @@ const TransactionsDescription = () => ( 'woocommerce-payments' ) }

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

    - + { __( 'Learn more about pending schedules', 'woocommerce-payments' @@ -116,11 +120,11 @@ const FraudProtectionDescription = () => {

    { __( 'Fraud protection', 'woocommerce-payments' ) }

    { __( - 'Help avoid chargebacks by setting your security and fraud protection risk level.', + 'Help avoid unauthorized transactions and disputes by setting your fraud protection level.', 'woocommerce-payments' ) }

    - + { __( 'Learn more about risk filtering', 'woocommerce-payments' @@ -130,6 +134,23 @@ const FraudProtectionDescription = () => { ); }; +const AdvancedDescription = () => { + return ( + <> +

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

    +

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

    + + { __( 'View our documentation', 'woocommerce-payments' ) } + + + ); +}; + const SettingsManager = () => { const { featureFlags: { @@ -248,7 +269,16 @@ const SettingsManager = () => { - + + + + + + + ); diff --git a/client/settings/settings-warnings/incompatibility-notice.js b/client/settings/settings-warnings/incompatibility-notice.js new file mode 100644 index 00000000000..8bcd9d677f4 --- /dev/null +++ b/client/settings/settings-warnings/incompatibility-notice.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import interpolateComponents from '@automattic/interpolate-components'; + +/** + * Internal dependencies + */ +import { Notice } from '@wordpress/components'; +import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; +import './style.scss'; + +const WooPayIncompatibilityNotice = () => ( + + + + + + { __( + 'One or more of your extensions are incompatible with WooPay.', + 'woocommerce-payments' + ) } +
    + { interpolateComponents( { + mixedString: __( + '{{learnMoreLink}}Learn More{{/learnMoreLink}}', + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content +
    + ), + }, + } ) } + + +); + +export default WooPayIncompatibilityNotice; diff --git a/client/settings/settings-warnings/style.scss b/client/settings/settings-warnings/style.scss new file mode 100644 index 00000000000..e401acb1e5e --- /dev/null +++ b/client/settings/settings-warnings/style.scss @@ -0,0 +1,5 @@ +.components-notice { + &__content { + display: flex; + } +} diff --git a/client/settings/support-email-input/index.js b/client/settings/support-email-input/index.js index 7f84e6f6994..a5661deded6 100644 --- a/client/settings/support-email-input/index.js +++ b/client/settings/support-email-input/index.js @@ -17,7 +17,7 @@ const SupportEmailInput = ( { setInputVallid } ) => { ?.account_business_support_email?.message; const currentEmail = useRef( supportEmail ).current; - if ( '' === supportEmail && '' !== currentEmail ) { + if ( supportEmail === '' && currentEmail !== '' ) { supportEmailError = __( 'Support email cannot be empty once it has been set before, please specify.', 'woocommerce-payments' diff --git a/client/settings/support-phone-input/index.js b/client/settings/support-phone-input/index.js index 0d6ae7b3a63..73b5a6d6321 100644 --- a/client/settings/support-phone-input/index.js +++ b/client/settings/support-phone-input/index.js @@ -18,7 +18,7 @@ const SupportPhoneInput = ( { setInputVallid } ) => { ?.account_business_support_phone?.message; const currentPhone = useRef( supportPhone ).current; - const isEmptyPhoneValid = '' === supportPhone && '' === currentPhone; + const isEmptyPhoneValid = supportPhone === '' && currentPhone === ''; const [ isPhoneValid, setPhoneValidity ] = useState( true ); if ( ! isPhoneValid && ! isEmptyPhoneValid ) { @@ -28,7 +28,7 @@ const SupportPhoneInput = ( { setInputVallid } ) => { ); } - if ( '' === supportPhone && '' !== currentPhone ) { + if ( supportPhone === '' && currentPhone !== '' ) { supportPhoneError = __( 'Support phone number cannot be empty once it has been set before, please specify.', 'woocommerce-payments' diff --git a/client/settings/support-phone-input/test/support-phone-input.test.js b/client/settings/support-phone-input/test/support-phone-input.test.js index e02adbb951a..5820a88da2b 100644 --- a/client/settings/support-phone-input/test/support-phone-input.test.js +++ b/client/settings/support-phone-input/test/support-phone-input.test.js @@ -21,6 +21,11 @@ describe( 'SupportPhoneInput', () => { jest.fn(), ] ); useGetSavingError.mockReturnValue( null ); + window.wcpaySettings = { + accountStatus: { + country: 'US', + }, + }; } ); it( 'updates phone input', async () => { diff --git a/client/settings/survey-modal/index.js b/client/settings/survey-modal/index.js index f75c8337904..45bfe42194a 100644 --- a/client/settings/survey-modal/index.js +++ b/client/settings/survey-modal/index.js @@ -2,7 +2,7 @@ * External dependencies */ import React, { useContext, useEffect } from 'react'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { dispatch } from '@wordpress/data'; import { Button, RadioControl, TextareaControl } from '@wordpress/components'; @@ -14,8 +14,8 @@ import ConfirmationModal from 'components/confirmation-modal'; import useIsUpeEnabled from 'settings/wcpay-upe-toggle/hook'; import { wcPaySurveys } from './questions'; import WcPaySurveyContext from './context'; -import InlineNotice from '../../components/inline-notice'; -import { LoadableBlock } from '../../components/loadable'; +import InlineNotice from 'components/inline-notice'; +import { LoadableBlock } from 'components/loadable'; const SurveyModalBody = ( { options, surveyQuestion } ) => { const [ isUpeEnabled ] = useIsUpeEnabled(); @@ -26,7 +26,7 @@ const SurveyModalBody = ( { options, surveyQuestion } ) => { return ( <> { ! isUpeEnabled && ( - + { __( "You've disabled the new payments experience in your store.", 'woocommerce-payments' @@ -94,9 +94,13 @@ const SurveyModalBody = ( { options, surveyQuestion } ) => { ) }

    - { __( - 'Feedback will be sent anonymously to the WooCommerce Payments development team.', - 'woocommerce-payments' + { sprintf( + /* translators: %s: WooPayments */ + __( + 'Feedback will be sent anonymously to the %s development team.', + 'woocommerce-payments' + ), + 'WooPayments' ) }

    @@ -149,7 +153,7 @@ const SurveyModal = ( { setOpenModal, surveyKey, surveyQuestion } ) => { useEffect( () => { if ( ! surveyKey || ! surveyQuestion ) { surveyCannotBeLoadedNotice(); - } else if ( 'error' === status ) { + } else if ( status === 'error' ) { submissionErrorNotice(); } else if ( isSurveySubmitted ) { surveySubmittedConfirmation(); @@ -157,7 +161,7 @@ const SurveyModal = ( { setOpenModal, surveyKey, surveyQuestion } ) => { } }, [ status, isSurveySubmitted, surveyKey, surveyQuestion, setOpenModal ] ); - if ( 1 > optionsArray ) return null; + if ( optionsArray < 1 ) return null; return ( <> @@ -172,15 +176,15 @@ const SurveyModal = ( { setOpenModal, surveyKey, surveyQuestion } ) => { <> - ) } +
    +
    @@ -31,7 +31,7 @@ exports[`ToS modal should render ToS modal 1`] = `
    @@ -103,7 +103,7 @@ exports[`ToS modal should render disable plugin modal on decline 1`] = ` > Terms of Service - , you’ll no longer be able to capture credit card payments using WooCommerce Payments. Your previous transaction and deposit data will still be available. + , you’ll no longer be able to capture credit card payments using WooPayments. Your previous transaction and deposit data will still be available.