From 737f579ec04f585226a0a0797ed2607e3ccd1104 Mon Sep 17 00:00:00 2001 From: Samir Merchant Date: Sun, 18 Aug 2024 21:07:06 -0400 Subject: [PATCH 01/44] Updates test mode instructions copy for card (#9272) Co-authored-by: Piero Rocca <109928953+pierorocca@users.noreply.github.com> Co-authored-by: Francesco --- assets/images/icons/copy.svg | 3 +++ ...fix-checkout-floating-labels-and-test-mode-copy | 4 ++++ client/checkout/blocks/style.scss | 14 ++++++++++++++ client/checkout/classic/style.scss | 14 ++++++++++++++ includes/class-wc-payments-checkout.php | 6 ++++-- .../payment-methods/class-cc-payment-method.php | 2 +- tests/unit/test-class-wc-payments-checkout.php | 2 +- 7 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 assets/images/icons/copy.svg create mode 100644 changelog/fix-checkout-floating-labels-and-test-mode-copy diff --git a/assets/images/icons/copy.svg b/assets/images/icons/copy.svg new file mode 100644 index 00000000000..65e9350a00e --- /dev/null +++ b/assets/images/icons/copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/changelog/fix-checkout-floating-labels-and-test-mode-copy b/changelog/fix-checkout-floating-labels-and-test-mode-copy new file mode 100644 index 00000000000..862a62bf389 --- /dev/null +++ b/changelog/fix-checkout-floating-labels-and-test-mode-copy @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Updates test mode instructions copy for cards at checkout. diff --git a/client/checkout/blocks/style.scss b/client/checkout/blocks/style.scss index df244b56a4f..2329dba9f91 100644 --- a/client/checkout/blocks/style.scss +++ b/client/checkout/blocks/style.scss @@ -110,5 +110,19 @@ button.wcpay-stripelink-modal-trigger:hover { } } +button.copy-icon { + display: inline-block; + height: 1.2em; + width: 1.2em; + line-height: 1.2em; + vertical-align: middle; + margin-left: -0.1em; + padding: 0; + border-radius: 0; + border: none !important; + background: url( 'assets/images/icons/copy.svg?asset' ); + background-size: cover; +} + @import '../woopay/style'; @import '../../components/loadable/style'; diff --git a/client/checkout/classic/style.scss b/client/checkout/classic/style.scss index 1b373153434..ef73dda2bb3 100644 --- a/client/checkout/classic/style.scss +++ b/client/checkout/classic/style.scss @@ -92,3 +92,17 @@ li.wc_payment_method:has( .input-radio:checked } } } + +button.copy-icon { + display: inline-block; + height: 1.2em; + width: 1.2em; + line-height: 1.2em; + vertical-align: middle; + margin-left: -0.1em; + padding: 0; + border-radius: 0; + border: none !important; + background: url( 'assets/images/icons/copy.svg?asset' ); + background-size: cover; +} diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index f2fcbcf5a23..3dcd55f570b 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -331,8 +331,9 @@ public function get_enabled_payment_method_config() { /* translators: link to Stripe testing page */ $payment_method->get_testing_instructions(), [ - 'strong' => '', 'a' => '', + 'button' => ' or refer to our testing guide.', 'woocommerce-payments' ); } } diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php index 34175b8dc70..8df6f307575 100644 --- a/tests/unit/test-class-wc-payments-checkout.php +++ b/tests/unit/test-class-wc-payments-checkout.php @@ -383,7 +383,7 @@ public function test_link_payment_method_provided_when_card_enabled() { 'darkIcon' => $dark_icon_url, 'showSaveOption' => true, 'countries' => [], - 'testingInstructions' => 'Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', + 'testingInstructions' => 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', 'forceNetworkSavedCards' => false, ], 'link' => [ From edfd339ccfb5233615d59f921658aa7315c5348f Mon Sep 17 00:00:00 2001 From: Francesco Date: Mon, 19 Aug 2024 12:33:27 +0200 Subject: [PATCH 02/44] fix: contrast ratio for link within tooltip (#9273) Co-authored-by: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> --- changelog/fix-tooltip-link-contrast-ratio | 5 ++ client/components/tooltip/style.scss | 3 +- client/utils/account-fees.tsx | 15 +++--- .../test/__snapshots__/account-fees.tsx.snap | 48 ++++++++++++++++++- 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 changelog/fix-tooltip-link-contrast-ratio diff --git a/changelog/fix-tooltip-link-contrast-ratio b/changelog/fix-tooltip-link-contrast-ratio new file mode 100644 index 00000000000..69007b6db19 --- /dev/null +++ b/changelog/fix-tooltip-link-contrast-ratio @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: fix: tooltip link color contrast ratio + + diff --git a/client/components/tooltip/style.scss b/client/components/tooltip/style.scss index 7e5d589c51a..51422218e10 100644 --- a/client/components/tooltip/style.scss +++ b/client/components/tooltip/style.scss @@ -56,7 +56,8 @@ text-align: center; a { - color: var( --wp-admin-theme-color, $gutenberg-blue ); + color: var( --wp-admin-theme-color-background-25, $wp-blue-5 ); + text-decoration: underline; } ul { diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 86ca513984d..b3d0f5b9f2c 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { BaseFee, DiscountFee, FeeStructure } from 'wcpay/types/fees'; import { PaymentMethod } from 'wcpay/types/payment-methods'; import { createInterpolateElement } from '@wordpress/element'; +import { ExternalLink } from '@wordpress/components'; const countryFeeStripeDocsBaseLink = 'https://woocommerce.com/document/woopayments/fees-and-debits/fees/#'; @@ -180,7 +181,7 @@ export const formatMethodFeesTooltip = ( { wcpaySettings && wcpaySettings.connect && wcpaySettings.connect.country ? ( -
+
{ stripeFeeSectionExistsForCountry( wcpaySettings.connect.country @@ -196,19 +197,17 @@ export const formatMethodFeesTooltip = ( ), components: { linkToStripePage: ( - { __( 'Learn more', 'woocommerce-payments' ) } - + ), }, } ) @@ -223,18 +222,16 @@ export const formatMethodFeesTooltip = ( ), components: { linkToStripePage: ( - { __( 'Learn more', 'woocommerce-payments' ) } - + ), }, } ) } diff --git a/client/utils/test/__snapshots__/account-fees.tsx.snap b/client/utils/test/__snapshots__/account-fees.tsx.snap index 2f4ff3a3fde..89321bc7582 100644 --- a/client/utils/test/__snapshots__/account-fees.tsx.snap +++ b/client/utils/test/__snapshots__/account-fees.tsx.snap @@ -44,11 +44,33 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base > Learn more + + (opens in a new tab) + + about WooPayments Fees in your country @@ -101,11 +123,33 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base > Learn more + + (opens in a new tab) + + about WooPayments Fees in your country From f5b4adac2d3828c46b36405f54ad4d7394dee2a4 Mon Sep 17 00:00:00 2001 From: Francesco Date: Mon, 19 Aug 2024 13:44:11 +0200 Subject: [PATCH 03/44] fix: platform_global_theme_support_enabled undefined index (#9280) --- changelog/fix-undefined-platform-global-theme-index-notice | 4 ++++ includes/class-wc-payments-features.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-undefined-platform-global-theme-index-notice diff --git a/changelog/fix-undefined-platform-global-theme-index-notice b/changelog/fix-undefined-platform-global-theme-index-notice new file mode 100644 index 00000000000..49befece76f --- /dev/null +++ b/changelog/fix-undefined-platform-global-theme-index-notice @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: platform_global_theme_support_enabled undefined index diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 97edcf32615..161cd8e3935 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -268,7 +268,8 @@ public static function is_woopay_direct_checkout_enabled() { */ public static function is_woopay_global_theme_support_eligible() { $account_cache = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); - return is_array( $account_cache ) && $account_cache['platform_global_theme_support_enabled'] ?? false; + + return is_array( $account_cache ) && ( $account_cache['platform_global_theme_support_enabled'] ?? false ); } /** From a56d57a0cc393e74fa141059adffc1194cf1a13e Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Mon, 19 Aug 2024 12:12:52 -0300 Subject: [PATCH 04/44] Fix tracks identity cookie cache (#9249) --- changelog/fix-wcpay-tracking-cookie-cache | 4 ++ client/frontend-tracks/index.js | 12 +++++ client/payment-methods-map.tsx | 4 ++ client/tracks/index.ts | 1 + includes/class-woopay-tracker.php | 59 +++++++++++++++++++++-- webpack/shared.js | 1 + 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-wcpay-tracking-cookie-cache create mode 100644 client/frontend-tracks/index.js diff --git a/changelog/fix-wcpay-tracking-cookie-cache b/changelog/fix-wcpay-tracking-cookie-cache new file mode 100644 index 00000000000..e0f40c40a19 --- /dev/null +++ b/changelog/fix-wcpay-tracking-cookie-cache @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix caching with tracking cookie. diff --git a/client/frontend-tracks/index.js b/client/frontend-tracks/index.js new file mode 100644 index 00000000000..33d13c3321b --- /dev/null +++ b/client/frontend-tracks/index.js @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { recordUserEvent } from 'tracks'; + +if ( window.wcPayFrontendTracks && window.wcPayFrontendTracks.length ) { + for ( const track of window.wcPayFrontendTracks ) { + recordUserEvent( track.event, track.properties ); + } + + window.wcPayFrontendTracks = []; +} diff --git a/client/payment-methods-map.tsx b/client/payment-methods-map.tsx index 10b96f5d527..1f5e1a9a9cf 100644 --- a/client/payment-methods-map.tsx +++ b/client/payment-methods-map.tsx @@ -31,6 +31,10 @@ declare global { country: string; }; }; + wcPayFrontendTracks: { + event: string; + properties: Record< string, unknown >; + }; } } diff --git a/client/tracks/index.ts b/client/tracks/index.ts index d3f14dd57b3..5648493c46a 100644 --- a/client/tracks/index.ts +++ b/client/tracks/index.ts @@ -125,6 +125,7 @@ export const getTracksIdentity = async (): Promise< string | undefined > => { body.append( 'tracksNonce', nonce ); body.append( 'action', 'get_identity' ); + try { const response = await fetch( ajaxUrl, { method: 'post', diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index 75ca5ecf3bc..22c5a82057c 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -73,6 +73,7 @@ public function __construct( $http ) { add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 ); add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 ); add_action( 'woocommerce_payments_save_user_in_woopay', [ $this, 'must_save_payment_method_to_platform' ] ); + add_action( 'wp_footer', [ $this, 'add_frontend_tracks_scripts' ] ); add_action( 'before_woocommerce_pay_form', [ $this, 'pay_for_order_page_view' ] ); add_action( 'woocommerce_thankyou', [ $this, 'thank_you_page_view' ] ); } @@ -144,8 +145,9 @@ public function maybe_record_event( $event, $data = [] ) { * * @param string $event name of the event. * @param array $data array of event properties. + * @param bool $record_on_frontend whether to record the event on the frontend to prevent cache break. */ - public function maybe_record_wcpay_shopper_event( $event, $data = [] ) { + public function maybe_record_wcpay_shopper_event( $event, $data = [], $record_on_frontend = true ) { // Top level events should not be namespaced. if ( '_aliasUser' !== $event ) { $event = self::$user_prefix . '_' . $event; @@ -154,7 +156,23 @@ public function maybe_record_wcpay_shopper_event( $event, $data = [] ) { $is_admin_event = false; $track_on_all_stores = true; - return $this->tracks_record_event( $event, $data, $is_admin_event, $track_on_all_stores ); + if ( ! $record_on_frontend ) { + return $this->tracks_record_event( $event, $data, $is_admin_event, $track_on_all_stores ); + } + + $data['record_event_data'] = compact( 'is_admin_event', 'track_on_all_stores' ); + + add_filter( + 'wcpay_frontend_tracks', + function ( $tracks ) use ( $event, $data ) { + $tracks[] = [ + 'event' => $event, + 'properties' => $data, + ]; + + return $tracks; + } + ); } /** @@ -275,6 +293,18 @@ public function tracks_record_event( $event_name, $properties = [], $is_admin_ev return false; } + if ( isset( $properties['record_event_data'] ) ) { + if ( isset( $properties['record_event_data']['is_admin_event'] ) ) { + $is_admin_event = $properties['record_event_data']['is_admin_event']; + } + + if ( isset( $properties['record_event_data']['track_on_all_stores'] ) ) { + $track_on_all_stores = $properties['record_event_data']['track_on_all_stores']; + } + + unset( $properties['record_event_data'] ); + } + if ( ! $this->should_enable_tracking( $is_admin_event, $track_on_all_stores ) ) { return false; } @@ -519,7 +549,7 @@ public function checkout_order_processed( $order_id ) { // Don't track WooPay orders. They will be tracked on WooPay side with more details. if ( ! $is_woopay_order ) { - $this->maybe_record_wcpay_shopper_event( 'checkout_order_placed', $properties ); + $this->maybe_record_wcpay_shopper_event( 'checkout_order_placed', $properties, false ); } // If the order was placed using a different payment gateway, just increment a counter. } else { @@ -576,4 +606,27 @@ public function woopay_locations_updated( $all_locations, $platform_checkout_ena $this->maybe_record_admin_event( 'woopay_express_button_locations_updated', $props ); } + + /** + * Add front-end tracks scripts to prevent cache break. + * + * @return void + */ + public function add_frontend_tracks_scripts() { + $frontent_tracks = apply_filters( 'wcpay_frontend_tracks', [] ); + + if ( count( $frontent_tracks ) === 0 ) { + return; + } + + WC_Payments::register_script_with_dependencies( 'wcpay-frontend-tracks', 'dist/frontend-tracks' ); + + wp_enqueue_script( 'wcpay-frontend-tracks' ); + + wp_localize_script( + 'wcpay-frontend-tracks', + 'wcPayFrontendTracks', + $frontent_tracks + ); + } } diff --git a/webpack/shared.js b/webpack/shared.js index 953830ba44f..7ef039967cd 100644 --- a/webpack/shared.js +++ b/webpack/shared.js @@ -41,6 +41,7 @@ module.exports = { 'product-details': './client/product-details/index.js', 'cart-block': './client/cart/blocks/index.js', 'plugins-page': './client/plugins-page/index.js', + 'frontend-tracks': './client/frontend-tracks/index.js', }, // Override webpack public path dynamically on every entry. // Required for chunks loading to work on sites with JS concatenation. From d09d0314ff2db2872f0499a708ab7d774f65157d Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Tue, 20 Aug 2024 11:19:00 -0300 Subject: [PATCH 05/44] Fix QIT error `OutputNotEscaped` (#9289) --- changelog/fix-9288-fix-qit-error-escape-output | 3 +++ ...ass-wc-payments-express-checkout-button-display-handler.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-9288-fix-qit-error-escape-output diff --git a/changelog/fix-9288-fix-qit-error-escape-output b/changelog/fix-9288-fix-qit-error-escape-output new file mode 100644 index 00000000000..b2df1c53c3e --- /dev/null +++ b/changelog/fix-9288-fix-qit-error-escape-output @@ -0,0 +1,3 @@ +Significance: patch +Type: fix +Comment: Fix QIT error "EscapeOutput.OutputNotEscaped" diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php index e10b9f17bff..a76f60ec1fb 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php @@ -120,7 +120,7 @@ public function display_express_checkout_separator_if_necessary( $separator_star $html_id = WC_Payments_Features::is_stripe_ece_enabled() ? 'wcpay-express-checkout-button-separator' : 'wcpay-payment-request-button-separator'; if ( $this->express_checkout_helper->is_checkout() ) { ?> -

+

Date: Tue, 20 Aug 2024 10:53:55 -0500 Subject: [PATCH 06/44] Update cache after persisting the User session (#9277) Co-authored-by: Brian Borman <68524302+bborman22@users.noreply.github.com> --- changelog/as-fix-woopay-data-mismatch | 4 ++++ .../woopay/class-woopay-store-api-session-handler.php | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelog/as-fix-woopay-data-mismatch diff --git a/changelog/as-fix-woopay-data-mismatch b/changelog/as-fix-woopay-data-mismatch new file mode 100644 index 00000000000..705d1dbbbd8 --- /dev/null +++ b/changelog/as-fix-woopay-data-mismatch @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Update cache after persisting the User session via WooPay diff --git a/includes/woopay/class-woopay-store-api-session-handler.php b/includes/woopay/class-woopay-store-api-session-handler.php index 444a20adf07..30fd10219a2 100644 --- a/includes/woopay/class-woopay-store-api-session-handler.php +++ b/includes/woopay/class-woopay-store-api-session-handler.php @@ -113,6 +113,15 @@ public function get_session( $customer_id, $default = false ) { return maybe_unserialize( $value ); } + /** + * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. + * + * @return string + */ + private function get_cache_prefix() { + return \WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); + } + /** * Save data and delete user session. */ @@ -129,7 +138,7 @@ public function save_data() { $this->session_expiration ) ); - + wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->session_expiration - time() ); $this->_dirty = false; } } From 6943a98223fc560782972750d8b42ca25ad402a2 Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Tue, 20 Aug 2024 17:23:20 -0300 Subject: [PATCH 07/44] Fix E2E errors (#9292) --- changelog/fix-fix-e2e-errors | 5 +++++ client/tracks/index.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-fix-e2e-errors diff --git a/changelog/fix-fix-e2e-errors b/changelog/fix-fix-e2e-errors new file mode 100644 index 00000000000..092fca8d978 --- /dev/null +++ b/changelog/fix-fix-e2e-errors @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Simple bug fix. + + diff --git a/client/tracks/index.ts b/client/tracks/index.ts index 5648493c46a..5fd10f9c5e8 100644 --- a/client/tracks/index.ts +++ b/client/tracks/index.ts @@ -81,7 +81,7 @@ export const recordUserEvent = ( fetch( ajaxUrl, { method: 'post', body, - } ); + } ).then( ( response ) => response.json() ); }; /** From d70a50f2c992331ef3df5326dc968a89ff828258 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:51:14 +1000 Subject: [PATCH 08/44] E2E: Fix the dispute save draft challenge failing due to redirect race condition (#9284) --- changelog/fix-9281-e2e-dispute-save-challenge | 5 ++++ ...hant-disputes-save-draft-challenge.spec.js | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 changelog/fix-9281-e2e-dispute-save-challenge diff --git a/changelog/fix-9281-e2e-dispute-save-challenge b/changelog/fix-9281-e2e-dispute-save-challenge new file mode 100644 index 00000000000..d858c32e0f3 --- /dev/null +++ b/changelog/fix-9281-e2e-dispute-save-challenge @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Not a user-facing change: fix e2e test save draft dispute challenge failing due to disputes list redirect race condition + + diff --git a/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js index 6a2224d2d58..5ff98fe002f 100644 --- a/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js +++ b/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js @@ -11,9 +11,10 @@ const { merchant, shopper, evalAndClick } = require( '@woocommerce/e2e-utils' ); import { fillCardDetails, setupProductCheckout } from '../../../utils/payments'; import { uiLoaded } from '../../../utils'; -let orderId; - describe( 'Disputes > Merchant can save and resume draft dispute challenge', () => { + let orderId; + let paymentDetailsLink; + beforeAll( async () => { await page.goto( config.get( 'url' ), { waitUntil: 'networkidle0' } ); @@ -36,7 +37,7 @@ describe( 'Disputes > Merchant can save and resume draft dispute challenge', () await merchant.goToOrder( orderId ); // Get the payment details link from the order page. - const paymentDetailsLink = await page.$eval( + paymentDetailsLink = await page.$eval( 'p.order_number > a', ( anchor ) => anchor.getAttribute( 'href' ) ); @@ -107,11 +108,24 @@ describe( 'Disputes > Merchant can save and resume draft dispute challenge', () } ); - // Reload the page - await page.reload(); - + // The merchant will be redirected to the dispute list page here, wait for it to load. await uiLoaded(); + // Open the payment details page again and wait for it to load. + await Promise.all( [ + page.goto( paymentDetailsLink, { + waitUntil: 'networkidle0', + } ), + uiLoaded(), + ] ); + + // Click the challenge dispute button. + await evalAndClick( '[data-testid="challenge-dispute-button"]' ); + await Promise.all( [ + page.waitForNavigation( { waitUntil: 'networkidle0' } ), + uiLoaded(), + ] ); + // Verify the previously selected Product type was saved await expect( page ).toMatchElement( '[data-testid="dispute-challenge-product-type-selector"]', From 007356ab179880100873b412b86051f702c20a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Tue, 20 Aug 2024 23:31:54 -0300 Subject: [PATCH 09/44] Migrate multi-currency e2e tests to Playwright (#9175) --- ...-8750-migrate-mccy-e2e-tests-to-playwright | 4 + ...tton-when-no-currencies-are-selected-1.png | Bin 0 -> 73924 bytes ...olocation-correctly-with-USD-and-GBP-1.png | Bin 0 -> 31634 bytes ...olocation-correctly-with-USD-and-GBP-2.png | Bin 0 -> 173484 bytes ...urrency-page-load-without-any-errors-1.png | Bin 0 -> 60986 bytes ...utes-view-details-via-order-notice.spec.ts | 4 +- .../multi-currency-on-boarding.spec.ts | 223 +++++++++++ .../merchant/multi-currency-setup.spec.ts | 222 +++++++++++ .../specs/merchant/multi-currency.spec.ts | 68 ++++ .../shopper/multi-currency-checkout.spec.ts | 100 +++++ tests/e2e-pw/utils/merchant-navigation.ts | 49 +++ tests/e2e-pw/utils/merchant.ts | 213 ++++++++++- tests/e2e-pw/utils/shopper-navigation.ts | 41 ++ tests/e2e-pw/utils/shopper.ts | 180 ++++++++- ...t-admin-multi-currency-on-boarding.spec.js | 362 ------------------ ...erchant-admin-multi-currency-setup.spec.js | 207 ---------- .../merchant-admin-multi-currency.spec.js | 61 --- .../shopper-checkout-multi-currency.spec.js | 147 ------- 18 files changed, 1089 insertions(+), 792 deletions(-) create mode 100644 changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright create mode 100644 tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Currency-selection--7b0d9-submit-button-when-no-currencies-are-selected-1.png create mode 100644 tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-83665-tch-by-geolocation-correctly-with-USD-and-GBP-1.png create mode 100644 tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-d8568-tch-by-geolocation-correctly-with-USD-and-GBP-2.png create mode 100644 tests/e2e-pw/specs/merchant/__snapshots__/multi-currency.spec.ts/Multi-currency-page-load-without-any-errors-1.png create mode 100644 tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts create mode 100644 tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts create mode 100644 tests/e2e-pw/specs/merchant/multi-currency.spec.ts create mode 100644 tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts create mode 100644 tests/e2e-pw/utils/merchant-navigation.ts create mode 100644 tests/e2e-pw/utils/shopper-navigation.ts delete mode 100644 tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js delete mode 100644 tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js delete mode 100644 tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js delete mode 100644 tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js diff --git a/changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright b/changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright new file mode 100644 index 00000000000..6d010298e91 --- /dev/null +++ b/changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate multi-currency e2e tests to Playwright. diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Currency-selection--7b0d9-submit-button-when-no-currencies-are-selected-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Currency-selection--7b0d9-submit-button-when-no-currencies-are-selected-1.png new file mode 100644 index 0000000000000000000000000000000000000000..fddb7c0a37edc0019f57403ebf57ecf36d332cbc GIT binary patch literal 73924 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 M=x!^M3g;Q8g=EE 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

y`J!7OnX2ZgOMcXQ#VQ+tAeApO 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

%UG559X+e@NMYHkySBsQ`;rF}bKu35q%Z)F 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

S<3ADd-{Xe4KGAzpX>l*%{l!|l= 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

oP9#^Xyv{uuIQPHm#5%JxQ#3OEnhwo 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 View dispute details via disputed order notice', @@ -44,7 +44,7 @@ test.describe( browser, } ) => { const { merchantPage } = await getMerchant( browser ); - await merchant.goToOrder( merchantPage, orderId ); + await goToOrder( merchantPage, orderId ); // If WC < 7.9, return early since the order dispute notice is not present. const orderPaymentDetailsContainerVisible = await merchantPage diff --git a/tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts b/tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts new file mode 100644 index 00000000000..f987ae71d8e --- /dev/null +++ b/tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts @@ -0,0 +1,223 @@ +/** + * External dependencies + */ +import { test, expect, Page } from '@playwright/test'; +/** + * Internal dependencies + */ +import { useMerchant } from '../../utils/helpers'; +import { + activateMulticurrency, + activateTheme, + addCurrency, + deactivateMulticurrency, + disableAllEnabledCurrencies, + getActiveThemeSlug, + removeCurrency, +} from '../../utils/merchant'; +import * as navigation from '../../utils/merchant-navigation'; + +test.describe( 'Multi-currency on-boarding', () => { + let page: Page; + let wasMulticurrencyEnabled: boolean; + let activeThemeSlug: string; + const goToNextOnboardingStep = async ( currentPage: Page ) => { + await currentPage + .locator( '.wcpay-wizard-task.is-active button.is-primary' ) + .click(); + }; + + useMerchant(); + + test.beforeAll( async ( { browser } ) => { + page = await browser.newPage(); + wasMulticurrencyEnabled = await activateMulticurrency( page ); + activeThemeSlug = await getActiveThemeSlug( page ); + } ); + + test.afterAll( async () => { + // Restore original theme. + await activateTheme( page, activeThemeSlug ); + + if ( ! wasMulticurrencyEnabled ) { + await deactivateMulticurrency( page ); + } + + await page.close(); + } ); + + test.describe( 'Currency selection and management', () => { + test.beforeAll( async () => { + await disableAllEnabledCurrencies( page ); + } ); + + test.beforeEach( async () => { + await navigation.goToMultiCurrencyOnboarding( page ); + } ); + + test( 'should disable the submit button when no currencies are selected', async () => { + // To take a better screenshot of the component. + await page.setViewportSize( { width: 1280, height: 2000 } ); + await expect( + page.locator( + '.multi-currency-setup-wizard > div > .components-card-body' + ) + ).toHaveScreenshot(); + // Set the viewport back to the default size. + await page.setViewportSize( { width: 1280, height: 720 } ); + + const checkboxes = await page + .locator( + 'li.enabled-currency-checkbox .components-checkbox-control__input' + ) + .all(); + + for ( const checkbox of checkboxes ) { + await checkbox.uncheck(); + } + + await expect( + page.getByRole( 'button', { name: 'Add currencies' } ) + ).toBeDisabled(); + } ); + + test( 'should allow multiple currencies to be selected', async () => { + const currenciesNotInRecommendedList = await page + .locator( + 'li.enabled-currency-checkbox:not([data-testid="recommended-currency"]) input[type="checkbox"]' + ) + .all(); + + // We don't need to check them all. + const maximumCurrencies = + currenciesNotInRecommendedList.length > 3 + ? 3 + : currenciesNotInRecommendedList.length; + + for ( let i = 0; i < maximumCurrencies; i++ ) { + await expect( + currenciesNotInRecommendedList[ i ] + ).toBeEnabled(); + await currenciesNotInRecommendedList[ i ].check(); + await expect( + currenciesNotInRecommendedList[ i ] + ).toBeChecked(); + } + } ); + + test( 'should exclude already enabled currencies from the onboarding', async () => { + await addCurrency( page, 'GBP' ); + await navigation.goToMultiCurrencyOnboarding( page ); + + const recommendedCurrencies = await page + .getByTestId( 'recommended-currency' ) + .allTextContents(); + + for ( const currency of recommendedCurrencies ) { + expect( currency ).not.toMatch( /GBP/ ); + } + + await removeCurrency( page, 'GBP' ); + } ); + + test( 'should display suggested currencies at the beginning of the list', async () => { + await expect( + ( await page.getByTestId( 'recommended-currency' ).all() ) + .length + ).toBeGreaterThan( 0 ); + } ); + + test( 'selected currencies are enabled after onboarding', async () => { + const currencyCodes = [ 'GBP', 'EUR', 'CAD', 'AUD' ]; + + for ( const currencyCode of currencyCodes ) { + await page + .locator( + `input[type="checkbox"][code="${ currencyCode }"]` + ) + .check(); + } + + await goToNextOnboardingStep( page ); + await navigation.goToMultiCurrencySettings( page ); + + // Ensure the currencies are enabled. + for ( const currencyCode of currencyCodes ) { + await expect( + page.locator( + `li.enabled-currency.${ currencyCode.toLowerCase() }` + ) + ).toBeVisible(); + } + } ); + } ); + + test.describe( 'Geolocation features', () => { + test( 'should offer currency switch by geolocation', async () => { + await navigation.goToMultiCurrencyOnboarding( page ); + await goToNextOnboardingStep( page ); + await page.getByTestId( 'enable_auto_currency' ).check(); + await expect( + page.getByTestId( 'enable_auto_currency' ) + ).toBeChecked(); + } ); + + test( 'should preview currency switch by geolocation correctly with USD and GBP', async () => { + await addCurrency( page, 'GBP' ); + await navigation.goToMultiCurrencyOnboarding( page ); + // To take a better screenshot of the iframe preview. + await page.setViewportSize( { width: 1280, height: 1280 } ); + await goToNextOnboardingStep( page ); + await expect( + page.locator( '.wcpay-wizard-task.is-active' ) + ).toHaveScreenshot(); + await page.getByTestId( 'enable_auto_currency' ).check(); + await page.getByRole( 'button', { name: 'Preview' } ).click(); + + const previewIframe = await page.locator( + '.multi-currency-store-settings-preview-iframe' + ); + + await expect( previewIframe ).toBeVisible(); + + const previewPage = previewIframe.contentFrame(); + + await expect( + await previewPage.locator( '.woocommerce-store-notice' ) + ).toBeVisible(); + await expect( + page.locator( '.multi-currency-store-settings-preview-iframe' ) + ).toHaveScreenshot(); + + const noticeText = await previewPage + .locator( '.woocommerce-store-notice' ) + .innerText(); + + expect( noticeText ).toContain( + "We noticed you're visiting from United Kingdom (UK). We've updated our prices to Pound sterling for your shopping convenience." + ); + } ); + } ); + + test.describe( 'Currency Switcher widget', () => { + test( 'should offer the currency switcher widget while Storefront theme is active', async () => { + await activateTheme( page, 'storefront' ); + await navigation.goToMultiCurrencyOnboarding( page ); + await goToNextOnboardingStep( page ); + await page.getByTestId( 'enable_storefront_switcher' ).check(); + await expect( + page.getByTestId( 'enable_storefront_switcher' ) + ).toBeChecked(); + } ); + + test( 'should not offer the currency switcher widget when an unsupported theme is active', async () => { + await activateTheme( page, 'twentytwentyfour' ); + await navigation.goToMultiCurrencyOnboarding( page ); + await goToNextOnboardingStep( page ); + await expect( + page.getByTestId( 'enable_storefront_switcher' ) + ).toBeHidden(); + await activateTheme( page, 'storefront' ); + } ); + } ); +} ); diff --git a/tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts b/tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts new file mode 100644 index 00000000000..515100ab2dc --- /dev/null +++ b/tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts @@ -0,0 +1,222 @@ +/** + * External dependencies + */ +import { test, expect, Page } from '@playwright/test'; +/** + * Internal dependencies + */ +import { getMerchant, getShopper } from '../../utils/helpers'; +import { + activateMulticurrency, + addCurrency, + deactivateMulticurrency, + disableAllEnabledCurrencies, + removeCurrency, + setCurrencyCharmPricing, + setCurrencyPriceRounding, + setCurrencyRate, +} from '../../utils/merchant'; +import * as navigation from '../../utils/shopper-navigation'; +import { getPriceFromProduct } from '../../utils/shopper'; + +test.describe( 'Multi-currency setup', () => { + let merchantPage: Page; + let shopperPage: Page; + let wasMulticurrencyEnabled: boolean; + + test.beforeAll( async ( { browser } ) => { + shopperPage = ( await getShopper( browser ) ).shopperPage; + merchantPage = ( await getMerchant( browser ) ).merchantPage; + wasMulticurrencyEnabled = await activateMulticurrency( merchantPage ); + } ); + + test.afterAll( async () => { + if ( ! wasMulticurrencyEnabled ) { + await deactivateMulticurrency( merchantPage ); + } + } ); + + test( 'can disable the multi-currency feature', async () => { + await deactivateMulticurrency( merchantPage ); + } ); + + test( 'can enable the multi-currency feature', async () => { + await activateMulticurrency( merchantPage ); + } ); + + test.describe( 'Currency management', () => { + const testCurrency = 'CHF'; + + test( 'can add a new currency', async () => { + await addCurrency( merchantPage, testCurrency ); + } ); + + test( 'can remove a currency', async () => { + await removeCurrency( merchantPage, testCurrency ); + } ); + } ); + + test.describe( 'Currency settings', () => { + let beanieRegularPrice: string; + const testData = { + currencyCode: 'CHF', + rate: '1.25', + charmPricing: '-0.01', + rounding: '0.5', + currencyPrecision: 2, + }; + + test.beforeAll( async () => { + await disableAllEnabledCurrencies( merchantPage ); + await navigation.goToShopWithCurrency( shopperPage, 'USD' ); + + beanieRegularPrice = await getPriceFromProduct( + shopperPage, + 'beanie' + ); + } ); + + test.beforeEach( async () => { + await addCurrency( merchantPage, testData.currencyCode ); + } ); + + test.afterEach( async () => { + await removeCurrency( merchantPage, testData.currencyCode ); + } ); + + test( 'can change the currency rate manually', async () => { + await setCurrencyRate( + merchantPage, + testData.currencyCode, + testData.rate + ); + await setCurrencyPriceRounding( + merchantPage, + testData.currencyCode, + '0' + ); + await navigation.goToShopWithCurrency( + shopperPage, + testData.currencyCode + ); + + const beaniePriceOnCurrency = await getPriceFromProduct( + shopperPage, + 'beanie' + ); + + expect( + parseFloat( beaniePriceOnCurrency ).toFixed( + testData.currencyPrecision + ) + ).toEqual( + ( + parseFloat( beanieRegularPrice ) * + parseFloat( testData.rate ) + ).toFixed( testData.currencyPrecision ) + ); + } ); + + test( 'can change the charm price manually', async () => { + await setCurrencyRate( + merchantPage, + testData.currencyCode, + '1.00' + ); + await setCurrencyPriceRounding( + merchantPage, + testData.currencyCode, + '0' + ); + await setCurrencyCharmPricing( + merchantPage, + testData.currencyCode, + testData.charmPricing + ); + await navigation.goToShopWithCurrency( + shopperPage, + testData.currencyCode + ); + + const beaniePriceOnCurrency = await getPriceFromProduct( + shopperPage, + 'beanie' + ); + + expect( + parseFloat( beaniePriceOnCurrency ).toFixed( + testData.currencyPrecision + ) + ).toEqual( + ( + parseFloat( beanieRegularPrice ) + + parseFloat( testData.charmPricing ) + ).toFixed( testData.currencyPrecision ) + ); + } ); + + test( 'can change the rounding precision manually', async () => { + const rateForTest = '1.20'; + + await setCurrencyRate( + merchantPage, + testData.currencyCode, + rateForTest + ); + await setCurrencyPriceRounding( + merchantPage, + testData.currencyCode, + testData.rounding + ); + + const beaniePriceOnCurrency = await getPriceFromProduct( + shopperPage, + 'beanie' + ); + + expect( + parseFloat( beaniePriceOnCurrency ).toFixed( + testData.currencyPrecision + ) + ).toEqual( + ( + Math.ceil( + parseFloat( beanieRegularPrice ) * + parseFloat( rateForTest ) * + ( 1 / parseFloat( testData.rounding ) ) + ) * parseFloat( testData.rounding ) + ).toFixed( testData.currencyPrecision ) + ); + } ); + } ); + + test.describe( 'Currency decimal points', () => { + const currencyDecimalMap = { + JPY: 0, + GBP: 2, + }; + + test.beforeAll( async () => { + for ( const currency of Object.keys( currencyDecimalMap ) ) { + await addCurrency( merchantPage, currency ); + } + } ); + + Object.keys( currencyDecimalMap ).forEach( ( currency: string ) => { + test( `the decimal points for ${ currency } are displayed correctly`, async () => { + await navigation.goToShopWithCurrency( shopperPage, currency ); + + const beaniePriceOnCurrency = await getPriceFromProduct( + shopperPage, + 'beanie' + ); + const decimalPart = + beaniePriceOnCurrency.split( '.' )[ 1 ] || ''; + + expect( decimalPart.length ).toEqual( + currencyDecimalMap[ currency ] + ); + } ); + } ); + } ); +} ); diff --git a/tests/e2e-pw/specs/merchant/multi-currency.spec.ts b/tests/e2e-pw/specs/merchant/multi-currency.spec.ts new file mode 100644 index 00000000000..45b6bf0b89b --- /dev/null +++ b/tests/e2e-pw/specs/merchant/multi-currency.spec.ts @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import { test, expect, Page } from '@playwright/test'; +/** + * Internal dependencies + */ +import { useMerchant } from '../../utils/helpers'; +import { + activateMulticurrency, + addMulticurrencyWidget, + deactivateMulticurrency, + disableAllEnabledCurrencies, +} from '../../utils/merchant'; +import * as navigation from '../../utils/merchant-navigation'; + +test.describe( 'Multi-currency', () => { + let wasMulticurrencyEnabled: boolean; + let page: Page; + + // Use the merchant user for this test suite. + useMerchant(); + + test.beforeAll( async ( { browser } ) => { + page = await browser.newPage(); + wasMulticurrencyEnabled = await activateMulticurrency( page ); + + await disableAllEnabledCurrencies( page ); + } ); + + test.afterAll( async () => { + if ( ! wasMulticurrencyEnabled ) { + await deactivateMulticurrency( page ); + } + await page.close(); + } ); + + test( 'page load without any errors', async () => { + await navigation.goToMultiCurrencySettings( page ); + await expect( + page.getByRole( 'heading', { name: 'Enabled currencies' } ) + ).toBeVisible(); + await expect( page.getByText( 'Default currency' ) ).toBeVisible(); + await expect( + page.locator( '.multi-currency-settings' ).last() + ).toHaveScreenshot(); + } ); + + test( 'add the currency switcher to the sidebar', async () => { + await addMulticurrencyWidget( page ); + } ); + + test( 'can add the currency switcher to a post/page', async () => { + await navigation.goToNewPost( page ); + + if ( await page.getByRole( 'button', { name: 'Close' } ).isVisible() ) { + await page.getByRole( 'button', { name: 'Close' } ).click(); + } + + await page.getByRole( 'button', { name: 'Add block' } ).click(); + await page + .locator( 'input[placeholder="Search"]' ) + .pressSequentially( 'switcher', { delay: 20 } ); + await expect( + page.getByRole( 'option', { name: 'Currency Switcher Block' } ) + ).toBeVisible(); + } ); +} ); diff --git a/tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts b/tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts new file mode 100644 index 00000000000..fa5453b70e4 --- /dev/null +++ b/tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import { test, expect, Page } from '@playwright/test'; +/** + * Internal dependencies + */ +import { getMerchant, getShopper } from '../../utils/helpers'; +import { + activateMulticurrency, + addCurrency, + deactivateMulticurrency, + removeCurrency, +} from '../../utils/merchant'; +import { emptyCart, placeOrderWithCurrency } from '../../utils/shopper'; +import * as navigation from '../../utils/shopper-navigation'; + +test.describe( 'Multi-currency checkout', () => { + let merchantPage: Page; + let shopperPage: Page; + let wasMulticurrencyEnabled: boolean; + const currenciesOrders = { + USD: null, + EUR: null, + }; + + test.beforeAll( async ( { browser } ) => { + shopperPage = ( await getShopper( browser ) ).shopperPage; + merchantPage = ( await getMerchant( browser ) ).merchantPage; + wasMulticurrencyEnabled = await activateMulticurrency( merchantPage ); + + await addCurrency( merchantPage, 'EUR' ); + } ); + + test.afterAll( async () => { + await removeCurrency( merchantPage, 'EUR' ); + await emptyCart( shopperPage ); + + if ( ! wasMulticurrencyEnabled ) { + await deactivateMulticurrency( merchantPage ); + } + } ); + + test.describe( `Checkout with multiple currencies`, async () => { + Object.keys( currenciesOrders ).forEach( ( currency: string ) => { + test( `checkout with ${ currency }`, async () => { + await test.step( `pay with ${ currency }`, async () => { + currenciesOrders[ currency ] = await placeOrderWithCurrency( + shopperPage, + currency + ); + } ); + + await test.step( + `should display ${ currency } in the order received page`, + async () => { + await expect( + shopperPage.locator( + '.woocommerce-order-overview__total' + ) + ).toHaveText( new RegExp( currency ) ); + } + ); + + await test.step( + `should display ${ currency } in the customer order page`, + async () => { + await navigation.goToOrder( + shopperPage, + currenciesOrders[ currency ] + ); + await expect( + shopperPage.locator( + '.woocommerce-table--order-details tfoot tr:last-child td' + ) + ).toHaveText( new RegExp( currency ) ); + } + ); + } ); + } ); + } ); + + test.describe( 'My account', () => { + test( 'should display the correct currency in the my account order history table', async () => { + await navigation.goToOrders( shopperPage ); + + for ( const currency in currenciesOrders ) { + if ( currenciesOrders[ currency ] ) { + await expect( + shopperPage.locator( 'tr' ).filter( { + has: shopperPage.getByText( + `#${ currenciesOrders[ currency ] }` + ), + } ) + ).toHaveText( new RegExp( currency ) ); + } + } + } ); + } ); +} ); diff --git a/tests/e2e-pw/utils/merchant-navigation.ts b/tests/e2e-pw/utils/merchant-navigation.ts new file mode 100644 index 00000000000..b91c31ba097 --- /dev/null +++ b/tests/e2e-pw/utils/merchant-navigation.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { Page } from 'playwright/test'; +import { dataHasLoaded } from './merchant'; + +export const goToOrder = async ( page: Page, orderId: string ) => { + await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` ); +}; + +export const goToWooPaymentsSettings = async ( page: Page ) => { + await page.goto( + '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments' + ); +}; + +export const goToMultiCurrencySettings = async ( page: Page ) => { + await page.goto( + '/wp-admin/admin.php?page=wc-settings&tab=wcpay_multi_currency', + { waitUntil: 'load' } + ); + await dataHasLoaded( page ); +}; + +export const goToWidgets = async ( page: Page ) => { + await page.goto( '/wp-admin/widgets.php', { + waitUntil: 'load', + } ); +}; + +export const goToNewPost = async ( page: Page ) => { + await page.goto( '/wp-admin/post-new.php', { + waitUntil: 'load', + } ); +}; + +export const goToThemes = async ( page: Page ) => { + await page.goto( '/wp-admin/themes.php', { + waitUntil: 'load', + } ); +}; + +export const goToMultiCurrencyOnboarding = async ( page: Page ) => { + await page.goto( + '/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Fmulti-currency-setup', + { waitUntil: 'load' } + ); + await dataHasLoaded( page ); +}; diff --git a/tests/e2e-pw/utils/merchant.ts b/tests/e2e-pw/utils/merchant.ts index 0f358e24491..c70045e9a0d 100644 --- a/tests/e2e-pw/utils/merchant.ts +++ b/tests/e2e-pw/utils/merchant.ts @@ -1,11 +1,214 @@ /** * External dependencies */ -import { Page } from 'playwright/test'; +import { Page, expect } from 'playwright/test'; +import * as navigation from './merchant-navigation'; -export const goToOrder = async ( +/** + * Checks if the data has loaded on the page. + * This check only applies to WooPayments settings pages. + * + * @param {Page} page The page object. + */ +export const dataHasLoaded = async ( page: Page ) => { + await expect( page.locator( '.is-loadable-placeholder' ) ).toHaveCount( 0 ); +}; + +export const saveWooPaymentsSettings = async ( page: Page ) => { + await page.getByRole( 'button', { name: 'Save changes' } ).click(); + await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( { + timeout: 10000, + } ); +}; + +export const activateMulticurrency = async ( page: Page ) => { + await navigation.goToWooPaymentsSettings( page ); + + const checkboxTestId = 'multi-currency-toggle'; + const wasInitiallyEnabled = await page + .getByTestId( checkboxTestId ) + .isChecked(); + + if ( ! wasInitiallyEnabled ) { + await page.getByTestId( checkboxTestId ).check(); + await saveWooPaymentsSettings( page ); + } + return wasInitiallyEnabled; +}; + +export const deactivateMulticurrency = async ( page: Page ) => { + await navigation.goToWooPaymentsSettings( page ); + await page.getByTestId( 'multi-currency-toggle' ).uncheck(); + await saveWooPaymentsSettings( page ); +}; + +export const addMulticurrencyWidget = async ( page: Page ) => { + await navigation.goToWidgets( page ); + // Wait for all widgets to load. This is important to prevent flakiness. + await expect( page.locator( '.components-spinner' ) ).toHaveCount( 0 ); + + if ( await page.getByRole( 'button', { name: 'Close' } ).isVisible() ) { + await page.getByRole( 'button', { name: 'Close' } ).click(); + } + + const isWidgetAdded = await page + .getByRole( 'heading', { name: 'Currency Switcher Widget' } ) + .isVisible(); + + if ( ! isWidgetAdded ) { + await page.getByRole( 'button', { name: 'Add block' } ).click(); + await page + .locator( 'input[placeholder="Search"]' ) + .pressSequentially( 'switcher', { delay: 20 } ); + await expect( + page.locator( 'button.components-button[role="option"]' ).first() + ).toBeVisible( { timeout: 5000 } ); + await page + .locator( 'button.components-button[role="option"]' ) + .first() + .click(); + await page.waitForTimeout( 2000 ); + await expect( + page.getByRole( 'button', { name: 'Update' } ) + ).toBeEnabled(); + await page.getByRole( 'button', { name: 'Update' } ).click(); + await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( { + timeout: 10000, + } ); + } +}; + +export const getActiveThemeSlug = async ( page: Page ) => { + await navigation.goToThemes( page ); + + const activeTheme = await page.locator( '.theme.active' ); + + return ( await activeTheme.getAttribute( 'data-slug' ) ) ?? ''; +}; + +export const activateTheme = async ( page: Page, slug: string ) => { + await navigation.goToThemes( page ); + + const isThemeActive = ( await getActiveThemeSlug( page ) ) === slug; + + if ( ! isThemeActive ) { + await page + .locator( `.theme[data-slug="${ slug }"] .button.activate` ) + .click(); + await expect( + await page.locator( '.notice.updated' ).innerText() + ).toContain( 'New theme activated.' ); + } +}; + +export const disableAllEnabledCurrencies = async ( page: Page ) => { + await navigation.goToMultiCurrencySettings( page ); + await expect( + await page.locator( '.enabled-currencies-list li' ).first() + ).toBeVisible(); + + const deleteButtons = await page + .locator( '.enabled-currency .enabled-currency__action.delete' ) + .all(); + + if ( deleteButtons.length === 0 ) { + return; + } + + for ( let i = 0; i < deleteButtons.length; i++ ) { + await page + .locator( '.enabled-currency .enabled-currency__action.delete' ) + .first() + .click(); + + const snackbar = await page.getByLabel( 'Dismiss this notice' ); + + await expect( snackbar ).toBeVisible( { timeout: 10000 } ); + await snackbar.click(); + await expect( snackbar ).toBeHidden( { timeout: 10000 } ); + } +}; + +export const addCurrency = async ( page: Page, currencyCode: string ) => { + // Default currency. + if ( currencyCode === 'USD' ) { + return; + } + + await navigation.goToMultiCurrencySettings( page ); + await page.getByTestId( 'enabled-currencies-add-button' ).click(); + + const checkbox = await page.locator( + `input[type="checkbox"][code="${ currencyCode }"]` + ); + + if ( ! ( await checkbox.isChecked() ) ) { + await checkbox.check(); + } + + await page.getByRole( 'button', { name: 'Update selected' } ).click(); + await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( { + timeout: 10000, + } ); + await expect( + page.locator( `li.enabled-currency.${ currencyCode.toLowerCase() }` ) + ).toBeVisible(); +}; + +export const removeCurrency = async ( page: Page, currencyCode: string ) => { + await navigation.goToMultiCurrencySettings( page ); + await page + .locator( + `li.enabled-currency.${ currencyCode.toLowerCase() } .enabled-currency__action.delete` + ) + .click(); + await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( { + timeout: 10000, + } ); + await expect( + page.locator( `li.enabled-currency.${ currencyCode.toLowerCase() }` ) + ).toBeHidden(); +}; + +export const editCurrency = async ( page: Page, currencyCode: string ) => { + await navigation.goToMultiCurrencySettings( page ); + await page + .locator( + `.enabled-currency.${ currencyCode.toLowerCase() } .enabled-currency__action.edit` + ) + .click(); + await dataHasLoaded( page ); +}; + +export const setCurrencyRate = async ( + page: Page, + currencyCode: string, + rate: string +) => { + await editCurrency( page, currencyCode ); + await page + .locator( '#single-currency-settings__manual_rate_radio' ) + .click(); + await page.getByTestId( 'manual_rate_input' ).fill( rate ); + await saveWooPaymentsSettings( page ); +}; + +export const setCurrencyPriceRounding = async ( + page: Page, + currencyCode: string, + rounding: string +) => { + await editCurrency( page, currencyCode ); + await page.getByTestId( 'price_rounding' ).selectOption( rounding ); + await saveWooPaymentsSettings( page ); +}; + +export const setCurrencyCharmPricing = async ( page: Page, - orderId: string -): Promise< void > => { - await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` ); + currencyCode: string, + charmPricing: string +) => { + await editCurrency( page, currencyCode ); + await page.getByTestId( 'price_charm' ).selectOption( charmPricing ); + await saveWooPaymentsSettings( page ); }; diff --git a/tests/e2e-pw/utils/shopper-navigation.ts b/tests/e2e-pw/utils/shopper-navigation.ts new file mode 100644 index 00000000000..04c5f3d7cc5 --- /dev/null +++ b/tests/e2e-pw/utils/shopper-navigation.ts @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { Page } from 'playwright/test'; +/** + * Internal dependencies + */ +import { isUIUnblocked } from './shopper'; + +export const goToShopWithCurrency = async ( page: Page, currency: string ) => { + await page.goto( `/shop/?currency=${ currency }`, { waitUntil: 'load' } ); +}; + +export const goToProductPageBySlug = async ( + page: Page, + productSlug: string +) => { + await page.goto( `/product/${ productSlug }`, { waitUntil: 'load' } ); +}; + +export const goToCart = async ( page: Page ) => { + await page.goto( '/cart/', { waitUntil: 'load' } ); + isUIUnblocked( page ); +}; + +export const goToCheckout = async ( page: Page ) => { + await page.goto( '/checkout/', { waitUntil: 'load' } ); + isUIUnblocked( page ); +}; + +export const goToOrders = async ( page: Page ) => { + await page.goto( '/my-account/orders/', { + waitUntil: 'load', + } ); +}; + +export const goToOrder = async ( page: Page, orderId: string ) => { + await page.goto( `/my-account/view-order/${ orderId }`, { + waitUntil: 'load', + } ); +}; diff --git a/tests/e2e-pw/utils/shopper.ts b/tests/e2e-pw/utils/shopper.ts index 9c984811cb8..3acff9d24c9 100644 --- a/tests/e2e-pw/utils/shopper.ts +++ b/tests/e2e-pw/utils/shopper.ts @@ -1,18 +1,21 @@ /** * External dependencies */ -import { Page } from 'playwright/test'; - +import { Page, expect } from 'playwright/test'; /** * Internal dependencies */ - +import * as navigation from './shopper-navigation'; import { config, CustomerAddress } from '../config/default'; +export const isUIUnblocked = async ( page: Page ) => { + await expect( page.locator( '.blockUI' ) ).toHaveCount( 0 ); +}; + export const fillBillingAddress = async ( page: Page, billingAddress: CustomerAddress -): Promise< void > => { +) => { await page .locator( '#billing_first_name' ) .fill( billingAddress.firstname ); @@ -34,21 +37,21 @@ export const fillBillingAddress = async ( await page.locator( '#billing_email' ).fill( billingAddress.email ); }; -export const placeOrder = async ( page: Page ): Promise< void > => { +export const placeOrder = async ( page: Page ) => { await page.locator( '#place_order' ).click(); }; export const addCartProduct = async ( page: Page, productId = 16 // Beanie -): Promise< void > => { +) => { await page.goto( `/shop/?add-to-cart=${ productId }` ); }; export const fillCardDetails = async ( page: Page, card = config.cards.basic -): Promise< void > => { +) => { if ( await page.$( '#payment .payment_method_woocommerce_payments .wcpay-upe-element' @@ -96,7 +99,7 @@ export const fillCardDetails = async ( export const confirmCardAuthentication = async ( page: Page, authorize = true -): Promise< void > => { +) => { // Stripe card input also uses __privateStripeFrame as a prefix, so need to make sure we wait for an iframe that // appears at the top of the DOM. await page.waitForSelector( @@ -122,3 +125,164 @@ export const confirmCardAuthentication = async ( await button.click(); }; + +/** + * Retrieves the product price from the current product page. + * + * This function assumes that the page object has already navigated to a product page. + */ +export const getPriceFromProduct = async ( page: Page, slug: string ) => { + await navigation.goToProductPageBySlug( page, slug ); + + const priceText = await page + .locator( 'ins .woocommerce-Price-amount.amount' ) + .first() + .textContent(); + + return priceText?.replace( /[^0-9.,]/g, '' ) ?? ''; +}; + +/** + * Adds a product to the cart from the shop page. + * + * @param {Page} page The Playwright page object. + * @param {string|number} product The product ID or title to add to the cart. + */ +export const addToCartFromShopPage = async ( + page: Page, + product: string | number +) => { + if ( Number.isInteger( product ) ) { + const addToCartSelector = `a[data-product_id="${ product }"]`; + + await page.locator( addToCartSelector ).click(); + await expect( + page.locator( `${ addToCartSelector }.added` ) + ).toBeVisible(); + } else { + // These unicode characters are the smart (or curly) quotes: “ ”. + const addToCartRegex = new RegExp( + `Add to cart: \u201C${ product }\u201D` + ); + + await page.getByLabel( addToCartRegex ).click(); + await expect( page.getByLabel( addToCartRegex ) ).toHaveAttribute( + 'class', + /added/ + ); + } +}; + +export const setupCheckout = async ( + page: Page, + billingAddress: CustomerAddress +) => { + await navigation.goToCheckout( page ); + await fillBillingAddress( page, billingAddress ); + // Woo core blocks and refreshes the UI after 1s after each key press + // in a text field or immediately after a select field changes. + // We need to wait to make sure that all key presses were processed by that mechanism. + await page.waitForTimeout( 1000 ); + await isUIUnblocked( page ); + await page + .locator( '.wc_payment_method.payment_method_woocommerce_payments' ) + .click(); +}; + +/** + * Sets up checkout with any number of products. + * + * @param {CustomerAddress} billingAddress The billing address to use for the checkout. + * @param {Array<[string, number]>} lineItems A 2D array of line items where each line item is an array + * that contains the product title as the first element, and the quantity as the second. + * For example, if you want to checkout x2 "Hoodie" and x3 "Belt" then set this parameter like this: + * + * `[ [ "Hoodie", 2 ], [ "Belt", 3 ] ]`. + */ +export async function setupProductCheckout( + page: Page, + billingAddress: CustomerAddress, + lineItems: Array< [ string, number ] > = [ + [ config.products.simple.name, 1 ], + ] +) { + const cartSizeText = await page + .locator( '.cart-contents .count' ) + .textContent(); + let cartSize = Number( cartSizeText?.replace( /\D/g, '' ) ?? '0' ); + + for ( const line of lineItems ) { + let [ productTitle, qty ] = line; + + while ( qty-- ) { + await addToCartFromShopPage( page, productTitle ); + // Make sure the number of items in the cart is incremented before adding another item. + await expect( page.locator( '.cart-contents .count' ) ).toHaveText( + new RegExp( `${ ++cartSize } items?` ), + { + timeout: 30000, + } + ); + } + } + + await setupCheckout( page, billingAddress ); +} + +/** + * Places an order with a specified currency. + * + * @param {Page} page The Playwright page object. + * @param {string} currency The currency code to use for the order. + * @return {Promise} The order ID. + */ +export const placeOrderWithCurrency = async ( + page: Page, + currency: string +) => { + await navigation.goToShopWithCurrency( page, currency ); + await setupProductCheckout( page, config.addresses.customer.billing, [ + [ config.products.simple.name, 1 ], + ] ); + await fillCardDetails( page, config.cards.basic ); + // Takes off the focus out of the Stripe elements to let Stripe logic + // wrap up and make sure the Place Order button is clickable. + await page.locator( '#place_order' ).focus(); + await page.waitForTimeout( 1000 ); + await placeOrder( page ); + await page.waitForURL( /\/order-received\//, { waitUntil: 'load' } ); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + const url = await page.url(); + return url.match( /\/order-received\/(\d+)\// )?.[ 1 ] ?? ''; +}; + +export const emptyCart = async ( page: Page ) => { + await navigation.goToCart( page ); + + // Remove products if they exist. + let products = await page.locator( '.remove' ).all(); + + while ( products.length ) { + await products[ 0 ].click(); + await isUIUnblocked( page ); + + products = await page.locator( '.remove' ).all(); + } + + // Remove coupons if they exist. + let coupons = await page.locator( '.woocommerce-remove-coupon' ).all(); + + while ( coupons.length ) { + await coupons[ 0 ].click(); + await isUIUnblocked( page ); + + coupons = await page.locator( '.woocommerce-remove-coupon' ).all(); + } + + await expect( page.locator( '.cart-empty.woocommerce-info' ) ).toHaveText( + 'Your cart is currently empty.' + ); +}; diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js deleted file mode 100644 index 51a0d36b930..00000000000 --- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js +++ /dev/null @@ -1,362 +0,0 @@ -/** - * External dependencies - */ -const { merchant, WP_ADMIN_DASHBOARD } = require( '@woocommerce/e2e-utils' ); -/** - * Internal dependencies - */ -import { - merchantWCP, - setCheckboxState, - takeScreenshot, - uiLoaded, -} from '../../../utils'; - -// Shared selector constants. -const THEME_SELECTOR = ( themeSlug ) => `.theme[data-slug="${ themeSlug }"]`; -const ACTIVATE_THEME_BUTTON_SELECTOR = ( themeSlug ) => - `${ THEME_SELECTOR( themeSlug ) } .button.activate`; -const MULTI_CURRENCY_TOGGLE_SELECTOR = "[data-testid='multi-currency-toggle']"; -const RECOMMENDED_CURRENCY_LIST_SELECTOR = - 'li[data-testid="recommended-currency"]'; -const CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR = - 'li.enabled-currency-checkbox:not([data-testid="recommended-currency"])'; -const ENABLED_CURRENCY_LIST_SELECTOR = 'li.enabled-currency-checkbox'; -const GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR = - 'input[data-testid="enable_auto_currency"]'; -const PREVIEW_STORE_BTN_SELECTOR = '.multi-currency-setup-preview-button'; -const PREVIEW_STORE_IFRAME_SELECTOR = - '.multi-currency-store-settings-preview-iframe'; -const SUBMIT_STEP_BTN_SELECTOR = - '.add-currencies-task.is-active .task-collapsible-body.is-active > button.is-primary'; -const STOREFRONT_SWITCH_CHECKBOX_SELECTOR = - 'input[data-testid="enable_storefront_switcher"]'; - -let wasMulticurrencyEnabled; - -const goToThemesPage = async () => { - await page.goto( `${ WP_ADMIN_DASHBOARD }themes.php`, { - waitUntil: 'networkidle0', - } ); -}; - -const activateTheme = async ( themeSlug ) => { - await goToThemesPage(); - - // Check if the theme is already active. - const isActive = await page.evaluate( ( selector ) => { - const themeElement = document.querySelector( selector ); - return themeElement && themeElement.classList.contains( 'active' ); - }, THEME_SELECTOR( themeSlug ) ); - - // Activate the theme if it's not already active. - if ( ! isActive ) { - await page.click( ACTIVATE_THEME_BUTTON_SELECTOR( themeSlug ) ); - await page.waitForNavigation( { waitUntil: 'networkidle0' } ); - } -}; - -const goToOnboardingPage = async () => { - await page.goto( - `${ WP_ADMIN_DASHBOARD }admin.php?page=wc-admin&path=%2Fpayments%2Fmulti-currency-setup`, - { - waitUntil: 'networkidle0', - } - ); - await uiLoaded(); -}; - -const goToNextOnboardingStep = async () => { - await page.click( SUBMIT_STEP_BTN_SELECTOR ); -}; - -describe( 'Merchant On-boarding', () => { - let activeThemeSlug; - - beforeAll( async () => { - await merchant.login(); - // Get initial multi-currency feature status. - await merchantWCP.openWCPSettings(); - await page.waitForSelector( MULTI_CURRENCY_TOGGLE_SELECTOR ); - wasMulticurrencyEnabled = await page.evaluate( ( selector ) => { - const checkbox = document.querySelector( selector ); - return checkbox ? checkbox.checked : false; - }, MULTI_CURRENCY_TOGGLE_SELECTOR ); - await merchantWCP.activateMulticurrency(); - - await goToThemesPage(); - - // Get current theme slug. - activeThemeSlug = await page.evaluate( () => { - const theme = document.querySelector( '.theme.active' ); - return theme ? theme.getAttribute( 'data-slug' ) : ''; - } ); - } ); - - afterAll( async () => { - // Restore original theme. - await activateTheme( activeThemeSlug ); - - // Disable multi-currency if it was not initially enabled. - if ( ! wasMulticurrencyEnabled ) { - await merchant.login(); - await merchantWCP.deactivateMulticurrency(); - } - await merchant.logout(); - } ); - - describe( 'Currency Selection and Management', () => { - beforeAll( async () => { - await merchantWCP.disableAllEnabledCurrencies(); - } ); - - beforeEach( async () => { - await goToOnboardingPage(); - } ); - - it( 'Should disable the submit button when no currencies are selected', async () => { - await takeScreenshot( 'merchant-on-boarding-multicurrency-screen' ); - await setCheckboxState( - `${ ENABLED_CURRENCY_LIST_SELECTOR } .components-checkbox-control__input`, - false - ); - - await page.waitForTimeout( 1000 ); - - const button = await page.$( SUBMIT_STEP_BTN_SELECTOR ); - expect( button ).not.toBeNull(); - - const isDisabled = await page.evaluate( - ( btn ) => btn.disabled, - button - ); - - expect( isDisabled ).toBeTruthy(); - } ); - - it( 'Should allow multiple currencies to be selectable', async () => { - await page.waitForSelector( - CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR, - { - timeout: 3000, - } - ); - - // Ensure the checkbox within the list item is present and not disabled. - const checkbox = await page.$( - `${ CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR } input[type="checkbox"]` - ); - expect( checkbox ).not.toBeNull(); - const isDisabled = await ( - await checkbox.getProperty( 'disabled' ) - ).jsonValue(); - expect( isDisabled ).toBe( false ); - - // Click the checkbox to select the currency and verify it's checked. - await checkbox.click(); - - const isChecked = await ( - await checkbox.getProperty( 'checked' ) - ).jsonValue(); - expect( isChecked ).toBe( true ); - } ); - - it( 'Should exclude already enabled currencies from the currency screen', async () => { - await merchantWCP.addCurrency( 'GBP' ); - - await goToOnboardingPage(); - - await page.waitForSelector( ENABLED_CURRENCY_LIST_SELECTOR, { - timeout: 3000, - } ); - - // Get the list of currencies as text - const currencies = await page.$$eval( - ENABLED_CURRENCY_LIST_SELECTOR, - ( items ) => items.map( ( item ) => item.textContent.trim() ) - ); - - expect( currencies ).not.toContain( 'GBP' ); - - await merchantWCP.removeCurrency( 'GBP' ); - } ); - - it( 'Should display some suggested currencies at the beginning of the list', async () => { - await page.waitForSelector( RECOMMENDED_CURRENCY_LIST_SELECTOR, { - timeout: 3000, - } ); - - // Get the list of recommended currencies - const recommendedCurrencies = await page.$$eval( - RECOMMENDED_CURRENCY_LIST_SELECTOR, - ( items ) => - items.map( ( item ) => ( { - code: item - .querySelector( 'input' ) - .getAttribute( 'code' ), - name: item - .querySelector( - 'span.enabled-currency-checkbox__code' - ) - .textContent.trim(), - } ) ) - ); - - expect( recommendedCurrencies.length ).toBeGreaterThan( 0 ); - } ); - - it( 'Should ensure selected currencies are enabled after submitting the form', async () => { - const testCurrencies = [ 'GBP', 'EUR', 'CAD', 'AUD' ]; - const addCurrenciesContentSelector = - '.add-currencies-task__content'; - const currencyCheckboxSelector = `${ addCurrenciesContentSelector } li input[type="checkbox"]`; - - await page.waitForSelector( addCurrenciesContentSelector, { - timeout: 3000, - } ); - - // Select the currencies - for ( const currency of testCurrencies ) { - await setCheckboxState( - `${ currencyCheckboxSelector }[code="${ currency }"]`, - true - ); - } - - // Submit the form. - await goToNextOnboardingStep(); - - await merchantWCP.openMultiCurrency(); - - // Ensure the currencies are enabled. - for ( const currency of testCurrencies ) { - const selector = `li.enabled-currency.${ currency.toLowerCase() }`; - await page.waitForSelector( selector, { timeout: 10000 } ); - const element = await page.$( selector ); - - expect( element ).not.toBeNull(); - } - } ); - } ); - - describe( 'Geolocation Features', () => { - beforeAll( async () => { - await merchantWCP.disableAllEnabledCurrencies(); - } ); - - beforeEach( async () => { - await goToOnboardingPage(); - } ); - - it( 'Should offer currency switch by geolocation', async () => { - await goToNextOnboardingStep(); - - const checkbox = await page.$( - GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR - ); - - // Check if exists and not disabled. - expect( checkbox ).not.toBeNull(); - const isDisabled = await ( - await checkbox.getProperty( 'disabled' ) - ).jsonValue(); - expect( isDisabled ).toBe( false ); - - // Click the checkbox to select it. - await page.click( GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR ); - - // Check if the checkbox is selected. - const isChecked = await ( - await checkbox.getProperty( 'checked' ) - ).jsonValue(); - expect( isChecked ).toBe( true ); - } ); - - it( 'Should preview currency switch by geolocation correctly with USD and GBP', async () => { - page.setViewport( { width: 1280, height: 1280 } ); // To take a better screenshot of the iframe preview. - - await goToNextOnboardingStep(); - - await takeScreenshot( - 'merchant-on-boarding-multicurrency-screen-2' - ); - - // Enable feature. - await setCheckboxState( - GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR, - true - ); - - // Click preview button. - await page.click( PREVIEW_STORE_BTN_SELECTOR ); - - await page.waitForSelector( PREVIEW_STORE_IFRAME_SELECTOR, { - timeout: 3000, - } ); - - const iframeElement = await page.$( PREVIEW_STORE_IFRAME_SELECTOR ); - const iframe = await iframeElement.contentFrame(); - - await iframe.waitForSelector( '.woocommerce-store-notice', { - timeout: 3000, - } ); - - await takeScreenshot( - 'merchant-on-boarding-multicurrency-geolocation-switcher-preview' - ); - - const noticeText = await iframe.$eval( - '.woocommerce-store-notice', - ( element ) => element.innerText - ); - expect( noticeText ).toContain( - // eslint-disable-next-line max-len - "We noticed you're visiting from United Kingdom (UK). We've updated our prices to Pound sterling for your shopping convenience." - ); - } ); - } ); - - describe( 'Currency Switcher Widget', () => { - it( 'Should offer the currency switcher widget while Storefront theme is active', async () => { - await activateTheme( 'storefront' ); - - await goToOnboardingPage(); - await goToNextOnboardingStep(); - - const checkbox = await page.$( - STOREFRONT_SWITCH_CHECKBOX_SELECTOR - ); - - // Check if exists and not disabled. - expect( checkbox ).not.toBeNull(); - const isDisabled = await ( - await checkbox.getProperty( 'disabled' ) - ).jsonValue(); - expect( isDisabled ).toBe( false ); - - // Click the checkbox to select it. - await page.click( STOREFRONT_SWITCH_CHECKBOX_SELECTOR ); - - // Check if the checkbox is selected. - const isChecked = await ( - await checkbox.getProperty( 'checked' ) - ).jsonValue(); - expect( isChecked ).toBe( true ); - } ); - - it( 'Should not offer the currency switcher widget when an unsupported theme is active', async () => { - await activateTheme( 'twentytwentyfour' ); - - await goToOnboardingPage(); - await goToNextOnboardingStep(); - - const checkbox = await page.$( - STOREFRONT_SWITCH_CHECKBOX_SELECTOR - ); - - expect( checkbox ).toBeNull(); - - await activateTheme( 'storefront' ); - } ); - } ); -} ); diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js deleted file mode 100644 index 50a4dd9683d..00000000000 --- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * External dependencies - */ -const { merchant } = require( '@woocommerce/e2e-utils' ); -/** - * Internal dependencies - */ -import { - getProductPriceFromProductPage, - merchantWCP, - shopperWCP, -} from '../../../utils'; - -let wasMulticurrencyEnabled; - -describe( 'Merchant Multi-Currency Settings', () => { - beforeAll( async () => { - await merchant.login(); - // Get initial multi-currency feature status. - await merchantWCP.openWCPSettings(); - await page.waitForSelector( "[data-testid='multi-currency-toggle']" ); - wasMulticurrencyEnabled = await page.evaluate( () => { - const checkbox = document.querySelector( - "[data-testid='multi-currency-toggle']" - ); - return checkbox ? checkbox.checked : false; - } ); - } ); - - afterAll( async () => { - // Disable multi-currency if it was not initially enabled. - if ( ! wasMulticurrencyEnabled ) { - await merchant.login(); - await merchantWCP.deactivateMulticurrency(); - } - await merchant.logout(); - } ); - - it( 'can enable multi-currency feature', async () => { - // Assertions are in the merchantWCP.wcpSettingsSaveChanges() flow. - await merchantWCP.activateMulticurrency(); - } ); - - it( 'can disable multi-currency feature', async () => { - // Assertions are in the merchantWCP.wcpSettingsSaveChanges() flow. - await merchantWCP.deactivateMulticurrency(); - } ); - - describe( 'Currency Management', () => { - const testCurrency = 'CHF'; - - beforeAll( async () => { - await merchantWCP.activateMulticurrency(); - } ); - - it( 'can add a new currency', async () => { - await merchantWCP.addCurrency( testCurrency ); - } ); - - it( 'can remove a currency', async () => { - await merchantWCP.removeCurrency( testCurrency ); - } ); - } ); - - describe( 'Currency Settings', () => { - const testData = { - currencyCode: 'CHF', - rate: '1.25', - charmPricing: '-0.01', - rounding: '0.5', - currencyPrecision: 2, - }; - - let beanieRegularPrice; - - beforeAll( async () => { - await merchantWCP.activateMulticurrency(); - await merchantWCP.disableAllEnabledCurrencies(); - - await shopperWCP.goToShopWithCurrency( 'USD' ); - await shopperWCP.goToProductPageBySlug( 'beanie' ); - beanieRegularPrice = await getProductPriceFromProductPage(); - } ); - - beforeEach( async () => { - await merchantWCP.openMultiCurrency(); - await merchantWCP.addCurrency( testData.currencyCode ); - } ); - - afterEach( async () => { - await merchantWCP.openMultiCurrency(); - await merchantWCP.removeCurrency( testData.currencyCode ); - } ); - - it( 'can change the currency rate manually', async () => { - await merchantWCP.setCurrencyRate( - testData.currencyCode, - testData.rate - ); - await merchantWCP.setCurrencyPriceRounding( - testData.currencyCode, - '0' - ); - - await shopperWCP.goToShopWithCurrency( testData.currencyCode ); - await shopperWCP.goToProductPageBySlug( 'beanie' ); - const beaniePriceOnCurrency = await getProductPriceFromProductPage(); - - expect( - parseFloat( beaniePriceOnCurrency ).toFixed( - testData.currencyPrecision - ) - ).toBe( - ( parseFloat( beanieRegularPrice ) * testData.rate ).toFixed( - testData.currencyPrecision - ) - ); - } ); - - it( 'can change the charm price manually', async () => { - await merchantWCP.setCurrencyRate( testData.currencyCode, '1.00' ); - await merchantWCP.setCurrencyPriceRounding( - testData.currencyCode, - '0' - ); - await merchantWCP.setCurrencyCharmPricing( - testData.currencyCode, - testData.charmPricing - ); - - await shopperWCP.goToShopWithCurrency( testData.currencyCode ); - await shopperWCP.goToProductPageBySlug( 'beanie' ); - const beaniePriceOnCurrency = await getProductPriceFromProductPage(); - - expect( - parseFloat( beaniePriceOnCurrency ).toFixed( - testData.currencyPrecision - ) - ).toBe( - ( - parseFloat( beanieRegularPrice ) + - parseFloat( testData.charmPricing ) - ).toFixed( testData.currencyPrecision ) - ); - } ); - - it( 'can change the rounding precision manually', async () => { - const rateForTest = 1.2; - - await merchantWCP.setCurrencyRate( - testData.currencyCode, - rateForTest.toString() - ); - await merchantWCP.setCurrencyPriceRounding( - testData.currencyCode, - testData.rounding - ); - - await shopperWCP.goToShopWithCurrency( testData.currencyCode ); - await shopperWCP.goToProductPageBySlug( 'beanie' ); - const beaniePriceOnCurrency = await getProductPriceFromProductPage(); - - expect( - parseFloat( beaniePriceOnCurrency ).toFixed( - testData.currencyPrecision - ) - ).toBe( - ( - Math.ceil( - parseFloat( beanieRegularPrice ) * - rateForTest * - ( 1 / testData.rounding ) - ) * testData.rounding - ).toFixed( testData.currencyPrecision ) - ); - } ); - } ); - - describe( 'Currency decimal points', () => { - const currencyDecimalMap = { - JPY: 0, - GBP: 2, - }; - - beforeAll( async () => { - await merchantWCP.activateMulticurrency(); - - for ( const currency of Object.keys( currencyDecimalMap ) ) { - await merchantWCP.addCurrency( currency ); - } - } ); - - it.each( Object.keys( currencyDecimalMap ) )( - 'sees the correct decimal points for %s', - async ( currency ) => { - await shopperWCP.goToShopWithCurrency( currency ); - await shopperWCP.goToProductPageBySlug( 'beanie' ); - const priceOnCurrency = await getProductPriceFromProductPage(); - - const decimalPart = priceOnCurrency.split( '.' )[ 1 ] || ''; - expect( decimalPart.length ).toBe( - currencyDecimalMap[ currency ] - ); - } - ); - } ); -} ); diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js deleted file mode 100644 index 96064318e2c..00000000000 --- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * External dependencies - */ -const { merchant, WP_ADMIN_DASHBOARD } = require( '@woocommerce/e2e-utils' ); - -/** - * Internal dependencies - */ -import { merchantWCP, takeScreenshot } from '../../../utils'; - -describe( 'Admin Multi-Currency', () => { - let wasMulticurrencyEnabled; - - beforeAll( async () => { - await merchant.login(); - wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency(); - } ); - - afterAll( async () => { - if ( ! wasMulticurrencyEnabled ) { - await merchantWCP.deactivateMulticurrency(); - } - await merchant.logout(); - } ); - - it( 'page should load without any errors', async () => { - await merchantWCP.openMultiCurrency(); - await expect( page ).toMatchElement( 'h2', { - text: 'Enabled currencies', - } ); - await takeScreenshot( 'merchant-admin-multi-currency' ); - } ); - - it( 'should be possible to add the currency switcher to the sidebar', async () => { - await merchantWCP.addMulticurrencyWidget(); - } ); - - it( 'should be possible to add the currency switcher to a post/page', async () => { - await page.goto( `${ WP_ADMIN_DASHBOARD }post-new.php`, { - waitUntil: 'load', - } ); - - const closeWelcomeModal = await page.$( 'button[aria-label="Close"]' ); - if ( closeWelcomeModal ) { - await closeWelcomeModal.click(); - } - - await page.click( 'button[aria-label="Add block"]' ); - - const searchInput = await page.waitForSelector( - 'input[placeholder="Search"]' - ); - searchInput.type( 'switcher', { delay: 20 } ); - - await page.waitForSelector( 'button[role="option"]' ); - await expect( page ).toMatchElement( 'button[role="option"]', { - text: 'Currency Switcher Block', - } ); - await page.waitForTimeout( 1000 ); - } ); -} ); diff --git a/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js deleted file mode 100644 index 98e275122f2..00000000000 --- a/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * External dependencies - */ -import config from 'config'; -const { shopper, merchant } = require( '@woocommerce/e2e-utils' ); -/** - * Internal dependencies - */ -import { fillCardDetails, setupProductCheckout } from '../../../utils/payments'; -import { merchantWCP, shopperWCP } from '../../../utils'; - -const ORDER_RECEIVED_ORDER_TOTAL_SELECTOR = - '.woocommerce-order-overview__total'; -const ORDER_HISTORY_ORDER_ROW_SELECTOR = '.woocommerce-orders-table__row'; -const ORDER_HISTORY_ORDER_ROW_NUMBER_COL_SELECTOR = - '.woocommerce-orders-table__cell-order-number'; -const ORDER_HISTORY_ORDER_ROW_TOTAL_COL_SELECTOR = - '.woocommerce-orders-table__cell-order-total'; -const ORDER_DETAILS_ORDER_TOTAL_SELECTOR = - '.woocommerce-table--order-details tfoot tr:last-child td'; - -const placeOrderWithCurrency = async ( currency ) => { - try { - await shopperWCP.goToShopWithCurrency( currency ); - await setupProductCheckout( - config.get( 'addresses.customer.billing' ), - [ [ config.get( 'products.simple.name' ), 1 ] ], - currency - ); - const card = config.get( 'cards.basic' ); - await fillCardDetails( page, card ); - await shopper.placeOrder(); - await expect( page ).toMatchTextContent( 'Order received' ); - - const url = await page.url(); - return url.match( /\/order-received\/(\d+)\// )[ 1 ]; - } catch ( error ) { - // eslint-disable-next-line no-console - console.error( - `Error placing order with currency ${ currency }: `, - error - ); - throw error; - } -}; - -const getOrderTotalTextForOrder = async ( orderId ) => { - return await page.$$eval( - ORDER_HISTORY_ORDER_ROW_SELECTOR, - ( rows, currentOrderId, orderNumberColSelector, totalColSelector ) => { - const orderSelector = `${ orderNumberColSelector } a[href*="view-order/${ currentOrderId }/"]`; - return rows - .filter( ( row ) => row.querySelector( orderSelector ) ) - .map( ( row ) => - row.querySelector( totalColSelector )?.textContent.trim() - ) - .find( ( text ) => text !== null ); - }, - orderId, - ORDER_HISTORY_ORDER_ROW_NUMBER_COL_SELECTOR, - ORDER_HISTORY_ORDER_ROW_TOTAL_COL_SELECTOR - ); -}; - -describe( 'Shopper Multi-Currency checkout', () => { - let wasMulticurrencyEnabled; - const currenciesOrders = { - USD: null, - EUR: null, - }; - beforeAll( async () => { - // Enable multi-currency - await merchant.login(); - - wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency(); - for ( const currency in currenciesOrders ) { - await merchantWCP.addCurrency( currency ); - } - - await merchant.logout(); - - await shopper.login(); - } ); - - afterAll( async () => { - await shopperWCP.emptyCart(); - await shopper.logout(); - - // Disable multi-currency if it was not initially enabled. - if ( ! wasMulticurrencyEnabled ) { - await merchant.login(); - await merchantWCP.deactivateMulticurrency(); - await merchant.logout(); - } - } ); - - describe.each( Object.keys( currenciesOrders ) )( - 'Checkout process with %s currency', - ( testCurrency ) => { - it( 'should allow checkout', async () => { - currenciesOrders[ testCurrency ] = await placeOrderWithCurrency( - testCurrency - ); - } ); - - it( 'should display the correct currency on the order received page', async () => { - expect( - await page.$eval( - ORDER_RECEIVED_ORDER_TOTAL_SELECTOR, - ( el ) => el.textContent - ) - ).toMatch( new RegExp( testCurrency ) ); - } ); - } - ); - - describe.each( Object.keys( currenciesOrders ) )( - 'My account order details for %s order', - ( testCurrency ) => { - beforeEach( async () => { - await shopperWCP.goToOrder( currenciesOrders[ testCurrency ] ); - } ); - - it( 'should show the correct currency in the order page for the order', async () => { - expect( - await page.$eval( - ORDER_DETAILS_ORDER_TOTAL_SELECTOR, - ( el ) => el.textContent - ) - ).toMatch( new RegExp( testCurrency ) ); - } ); - } - ); - - describe( 'My account order history', () => { - it( 'should show the correct currency in the order history table', async () => { - await shopperWCP.goToOrders(); - - for ( const currency in currenciesOrders ) { - const orderTotalText = await getOrderTotalTextForOrder( - currenciesOrders[ currency ] - ); - expect( orderTotalText ).toMatch( new RegExp( currency ) ); - } - } ); - } ); -} ); From cd0e4b3c030781fbfd51f5848e5007065349a645 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 21 Aug 2024 02:44:56 -0500 Subject: [PATCH 10/44] fix: missing translations, improved a11y on new testing instructions (#9279) --- assets/images/icons/copy.svg | 4 +-- changelog/fix-test-instructions-translations | 4 +++ client/checkout/blocks/index.js | 1 + client/checkout/blocks/style.scss | 20 ++--------- client/checkout/classic/event-handlers.js | 1 + client/checkout/classic/style.scss | 16 ++------- client/checkout/style.scss | 35 ++++++++++++------ client/checkout/utils/copy-test-number.js | 36 +++++++++++++++++++ includes/class-wc-payments-checkout.php | 8 +++-- .../class-becs-payment-method.php | 2 +- .../class-cc-payment-method.php | 2 +- .../class-sepa-payment-method.php | 2 +- .../unit/test-class-wc-payments-checkout.php | 2 +- 13 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 changelog/fix-test-instructions-translations create mode 100644 client/checkout/utils/copy-test-number.js diff --git a/assets/images/icons/copy.svg b/assets/images/icons/copy.svg index 65e9350a00e..9452ee2ec86 100644 --- a/assets/images/icons/copy.svg +++ b/assets/images/icons/copy.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/changelog/fix-test-instructions-translations b/changelog/fix-test-instructions-translations new file mode 100644 index 00000000000..bc1d8474363 --- /dev/null +++ b/changelog/fix-test-instructions-translations @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: missing translations on testing instructions. diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index e837734ac8c..1648598a24b 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -44,6 +44,7 @@ import { handleWooPayEmailInput } from '../woopay/email-input-iframe'; import { recordUserEvent } from 'tracks'; import wooPayExpressCheckoutPaymentMethod from '../woopay/express-button/woopay-express-checkout-payment-method'; import { isPreviewing } from '../preview'; +import '../utils/copy-test-number'; const upeMethods = { card: PAYMENT_METHOD_NAME_CARD, diff --git a/client/checkout/blocks/style.scss b/client/checkout/blocks/style.scss index 2329dba9f91..980c587b4d4 100644 --- a/client/checkout/blocks/style.scss +++ b/client/checkout/blocks/style.scss @@ -1,11 +1,9 @@ +@import '../style'; + .wcpay-payment-element { margin-bottom: 2rem; } -.wcpay-card-mounted { - background-color: #fff; -} - /* stylelint-disable-next-line selector-id-pattern */ #express-payment-method-woopay { width: 100%; @@ -110,19 +108,5 @@ button.wcpay-stripelink-modal-trigger:hover { } } -button.copy-icon { - display: inline-block; - height: 1.2em; - width: 1.2em; - line-height: 1.2em; - vertical-align: middle; - margin-left: -0.1em; - padding: 0; - border-radius: 0; - border: none !important; - background: url( 'assets/images/icons/copy.svg?asset' ); - background-size: cover; -} - @import '../woopay/style'; @import '../../components/loadable/style'; diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index cd04e3a1374..80982473177 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -31,6 +31,7 @@ import { handleWooPayEmailInput } from 'wcpay/checkout/woopay/email-input-iframe import { isPreviewing } from 'wcpay/checkout/preview'; import { recordUserEvent } from 'tracks'; import { SHORTCODE_BILLING_ADDRESS_FIELDS } from 'wcpay/checkout/constants'; +import '../utils/copy-test-number'; jQuery( function ( $ ) { enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); diff --git a/client/checkout/classic/style.scss b/client/checkout/classic/style.scss index ef73dda2bb3..c25d285d212 100644 --- a/client/checkout/classic/style.scss +++ b/client/checkout/classic/style.scss @@ -1,3 +1,5 @@ +@import '../style'; + #payment .payment_method_woocommerce_payments .testmode-info { margin-bottom: 0.5em; } @@ -92,17 +94,3 @@ li.wc_payment_method:has( .input-radio:checked } } } - -button.copy-icon { - display: inline-block; - height: 1.2em; - width: 1.2em; - line-height: 1.2em; - vertical-align: middle; - margin-left: -0.1em; - padding: 0; - border-radius: 0; - border: none !important; - background: url( 'assets/images/icons/copy.svg?asset' ); - background-size: cover; -} diff --git a/client/checkout/style.scss b/client/checkout/style.scss index b89f2333b00..c2310d7b080 100644 --- a/client/checkout/style.scss +++ b/client/checkout/style.scss @@ -1,14 +1,29 @@ -#wcpay-card-element { - border: 1px solid #ddd; - padding: 5px 7px; - min-height: 29px; - margin-bottom: 0.5rem; +#payment .payment_method_woocommerce_payments .woocommerce-error { + margin: 1rem 0; } -.wcpay-card-mounted { - background-color: #fff; -} +.js-woopayments-copy-test-number { + line-height: 1.2em; + vertical-align: middle; + border-radius: 0; + border: none !important; + background-color: transparent; + padding: 3px; -#payment .payment_method_woocommerce_payments .woocommerce-error { - margin: 1rem 0; + i { + display: inline-block; + width: 1.2em; + height: 1.2em; + + background: url( 'assets/images/icons/copy.svg?asset' ) no-repeat center; + background-size: contain; + } + + &:hover { + background-color: transparent; + } + + &:active i { + transform: scale( 0.9 ); + } } diff --git a/client/checkout/utils/copy-test-number.js b/client/checkout/utils/copy-test-number.js new file mode 100644 index 00000000000..9fad53e7de3 --- /dev/null +++ b/client/checkout/utils/copy-test-number.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +document.addEventListener( + 'click', + function ( event ) { + const copyNumberButton = event.target?.closest( + '.js-woopayments-copy-test-number' + ); + if ( copyNumberButton ) { + event.preventDefault(); + const number = copyNumberButton.parentElement.querySelector( + '.js-woopayments-test-number' + ).innerText; + navigator.clipboard.writeText( number ); + + window.wp?.data + ?.dispatch( 'core/notices' ) + ?.createInfoNotice( + __( + 'Test number copied to your clipboard!', + 'woocommerce-payments' + ), + { + // the unique `id` prevents the JS from creating multiple notices with the same text before they're dismissed. + id: 'woopayments/test-number-copied', + type: 'snackbar', + context: 'wc/checkout/payments', + } + ); + } + }, + false +); diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 3dcd55f570b..b94d7354cf8 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -332,8 +332,10 @@ public function get_enabled_payment_method_config() { $payment_method->get_testing_instructions(), [ 'a' => '', - 'button' => '. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); } } diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php index 32ad3492e32..6f85995d5e0 100644 --- a/includes/payment-methods/class-cc-payment-method.php +++ b/includes/payment-methods/class-cc-payment-method.php @@ -70,6 +70,6 @@ public function get_title( string $account_country = null, $payment_details = fa * @return string */ public function get_testing_instructions() { - return __( 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', 'woocommerce-payments' ); + return __( 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', 'woocommerce-payments' ); } } diff --git a/includes/payment-methods/class-sepa-payment-method.php b/includes/payment-methods/class-sepa-payment-method.php index b11801ee738..2e638568b52 100644 --- a/includes/payment-methods/class-sepa-payment-method.php +++ b/includes/payment-methods/class-sepa-payment-method.php @@ -43,6 +43,6 @@ public function __construct( $token_service ) { * @return string */ public function get_testing_instructions() { - return __( 'Test mode: use the test account number AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); + return __( 'Test mode: use the test account number AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); } } diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php index 8df6f307575..90a45062f24 100644 --- a/tests/unit/test-class-wc-payments-checkout.php +++ b/tests/unit/test-class-wc-payments-checkout.php @@ -383,7 +383,7 @@ public function test_link_payment_method_provided_when_card_enabled() { 'darkIcon' => $dark_icon_url, 'showSaveOption' => true, 'countries' => [], - 'testingInstructions' => 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', + 'testingInstructions' => 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', 'forceNetworkSavedCards' => false, ], 'link' => [ From 83b00dbf71e8bb56fcbb823caf99c4814699422a Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 21 Aug 2024 09:13:14 -0500 Subject: [PATCH 11/44] update: payment method fees in one line in settings (#9287) Co-authored-by: Brett Shumaker --- changelog/refactor-payment-method-fees-oneline | 4 ++++ .../payment-methods-list/payment-method.scss | 18 ++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 changelog/refactor-payment-method-fees-oneline diff --git a/changelog/refactor-payment-method-fees-oneline b/changelog/refactor-payment-method-fees-oneline new file mode 100644 index 00000000000..5a12bb1da7a --- /dev/null +++ b/changelog/refactor-payment-method-fees-oneline @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +update: payment method fees in one line diff --git a/client/settings/payment-methods-list/payment-method.scss b/client/settings/payment-methods-list/payment-method.scss index a020b7562cb..b540af1bb31 100644 --- a/client/settings/payment-methods-list/payment-method.scss +++ b/client/settings/payment-methods-list/payment-method.scss @@ -27,17 +27,16 @@ flex: 1 1 100%; order: 1; display: flex; - flex-wrap: nowrap; justify-content: space-between; + flex-direction: column; @include breakpoint( '>660px' ) { + flex-wrap: nowrap; + flex-direction: row; + align-items: center; flex: 1 1 auto; order: 3; } - - @include breakpoint( '<660px' ) { - flex-wrap: wrap; - } } &__checkbox { @@ -158,13 +157,12 @@ &__fees { display: flex; - align-items: center; - justify-content: flex-end; white-space: nowrap; - margin-left: 16px; - @include breakpoint( '<660px' ) { - margin-left: 0; + @include breakpoint( '>660px' ) { + justify-content: flex-end; + margin-left: 16px; + flex-shrink: 0; } } From 039cbd5deac8e6d6cad695b064c6f024cb52cbb1 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 21 Aug 2024 10:09:00 -0500 Subject: [PATCH 12/44] chore: remove unused UPE appearance constants (#9299) --- ...ve-frontend-upe-appearance-theme-constants | 5 +++ includes/class-wc-payments-checkout.php | 38 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 changelog/chore-remove-frontend-upe-appearance-theme-constants diff --git a/changelog/chore-remove-frontend-upe-appearance-theme-constants b/changelog/chore-remove-frontend-upe-appearance-theme-constants new file mode 100644 index 00000000000..2dfab5061d1 --- /dev/null +++ b/changelog/chore-remove-frontend-upe-appearance-theme-constants @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: chore: remove unused upe appearance theme constants from JS config. + + diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index b94d7354cf8..65887a6b61b 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -218,27 +218,23 @@ public function get_payment_fields_js_config() { */ $payment_fields = apply_filters( 'wcpay_payment_fields_js_config', $js_config ); - $payment_fields['accountDescriptor'] = $this->gateway->get_account_statement_descriptor(); - $payment_fields['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' ); - $payment_fields['gatewayId'] = WC_Payment_Gateway_WCPay::GATEWAY_ID; - $payment_fields['isCheckout'] = is_checkout(); - $payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config(); - $payment_fields['testMode'] = WC_Payments::mode()->is_test(); - $payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT ); - $payment_fields['upeAddPaymentMethodAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT ); - $payment_fields['upeAddPaymentMethodAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_ADD_PAYMENT_METHOD_APPEARANCE_THEME_TRANSIENT ); - $payment_fields['upeBnplProductPageAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT ); - $payment_fields['upeBnplProductPageAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT ); - $payment_fields['upeBnplClassicCartAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT ); - $payment_fields['upeBnplClassicCartAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT ); - $payment_fields['upeBnplCartBlockAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT ); - $payment_fields['upeBnplCartBlockAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT ); - $payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); - $payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT ); - $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart(); - $payment_fields['currency'] = get_woocommerce_currency(); - $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); - $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); + $payment_fields['accountDescriptor'] = $this->gateway->get_account_statement_descriptor(); + $payment_fields['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' ); + $payment_fields['gatewayId'] = WC_Payment_Gateway_WCPay::GATEWAY_ID; + $payment_fields['isCheckout'] = is_checkout(); + $payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config(); + $payment_fields['testMode'] = WC_Payments::mode()->is_test(); + $payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT ); + $payment_fields['upeAddPaymentMethodAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT ); + $payment_fields['upeBnplProductPageAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT ); + $payment_fields['upeBnplClassicCartAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT ); + $payment_fields['upeBnplCartBlockAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT ); + $payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); + $payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT ); + $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart(); + $payment_fields['currency'] = get_woocommerce_currency(); + $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); + $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); $enabled_billing_fields = []; foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) { From 0d017f60546aa1870cac7b437b7cedad14ccbf0e Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Thu, 22 Aug 2024 16:57:58 -0300 Subject: [PATCH 13/44] Remove obsolete ApplePay wp-admin notice for test accounts. (#9307) --- ...ve-apple-pay-admin-notice-for-live-account | 4 +++ ...ass-wc-payments-apple-pay-registration.php | 25 ------------------- 2 files changed, 4 insertions(+), 25 deletions(-) create mode 100644 changelog/fix-remove-apple-pay-admin-notice-for-live-account diff --git a/changelog/fix-remove-apple-pay-admin-notice-for-live-account b/changelog/fix-remove-apple-pay-admin-notice-for-live-account new file mode 100644 index 00000000000..e2424d553d2 --- /dev/null +++ b/changelog/fix-remove-apple-pay-admin-notice-for-live-account @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Remove obsolete ApplePay warning on wp-admin for test sites. diff --git a/includes/class-wc-payments-apple-pay-registration.php b/includes/class-wc-payments-apple-pay-registration.php index cb58eaa6796..a9ffdca6c9b 100644 --- a/includes/class-wc-payments-apple-pay-registration.php +++ b/includes/class-wc-payments-apple-pay-registration.php @@ -95,7 +95,6 @@ public function init() { add_action( 'parse_request', [ $this, 'parse_domain_association_request' ], 10, 1 ); add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_error_notice' ] ); - add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_live_account_notice' ] ); add_action( 'add_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_new_settings' ], 10, 2 ); add_action( 'update_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_updated_settings' ], 10, 2 ); } @@ -353,30 +352,6 @@ public function verify_domain_on_updated_settings( $prev_settings, $settings ) { } } - /** - * Display warning notice explaining that the domain can't be registered without a live account. - */ - public function display_live_account_notice() { - if ( ! $this->is_enabled() || $this->account->get_is_live() ) { - return; - } - - ?> -

-

- - -

-
- Date: Fri, 23 Aug 2024 11:46:31 -0500 Subject: [PATCH 14/44] Pass appearance data when initiating WooPay via email input (#9310) --- changelog/fix-woopay-themeing-on-email-flow | 4 ++++ client/checkout/woopay/email-input-iframe.js | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog/fix-woopay-themeing-on-email-flow diff --git a/changelog/fix-woopay-themeing-on-email-flow b/changelog/fix-woopay-themeing-on-email-flow new file mode 100644 index 00000000000..d09923a10e2 --- /dev/null +++ b/changelog/fix-woopay-themeing-on-email-flow @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Pass appearance data when initiating WooPay via the email input flow diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index 50ad4940d1d..4dfa1698fcd 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -6,8 +6,10 @@ import { getConfig } from 'wcpay/utils/checkout'; import { recordUserEvent, getTracksIdentity } from 'tracks'; import request from '../utils/request'; import { buildAjaxURL } from 'utils/express-checkout'; +import { getAppearance } from 'checkout/upe-styles'; import { getTargetElement, + getAppearanceType, validateEmail, appendRedirectionParams, shouldSkipWooPay, @@ -177,6 +179,7 @@ export const handleWooPayEmailInput = async ( iframe.addEventListener( 'load', () => { // Set the initial value. iframeHeaderValue = true; + const appearanceType = getAppearanceType(); if ( getConfig( 'isWoopayFirstPartyAuthEnabled' ) ) { request( @@ -186,6 +189,9 @@ export const handleWooPayEmailInput = async ( order_id: getConfig( 'order_id' ), key: getConfig( 'key' ), billing_email: getConfig( 'billing_email' ), + appearance: getConfig( 'isWooPayGlobalThemeSupportEnabled' ) + ? getAppearance( appearanceType ) + : null, } ).then( ( response ) => { if ( response?.data?.session ) { From 0bb06805736421e8612b6c0aaa9c0f6137cd1248 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 23 Aug 2024 21:21:53 -0500 Subject: [PATCH 15/44] Fix broken Express Checkout buttons when size is set to "Large" (#9327) --- changelog/as-fix-max-height-allowed | 4 ++++ .../blocks/components/express-checkout-component.js | 6 +++++- .../blocks/components/express-checkout-container.js | 8 +++++--- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 changelog/as-fix-max-height-allowed diff --git a/changelog/as-fix-max-height-allowed b/changelog/as-fix-max-height-allowed new file mode 100644 index 00000000000..fe15603cc63 --- /dev/null +++ b/changelog/as-fix-max-height-allowed @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Ensure 55px is the maximum height for Apple Pay button. diff --git a/client/express-checkout/blocks/components/express-checkout-component.js b/client/express-checkout/blocks/components/express-checkout-component.js index 492c9f325be..3a996220167 100644 --- a/client/express-checkout/blocks/components/express-checkout-component.js +++ b/client/express-checkout/blocks/components/express-checkout-component.js @@ -40,7 +40,11 @@ const adjustButtonHeights = ( buttonOptions, expressPaymentMethod ) => { // Apple Pay has a nearly imperceptible height difference. We increase it by 1px here. if ( buttonOptions.buttonTheme.applePay === 'black' ) { if ( expressPaymentMethod === 'applePay' ) { - buttonOptions.buttonHeight = buttonOptions.buttonHeight + 0.4; + // The maximum allowed size is 55px. + buttonOptions.buttonHeight = Math.min( + buttonOptions.buttonHeight + 0.4, + 55 + ); } } diff --git a/client/express-checkout/blocks/components/express-checkout-container.js b/client/express-checkout/blocks/components/express-checkout-container.js index af575f90673..1476d82a615 100644 --- a/client/express-checkout/blocks/components/express-checkout-container.js +++ b/client/express-checkout/blocks/components/express-checkout-container.js @@ -27,9 +27,11 @@ const ExpressCheckoutContainer = ( props ) => { }; return ( - - - +
+ + + +
); }; From 9c2202f462a345660d37a56614a00195b64ad368 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:42:13 +1000 Subject: [PATCH 16/44] =?UTF-8?q?Make=20correction=20to=20changelog=20entr?= =?UTF-8?q?y=20for=20Docker=20compose=20v2=20improvement=20(`Fix`=20?= =?UTF-8?q?=E2=86=92=20`Dev`)=20(#9316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.txt | 2 +- changelog/dev-correct-docker-compose-v2-changelog-entry | 5 +++++ readme.txt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog/dev-correct-docker-compose-v2-changelog-entry diff --git a/changelog.txt b/changelog.txt index 9bd78be14c7..0409dc40fdb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,7 +20,6 @@ * Fix - Fix uncaught error on the block based Cart page when WooPayments is disabled. * Fix - Fix WooPay checkboxes while signed in. * Fix - If a payment method fails to be created in the frontend during checkout, forward the errors to the server so it can be recorded in an order. -* Fix - Migrate to Docker Compose V2 for test runner environment setup scripts * Fix - Reverts changes related to Direct Checkout that broke the PayPal extension. * Fix - Translate hardcoded strings on the Connect page * Update - refactor: separate BNPL methods from settings list @@ -32,6 +31,7 @@ * Dev - Match the Node version in nvm with the minimum version in package.json. * Dev - Remove unnecessary console.warn statements added in #9121. * Dev - Update bundle size checker workflow to support node v20 +* Dev - Migrate to Docker Compose V2 for test runner environment setup scripts = 8.0.2 - 2024-08-07 = * Fix - Add opt-in checks to prevent blocking customers using other payment methods. diff --git a/changelog/dev-correct-docker-compose-v2-changelog-entry b/changelog/dev-correct-docker-compose-v2-changelog-entry new file mode 100644 index 00000000000..1d13ad6202c --- /dev/null +++ b/changelog/dev-correct-docker-compose-v2-changelog-entry @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Placeholder changelog entry - making a correction to previous changelog entry 'Fix - Migrate to Docker Compose V2 for test runner environment setup scripts' + + diff --git a/readme.txt b/readme.txt index 2b540d8eebd..9f4c9cff7ec 100644 --- a/readme.txt +++ b/readme.txt @@ -114,7 +114,6 @@ Please note that our support for the checkout block is still experimental and th * Fix - Fix uncaught error on the block based Cart page when WooPayments is disabled. * Fix - Fix WooPay checkboxes while signed in. * Fix - If a payment method fails to be created in the frontend during checkout, forward the errors to the server so it can be recorded in an order. -* Fix - Migrate to Docker Compose V2 for test runner environment setup scripts * Fix - Reverts changes related to Direct Checkout that broke the PayPal extension. * Fix - Translate hardcoded strings on the Connect page * Update - refactor: separate BNPL methods from settings list @@ -126,6 +125,7 @@ Please note that our support for the checkout block is still experimental and th * Dev - Match the Node version in nvm with the minimum version in package.json. * Dev - Remove unnecessary console.warn statements added in #9121. * Dev - Update bundle size checker workflow to support node v20 +* Dev - Migrate to Docker Compose V2 for test runner environment setup scripts = 8.0.2 - 2024-08-07 = * Fix - Add opt-in checks to prevent blocking customers using other payment methods. From ad1395dc096bac3a8f384c6055260acd16a8373c Mon Sep 17 00:00:00 2001 From: Francesco Date: Mon, 26 Aug 2024 03:09:02 -0500 Subject: [PATCH 17/44] fix: testing instructions dark theme support (#9295) --- changelog/fix-testing-instructions-dark-theme-support | 4 ++++ client/checkout/blocks/payment-elements.js | 1 + client/checkout/blocks/payment-processor.js | 6 +++++- client/checkout/classic/payment-processing.js | 9 ++++++++- client/checkout/style.scss | 9 ++++++++- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-testing-instructions-dark-theme-support diff --git a/changelog/fix-testing-instructions-dark-theme-support b/changelog/fix-testing-instructions-dark-theme-support new file mode 100644 index 00000000000..3ac673bcf78 --- /dev/null +++ b/changelog/fix-testing-instructions-dark-theme-support @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: testing instructions dark theme support diff --git a/client/checkout/blocks/payment-elements.js b/client/checkout/blocks/payment-elements.js index 7bc9c3a1372..84ac4be9393 100644 --- a/client/checkout/blocks/payment-elements.js +++ b/client/checkout/blocks/payment-elements.js @@ -90,6 +90,7 @@ const PaymentElements = ( { api, ...props } ) => { errorMessage={ errorMessage } fingerprint={ fingerprint } onLoadError={ setPaymentProcessorLoadErrorMessage } + theme={ appearance?.theme } { ...props } /> diff --git a/client/checkout/blocks/payment-processor.js b/client/checkout/blocks/payment-processor.js index 88b72393524..c8ead3ad914 100644 --- a/client/checkout/blocks/payment-processor.js +++ b/client/checkout/blocks/payment-processor.js @@ -12,6 +12,7 @@ import { } from '@woocommerce/blocks-registry'; import { __ } from '@wordpress/i18n'; import { useEffect, useRef } from 'react'; +import classNames from 'classnames'; /** * Internal dependencies @@ -64,6 +65,7 @@ const PaymentProcessor = ( { shouldSavePayment, fingerprint, onLoadError = noop, + theme, } ) => { const stripe = useStripe(); const elements = useElements(); @@ -267,7 +269,9 @@ const PaymentProcessor = ( { <> { isTestMode && (

Date: Mon, 26 Aug 2024 12:18:29 -0300 Subject: [PATCH 18/44] Send blog timezone to WooPay (#9325) --- changelog/fix-send-blog-timezone-to-woopay | 5 +++++ includes/woopay/class-woopay-session.php | 1 + 2 files changed, 6 insertions(+) create mode 100644 changelog/fix-send-blog-timezone-to-woopay diff --git a/changelog/fix-send-blog-timezone-to-woopay b/changelog/fix-send-blog-timezone-to-woopay new file mode 100644 index 00000000000..41fc9f42eb2 --- /dev/null +++ b/changelog/fix-send-blog-timezone-to-woopay @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Send blog timezone to WooPay. + + diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 8488354ad4f..a75ca96250f 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -480,6 +480,7 @@ public static function get_init_session_request( $order_id = null, $key = null, 'blog_url' => get_site_url(), 'blog_checkout_url' => ! $is_pay_for_order ? wc_get_checkout_url() : $order->get_checkout_payment_url(), 'blog_shop_url' => get_permalink( wc_get_page_id( 'shop' ) ), + 'blog_timezone' => wp_timezone_string(), 'store_api_url' => self::get_store_api_url(), 'account_id' => $account_id, 'test_mode' => WC_Payments::mode()->is_test(), From 244b7c937b86ba902a74fc21050d7ff1618bf690 Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Mon, 26 Aug 2024 15:59:10 -0300 Subject: [PATCH 19/44] Fix logic to retrieve initial shipping rates for ECE on shortcode cart and checkout pages (#9322) --- changelog/fix-ece-shortcode-get-shipping-rates | 4 ++++ client/express-checkout/index.js | 7 ++----- .../class-wc-payments-express-checkout-button-helper.php | 1 + ...st-class-wc-payments-payment-request-button-handler.php | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-ece-shortcode-get-shipping-rates diff --git a/changelog/fix-ece-shortcode-get-shipping-rates b/changelog/fix-ece-shortcode-get-shipping-rates new file mode 100644 index 00000000000..b5fb9899bb5 --- /dev/null +++ b/changelog/fix-ece-shortcode-get-shipping-rates @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix shipping rates retrieval method for shortcode cart/checkout. diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index d0a5966ffcf..d1fa717b606 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -210,10 +210,7 @@ jQuery( ( $ ) => { } return options.displayItems - .filter( - ( i ) => - i.label === __( 'Shipping', 'woocommerce-payments' ) - ) + .filter( ( i ) => i.key === 'total_shipping' ) .map( ( i ) => ( { id: `rate-${ i.label }`, amount: i.amount, @@ -228,7 +225,7 @@ jQuery( ( $ ) => { // Relying on what's provided in the cart response seems safest since it should always include a valid shipping // rate if one is required and available. // If no shipping rate is found we can't render the button so we just exit. - if ( options.requestShipping && ! shippingRates ) { + if ( options.requestShipping && ! shippingRates.length ) { return; } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 460cd5f27dc..1a189749dff 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -117,6 +117,7 @@ public function build_display_items( $itemized_display_items = false ) { if ( WC()->cart->needs_shipping() ) { $shipping_tax = $this->cart_prices_include_tax() ? WC()->cart->shipping_tax_total : 0; $items[] = [ + 'key' => 'total_shipping', 'label' => esc_html( __( 'Shipping', 'woocommerce-payments' ) ), 'amount' => WC_Payments_Utils::prepare_amount( $shipping + $shipping_tax, $currency ), ]; diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php index de372ddc989..db1c564d44c 100644 --- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php +++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php @@ -430,6 +430,7 @@ public function test_get_shipping_options_returns_chosen_option() { [ 'label' => 'Shipping', 'amount' => $flat_rate['amount'], + 'key' => 'total_shipping', ], ]; From 6e983cbda75f2a8a2abc8e2188e4151c6a83d458 Mon Sep 17 00:00:00 2001 From: Samir Merchant Date: Tue, 27 Aug 2024 13:01:38 -0400 Subject: [PATCH 20/44] Disables test instructions clipboard button on insecure sites (#9330) --- ...lipboard-navigator-undefined-error-in-http | 4 ++ client/checkout/utils/copy-test-number.js | 50 +++++++++++-------- 2 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 changelog/fix-catch-clipboard-navigator-undefined-error-in-http diff --git a/changelog/fix-catch-clipboard-navigator-undefined-error-in-http b/changelog/fix-catch-clipboard-navigator-undefined-error-in-http new file mode 100644 index 00000000000..22c4516ccde --- /dev/null +++ b/changelog/fix-catch-clipboard-navigator-undefined-error-in-http @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Disables testing instructions clipboard button on HTTP sites when navigator.clipboard is undefined. diff --git a/client/checkout/utils/copy-test-number.js b/client/checkout/utils/copy-test-number.js index 9fad53e7de3..c649336a5ab 100644 --- a/client/checkout/utils/copy-test-number.js +++ b/client/checkout/utils/copy-test-number.js @@ -9,28 +9,38 @@ document.addEventListener( const copyNumberButton = event.target?.closest( '.js-woopayments-copy-test-number' ); - if ( copyNumberButton ) { - event.preventDefault(); - const number = copyNumberButton.parentElement.querySelector( - '.js-woopayments-test-number' - ).innerText; - navigator.clipboard.writeText( number ); + if ( ! copyNumberButton ) { + return; + } + + event.preventDefault(); + const number = copyNumberButton.parentElement.querySelector( + '.js-woopayments-test-number' + ).innerText; - window.wp?.data - ?.dispatch( 'core/notices' ) - ?.createInfoNotice( - __( - 'Test number copied to your clipboard!', - 'woocommerce-payments' - ), - { - // the unique `id` prevents the JS from creating multiple notices with the same text before they're dismissed. - id: 'woopayments/test-number-copied', - type: 'snackbar', - context: 'wc/checkout/payments', - } - ); + if ( ! navigator.clipboard ) { + prompt( + __( 'Copy the test number:', 'woocommerce-payments' ), + number + ); + return; } + navigator.clipboard.writeText( number ); + + window.wp?.data + ?.dispatch( 'core/notices' ) + ?.createInfoNotice( + __( + 'Test number copied to your clipboard!', + 'woocommerce-payments' + ), + { + // the unique `id` prevents the JS from creating multiple notices with the same text before they're dismissed. + id: 'woopayments/test-number-copied', + type: 'snackbar', + context: 'wc/checkout/payments', + } + ); }, false ); From d885477c822c543b705f2ae98884c658e5f9bd7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:51:57 -0300 Subject: [PATCH 21/44] Apply some fixes in the e2e tests scripts (#9315) --- changelog/dev-fix-e2e-scripts | 5 +++++ tests/e2e-pw/README.md | 3 +++ tests/e2e/env/down.sh | 5 +++++ tests/e2e/env/up.sh | 4 ++-- 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 changelog/dev-fix-e2e-scripts diff --git a/changelog/dev-fix-e2e-scripts b/changelog/dev-fix-e2e-scripts new file mode 100644 index 00000000000..51e19f80e87 --- /dev/null +++ b/changelog/dev-fix-e2e-scripts @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Fix some aspects of the e2e tests scripts. + + diff --git a/tests/e2e-pw/README.md b/tests/e2e-pw/README.md index 84bf18a95ff..f18388e3f31 100644 --- a/tests/e2e-pw/README.md +++ b/tests/e2e-pw/README.md @@ -9,6 +9,9 @@ See [tests/e2e/README.md](/tests/e2e/README.md) for detailed e2e environment set 1. `npm run test:e2e-setup` 1. `npm run test:e2e-up` +> [!TIP] +> In case some tests fail due to the lack of `data-test-id` attributes, you'll need to run `npm start` or `NODE_ENV=test npm run build:client` to re-build the assets. + ## Running Playwright e2e tests - `npm run test:e2e-pw` headless run from within a linux docker container. diff --git a/tests/e2e/env/down.sh b/tests/e2e/env/down.sh index e70ba901cda..6dc112bef7f 100755 --- a/tests/e2e/env/down.sh +++ b/tests/e2e/env/down.sh @@ -15,3 +15,8 @@ if [[ "$E2E_USE_LOCAL_SERVER" != false ]]; then step "Stopping server containers" docker compose -f $E2E_ROOT/deps/wcp-server/docker-compose.yml down fi + +# Remove auth credentials from the Playwright config. +# This must be kept when we fully migrate. +step "Removing Playwright auth credentials" +rm -rf "$E2E_ROOT/../e2e-pw/.auth" diff --git a/tests/e2e/env/up.sh b/tests/e2e/env/up.sh index 671e0343553..1a5998b9047 100755 --- a/tests/e2e/env/up.sh +++ b/tests/e2e/env/up.sh @@ -9,9 +9,9 @@ if [[ -f "$E2E_ROOT/config/local.env" ]]; then fi step "Starting client containers" -docker compose -f "$E2E_ROOT/env/docker-compose.yml" start +docker compose -f "$E2E_ROOT/env/docker-compose.yml" up -d if [[ "$E2E_USE_LOCAL_SERVER" != false ]]; then step "Starting server containers" - docker compose -f "$E2E_ROOT/deps/wcp-server/docker-compose.yml" start + docker compose -f "$E2E_ROOT/deps/wcp-server/docker-compose.yml" up -d fi From cb9c090951dd40e81c6fea6314c16d3a429fc59c Mon Sep 17 00:00:00 2001 From: Samir Merchant Date: Wed, 28 Aug 2024 00:13:49 -0400 Subject: [PATCH 22/44] Temporarily disables custom field detection notices (#9329) --- ...re-disable-custom-checkout-field-detection | 4 +++ client/data/settings/hooks.js | 5 --- client/data/settings/selectors.js | 7 ---- .../payment-request-settings.js | 7 ---- .../express-checkout-settings/test/index.js | 3 -- .../test/payment-request-settings.test.js | 26 -------------- .../apple-google-pay-item.tsx | 10 +----- .../express-checkout/test/index.test.js | 22 ------------ .../incompatibility-notice.js | 11 ------ ...s-wc-rest-payments-settings-controller.php | 35 ------------------- 10 files changed, 5 insertions(+), 125 deletions(-) create mode 100644 changelog/chore-disable-custom-checkout-field-detection diff --git a/changelog/chore-disable-custom-checkout-field-detection b/changelog/chore-disable-custom-checkout-field-detection new file mode 100644 index 00000000000..9c77c983ee2 --- /dev/null +++ b/changelog/chore-disable-custom-checkout-field-detection @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Disables custom checkout field detection due to compatibility issues and false positives. diff --git a/client/data/settings/hooks.js b/client/data/settings/hooks.js index 8dca41224e6..737988c0046 100644 --- a/client/data/settings/hooks.js +++ b/client/data/settings/hooks.js @@ -491,11 +491,6 @@ export const useWooPayShowIncompatibilityNotice = () => select( STORE_NAME ).getShowWooPayIncompatibilityNotice() ); -export const useExpressCheckoutShowIncompatibilityNotice = () => - useSelect( ( select ) => - select( STORE_NAME ).getShowExpressCheckoutIncompatibilityNotice() - ); - export const useStripeBilling = () => { const { updateIsStripeBillingEnabled } = useDispatch( STORE_NAME ); diff --git a/client/data/settings/selectors.js b/client/data/settings/selectors.js index 912f67606d6..898c99efbd0 100644 --- a/client/data/settings/selectors.js +++ b/client/data/settings/selectors.js @@ -253,13 +253,6 @@ export const getShowWooPayIncompatibilityNotice = ( state ) => { return getSettings( state ).show_woopay_incompatibility_notice || false; }; -export const getShowExpressCheckoutIncompatibilityNotice = ( state ) => { - return ( - getSettings( state ).show_express_checkout_incompatibility_notice || - false - ); -}; - export const getIsStripeBillingEnabled = ( state ) => { return getSettings( state ).is_stripe_billing_enabled || false; }; diff --git a/client/settings/express-checkout-settings/payment-request-settings.js b/client/settings/express-checkout-settings/payment-request-settings.js index 3ea6a055c64..a676eba6bbe 100644 --- a/client/settings/express-checkout-settings/payment-request-settings.js +++ b/client/settings/express-checkout-settings/payment-request-settings.js @@ -14,9 +14,7 @@ import GeneralPaymentRequestButtonSettings from './general-payment-request-butto import { usePaymentRequestEnabledSettings, usePaymentRequestLocations, - useExpressCheckoutShowIncompatibilityNotice, } from 'wcpay/data'; -import { ExpressCheckoutIncompatibilityNotice } from 'wcpay/settings/settings-warnings/incompatibility-notice'; const PaymentRequestSettings = ( { section } ) => { const [ @@ -42,15 +40,10 @@ const PaymentRequestSettings = ( { section } ) => { } }; - const showIncompatibilityNotice = useExpressCheckoutShowIncompatibilityNotice(); - return ( { section === 'enable' && ( - { showIncompatibilityNotice && ( - - ) } ( { .fn() .mockReturnValue( [ [ true, true, true ], jest.fn() ] ), useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), - useExpressCheckoutShowIncompatibilityNotice: jest - .fn() - .mockReturnValue( false ), } ) ); jest.mock( '@wordpress/data', () => ( { 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 b3746b1964c..5094c44ee06 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 @@ -18,7 +18,6 @@ import { usePaymentRequestButtonSize, usePaymentRequestButtonTheme, useWooPayEnabledSettings, - useExpressCheckoutShowIncompatibilityNotice, } from '../../../data'; jest.mock( '../../../data', () => ( { @@ -29,7 +28,6 @@ jest.mock( '../../../data', () => ( { usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayEnabledSettings: jest.fn(), - useExpressCheckoutShowIncompatibilityNotice: jest.fn(), useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), useWooPayGlobalThemeSupportEnabledSettings: jest .fn() @@ -260,28 +258,4 @@ describe( 'PaymentRequestSettings', () => { updatePaymentRequestLocationsHandler ).toHaveBeenLastCalledWith( [ 'checkout', 'product' ] ); } ); - - it( 'triggers the hooks when the enable setting is being interacted with', () => { - useExpressCheckoutShowIncompatibilityNotice.mockReturnValue( true ); - - render( ); - - expect( - screen.queryByText( - 'Your custom checkout fields may not be compatible with these payment methods.' - ) - ).toBeInTheDocument(); - } ); - - it( 'triggers the hooks when the enable setting is being interacted with', () => { - useExpressCheckoutShowIncompatibilityNotice.mockReturnValue( false ); - - render( ); - - expect( - screen.queryByText( - 'Your custom checkout fields may not be compatible with these payment methods.' - ) - ).not.toBeInTheDocument(); - } ); } ); diff --git a/client/settings/express-checkout/apple-google-pay-item.tsx b/client/settings/express-checkout/apple-google-pay-item.tsx index a43af822015..3b1471fece8 100644 --- a/client/settings/express-checkout/apple-google-pay-item.tsx +++ b/client/settings/express-checkout/apple-google-pay-item.tsx @@ -10,13 +10,9 @@ import React, { useContext } from 'react'; * Internal dependencies */ import { getPaymentMethodSettingsUrl } from '../../utils'; -import { - usePaymentRequestEnabledSettings, - useExpressCheckoutShowIncompatibilityNotice, -} from 'wcpay/data'; +import { usePaymentRequestEnabledSettings } from 'wcpay/data'; import { PaymentRequestEnabledSettingsHook } from './interfaces'; import { ApplePayIcon, GooglePayIcon } from 'wcpay/payment-methods-icons'; -import { ExpressCheckoutIncompatibilityNotice } from 'wcpay/settings/settings-warnings/incompatibility-notice'; import DuplicateNotice from 'wcpay/components/duplicate-notice'; import DuplicatedPaymentMethodsContext from '../settings-manager/duplicated-payment-methods-context'; @@ -28,7 +24,6 @@ const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => { updateIsPaymentRequestEnabled, ] = usePaymentRequestEnabledSettings() as PaymentRequestEnabledSettingsHook; - const showIncompatibilityNotice = useExpressCheckoutShowIncompatibilityNotice(); const { duplicates, dismissedDuplicateNotices, @@ -176,9 +171,6 @@ const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => {

- { showIncompatibilityNotice && ( - - ) } { isDuplicate && ( ( { useEnabledPaymentMethodIds: jest.fn(), useGetAvailablePaymentMethodIds: jest.fn(), useWooPayShowIncompatibilityNotice: jest.fn(), - useExpressCheckoutShowIncompatibilityNotice: jest.fn(), useGetDuplicatedPaymentMethodIds: jest.fn(), } ) ); @@ -220,24 +218,4 @@ describe( 'ExpressCheckout', () => { ) ).toBeInTheDocument(); } ); - - it( 'should show Express Checkout incompatibility warning', async () => { - const context = { featureFlags: { woopay: true } }; - useGetAvailablePaymentMethodIds.mockReturnValue( [ 'link', 'card' ] ); - useEnabledPaymentMethodIds.mockReturnValue( [ [ 'card', 'link' ] ] ); - - useExpressCheckoutShowIncompatibilityNotice.mockReturnValue( true ); - - render( - - - - ); - - expect( - screen.queryByText( - 'Your custom checkout fields may not be compatible with these payment methods.' - ) - ).toBeInTheDocument(); - } ); } ); diff --git a/client/settings/settings-warnings/incompatibility-notice.js b/client/settings/settings-warnings/incompatibility-notice.js index c3a388f8df6..bd69324b11a 100644 --- a/client/settings/settings-warnings/incompatibility-notice.js +++ b/client/settings/settings-warnings/incompatibility-notice.js @@ -40,14 +40,3 @@ export const WooPayIncompatibilityNotice = () => ( learnMoreLinkHref="https://woocommerce.com/document/woopay-merchant-documentation/#compatibility" /> ); - -export const ExpressCheckoutIncompatibilityNotice = () => ( - -); diff --git a/includes/admin/class-wc-rest-payments-settings-controller.php b/includes/admin/class-wc-rest-payments-settings-controller.php index 314faad586f..5560aafef88 100644 --- a/includes/admin/class-wc-rest-payments-settings-controller.php +++ b/includes/admin/class-wc-rest-payments-settings-controller.php @@ -513,7 +513,6 @@ public function get_settings(): WP_REST_Response { 'is_card_present_eligible' => $this->wcpay_gateway->is_card_present_eligible() && isset( WC()->payment_gateways()->get_available_payment_gateways()['cod'] ), 'is_woopay_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'platform_checkout' ), 'show_woopay_incompatibility_notice' => get_option( 'woopay_invalid_extension_found', false ), - 'show_express_checkout_incompatibility_notice' => $this->should_show_express_checkout_incompatibility_notice(), 'woopay_custom_message' => $this->wcpay_gateway->get_option( 'platform_checkout_custom_message' ), 'woopay_store_logo' => $this->wcpay_gateway->get_option( 'platform_checkout_store_logo' ), 'woopay_enabled_locations' => $this->wcpay_gateway->get_option( 'platform_checkout_button_locations', array_keys( $wcpay_form_fields['payment_request_button_locations']['options'] ) ), @@ -1101,38 +1100,4 @@ private function update_reporting_export_language( WP_REST_Request $request ) { $this->wcpay_gateway->update_option( 'reporting_export_language', $reporting_export_language ); } - - /** - * Whether to show the express checkout incompatibility notice. - * - * @return bool - */ - private function should_show_express_checkout_incompatibility_notice() { - // Apply filters to empty arrays to check if any plugin is modifying the checkout fields. - $after_apply_billing = apply_filters( 'woocommerce_billing_fields', [], '' ); - $after_apply_shipping = apply_filters( 'woocommerce_shipping_fields', [], '' ); - $after_apply_checkout = array_filter( - apply_filters( - 'woocommerce_checkout_fields', - [ - 'billing' => [], - 'shipping' => [], - 'account' => [], - 'order' => [], - ] - ) - ); - // All the input values are empty, so if any of them is not empty, it means that the checkout fields are being modified. - $is_modifying_checkout_fields = ! empty( - array_filter( - [ - 'after_apply_billing' => $after_apply_billing, - 'after_apply_shipping' => $after_apply_shipping, - 'after_apply_checkout' => $after_apply_checkout, - ] - ) - ); - - return $is_modifying_checkout_fields; - } } From 8963777d92b782f09fa03436ca40422a1b745ce5 Mon Sep 17 00:00:00 2001 From: Dat Hoang Date: Wed, 28 Aug 2024 17:08:10 +0700 Subject: [PATCH 23/44] Update Jetpack packages for ver 8.2.0 (#9349) --- changelog/update-jetpack-packages | 4 + composer.json | 9 +- composer.lock | 829 ++++++++++++++---------------- woocommerce-payments.php | 2 +- 4 files changed, 402 insertions(+), 442 deletions(-) create mode 100644 changelog/update-jetpack-packages diff --git a/changelog/update-jetpack-packages b/changelog/update-jetpack-packages new file mode 100644 index 00000000000..1899494f14a --- /dev/null +++ b/changelog/update-jetpack-packages @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Jetpack packages to the latest versions diff --git a/composer.json b/composer.json index ecad75b952b..afe075d9d50 100644 --- a/composer.json +++ b/composer.json @@ -22,11 +22,10 @@ "require": { "php": ">=7.2", "ext-json": "*", - "automattic/jetpack-connection": "2.8.2", - "automattic/jetpack-config": "1.15.2", - "automattic/jetpack-autoloader": "2.11.18", - "automattic/jetpack-identity-crisis": "0.19.0", - "automattic/jetpack-sync": "2.16.3", + "automattic/jetpack-connection": "2.12.4", + "automattic/jetpack-config": "2.0.4", + "automattic/jetpack-autoloader": "3.0.10", + "automattic/jetpack-sync": "3.8.0", "woocommerce/subscriptions-core": "6.7.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 8642a05ca72..6e718cccbfc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,28 +4,28 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2fdb6e373349308e7e7855402cf48871", + "content-hash": "c9198ad6fb169b71e08cb40e127c8e3f", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", - "version": "v2.0.1", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git", - "reference": "64748d02bf646e6b000f79dc8e4ea127bbd8df14" + "reference": "5753860f28e1a8629b3c6ab481c1ab75e38a244f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/64748d02bf646e6b000f79dc8e4ea127bbd8df14", - "reference": "64748d02bf646e6b000f79dc8e4ea127bbd8df14", + "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/5753860f28e1a8629b3c6ab481c1ab75e38a244f", + "reference": "5753860f28e1a8629b3c6ab481c1ab75e38a244f", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.1.1", - "yoast/phpunit-polyfills": "1.1.0" + "automattic/jetpack-changelogger": "^4.2.6", + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -52,32 +52,32 @@ ], "description": "Used to record internal usage stats for Automattic. Not visible to site owners.", "support": { - "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v2.0.1" + "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v2.0.2" }, - "time": "2024-03-12T22:00:11+00:00" + "time": "2024-08-23T14:28:10+00:00" }, { "name": "automattic/jetpack-admin-ui", - "version": "v0.4.2", + "version": "v0.4.3", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-admin-ui.git", - "reference": "cc7062363464f53bb3ae5745754f353c248d6278" + "reference": "83e500408e8542b108987b54d845ecb47b8e36b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/cc7062363464f53bb3ae5745754f353c248d6278", - "reference": "cc7062363464f53bb3ae5745754f353c248d6278", + "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/83e500408e8542b108987b54d845ecb47b8e36b8", + "reference": "83e500408e8542b108987b54d845ecb47b8e36b8", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.2", - "automattic/jetpack-logo": "^2.0.2", + "automattic/jetpack-changelogger": "^4.2.6", + "automattic/jetpack-logo": "^2.0.4", "automattic/wordbless": "dev-master", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -108,33 +108,33 @@ ], "description": "Generic Jetpack wp-admin UI elements", "support": { - "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.4.2" + "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.4.3" }, - "time": "2024-04-22T18:47:32+00:00" + "time": "2024-08-23T14:28:39+00:00" }, { "name": "automattic/jetpack-assets", - "version": "v2.1.10", + "version": "v2.3.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-assets.git", - "reference": "f4da7331e5bd2a0c511b8569e1028d5195e4bcf8" + "reference": "46ca5819bdc37587f7bfbab45168fbc3aea9facb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/f4da7331e5bd2a0c511b8569e1028d5195e4bcf8", - "reference": "f4da7331e5bd2a0c511b8569e1028d5195e4bcf8", + "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/46ca5819bdc37587f7bfbab45168fbc3aea9facb", + "reference": "46ca5819bdc37587f7bfbab45168fbc3aea9facb", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2.0.2", + "automattic/jetpack-constants": "^2.0.4", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.3", + "automattic/jetpack-changelogger": "^4.2.6", "brain/monkey": "2.6.1", "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -148,7 +148,7 @@ "link-template": "https://github.com/Automattic/jetpack-assets/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "2.1.x-dev" + "dev-trunk": "2.3.x-dev" } }, "autoload": { @@ -165,30 +165,32 @@ ], "description": "Asset management utilities for Jetpack ecosystem packages", "support": { - "source": "https://github.com/Automattic/jetpack-assets/tree/v2.1.10" + "source": "https://github.com/Automattic/jetpack-assets/tree/v2.3.4" }, - "time": "2024-05-16T10:58:04+00:00" + "time": "2024-08-23T14:29:11+00:00" }, { "name": "automattic/jetpack-autoloader", - "version": "v2.11.18", + "version": "v3.0.10", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-autoloader.git", - "reference": "53cbf0528fa6931c4fa6465bccd37514f9eda720" + "reference": "ec4c465ce6a47fb15c15ab0224ec5b1272422d3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/53cbf0528fa6931c4fa6465bccd37514f9eda720", - "reference": "53cbf0528fa6931c4fa6465bccd37514f9eda720", + "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/ec4c465ce6a47fb15c15ab0224ec5b1272422d3e", + "reference": "ec4c465ce6a47fb15c15ab0224ec5b1272422d3e", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1 || ^2.0" + "composer-plugin-api": "^1.1 || ^2.0", + "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2", - "yoast/phpunit-polyfills": "1.0.4" + "automattic/jetpack-changelogger": "^4.2.6", + "composer/composer": "^1.1 || ^2.0", + "yoast/phpunit-polyfills": "^1.1.1" }, "type": "composer-plugin", "extra": { @@ -198,8 +200,11 @@ "changelogger": { "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" }, + "version-constants": { + "::VERSION": "src/AutoloadGenerator.php" + }, "branch-alias": { - "dev-trunk": "2.11.x-dev" + "dev-trunk": "3.0.x-dev" } }, "autoload": { @@ -215,27 +220,51 @@ "GPL-2.0-or-later" ], "description": "Creates a custom autoloader for a plugin or theme.", + "keywords": [ + "autoload", + "autoloader", + "composer", + "jetpack", + "plugin", + "wordpress" + ], "support": { - "source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.11.18" + "source": "https://github.com/Automattic/jetpack-autoloader/tree/v3.0.10" }, - "time": "2023-03-29T12:51:59+00:00" + "time": "2024-08-26T14:49:14+00:00" }, { "name": "automattic/jetpack-config", - "version": "v1.15.2", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-config.git", - "reference": "f1fa6e24a89192336a1499968bf8c68e173b6e34" + "reference": "9f075c81bae6fd638e0b3183612cda5cc9e01e06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/f1fa6e24a89192336a1499968bf8c68e173b6e34", - "reference": "f1fa6e24a89192336a1499968bf8c68e173b6e34", + "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/9f075c81bae6fd638e0b3183612cda5cc9e01e06", + "reference": "9f075c81bae6fd638e0b3183612cda5cc9e01e06", "shasum": "" }, + "require": { + "php": ">=7.0" + }, "require-dev": { - "automattic/jetpack-changelogger": "^3.3.2" + "automattic/jetpack-changelogger": "^4.2.4", + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-import": "@dev", + "automattic/jetpack-jitm": "@dev", + "automattic/jetpack-post-list": "@dev", + "automattic/jetpack-publicize": "@dev", + "automattic/jetpack-search": "@dev", + "automattic/jetpack-stats": "@dev", + "automattic/jetpack-stats-admin": "@dev", + "automattic/jetpack-sync": "@dev", + "automattic/jetpack-videopress": "@dev", + "automattic/jetpack-waf": "@dev", + "automattic/jetpack-wordads": "@dev", + "automattic/jetpack-yoast-promo": "@dev" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -249,7 +278,24 @@ "link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "1.15.x-dev" + "dev-trunk": "2.0.x-dev" + }, + "dependencies": { + "test-only": [ + "packages/connection", + "packages/import", + "packages/jitm", + "packages/post-list", + "packages/publicize", + "packages/search", + "packages/stats", + "packages/stats-admin", + "packages/sync", + "packages/videopress", + "packages/waf", + "packages/wordads", + "packages/yoast-promo" + ] } }, "autoload": { @@ -263,41 +309,39 @@ ], "description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.", "support": { - "source": "https://github.com/Automattic/jetpack-config/tree/v1.15.2" + "source": "https://github.com/Automattic/jetpack-config/tree/v2.0.4" }, - "time": "2023-04-10T11:43:31+00:00" + "time": "2024-06-24T19:22:07+00:00" }, { "name": "automattic/jetpack-connection", - "version": "v2.8.2", + "version": "v2.12.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-connection.git", - "reference": "11441e20c4fa657f182bc94861d14ed5e320ab01" + "reference": "35dd5b89b9936555ac185e83a489f41655974e70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/11441e20c4fa657f182bc94861d14ed5e320ab01", - "reference": "11441e20c4fa657f182bc94861d14ed5e320ab01", + "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/35dd5b89b9936555ac185e83a489f41655974e70", + "reference": "35dd5b89b9936555ac185e83a489f41655974e70", "shasum": "" }, "require": { - "automattic/jetpack-a8c-mc-stats": "^2.0.1", - "automattic/jetpack-admin-ui": "^0.4.2", - "automattic/jetpack-assets": "^2.1.10", - "automattic/jetpack-constants": "^2.0.2", - "automattic/jetpack-redirect": "^2.0.2", - "automattic/jetpack-roles": "^2.0.2", - "automattic/jetpack-status": "^3.0.3", + "automattic/jetpack-a8c-mc-stats": "^2.0.2", + "automattic/jetpack-admin-ui": "^0.4.3", + "automattic/jetpack-assets": "^2.3.4", + "automattic/jetpack-constants": "^2.0.4", + "automattic/jetpack-redirect": "^2.0.3", + "automattic/jetpack-roles": "^2.0.3", + "automattic/jetpack-status": "^3.3.4", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.3", - "automattic/jetpack-licensing": "@dev", - "automattic/jetpack-sync": "@dev", + "automattic/jetpack-changelogger": "^4.2.6", "automattic/wordbless": "@dev", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -314,7 +358,7 @@ "link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "2.8.x-dev" + "dev-trunk": "2.12.x-dev" }, "dependencies": { "test-only": [ @@ -327,7 +371,8 @@ "classmap": [ "legacy", "src/", - "src/webhooks" + "src/webhooks", + "src/identity-crisis" ] }, "notification-url": "https://packagist.org/downloads/", @@ -336,31 +381,31 @@ ], "description": "Everything needed to connect to the Jetpack infrastructure", "support": { - "source": "https://github.com/Automattic/jetpack-connection/tree/v2.8.2" + "source": "https://github.com/Automattic/jetpack-connection/tree/v2.12.4" }, - "time": "2024-05-16T10:58:12+00:00" + "time": "2024-08-23T14:29:32+00:00" }, { "name": "automattic/jetpack-constants", - "version": "v2.0.2", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", - "reference": "6f7991f9e4475d4e2c3fd843111abfadb8a8c187" + "reference": "f6958c313a34c5e92171c45a57d9dc978e5975ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/6f7991f9e4475d4e2c3fd843111abfadb8a8c187", - "reference": "6f7991f9e4475d4e2c3fd843111abfadb8a8c187", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/f6958c313a34c5e92171c45a57d9dc978e5975ed", + "reference": "f6958c313a34c5e92171c45a57d9dc978e5975ed", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.2", + "automattic/jetpack-changelogger": "^4.2.6", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -387,91 +432,31 @@ ], "description": "A wrapper for defining constants in a more testable way.", "support": { - "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.2" + "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.4" }, - "time": "2024-04-30T19:01:57+00:00" - }, - { - "name": "automattic/jetpack-identity-crisis", - "version": "v0.19.0", - "source": { - "type": "git", - "url": "https://github.com/Automattic/jetpack-identity-crisis.git", - "reference": "a29d1be468e0b567c6e4248c6a2017e9597d5ca8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-identity-crisis/zipball/a29d1be468e0b567c6e4248c6a2017e9597d5ca8", - "reference": "a29d1be468e0b567c6e4248c6a2017e9597d5ca8", - "shasum": "" - }, - "require": { - "automattic/jetpack-assets": "^2.1.10", - "automattic/jetpack-connection": "^2.8.2", - "automattic/jetpack-constants": "^2.0.2", - "automattic/jetpack-logo": "^2.0.2", - "automattic/jetpack-status": "^3.0.3", - "php": ">=7.0" - }, - "require-dev": { - "automattic/jetpack-changelogger": "^4.2.3", - "automattic/wordbless": "@dev", - "yoast/phpunit-polyfills": "1.1.0" - }, - "suggest": { - "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." - }, - "type": "jetpack-library", - "extra": { - "autotagger": true, - "mirror-repo": "Automattic/jetpack-identity-crisis", - "textdomain": "jetpack-idc", - "version-constants": { - "::PACKAGE_VERSION": "src/class-identity-crisis.php" - }, - "changelogger": { - "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "0.19.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "description": "Identity Crisis.", - "support": { - "source": "https://github.com/Automattic/jetpack-identity-crisis/tree/v0.19.0" - }, - "time": "2024-05-16T10:58:31+00:00" + "time": "2024-08-23T14:28:14+00:00" }, { "name": "automattic/jetpack-ip", - "version": "v0.2.2", + "version": "v0.2.3", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-ip.git", - "reference": "b3efffb57901e236c3c1e7299b575563e1937013" + "reference": "f7a42b1603a24775c6f20eef2ac5cba3d6b37194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/b3efffb57901e236c3c1e7299b575563e1937013", - "reference": "b3efffb57901e236c3c1e7299b575563e1937013", + "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/f7a42b1603a24775c6f20eef2ac5cba3d6b37194", + "reference": "f7a42b1603a24775c6f20eef2ac5cba3d6b37194", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.1.1", + "automattic/jetpack-changelogger": "^4.2.6", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -502,81 +487,31 @@ ], "description": "Utilities for working with IP addresses.", "support": { - "source": "https://github.com/Automattic/jetpack-ip/tree/v0.2.2" - }, - "time": "2024-03-12T22:00:05+00:00" - }, - { - "name": "automattic/jetpack-logo", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/Automattic/jetpack-logo.git", - "reference": "25a1824973439f6cbb1e4d9bb88cb7fdd9c83305" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-logo/zipball/25a1824973439f6cbb1e4d9bb88cb7fdd9c83305", - "reference": "25a1824973439f6cbb1e4d9bb88cb7fdd9c83305", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "automattic/jetpack-changelogger": "^4.1.2", - "yoast/phpunit-polyfills": "1.1.0" - }, - "suggest": { - "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." - }, - "type": "jetpack-library", - "extra": { - "autotagger": true, - "mirror-repo": "Automattic/jetpack-logo", - "changelogger": { - "link-template": "https://github.com/Automattic/jetpack-logo/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "description": "A logo for Jetpack", - "support": { - "source": "https://github.com/Automattic/jetpack-logo/tree/v2.0.2" + "source": "https://github.com/Automattic/jetpack-ip/tree/v0.2.3" }, - "time": "2024-03-18T17:10:26+00:00" + "time": "2024-08-23T14:28:05+00:00" }, { "name": "automattic/jetpack-password-checker", - "version": "v0.3.1", + "version": "v0.3.2", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-password-checker.git", - "reference": "d9be23791e6f38debeacbf74174903f223ddfd89" + "reference": "bdf70591123932112e447e295d7f174b5c0e3a44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/d9be23791e6f38debeacbf74174903f223ddfd89", - "reference": "d9be23791e6f38debeacbf74174903f223ddfd89", + "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/bdf70591123932112e447e295d7f174b5c0e3a44", + "reference": "bdf70591123932112e447e295d7f174b5c0e3a44", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.1.1", + "automattic/jetpack-changelogger": "^4.2.6", "automattic/wordbless": "@dev", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -604,32 +539,32 @@ ], "description": "Password Checker.", "support": { - "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.3.1" + "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.3.2" }, - "time": "2024-03-14T20:42:38+00:00" + "time": "2024-08-23T14:28:17+00:00" }, { "name": "automattic/jetpack-redirect", - "version": "v2.0.2", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-redirect.git", - "reference": "7214fcd3684eb99178d6368c01f8778a182444cb" + "reference": "2c049bb08f736dc0dbafac7eaebea6f97cf8019e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/7214fcd3684eb99178d6368c01f8778a182444cb", - "reference": "7214fcd3684eb99178d6368c01f8778a182444cb", + "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/2c049bb08f736dc0dbafac7eaebea6f97cf8019e", + "reference": "2c049bb08f736dc0dbafac7eaebea6f97cf8019e", "shasum": "" }, "require": { - "automattic/jetpack-status": "^3.0.0", + "automattic/jetpack-status": "^3.3.4", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.2", + "automattic/jetpack-changelogger": "^4.2.6", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -656,31 +591,31 @@ ], "description": "Utilities to build URLs to the jetpack.com/redirect/ service", "support": { - "source": "https://github.com/Automattic/jetpack-redirect/tree/v2.0.2" + "source": "https://github.com/Automattic/jetpack-redirect/tree/v2.0.3" }, - "time": "2024-04-25T07:24:30+00:00" + "time": "2024-08-23T14:28:46+00:00" }, { "name": "automattic/jetpack-roles", - "version": "v2.0.2", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-roles.git", - "reference": "7d452c94509612e94e045b66bbfabee43fdf8728" + "reference": "32e45299a6ff93de0b1f4c71e6669f15917220fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/7d452c94509612e94e045b66bbfabee43fdf8728", - "reference": "7d452c94509612e94e045b66bbfabee43fdf8728", + "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/32e45299a6ff93de0b1f4c71e6669f15917220fb", + "reference": "32e45299a6ff93de0b1f4c71e6669f15917220fb", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.2", + "automattic/jetpack-changelogger": "^4.2.6", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -707,36 +642,35 @@ ], "description": "Utilities, related with user roles and capabilities.", "support": { - "source": "https://github.com/Automattic/jetpack-roles/tree/v2.0.2" + "source": "https://github.com/Automattic/jetpack-roles/tree/v2.0.3" }, - "time": "2024-04-22T18:47:11+00:00" + "time": "2024-08-23T14:28:15+00:00" }, { "name": "automattic/jetpack-status", - "version": "v3.0.3", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-status.git", - "reference": "dee3a6f2dcc129bd3869c44ba661484adda0a2f8" + "reference": "cf023b164ded674d66998b5b5870a3b6cf26679a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/dee3a6f2dcc129bd3869c44ba661484adda0a2f8", - "reference": "dee3a6f2dcc129bd3869c44ba661484adda0a2f8", + "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/cf023b164ded674d66998b5b5870a3b6cf26679a", + "reference": "cf023b164ded674d66998b5b5870a3b6cf26679a", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2.0.2", + "automattic/jetpack-constants": "^2.0.4", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.3", + "automattic/jetpack-changelogger": "^4.2.6", "automattic/jetpack-connection": "@dev", - "automattic/jetpack-identity-crisis": "@dev", - "automattic/jetpack-ip": "^0.2.2", + "automattic/jetpack-ip": "^0.2.3", "automattic/jetpack-plans": "@dev", "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -749,12 +683,11 @@ "link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "3.0.x-dev" + "dev-trunk": "3.3.x-dev" }, "dependencies": { "test-only": [ "packages/connection", - "packages/identity-crisis", "packages/plans" ] } @@ -770,40 +703,39 @@ ], "description": "Used to retrieve information about the current status of Jetpack and the site overall.", "support": { - "source": "https://github.com/Automattic/jetpack-status/tree/v3.0.3" + "source": "https://github.com/Automattic/jetpack-status/tree/v3.3.4" }, - "time": "2024-05-08T10:06:11+00:00" + "time": "2024-08-23T14:28:43+00:00" }, { "name": "automattic/jetpack-sync", - "version": "v2.16.3", + "version": "v3.8.0", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-sync.git", - "reference": "f98ab01117b57ac948616a284917f6ff3db8cbcf" + "reference": "30b29f0c5a27e01cbf2fa592fbde97f617665153" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/f98ab01117b57ac948616a284917f6ff3db8cbcf", - "reference": "f98ab01117b57ac948616a284917f6ff3db8cbcf", + "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/30b29f0c5a27e01cbf2fa592fbde97f617665153", + "reference": "30b29f0c5a27e01cbf2fa592fbde97f617665153", "shasum": "" }, "require": { - "automattic/jetpack-connection": "^2.8.2", - "automattic/jetpack-constants": "^2.0.2", - "automattic/jetpack-identity-crisis": "^0.19.0", - "automattic/jetpack-ip": "^0.2.2", - "automattic/jetpack-password-checker": "^0.3.1", - "automattic/jetpack-roles": "^2.0.2", - "automattic/jetpack-status": "^3.0.3", + "automattic/jetpack-connection": "^2.12.4", + "automattic/jetpack-constants": "^2.0.4", + "automattic/jetpack-ip": "^0.2.3", + "automattic/jetpack-password-checker": "^0.3.2", + "automattic/jetpack-roles": "^2.0.3", + "automattic/jetpack-status": "^3.3.4", "php": ">=7.0" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.3", + "automattic/jetpack-changelogger": "^4.2.6", "automattic/jetpack-search": "@dev", - "automattic/jetpack-waf": "^0.16.7", + "automattic/jetpack-waf": "^0.18.4", "automattic/wordbless": "@dev", - "yoast/phpunit-polyfills": "1.1.0" + "yoast/phpunit-polyfills": "^1.1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -820,7 +752,7 @@ "link-template": "https://github.com/Automattic/jetpack-sync/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "2.16.x-dev" + "dev-trunk": "3.8.x-dev" }, "dependencies": { "test-only": [ @@ -840,9 +772,9 @@ ], "description": "Everything needed to allow syncing to the WP.com infrastructure.", "support": { - "source": "https://github.com/Automattic/jetpack-sync/tree/v2.16.3" + "source": "https://github.com/Automattic/jetpack-sync/tree/v3.8.0" }, - "time": "2024-05-16T10:58:32+00:00" + "time": "2024-08-26T14:49:56+00:00" }, { "name": "composer/installers", @@ -1053,16 +985,16 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.6.2", + "version": "v2.6.4", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", "shasum": "" }, "require": { @@ -1074,8 +1006,8 @@ "ext-json": "*", "jetbrains/phpstorm-stubs": "^2019.3", "phpunit/phpunit": "^7 | ^8 | ^9", - "psalm/phar": "^3.11@dev", - "react/promise": "^2" + "react/promise": "^2", + "vimeo/psalm": "^3.12" }, "type": "library", "extra": { @@ -1130,7 +1062,7 @@ "support": { "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.2" + "source": "https://github.com/amphp/amp/tree/v2.6.4" }, "funding": [ { @@ -1138,20 +1070,20 @@ "type": "github" } ], - "time": "2022-02-20T17:52:18+00:00" + "time": "2024-03-21T18:52:26+00:00" }, { "name": "amphp/byte-stream", - "version": "v1.8.1", + "version": "v1.8.2", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", "shasum": "" }, "require": { @@ -1167,11 +1099,6 @@ "psalm/phar": "^3.11.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { "files": [ "lib/functions.php" @@ -1195,7 +1122,7 @@ } ], "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "http://amphp.org/byte-stream", + "homepage": "https://amphp.org/byte-stream", "keywords": [ "amp", "amphp", @@ -1205,9 +1132,8 @@ "stream" ], "support": { - "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" }, "funding": [ { @@ -1215,7 +1141,7 @@ "type": "github" } ], - "time": "2021-03-30T17:13:30+00:00" + "time": "2024-04-13T18:00:56+00:00" }, { "name": "automattic/jetpack-changelogger", @@ -1632,16 +1558,16 @@ }, { "name": "composer/semver", - "version": "3.4.0", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", "shasum": "" }, "require": { @@ -1693,7 +1619,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" + "source": "https://github.com/composer/semver/tree/3.4.2" }, "funding": [ { @@ -1709,7 +1635,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2024-07-12T11:35:52+00:00" }, { "name": "composer/xdebug-handler", @@ -2384,16 +2310,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -2401,11 +2327,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -2431,7 +2358,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -2439,7 +2366,7 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "netresearch/jsonmapper", @@ -2494,21 +2421,21 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v4.19.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.1" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", @@ -2544,9 +2471,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "openlss/lib-array2xml", @@ -2871,28 +2798,28 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "paragonie/random_compat": "dev-master", "paragonie/sodium_compat": "dev-master" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -2922,22 +2849,37 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2022-10-25T01:46:02+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:30:46+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.4", + "version": "2.1.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", "shasum": "" }, "require": { @@ -2945,10 +2887,10 @@ "phpcompatibility/phpcompatibility-paragonie": "^1.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -2977,9 +2919,24 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2022-10-24T09:00:36+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:37:59+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -3061,22 +3018,22 @@ }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.9", + "version": "1.0.12", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "908247bc65010c7b7541a9551e002db12e9dae70" + "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/908247bc65010c7b7541a9551e002db12e9dae70", - "reference": "908247bc65010c7b7541a9551e002db12e9dae70", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", + "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.8.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" }, "require-dev": { "ext-filter": "*", @@ -3145,7 +3102,7 @@ "type": "open_collective" } ], - "time": "2023-12-08T14:50:00+00:00" + "time": "2024-05-20T13:34:27+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3386,16 +3343,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.26.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", - "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -3427,41 +3384,41 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-02-23T16:05:55+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3470,7 +3427,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -3499,7 +3456,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -3507,7 +3464,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4025,16 +3982,16 @@ }, { "name": "react/stream", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/reactphp/stream.git", - "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66" + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/6fbc9672905c7d5a885f2da2fc696f65840f4a66", - "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { @@ -4044,7 +4001,7 @@ }, "require-dev": { "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { @@ -4091,7 +4048,7 @@ ], "support": { "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.3.0" + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, "funding": [ { @@ -4099,7 +4056,7 @@ "type": "open_collective" } ], - "time": "2023-06-16T10:52:11+00:00" + "time": "2024-06-11T12:45:25+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -5112,16 +5069,16 @@ }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.17", + "version": "v2.11.19", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" + "reference": "bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1", + "reference": "bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1", "shasum": "" }, "require": { @@ -5166,7 +5123,7 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2023-08-05T23:46:11+00:00" + "time": "2024-06-26T20:08:34+00:00" }, { "name": "slevomat/coding-standard", @@ -5297,16 +5254,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", + "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", "shasum": "" }, "require": { @@ -5373,20 +5330,20 @@ "type": "open_collective" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2024-07-21T23:26:44+00:00" }, { "name": "symfony/console", - "version": "v5.4.36", + "version": "v5.4.42", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e" + "reference": "cef62396a0477e94fc52e87a17c6e5c32e226b7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", - "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", + "url": "https://api.github.com/repos/symfony/console/zipball/cef62396a0477e94fc52e87a17c6e5c32e226b7f", + "reference": "cef62396a0477e94fc52e87a17c6e5c32e226b7f", "shasum": "" }, "require": { @@ -5456,7 +5413,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.36" + "source": "https://github.com/symfony/console/tree/v5.4.42" }, "funding": [ { @@ -5472,20 +5429,20 @@ "type": "tidelift" } ], - "time": "2024-02-20T16:33:57+00:00" + "time": "2024-07-26T12:21:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + "reference": "80d075412b557d41002320b96a096ca65aa2c98d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d", "shasum": "" }, "require": { @@ -5523,7 +5480,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" }, "funding": [ { @@ -5539,20 +5496,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-01-24T14:02:46+00:00" }, { "name": "symfony/finder", - "version": "v5.4.35", + "version": "v5.4.42", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435" + "reference": "0724c51fa067b198e36506d2864e09a52180998a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/abe6d6f77d9465fed3cd2d029b29d03b56b56435", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435", + "url": "https://api.github.com/repos/symfony/finder/zipball/0724c51fa067b198e36506d2864e09a52180998a", + "reference": "0724c51fa067b198e36506d2864e09a52180998a", "shasum": "" }, "require": { @@ -5586,7 +5543,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.35" + "source": "https://github.com/symfony/finder/tree/v5.4.42" }, "funding": [ { @@ -5602,20 +5559,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-07-22T08:53:29+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -5665,7 +5622,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -5681,20 +5638,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -5743,7 +5700,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -5759,20 +5716,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -5824,7 +5781,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -5840,20 +5797,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -5904,7 +5861,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -5920,20 +5877,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", "shasum": "" }, "require": { @@ -5980,7 +5937,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" }, "funding": [ { @@ -5996,20 +5953,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -6060,7 +6017,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -6076,20 +6033,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/process", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "shasum": "" }, "require": { @@ -6122,7 +6079,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.36" + "source": "https://github.com/symfony/process/tree/v5.4.40" }, "funding": [ { @@ -6138,20 +6095,20 @@ "type": "tidelift" } ], - "time": "2024-02-12T15:49:53+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", "shasum": "" }, "require": { @@ -6205,7 +6162,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" }, "funding": [ { @@ -6221,20 +6178,20 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:29+00:00" + "time": "2023-04-21T15:04:16+00:00" }, { "name": "symfony/string", - "version": "v5.4.36", + "version": "v5.4.42", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b" + "reference": "909cec913edea162a3b2836788228ad45fcab337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e232c83622bd8cd32b794216aa29d0d266d353b", - "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b", + "url": "https://api.github.com/repos/symfony/string/zipball/909cec913edea162a3b2836788228ad45fcab337", + "reference": "909cec913edea162a3b2836788228ad45fcab337", "shasum": "" }, "require": { @@ -6291,7 +6248,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.36" + "source": "https://github.com/symfony/string/tree/v5.4.42" }, "funding": [ { @@ -6307,20 +6264,20 @@ "type": "tidelift" } ], - "time": "2024-02-01T08:49:30+00:00" + "time": "2024-07-20T18:38:32+00:00" }, { "name": "symfony/yaml", - "version": "v5.4.35", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e78db7f5c70a21f0417a31f414c4a95fe76c07e4" + "reference": "81cad0ceab3d61fe14fe941ff18a230ac9c80f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e78db7f5c70a21f0417a31f414c4a95fe76c07e4", - "reference": "e78db7f5c70a21f0417a31f414c4a95fe76c07e4", + "url": "https://api.github.com/repos/symfony/yaml/zipball/81cad0ceab3d61fe14fe941ff18a230ac9c80f83", + "reference": "81cad0ceab3d61fe14fe941ff18a230ac9c80f83", "shasum": "" }, "require": { @@ -6366,7 +6323,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.35" + "source": "https://github.com/symfony/yaml/tree/v5.4.40" }, "funding": [ { @@ -6382,7 +6339,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "theseer/tokenizer", @@ -6817,16 +6774,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.0.1", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" + "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", "shasum": "" }, "require": { @@ -6835,16 +6792,16 @@ "ext-tokenizer": "*", "ext-xmlreader": "*", "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.1.0", - "phpcsstandards/phpcsutils": "^1.0.8", - "squizlabs/php_codesniffer": "^3.7.2" + "phpcsstandards/phpcsextra": "^1.2.1", + "phpcsstandards/phpcsutils": "^1.0.10", + "squizlabs/php_codesniffer": "^3.9.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.0", "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "suggest": { "ext-iconv": "For improved results", @@ -6875,11 +6832,11 @@ }, "funding": [ { - "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", + "url": "https://opencollective.com/php_codesniffer", "type": "custom" } ], - "time": "2023-09-14T07:06:09+00:00" + "time": "2024-03-25T16:39:00+00:00" }, { "name": "yoast/phpunit-polyfills", @@ -7014,5 +6971,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/woocommerce-payments.php b/woocommerce-payments.php index b3aa0cdfe1e..839e03055e9 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -43,7 +43,7 @@ function wcpay_activated() { // Only redirect to onboarding when activated on its own. Either with a link... isset( $_GET['action'] ) && 'activate' === $_GET['action'] // phpcs:ignore WordPress.Security.NonceVerification // ...or with a bulk action. - || isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) && 1 === count( $_POST['checked'] ) // phpcs:ignore WordPress.Security.NonceVerification + || isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) && 1 === count( $_POST['checked'] ) // phpcs:ignore WordPress.Security.NonceVerification, Generic.CodeAnalysis.RequireExplicitBooleanOperatorPrecedence.MissingParentheses ) { update_option( 'wcpay_should_redirect_to_onboarding', true ); } From 1f8d7d709f4d755c8a84b03a678c960b738fdd0d Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:15:18 -0500 Subject: [PATCH 24/44] Support merchant site styles for WooPay when initializing through classic checkout (#9308) --- ...woopay-theme-support-from-classic-checkout | 4 ++++ client/checkout/upe-styles/index.js | 24 +++++++++++++++++++ client/checkout/utils/index.js | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-woopay-theme-support-from-classic-checkout diff --git a/changelog/fix-woopay-theme-support-from-classic-checkout b/changelog/fix-woopay-theme-support-from-classic-checkout new file mode 100644 index 00000000000..3642cde31ca --- /dev/null +++ b/changelog/fix-woopay-theme-support-from-classic-checkout @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix support for merchant site styling when initializing WooPay via classic checkout diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js index 8fa4ece207e..db3723aae6f 100644 --- a/client/checkout/upe-styles/index.js +++ b/client/checkout/upe-styles/index.js @@ -123,6 +123,27 @@ export const appearanceSelectors = { buttonSelectors: [ '.wc-block-cart__submit-button' ], linkSelectors: [ 'a' ], }, + wooPayClassicCheckout: { + appendTarget: '.woocommerce-billing-fields__field-wrapper', + upeThemeInputSelector: '#billing_first_name', + upeThemeLabelSelector: '.woocommerce-checkout .form-row label', + rowElement: 'p', + validClasses: [ 'form-row' ], + invalidClasses: [ + 'form-row', + 'woocommerce-invalid', + 'woocommerce-invalid-required-field', + ], + backgroundSelectors: [ + '#customer_details', + '#order_review', + 'form.checkout', + 'body', + ], + headingSelectors: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], + buttonSelectors: [ '#place_order' ], + linkSelectors: [ 'a' ], + }, /** * Update selectors to use alternate if not present on DOM. @@ -175,6 +196,9 @@ export const appearanceSelectors = { case 'bnpl_cart_block': appearanceSelector = this.bnplCartBlock; break; + case 'woopay_shortcode_checkout': + appearanceSelector = this.wooPayClassicCheckout; + break; } return { diff --git a/client/checkout/utils/index.js b/client/checkout/utils/index.js index b0c14b7ffa5..de8ed29d773 100644 --- a/client/checkout/utils/index.js +++ b/client/checkout/utils/index.js @@ -20,7 +20,7 @@ export const getAppearanceType = () => { } if ( document.querySelector( '.woocommerce-billing-fields' ) ) { - return 'shortcode_checkout'; + return 'woopay_shortcode_checkout'; } if ( document.querySelector( '.wp-block-woocommerce-cart' ) ) { From 477fa2b60dee0f3d01e31ab369fd3928e0a8cfd6 Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:53:19 -0500 Subject: [PATCH 25/44] Move WooPay global theme checkbox to the appearance section (#9321) --- changelog/fix-move-woopay-theme-checkbox | 4 +++ ...general-payment-request-button-settings.js | 31 ------------------- .../test/payment-request-settings.test.js | 3 -- .../test/woopay-settings.test.js | 3 ++ .../woopay-settings.js | 31 +++++++++++++++++++ 5 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 changelog/fix-move-woopay-theme-checkbox diff --git a/changelog/fix-move-woopay-theme-checkbox b/changelog/fix-move-woopay-theme-checkbox new file mode 100644 index 00000000000..615b9728fdc --- /dev/null +++ b/changelog/fix-move-woopay-theme-checkbox @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Move woopay theme support checkbox to the appearance section. diff --git a/client/settings/express-checkout-settings/general-payment-request-button-settings.js b/client/settings/express-checkout-settings/general-payment-request-button-settings.js index 9154cf7fcde..296a09b08b0 100644 --- a/client/settings/express-checkout-settings/general-payment-request-button-settings.js +++ b/client/settings/express-checkout-settings/general-payment-request-button-settings.js @@ -7,7 +7,6 @@ import { __, sprintf } from '@wordpress/i18n'; import { // eslint-disable-next-line @wordpress/no-unsafe-wp-apis __experimentalNumberControl as NumberControl, - CheckboxControl, SelectControl, RadioControl, RangeControl, @@ -32,7 +31,6 @@ import { usePaymentRequestButtonBorderRadius, usePaymentRequestEnabledSettings, useWooPayEnabledSettings, - useWooPayGlobalThemeSupportEnabledSettings, } from 'wcpay/data'; const makeButtonSizeText = ( string ) => @@ -167,11 +165,6 @@ const GeneralPaymentRequestButtonSettings = ( { type } ) => { isPaymentRequestEnabled && isWooPayFeatureFlagEnabled; - const [ - isWooPayGlobalThemeSupportEnabled, - updateIsWooPayGlobalThemeSupportEnabled, - ] = useWooPayGlobalThemeSupportEnabledSettings(); - return ( { showWarning && ( @@ -267,30 +260,6 @@ const GeneralPaymentRequestButtonSettings = ( { type } ) => {

) } - { wcpaySettings.isWooPayGlobalThemeSupportEligible && - type === 'woopay' && ( - <> -

- { __( - 'WooPay Global Theme Support', - 'woocommerce-payments' - ) } -

-
- -
- - ) }

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

{ __( 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 5094c44ee06..4bc3a0b6e12 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 @@ -29,9 +29,6 @@ jest.mock( '../../../data', () => ( { usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayEnabledSettings: jest.fn(), useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), - useWooPayGlobalThemeSupportEnabledSettings: jest - .fn() - .mockReturnValue( [ false, jest.fn() ] ), } ) ); 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 5846e397ad6..2fcab112599 100644 --- a/client/settings/express-checkout-settings/test/woopay-settings.test.js +++ b/client/settings/express-checkout-settings/test/woopay-settings.test.js @@ -30,6 +30,9 @@ jest.mock( '../../../data', () => ( { usePaymentRequestButtonTheme: jest.fn(), useWooPayLocations: jest.fn(), useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), + useWooPayGlobalThemeSupportEnabledSettings: jest + .fn() + .mockReturnValue( [ false, jest.fn() ] ), } ) ); jest.mock( '@wordpress/data', () => ( { diff --git a/client/settings/express-checkout-settings/woopay-settings.js b/client/settings/express-checkout-settings/woopay-settings.js index 1cf1c72ea31..aa73506dde1 100644 --- a/client/settings/express-checkout-settings/woopay-settings.js +++ b/client/settings/express-checkout-settings/woopay-settings.js @@ -26,6 +26,7 @@ import { useWooPayStoreLogo, useWooPayLocations, useWooPayShowIncompatibilityNotice, + useWooPayGlobalThemeSupportEnabledSettings, } from 'wcpay/data'; import GeneralPaymentRequestButtonSettings from './general-payment-request-button-settings'; import { WooPayIncompatibilityNotice } from '../settings-warnings/incompatibility-notice'; @@ -41,6 +42,11 @@ const WooPaySettings = ( { section } ) => { setWooPayCustomMessage, ] = useWooPayCustomMessage(); + const [ + isWooPayGlobalThemeSupportEnabled, + updateIsWooPayGlobalThemeSupportEnabled, + ] = useWooPayGlobalThemeSupportEnabledSettings(); + const [ woopayStoreLogo, setWooPayStoreLogo ] = useWooPayStoreLogo(); const [ woopayLocations, updateWooPayLocations ] = useWooPayLocations(); @@ -203,6 +209,31 @@ const WooPaySettings = ( { section } ) => { updateFileID={ setWooPayStoreLogo } />
+ { wcpaySettings.isWooPayGlobalThemeSupportEligible && ( +
+

+ { __( + 'WooPay Global Theme Support', + 'woocommerce-payments' + ) } +

+
+ +
+
+ ) }

{ __( From a9cfe1e8a65ed9ee0850db7194f9707a46a22e78 Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Wed, 28 Aug 2024 16:58:35 -0300 Subject: [PATCH 26/44] Change WooPay session route namespace (#9317) --- changelog/fix-woopay-direct-checkout-route-namespace | 4 ++++ includes/admin/class-wc-rest-woopay-session-controller.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-woopay-direct-checkout-route-namespace diff --git a/changelog/fix-woopay-direct-checkout-route-namespace b/changelog/fix-woopay-direct-checkout-route-namespace new file mode 100644 index 00000000000..01e5dbb1384 --- /dev/null +++ b/changelog/fix-woopay-direct-checkout-route-namespace @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix WooPay direct checkout. diff --git a/includes/admin/class-wc-rest-woopay-session-controller.php b/includes/admin/class-wc-rest-woopay-session-controller.php index 6cbdff4d9a0..d5069412c6f 100644 --- a/includes/admin/class-wc-rest-woopay-session-controller.php +++ b/includes/admin/class-wc-rest-woopay-session-controller.php @@ -21,14 +21,14 @@ class WC_REST_WooPay_Session_Controller extends WP_REST_Controller { * * @var string */ - protected $namespace = 'wc/v3'; + protected $namespace = 'payments/woopay'; /** * Endpoint path. * * @var string */ - protected $rest_base = 'woopay/session'; + protected $rest_base = 'session'; /** * Configure REST API routes. From 955571e70fd0180e538e2b320914ad03c6173faf Mon Sep 17 00:00:00 2001 From: Cvetan Cvetanov Date: Thu, 29 Aug 2024 13:07:35 +0300 Subject: [PATCH 27/44] Fix Connect page scrolls to top when the Enable Sandbox Mode button is clicked (#9343) Co-authored-by: Dan Paun <82826872+dpaun1985@users.noreply.github.com> Co-authored-by: Vlad Olaru --- changelog/fix-9324-test-drive-onboarding-scoll | 4 ++++ client/connect-account-page/index.tsx | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-9324-test-drive-onboarding-scoll diff --git a/changelog/fix-9324-test-drive-onboarding-scoll b/changelog/fix-9324-test-drive-onboarding-scoll new file mode 100644 index 00000000000..d290ac6e168 --- /dev/null +++ b/changelog/fix-9324-test-drive-onboarding-scoll @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed an issue where the Connect page would scroll to the top upon clicking the Enable Sandbox Mode button. diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index e4eb86aef2f..e0163d5b788 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -206,11 +206,6 @@ const ConnectAccountPage: React.FC = () => { setTestDriveModeSubmitted( true ); trackConnectAccountClicked( true ); - // Scroll the page to the top to ensure the logo is visible. - window.scrollTo( { - top: 0, - } ); - const customizedConnectUrl = addQueryArgs( connectUrl, { test_drive: 'true', } ); From 6e2fc653542b32712e130f86223c2cb99b447f33 Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Thu, 29 Aug 2024 16:49:20 -0300 Subject: [PATCH 28/44] Redirect user to WooPay OTP when the email is saved (#9354) --- changelog/fix-woopay-email-redirect | 4 ++++ client/checkout/blocks/style.scss | 2 -- .../express-button/express-checkout-iframe.js | 2 +- client/checkout/woopay/index.js | 2 +- .../woopay/save-user/checkout-page-save-user.js | 14 ++++++++------ includes/class-wc-payments-utils.php | 2 +- .../class-wc-payments-woopay-button-handler.php | 4 ++++ includes/woopay/class-woopay-session.php | 17 ++++++++++++++++- 8 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 changelog/fix-woopay-email-redirect diff --git a/changelog/fix-woopay-email-redirect b/changelog/fix-woopay-email-redirect new file mode 100644 index 00000000000..9bba71ff1a8 --- /dev/null +++ b/changelog/fix-woopay-email-redirect @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Redirect user to WooPay OTP when the email is saved. diff --git a/client/checkout/blocks/style.scss b/client/checkout/blocks/style.scss index 980c587b4d4..ab2878f1a03 100644 --- a/client/checkout/blocks/style.scss +++ b/client/checkout/blocks/style.scss @@ -75,8 +75,6 @@ button.wcpay-stripelink-modal-trigger:hover { #remember-me { margin: 36px 0 0 0; - padding: 0; - border: 0; h2 { font-size: 18px; diff --git a/client/checkout/woopay/express-button/express-checkout-iframe.js b/client/checkout/woopay/express-button/express-checkout-iframe.js index 1abe335763a..6685d237f49 100644 --- a/client/checkout/woopay/express-button/express-checkout-iframe.js +++ b/client/checkout/woopay/express-button/express-checkout-iframe.js @@ -285,5 +285,5 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => { } } - openIframe( woopayEmailInput?.value ); + openIframe( woopayEmailInput?.value || getConfig( 'woopaySessionEmail' ) ); }; diff --git a/client/checkout/woopay/index.js b/client/checkout/woopay/index.js index a9766f4c4ee..cdc2ca864f3 100644 --- a/client/checkout/woopay/index.js +++ b/client/checkout/woopay/index.js @@ -31,7 +31,7 @@ const renderSaveUserSection = () => { )?.[ 0 ]; checkoutPageSaveUserContainer.className = - 'wc-block-checkout__payment-method wp-block-woocommerce-checkout-remember-block '; + 'wc-block-checkout__payment-method wp-block-woocommerce-checkout-remember-block wc-block-components-checkout-step '; checkoutPageSaveUserContainer.id = 'remember-me'; if ( paymentOptions ) { diff --git a/client/components/woopay/save-user/checkout-page-save-user.js b/client/components/woopay/save-user/checkout-page-save-user.js index 4087255a68b..69cde030454 100644 --- a/client/components/woopay/save-user/checkout-page-save-user.js +++ b/client/components/woopay/save-user/checkout-page-save-user.js @@ -86,7 +86,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { rememberMe.removeAttribute( 'disabled', 'disabled' ); }, [ checkoutIsProcessing, isBlocksCheckout ] ); - const getPhoneFieldValue = () => { + const getPhoneFieldValue = useCallback( () => { let phoneFieldValue = ''; if ( isBlocksCheckout ) { phoneFieldValue = @@ -109,7 +109,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { } return phoneFieldValue; - }; + }, [ isBlocksCheckout ] ); const sendExtensionData = useCallback( ( shouldClearData = false ) => { @@ -172,7 +172,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { } ); }, [] ); - const updatePhoneNumberValidationError = useCallback( () => { + useEffect( () => { if ( ! isSaveDetailsChecked ) { clearValidationError( errorId ); if ( isPhoneValid !== null ) { @@ -218,6 +218,10 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { ? isWCPayChosen : isWCPayChosen && isNewPaymentTokenChosen; + useEffect( () => { + setPhoneNumber( getPhoneFieldValue() ); + }, [ getPhoneFieldValue, isWCPayWithNewTokenChosen ] ); + if ( ! getConfig( 'forceNetworkSavedCards' ) || ! isWCPayWithNewTokenChosen || @@ -232,8 +236,6 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { return null; } - updatePhoneNumberValidationError(); - return ( { ) } - + { __( 'Securely save my information for 1-click checkout', 'woocommerce-payments' diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 58b3ce95451..5045871a3bd 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -51,7 +51,7 @@ class WC_Payments_Utils { '@^\/wc\/store(\/v[\d]+)?\/order\/(?P[\d]+)@', // The route below is not a Store API route. However, this REST endpoint is used by WooPay to indirectly reach the Store API. // By adding it to this list, we're able to identify the user and load the correct session for this route. - '@^\/wc\/v3\/woopay\/session$@', + '@^\/payments\/woopay\/session$@', ]; /** diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index 0cf57cac7f1..391358f97ec 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -16,6 +16,7 @@ use WCPay\Exceptions\Invalid_Price_Exception; use WCPay\Logger; use WCPay\Payment_Information; +use WCPay\WooPay\WooPay_Session; use WCPay\WooPay\WooPay_Utilities; /** @@ -148,10 +149,13 @@ public function init() { * @return array The modified config array. */ public function add_woopay_config( $config ) { + $user = wp_get_current_user(); + $config['woopayButton'] = $this->get_button_settings(); $config['woopayButtonNonce'] = wp_create_nonce( 'woopay_button_nonce' ); $config['addToCartNonce'] = wp_create_nonce( 'wcpay-add-to-cart' ); $config['shouldShowWooPayButton'] = $this->should_show_woopay_button(); + $config['woopaySessionEmail'] = WooPay_Session::get_user_email( $user ); return $config; } diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index a75ca96250f..cde7ffc0e5f 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -385,7 +385,7 @@ private static function get_checkout_data( $woopay_request ) { * @param \WP_User $user The user object. * @return string The user email. */ - private static function get_user_email( $user ) { + public static function get_user_email( $user ) { if ( ! empty( $_POST['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return sanitize_email( wp_unslash( $_POST['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } @@ -403,6 +403,21 @@ private static function get_user_email( $user ) { } } + // Get the email from the customer object if it's available. + if ( ! empty( WC()->customer ) ) { + $billing_email = WC()->customer->get_billing_email(); + + if ( ! empty( $billing_email ) ) { + return $billing_email; + } + + $customer_email = WC()->customer->get_email(); + + if ( ! empty( $customer_email ) ) { + return $customer_email; + } + } + // As a last resort, we try to get the email from the customer logged in the store. if ( $user->exists() ) { return $user->user_email; From ea84f7a8783feeb5189743f02a50eb86a458ded2 Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Fri, 30 Aug 2024 04:46:13 +0200 Subject: [PATCH 29/44] Fix saved cards e2e tests (#9359) --- changelog/fix-saved-cards-e2e | 4 ++++ tests/e2e/utils/payments.js | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-saved-cards-e2e diff --git a/changelog/fix-saved-cards-e2e b/changelog/fix-saved-cards-e2e new file mode 100644 index 00000000000..9aa7abb6cfc --- /dev/null +++ b/changelog/fix-saved-cards-e2e @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Fix failing e2e tests for saved cards. diff --git a/tests/e2e/utils/payments.js b/tests/e2e/utils/payments.js index 25c69b47a23..9754bc85418 100644 --- a/tests/e2e/utils/payments.js +++ b/tests/e2e/utils/payments.js @@ -277,9 +277,7 @@ export async function setupCheckout( billingDetails ) { // field changes. Need to wait to make sure that all key presses were processed by that mechanism. await page.waitForTimeout( 1000 ); await uiUnblocked(); - await expect( page ).toClick( - '.wc_payment_method.payment_method_woocommerce_payments' - ); + await page.click( 'label[for="payment_method_woocommerce_payments"]' ); } // Copy of the fillBillingDetails function from woocommerce/e2e-utils/src/flows/shopper.js From 0dd2726095b6b8e73848a9c703b4318355fc310a Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Fri, 30 Aug 2024 06:35:01 +0300 Subject: [PATCH 30/44] Dev: Fix Klarna product page message E2E test (#9361) Co-authored-by: Dat Hoang --- changelog/dev-fix-klarna-product-page-e2e | 4 ++++ .../wcpay/shopper/shopper-klarna-checkout.spec.js | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 changelog/dev-fix-klarna-product-page-e2e diff --git a/changelog/dev-fix-klarna-product-page-e2e b/changelog/dev-fix-klarna-product-page-e2e new file mode 100644 index 00000000000..f84395e5f44 --- /dev/null +++ b/changelog/dev-fix-klarna-product-page-e2e @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix Klarna product page message E2E test after the contents inside the iframe were updated. diff --git a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js index 7b10010a9cc..6bf22fd4f0a 100644 --- a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js +++ b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js @@ -46,12 +46,10 @@ describe( 'Klarna checkout', () => { const paymentMethodMessageIframe = await paymentMethodMessageFrameHandle.contentFrame(); // Click on Klarna link to open the modal. - await paymentMethodMessageIframe.evaluate( ( selector ) => { - const element = document.querySelector( selector ); - if ( element ) { - element.click(); - } - }, 'a[aria-label="Open Learn More Modal"]' ); + const learnMoreLink = await paymentMethodMessageIframe.waitForSelector( + 'a[aria-haspopup="dialog"]' + ); + await learnMoreLink.click(); // Wait for the iframe to be added by Stripe JS after clicking on the element. await page.waitForTimeout( 1000 ); From 638a650a49a337a369aec310498e5d2b631fa2d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:15:10 -0300 Subject: [PATCH 31/44] Fix BNPL payment method messaging loading state (#9355) --- changelog/fix-9244-pmme-skeleton | 4 ++ .../bnpl-site-messaging/index.js | 32 +++++---- ...ments-payment-method-messaging-element.php | 27 ++++---- includes/class-wc-payments-utils.php | 68 ++++++++++++++++++- tests/unit/test-class-wc-payments-utils.php | 6 ++ 5 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 changelog/fix-9244-pmme-skeleton diff --git a/changelog/fix-9244-pmme-skeleton b/changelog/fix-9244-pmme-skeleton new file mode 100644 index 00000000000..ba34b475bff --- /dev/null +++ b/changelog/fix-9244-pmme-skeleton @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Prevent preload of BNPL messaging if minimum order amount isn't hit. diff --git a/client/product-details/bnpl-site-messaging/index.js b/client/product-details/bnpl-site-messaging/index.js index 91be16744ab..36cb845fb71 100644 --- a/client/product-details/bnpl-site-messaging/index.js +++ b/client/product-details/bnpl-site-messaging/index.js @@ -8,14 +8,6 @@ import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles'; import { getUPEConfig } from 'wcpay/utils/checkout'; import apiRequest from 'wcpay/checkout/utils/request'; -/** - * Initializes the appearance of the payment element by retrieving the UPE configuration - * from the API and saving the appearance if it doesn't exist. If the appearance already exists, - * it is simply returned. - * - * @param {Object} api The API object used to save the UPE configuration. - * @return {Promise} The appearance object for the UPE. - */ const elementsLocations = { bnplProductPage: { configKey: 'upeBnplProductPageAppearance', @@ -27,6 +19,16 @@ const elementsLocations = { }, }; +/** + * Initializes the appearance of the payment element by retrieving the UPE configuration + * from the API and saving the appearance if it doesn't exist. If the appearance already exists, + * it is simply returned. + * + * @param {Object} api The API object used to save the UPE configuration. + * @param {string} location The location of the UPE. + * + * @return {Promise} The appearance object for the UPE. + */ async function initializeAppearance( api, location ) { const { configKey, appearanceKey } = elementsLocations[ location ]; @@ -53,16 +55,25 @@ export const initializeBnplSiteMessaging = async () => { isCart, isCartBlock, cartTotal, + minimumOrderAmount, } = window.wcpayStripeSiteMessaging; let amount; let elementLocation = 'bnplProductPage'; + const minOrderAmount = parseInt( minimumOrderAmount, 10 ) || 0; + const paymentMessageContainer = document.getElementById( + 'payment-method-message' + ); if ( isCart || isCartBlock ) { amount = parseInt( cartTotal, 10 ) || 0; elementLocation = 'bnplClassicCart'; } else { amount = parseInt( productVariations.base_product.amount, 10 ) || 0; + + if ( amount < minOrderAmount ) { + paymentMessageContainer.style.setProperty( 'display', 'none' ); + } } let paymentMessageElement; @@ -140,9 +151,6 @@ export const initializeBnplSiteMessaging = async () => { } // Set the `--wc-bnpl-margin-bottom` CSS variable to the computed bottom margin of the price element. - const paymentMessageContainer = document.getElementById( - 'payment-method-message' - ); paymentMessageContainer.style.setProperty( '--wc-bnpl-margin-bottom', bottomMargin @@ -208,7 +216,7 @@ export const initializeBnplSiteMessaging = async () => { pmme.style.setProperty( '--wc-bnpl-margin-bottom', '-4px' ); }, 2000 ); } else { - paymentMessageLoading.remove(); + paymentMessageLoading?.remove(); } } ); } diff --git a/includes/class-wc-payments-payment-method-messaging-element.php b/includes/class-wc-payments-payment-method-messaging-element.php index d27409d1be0..6270b99e912 100644 --- a/includes/class-wc-payments-payment-method-messaging-element.php +++ b/includes/class-wc-payments-payment-method-messaging-element.php @@ -98,19 +98,20 @@ public function init() { 'WCPAY_PRODUCT_DETAILS', 'wcpayStripeSiteMessaging', [ - 'productId' => 'base_product', - 'productVariations' => $product_variations, - 'country' => empty( $billing_country ) ? $store_country : $billing_country, - 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), - 'accountId' => $this->account->get_stripe_account_id(), - 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), - 'paymentMethods' => array_values( $bnpl_payment_methods ), - 'currencyCode' => $currency_code, - 'isCart' => is_cart(), - 'isCartBlock' => $is_cart_block, - 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), - 'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ), - 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'productId' => 'base_product', + 'productVariations' => $product_variations, + 'country' => empty( $billing_country ) ? $store_country : $billing_country, + 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), + 'accountId' => $this->account->get_stripe_account_id(), + 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), + 'paymentMethods' => array_values( $bnpl_payment_methods ), + 'currencyCode' => $currency_code, + 'isCart' => is_cart(), + 'isCartBlock' => $is_cart_block, + 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), + 'minimumOrderAmount' => WC_Payments_Utils::get_cached_minimum_amount( $currency_code, true ), + 'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ), + 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), ] ); diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 5045871a3bd..4593fe65019 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -702,6 +702,62 @@ public static function get_filtered_error_status_code( Exception $e ): int { return $status_code ?? 400; } + /** + * Retrieves Stripe minimum order value authorized per currency. + * The values are based on Stripe's recommendations. + * See https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts. + * + * @param string $currency The currency. + * + * @return int The minimum amount. + */ + public static function get_stripe_minimum_amount( $currency ) { + switch ( $currency ) { + case 'AED': + case 'MYR': + case 'PLN': + case 'RON': + $minimum_amount = 200; + break; + case 'BGN': + $minimum_amount = 100; + break; + case 'CZK': + $minimum_amount = 1500; + break; + case 'DKK': + $minimum_amount = 250; + break; + case 'GBP': + $minimum_amount = 30; + break; + case 'HKD': + $minimum_amount = 400; + break; + case 'HUF': + $minimum_amount = 17500; + break; + case 'JPY': + $minimum_amount = 5000; + break; + case 'MXN': + case 'THB': + $minimum_amount = 1000; + break; + case 'NOK': + case 'SEK': + $minimum_amount = 300; + break; + default: + $minimum_amount = 50; + break; + } + + self::cache_minimum_amount( $currency, $minimum_amount ); + + return $minimum_amount; + } + /** * Saves the minimum amount required for transactions in a given currency. * @@ -716,12 +772,20 @@ public static function cache_minimum_amount( $currency, $amount ) { * Checks if there is a minimum amount required for transactions in a given currency. * * @param string $currency The currency to check for. + * @param bool $fallback_to_local_list Whether to fallback to the local Stripe list if the cached value is not available. * * @return int|null Either the minimum amount, or `null` if not available. */ - public static function get_cached_minimum_amount( $currency ) { + public static function get_cached_minimum_amount( $currency, $fallback_to_local_list = false ) { $cached = get_transient( 'wcpay_minimum_amount_' . strtolower( $currency ) ); - return (int) $cached ? (int) $cached : null; + + if ( (int) $cached ) { + return (int) $cached; + } elseif ( $fallback_to_local_list ) { + return self::get_stripe_minimum_amount( $currency ); + } + + return null; } /** diff --git a/tests/unit/test-class-wc-payments-utils.php b/tests/unit/test-class-wc-payments-utils.php index 08a9420d674..a7aba06f9ab 100644 --- a/tests/unit/test-class-wc-payments-utils.php +++ b/tests/unit/test-class-wc-payments-utils.php @@ -931,6 +931,12 @@ public function test_get_cached_minimum_amount_returns_null_without_cache() { $this->assertNull( $result ); } + public function test_get_cached_minimum_amount_returns_amount_fallbacking_from_stripe_list() { + delete_transient( 'wcpay_minimum_amount_usd' ); + $result = WC_Payments_Utils::get_cached_minimum_amount( 'usd', true ); + $this->assertSame( 50, $result ); + } + public function test_get_last_refund_from_order_id_returns_correct_refund() { $order = WC_Helper_Order::create_order(); $refund_1 = wc_create_refund( [ 'order_id' => $order->get_id() ] ); From 0bd29ce1882f7e1f4b81a62a4203b6750ef87f52 Mon Sep 17 00:00:00 2001 From: Alex Florisca Date: Tue, 3 Sep 2024 10:39:29 +0100 Subject: [PATCH 32/44] Remove the override css for the express payment grid. (#9248) Co-authored-by: Alfredo Sumaran --- changelog/remove-override-for-express-payment-grid | 5 +++++ .../blocks/express-checkout-element.scss | 11 ----------- 2 files changed, 5 insertions(+), 11 deletions(-) create mode 100644 changelog/remove-override-for-express-payment-grid diff --git a/changelog/remove-override-for-express-payment-grid b/changelog/remove-override-for-express-payment-grid new file mode 100644 index 00000000000..3bdc7499449 --- /dev/null +++ b/changelog/remove-override-for-express-payment-grid @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Insignificant css change + + diff --git a/client/express-checkout/blocks/express-checkout-element.scss b/client/express-checkout/blocks/express-checkout-element.scss index 17c3b5a9a4f..06ae725dfbc 100644 --- a/client/express-checkout/blocks/express-checkout-element.scss +++ b/client/express-checkout/blocks/express-checkout-element.scss @@ -14,14 +14,3 @@ margin: 24px 0 !important; height: 20px; } - -// Checkout Block -.wc-block-components-express-payment--checkout { - .wc-block-components-express-payment__event-buttons { - grid-gap: 12px !important; - grid-template-columns: repeat( - auto-fit, - minmax( 232px, 1fr ) - ) !important; - } -} From eb584b2046914be227f936cb34e3b824a2f99020 Mon Sep 17 00:00:00 2001 From: Achyuth Ajoy Date: Tue, 3 Sep 2024 18:38:14 +0530 Subject: [PATCH 33/44] Upgrade docker image to PHP 8.1 (#9146) --- .github/workflows/coverage.yml | 4 +- bin/check-test-coverage.sh | 11 ++-- bin/run-tests.sh | 4 +- .../enhancement-upgrade-docker-to-php8.1 | 5 ++ composer.json | 2 +- composer.lock | 63 ++++++++++--------- docker-compose.yml | 1 + docker/wordpress_xdebug/Dockerfile | 18 +++--- .../multi-currency/CurrencySwitcherBlock.php | 2 +- .../wc-payments-multi-currency.php | 6 +- 10 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 changelog/enhancement-upgrade-docker-to-php8.1 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2fc5beb975e..79ff0d7a01f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,7 +17,7 @@ jobs: matrix: woocommerce: [ 'latest' ] wordpress: [ 'latest' ] - php: [ '7.4' ] + php: [ '7.4', '8.1' ] directory: [ 'includes', 'src' ] env: WP_VERSION: ${{ matrix.wordpress }} @@ -36,6 +36,6 @@ jobs: with: php-version: ${{ matrix.php }} tools: composer - coverage: xdebug2 + coverage: xdebug # run CI checks - run: bash bin/run-ci-tests-check-coverage.bash diff --git a/bin/check-test-coverage.sh b/bin/check-test-coverage.sh index b634be4bed7..ad30c93a0c6 100755 --- a/bin/check-test-coverage.sh +++ b/bin/check-test-coverage.sh @@ -17,11 +17,10 @@ docker compose exec -u www-data wordpress \ echo "Checking coverage..." -docker compose exec -u www-data wordpress \ - php -d xdebug.remote_autostart=on \ - /var/www/html/wp-content/plugins/woocommerce-payments/vendor/bin/phpunit \ - --configuration "/var/www/html/wp-content/plugins/woocommerce-payments/$CONFIGURATION_FILE" \ - --coverage-html /var/www/html/php-test-coverage \ - --coverage-clover /var/www/html/clover.xml +docker-compose exec -u www-data wordpress \ + /var/www/html/wp-content/plugins/woocommerce-payments/vendor/bin/phpunit \ + --configuration "/var/www/html/wp-content/plugins/woocommerce-payments/$CONFIGURATION_FILE" \ + --coverage-html /var/www/html/php-test-coverage \ + --coverage-clover /var/www/html/clover.xml ./vendor/bin/coverage-check docker/wordpress/clover.xml $COVERAGE diff --git a/bin/run-tests.sh b/bin/run-tests.sh index 97e23b957a4..cf194d39079 100755 --- a/bin/run-tests.sh +++ b/bin/run-tests.sh @@ -24,13 +24,11 @@ if $WATCH_FLAG; then # Change directory to WooCommerce Payments' root in order to have access to .phpunit-watcher.yml docker compose exec -u www-data wordpress bash -c \ "cd /var/www/html/wp-content/plugins/woocommerce-payments && \ - php -d xdebug.remote_autostart=on \ ./vendor/bin/phpunit-watcher watch --configuration ./phpunit.xml.dist $*" else echo "Running the tests..." - docker compose exec -u www-data wordpress \ - php -d xdebug.remote_autostart=on \ + docker-compose exec -u www-data wordpress \ /var/www/html/wp-content/plugins/woocommerce-payments/vendor/bin/phpunit \ --configuration /var/www/html/wp-content/plugins/woocommerce-payments/phpunit.xml.dist \ $* diff --git a/changelog/enhancement-upgrade-docker-to-php8.1 b/changelog/enhancement-upgrade-docker-to-php8.1 new file mode 100644 index 00000000000..c5bcfc2bcef --- /dev/null +++ b/changelog/enhancement-upgrade-docker-to-php8.1 @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Upgrade local dev env to PHP8.1 + + diff --git a/composer.json b/composer.json index afe075d9d50..0b161bc9dcc 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "woocommerce/action-scheduler": "3.1.6", "kalessil/production-dependencies-guard": "dev-master", "vimeo/psalm": "4.13.1", - "php-stubs/wordpress-stubs": "5.8.2", + "php-stubs/wordpress-stubs": "5.9.6", "php-stubs/woocommerce-stubs": "6.8.0", "rregeer/phpunit-coverage-check": "0.3.1", "yoast/phpunit-polyfills": "1.1.0", diff --git a/composer.lock b/composer.lock index 6e718cccbfc..3485eaf39d8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c9198ad6fb169b71e08cb40e127c8e3f", + "content-hash": "70e55b7cc94894694705ccb1e6cb3df2", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -58,16 +58,16 @@ }, { "name": "automattic/jetpack-admin-ui", - "version": "v0.4.3", + "version": "v0.4.4", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-admin-ui.git", - "reference": "83e500408e8542b108987b54d845ecb47b8e36b8" + "reference": "18ea3a92f910f7afa9641dadb956b95b75ce6e0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/83e500408e8542b108987b54d845ecb47b8e36b8", - "reference": "83e500408e8542b108987b54d845ecb47b8e36b8", + "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/18ea3a92f910f7afa9641dadb956b95b75ce6e0f", + "reference": "18ea3a92f910f7afa9641dadb956b95b75ce6e0f", "shasum": "" }, "require": { @@ -108,22 +108,22 @@ ], "description": "Generic Jetpack wp-admin UI elements", "support": { - "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.4.3" + "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.4.4" }, - "time": "2024-08-23T14:28:39+00:00" + "time": "2024-08-29T08:39:06+00:00" }, { "name": "automattic/jetpack-assets", - "version": "v2.3.4", + "version": "v2.3.5", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-assets.git", - "reference": "46ca5819bdc37587f7bfbab45168fbc3aea9facb" + "reference": "e869f7a7780da9b0c1ff9612701fcffde83cda12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/46ca5819bdc37587f7bfbab45168fbc3aea9facb", - "reference": "46ca5819bdc37587f7bfbab45168fbc3aea9facb", + "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/e869f7a7780da9b0c1ff9612701fcffde83cda12", + "reference": "e869f7a7780da9b0c1ff9612701fcffde83cda12", "shasum": "" }, "require": { @@ -165,9 +165,9 @@ ], "description": "Asset management utilities for Jetpack ecosystem packages", "support": { - "source": "https://github.com/Automattic/jetpack-assets/tree/v2.3.4" + "source": "https://github.com/Automattic/jetpack-assets/tree/v2.3.5" }, - "time": "2024-08-23T14:29:11+00:00" + "time": "2024-08-29T08:39:36+00:00" }, { "name": "automattic/jetpack-autoloader", @@ -2692,24 +2692,25 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v5.8.2", + "version": "v5.9.6", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "67fd773742b7be5b4463f40318b0b4890a07033b" + "reference": "6a18d938d0aef39d091505a4a35b025fb6c10098" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/67fd773742b7be5b4463f40318b0b4890a07033b", - "reference": "67fd773742b7be5b4463f40318b0b4890a07033b", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6a18d938d0aef39d091505a4a35b025fb6c10098", + "reference": "6a18d938d0aef39d091505a4a35b025fb6c10098", "shasum": "" }, - "replace": { - "giacocorsiglia/wordpress-stubs": "*" - }, "require-dev": { - "giacocorsiglia/stubs-generator": "^0.5.0", - "php": "~7.1" + "nikic/php-parser": "< 4.12.0", + "php": "~7.3 || ~8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.3", + "phpstan/phpstan": "^1.10.12", + "phpunit/phpunit": "^9.5" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", @@ -2730,9 +2731,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.8.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.6" }, - "time": "2021-11-11T13:57:00+00:00" + "time": "2023-05-18T04:34:27+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -3343,16 +3344,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "1.30.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f", + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f", "shasum": "" }, "require": { @@ -3384,9 +3385,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2024-08-29T09:54:52+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6971,5 +6972,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 2804781d6bc..8c35b58b439 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: dockerfile: ./docker/wordpress_xdebug/Dockerfile args: - XDEBUG_REMOTE_PORT=9000 # IDE/Editor's listener port + - XDEBUG_START_WITH_REQUEST=trigger container_name: woocommerce_payments_wordpress image: woocommerce_payments_wordpress restart: always diff --git a/docker/wordpress_xdebug/Dockerfile b/docker/wordpress_xdebug/Dockerfile index 7b612f20806..616655012f3 100755 --- a/docker/wordpress_xdebug/Dockerfile +++ b/docker/wordpress_xdebug/Dockerfile @@ -1,15 +1,15 @@ -FROM wordpress:php7.4 +FROM wordpress:php8.1 ARG XDEBUG_REMOTE_PORT ARG XDEBUG_REMOTE_HOST=host.docker.internal -RUN pecl install xdebug-2.9.8 \ - && echo 'xdebug.remote_enable=1' >> $PHP_INI_DIR/php.ini \ - && echo "xdebug.remote_port=$XDEBUG_REMOTE_PORT" >> $PHP_INI_DIR/php.ini \ - && echo "xdebug.remote_host=$XDEBUG_REMOTE_HOST" >> $PHP_INI_DIR/php.ini \ - && echo 'xdebug.remote_autostart=0' >> $PHP_INI_DIR/php.ini \ - && docker-php-ext-enable xdebug +ARG XDEBUG_START_WITH_REQUEST=trigger +RUN pecl install xdebug \ + && echo 'xdebug.mode=coverage,debug' >> $PHP_INI_DIR/php.ini \ + && echo "xdebug.client_port=${XDEBUG_REMOTE_PORT}" >> $PHP_INI_DIR/php.ini \ + && echo "xdebug.client_host=${XDEBUG_REMOTE_HOST}" >> $PHP_INI_DIR/php.ini \ + && echo "xdebug.start_with_request=${XDEBUG_START_WITH_REQUEST}" >> $PHP_INI_DIR/php.ini \ + && docker-php-ext-enable xdebug RUN apt-get update \ - && apt-get install --assume-yes --quiet --no-install-recommends gnupg2 subversion mariadb-client less jq -RUN apt-get install -y openssh-client + && apt-get install --assume-yes --quiet --no-install-recommends gnupg2 subversion mariadb-client less jq openssh-client RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \ && chmod +x wp-cli.phar \ && mv wp-cli.phar /usr/local/bin/wp diff --git a/includes/multi-currency/CurrencySwitcherBlock.php b/includes/multi-currency/CurrencySwitcherBlock.php index 840530e32a2..95d4762365c 100644 --- a/includes/multi-currency/CurrencySwitcherBlock.php +++ b/includes/multi-currency/CurrencySwitcherBlock.php @@ -65,7 +65,7 @@ public function init_block_widget() { register_block_type( 'woocommerce-payments/multi-currency-switcher', [ - 'api_version' => 2, + 'api_version' => '2', 'editor_script' => 'woocommerce-payments/multi-currency-switcher', 'render_callback' => [ $this, 'render_block_widget' ], 'attributes' => [ diff --git a/includes/multi-currency/wc-payments-multi-currency.php b/includes/multi-currency/wc-payments-multi-currency.php index 31017f9ff35..fef6e10db81 100644 --- a/includes/multi-currency/wc-payments-multi-currency.php +++ b/includes/multi-currency/wc-payments-multi-currency.php @@ -15,8 +15,10 @@ function wcpay_multi_currency_onboarding_check() { // Skip checking the HTTP referer if it is a cron job. if ( ! defined( 'DOING_CRON' ) ) { - $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); - $is_setup_page = 0 < strpos( $http_referer, 'multi-currency-setup' ); + $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); + if ( ! empty( $http_referer ) ) { + $is_setup_page = strpos( $http_referer, 'multi-currency-setup' ) !== false; + } } return $is_setup_page; From 81e3207c2ee13c3d5dc68b40839e984992c5ea12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20L=C3=B3pez=20Ariza?= <45979455+alopezari@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:26:49 +0200 Subject: [PATCH 34/44] Bump required PHP version to 7.3 (#9388) --- changelog/update-required-php-version | 5 +++++ composer.json | 2 +- composer.lock | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog/update-required-php-version diff --git a/changelog/update-required-php-version b/changelog/update-required-php-version new file mode 100644 index 00000000000..0aa1795b408 --- /dev/null +++ b/changelog/update-required-php-version @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Bump required PHP version to 7.3. + + diff --git a/composer.json b/composer.json index 0b161bc9dcc..284f633e65b 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ } }, "require": { - "php": ">=7.2", + "php": ">=7.3", "ext-json": "*", "automattic/jetpack-connection": "2.12.4", "automattic/jetpack-config": "2.0.4", diff --git a/composer.lock b/composer.lock index 3485eaf39d8..4572fed2932 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "70e55b7cc94894694705ccb1e6cb3df2", + "content-hash": "ef9fe43c4fffc996cf70034d89fea352", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -6965,7 +6965,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.2", + "php": ">=7.3", "ext-json": "*" }, "platform-dev": [], From 64b54f39077038480bbdc237eedf77cf7fdb4205 Mon Sep 17 00:00:00 2001 From: Francesco Date: Fri, 6 Sep 2024 16:09:54 +0200 Subject: [PATCH 35/44] add: test instructions icon animation (#9290) --- assets/images/icons/copy.svg | 2 +- .../update-test-instructions-icon-animation | 4 +++ client/checkout/style.scss | 27 ++++++++++++++++--- client/checkout/utils/copy-test-number.js | 12 ++++++--- includes/class-wc-payments-checkout.php | 8 ++---- .../class-becs-payment-method.php | 2 +- .../class-cc-payment-method.php | 2 +- .../class-sepa-payment-method.php | 2 +- .../unit/test-class-wc-payments-checkout.php | 6 ++--- 9 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 changelog/update-test-instructions-icon-animation diff --git a/assets/images/icons/copy.svg b/assets/images/icons/copy.svg index 9452ee2ec86..ab7cb5136ba 100644 --- a/assets/images/icons/copy.svg +++ b/assets/images/icons/copy.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/changelog/update-test-instructions-icon-animation b/changelog/update-test-instructions-icon-animation new file mode 100644 index 00000000000..6a92da93656 --- /dev/null +++ b/changelog/update-test-instructions-icon-animation @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +add: test instructions icon animation diff --git a/client/checkout/style.scss b/client/checkout/style.scss index 73826b25385..2c365b2fc14 100644 --- a/client/checkout/style.scss +++ b/client/checkout/style.scss @@ -9,25 +9,46 @@ border: none !important; // some themes might override the color on all `button`s - whose selector has higher specificity and our class selector. background-color: transparent !important; - padding: 3px; + font-weight: normal; + display: inline-flex; + cursor: pointer; + color: inherit; + font-size: initial; + padding: 2px 1px; + align-items: center; + + span { + margin-right: 4px; + } i { - display: inline-block; + order: 1; + display: block; width: 1.2em; height: 1.2em; - background: url( 'assets/images/icons/copy.svg?asset' ) no-repeat center; background-size: contain; } &:hover { background-color: transparent; + filter: invert( 0.3 ); + + i { + filter: invert( 0.3 ); + } } &:active i { transform: scale( 0.9 ); } + &.state--success { + i { + background-image: url( 'assets/images/icons/check-green.svg?asset' ); + } + } + .theme--night & { i { filter: invert( 100% ) hue-rotate( 180deg ); diff --git a/client/checkout/utils/copy-test-number.js b/client/checkout/utils/copy-test-number.js index c649336a5ab..812f1fabe1c 100644 --- a/client/checkout/utils/copy-test-number.js +++ b/client/checkout/utils/copy-test-number.js @@ -3,9 +3,12 @@ */ import { __ } from '@wordpress/i18n'; +let previousTimeoutRef = null; + document.addEventListener( 'click', function ( event ) { + // using "closest", just in case the user clicks on the icon. const copyNumberButton = event.target?.closest( '.js-woopayments-copy-test-number' ); @@ -14,9 +17,7 @@ document.addEventListener( } event.preventDefault(); - const number = copyNumberButton.parentElement.querySelector( - '.js-woopayments-test-number' - ).innerText; + const number = copyNumberButton.querySelector( 'span' ).innerText; if ( ! navigator.clipboard ) { prompt( @@ -41,6 +42,11 @@ document.addEventListener( context: 'wc/checkout/payments', } ); + copyNumberButton.classList.add( 'state--success' ); + clearTimeout( previousTimeoutRef ); + previousTimeoutRef = setTimeout( () => { + copyNumberButton.classList.remove( 'state--success' ); + }, 2000 ); }, false ); diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 65887a6b61b..5764ecd96d5 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -328,10 +328,8 @@ public function get_enabled_payment_method_config() { $payment_method->get_testing_instructions(), [ 'a' => '', - 'button' => '. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); + return __( 'Test mode: use the test account number 000123456. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); } } diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php index 6f85995d5e0..e0a3677d53e 100644 --- a/includes/payment-methods/class-cc-payment-method.php +++ b/includes/payment-methods/class-cc-payment-method.php @@ -70,6 +70,6 @@ public function get_title( string $account_country = null, $payment_details = fa * @return string */ public function get_testing_instructions() { - return __( 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', 'woocommerce-payments' ); + return __( 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', 'woocommerce-payments' ); } } diff --git a/includes/payment-methods/class-sepa-payment-method.php b/includes/payment-methods/class-sepa-payment-method.php index 2e638568b52..8ea671f7706 100644 --- a/includes/payment-methods/class-sepa-payment-method.php +++ b/includes/payment-methods/class-sepa-payment-method.php @@ -43,6 +43,6 @@ public function __construct( $token_service ) { * @return string */ public function get_testing_instructions() { - return __( 'Test mode: use the test account number AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); + return __( 'Test mode: use the test account number AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', 'woocommerce-payments' ); } } diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php index 90a45062f24..1c471baa5fd 100644 --- a/tests/unit/test-class-wc-payments-checkout.php +++ b/tests/unit/test-class-wc-payments-checkout.php @@ -374,7 +374,6 @@ public function test_link_payment_method_provided_when_card_enabled() { ->willReturn( $payment_methods ); $this->assertSame( - $this->system_under_test->get_payment_fields_js_config()['paymentMethodsConfig'], [ 'card' => [ 'isReusable' => true, @@ -383,7 +382,7 @@ public function test_link_payment_method_provided_when_card_enabled() { 'darkIcon' => $dark_icon_url, 'showSaveOption' => true, 'countries' => [], - 'testingInstructions' => 'Test mode: use test card 4242 4242 4242 4242 or refer to our testing guide.', + 'testingInstructions' => 'Test mode: use test card or refer to our testing guide.', 'forceNetworkSavedCards' => false, ], 'link' => [ @@ -396,7 +395,8 @@ public function test_link_payment_method_provided_when_card_enabled() { 'testingInstructions' => '', 'forceNetworkSavedCards' => false, ], - ] + ], + $this->system_under_test->get_payment_fields_js_config()['paymentMethodsConfig'] ); } From f65efc5ba711d2a91534723ef3e0635e8a5b7eff Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto Date: Fri, 6 Sep 2024 12:52:06 -0300 Subject: [PATCH 36/44] Fixed default borderRadius value for the express checkout buttons (#9398) --- changelog/9311-fix-default-border-radius | 4 ++++ client/express-checkout/utils/index.ts | 2 +- client/globals.d.ts | 7 +++++++ client/payment-methods-map.tsx | 14 -------------- client/utils/express-checkout/index.js | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) create mode 100644 changelog/9311-fix-default-border-radius diff --git a/changelog/9311-fix-default-border-radius b/changelog/9311-fix-default-border-radius new file mode 100644 index 00000000000..2928de05076 --- /dev/null +++ b/changelog/9311-fix-default-border-radius @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed default borderRadius value for the express checkout buttons diff --git a/client/express-checkout/utils/index.ts b/client/express-checkout/utils/index.ts index 8a21a75260c..28174431447 100644 --- a/client/express-checkout/utils/index.ts +++ b/client/express-checkout/utils/index.ts @@ -179,7 +179,7 @@ export const getExpressCheckoutButtonAppearance = () => { return { variables: { borderRadius: `${ - buttonSettings?.radius ?? getDefaultBorderRadius() + buttonSettings?.radius || getDefaultBorderRadius() }px`, spacingUnit: '6px', }, diff --git a/client/globals.d.ts b/client/globals.d.ts index 7284347d48e..f00b521943a 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -185,4 +185,11 @@ declare global { homeUrl: string; siteTitle: string; }; + + interface Window { + wcpaySettings: typeof wcpaySettings; + wc: typeof wc; + wcTracks: typeof wcTracks; + wcSettings: typeof wcSettings; + } } diff --git a/client/payment-methods-map.tsx b/client/payment-methods-map.tsx index 1f5e1a9a9cf..b22ca1f9ae2 100644 --- a/client/payment-methods-map.tsx +++ b/client/payment-methods-map.tsx @@ -24,20 +24,6 @@ import { SofortIcon, } from 'wcpay/payment-methods-icons'; -declare global { - interface Window { - wcpaySettings: { - accountStatus: { - country: string; - }; - }; - wcPayFrontendTracks: { - event: string; - properties: Record< string, unknown >; - }; - } -} - const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US'; export interface PaymentMethodMapEntry { diff --git a/client/utils/express-checkout/index.js b/client/utils/express-checkout/index.js index c47ef82f490..bacc56c7f7e 100644 --- a/client/utils/express-checkout/index.js +++ b/client/utils/express-checkout/index.js @@ -24,7 +24,7 @@ export const getExpressCheckoutConfig = ( key ) => { export const getDefaultBorderRadius = () => { return parseInt( - wcpaySettings?.defaultExpressCheckoutBorderRadius ?? 4, + window?.wcpaySettings?.defaultExpressCheckoutBorderRadius || 4, 10 ); }; From 1b17a555a713c65092b3a5d04067e7389d72ec5e Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Fri, 6 Sep 2024 17:59:30 +0100 Subject: [PATCH 37/44] Add connect.js library and embedded onboarding integration (#9251) Co-authored-by: oaratovskyi Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Co-authored-by: Vlad Olaru --- changelog/dev-include-connect-js | 4 + client/globals.d.ts | 1 + client/index.js | 21 ++ client/onboarding/index.tsx | 11 +- client/onboarding/kyc/appearance.ts | 62 ++++ client/onboarding/kyc/index.tsx | 77 +++++ client/onboarding/step.tsx | 23 +- client/onboarding/steps/embedded-kyc.tsx | 234 ++++++++++++++ .../steps/test/embedded-onboarding.tsx | 93 ++++++ client/onboarding/strings.tsx | 10 + client/onboarding/style.scss | 4 + client/onboarding/types.ts | 21 +- client/overview/index.js | 1 + client/utils/index.js | 34 ++ includes/admin/class-wc-payments-admin.php | 27 ++ ...wc-rest-payments-onboarding-controller.php | 150 ++++++++- includes/class-wc-payments-account.php | 262 +++++++-------- includes/class-wc-payments-features.php | 11 + .../class-wc-payments-onboarding-service.php | 300 +++++++++++++++++- includes/class-wc-payments-utils.php | 13 + includes/class-wc-payments.php | 4 +- .../class-wc-payments-api-client.php | 58 ++++ package-lock.json | 17 + package.json | 2 + ...wc-rest-payments-onboarding-controller.php | 83 +++++ ...test-class-wc-payments-account-capital.php | 12 +- .../test-class-wc-payments-account-link.php | 10 +- tests/unit/test-class-wc-payments-account.php | 78 ++++- .../unit/test-class-wc-payments-features.php | 30 ++ ...t-class-wc-payments-onboarding-service.php | 26 +- 30 files changed, 1489 insertions(+), 190 deletions(-) create mode 100644 changelog/dev-include-connect-js create mode 100644 client/onboarding/kyc/appearance.ts create mode 100644 client/onboarding/kyc/index.tsx create mode 100644 client/onboarding/steps/embedded-kyc.tsx create mode 100644 client/onboarding/steps/test/embedded-onboarding.tsx diff --git a/changelog/dev-include-connect-js b/changelog/dev-include-connect-js new file mode 100644 index 00000000000..218ec497755 --- /dev/null +++ b/changelog/dev-include-connect-js @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added Embdedded KYC, currently behind feature flag. diff --git a/client/globals.d.ts b/client/globals.d.ts index f00b521943a..44d94baa01b 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -16,6 +16,7 @@ declare global { paymentTimeline: boolean; isDisputeIssuerEvidenceEnabled: boolean; isPaymentOverviewWidgetEnabled?: boolean; + isEmbeddedKycEnabled?: boolean; }; fraudServices: unknown[]; testMode: boolean; diff --git a/client/index.js b/client/index.js index 9a530a54010..a5eb2d8376c 100644 --- a/client/index.js +++ b/client/index.js @@ -29,6 +29,7 @@ import CapitalPage from 'capital'; import OverviewPage from 'overview'; import DocumentsPage from 'documents'; import OnboardingPage from 'onboarding'; +import OnboardingKycPage from 'onboarding/kyc'; import FraudProtectionAdvancedSettingsPage from './settings/fraud-protection/advanced-settings'; import { getTasks } from 'overview/task-list/tasks'; @@ -69,6 +70,26 @@ addFilter( capability: 'manage_woocommerce', } ); + // Currently under feature flag. + if ( + wcpaySettings && + wcpaySettings.featureFlags.isEmbeddedKycEnabled + ) { + pages.push( { + container: OnboardingKycPage, + path: '/payments/onboarding/kyc', + wpOpenMenu: menuID, + breadcrumbs: [ + rootLink, + __( 'Continue onboarding', 'woocommerce-payments' ), + ], + navArgs: { + id: 'wc-payments-continue-onboarding', + }, + capability: 'manage_woocommerce', + } ); + } + pages.push( { container: OverviewPage, path: '/payments/overview', diff --git a/client/onboarding/index.tsx b/client/onboarding/index.tsx index 5def61ebd4b..62e179bb7c2 100644 --- a/client/onboarding/index.tsx +++ b/client/onboarding/index.tsx @@ -13,11 +13,12 @@ import { getMccFromIndustry } from 'onboarding/utils'; import { OnboardingForm } from './form'; import Step from './step'; import BusinessDetails from './steps/business-details'; +import EmbeddedKyc from './steps/embedded-kyc'; import StoreDetails from './steps/store-details'; -import LoadingStep from './steps/loading'; import { trackStarted } from './tracking'; import { getAdminUrl } from 'wcpay/utils'; import './style.scss'; +import LoadingStep from 'wcpay/onboarding/steps/loading'; const OnboardingStepper = () => { const handleExit = () => { @@ -47,7 +48,13 @@ const OnboardingStepper = () => { - + { wcpaySettings?.featureFlags?.isEmbeddedKycEnabled ? ( + + + + ) : ( + + ) } ); }; diff --git a/client/onboarding/kyc/appearance.ts b/client/onboarding/kyc/appearance.ts new file mode 100644 index 00000000000..db0260c302f --- /dev/null +++ b/client/onboarding/kyc/appearance.ts @@ -0,0 +1,62 @@ +/* eslint-disable max-len */ + +/** + * Customised appearance variables for the external KYC flow. + */ +export default { + variables: { + colorPrimary: '#3C2861', + colorBackground: '#FFFFFF', + buttonPrimaryColorBackground: '#3858E9', + buttonPrimaryColorBorder: '#3858E9', + buttonPrimaryColorText: '#FFFFFF', + buttonSecondaryColorBackground: '#FFFFFF', + buttonSecondaryColorBorder: '#3858E9', + buttonSecondaryColorText: '#3858E9', + colorText: '#101517', + colorSecondaryText: '#50575E', + actionPrimaryColorText: '#3858E9', + actionSecondaryColorText: '#101517', + colorBorder: '#DDDDDD', + formHighlightColorBorder: '#3858E9', + formAccentColor: '#3858E9', + colorDanger: '#CC1818', + offsetBackgroundColor: '#F0F0F0', + formBackgroundColor: '#FFFFFF', + badgeNeutralColorText: '#2C3338', + badgeNeutralColorBackground: '#F6F7F7', + badgeNeutralColorBorder: '#F6F7F7', + badgeSuccessColorText: '#005C12', + badgeSuccessColorBackground: '#EDFAEF', + badgeSuccessColorBorder: '#EDFAEF', + badgeWarningColorText: '#614200', + badgeWarningColorBackground: '#FCF9E8', + badgeWarningColorBorder: '#FCF9E8', + badgeDangerColorText: '#8A2424', + badgeDangerColorBackground: '#FCF0F1', + badgeDangerColorBorder: '#FCF0F1', + borderRadius: '2px', + buttonBorderRadius: '2px', + formBorderRadius: '2px', + badgeBorderRadius: '2px', + overlayBorderRadius: '8px', + spacingUnit: '10px', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'system-ui', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Roboto', 'Arial', sans-serif", + fontSizeBase: '16px', + headingXlFontSize: '32px', + headingXlFontWeight: '400', + headingLgFontSize: '24px', + headingLgFontWeight: '400', + headingMdFontSize: '20px', + headingMdFontWeight: '400', + headingSmFontSize: '16px', + headingSmFontWeight: '600', + headingXsFontSize: '12px', + headingXsFontWeight: '600', + bodyMdFontWeight: '400', + bodyMdFontSize: '16px', + bodySmFontSize: '14px', + bodySmFontWeight: '400', + }, +}; diff --git a/client/onboarding/kyc/index.tsx b/client/onboarding/kyc/index.tsx new file mode 100644 index 00000000000..d2e9a4b5140 --- /dev/null +++ b/client/onboarding/kyc/index.tsx @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import React, { useEffect } from 'react'; +import { closeSmall, Icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import Logo from 'assets/images/woopayments.svg'; +import Page from 'components/page'; +import { OnboardingContextProvider } from 'onboarding/context'; +import EmbeddedKyc from 'onboarding/steps/embedded-kyc'; +import strings from 'onboarding/strings'; +import { getConnectUrl } from 'utils'; + +const OnboardingKycPage: React.FC = () => { + const handleExit = () => { + const urlParams = new URLSearchParams( window.location.search ); + + window.location.href = getConnectUrl( + { + source: + urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || + 'unknown', + }, + 'WCPAY_ONBOARDING_KYC' + ); + }; + + useEffect( () => { + // Remove loading class and add those required for full screen. + document.body.classList.remove( 'woocommerce-admin-is-loading' ); + document.body.classList.add( 'woocommerce-admin-full-screen' ); + document.body.classList.add( 'is-wp-toolbar-disabled' ); + document.body.classList.add( 'wcpay-onboarding__body' ); + + // Remove full screen classes on unmount. + return () => { + document.body.classList.remove( 'woocommerce-admin-full-screen' ); + document.body.classList.remove( 'is-wp-toolbar-disabled' ); + document.body.classList.remove( 'wcpay-onboarding__body' ); + }; + }, [] ); + return ( + + +
+ + WooPayments + +
+
+
+ +
+
+
+
+ ); +}; +export default OnboardingKycPage; diff --git a/client/onboarding/step.tsx b/client/onboarding/step.tsx index c6f4aee0b17..92ed2e4e3d4 100644 --- a/client/onboarding/step.tsx +++ b/client/onboarding/step.tsx @@ -17,9 +17,10 @@ import './style.scss'; interface Props { name: OnboardingSteps; + showHeading?: boolean; } -const Step: React.FC< Props > = ( { name, children } ) => { +const Step: React.FC< Props > = ( { name, children, showHeading = true } ) => { const { trackAbandoned } = useTrackAbandoned(); const { prevStep, exit } = useStepperContext(); const handleExit = () => { @@ -36,7 +37,9 @@ const Step: React.FC< Props > = ( { name, children } ) => {
-

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

-

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

+ { showHeading && ( + <> +

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

+

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

+ + ) }
{ children }
diff --git a/client/onboarding/steps/embedded-kyc.tsx b/client/onboarding/steps/embedded-kyc.tsx new file mode 100644 index 00000000000..1ca2c9c6a92 --- /dev/null +++ b/client/onboarding/steps/embedded-kyc.tsx @@ -0,0 +1,234 @@ +/** + * External dependencies + */ +import React, { useEffect, useState } from 'react'; +import { + loadConnectAndInitialize, + StripeConnectInstance, +} from '@stripe/connect-js'; +import { LoadError } from '@stripe/connect-js/types/config'; +import { + ConnectAccountOnboarding, + ConnectComponentsProvider, +} from '@stripe/react-connect-js'; +import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { NAMESPACE } from 'data/constants'; +import appearance from '../kyc/appearance'; +import BannerNotice from 'wcpay/components/banner-notice'; +import LoadBar from 'wcpay/components/load-bar'; +import { useOnboardingContext } from 'wcpay/onboarding/context'; +import { + AccountKycSession, + PoEligibleData, + PoEligibleResult, +} from 'wcpay/onboarding/types'; +import { fromDotNotation } from 'wcpay/onboarding/utils'; +import { getConnectUrl, getOverviewUrl } from 'wcpay/utils'; + +type AccountKycSessionData = AccountKycSession; + +interface FinalizeResponse { + success: boolean; + params: Record< string, string >; +} + +interface Props { + continueKyc?: boolean; +} + +const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => { + const { data } = useOnboardingContext(); + const [ publishableKey, setPublishableKey ] = useState( '' ); + const [ locale, setLocale ] = useState( '' ); + const [ clientSecret, setClientSecret ] = useState< + ( () => Promise< string > ) | null + >( null ); + const [ + stripeConnectInstance, + setStripeConnectInstance, + ] = useState< StripeConnectInstance | null >( null ); + const [ loading, setLoading ] = useState( true ); + const [ loadErrorMessage, setLoadErrorMessage ] = useState( '' ); + const onLoaderStart = () => { + setLoading( false ); + }; + const onLoadError = ( loadError: LoadError ) => { + setLoadErrorMessage( loadError.error.message || 'Unknown error' ); + }; + + useEffect( () => { + const isEligibleForPo = async () => { + if ( + ! data.country || + ! data.business_type || + ! data.mcc || + ! data.annual_revenue || + ! data.go_live_timeframe + ) { + return false; + } + const eligibilityDetails: PoEligibleData = { + business: { + country: data.country, + type: data.business_type, + mcc: data.mcc, + }, + store: { + annual_revenue: data.annual_revenue, + go_live_timeframe: data.go_live_timeframe, + }, + }; + + try { + const eligibleResult = await apiFetch< PoEligibleResult >( { + path: '/wc/v3/payments/onboarding/router/po_eligible', + method: 'POST', + data: eligibilityDetails, + } ); + + return 'eligible' === eligibleResult.result; + } catch ( error ) { + // Fall back to full KYC scenario. + return false; + } + }; + + const fetchKeys = async () => { + // By default, we assume the merchant is not eligible for PO. + let isEligible = false; + + // If we are resuming an onboarding session, we don't need to check for PO eligibility again. + if ( ! continueKyc ) { + isEligible = await isEligibleForPo(); + } + + const path = addQueryArgs( + `${ NAMESPACE }/onboarding/kyc/session`, + { + self_assessment: fromDotNotation( data ), + progressive: isEligible, + } + ); + const accountSession = await apiFetch< AccountKycSessionData >( { + path: path, + method: 'GET', + } ); + if ( + accountSession.publishableKey && + accountSession.clientSecret + ) { + setPublishableKey( accountSession.publishableKey ); + setLocale( accountSession.locale ); + setClientSecret( () => () => + Promise.resolve( accountSession.clientSecret ) + ); // Ensure clientSecret is wrapped as a function returning a Promise + } else { + setLoading( false ); + setLoadErrorMessage( + __( + "Failed to create account session. Please check that you're using the latest version of WooCommerce Payments.", + 'woocommerce-payments' + ) + ); + } + }; + + fetchKeys(); + }, [ data, continueKyc ] ); + + // Initialize the Stripe Connect instance only once when publishableKey and clientSecret are ready + useEffect( () => { + if ( publishableKey && clientSecret && ! stripeConnectInstance ) { + const stripeInstance = loadConnectAndInitialize( { + publishableKey: publishableKey, + fetchClientSecret: clientSecret, // Pass the function returning the Promise + appearance: { + // See all possible variables below + overlays: 'drawer', + variables: appearance.variables, + }, + locale: locale.replace( '_', '-' ), + } ); + + setStripeConnectInstance( stripeInstance ); + } + }, [ publishableKey, clientSecret, stripeConnectInstance, locale ] ); + + return ( + <> + { loading && } + { loadErrorMessage && ( + { loadErrorMessage } + ) } + { stripeConnectInstance && ( + + { + const urlParams = new URLSearchParams( + window.location.search + ); + const urlSource = + urlParams + .get( 'source' ) + ?.replace( /[^\w-]+/g, '' ) || 'unknown'; + try { + const response = await apiFetch< + FinalizeResponse + >( { + path: `${ NAMESPACE }/onboarding/kyc/finalize`, + method: 'POST', + data: { + source: urlSource, + from: 'WCPAY_ONBOARDING_WIZARD', + clientSecret: clientSecret, + }, + } ); + + if ( response.success ) { + window.location.href = getOverviewUrl( + { + ...response.params, + 'wcpay-connection-success': '1', + }, + 'WCPAY_ONBOARDING_WIZARD' + ); + } else { + // If a non-success response is received we should redirect to the Connect page with an error flag: + window.location.href = getConnectUrl( + { + ...response.params, + 'wcpay-connection-error': '1', + }, + 'WCPAY_ONBOARDING_WIZARD' + ); + } + } catch ( error ) { + // If an error response is received we should redirect to the Connect page with an error flag: + // Note that this should never happen, since we always expect a response from the server. + window.location.href = getConnectUrl( + { + 'wcpay-connection-error': '1', + source: urlSource, + }, + 'WCPAY_ONBOARDING_WIZARD' + ); + } + } } + /> + + ) } + + ); +}; + +export default EmbeddedKyc; diff --git a/client/onboarding/steps/test/embedded-onboarding.tsx b/client/onboarding/steps/test/embedded-onboarding.tsx new file mode 100644 index 00000000000..bd3ee34b1bd --- /dev/null +++ b/client/onboarding/steps/test/embedded-onboarding.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import apiFetch from '@wordpress/api-fetch'; +import { loadConnectAndInitialize } from '@stripe/connect-js'; + +/** + * Internal dependencies + */ +import EmbeddedKyc from '../embedded-kyc'; + +jest.mock( '@wordpress/api-fetch' ); +jest.mock( '@stripe/connect-js', () => ( { + loadConnectAndInitialize: jest.fn(), +} ) ); + +// Mock data, setData from OnboardingContext +const data = { + country: 'US', + business_type: 'individual', + mcc: 'most_popular__software_services', + annual_revenue: 'less_than_250k', + go_live_timeframe: 'within_1month', +}; +const setData = jest.fn(); + +jest.mock( '../../context', () => ( { + useOnboardingContext: jest.fn( () => ( { + data, + setData, + } ) ), +} ) ); + +jest.mock( 'components/stepper', () => ( { + useStepperContext: jest.fn( () => ( { + currentStep: 'loading', + } ) ), +} ) ); + +describe( 'EmbeddedOnboarding', () => { + beforeEach( () => { + jest.clearAllMocks(); // Clear all mocks between tests + } ); + + it( 'should show error if account session fails', async () => { + jest.mocked( apiFetch ).mockResolvedValueOnce( { + result: 'eligible', + data: [], + } ); + + jest.mocked( apiFetch ).mockResolvedValueOnce( { + success: false, + } ); + + render( ); + + await waitFor( () => + expect( + screen.getByText( /Failed to create account session/i ) + ).toBeInTheDocument() + ); + } ); + + it( 'should initialize Stripe Connect when publishableKey and clientSecret are set', async () => { + jest.mocked( apiFetch ).mockResolvedValueOnce( { + result: 'eligible', + data: [], + } ); + + const mockAccountSessionData = { + publishableKey: 'test_publishable_key', + clientSecret: 'test_client_secret', + locale: 'en_US', + }; + jest.mocked( apiFetch ).mockResolvedValueOnce( mockAccountSessionData ); + + render( ); + + await waitFor( () => + expect( loadConnectAndInitialize ).toHaveBeenCalledWith( { + publishableKey: mockAccountSessionData.publishableKey, + fetchClientSecret: expect.any( Function ), + appearance: { + overlays: 'drawer', + variables: expect.any( Object ), + }, + locale: 'en-US', // Locale should be formatted correctly + } ) + ); + } ); +} ); diff --git a/client/onboarding/strings.tsx b/client/onboarding/strings.tsx index 02ea32d2998..432e4aae21d 100644 --- a/client/onboarding/strings.tsx +++ b/client/onboarding/strings.tsx @@ -44,6 +44,16 @@ export default { 'woocommerce-payments' ), }, + embedded: { + heading: __( + 'One last step! Verify your identity with our partner', + 'woocommerce-payments' + ), + subheading: __( + 'This info will verify your account', + 'woocommerce-payments' + ), + }, }, fields: { country: __( diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 7a5efb46b0c..eb0c84771f6 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -36,6 +36,10 @@ body.wcpay-onboarding__body { &:last-child { justify-self: end; } + + &.hide { + visibility: hidden; + } } &-logo { diff --git a/client/onboarding/types.ts b/client/onboarding/types.ts index 2f57aa08e94..c2781390524 100644 --- a/client/onboarding/types.ts +++ b/client/onboarding/types.ts @@ -2,7 +2,7 @@ * Internal dependencies */ -export type OnboardingSteps = 'business' | 'store' | 'loading'; +export type OnboardingSteps = 'business' | 'store' | 'embedded' | 'loading'; export type OnboardingFields = { country?: string; @@ -13,6 +13,15 @@ export type OnboardingFields = { go_live_timeframe?: string; }; +export interface OnboardingProps { + country?: string; + type?: string; + structure?: string; + mcc?: string; + annual_revenue?: string; + go_live_timeframe?: string; +} + export interface PoEligibleResult { result: 'eligible' | 'not_eligible'; } @@ -55,3 +64,13 @@ export interface MccsDisplayTreeItem { mcc?: number; keywords?: string[]; } + +export interface AccountKycSession { + clientSecret: string; + expiresAt: number; + accountId: string; + isLive: boolean; + accountCreated: boolean; + publishableKey: string; + locale: string; +} diff --git a/client/overview/index.js b/client/overview/index.js index 5a2cc84ba52..f6d5afd4a72 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -190,6 +190,7 @@ const OverviewPage = () => { + { showConnectionSuccess && } { ! accountRejected && ! accountUnderReview && ( diff --git a/client/utils/index.js b/client/utils/index.js index bc24bf44f4f..ba5b0f9493a 100644 --- a/client/utils/index.js +++ b/client/utils/index.js @@ -100,6 +100,40 @@ export const getDocumentUrl = ( documentId ) => { ); }; +export const getConnectUrl = ( urlParams, from ) => { + // Ensure urlParams is an object. + const queryParams = typeof urlParams === 'object' ? urlParams : {}; + + const baseParams = { + page: 'wc-admin', + path: '/payments/connect', + source: queryParams.source?.replace( /[^\w-]+/g, '' ) || 'unknown', + from: from, + }; + + // Merge queryParams and baseParams into baseParams, ensuring baseParams takes precedence. + const params = { ...queryParams, ...baseParams }; + + return getAdminUrl( params ); +}; + +export const getOverviewUrl = ( urlParams, from ) => { + // Ensure urlParams is an object. + const queryParams = typeof urlParams === 'object' ? urlParams : {}; + + const baseParams = { + page: 'wc-admin', + path: '/payments/overview', + source: queryParams.source?.replace( /[^\w-]+/g, '' ) || 'unknown', + from: from, + }; + + // Merge queryParams and baseParams into baseParams, ensuring baseParams takes precedence. + const params = { ...queryParams, ...baseParams }; + + return getAdminUrl( params ); +}; + /** * Returns the URL to the WooPayments settings. * diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 448d85831b2..85f3439ba85 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -347,6 +347,23 @@ public function add_payments_menu() { remove_submenu_page( 'wc-admin&path=/payments/connect', 'wc-admin&path=/payments/onboarding' ); } + // Register /payments/onboarding/kyc only when we have a Stripe account, but the Stripe KYC is not finished (details not submitted). + if ( WC_Payments_Features::is_embedded_kyc_enabled() && $this->account->is_stripe_connected() && ! $this->account->is_details_submitted() ) { + wc_admin_register_page( + [ + 'id' => 'wc-payments-onboarding-kyc', + 'title' => __( 'Continue onboarding', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/onboarding/kyc', + 'capability' => 'manage_woocommerce', + 'nav_args' => [ + 'parent' => 'wc-payments', + ], + ] + ); + remove_submenu_page( 'wc-admin&path=/payments/connect', 'wc-admin&path=/payments/onboarding/kyc' ); + } + if ( $should_render_full_menu ) { if ( $this->account->is_card_present_eligible() && $this->account->has_card_readers_available() ) { $this->admin_child_pages['wc-payments-card-readers'] = [ @@ -598,6 +615,15 @@ public function enqueue_payments_scripts() { wp_enqueue_style( 'WCPAY_ADMIN_SETTINGS' ); } + // Enqueue the onboarding scripts if the user is on the onboarding page. + if ( WC_Payments_Utils::is_onboarding_page() ) { + wp_localize_script( + 'WCPAY_ONBOARDING_SETTINGS', + 'wcpayOnboardingSettings', + [] + ); + } + // TODO: Try to enqueue the JS and CSS bundles lazily (will require changes on WC-Admin). $current_screen = get_current_screen() ? get_current_screen()->base : null; if ( wc_admin_is_registered_page() || 'widgets' === $current_screen ) { @@ -849,6 +875,7 @@ private function get_js_settings(): array { // Set this flag for use in the front-end to alter messages and notices if on-boarding has been disabled. 'onBoardingDisabled' => WC_Payments_Account::is_on_boarding_disabled(), 'onboardingFieldsData' => $this->onboarding_service->get_fields_data( get_user_locale() ), + 'onboardingEmbeddedKycInProgress' => $this->onboarding_service->is_embedded_kyc_in_progress(), 'errorMessage' => $error_message, 'featureFlags' => $this->get_frontend_feature_flags(), 'isSubscriptionsActive' => class_exists( 'WC_Subscriptions' ) && version_compare( WC_Subscriptions::$version, '2.2.0', '>=' ), diff --git a/includes/admin/class-wc-rest-payments-onboarding-controller.php b/includes/admin/class-wc-rest-payments-onboarding-controller.php index bfad476ce87..1a9553d46c8 100644 --- a/includes/admin/class-wc-rest-payments-onboarding-controller.php +++ b/includes/admin/class-wc-rest-payments-onboarding-controller.php @@ -5,8 +5,7 @@ * @package WooCommerce\Payments\Admin */ -use WCPay\Exceptions\API_Exception; -use WCPay\Exceptions\Rest_Request_Exception; +use WCPay\Logger; defined( 'ABSPATH' ) || exit; @@ -49,6 +48,87 @@ public function __construct( * Configure REST API routes. */ public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/kyc/session', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_embedded_kyc_session' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'progressive' => [ + 'required' => false, + 'description' => 'Whether the session is for progressive onboarding.', + 'type' => 'string', + ], + 'collect_payout_requirements' => [ + 'required' => false, + 'description' => 'Whether the session is for collecting payout requirements.', + 'type' => 'string', + ], + 'self_assessment' => [ + 'required' => false, + 'description' => 'The self-assessment data.', + 'type' => 'object', + 'properties' => [ + 'country' => [ + 'type' => 'string', + 'description' => 'The country code where the company is legally registered.', + 'required' => true, + ], + 'business_type' => [ + 'type' => 'string', + 'description' => 'The company incorporation type.', + 'required' => true, + ], + 'mcc' => [ + 'type' => 'string', + 'description' => 'The merchant category code. This can either be a true MCC or an MCCs tree item id from the onboarding form.', + 'required' => true, + ], + 'annual_revenue' => [ + 'type' => 'string', + 'description' => 'The estimated annual revenue bucket id.', + 'required' => true, + ], + 'go_live_timeframe' => [ + 'type' => 'string', + 'description' => 'The timeframe bucket for the estimated first live transaction.', + 'required' => true, + ], + 'url' => [ + 'type' => 'string', + 'description' => 'The URL of the store.', + 'required' => true, + ], + ], + ], + ], + ] + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/kyc/finalize', + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'finalize_embedded_kyc' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'source' => [ + 'required' => false, + 'description' => 'The very first entry point the merchant entered our onboarding flow.', + 'type' => 'string', + ], + 'from' => [ + 'required' => false, + 'description' => 'The previous step in the onboarding flow leading the merchant to arrive at the current step.', + 'type' => 'string', + ], + ], + ] + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/business_types', @@ -116,6 +196,72 @@ public function register_routes() { ); } + /** + * Create an account embedded KYC session via the API. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function get_embedded_kyc_session( WP_REST_Request $request ) { + $account_session = $this->onboarding_service->create_embedded_kyc_session( + ! empty( $request->get_param( 'self_assessment' ) ) ? wc_clean( wp_unslash( $request->get_param( 'self_assessment' ) ) ) : [], + ! empty( $request->get_param( 'progressive' ) ) && 'true' === $request->get_param( 'progressive' ), + ! empty( $request->get_param( 'collect_payout_requirements' ) ) && 'true' === $request->get_param( 'collect_payout_requirements' ) + ); + + if ( $account_session ) { + $account_session['locale'] = get_user_locale(); + } + + // Set the onboarding in progress option. + $this->onboarding_service->set_embedded_kyc_in_progress(); + + return rest_ensure_response( $account_session ); + } + + /** + * Finalize the embedded KYC session via the API. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_HTTP_Response|WP_REST_Response + */ + public function finalize_embedded_kyc( WP_REST_Request $request ) { + $source = $request->get_param( 'source' ) ?? ''; + $from = $request->get_param( 'from' ) ?? ''; + $actioned_notes = WC_Payments_Onboarding_Service::get_actioned_notes(); + + // Call the API to finalize the onboarding. + try { + $response = $this->onboarding_service->finalize_embedded_kyc( + get_user_locale(), + $source, + $actioned_notes + ); + } catch ( Exception $e ) { + return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] ); + } + + // Handle some post-onboarding tasks and get the redirect params. + $finalize = WC_Payments::get_account_service()->finalize_embedded_connection( + $response['mode'], + [ + 'promo' => $response['promotion_id'] ?? '', + 'from' => $from, + 'source' => $source, + ] + ); + + // Return the response, the client will handle the redirect. + return rest_ensure_response( + array_merge( + $response, + $finalize + ) + ); + } + /** * Get business types via API. * diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 84620ca71c7..3f8fbd60789 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -9,8 +9,6 @@ exit; // Exit if accessed directly. } -use Automattic\WooCommerce\Admin\Notes\DataStore; -use Automattic\WooCommerce\Admin\Notes\Note; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\Core\Server\Request\Get_Account; @@ -30,6 +28,7 @@ class WC_Payments_Account { const ONBOARDING_DISABLED_TRANSIENT = 'wcpay_on_boarding_disabled'; const ONBOARDING_STARTED_TRANSIENT = 'wcpay_on_boarding_started'; const ONBOARDING_STATE_TRANSIENT = 'wcpay_stripe_onboarding_state'; + const EMBEDDED_KYC_IN_PROGRESS_OPTION = 'wcpay_onboarding_embedded_kyc_in_progress'; const ERROR_MESSAGE_TRANSIENT = 'wcpay_error_message'; const INSTANT_DEPOSITS_REMINDER_ACTION = 'wcpay_instant_deposit_reminder'; const TRACKS_EVENT_ACCOUNT_CONNECT_START = 'wcpay_account_connect_start'; @@ -61,11 +60,11 @@ class WC_Payments_Account { private $action_scheduler_service; /** - * WC_Payments_Session_Service instance for working with session information + * WC_Payments_Onboarding_Service instance for working with onboarding business logic * - * @var WC_Payments_Session_Service + * @var WC_Payments_Onboarding_Service */ - private $session_service; + private $onboarding_service; /** * WC_Payments_Redirect_Service instance for handling redirects business logic @@ -80,20 +79,20 @@ class WC_Payments_Account { * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param Database_Cache $database_cache Database cache util. * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Action scheduler service. - * @param WC_Payments_Session_Service $session_service Session service. + * @param WC_Payments_Onboarding_Service $onboarding_service Onboarding service. * @param WC_Payments_Redirect_Service $redirect_service Redirect service. */ public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Action_Scheduler_Service $action_scheduler_service, - WC_Payments_Session_Service $session_service, + WC_Payments_Onboarding_Service $onboarding_service, WC_Payments_Redirect_Service $redirect_service ) { $this->payments_api_client = $payments_api_client; $this->database_cache = $database_cache; $this->action_scheduler_service = $action_scheduler_service; - $this->session_service = $session_service; + $this->onboarding_service = $onboarding_service; $this->redirect_service = $redirect_service; } @@ -932,6 +931,25 @@ public function maybe_redirect_from_connect_page(): bool { return false; } + // There are certain cases where it is best to refresh the account data + // to be sure we are dealing with the current account state on the Connect page: + // - When the merchant is coming from the onboarding wizard it is best to refresh the account data because + // the merchant might have started the embedded Stripe KYC. + // - When the merchant is coming from the embedded KYC, definitely refresh the account data. + // The account data shouldn't be refreshed with force disconnected option enabled. + if ( ! WC_Payments_Utils::force_disconnected_enabled() + && in_array( + WC_Payments_Onboarding_Service::get_from(), + [ + WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD, + WC_Payments_Onboarding_Service::FROM_ONBOARDING_KYC, + ], + true + ) ) { + + $this->refresh_account_data(); + } + // If everything is in good working condition, redirect to Payments Overview page. if ( $this->has_working_jetpack_connection() && $this->is_stripe_account_valid() ) { $this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_CONNECT_PAGE ); @@ -1174,6 +1192,7 @@ public function maybe_handle_onboarding() { || ( WC_Payments_Onboarding_Service::FROM_STRIPE === $from && ! empty( $_GET['wcpay-connection-error'] ) ) ) { delete_transient( self::ONBOARDING_STATE_TRANSIENT ); + delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION ); } // Make changes to account data as instructed by action GET params. @@ -1400,6 +1419,7 @@ public function maybe_handle_onboarding() { if ( $create_test_drive_account ) { // Since there should be no Stripe KYC needed, make sure we start with a clean state. delete_transient( self::ONBOARDING_STATE_TRANSIENT ); + delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION ); // If we have the auto_start_test_drive_onboarding flag, we redirect to the Connect page // to let the JS logic take control and orchestrate things. @@ -1482,7 +1502,7 @@ public function maybe_handle_onboarding() { if ( $create_test_drive_account && ! empty( $redirect_to ) ) { wp_send_json_success( [ 'redirect_to' => $redirect_to ] ); } else { - // Redirect the user to where our Stripe onboarding instructed. + // Redirect the user to where our Stripe onboarding instructed (or to our own embedded Stripe KYC). $this->redirect_service->redirect_to( $redirect_to ); } } catch ( API_Exception $e ) { @@ -1561,6 +1581,7 @@ private function cleanup_on_account_reset() { // Discard any ongoing onboarding session. delete_transient( self::ONBOARDING_STATE_TRANSIENT ); delete_transient( self::ONBOARDING_STARTED_TRANSIENT ); + delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION ); delete_transient( 'woopay_enabled_by_default' ); // Clear the cache to avoid stale data. @@ -1744,6 +1765,24 @@ private function get_onboarding_return_url( string $wcpay_connect_from ): string } } + /** + * Get the URL to the embedded onboarding KYC page. + * + * @param array $additional_args Additional query args to add to the URL. + * + * @return string + */ + private function get_onboarding_kyc_url( array $additional_args = [] ): string { + $params = [ + 'page' => 'wc-admin', + 'path' => '/payments/onboarding/kyc', + ]; + + $params = array_merge( $params, $additional_args ); + + return admin_url( add_query_arg( $params, 'admin.php' ) ); + } + /** * Initializes the onboarding flow by fetching the URL from the API and redirecting to it. * @@ -1775,83 +1814,29 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne WC_Payments_Onboarding_Service::set_onboarding_eligibility_modal_dismissed(); } + // If we are in the middle of an embedded onboarding, go to the KYC page. + // In this case, we don't need to generate a return URL from Stripe, and we + // can rely on the JS logic to generate the session. + // Currently under feature flag. + if ( WC_Payments_Features::is_embedded_kyc_enabled() && $this->onboarding_service->is_embedded_kyc_in_progress() ) { + // We want to carry over the connect link from value because with embedded KYC + // there is no interim step for the user. + $additional_args['from'] = WC_Payments_Onboarding_Service::get_from(); + + return $this->get_onboarding_kyc_url( $additional_args ); + } + + // Else, go on with the normal onboarding redirect logic. $return_url = $this->get_onboarding_return_url( $wcpay_connect_from ); if ( ! empty( $additional_args ) ) { $return_url = add_query_arg( $additional_args, $return_url ); } - $home_url = get_home_url(); - // If the site is running on localhost, use a bogus URL. This is to avoid Stripe's errors. - // wp_http_validate_url does not check that, unfortunately. - $home_is_localhost = 'localhost' === wp_parse_url( $home_url, PHP_URL_HOST ); - $fallback_url = ( 'live' !== $setup_mode || $home_is_localhost ) ? 'https://wcpay.test' : null; - - $current_user = get_userdata( get_current_user_id() ); - - // The general account data. - $account_data = [ - 'setup_mode' => $setup_mode, - // We use the store base country to create a customized account. - 'country' => WC()->countries->get_base_country() ?? null, - 'url' => ! $home_is_localhost && wp_http_validate_url( $home_url ) ? $home_url : $fallback_url, - 'business_name' => get_bloginfo( 'name' ), - ]; - - // Gather all the account data depending on the request context. - // Onboarding self-assessment data. $self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : []; - if ( ! empty( $self_assessment_data ) ) { - $business_type = $self_assessment_data['business_type'] ?? null; - $account_data = WC_Payments_Utils::array_merge_recursive_distinct( - $account_data, - [ - // Overwrite the country if the merchant chose a different one than the Woo base location. - 'country' => $self_assessment_data['country'] ?? null, - 'email' => $self_assessment_data['email'] ?? null, - 'business_name' => $self_assessment_data['business_name'] ?? null, - 'url' => $self_assessment_data['url'] ?? null, - 'mcc' => $self_assessment_data['mcc'] ?? null, - 'business_type' => $business_type, - 'company' => [ - 'structure' => 'company' === $business_type ? ( $self_assessment_data['company']['structure'] ?? null ) : null, - ], - 'individual' => [ - 'first_name' => $self_assessment_data['individual']['first_name'] ?? null, - 'last_name' => $self_assessment_data['individual']['last_name'] ?? null, - 'phone' => $self_assessment_data['phone'] ?? null, - ], - 'store' => [ - 'annual_revenue' => $self_assessment_data['annual_revenue'] ?? null, - 'go_live_timeframe' => $self_assessment_data['go_live_timeframe'] ?? null, - ], - ] - ); - } elseif ( 'test_drive' === $setup_mode ) { - $account_data = WC_Payments_Utils::array_merge_recursive_distinct( - $account_data, - [ - 'individual' => [ - 'first_name' => $current_user->first_name ?? null, - 'last_name' => $current_user->last_name ?? null, - ], - ] - ); - + if ( 'test_drive' === $setup_mode ) { // If we get to the overview page, we want to show the success message. $return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url ); } elseif ( 'test' === $setup_mode ) { - $account_data = WC_Payments_Utils::array_merge_recursive_distinct( - $account_data, - [ - 'business_type' => 'individual', - 'mcc' => '5734', - 'individual' => [ - 'first_name' => $current_user->first_name ?? null, - 'last_name' => $current_user->last_name ?? null, - ], - ] - ); - // If we get to the overview page, we want to show the success message. $return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url ); } @@ -1861,7 +1846,8 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne 'site_locale' => get_locale(), ]; - $user_data = $this->get_onboarding_user_data(); + $user_data = $this->onboarding_service->get_onboarding_user_data(); + $account_data = $this->onboarding_service->get_account_data( $setup_mode, $self_assessment_data ); $onboarding_data = $this->payments_api_client->get_onboarding_data( 'live' === $setup_mode, @@ -1869,7 +1855,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne $site_data, WC_Payments_Utils::array_filter_recursive( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. - $this->get_actioned_notes(), + WC_Payments_Onboarding_Service::get_actioned_notes(), $progressive, $collect_payout_requirements ); @@ -1888,6 +1874,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne // Clean up any existing onboarding state. delete_transient( self::ONBOARDING_STATE_TRANSIENT ); + delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION ); return add_query_arg( [ 'wcpay-connection-success' => '1' ], @@ -1920,6 +1907,50 @@ public function maybe_activate_woopay() { } } + /** + * Handle the finalization of an embedded onboarding. This includes updating the cache, setting the gateway mode, + * tracking the event, and redirecting the user to the overview page. + * + * @param string $mode The mode in which the account was created. Either 'test' or 'live'. + * @param array $additional_args Additional query args to add to the redirect URLs. + * + * @return array Returns whether the operation was successful, along with the URL params to handle the redirect. + */ + public function finalize_embedded_connection( string $mode, array $additional_args = [] ): array { + // Clear the account cache. + $this->clear_cache(); + + // Set the gateway options. + $gateway = WC_Payments::get_gateway(); + $gateway->update_option( 'enabled', 'yes' ); + $gateway->update_option( 'test_mode', 'live' !== $mode ? 'yes' : 'no' ); + + // Store a state after completing KYC for tracks. This is stored temporarily in option because + // user might not have agreed to TOS yet. + update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => false ] ); + + // Track account connection finish. + $event_properties = [ + 'mode' => 'test' === $mode ? 'test' : 'live', + 'incentive' => ! empty( $additional_args['promo'] ) ? sanitize_text_field( $additional_args['promo'] ) : '', + 'from' => ! empty( $additional_args['from'] ) ? sanitize_text_field( $additional_args['from'] ) : '', + 'source' => ! empty( $additional_args['source'] ) ? sanitize_text_field( $additional_args['source'] ) : '', + ]; + + $this->tracks_event( + self::TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED, + $event_properties + ); + + $params = $additional_args; + + $params['wcpay-connection-success'] = '1'; + return [ + 'success' => true, + 'params' => $params, + ]; + } + /** * Once the API redirects back to the site after the onboarding flow, verifies the parameters and stores the data. * @@ -2204,59 +2235,6 @@ public function get_latest_tos_agreement() { : null; } - /** - * Returns an array containing the names of all the WCPay related notes that have been actioned. - * - * @return array - */ - private function get_actioned_notes(): array { - $wcpay_note_names = []; - - try { - /** - * Data Store for admin notes - * - * @var DataStore $data_store - */ - $data_store = WC_Data_Store::load( 'admin-note' ); - } catch ( Exception $e ) { - // Don't stop the on-boarding process if something goes wrong here. Log the error and return the empty array - // of actioned notes. - Logger::error( $e ); - return $wcpay_note_names; - } - - // Fetch the last 10 actioned wcpay-promo admin notifications. - $add_like_clause = function ( $where_clause ) { - return $where_clause . " AND name like 'wcpay-promo-%'"; - }; - - add_filter( 'woocommerce_note_where_clauses', $add_like_clause ); - - $wcpay_promo_notes = $data_store->get_notes( - [ - 'status' => [ Note::E_WC_ADMIN_NOTE_ACTIONED ], - 'is_deleted' => false, - 'per_page' => 10, - ] - ); - - remove_filter( 'woocommerce_note_where_clauses', $add_like_clause ); - - // If we didn't get an array back from the data store, return an empty array of results. - if ( ! is_array( $wcpay_promo_notes ) ) { - return $wcpay_note_names; - } - - // Copy the name of each note into the results. - foreach ( (array) $wcpay_promo_notes as $wcpay_note ) { - $note = new Note( $wcpay_note->note_id ); - $wcpay_note_names[] = $note->get_name(); - } - - return $wcpay_note_names; - } - /** * Gets the account country. * @@ -2455,24 +2433,4 @@ public function get_lifetime_total_payment_volume(): int { $account = $this->get_cached_account_data(); return (int) ! empty( $account ) && isset( $account['lifetime_total_payment_volume'] ) ? $account['lifetime_total_payment_volume'] : 0; } - - /** - * Get user data to send to the onboarding flow. - * - * @return array The user data. - */ - private function get_onboarding_user_data(): array { - return [ - 'user_id' => get_current_user_id(), - 'sift_session_id' => $this->session_service->get_sift_session_id(), - 'ip_address' => \WC_Geolocation::get_ip_address(), - 'browser' => [ - 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', - 'accept_language' => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '', - 'content_language' => empty( get_user_locale() ) ? 'en-US' : str_replace( '_', '-', get_user_locale() ), - ], - 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', - 'onboarding_source' => WC_Payments_Onboarding_Service::get_source(), - ]; - } } diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 161cd8e3935..8df7d249206 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -32,6 +32,7 @@ class WC_Payments_Features { const TOKENIZED_CART_PRB_FLAG_NAME = '_wcpay_feature_tokenized_cart_prb'; const PAYMENT_OVERVIEW_WIDGET_FLAG_NAME = '_wcpay_feature_payment_overview_widget'; const WOOPAY_GLOBAL_THEME_SUPPORT_FLAG_NAME = '_wcpay_feature_woopay_global_theme_support'; + const EMBEDDED_KYC_FLAG_NAME = '_wcpay_feature_embedded_kyc'; /** * Indicates whether card payments are enabled for this (Stripe) account. @@ -75,6 +76,15 @@ public static function is_customer_multi_currency_enabled() { return '1' === get_option( '_wcpay_feature_customer_multi_currency', '1' ); } + /** + * Checks whether Embedded KYC is enabled. + * + * @return bool + */ + public static function is_embedded_kyc_enabled(): bool { + return '1' === get_option( self::EMBEDDED_KYC_FLAG_NAME, '0' ); + } + /** * Checks whether WCPay Subscriptions is enabled. * @@ -387,6 +397,7 @@ public static function to_array() { 'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(), 'isPaymentOverviewWidgetEnabled' => self::is_payment_overview_widget_ui_enabled(), 'isStripeEceEnabled' => self::is_stripe_ece_enabled(), + 'isEmbeddedKycEnabled' => self::is_embedded_kyc_enabled(), ] ); } diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 56bb49f6607..e4332f42ab9 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -9,8 +9,11 @@ exit; // Exit if accessed directly. } +use Automattic\WooCommerce\Admin\Notes\DataStore; +use Automattic\WooCommerce\Admin\Notes\Note; use WCPay\Database_Cache; use WCPay\Exceptions\API_Exception; +use WCPay\Logger; /** * Class handling onboarding related business logic. @@ -52,6 +55,7 @@ class WC_Payments_Onboarding_Service { const FROM_OVERVIEW_PAGE = 'WCPAY_OVERVIEW'; const FROM_ACCOUNT_DETAILS = 'WCPAY_ACCOUNT_DETAILS'; const FROM_ONBOARDING_WIZARD = 'WCPAY_ONBOARDING_WIZARD'; + const FROM_ONBOARDING_KYC = 'WCPAY_ONBOARDING_KYC'; // The embedded Stripe KYC step/page. const FROM_SETTINGS = 'WCPAY_SETTINGS'; const FROM_PAYOUTS = 'WCPAY_PAYOUTS'; const FROM_TEST_TO_LIVE = 'WCPAY_TEST_TO_LIVE'; @@ -62,6 +66,7 @@ class WC_Payments_Onboarding_Service { const FROM_WPCOM = 'WPCOM'; const FROM_WPCOM_CONNECTION = 'WPCOM_CONNECTION'; const FROM_STRIPE = 'STRIPE'; + const FROM_STRIPE_EMBEDDED = 'STRIPE_EMBEDDED'; /** * Client for making requests to the WooCommerce Payments API @@ -77,15 +82,24 @@ class WC_Payments_Onboarding_Service { */ private $database_cache; + /** + * Session service. + * + * @var WC_Payments_Session_Service instance for working with session information + */ + private $session_service; + /** * Class constructor * - * @param WC_Payments_API_Client $payments_api_client Payments API client. - * @param Database_Cache $database_cache Database cache util. + * @param WC_Payments_API_Client $payments_api_client Payments API client. + * @param Database_Cache $database_cache Database cache util. + * @param WC_Payments_Session_Service $session_service Session service. */ - public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache ) { + public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Session_Service $session_service ) { $this->payments_api_client = $payments_api_client; $this->database_cache = $database_cache; + $this->session_service = $session_service; } /** @@ -136,6 +150,109 @@ function () use ( $locale ) { ); } + /** + * Retrieve the embedded KYC session and handle initial account creation (if necessary). + * + * Will return the session key used to initialise the embedded onboarding session. + * + * @param array $self_assessment_data Self assessment data. + * @param boolean $progressive Whether the onboarding is progressive. + * @param boolean $collect_payout_requirements Whether to collect payout requirements. + * + * @return array Session data. + * + * @throws API_Exception + */ + public function create_embedded_kyc_session( array $self_assessment_data, bool $progressive = false, bool $collect_payout_requirements = false ): array { + if ( ! $this->payments_api_client->is_server_connected() ) { + return []; + } + $setup_mode = WC_Payments::mode()->is_live() ? 'live' : 'test'; + + // Make sure the onboarding test mode DB flag is set. + self::set_test_mode( 'live' !== $setup_mode ); + + if ( ! $collect_payout_requirements ) { + // Clear onboarding related account options if this is an initial onboarding attempt. + self::clear_account_options(); + } else { + // Since we assume user has already either gotten here from the eligibility modal, + // or has already dismissed it, we should set the modal as dismissed so it doesn't display again. + self::set_onboarding_eligibility_modal_dismissed(); + } + + $site_data = [ + 'site_username' => wp_get_current_user()->user_login, + 'site_locale' => get_locale(), + ]; + $user_data = $this->get_onboarding_user_data(); + $account_data = $this->get_account_data( $setup_mode, $self_assessment_data ); + $actioned_notes = self::get_actioned_notes(); + + try { + $account_session = $this->payments_api_client->initialize_onboarding_embedded_kyc( + 'live' === $setup_mode, + $site_data, + array_filter( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. + array_filter( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. + $actioned_notes, + $progressive, + $collect_payout_requirements + ); + } catch ( API_Exception $e ) { + // If we fail to create the session, return an empty array. + return []; + } + + return [ + 'clientSecret' => $account_session['client_secret'] ?? '', + 'expiresAt' => $account_session['expires_at'] ?? 0, + 'accountId' => $account_session['account_id'] ?? '', + 'isLive' => $account_session['is_live'] ?? false, + 'accountCreated' => $account_session['account_created'] ?? false, + 'publishableKey' => $account_session['publishable_key'] ?? '', + ]; + } + + /** + * Finalize the embedded KYC session. + * + * @param string $locale The locale to use to i18n the data. + * @param string $source The source of the onboarding flow. + * @param array $actioned_notes The actioned notes for this onboarding. + * + * @return array Containing the following keys: success, account_id, mode. + * + * @throws API_Exception + */ + public function finalize_embedded_kyc( string $locale, string $source, array $actioned_notes ): array { + if ( ! $this->payments_api_client->is_server_connected() ) { + return [ + 'success' => false, + ]; + } + + $result = $this->payments_api_client->finalize_onboarding_embedded_kyc( $locale, $source, $actioned_notes ); + + $success = $result['success'] ?? false; + $details_submitted = $result['details_submitted'] ?? false; + + if ( ! $result || ! $success ) { + throw new API_Exception( __( 'Failed to finalize onboarding session.', 'woocommerce-payments' ), 'wcpay-onboarding-finalize-error', 400 ); + } + + // Clear the onboarding in progress option, since the onboarding flow is now complete. + $this->clear_embedded_kyc_in_progress(); + + return [ + 'success' => $success, + 'details_submitted' => $details_submitted, + 'account_id' => $result['account_id'] ?? '', + 'mode' => $result['mode'], + 'promotion_id' => $result['promotion_id'] ?? null, + ]; + } + /** * Gets and caches the business types per country from the server. * @@ -215,6 +332,183 @@ public function add_admin_body_classes( string $classes = '' ): string { return $classes; } + /** + * Get account data for onboarding from self assestment data. + * + * @param string $setup_mode Setup mode. + * @param array $self_assessment_data Self assessment data. + * + * @return array Account data. + */ + public function get_account_data( string $setup_mode, array $self_assessment_data ): array { + $home_url = get_home_url(); + // If the site is running on localhost, use a bogus URL. This is to avoid Stripe's errors. + // wp_http_validate_url does not check that, unfortunately. + $home_is_localhost = 'localhost' === wp_parse_url( $home_url, PHP_URL_HOST ); + $fallback_url = ( 'live' !== $setup_mode || $home_is_localhost ) ? 'https://wcpay.test' : null; + $current_user = get_userdata( get_current_user_id() ); + + // The general account data. + $account_data = [ + 'setup_mode' => $setup_mode, + // We use the store base country to create a customized account. + 'country' => WC()->countries->get_base_country() ?? null, + 'url' => ! $home_is_localhost && wp_http_validate_url( $home_url ) ? $home_url : $fallback_url, + 'business_name' => get_bloginfo( 'name' ), + ]; + + if ( ! empty( $self_assessment_data ) ) { + $business_type = $self_assessment_data['business_type'] ?? null; + $account_data = WC_Payments_Utils::array_merge_recursive_distinct( + $account_data, + [ + // Overwrite the country if the merchant chose a different one than the Woo base location. + 'country' => $self_assessment_data['country'] ?? null, + 'email' => $self_assessment_data['email'] ?? null, + 'business_name' => $self_assessment_data['business_name'] ?? null, + 'url' => $self_assessment_data['url'] ?? null, + 'mcc' => $self_assessment_data['mcc'] ?? null, + 'business_type' => $business_type, + 'company' => [ + 'structure' => 'company' === $business_type ? ( $self_assessment_data['company']['structure'] ?? null ) : null, + ], + 'individual' => [ + 'first_name' => $self_assessment_data['individual']['first_name'] ?? null, + 'last_name' => $self_assessment_data['individual']['last_name'] ?? null, + 'phone' => $self_assessment_data['phone'] ?? null, + ], + 'store' => [ + 'annual_revenue' => $self_assessment_data['annual_revenue'] ?? null, + 'go_live_timeframe' => $self_assessment_data['go_live_timeframe'] ?? null, + ], + ] + ); + } elseif ( 'test_drive' === $setup_mode ) { + $account_data = WC_Payments_Utils::array_merge_recursive_distinct( + $account_data, + [ + 'individual' => [ + 'first_name' => $current_user->first_name ?? null, + 'last_name' => $current_user->last_name ?? null, + ], + ] + ); + } elseif ( 'test' === $setup_mode ) { + $account_data = WC_Payments_Utils::array_merge_recursive_distinct( + $account_data, + [ + 'business_type' => 'individual', + 'mcc' => '5734', + 'individual' => [ + 'first_name' => $current_user->first_name ?? null, + 'last_name' => $current_user->last_name ?? null, + ], + ] + ); + } + return $account_data; + } + + /** + * Get user data to send to the onboarding flow. + * + * @return array The user data. + */ + public function get_onboarding_user_data(): array { + return [ + 'user_id' => get_current_user_id(), + 'sift_session_id' => $this->session_service->get_sift_session_id(), + 'ip_address' => \WC_Geolocation::get_ip_address(), + 'browser' => [ + 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', + 'accept_language' => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '', + 'content_language' => empty( get_user_locale() ) ? 'en-US' : str_replace( '_', '-', get_user_locale() ), + ], + 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', + 'onboarding_source' => self::get_source(), + ]; + } + + /** + * Determine whether an embedded KYC flow is in progress. + * + * @return bool True if embedded KYC is in progress, false otherwise. + */ + public function is_embedded_kyc_in_progress(): bool { + return in_array( get_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION, 'no' ), [ 'yes', '1' ], true ); + } + + /** + * Mark the embedded KYC flow as in progress. + * + * @return bool Whether we successfully marked the flow as in progress. + */ + public function set_embedded_kyc_in_progress(): bool { + return update_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION, 'yes' ); + } + + /** + * Clear any embedded KYC in progress flags. + * + * @return boolean Whether we successfully cleared the flags. + */ + public function clear_embedded_kyc_in_progress(): bool { + return delete_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION ); + } + + /** + * Get actioned notes. + * + * @return array + */ + public static function get_actioned_notes(): array { + $wcpay_note_names = []; + + try { + /** + * Data Store for admin notes + * + * @var DataStore $data_store + */ + $data_store = WC_Data_Store::load( 'admin-note' ); + } catch ( Exception $e ) { + // Don't stop the on-boarding process if something goes wrong here. Log the error and return the empty array + // of actioned notes. + Logger::error( $e ); + return $wcpay_note_names; + } + + // Fetch the last 10 actioned wcpay-promo admin notifications. + $add_like_clause = function ( $where_clause ) { + return $where_clause . " AND name like 'wcpay-promo-%'"; + }; + + add_filter( 'woocommerce_note_where_clauses', $add_like_clause ); + + $wcpay_promo_notes = $data_store->get_notes( + [ + 'status' => [ Note::E_WC_ADMIN_NOTE_ACTIONED ], + 'is_deleted' => false, + 'per_page' => 10, + ] + ); + + remove_filter( 'woocommerce_note_where_clauses', $add_like_clause ); + + // If we didn't get an array back from the data store, return an empty array of results. + if ( ! is_array( $wcpay_promo_notes ) ) { + return $wcpay_note_names; + } + + // Copy the name of each note into the results. + foreach ( (array) $wcpay_promo_notes as $wcpay_note ) { + $note = new Note( $wcpay_note->note_id ); + $wcpay_note_names[] = $note->get_name(); + } + + return $wcpay_note_names; + } + /** * Clear any account options we may want to reset when a new onboarding flow is initialised. * Currently, just deletes the option which stores whether the eligibility modal has been dismissed. diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 4593fe65019..0b2506d73b6 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -554,6 +554,19 @@ public static function is_payments_settings_page(): bool { ); } + /** + * Checks if the currently displayed page is the WooPayments onboarding page. + * + * @return bool + */ + public static function is_onboarding_page(): bool { + return ( + is_admin() + && isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification + && isset( $_GET['path'] ) && '/payments/onboarding' === $_GET['path'] // phpcs:ignore WordPress.Security.NonceVerification + ); + } + /** * Converts a locale to the closest supported by Stripe.js. * diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 2a5f8711bec..cd8854afa93 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -505,7 +505,8 @@ public static function init() { self::$action_scheduler_service = new WC_Payments_Action_Scheduler_Service( self::$api_client, self::$order_service ); self::$session_service = new WC_Payments_Session_Service( self::$api_client ); self::$redirect_service = new WC_Payments_Redirect_Service( self::$api_client ); - self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$session_service, self::$redirect_service ); + self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache, self::$session_service ); + self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$onboarding_service, self::$redirect_service ); self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$database_cache, self::$session_service, self::$order_service ); self::$token_service = new WC_Payments_Token_Service( self::$api_client, self::$customer_service ); self::$remote_note_service = new WC_Payments_Remote_Note_Service( WC_Data_Store::load( 'admin-note' ) ); @@ -514,7 +515,6 @@ public static function init() { self::$localization_service = new WC_Payments_Localization_Service(); self::$failed_transaction_rate_limiter = new Session_Rate_Limiter( Session_Rate_Limiter::SESSION_KEY_DECLINED_CARD_REGISTRY, 5, 10 * MINUTE_IN_SECONDS ); self::$order_success_page = new WC_Payments_Order_Success_Page(); - self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache ); self::$woopay_util = new WooPay_Utilities(); self::$woopay_tracker = new WooPay_Tracker( self::get_wc_payments_http() ); self::$incentives_service = new WC_Payments_Incentives_Service( self::$database_cache ); diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index d360b76f07f..3ec8bc40e4b 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -970,6 +970,64 @@ public function get_onboarding_data( bool $live_account, string $return_url, arr return $this->request( $request_args, self::ONBOARDING_API . '/init', self::POST, true, true ); } + /** + * Initialize the onboarding embedded KYC flow, returning a session object which is used by the frontend. + * + * @param bool $live_account Whether to create live account. + * @param array $site_data Site data. + * @param array $user_data User data. + * @param array $account_data Account data to be prefilled. + * @param array $actioned_notes Actioned notes to be sent. + * @param bool $progressive Whether progressive onboarding should be enabled for this onboarding. + * @param bool $collect_payout_requirements Whether we need to collect payout requirements. + * + * @return array + * + * @throws API_Exception + */ + public function initialize_onboarding_embedded_kyc( bool $live_account, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $progressive = false, bool $collect_payout_requirements = false ): array { + $request_args = apply_filters( + 'wc_payments_get_onboarding_data_args', + [ + 'site_data' => $site_data, + 'user_data' => $user_data, + 'account_data' => $account_data, + 'actioned_notes' => $actioned_notes, + 'create_live_account' => $live_account, + 'progressive' => $progressive, + 'collect_payout_requirements' => $collect_payout_requirements, + ] + ); + + $session = $this->request( $request_args, self::ONBOARDING_API . '/embedded', self::POST, true, true ); + + if ( ! is_array( $session ) ) { + return []; + } + + return $session; + } + + /** + * Finalize the onboarding embedded KYC flow. + * + * @param string $locale The locale to use to i18n the data. + * @param string $source The source of the onboarding flow. + * @param array $actioned_notes The actioned notes on the account related to this onboarding. + * @return array + * + * @throws API_Exception + */ + public function finalize_onboarding_embedded_kyc( string $locale, string $source, array $actioned_notes ): array { + $request_args = [ + 'locale' => $locale, + 'source' => $source, + 'actioned_notes' => $actioned_notes, + ]; + + return $this->request( $request_args, self::ONBOARDING_API . '/embedded/finalize', self::POST, true, true ); + } + /** * Get the fields data to be used by the onboarding flow. * diff --git a/package-lock.json b/package-lock.json index 096544d3a4a..03592cffe24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "dependencies": { "@automattic/interpolate-components": "1.2.1", "@fingerprintjs/fingerprintjs": "3.4.1", + "@stripe/connect-js": "3.3.12", + "@stripe/react-connect-js": "3.3.13", "@stripe/react-stripe-js": "2.5.1", "@stripe/stripe-js": "1.15.1", "@woocommerce/explat": "2.3.0", @@ -9155,6 +9157,21 @@ } } }, + "node_modules/@stripe/connect-js": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@stripe/connect-js/-/connect-js-3.3.12.tgz", + "integrity": "sha512-hXbgvGq9Lb6BYgsb8lcbjL76Yqsxr0yAj6T9ZFTfUK0O4otI5GSEWum9do9rf/E5OfYy6fR1FG/77Jve2w1o6Q==" + }, + "node_modules/@stripe/react-connect-js": { + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@stripe/react-connect-js/-/react-connect-js-3.3.13.tgz", + "integrity": "sha512-kMxYjeQUcl/ixu/mSeX5QGIr/MuP+YxFSEBdb8j6w+tbK82tmcjyFDgoQTQwVXNqUV6jI66Kks3XcfpPRfeiJA==", + "peerDependencies": { + "@stripe/connect-js": ">=3.3.11", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@stripe/react-stripe-js": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.5.1.tgz", diff --git a/package.json b/package.json index e77b6facd24..6f11833d574 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "dependencies": { "@automattic/interpolate-components": "1.2.1", "@fingerprintjs/fingerprintjs": "3.4.1", + "@stripe/connect-js": "3.3.12", + "@stripe/react-connect-js": "3.3.13", "@stripe/react-stripe-js": "2.5.1", "@stripe/stripe-js": "1.15.1", "@woocommerce/explat": "2.3.0", diff --git a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php index e7bc456c46b..07a00a2b690 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php @@ -140,4 +140,87 @@ public function test_get_progressive_onboarding_not_eligible() { $response->get_data() ); } + + public function test_get_embedded_kyc_session() { + $kyc_session = [ + 'clientSecret' => 'accs_secret__XXX', + 'expiresAt' => time() + 120, + 'accountId' => 'acct_XXX', + 'isLive' => false, + 'accountCreated' => true, + 'publishableKey' => 'pk_test_XXX', + ]; + + $this->mock_onboarding_service + ->expects( $this->once() ) + ->method( 'create_embedded_kyc_session' ) + ->willReturn( + $kyc_session + ); + + $this->mock_onboarding_service + ->expects( $this->once() ) + ->method( 'set_embedded_kyc_in_progress' ); + + $request = new WP_REST_Request( 'GET' ); + $request->set_query_params( + [ + 'progressive' => true, + 'create_live_account' => true, + ] + ); + + $response = $this->controller->get_embedded_kyc_session( $request ); + $this->assertSame( 200, $response->status ); + $this->assertSame( + array_merge( + $kyc_session, + [ + 'locale' => 'en_US', + ] + ), + $response->get_data() + ); + } + + public function test_finalize_embedded_kyc() { + $response_data = [ + 'success' => true, + 'account_id' => 'acct_1PvxJQQujq4nxoo6', + 'details_submitted' => true, + 'mode' => 'test', + 'promotion_id' => null, + ]; + $this->mock_onboarding_service + ->expects( $this->once() ) + ->method( 'finalize_embedded_kyc' ) + ->willReturn( + $response_data + ); + + $request = new WP_REST_Request( 'POST' ); + $request->set_body_params( + [ + 'source' => 'embedded', + 'from' => 'wcpay-connect', + ] + ); + + $response = $this->controller->finalize_embedded_kyc( $request ); + $this->assertSame( 200, $response->status ); + $this->assertSame( + array_merge( + $response_data, + [ + 'params' => [ + 'promo' => '', + 'from' => 'wcpay-connect', + 'source' => 'embedded', + 'wcpay-connection-success' => '1', + ], + ] + ), + $response->get_data() + ); + } } diff --git a/tests/unit/test-class-wc-payments-account-capital.php b/tests/unit/test-class-wc-payments-account-capital.php index 240d2bc2e51..cf48a9ceb26 100644 --- a/tests/unit/test-class-wc-payments-account-capital.php +++ b/tests/unit/test-class-wc-payments-account-capital.php @@ -50,11 +50,11 @@ class WC_Payments_Account_Capital_Test extends WCPAY_UnitTestCase { private $mock_action_scheduler_service; /** - * Mock WC_Payments_Session_Service. + * Mock WC_Payments_Onboarding_Service. * - * @var WC_Payments_Session_Service|MockObject + * @var WC_Payments_Onboarding_Service|MockObject */ - private $mock_session_service; + private $mock_onboarding_service; /** * Mock WC_Payments_Redirect_Service. @@ -80,13 +80,13 @@ public function set_up() { $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); $this->mock_database_cache = $this->createMock( Database_Cache::class ); $this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); - $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); + $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class ); $this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class ); // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. $this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) ->setMethods( [ 'init_hooks' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] ) ->getMock(); $this->wcpay_account->init_hooks(); } @@ -105,7 +105,7 @@ public function tear_down() { public function test_maybe_redirect_by_get_param_will_run() { $wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) ->setMethodsExcept( [ 'init_hooks' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] ) ->getMock(); $wcpay_account->init_hooks(); diff --git a/tests/unit/test-class-wc-payments-account-link.php b/tests/unit/test-class-wc-payments-account-link.php index 4ab2c92a8af..e8711f10b88 100644 --- a/tests/unit/test-class-wc-payments-account-link.php +++ b/tests/unit/test-class-wc-payments-account-link.php @@ -48,11 +48,11 @@ class WC_Payments_Account_Server_Links_Test extends WCPAY_UnitTestCase { private $mock_action_scheduler_service; /** - * Mock WC_Payments_Session_Service. + * Mock WC_Payments_Onboarding_Service. * - * @var WC_Payments_Session_Service|MockObject + * @var WC_Payments_Onboarding_Service|MockObject */ - private $mock_session_service; + private $mock_onboarding_service; /** * Mock WC_Payments_Redirect_Service. @@ -78,13 +78,13 @@ public function set_up() { $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); $this->mock_database_cache = $this->createMock( Database_Cache::class ); $this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); - $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); + $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class ); $this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class ); // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. $this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) ->setMethods( [ 'init_hooks' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] ) ->getMock(); $this->wcpay_account->init_hooks(); diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index 61215b93466..fa685451897 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -51,11 +51,11 @@ class WC_Payments_Account_Test extends WCPAY_UnitTestCase { private $mock_action_scheduler_service; /** - * Mock WC_Payments_Session_Service. + * Mock WC_Payments_Onboarding_Service. * - * @var WC_Payments_Session_Service|MockObject + * @var WC_Payments_Onboarding_Service|MockObject */ - private $mock_session_service; + private $mock_onboarding_service; /** * Mock WC_Payments_Redirect_Service. @@ -82,10 +82,10 @@ public function set_up() { $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); $this->mock_database_cache = $this->createMock( Database_Cache::class ); $this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); - $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); + $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class ); $this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class ); - $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ); + $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ); $this->wcpay_account->init_hooks(); } @@ -95,6 +95,7 @@ public function tear_down() { unset( $_GET ); unset( $_REQUEST ); parent::tear_down(); + delete_option( '_wcpay_feature_embedded_kyc' ); } public function test_filters_registered_properly() { @@ -268,7 +269,7 @@ public function test_maybe_handle_onboarding_connect_from_known_from( ->disableOriginalConstructor() ->onlyMethods( [ 'redirect_to' ] ) ->getMock(); - $wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $mock_redirect_service ); + $wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $mock_redirect_service ); $_GET['wcpay-connect'] = 'connect-from'; $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' ); @@ -796,6 +797,71 @@ public function test_maybe_handle_onboarding_init_stripe_onboarding() { $this->wcpay_account->maybe_handle_onboarding(); } + public function test_maybe_handle_onboarding_init_embedded_kyc() { + // Arrange. + // We need to be in the WP admin dashboard. + $this->set_is_admin( true ); + // Test as an admin user. + wp_set_current_user( 1 ); + + $_GET['wcpay-connect'] = 'connect-from'; + $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' ); + // Set the request as if the user is on some bogus page. It doesn't matter. + $_GET['page'] = 'wc-admin'; + $_GET['path'] = '/payments/some-bogus-page'; + // We need to come from the onboarding wizard to initialize an account! + $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD; + $_GET['source'] = WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE; + // Make sure important flags are carried over. + $_GET['promo'] = 'incentive_id'; + $_GET['progressive'] = 'true'; + // There is no `test_mode` param and no test mode is set. It should end up as a live mode onboarding. + + // The Jetpack connection is in working order. + $this->mock_jetpack_connection(); + + $this->mock_database_cache + ->expects( $this->any() ) + ->method( 'get_or_add' ) + ->willReturn( [] ); // Empty array means no Stripe account connected. + + // Assert. + $this->mock_redirect_service + ->expects( $this->never() ) + ->method( 'redirect_to_overview_page' ); + $this->mock_redirect_service + ->expects( $this->never() ) + ->method( 'redirect_to_connect_page' ); + $this->mock_redirect_service + ->expects( $this->never() ) + ->method( 'redirect_to_onboarding_wizard' ); + + update_option( '_wcpay_feature_embedded_kyc', '1' ); + + // If embedded KYC is in progress, we expect different URL. + $this->mock_onboarding_service + ->expects( $this->once() ) + ->method( 'is_embedded_kyc_in_progress' ) + ->willReturn( true ); + + $this->mock_api_client + ->expects( $this->never() ) + ->method( 'get_onboarding_data' ); + + $this->mock_redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( + $this->logicalOr( + $this->stringContains( 'page=wc-admin&path=/payments/onboarding/kyc' ), + $this->stringContains( 'page=wc-admin&path=%2Fpayments%2Fonboarding%2Fkyc' ) + ) + ); + + // Act. + $this->wcpay_account->maybe_handle_onboarding(); + } + public function test_maybe_handle_onboarding_init_stripe_onboarding_existing_account() { // Arrange. // We need to be in the WP admin dashboard. diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index 217bb7be513..e9405375d8f 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -30,6 +30,7 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase { '_wcpay_feature_documents' => 'documents', '_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled', '_wcpay_feature_stripe_ece' => 'isStripeEceEnabled', + '_wcpay_feature_embedded_kyc' => 'isEmbeddedKycEnabled', ]; public function set_up() { @@ -300,6 +301,35 @@ public function test_is_frt_review_feature_active_returns_false_when_flag_is_not $this->assertFalse( WC_Payments_Features::is_frt_review_feature_active() ); } + public function test_is_embedded_kyc_enabled_returns_true() { + add_filter( + 'pre_option_' . WC_Payments_Features::EMBEDDED_KYC_FLAG_NAME, + function ( $pre_option, $option, $default ) { + return '1'; + }, + 10, + 3 + ); + $this->assertTrue( WC_Payments_Features::is_embedded_kyc_enabled() ); + } + + public function test_is_embedded_kyc_enabled_returns_false_when_flag_is_false() { + add_filter( + 'pre_option_' . WC_Payments_Features::EMBEDDED_KYC_FLAG_NAME, + function ( $pre_option, $option, $default ) { + return '0'; + }, + 10, + 3 + ); + $this->assertFalse( WC_Payments_Features::is_embedded_kyc_enabled() ); + $this->assertArrayNotHasKey( 'isEmbeddedKycEnabled', WC_Payments_Features::to_array() ); + } + + public function test_is_embedded_kyc_enabled_returns_false_when_flag_is_not_set() { + $this->assertFalse( WC_Payments_Features::is_embedded_kyc_enabled() ); + } + private function setup_enabled_flags( array $enabled_flags ) { foreach ( array_keys( self::FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING ) as $flag ) { add_filter( diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php index 7ec162205e4..a8d9686f763 100644 --- a/tests/unit/test-class-wc-payments-onboarding-service.php +++ b/tests/unit/test-class-wc-payments-onboarding-service.php @@ -34,6 +34,13 @@ class WC_Payments_Onboarding_Service_Test extends WCPAY_UnitTestCase { */ private $mock_database_cache; + /** + * Mock WC_Payments_Session_Service + * + * @var MockObject + */ + private $mock_session_service; + /** * Example business types array. * @@ -129,10 +136,11 @@ class WC_Payments_Onboarding_Service_Test extends WCPAY_UnitTestCase { public function set_up() { parent::set_up(); - $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); - $this->mock_database_cache = $this->createMock( Database_Cache::class ); + $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); + $this->mock_database_cache = $this->createMock( Database_Cache::class ); + $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); - $this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache ); + $this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache, $this->mock_session_service ); $this->onboarding_service->init_hooks(); } @@ -202,6 +210,18 @@ public function test_set_test_mode() { delete_option( WC_Payments_Onboarding_Service::TEST_MODE_OPTION ); } + public function test_is_embedded_kyc_in_progress() { + $this->assertFalse( $this->onboarding_service->is_embedded_kyc_in_progress() ); + + $this->onboarding_service->set_embedded_kyc_in_progress(); + + $this->assertTrue( $this->onboarding_service->is_embedded_kyc_in_progress() ); + + $this->onboarding_service->clear_embedded_kyc_in_progress(); + + $this->assertFalse( $this->onboarding_service->is_embedded_kyc_in_progress() ); + } + /** * @dataProvider data_get_from */ From 2eea958f66a81109d05f029c725788ba0c7dcb42 Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Fri, 6 Sep 2024 20:23:01 +0300 Subject: [PATCH 38/44] Fix and improve admin child pages redirections logic (#9400) Co-authored-by: Daniel Mallory --- ...-account-data-force-refresh-on-wcpay-pages | 4 ++ includes/admin/class-wc-payments-admin.php | 51 ++++++++++++------- includes/class-wc-payments-account.php | 11 ++-- .../admin/test-class-wc-payments-admin.php | 36 ++++++++++--- 4 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 changelog/fix-9395-account-data-force-refresh-on-wcpay-pages diff --git a/changelog/fix-9395-account-data-force-refresh-on-wcpay-pages b/changelog/fix-9395-account-data-force-refresh-on-wcpay-pages new file mode 100644 index 00000000000..9535c429acb --- /dev/null +++ b/changelog/fix-9395-account-data-force-refresh-on-wcpay-pages @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Avoid unnecessary account data cache refresh on WooPayments pages refresh. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 85f3439ba85..de577e4bcad 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -202,7 +202,8 @@ public function init_hooks() { // Add menu items. add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 ); - add_action( 'admin_init', [ $this, 'maybe_redirect_from_payments_admin_child_pages' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. + // Run this after the redirects in WC_Payments_Account. + add_action( 'admin_init', [ $this, 'maybe_redirect_from_payments_admin_child_pages' ], 16 ); add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 ); add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] ); @@ -1120,44 +1121,56 @@ private function get_settings_menu_item_name() { } /** - * If the user is attempting to view a WCPay admin page without a connected Stripe account, - * redirect them to the connect account page. + * Redirects WCPay admin pages to the Connect page for stores that + * don't have a working Jetpack connection or a valid connected Stripe account. + * + * Please note that the overview page is handled separately in the + * `WC_Payments_Account::maybe_redirect_from_overview_page` method, before this method is called (priority 15 vs 16). + * + * IMPORTANT: The logic should be kept in sync with the one in maybe_redirect_from_connect_page to avoid loops. + * + * @see WC_Payments_Account::maybe_redirect_from_overview_page() for overview page handling. + * @see WC_Payments_Account::maybe_handle_onboarding() for connect links handling. + * + * @return bool True if a redirection happened, false otherwise. */ - public function maybe_redirect_from_payments_admin_child_pages() { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return; - } - if ( wp_doing_ajax() ) { - return; + public function maybe_redirect_from_payments_admin_child_pages(): bool { + if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { + return false; } $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification - if ( empty( $url_params['page'] ) || 'wc-admin' !== $url_params['page'] ) { - return; + return false; } $current_path = ! empty( $url_params['path'] ) ? $url_params['path'] : ''; - if ( empty( $current_path ) ) { - return; + return false; } + // If the current path doesn't match any of the paths we're interested in, do not redirect. $page_paths = []; - foreach ( $this->admin_child_pages as $payments_child_page ) { $page_paths[] = preg_quote( $payments_child_page['path'], '/' ); } - if ( ! preg_match( '/^(' . implode( '|', $page_paths ) . ')/', $current_path ) ) { - return; + return false; } - if ( $this->account->is_stripe_connected( true, true ) ) { - return; + // If everything is NOT in good working condition, redirect to Payments Connect page. + if ( ! $this->account->has_working_jetpack_connection() || ! $this->account->is_stripe_account_valid() ) { + $this->account->redirect_to_onboarding_welcome_page( + sprintf( + /* translators: 1: WooPayments. */ + __( 'Please complete your %1$s setup to continue using it.', 'woocommerce-payments' ), + 'WooPayments' + ) + ); + return true; } - $this->account->redirect_to_onboarding_welcome_page(); + return false; } /** diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 3f8fbd60789..928b8657fc4 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -199,13 +199,12 @@ public function has_account_data(): bool { * Checks if the account is connected, assumes the value of $on_error on server error. * * @param bool $on_error Value to return on server error, defaults to false. - * @param bool $force_refresh Force refresh account data cache. * * @return bool True if the account is connected, false otherwise, $on_error on error. */ - public function is_stripe_connected( bool $on_error = false, bool $force_refresh = false ): bool { + public function is_stripe_connected( bool $on_error = false ): bool { try { - return $this->try_is_stripe_connected( $force_refresh ); + return $this->try_is_stripe_connected(); } catch ( Exception $e ) { return $on_error; } @@ -214,13 +213,11 @@ public function is_stripe_connected( bool $on_error = false, bool $force_refresh /** * Checks if the account is connected, throws on server error. * - * @param bool $force_refresh Force refresh account data cache. - * * @return bool True if the account is connected, false otherwise. * @throws Exception Throws exception when unable to detect connection status. */ - public function try_is_stripe_connected( bool $force_refresh = false ): bool { - $account = $this->get_cached_account_data( $force_refresh ); + public function try_is_stripe_connected(): bool { + $account = $this->get_cached_account_data(); if ( false === $account ) { throw new Exception( esc_html__( 'Failed to detect connection status', 'woocommerce-payments' ) ); } diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 2000b18e0e2..25612ab0f9a 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -201,13 +201,17 @@ private function mock_current_user_is_admin() { /** * @dataProvider data_maybe_redirect_from_payments_admin_child_pages */ - public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $is_stripe_connected, $get_params ) { + public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $has_working_jetpack_connection, $is_stripe_account_valid, $get_params ) { $this->mock_current_user_is_admin(); $_GET = $get_params; $this->mock_account - ->method( 'is_stripe_connected' ) - ->willReturn( $is_stripe_connected ); + ->method( 'has_working_jetpack_connection' ) + ->willReturn( $has_working_jetpack_connection ); + + $this->mock_account + ->method( 'is_stripe_account_valid' ) + ->willReturn( $is_stripe_account_valid ); $this->mock_account ->expects( $this->exactly( $expected_times_redirect_called ) ) @@ -224,11 +228,13 @@ public function data_maybe_redirect_from_payments_admin_child_pages() { 'no_get_params' => [ 0, false, + false, [], ], 'empty_page_param' => [ 0, false, + false, [ 'path' => '/payments/overview', ], @@ -236,6 +242,7 @@ public function data_maybe_redirect_from_payments_admin_child_pages() { 'incorrect_page_param' => [ 0, false, + false, [ 'page' => 'wc-settings', 'path' => '/payments/overview', @@ -244,6 +251,7 @@ public function data_maybe_redirect_from_payments_admin_child_pages() { 'empty_path_param' => [ 0, false, + false, [ 'page' => 'wc-admin', ], @@ -251,25 +259,37 @@ public function data_maybe_redirect_from_payments_admin_child_pages() { 'incorrect_path_param' => [ 0, false, + false, [ 'page' => 'wc-admin', 'path' => '/payments/does-not-exist', ], ], - 'stripe_connected' => [ - 0, + 'working Jetpack connection - invalid Stripe account' => [ + 1, true, + false, [ 'page' => 'wc-admin', - 'path' => '/payments/overview', + 'path' => '/payments/deposits', ], ], - 'happy_path' => [ + 'not working Jetpack connection - valid Stripe account' => [ 1, false, + true, [ 'page' => 'wc-admin', - 'path' => '/payments/overview', + 'path' => '/payments/deposits', + ], + ], + 'working Jetpack connection - valid Stripe account' => [ + 0, + true, + true, + [ + 'page' => 'wc-admin', + 'path' => '/payments/transactions', ], ], ]; From f87ea8e82b3b26e64b8493867f64a8f9dccc2174 Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:27:05 +0300 Subject: [PATCH 39/44] Dev: Move Klarna E2E tests to Playwright (#9377) --- changelog/dev-update-klarna-e2e-tests | 4 + .../shopper/klarna-checkout-purchase.spec.ts | 80 +++++++++ tests/e2e-pw/utils/merchant.ts | 44 ++++- tests/e2e-pw/utils/shopper-navigation.ts | 4 + tests/e2e-pw/utils/shopper.ts | 21 ++- tests/e2e/config/jest.setup.js | 3 +- .../shopper/shopper-klarna-checkout.spec.js | 165 ------------------ 7 files changed, 145 insertions(+), 176 deletions(-) create mode 100644 changelog/dev-update-klarna-e2e-tests create mode 100644 tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts delete mode 100644 tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js diff --git a/changelog/dev-update-klarna-e2e-tests b/changelog/dev-update-klarna-e2e-tests new file mode 100644 index 00000000000..f261610446d --- /dev/null +++ b/changelog/dev-update-klarna-e2e-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate Klarna E2E tests to playwright. Reduce noise in E2E tests console output. diff --git a/tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts b/tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts new file mode 100644 index 00000000000..b810f32ecb0 --- /dev/null +++ b/tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { test, expect, Page } from '@playwright/test'; + +/** + * Internal dependencies + */ +import * as shopper from '../../utils/shopper'; +import { getMerchant, getShopper } from '../../utils/helpers'; +import * as merchant from '../../utils/merchant'; +import { config } from '../../config/default'; +import { + goToProductPageBySlug, + goToShop, +} from '../../utils/shopper-navigation'; + +test.describe( 'Klarna Checkout', () => { + let merchantPage: Page; + let shopperPage: Page; + let wasMulticurrencyEnabled: boolean; + + test.beforeAll( async ( { browser } ) => { + shopperPage = ( await getShopper( browser ) ).shopperPage; + merchantPage = ( await getMerchant( browser ) ).merchantPage; + wasMulticurrencyEnabled = await merchant.isMulticurrencyEnabled( + merchantPage + ); + if ( wasMulticurrencyEnabled ) { + await merchant.deactivateMulticurrency( merchantPage ); + } + await merchant.enablePaymentMethods( merchantPage, [ 'klarna' ] ); + } ); + + test.afterAll( async () => { + await shopper.emptyCart( shopperPage ); + + await merchant.disablePaymentMethods( merchantPage, [ 'klarna' ] ); + + if ( wasMulticurrencyEnabled ) { + await merchant.activateMulticurrency( merchantPage ); + } + } ); + + test( 'shows the message in the product page', async () => { + await goToProductPageBySlug( shopperPage, 'belt' ); + + // Since we cant' control the exact contents of the iframe, we just make sure it's there. + await expect( + shopperPage + .frameLocator( '#payment-method-message iframe' ) + .locator( 'body' ) + ).not.toBeEmpty(); + } ); + + test( 'allows to use Klarna as a payment method', async () => { + await goToShop( shopperPage ); + await shopper.setupProductCheckout( shopperPage, [ [ 'Belt', 1 ] ], { + ...config.addresses.customer.billing, + // these are Klarna-specific values: + // https://docs.klarna.com/resources/test-environment/sample-customer-data/#united-states-of-america + email: 'customer@email.us', + phone: '+13106683312', + firstname: 'Test', + lastname: 'Person-us', + } ); + + await shopperPage + .locator( '.wc_payment_methods' ) + .getByText( 'Klarna' ) + .click(); + + await shopper.placeOrder( shopperPage ); + + // Since we don't have control over the html in the Klarna playground page, + // verifying the redirect is all we can do consistently without introducing a + // flaky test. + await expect( shopperPage ).toHaveURL( /.*klarna\.com/ ); + } ); +} ); diff --git a/tests/e2e-pw/utils/merchant.ts b/tests/e2e-pw/utils/merchant.ts index c70045e9a0d..2cbefa58615 100644 --- a/tests/e2e-pw/utils/merchant.ts +++ b/tests/e2e-pw/utils/merchant.ts @@ -21,13 +21,20 @@ export const saveWooPaymentsSettings = async ( page: Page ) => { } ); }; +export const isMulticurrencyEnabled = async ( page: Page ) => { + await navigation.goToWooPaymentsSettings( page ); + + const checkboxTestId = 'multi-currency-toggle'; + const isEnabled = await page.getByTestId( checkboxTestId ).isChecked(); + + return isEnabled; +}; + export const activateMulticurrency = async ( page: Page ) => { await navigation.goToWooPaymentsSettings( page ); const checkboxTestId = 'multi-currency-toggle'; - const wasInitiallyEnabled = await page - .getByTestId( checkboxTestId ) - .isChecked(); + const wasInitiallyEnabled = await isMulticurrencyEnabled( page ); if ( ! wasInitiallyEnabled ) { await page.getByTestId( checkboxTestId ).check(); @@ -212,3 +219,34 @@ export const setCurrencyCharmPricing = async ( await page.getByTestId( 'price_charm' ).selectOption( charmPricing ); await saveWooPaymentsSettings( page ); }; + +export const enablePaymentMethods = async ( + page: Page, + paymentMethods: string[] +) => { + await navigation.goToWooPaymentsSettings( page ); + + for ( const paymentMethodName of paymentMethods ) { + await page.getByLabel( paymentMethodName ).check(); + } + + await saveWooPaymentsSettings( page ); +}; + +export const disablePaymentMethods = async ( + page: Page, + paymentMethods: string[] +) => { + await navigation.goToWooPaymentsSettings( page ); + + for ( const paymentMethodName of paymentMethods ) { + const checkbox = await page.getByLabel( paymentMethodName ); + + if ( await checkbox.isChecked() ) { + await checkbox.click(); + await page.getByRole( 'button', { name: 'Remove' } ).click(); + } + } + + await saveWooPaymentsSettings( page ); +}; diff --git a/tests/e2e-pw/utils/shopper-navigation.ts b/tests/e2e-pw/utils/shopper-navigation.ts index 04c5f3d7cc5..7f96a1e9055 100644 --- a/tests/e2e-pw/utils/shopper-navigation.ts +++ b/tests/e2e-pw/utils/shopper-navigation.ts @@ -7,6 +7,10 @@ import { Page } from 'playwright/test'; */ import { isUIUnblocked } from './shopper'; +export const goToShop = async ( page: Page ) => { + await page.goto( `/shop/`, { waitUntil: 'load' } ); +}; + export const goToShopWithCurrency = async ( page: Page, currency: string ) => { await page.goto( `/shop/?currency=${ currency }`, { waitUntil: 'load' } ); }; diff --git a/tests/e2e-pw/utils/shopper.ts b/tests/e2e-pw/utils/shopper.ts index 3acff9d24c9..b39869b8426 100644 --- a/tests/e2e-pw/utils/shopper.ts +++ b/tests/e2e-pw/utils/shopper.ts @@ -37,8 +37,17 @@ export const fillBillingAddress = async ( await page.locator( '#billing_email' ).fill( billingAddress.email ); }; +// This is currently the source of some flaky tests since sometimes the form is not submitted +// after the first click, so we retry until the ui is blocked. export const placeOrder = async ( page: Page ) => { - await page.locator( '#place_order' ).click(); + let orderPlaced = false; + while ( ! orderPlaced ) { + await page.locator( '#place_order' ).click(); + + if ( await page.$( '.blockUI' ) ) { + orderPlaced = true; + } + } }; export const addCartProduct = async ( @@ -192,19 +201,19 @@ export const setupCheckout = async ( /** * Sets up checkout with any number of products. * - * @param {CustomerAddress} billingAddress The billing address to use for the checkout. * @param {Array<[string, number]>} lineItems A 2D array of line items where each line item is an array * that contains the product title as the first element, and the quantity as the second. * For example, if you want to checkout x2 "Hoodie" and x3 "Belt" then set this parameter like this: * * `[ [ "Hoodie", 2 ], [ "Belt", 3 ] ]`. + * @param {CustomerAddress} billingAddress The billing address to use for the checkout. */ export async function setupProductCheckout( page: Page, - billingAddress: CustomerAddress, lineItems: Array< [ string, number ] > = [ [ config.products.simple.name, 1 ], - ] + ], + billingAddress: CustomerAddress = config.addresses.customer.billing ) { const cartSizeText = await page .locator( '.cart-contents .count' ) @@ -241,9 +250,7 @@ export const placeOrderWithCurrency = async ( currency: string ) => { await navigation.goToShopWithCurrency( page, currency ); - await setupProductCheckout( page, config.addresses.customer.billing, [ - [ config.products.simple.name, 1 ], - ] ); + await setupProductCheckout( page, [ [ config.products.simple.name, 1 ] ] ); await fillCardDetails( page, config.cards.basic ); // Takes off the focus out of the Stripe elements to let Stripe logic // wrap up and make sure the Place Order button is clickable. diff --git a/tests/e2e/config/jest.setup.js b/tests/e2e/config/jest.setup.js index 9caf9592668..791c22a46ba 100644 --- a/tests/e2e/config/jest.setup.js +++ b/tests/e2e/config/jest.setup.js @@ -36,7 +36,8 @@ const ERROR_MESSAGES_TO_IGNORE = [ 'Failed to load resource: the server responded with a status of 400 (Bad Request)', 'No Amplitude API key provided', 'is registered with an invalid category', - '"Heading" is not a supported class', + 'is not a supported class', // Silence stripe.js warnings regarding styles. + 'is not a supported property for', // Silence stripe.js warnings regarding styles. ]; ERROR_MESSAGES_TO_IGNORE.forEach( ( errorMessage ) => { diff --git a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js deleted file mode 100644 index 6bf22fd4f0a..00000000000 --- a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * External dependencies - */ -const { shopper, merchant } = require( '@woocommerce/e2e-utils' ); -import config from 'config'; -import { uiUnblocked } from '@woocommerce/e2e-utils/build/page-utils'; -/** - * Internal dependencies - */ -import { merchantWCP, shopperWCP } from '../../../utils'; -import { setupProductCheckout } from '../../../utils/payments'; - -const UPE_METHOD_CHECKBOXES = [ - "//label[contains(text(), 'Klarna')]/preceding-sibling::span/input[@type='checkbox']", -]; - -describe( 'Klarna checkout', () => { - let wasMulticurrencyEnabled; - beforeAll( async () => { - await merchant.login(); - wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency(); - await merchantWCP.deactivateMulticurrency(); - await merchantWCP.enablePaymentMethod( UPE_METHOD_CHECKBOXES ); - await merchant.logout(); - await shopper.login(); - } ); - - afterAll( async () => { - await shopperWCP.emptyCart(); - await shopperWCP.logout(); - await merchant.login(); - if ( wasMulticurrencyEnabled ) { - await merchantWCP.activateMulticurrency(); - } - await merchantWCP.disablePaymentMethod( UPE_METHOD_CHECKBOXES ); - await merchant.logout(); - } ); - - it( 'should show the product messaging on the product page', async () => { - await shopperWCP.goToProductPageBySlug( 'belt' ); - - // waiting for the "product messaging" component to be rendered, so we can click on it. - const paymentMethodMessageFrameHandle = await page.waitForSelector( - '#payment-method-message iframe' - ); - const paymentMethodMessageIframe = await paymentMethodMessageFrameHandle.contentFrame(); - - // Click on Klarna link to open the modal. - const learnMoreLink = await paymentMethodMessageIframe.waitForSelector( - 'a[aria-haspopup="dialog"]' - ); - await learnMoreLink.click(); - - // Wait for the iframe to be added by Stripe JS after clicking on the element. - await page.waitForTimeout( 1000 ); - - const paymentMethodMessageModalIframeHandle = await page.waitForSelector( - 'iframe[src*="js.stripe.com/v3/elements-inner-payment-method-messaging-modal"]' - ); - const paymentMethodMessageModalIframe = await paymentMethodMessageModalIframeHandle.contentFrame(); - - await expect( paymentMethodMessageModalIframe ).toMatchElement( - '[data-testid="ModalHeader"] > p', - { - text: 'Buy Now. Pay Later.', - } - ); - - await expect( paymentMethodMessageModalIframe ).toMatchElement( - '[data-testid="ModalDescription"] > p', - { - text: - 'Select Klarna as your payment method at checkout to pay in installments.', - } - ); - } ); - - it( `should successfully place an order with Klarna`, async () => { - await setupProductCheckout( - { - ...config.get( 'addresses.customer.billing' ), - // these are Klarna-specific values: - // https://docs.klarna.com/resources/test-environment/sample-customer-data/#united-states-of-america - email: 'customer@email.us', - phone: '+13106683312', - firstname: 'Test', - lastname: 'Person-us', - }, - [ [ 'Beanie', 3 ] ] - ); - - await uiUnblocked(); - - await page.evaluate( async () => { - const paymentMethodLabel = document.querySelector( - 'label[for="payment_method_woocommerce_payments_klarna"]' - ); - if ( paymentMethodLabel ) { - paymentMethodLabel.click(); - } - } ); - - await shopper.placeOrder(); - - await page.waitForSelector( '#phone' ); - - await page.waitForTimeout( 2000 ); - - await page - .waitForSelector( '#onContinue' ) - .then( ( button ) => button.click() ); - - // This is where the OTP code is entered. - await page.waitForSelector( '#phoneOtp' ); - - await page.waitForTimeout( 2000 ); // Wait for animations - - await expect( page ).toFill( 'input#otp_field', '123456' ); - - await page.waitForSelector( '[role="heading"]', { - visible: true, - text: /(Confirm and pay)|(Choose how to pay)/i, - } ); - - let readyToBuy; - try { - await page.waitForSelector( 'button#buy_button', { - visible: true, - timeout: 10000, - } ); - readyToBuy = true; - } catch ( err ) { - readyToBuy = false; - } - - if ( ! readyToBuy ) { - // Select Payment Plan - 4 weeks & click continue. - await page - .waitForSelector( - '[id="payinparts_kp.0-ui"] input[type="radio"]' - ) - .then( ( radio ) => radio.click() ); - - await page.waitForTimeout( 2000 ); - - await expect( page ).toClick( 'button', { - text: 'Continue', - } ); - } - - // Confirm payment. - await page.waitForSelector( 'button#buy_button' ); - await page.waitForTimeout( 2000 ); // We need to wait a bit for the button to become clickable. - await expect( page ).toClick( 'button#buy_button' ); - - // Wait for the order confirmation page to load. - await page.waitForNavigation( { - waitUntil: 'networkidle0', - } ); - - await page.waitForSelector( 'h1.entry-title' ); - - await expect( page ).toMatchTextContent( 'Order received' ); - } ); -} ); From f34c48c777d8abf399f9e6f83641a02fb4b819e6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 6 Sep 2024 13:42:09 -0500 Subject: [PATCH 40/44] Check payment method is available before rendering it (#9378) Co-authored-by: Brian Borman <68524302+bborman22@users.noreply.github.com> --- .../as-fix-empty-express-checkout-element | 4 + .../components/express-checkout-component.js | 11 +-- .../blocks/express-checkout-element.scss | 7 ++ client/express-checkout/blocks/index.js | 51 ++++++------ .../utils/checkPaymentMethodIsAvailable.js | 78 +++++++++++++++++++ ...xpress-checkout-button-display-handler.php | 29 +++++++ 6 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 changelog/as-fix-empty-express-checkout-element create mode 100644 client/express-checkout/utils/checkPaymentMethodIsAvailable.js diff --git a/changelog/as-fix-empty-express-checkout-element b/changelog/as-fix-empty-express-checkout-element new file mode 100644 index 00000000000..18b5e8de0b6 --- /dev/null +++ b/changelog/as-fix-empty-express-checkout-element @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Check payment method is available before rendering it. diff --git a/client/express-checkout/blocks/components/express-checkout-component.js b/client/express-checkout/blocks/components/express-checkout-component.js index 3a996220167..572e88a8e9e 100644 --- a/client/express-checkout/blocks/components/express-checkout-component.js +++ b/client/express-checkout/blocks/components/express-checkout-component.js @@ -40,11 +40,7 @@ const adjustButtonHeights = ( buttonOptions, expressPaymentMethod ) => { // Apple Pay has a nearly imperceptible height difference. We increase it by 1px here. if ( buttonOptions.buttonTheme.applePay === 'black' ) { if ( expressPaymentMethod === 'applePay' ) { - // The maximum allowed size is 55px. - buttonOptions.buttonHeight = Math.min( - buttonOptions.buttonHeight + 0.4, - 55 - ); + buttonOptions.buttonHeight = buttonOptions.buttonHeight + 0.4; } } @@ -56,6 +52,11 @@ const adjustButtonHeights = ( buttonOptions, expressPaymentMethod ) => { buttonOptions.buttonHeight = buttonOptions.buttonHeight - 2; } + // Clamp the button height to the allowed range 40px to 55px. + buttonOptions.buttonHeight = Math.max( + 40, + Math.min( buttonOptions.buttonHeight, 55 ) + ); return buttonOptions; }; diff --git a/client/express-checkout/blocks/express-checkout-element.scss b/client/express-checkout/blocks/express-checkout-element.scss index 06ae725dfbc..4b4e3d5e29e 100644 --- a/client/express-checkout/blocks/express-checkout-element.scss +++ b/client/express-checkout/blocks/express-checkout-element.scss @@ -14,3 +14,10 @@ margin: 24px 0 !important; height: 20px; } + +.wc-block-components-express-payment + .wc-block-components-express-payment__event-buttons + > li { + margin-left: 1px !important; + width: 99% !important; +} diff --git a/client/express-checkout/blocks/index.js b/client/express-checkout/blocks/index.js index 30dc6853111..b77bbb1fd69 100644 --- a/client/express-checkout/blocks/index.js +++ b/client/express-checkout/blocks/index.js @@ -6,6 +6,7 @@ import { getConfig } from 'wcpay/utils/checkout'; import ApplePayPreview from './components/apple-pay-preview'; import ExpressCheckoutContainer from './components/express-checkout-container'; import GooglePayPreview from './components/google-pay-preview'; +import { checkPaymentMethodIsAvailable } from '../utils/checkPaymentMethodIsAvailable'; const expressCheckoutElementApplePay = ( api ) => ( { paymentMethodId: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT, @@ -17,35 +18,41 @@ const expressCheckoutElementApplePay = ( api ) => ( { supports: { features: getConfig( 'features' ), }, - canMakePayment: () => { + canMakePayment: ( { cart } ) => { if ( typeof wcpayExpressCheckoutParams === 'undefined' ) { return false; } - return true; + return new Promise( ( resolve ) => { + checkPaymentMethodIsAvailable( 'applePay', cart, resolve ); + } ); }, } ); -const expressCheckoutElementGooglePay = ( api ) => ( { - paymentMethodId: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT, - name: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT + '_googlePay', - content: ( - - ), - edit: , - supports: { - features: getConfig( 'features' ), - }, - canMakePayment: () => { - if ( typeof wcpayExpressCheckoutParams === 'undefined' ) { - return false; - } +const expressCheckoutElementGooglePay = ( api ) => { + return { + paymentMethodId: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT, + name: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT + '_googlePay', + content: ( + + ), + edit: , + supports: { + features: getConfig( 'features' ), + }, + canMakePayment: ( { cart } ) => { + if ( typeof wcpayExpressCheckoutParams === 'undefined' ) { + return false; + } - return true; - }, -} ); + return new Promise( ( resolve ) => { + checkPaymentMethodIsAvailable( 'googlePay', cart, resolve ); + } ); + }, + }; +}; export { expressCheckoutElementApplePay, expressCheckoutElementGooglePay }; diff --git a/client/express-checkout/utils/checkPaymentMethodIsAvailable.js b/client/express-checkout/utils/checkPaymentMethodIsAvailable.js new file mode 100644 index 00000000000..75296840a9b --- /dev/null +++ b/client/express-checkout/utils/checkPaymentMethodIsAvailable.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import ReactDOM from 'react-dom'; +import { ExpressCheckoutElement, Elements } from '@stripe/react-stripe-js'; +import { memoize } from 'lodash'; + +/** + * Internal dependencies + */ +import { isLinkEnabled } from 'wcpay/checkout/utils/upe'; +import request from 'wcpay/checkout/utils/request'; +import WCPayAPI from 'wcpay/checkout/api'; +import { getUPEConfig } from 'wcpay/utils/checkout'; + +export const checkPaymentMethodIsAvailable = memoize( + ( paymentMethod, cart, resolve ) => { + const root = ReactDOM.createRoot( + document.getElementById( + `express-checkout-check-availability-container-${ paymentMethod }` + ) + ); + + const api = new WCPayAPI( + { + publishableKey: getUPEConfig( 'publishableKey' ), + accountId: getUPEConfig( 'accountId' ), + forceNetworkSavedCards: getUPEConfig( + 'forceNetworkSavedCards' + ), + locale: getUPEConfig( 'locale' ), + isStripeLinkEnabled: isLinkEnabled( + getUPEConfig( 'paymentMethodsConfig' ) + ), + }, + request + ); + + root.render( + + { + let canMakePayment = false; + if ( event.availablePaymentMethods ) { + canMakePayment = + event.availablePaymentMethods[ paymentMethod ]; + } + resolve( canMakePayment ); + root.unmount(); + } } + /> + + ); + } +); diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php index a76f60ec1fb..fd7ba4eae92 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php @@ -95,6 +95,10 @@ public function init() { $is_woopay_enabled = WC_Payments_Features::is_woopay_enabled(); $is_payment_request_enabled = 'yes' === $this->gateway->get_option( 'payment_request' ); + if ( $is_payment_request_enabled ) { + $this->add_html_container_for_test_express_checkout_buttons(); + } + if ( $is_woopay_enabled || $is_payment_request_enabled ) { add_action( 'wc_ajax_wcpay_add_to_cart', [ $this->express_checkout_ajax_handler, 'ajax_add_to_cart' ] ); add_action( 'wc_ajax_wcpay_empty_cart', [ $this->express_checkout_ajax_handler, 'ajax_empty_cart' ] ); @@ -174,6 +178,31 @@ public function add_order_attribution_inputs() { echo ''; } + + /** + * Add HTML containers to be used by the Express Checkout buttons that check if the payment method is available. + * + * @return void + */ + private function add_html_container_for_test_express_checkout_buttons() { + add_filter( + 'the_content', + function ( $content ) { + $supported_payment_methods = [ 'applePay' , 'googlePay' ]; + // Restrict adding these HTML containers to only the necessary pages. + if ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) { + foreach ( $supported_payment_methods as $value ) { + // The inline styles ensure that the HTML elements don't occupy space on the page. + $content = '
' . $content; + } + } + return $content; + }, + 10, + 1 + ); + } + /** * Check if the pay-for-order flow is supported. * From 9e95662a5118e0dac2f6c38e2e71adbd8719afbd Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Sat, 7 Sep 2024 00:57:35 +0300 Subject: [PATCH 41/44] Add ExPlat experiment: Payments task onboarding flow skips Connect page (#9383) --- ...ts-task-onboarding-flow-skips-connect-page | 5 ++ client/onboarding/index.tsx | 5 +- client/onboarding/step.tsx | 6 +- client/onboarding/steps/embedded-kyc.tsx | 2 +- client/onboarding/steps/loading.tsx | 20 +++---- client/onboarding/tracking.ts | 40 +++++++------ client/utils/index.js | 12 ++++ includes/class-wc-payments-account.php | 58 +++++++++++++++---- .../class-wc-payments-redirect-service.php | 15 ++--- includes/class-wc-payments-utils.php | 19 ++++++ tests/unit/test-class-wc-payments-account.php | 11 +++- 11 files changed, 136 insertions(+), 57 deletions(-) create mode 100644 changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page diff --git a/changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page b/changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page new file mode 100644 index 00000000000..a95d596ef55 --- /dev/null +++ b/changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Add ExPlat experiment for Payments task originated onboarding flows. + + diff --git a/client/onboarding/index.tsx b/client/onboarding/index.tsx index 62e179bb7c2..1dbb237f7cd 100644 --- a/client/onboarding/index.tsx +++ b/client/onboarding/index.tsx @@ -85,10 +85,7 @@ const initialData = { const OnboardingPage: React.FC = () => { useEffect( () => { - const urlParams = new URLSearchParams( window.location.search ); - const source = - urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown'; - trackStarted( source ); + trackStarted(); // Remove loading class and add those required for full screen. document.body.classList.remove( 'woocommerce-admin-is-loading' ); diff --git a/client/onboarding/step.tsx b/client/onboarding/step.tsx index 92ed2e4e3d4..e827a06c107 100644 --- a/client/onboarding/step.tsx +++ b/client/onboarding/step.tsx @@ -24,11 +24,7 @@ const Step: React.FC< Props > = ( { name, children, showHeading = true } ) => { const { trackAbandoned } = useTrackAbandoned(); const { prevStep, exit } = useStepperContext(); const handleExit = () => { - const urlParams = new URLSearchParams( window.location.search ); - const source = - urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown'; - - trackAbandoned( 'exit', source ); + trackAbandoned( 'exit' ); exit(); }; diff --git a/client/onboarding/steps/embedded-kyc.tsx b/client/onboarding/steps/embedded-kyc.tsx index 1ca2c9c6a92..68981261efd 100644 --- a/client/onboarding/steps/embedded-kyc.tsx +++ b/client/onboarding/steps/embedded-kyc.tsx @@ -132,7 +132,7 @@ const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => { setLoading( false ); setLoadErrorMessage( __( - "Failed to create account session. Please check that you're using the latest version of WooCommerce Payments.", + "Failed to create account session. Please check that you're using the latest version of WooPayments.", 'woocommerce-payments' ) ); diff --git a/client/onboarding/steps/loading.tsx b/client/onboarding/steps/loading.tsx index 297bf5fb799..c7b08dc905b 100644 --- a/client/onboarding/steps/loading.tsx +++ b/client/onboarding/steps/loading.tsx @@ -57,26 +57,26 @@ const LoadingStep: React.FC< Props > = () => { const handleComplete = async () => { const { connectUrl } = wcpaySettings; - const urlParams = new URLSearchParams( window.location.search ); - const urlSource = - urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown'; - - let isEligible; + let isPoEligible; try { - isEligible = await isEligibleForPo(); + isPoEligible = await isEligibleForPo(); } catch ( error ) { // fall back to full KYC scenario. // TODO maybe log these errors in future, e.g. with tracks. - isEligible = false; + isPoEligible = false; } - trackRedirected( isEligible, urlSource ); + trackRedirected( isPoEligible ); removeTrackListener(); + const urlParams = new URLSearchParams( window.location.search ); + window.location.href = addQueryArgs( connectUrl, { self_assessment: fromDotNotation( data ), - progressive: isEligible, - source: urlSource, + progressive: isPoEligible, + source: + urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || + 'unknown', from: 'WCPAY_ONBOARDING_WIZARD', } ); }; diff --git a/client/onboarding/tracking.ts b/client/onboarding/tracking.ts index c46997b0b36..e4d9ec557f7 100644 --- a/client/onboarding/tracking.ts +++ b/client/onboarding/tracking.ts @@ -23,11 +23,15 @@ const stepElapsed = () => { return result; }; -export const trackStarted = ( source: string ): void => { +export const trackStarted = (): void => { + // Initialize the elapsed time tracking startTime = stepStartTime = Date.now(); + const urlParams = new URLSearchParams( window.location.search ); + recordEvent( 'wcpay_onboarding_flow_started', { - source, + source: + urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown', } ); }; @@ -42,14 +46,14 @@ export const trackStepCompleted = ( step: string ): void => { trackedSteps.add( step ); }; -export const trackRedirected = ( - isEligible: boolean, - source: string -): void => { +export const trackRedirected = ( isPoEligible: boolean ): void => { + const urlParams = new URLSearchParams( window.location.search ); + recordEvent( 'wcpay_onboarding_flow_redirected', { - is_po_eligible: isEligible, + is_po_eligible: isPoEligible, elapsed: elapsed( startTime ), - source, + source: + urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown', } ); }; @@ -66,13 +70,13 @@ export const trackEligibilityModalClosed = ( } ); export const useTrackAbandoned = (): { - trackAbandoned: ( method: 'hide' | 'exit', source: string ) => void; + trackAbandoned: ( method: 'hide' | 'exit' ) => void; removeTrackListener: () => void; } => { const { errors, touched } = useOnboardingContext(); const { currentStep: step } = useStepperContext(); - const trackEvent = ( method = 'hide', source = 'unknown' ) => { + const trackEvent = ( method = 'hide' ) => { const event = method === 'hide' ? 'wcpay_onboarding_flow_hidden' @@ -81,21 +85,21 @@ export const useTrackAbandoned = (): { ( field ) => touched[ field as keyof OnboardingFields ] ); + const urlParams = new URLSearchParams( window.location.search ); + recordEvent( event, { step, errored, elapsed: elapsed( startTime ), - source, + source: + urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || + 'unknown', } ); }; const listener = () => { if ( document.visibilityState === 'hidden' ) { - const urlParams = new URLSearchParams( window.location.search ); - const source = - urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || - 'unknown'; - trackEvent( 'hide', source ); + trackEvent( 'hide' ); } }; @@ -108,8 +112,8 @@ export const useTrackAbandoned = (): { }, [ step, errors, touched ] ); return { - trackAbandoned: ( method: string, source = 'unknown' ) => { - trackEvent( method, source ); + trackAbandoned: ( method: string ) => { + trackEvent( method ); document.removeEventListener( 'visibilitychange', listener ); }, removeTrackListener: () => diff --git a/client/utils/index.js b/client/utils/index.js index ba5b0f9493a..c642c1034e1 100644 --- a/client/utils/index.js +++ b/client/utils/index.js @@ -310,3 +310,15 @@ export const getExportLanguageOptions = () => { }, ]; }; + +/** + * Given an object, remove all properties with null or undefined values. + * + * @param {Object} obj The object to remove empty properties from. + * @return {Object|any} A new object with all properties with null or undefined values removed. + */ +export const objectRemoveEmptyProperties = ( obj ) => { + return Object.keys( obj ) + .filter( ( k ) => obj[ k ] !== null && obj[ k ] !== undefined ) + .reduce( ( a, k ) => ( { ...a, [ k ]: obj[ k ] } ), {} ); +}; diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 928b8657fc4..2f4afc40155 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -899,8 +899,9 @@ public function maybe_redirect_from_onboarding_wizard_page(): bool { } /** - * Redirects connect page (payments/connect) to the overview page for stores that - * have a working Jetpack connection and a valid Stripe account. + * Maybe redirects the connect page (payments/connect) + * + * We redirect to the overview page for stores that have a working Jetpack connection and a valid Stripe account. * * Note: Connect _page_ links are not the same as connect links. * Connect links are used to start/re-start/continue the onboarding flow and they are independent of @@ -953,6 +954,28 @@ public function maybe_redirect_from_connect_page(): bool { return true; } + // Determine from where the merchant was directed to the Connect page. + $from = WC_Payments_Onboarding_Service::get_from(); + + // If the user came from the core Payments task list item, + // we run an experiment to skip the Connect page + // and go directly to the Jetpack connection flow and/or onboarding wizard. + if ( WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK === $from + && WC_Payments_Utils::is_in_core_payments_task_onboarding_flow_treatment_mode() ) { + + // We use a connect link to allow our logic to determine what comes next: + // the Jetpack connection setup and/or onboarding wizard (MOX). + $this->redirect_service->redirect_to_wcpay_connect( + // The next step should treat the merchant as coming from the Payments task list item, + // not the Connect page. + WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK, + [ + 'source' => WC_Payments_Onboarding_Service::get_source(), + ] + ); + return true; + } + return false; } @@ -1274,7 +1297,9 @@ public function maybe_handle_onboarding() { 'WooPayments' ), WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, - [ 'source' => $onboarding_source ] + [ + 'source' => $onboarding_source, + ] ); return; @@ -1315,11 +1340,18 @@ public function maybe_handle_onboarding() { $from, [ WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS, - WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK, WC_Payments_Onboarding_Service::FROM_STRIPE, ], true ) + /** + * We are running an experiment to skip the Connect page for Payments Task flows. + * Only redirect to the Connect page if the user is not in the experiment's treatment mode. + * + * @see self::maybe_redirect_from_connect_page() + */ + || ( WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK === $from + && ! WC_Payments_Utils::is_in_core_payments_task_onboarding_flow_treatment_mode() ) // This is a weird case, but it is best to handle it. || ( WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD === $from && ! $this->has_working_jetpack_connection() ) ) { @@ -1376,7 +1408,9 @@ public function maybe_handle_onboarding() { /* translators: %s: error message. */ sprintf( __( 'There was a problem connecting your store to WordPress.com: "%s"', 'woocommerce-payments' ), $e->getMessage() ), WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, - [ 'source' => $onboarding_source ] + [ + 'source' => $onboarding_source, + ] ); return; } @@ -1392,7 +1426,9 @@ public function maybe_handle_onboarding() { // When we redirect to the onboarding wizard, we carry over the `from`, if we have it. // This is because there is no interim step between the user clicking the connect link and the onboarding wizard. ! empty( $from ) ? $from : $next_step_from, - [ 'source' => $onboarding_source ] + [ + 'source' => $onboarding_source, + ] ); return; } @@ -1516,7 +1552,9 @@ public function maybe_handle_onboarding() { 'WooPayments' ), null, - [ 'source' => $onboarding_source ] + [ + 'source' => $onboarding_source, + ] ); return; } @@ -1984,8 +2022,8 @@ private function finalize_connection( string $state, string $mode, array $additi update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => false ] ); // Track account connection finish. - $incentive_id = ! empty( $_GET['promo'] ) ? sanitize_text_field( wp_unslash( $_GET['promo'] ) ) : ''; - $event_properties = [ + $incentive_id = ! empty( $_GET['promo'] ) ? sanitize_text_field( wp_unslash( $_GET['promo'] ) ) : ''; + $tracks_props = [ 'incentive' => $incentive_id, 'mode' => 'live' !== $mode ? 'test' : 'live', 'from' => $additional_args['from'] ?? '', @@ -1993,7 +2031,7 @@ private function finalize_connection( string $state, string $mode, array $additi ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED, - $event_properties + $tracks_props ); $params = $additional_args; diff --git a/includes/class-wc-payments-redirect-service.php b/includes/class-wc-payments-redirect-service.php index b3f352eeb80..bc4c3c77a54 100644 --- a/includes/class-wc-payments-redirect-service.php +++ b/includes/class-wc-payments-redirect-service.php @@ -49,22 +49,23 @@ public function redirect_to( string $location ): void { } /** - * Redirects to the wcpay-connect URL, which then redirects to the KYC flow. + * Redirects to a wcpay-connect URL which then handles the next step for the onboarding flow. * - * This URL is used by the KYC reminder email. We can't take the merchant - * directly to the wcpay-connect URL because it's nonced, and the - * nonce will likely be expired by the time the user follows the link. - * That's why we need this middleman instead. + * This is a sure way to ensure that the user is redirected to the correct URL to continue their onboarding. * - * @param string $from Source of the redirect. + * @param string $from Source of the redirect. + * @param array $additional_params Optional. Additional URL params to add to the redirect URL. */ - public function redirect_to_wcpay_connect( string $from = '' ): void { + public function redirect_to_wcpay_connect( string $from = '', array $additional_params = [] ): void { // Take the user to the 'wcpay-connect' URL. // We handle creating and redirecting to the account link there. $params = [ 'wcpay-connect' => '1', '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), ]; + + $params = array_merge( $params, $additional_params ); + if ( '' !== $from ) { $params['from'] = $from; } diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 0b2506d73b6..b140f16fbf4 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -910,6 +910,25 @@ public static function get_last_refund_from_order_id( $order_id ) { return null; } + /** + * Check to see if the current user is in Core Payments task onboarding flow experiment treatment mode. + * + * @return bool + */ + public static function is_in_core_payments_task_onboarding_flow_treatment_mode(): bool { + if ( ! isset( $_COOKIE['tk_ai'] ) ) { + return false; + } + + $abtest = new \WCPay\Experimental_Abtest( + sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ), + 'woocommerce', + 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) + ); + + return 'treatment' === $abtest->get_variation( 'woopayments_core_payments_task_onboarding_flow_2024_v1' ); + } + /** * Helper function to check whether to show default new onboarding flow or as an exception disable it (if specific constant is set) . * diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index fa685451897..7b26455c9a2 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -240,7 +240,9 @@ public function test_maybe_handle_onboarding_return_from_jetpack_connection_with ->with( 'Connection to WordPress.com failed. Please connect to WordPress.com to start using WooPayments.', WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, - [ 'source' => WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE ] + [ + 'source' => WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE, + ] ); // Act. @@ -619,7 +621,12 @@ public function test_maybe_handle_onboarding_test_mode_to_live() { $this->mock_redirect_service ->expects( $this->once() ) ->method( 'redirect_to_onboarding_wizard' ) - ->with( WC_Payments_Onboarding_Service::FROM_TEST_TO_LIVE, [ 'source' => WC_Payments_Onboarding_Service::SOURCE_WCPAY_SETUP_LIVE_PAYMENTS ] ); + ->with( + WC_Payments_Onboarding_Service::FROM_TEST_TO_LIVE, + [ + 'source' => WC_Payments_Onboarding_Service::SOURCE_WCPAY_SETUP_LIVE_PAYMENTS, + ] + ); // Act. $this->wcpay_account->maybe_handle_onboarding(); From a588f89052e94bb22e94ffa1df3d9790f10f8141 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:09:11 +0000 Subject: [PATCH 42/44] Update version and add changelog entries for release 8.2.0 --- changelog.txt | 30 +++++++++++++++++ changelog/9311-fix-default-border-radius | 4 --- ...ts-task-onboarding-flow-skips-connect-page | 5 --- .../as-fix-empty-express-checkout-element | 4 --- changelog/as-fix-max-height-allowed | 4 --- changelog/as-fix-woopay-data-mismatch | 4 --- ...re-disable-custom-checkout-field-detection | 4 --- ...ve-frontend-upe-appearance-theme-constants | 5 --- ...-8750-migrate-mccy-e2e-tests-to-playwright | 4 --- ...-correct-docker-compose-v2-changelog-entry | 5 --- changelog/dev-fix-e2e-scripts | 5 --- changelog/dev-fix-klarna-product-page-e2e | 4 --- changelog/dev-include-connect-js | 4 --- changelog/dev-update-klarna-e2e-tests | 4 --- .../enhancement-upgrade-docker-to-php8.1 | 5 --- changelog/fix-9244-pmme-skeleton | 4 --- changelog/fix-9281-e2e-dispute-save-challenge | 5 --- .../fix-9288-fix-qit-error-escape-output | 3 -- .../fix-9324-test-drive-onboarding-scoll | 4 --- ...-account-data-force-refresh-on-wcpay-pages | 4 --- ...lipboard-navigator-undefined-error-in-http | 4 --- ...heckout-floating-labels-and-test-mode-copy | 4 --- .../fix-ece-shortcode-get-shipping-rates | 4 --- changelog/fix-fix-e2e-errors | 5 --- changelog/fix-move-woopay-theme-checkbox | 4 --- ...ve-apple-pay-admin-notice-for-live-account | 4 --- changelog/fix-saved-cards-e2e | 4 --- changelog/fix-send-blog-timezone-to-woopay | 5 --- changelog/fix-test-instructions-translations | 4 --- ...ix-testing-instructions-dark-theme-support | 4 --- changelog/fix-tooltip-link-contrast-ratio | 5 --- ...defined-platform-global-theme-index-notice | 4 --- changelog/fix-wcpay-tracking-cookie-cache | 4 --- ...fix-woopay-direct-checkout-route-namespace | 4 --- changelog/fix-woopay-email-redirect | 4 --- ...woopay-theme-support-from-classic-checkout | 4 --- changelog/fix-woopay-themeing-on-email-flow | 4 --- .../refactor-payment-method-fees-oneline | 4 --- .../remove-override-for-express-payment-grid | 5 --- changelog/update-e2e-setup-script | 5 --- changelog/update-e2e-setup-script-env-var | 5 --- changelog/update-jetpack-packages | 4 --- changelog/update-required-php-version | 5 --- .../update-test-instructions-icon-animation | 4 --- package-lock.json | 4 +-- package.json | 2 +- readme.txt | 32 ++++++++++++++++++- woocommerce-payments.php | 2 +- 48 files changed, 65 insertions(+), 189 deletions(-) delete mode 100644 changelog/9311-fix-default-border-radius delete mode 100644 changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page delete mode 100644 changelog/as-fix-empty-express-checkout-element delete mode 100644 changelog/as-fix-max-height-allowed delete mode 100644 changelog/as-fix-woopay-data-mismatch delete mode 100644 changelog/chore-disable-custom-checkout-field-detection delete mode 100644 changelog/chore-remove-frontend-upe-appearance-theme-constants delete mode 100644 changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright delete mode 100644 changelog/dev-correct-docker-compose-v2-changelog-entry delete mode 100644 changelog/dev-fix-e2e-scripts delete mode 100644 changelog/dev-fix-klarna-product-page-e2e delete mode 100644 changelog/dev-include-connect-js delete mode 100644 changelog/dev-update-klarna-e2e-tests delete mode 100644 changelog/enhancement-upgrade-docker-to-php8.1 delete mode 100644 changelog/fix-9244-pmme-skeleton delete mode 100644 changelog/fix-9281-e2e-dispute-save-challenge delete mode 100644 changelog/fix-9288-fix-qit-error-escape-output delete mode 100644 changelog/fix-9324-test-drive-onboarding-scoll delete mode 100644 changelog/fix-9395-account-data-force-refresh-on-wcpay-pages delete mode 100644 changelog/fix-catch-clipboard-navigator-undefined-error-in-http delete mode 100644 changelog/fix-checkout-floating-labels-and-test-mode-copy delete mode 100644 changelog/fix-ece-shortcode-get-shipping-rates delete mode 100644 changelog/fix-fix-e2e-errors delete mode 100644 changelog/fix-move-woopay-theme-checkbox delete mode 100644 changelog/fix-remove-apple-pay-admin-notice-for-live-account delete mode 100644 changelog/fix-saved-cards-e2e delete mode 100644 changelog/fix-send-blog-timezone-to-woopay delete mode 100644 changelog/fix-test-instructions-translations delete mode 100644 changelog/fix-testing-instructions-dark-theme-support delete mode 100644 changelog/fix-tooltip-link-contrast-ratio delete mode 100644 changelog/fix-undefined-platform-global-theme-index-notice delete mode 100644 changelog/fix-wcpay-tracking-cookie-cache delete mode 100644 changelog/fix-woopay-direct-checkout-route-namespace delete mode 100644 changelog/fix-woopay-email-redirect delete mode 100644 changelog/fix-woopay-theme-support-from-classic-checkout delete mode 100644 changelog/fix-woopay-themeing-on-email-flow delete mode 100644 changelog/refactor-payment-method-fees-oneline delete mode 100644 changelog/remove-override-for-express-payment-grid delete mode 100644 changelog/update-e2e-setup-script delete mode 100644 changelog/update-e2e-setup-script-env-var delete mode 100644 changelog/update-jetpack-packages delete mode 100644 changelog/update-required-php-version delete mode 100644 changelog/update-test-instructions-icon-animation diff --git a/changelog.txt b/changelog.txt index 13bfd0c076c..41f3c511d8b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,35 @@ *** WooPayments Changelog *** += 8.2.0 - 2024-09-11 = +* Add - add: test instructions icon animation +* Add - Added Embdedded KYC, currently behind feature flag. +* Fix - Avoid unnecessary account data cache refresh on WooPayments pages refresh. +* Fix - Check payment method is available before rendering it. +* Fix - Disables custom checkout field detection due to compatibility issues and false positives. +* Fix - Disables testing instructions clipboard button on HTTP sites when navigator.clipboard is undefined. +* Fix - fix: missing translations on testing instructions. +* Fix - fix: platform_global_theme_support_enabled undefined index +* Fix - fix: testing instructions dark theme support +* Fix - Fix caching with tracking cookie. +* Fix - Fixed an issue where the Connect page would scroll to the top upon clicking the Enable Sandbox Mode button. +* Fix - Fixed default borderRadius value for the express checkout buttons +* Fix - Fix shipping rates retrieval method for shortcode cart/checkout. +* Fix - Fix support for merchant site styling when initializing WooPay via classic checkout +* Fix - Fix WooPay direct checkout. +* Fix - Move woopay theme support checkbox to the appearance section. +* Fix - Pass appearance data when initiating WooPay via the email input flow +* Fix - Prevent preload of BNPL messaging if minimum order amount isn't hit. +* Fix - Redirect user to WooPay OTP when the email is saved. +* Fix - Remove obsolete ApplePay warning on wp-admin for test sites. +* Fix - Update cache after persisting the User session via WooPay +* Fix - Updates test mode instructions copy for cards at checkout. +* Update - update: payment method fees in one line +* Update - Update Jetpack packages to the latest versions +* Dev - Fix failing e2e tests for saved cards. +* Dev - Fix Klarna product page message E2E test after the contents inside the iframe were updated. +* Dev - Migrate Klarna E2E tests to playwright. Reduce noise in E2E tests console output. +* Dev - Migrate multi-currency e2e tests to Playwright. + = 8.1.1 - 2024-08-29 = * Fix - Ensure 55px is the maximum height for Apple Pay button. * Fix - Fixed sandbox mode accounts being able to disable test mode for the payment gateway settings. diff --git a/changelog/9311-fix-default-border-radius b/changelog/9311-fix-default-border-radius deleted file mode 100644 index 2928de05076..00000000000 --- a/changelog/9311-fix-default-border-radius +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixed default borderRadius value for the express checkout buttons diff --git a/changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page b/changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page deleted file mode 100644 index a95d596ef55..00000000000 --- a/changelog/add-9382-explat-experiment-payments-task-onboarding-flow-skips-connect-page +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: add -Comment: Add ExPlat experiment for Payments task originated onboarding flows. - - diff --git a/changelog/as-fix-empty-express-checkout-element b/changelog/as-fix-empty-express-checkout-element deleted file mode 100644 index 18b5e8de0b6..00000000000 --- a/changelog/as-fix-empty-express-checkout-element +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Check payment method is available before rendering it. diff --git a/changelog/as-fix-max-height-allowed b/changelog/as-fix-max-height-allowed deleted file mode 100644 index fe15603cc63..00000000000 --- a/changelog/as-fix-max-height-allowed +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Ensure 55px is the maximum height for Apple Pay button. diff --git a/changelog/as-fix-woopay-data-mismatch b/changelog/as-fix-woopay-data-mismatch deleted file mode 100644 index 705d1dbbbd8..00000000000 --- a/changelog/as-fix-woopay-data-mismatch +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Update cache after persisting the User session via WooPay diff --git a/changelog/chore-disable-custom-checkout-field-detection b/changelog/chore-disable-custom-checkout-field-detection deleted file mode 100644 index 9c77c983ee2..00000000000 --- a/changelog/chore-disable-custom-checkout-field-detection +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Disables custom checkout field detection due to compatibility issues and false positives. diff --git a/changelog/chore-remove-frontend-upe-appearance-theme-constants b/changelog/chore-remove-frontend-upe-appearance-theme-constants deleted file mode 100644 index 2dfab5061d1..00000000000 --- a/changelog/chore-remove-frontend-upe-appearance-theme-constants +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: chore: remove unused upe appearance theme constants from JS config. - - diff --git a/changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright b/changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright deleted file mode 100644 index 6d010298e91..00000000000 --- a/changelog/dev-8750-migrate-mccy-e2e-tests-to-playwright +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Migrate multi-currency e2e tests to Playwright. diff --git a/changelog/dev-correct-docker-compose-v2-changelog-entry b/changelog/dev-correct-docker-compose-v2-changelog-entry deleted file mode 100644 index 1d13ad6202c..00000000000 --- a/changelog/dev-correct-docker-compose-v2-changelog-entry +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Placeholder changelog entry - making a correction to previous changelog entry 'Fix - Migrate to Docker Compose V2 for test runner environment setup scripts' - - diff --git a/changelog/dev-fix-e2e-scripts b/changelog/dev-fix-e2e-scripts deleted file mode 100644 index 51e19f80e87..00000000000 --- a/changelog/dev-fix-e2e-scripts +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Fix some aspects of the e2e tests scripts. - - diff --git a/changelog/dev-fix-klarna-product-page-e2e b/changelog/dev-fix-klarna-product-page-e2e deleted file mode 100644 index f84395e5f44..00000000000 --- a/changelog/dev-fix-klarna-product-page-e2e +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Fix Klarna product page message E2E test after the contents inside the iframe were updated. diff --git a/changelog/dev-include-connect-js b/changelog/dev-include-connect-js deleted file mode 100644 index 218ec497755..00000000000 --- a/changelog/dev-include-connect-js +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Added Embdedded KYC, currently behind feature flag. diff --git a/changelog/dev-update-klarna-e2e-tests b/changelog/dev-update-klarna-e2e-tests deleted file mode 100644 index f261610446d..00000000000 --- a/changelog/dev-update-klarna-e2e-tests +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Migrate Klarna E2E tests to playwright. Reduce noise in E2E tests console output. diff --git a/changelog/enhancement-upgrade-docker-to-php8.1 b/changelog/enhancement-upgrade-docker-to-php8.1 deleted file mode 100644 index c5bcfc2bcef..00000000000 --- a/changelog/enhancement-upgrade-docker-to-php8.1 +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Upgrade local dev env to PHP8.1 - - diff --git a/changelog/fix-9244-pmme-skeleton b/changelog/fix-9244-pmme-skeleton deleted file mode 100644 index ba34b475bff..00000000000 --- a/changelog/fix-9244-pmme-skeleton +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Prevent preload of BNPL messaging if minimum order amount isn't hit. diff --git a/changelog/fix-9281-e2e-dispute-save-challenge b/changelog/fix-9281-e2e-dispute-save-challenge deleted file mode 100644 index d858c32e0f3..00000000000 --- a/changelog/fix-9281-e2e-dispute-save-challenge +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Not a user-facing change: fix e2e test save draft dispute challenge failing due to disputes list redirect race condition - - diff --git a/changelog/fix-9288-fix-qit-error-escape-output b/changelog/fix-9288-fix-qit-error-escape-output deleted file mode 100644 index b2df1c53c3e..00000000000 --- a/changelog/fix-9288-fix-qit-error-escape-output +++ /dev/null @@ -1,3 +0,0 @@ -Significance: patch -Type: fix -Comment: Fix QIT error "EscapeOutput.OutputNotEscaped" diff --git a/changelog/fix-9324-test-drive-onboarding-scoll b/changelog/fix-9324-test-drive-onboarding-scoll deleted file mode 100644 index d290ac6e168..00000000000 --- a/changelog/fix-9324-test-drive-onboarding-scoll +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixed an issue where the Connect page would scroll to the top upon clicking the Enable Sandbox Mode button. diff --git a/changelog/fix-9395-account-data-force-refresh-on-wcpay-pages b/changelog/fix-9395-account-data-force-refresh-on-wcpay-pages deleted file mode 100644 index 9535c429acb..00000000000 --- a/changelog/fix-9395-account-data-force-refresh-on-wcpay-pages +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Avoid unnecessary account data cache refresh on WooPayments pages refresh. diff --git a/changelog/fix-catch-clipboard-navigator-undefined-error-in-http b/changelog/fix-catch-clipboard-navigator-undefined-error-in-http deleted file mode 100644 index 22c4516ccde..00000000000 --- a/changelog/fix-catch-clipboard-navigator-undefined-error-in-http +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Disables testing instructions clipboard button on HTTP sites when navigator.clipboard is undefined. diff --git a/changelog/fix-checkout-floating-labels-and-test-mode-copy b/changelog/fix-checkout-floating-labels-and-test-mode-copy deleted file mode 100644 index 862a62bf389..00000000000 --- a/changelog/fix-checkout-floating-labels-and-test-mode-copy +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Updates test mode instructions copy for cards at checkout. diff --git a/changelog/fix-ece-shortcode-get-shipping-rates b/changelog/fix-ece-shortcode-get-shipping-rates deleted file mode 100644 index b5fb9899bb5..00000000000 --- a/changelog/fix-ece-shortcode-get-shipping-rates +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix shipping rates retrieval method for shortcode cart/checkout. diff --git a/changelog/fix-fix-e2e-errors b/changelog/fix-fix-e2e-errors deleted file mode 100644 index 092fca8d978..00000000000 --- a/changelog/fix-fix-e2e-errors +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Simple bug fix. - - diff --git a/changelog/fix-move-woopay-theme-checkbox b/changelog/fix-move-woopay-theme-checkbox deleted file mode 100644 index 615b9728fdc..00000000000 --- a/changelog/fix-move-woopay-theme-checkbox +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Move woopay theme support checkbox to the appearance section. diff --git a/changelog/fix-remove-apple-pay-admin-notice-for-live-account b/changelog/fix-remove-apple-pay-admin-notice-for-live-account deleted file mode 100644 index e2424d553d2..00000000000 --- a/changelog/fix-remove-apple-pay-admin-notice-for-live-account +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Remove obsolete ApplePay warning on wp-admin for test sites. diff --git a/changelog/fix-saved-cards-e2e b/changelog/fix-saved-cards-e2e deleted file mode 100644 index 9aa7abb6cfc..00000000000 --- a/changelog/fix-saved-cards-e2e +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Fix failing e2e tests for saved cards. diff --git a/changelog/fix-send-blog-timezone-to-woopay b/changelog/fix-send-blog-timezone-to-woopay deleted file mode 100644 index 41fc9f42eb2..00000000000 --- a/changelog/fix-send-blog-timezone-to-woopay +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Send blog timezone to WooPay. - - diff --git a/changelog/fix-test-instructions-translations b/changelog/fix-test-instructions-translations deleted file mode 100644 index bc1d8474363..00000000000 --- a/changelog/fix-test-instructions-translations +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -fix: missing translations on testing instructions. diff --git a/changelog/fix-testing-instructions-dark-theme-support b/changelog/fix-testing-instructions-dark-theme-support deleted file mode 100644 index 3ac673bcf78..00000000000 --- a/changelog/fix-testing-instructions-dark-theme-support +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -fix: testing instructions dark theme support diff --git a/changelog/fix-tooltip-link-contrast-ratio b/changelog/fix-tooltip-link-contrast-ratio deleted file mode 100644 index 69007b6db19..00000000000 --- a/changelog/fix-tooltip-link-contrast-ratio +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: fix: tooltip link color contrast ratio - - diff --git a/changelog/fix-undefined-platform-global-theme-index-notice b/changelog/fix-undefined-platform-global-theme-index-notice deleted file mode 100644 index 49befece76f..00000000000 --- a/changelog/fix-undefined-platform-global-theme-index-notice +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -fix: platform_global_theme_support_enabled undefined index diff --git a/changelog/fix-wcpay-tracking-cookie-cache b/changelog/fix-wcpay-tracking-cookie-cache deleted file mode 100644 index e0f40c40a19..00000000000 --- a/changelog/fix-wcpay-tracking-cookie-cache +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix caching with tracking cookie. diff --git a/changelog/fix-woopay-direct-checkout-route-namespace b/changelog/fix-woopay-direct-checkout-route-namespace deleted file mode 100644 index 01e5dbb1384..00000000000 --- a/changelog/fix-woopay-direct-checkout-route-namespace +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix WooPay direct checkout. diff --git a/changelog/fix-woopay-email-redirect b/changelog/fix-woopay-email-redirect deleted file mode 100644 index 9bba71ff1a8..00000000000 --- a/changelog/fix-woopay-email-redirect +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Redirect user to WooPay OTP when the email is saved. diff --git a/changelog/fix-woopay-theme-support-from-classic-checkout b/changelog/fix-woopay-theme-support-from-classic-checkout deleted file mode 100644 index 3642cde31ca..00000000000 --- a/changelog/fix-woopay-theme-support-from-classic-checkout +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix support for merchant site styling when initializing WooPay via classic checkout diff --git a/changelog/fix-woopay-themeing-on-email-flow b/changelog/fix-woopay-themeing-on-email-flow deleted file mode 100644 index d09923a10e2..00000000000 --- a/changelog/fix-woopay-themeing-on-email-flow +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Pass appearance data when initiating WooPay via the email input flow diff --git a/changelog/refactor-payment-method-fees-oneline b/changelog/refactor-payment-method-fees-oneline deleted file mode 100644 index 5a12bb1da7a..00000000000 --- a/changelog/refactor-payment-method-fees-oneline +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -update: payment method fees in one line diff --git a/changelog/remove-override-for-express-payment-grid b/changelog/remove-override-for-express-payment-grid deleted file mode 100644 index 3bdc7499449..00000000000 --- a/changelog/remove-override-for-express-payment-grid +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: update -Comment: Insignificant css change - - diff --git a/changelog/update-e2e-setup-script b/changelog/update-e2e-setup-script deleted file mode 100644 index 684cf9ced6d..00000000000 --- a/changelog/update-e2e-setup-script +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Define WooPay url for e2e env setup - - diff --git a/changelog/update-e2e-setup-script-env-var b/changelog/update-e2e-setup-script-env-var deleted file mode 100644 index 457978d1fae..00000000000 --- a/changelog/update-e2e-setup-script-env-var +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Define WooPay Blog ID for e2e env setup - - diff --git a/changelog/update-jetpack-packages b/changelog/update-jetpack-packages deleted file mode 100644 index 1899494f14a..00000000000 --- a/changelog/update-jetpack-packages +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update Jetpack packages to the latest versions diff --git a/changelog/update-required-php-version b/changelog/update-required-php-version deleted file mode 100644 index 0aa1795b408..00000000000 --- a/changelog/update-required-php-version +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Bump required PHP version to 7.3. - - diff --git a/changelog/update-test-instructions-icon-animation b/changelog/update-test-instructions-icon-animation deleted file mode 100644 index 6a92da93656..00000000000 --- a/changelog/update-test-instructions-icon-animation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -add: test instructions icon animation diff --git a/package-lock.json b/package-lock.json index 03592cffe24..23ef7829ee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "woocommerce-payments", - "version": "8.1.1", + "version": "8.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "woocommerce-payments", - "version": "8.1.1", + "version": "8.2.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 6f11833d574..00dea7ef57a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-payments", - "version": "8.1.1", + "version": "8.2.0", "main": "webpack.config.js", "author": "Automattic", "license": "GPL-3.0-or-later", diff --git a/readme.txt b/readme.txt index 7797e6da978..ca0f1154a38 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment Requires at least: 6.0 Tested up to: 6.6 Requires PHP: 7.3 -Stable tag: 8.1.1 +Stable tag: 8.2.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -94,6 +94,36 @@ Please note that our support for the checkout block is still experimental and th == Changelog == += 8.2.0 - 2024-09-11 = +* Add - add: test instructions icon animation +* Add - Added Embdedded KYC, currently behind feature flag. +* Fix - Avoid unnecessary account data cache refresh on WooPayments pages refresh. +* Fix - Check payment method is available before rendering it. +* Fix - Disables custom checkout field detection due to compatibility issues and false positives. +* Fix - Disables testing instructions clipboard button on HTTP sites when navigator.clipboard is undefined. +* Fix - fix: missing translations on testing instructions. +* Fix - fix: platform_global_theme_support_enabled undefined index +* Fix - fix: testing instructions dark theme support +* Fix - Fix caching with tracking cookie. +* Fix - Fixed an issue where the Connect page would scroll to the top upon clicking the Enable Sandbox Mode button. +* Fix - Fixed default borderRadius value for the express checkout buttons +* Fix - Fix shipping rates retrieval method for shortcode cart/checkout. +* Fix - Fix support for merchant site styling when initializing WooPay via classic checkout +* Fix - Fix WooPay direct checkout. +* Fix - Move woopay theme support checkbox to the appearance section. +* Fix - Pass appearance data when initiating WooPay via the email input flow +* Fix - Prevent preload of BNPL messaging if minimum order amount isn't hit. +* Fix - Redirect user to WooPay OTP when the email is saved. +* Fix - Remove obsolete ApplePay warning on wp-admin for test sites. +* Fix - Update cache after persisting the User session via WooPay +* Fix - Updates test mode instructions copy for cards at checkout. +* Update - update: payment method fees in one line +* Update - Update Jetpack packages to the latest versions +* Dev - Fix failing e2e tests for saved cards. +* Dev - Fix Klarna product page message E2E test after the contents inside the iframe were updated. +* Dev - Migrate Klarna E2E tests to playwright. Reduce noise in E2E tests console output. +* Dev - Migrate multi-currency e2e tests to Playwright. + = 8.1.1 - 2024-08-29 = * Fix - Ensure 55px is the maximum height for Apple Pay button. * Fix - Fixed sandbox mode accounts being able to disable test mode for the payment gateway settings. diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 8b86ec61709..149100add8e 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -11,7 +11,7 @@ * WC tested up to: 9.2.0 * Requires at least: 6.0 * Requires PHP: 7.3 - * Version: 8.1.1 + * Version: 8.2.0 * Requires Plugins: woocommerce * * @package WooCommerce\Payments From 2ce85a59a4d868b1f2964f38929ad38172fefa12 Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Tue, 10 Sep 2024 22:38:52 -0300 Subject: [PATCH 43/44] Add error handling for `loadError` in ECE (#9416) --- changelog/fix-9414-unhandled-ece-loaderror | 4 ++ client/checkout/express-checkout-buttons.scss | 1 - client/express-checkout/index.js | 42 +++++++++++++++---- .../utils/checkPaymentMethodIsAvailable.js | 1 + ...xpress-checkout-button-display-handler.php | 2 +- ...yments-express-checkout-button-handler.php | 2 +- 6 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 changelog/fix-9414-unhandled-ece-loaderror diff --git a/changelog/fix-9414-unhandled-ece-loaderror b/changelog/fix-9414-unhandled-ece-loaderror new file mode 100644 index 00000000000..654ccdcfbba --- /dev/null +++ b/changelog/fix-9414-unhandled-ece-loaderror @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Handle loadError in ECE for Block Context Initialization. diff --git a/client/checkout/express-checkout-buttons.scss b/client/checkout/express-checkout-buttons.scss index 10b6058a3b3..b20ac8a5be8 100644 --- a/client/checkout/express-checkout-buttons.scss +++ b/client/checkout/express-checkout-buttons.scss @@ -2,7 +2,6 @@ margin-top: 1em; width: 100%; clear: both; - margin-bottom: 1.5em; .woocommerce-cart & { margin-bottom: 0; diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index d1fa717b606..97eae8a9f97 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -242,7 +242,17 @@ jQuery( ( $ ) => { getExpressCheckoutButtonStyleSettings() ); - wcpayECE.showButton( eceButton ); + wcpayECE.renderButton( eceButton ); + + eceButton.on( 'loaderror', () => { + wcPayECEError = __( + 'The cart is incompatible with express checkout.', + 'woocommerce-payments' + ); + if ( ! document.getElementById( 'wcpay-woopay-button' ) ) { + wcpayECE?.getButtonSeparator()?.hide(); + } + } ); eceButton.on( 'click', function ( event ) { // If login is required for checkout, display redirect confirmation dialog. @@ -326,7 +336,19 @@ jQuery( ( $ ) => { onCancelHandler(); } ); - eceButton.on( 'ready', onReadyHandler ); + eceButton.on( 'ready', ( onReadyParams ) => { + onReadyHandler( onReadyParams ); + + if ( + onReadyParams?.availablePaymentMethods && + Object.values( + onReadyParams.availablePaymentMethods + ).filter( Boolean ).length + ) { + wcpayECE.show(); + wcpayECE.getButtonSeparator().show(); + } + } ); if ( getExpressCheckoutData( 'is_product_page' ) ) { wcpayECE.attachProductPageEventListeners( elements ); @@ -520,18 +542,24 @@ jQuery( ( $ ) => { }, getElements: () => { - return $( - '.wcpay-payment-request-wrapper,#wcpay-express-checkout-button-separator' - ); + return $( '#wcpay-express-checkout-element' ); + }, + + getButtonSeparator: () => { + return $( '#wcpay-express-checkout-button-separator' ); }, show: () => { wcpayECE.getElements().show(); }, - showButton: ( eceButton ) => { + hide: () => { + wcpayECE.getElements().hide(); + wcpayECE.getButtonSeparator().hide(); + }, + + renderButton: ( eceButton ) => { if ( $( '#wcpay-express-checkout-element' ).length ) { - wcpayECE.show(); eceButton.mount( '#wcpay-express-checkout-element' ); } }, diff --git a/client/express-checkout/utils/checkPaymentMethodIsAvailable.js b/client/express-checkout/utils/checkPaymentMethodIsAvailable.js index 75296840a9b..a42d7ffefe9 100644 --- a/client/express-checkout/utils/checkPaymentMethodIsAvailable.js +++ b/client/express-checkout/utils/checkPaymentMethodIsAvailable.js @@ -47,6 +47,7 @@ export const checkPaymentMethodIsAvailable = memoize( } } > resolve( false ) } options={ { paymentMethods: { amazonPay: 'never', diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php index fd7ba4eae92..a5e3e08939c 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php @@ -141,7 +141,7 @@ public function display_express_checkout_buttons() { // When Payment Request button is enabled, we need the separator markup on the page, but hidden in case the browser doesn't have any payment request methods to display. // More details: https://github.com/Automattic/woocommerce-payments/pull/5399#discussion_r1073633776. - $separator_starts_hidden = ( $should_show_payment_request || $should_show_express_checkout_button ) && ! $should_show_woopay; + $separator_starts_hidden = ! $should_show_woopay; if ( $should_show_woopay || $should_show_payment_request || $should_show_express_checkout_button ) { ?>
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php index 1b6f68f6275..682b4eac903 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php @@ -288,7 +288,7 @@ public function display_express_checkout_button_html() { return; } ?> -
+ Date: Wed, 11 Sep 2024 02:47:06 +0000 Subject: [PATCH 44/44] Amend changelog entries for release 8.2.0 --- changelog.txt | 1 + changelog/fix-9414-unhandled-ece-loaderror | 4 ---- readme.txt | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 changelog/fix-9414-unhandled-ece-loaderror diff --git a/changelog.txt b/changelog.txt index 41f3c511d8b..f7bccd8a1d5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,7 @@ * Fix - Fix shipping rates retrieval method for shortcode cart/checkout. * Fix - Fix support for merchant site styling when initializing WooPay via classic checkout * Fix - Fix WooPay direct checkout. +* Fix - Handle loadError in ECE for Block Context Initialization. * Fix - Move woopay theme support checkbox to the appearance section. * Fix - Pass appearance data when initiating WooPay via the email input flow * Fix - Prevent preload of BNPL messaging if minimum order amount isn't hit. diff --git a/changelog/fix-9414-unhandled-ece-loaderror b/changelog/fix-9414-unhandled-ece-loaderror deleted file mode 100644 index 654ccdcfbba..00000000000 --- a/changelog/fix-9414-unhandled-ece-loaderror +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Handle loadError in ECE for Block Context Initialization. diff --git a/readme.txt b/readme.txt index ca0f1154a38..f89a0a662c1 100644 --- a/readme.txt +++ b/readme.txt @@ -110,6 +110,7 @@ Please note that our support for the checkout block is still experimental and th * Fix - Fix shipping rates retrieval method for shortcode cart/checkout. * Fix - Fix support for merchant site styling when initializing WooPay via classic checkout * Fix - Fix WooPay direct checkout. +* Fix - Handle loadError in ECE for Block Context Initialization. * Fix - Move woopay theme support checkbox to the appearance section. * Fix - Pass appearance data when initiating WooPay via the email input flow * Fix - Prevent preload of BNPL messaging if minimum order amount isn't hit.