diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 041455441..9fc8d4a29 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -27,7 +27,7 @@ web_environment: - ADMIN_USER=admin - ADMIN_PASS=admin - ADMIN_EMAIL=admin@example.com - - WC_VERSION=7.7.2 + - WC_VERSION=9.5.1 # Key features of ddev's config.yaml: diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index e7957d210..c3e07d1bb 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -50,7 +50,7 @@ jobs: if: github.event.inputs.filePrefix - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.FILENAME }} path: dist/ diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 9bf4f02b0..00ff24fcb 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: @@ -30,6 +30,7 @@ jobs: run: vendor/bin/phpunit - name: Psalm + if: ${{ matrix.php-versions == '7.4' }} run: ./vendor/bin/psalm --show-info=false --threads=8 --diff - name: Run PHPCS diff --git a/changelog.txt b/changelog.txt index 4b2fe964b..1efeb16a3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,22 +1,34 @@ *** Changelog *** -= 2.9.5 - xxxx-xx-xx = -Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 -Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 -Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 -Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752 -Fix - Additional payment tokens saved with new customer_id #2820 -Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809 -Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845 -Fix - Restore accidentally removed ACDC currencies #2838 -Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712 -Enhancement - Allow disabling specific card types for Fastlane #2704 -Enhancement - Fastlane Insights SDK implementation for block Checkout #2737 -Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703 -Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792 -Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772 -Enhancement - Disable “Add payment method” button while saving ACDC payment #2794 -Enhancement - Sanitize soft_descriptor field #2846 #2854 += 2.9.6 - XXXX-XX-XX = +* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955 +* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872 +* Fix - String translations not applied to Card Fields on Block Checkout #2934 +* Fix - Fastlane component included in script when Fastlane is disabled #2911 +* Fix - Zero amount line items may trigger CANNOT_BE_ZERO_OR_NEGATIVE error after rounding error #2906 +* Fix - “Save changes” is grey and unclickable when switching from Sandbox to Live #2895 +* Fix - plugin queries variations when button/messaging is disabled on single product page #2896 +* Fix - Use get_id instead of get_order_number on setting custom_id (author @0verscore) #2930 +* Enhancement - Improve fraud response order notes for Advanced Card Processing transactions #2905 +* Tweak - Update the minimum plugin requirements to WordPress 6.5 & WooCommerce 9.2 #2920 + += 2.9.5 - 2024-12-10 = +* Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 +* Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 +* Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 +* Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752 +* Fix - Additional payment tokens saved with new customer_id #2820 +* Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809 +* Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845 +* Fix - Restore accidentally removed ACDC currencies #2838 +* Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712 +* Enhancement - Allow disabling specific card types for Fastlane #2704 +* Enhancement - Fastlane Insights SDK implementation for block Checkout #2737 +* Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703 +* Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792 +* Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772 +* Enhancement - Disable “Add payment method” button while saving ACDC payment #2794 +* Enhancement - Sanitize soft_descriptor field #2846 #2854 = 2.9.4 - 2024-11-11 = * Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755 diff --git a/composer.json b/composer.json index 4d4b9f2f2..17ecd3eda 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "woocommerce/woocommerce-paypal-payments", "type": "wordpress-plugin", "description": "PayPal Commerce Platform for WooCommerce", - "license": "GPL-2.0", + "license": "GPL-2.0-or-later", "require": { "php": "^7.4 | ^8.0", "ext-json": "*", diff --git a/composer.lock b/composer.lock index a18b56927..180c71364 100644 --- a/composer.lock +++ b/composer.lock @@ -5541,8 +5541,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "php-stubs/wordpress-stubs": 0, - "php-stubs/woocommerce-stubs": 0 + "php-stubs/woocommerce-stubs": 0, + "php-stubs/wordpress-stubs": 0 }, "prefer-stable": true, "prefer-lowest": false, @@ -5550,7 +5550,7 @@ "php": "^7.4 | ^8.0", "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4" }, diff --git a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php index c7f5ec131..1d100cdda 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php @@ -16,6 +16,8 @@ use Psr\Log\LoggerInterface; /** * Class PartnerReferrals + * + * @see https://developer.paypal.com/docs/api/partner-referrals/v2/ */ class PartnerReferrals { diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index 2cc7c5480..baecabf73 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -17,44 +17,44 @@ class FraudProcessorResponse { /** * The AVS response code. * - * @var string|null + * @var string */ - protected $avs_code; + protected string $avs_code; /** * The CVV response code. * - * @var string|null + * @var string */ - protected $cvv_code; + protected string $cvv2_code; /** * FraudProcessorResponse constructor. * * @param string|null $avs_code The AVS response code. - * @param string|null $cvv_code The CVV response code. + * @param string|null $cvv2_code The CVV response code. */ - public function __construct( ?string $avs_code, ?string $cvv_code ) { - $this->avs_code = $avs_code; - $this->cvv_code = $cvv_code; + public function __construct( ?string $avs_code, ?string $cvv2_code ) { + $this->avs_code = (string) $avs_code; + $this->cvv2_code = (string) $cvv2_code; } /** * Returns the AVS response code. * - * @return string|null + * @return string */ - public function avs_code(): ?string { + public function avs_code(): string { return $this->avs_code; } /** * Returns the CVV response code. * - * @return string|null + * @return string */ - public function cvv_code(): ?string { - return $this->cvv_code; + public function cvv_code(): string { + return $this->cvv2_code; } /** @@ -64,11 +64,99 @@ class FraudProcessorResponse { */ public function to_array(): array { return array( - 'avs_code' => $this->avs_code() ?: '', + 'avs_code' => $this->avs_code(), + 'cvv2_code' => $this->cvv_code(), + // For backwards compatibility. 'address_match' => $this->avs_code() === 'M' ? 'Y' : 'N', 'postal_match' => $this->avs_code() === 'M' ? 'Y' : 'N', 'cvv_match' => $this->cvv_code() === 'M' ? 'Y' : 'N', ); } + /** + * Retrieves the AVS (Address Verification System) code messages based on the AVS response code. + * + * Provides human-readable descriptions for various AVS response codes + * and returns the corresponding message for the given code. + * + * @return string The AVS response code message. If the code is not found, an error message is returned. + */ + public function get_avs_code_message(): string { + if ( ! $this->avs_code() ) { + return ''; + } + $messages = array( + /* Visa, Mastercard, Discover, American Express */ + 'A' => 'A: Address - Address only (no ZIP code)', + 'B' => 'B: International "A" - Address only (no ZIP code)', + 'C' => 'C: International "N" - None. The transaction is declined.', + 'D' => 'D: International "X" - Address and Postal Code', + 'E' => 'E: Not allowed for MOTO (Internet/Phone) transactions - Not applicable. The transaction is declined.', + 'F' => 'F: UK-specific "X" - Address and Postal Code', + 'G' => 'G: Global Unavailable - Not applicable', + 'I' => 'I: International Unavailable - Not applicable', + 'M' => 'M: Address - Address and Postal Code', + 'N' => 'N: No - None. The transaction is declined.', + 'P' => 'P: Postal (International "Z") - Postal Code only (no Address)', + 'R' => 'R: Retry - Not applicable', + 'S' => 'S: Service not Supported - Not applicable', + 'U' => 'U: Unavailable / Address not checked, or acquirer had no response. Service not available.', + 'W' => 'W: Whole ZIP - Nine-digit ZIP code (no Address)', + 'X' => 'X: Exact match - Address and nine-digit ZIP code)', + 'Y' => 'Y: Yes - Address and five-digit ZIP', + 'Z' => 'Z: ZIP - Five-digit ZIP code (no Address)', + /* Maestro */ + '0' => '0: All the address information matched.', + '1' => '1: None of the address information matched. The transaction is declined.', + '2' => '2: Part of the address information matched.', + '3' => '3: The merchant did not provide AVS information. Not processed.', + '4' => '4: Address not checked, or acquirer had no response. Service not available.', + ); + + /** + * Psalm suppress + * + * @psalm-suppress PossiblyNullArrayOffset + * @psalm-suppress PossiblyNullArgument + */ + return $messages[ $this->avs_code() ] ?? sprintf( '%s: Error', $this->avs_code() ); + } + + /** + * Retrieves the CVV2 code message based on the CVV code provided. + * + * This method maps CVV response codes to their corresponding descriptive messages. + * + * @return string The descriptive message corresponding to the CVV2 code, or a formatted error message if the code is unrecognized. + */ + public function get_cvv2_code_message(): string { + if ( ! $this->cvv_code() ) { + return ''; + } + $messages = array( + /* Visa, Mastercard, Discover, American Express */ + 'E' => 'E: Error - Unrecognized or Unknown response', + 'I' => 'I: Invalid or Null', + 'M' => 'M: Match or CSC', + 'N' => 'N: No match', + 'P' => 'P: Not processed', + 'S' => 'S: Service not supported', + 'U' => 'U: Unknown - Issuer is not certified', + 'X' => 'X: No response / Service not available', + /* Maestro */ + '0' => '0: Matched CVV2', + '1' => '1: No match', + '2' => '2: The merchant has not implemented CVV2 code handling', + '3' => '3: Merchant has indicated that CVV2 is not present on card', + '4' => '4: Service not available', + ); + + /** + * Psalm suppress + * + * @psalm-suppress PossiblyNullArrayOffset + * @psalm-suppress PossiblyNullArgument + */ + return $messages[ $this->cvv_code() ] ?? sprintf( '%s: Error', $this->cvv_code() ); + } } diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 7cb0c048f..1d222f605 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -178,6 +178,11 @@ class PurchaseUnitSanitizer { // Get a more intelligent adjustment mechanism. $increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] ); + // not floor items that will be negative then. + if ( (float) $item['unit_amount']['value'] < $increment ) { + continue; + } + $this->purchase_unit['items'][ $index ]['unit_amount'] = ( new Money( ( (float) $item['unit_amount']['value'] ) - $increment, $item['unit_amount']['currency_code'] diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index c5cc6a1b9..aa9876069 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -182,6 +182,26 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule 2 ); + add_filter( + 'woocommerce_paypal_payments_rest_common_merchant_data', + function( array $merchant_data ) use ( $c ): array { + if ( ! isset( $merchant_data['features'] ) ) { + $merchant_data['features'] = array(); + } + + $product_status = $c->get( 'applepay.apple-product-status' ); + assert( $product_status instanceof AppleProductStatus ); + + $apple_pay_enabled = $product_status->is_active(); + + $merchant_data['features']['apple_pay'] = array( + 'enabled' => $apple_pay_enabled, + ); + + return $merchant_data; + } + ); + return true; } diff --git a/modules/ppcp-axo-block/src/AxoBlockModule.php b/modules/ppcp-axo-block/src/AxoBlockModule.php index c8216bf62..af94976a3 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -92,7 +92,10 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule */ add_filter( 'woocommerce_paypal_payments_sdk_components_hook', - function( $components ) { + function( $components ) use ( $c ) { + if ( ! $c->has( 'axo.available' ) || ! $c->get( 'axo.available' ) ) { + return $components; + } $components[] = 'fastlane'; return $components; } diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 121b17805..ca427700d 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -44,7 +44,9 @@ return array( // If AXO is configured and onboarded. 'axo.available' => static function ( ContainerInterface $container ): bool { - return true; + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + return $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ); }, 'axo.url' => static function ( ContainerInterface $container ): string { diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 3ac0ff157..21ee84a35 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -246,7 +246,13 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { */ add_filter( 'woocommerce_paypal_payments_sdk_components_hook', - function( $components ) { + function( $components ) use ( $c ) { + $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); + assert( $dcc_configuration instanceof DCCGatewayConfiguration ); + + if ( ! $dcc_configuration->use_fastlane() ) { + return $components; + } $components[] = 'fastlane'; return $components; } @@ -255,14 +261,18 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { add_action( 'wp_head', function () use ( $c ) { - // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript - echo ''; - // Add meta tag to allow feature-detection of the site's AXO payment state. $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); assert( $dcc_configuration instanceof DCCGatewayConfiguration ); - $this->add_feature_detection_tag( $dcc_configuration->use_fastlane() ); + if ( $dcc_configuration->use_fastlane() ) { + // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + echo ''; + + $this->add_feature_detection_tag( true ); + } else { + $this->add_feature_detection_tag( false ); + } } ); diff --git a/modules/ppcp-blocks/resources/js/Components/block-editor-paypal.js b/modules/ppcp-blocks/resources/js/Components/block-editor-paypal.js new file mode 100644 index 000000000..37d99539c --- /dev/null +++ b/modules/ppcp-blocks/resources/js/Components/block-editor-paypal.js @@ -0,0 +1,52 @@ +import { useMemo } from '@wordpress/element'; +import { normalizeStyleForFundingSource } from '../../../../ppcp-button/resources/js/modules/Helper/Style'; +import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js'; + +export const BlockEditorPayPalComponent = ( { + config, + fundingSource, + buttonAttributes, +} ) => { + const urlParams = useMemo( + () => ( { + clientId: 'test', + ...config.scriptData.url_params, + dataNamespace: 'ppcp-blocks-editor-paypal-buttons', + components: 'buttons', + } ), + [] + ); + + const style = useMemo( () => { + const configStyle = normalizeStyleForFundingSource( + config.scriptData.button.style, + fundingSource + ); + + if ( buttonAttributes ) { + return { + ...configStyle, + height: buttonAttributes.height + ? Number( buttonAttributes.height ) + : configStyle.height, + borderRadius: buttonAttributes.borderRadius + ? Number( buttonAttributes.borderRadius ) + : configStyle.borderRadius, + }; + } + + return configStyle; + }, [ fundingSource, buttonAttributes ] ); + + return ( + + false } + /> + + ); +}; diff --git a/modules/ppcp-blocks/resources/js/Components/card-fields.js b/modules/ppcp-blocks/resources/js/Components/card-fields.js index f47b18f35..30de01bb1 100644 --- a/modules/ppcp-blocks/resources/js/Components/card-fields.js +++ b/modules/ppcp-blocks/resources/js/Components/card-fields.js @@ -3,10 +3,10 @@ import { useEffect, useState } from '@wordpress/element'; import { PayPalScriptProvider, PayPalCardFieldsProvider, - PayPalNameField, - PayPalNumberField, - PayPalExpiryField, - PayPalCVVField, + PayPalNameField, + PayPalNumberField, + PayPalExpiryField, + PayPalCVVField, } from '@paypal/react-paypal-js'; import { CheckoutHandler } from './checkout-handler'; @@ -19,11 +19,7 @@ import { import { cartHasSubscriptionProducts } from '../Helper/Subscription'; import { __ } from '@wordpress/i18n'; -export function CardFields( { - config, - eventRegistration, - emitResponse, -} ) { +export function CardFields( { config, eventRegistration, emitResponse } ) { const { onPaymentSetup } = eventRegistration; const { responseTypes } = emitResponse; @@ -96,16 +92,36 @@ export function CardFields( { console.error( err ); } } > - - -
-
- -
-
- -
-
+ + +
+
+ +
+
+ +
+
{ + const { PaymentMethodIcons } = components; + + return ( + <> + + + + ); +}; diff --git a/modules/ppcp-blocks/resources/js/Components/paypal.js b/modules/ppcp-blocks/resources/js/Components/paypal.js new file mode 100644 index 000000000..c9bb1ad88 --- /dev/null +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -0,0 +1,493 @@ +import { useEffect, useState } from '@wordpress/element'; +import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; +import { + mergeWcAddress, + paypalAddressToWc, + paypalOrderToWcAddresses, +} from '../Helper/Address'; +import { convertKeysToSnakeCase } from '../Helper/Helper'; +import buttonModuleWatcher from '../../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; +import { normalizeStyleForFundingSource } from '../../../../ppcp-button/resources/js/modules/Helper/Style'; +import { + cartHasSubscriptionProducts, + isPayPalSubscription, +} from '../Helper/Subscription'; +import { + createOrder, + createSubscription, + createVaultSetupToken, + handleApprove, + handleApproveSubscription, + onApproveSavePayment, +} from '../paypal-config'; + +const PAYPAL_GATEWAY_ID = 'ppcp-gateway'; + +const namespace = 'ppcpBlocksPaypalExpressButtons'; +let registeredContext = false; +let paypalScriptPromise = null; + +export const PayPalComponent = ( { + config, + onClick, + onClose, + onSubmit, + onError, + eventRegistration, + emitResponse, + activePaymentMethod, + shippingData, + isEditing, + fundingSource, + buttonAttributes, +} ) => { + const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } = + eventRegistration; + const { responseTypes } = emitResponse; + + const [ paypalOrder, setPaypalOrder ] = useState( null ); + const [ continuationFilled, setContinuationFilled ] = useState( false ); + const [ gotoContinuationOnError, setGotoContinuationOnError ] = + useState( false ); + + const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false ); + + if ( ! paypalScriptLoaded ) { + if ( ! paypalScriptPromise ) { + // for editor, since canMakePayment was not called + paypalScriptPromise = loadPayPalScript( + namespace, + config.scriptData + ); + } + paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) ); + } + + const methodId = fundingSource + ? `${ config.id }-${ fundingSource }` + : config.id; + + /** + * The block cart displays express checkout buttons. Those buttons are handled by the + * PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons"). + * + * A possible bug in WooCommerce does not use the correct payment method ID for the express + * payment buttons inside the cart, but sends the ID of the _first_ active payment method. + * + * This function uses an internal WooCommerce dispatcher method to set the correct method ID. + */ + const enforcePaymentMethodForCart = () => { + // Do nothing, unless we're handling block cart express payment buttons. + if ( 'cart-block' !== config.scriptData.context ) { + return; + } + + // Set the active payment method to PAYPAL_GATEWAY_ID. + wp.data + .dispatch( 'wc/store/payment' ) + .__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} ); + }; + + useEffect( () => { + // fill the form if in continuation (for product or mini-cart buttons) + if ( continuationFilled || ! config.scriptData.continuation?.order ) { + return; + } + + try { + const paypalAddresses = paypalOrderToWcAddresses( + config.scriptData.continuation.order + ); + const wcAddresses = wp.data + .select( 'wc/store/cart' ) + .getCustomerData(); + const addresses = mergeWcAddress( wcAddresses, paypalAddresses ); + + wp.data + .dispatch( 'wc/store/cart' ) + .setBillingAddress( addresses.billingAddress ); + + if ( shippingData.needsShipping ) { + wp.data + .dispatch( 'wc/store/cart' ) + .setShippingAddress( addresses.shippingAddress ); + } + } catch ( err ) { + // sometimes the PayPal address is missing, skip in this case. + console.error( err ); + } + + // this useEffect should run only once, but adding this in case of some kind of full re-rendering + setContinuationFilled( true ); + }, [ shippingData, continuationFilled ] ); + + const getCheckoutRedirectUrl = () => { + const checkoutUrl = new URL( config.scriptData.redirect ); + // sometimes some browsers may load some kind of cached version of the page, + // so adding a parameter to avoid that + checkoutUrl.searchParams.append( + 'ppcp-continuation-redirect', + new Date().getTime().toString() + ); + return checkoutUrl.toString(); + }; + + useEffect( () => { + const unsubscribe = onCheckoutValidation( () => { + if ( config.scriptData.continuation ) { + return true; + } + if ( + gotoContinuationOnError && + wp.data.select( 'wc/store/validation' ).hasValidationErrors() + ) { + location.href = getCheckoutRedirectUrl(); + return { type: responseTypes.ERROR }; + } + + return true; + } ); + return unsubscribe; + }, [ onCheckoutValidation, gotoContinuationOnError ] ); + + const handleClick = ( data, actions ) => { + if ( isEditing ) { + return actions.reject(); + } + + window.ppcpFundingSource = data.fundingSource; + + onClick(); + }; + + const shouldHandleShippingInPayPal = () => { + return shouldskipFinalConfirmation() && config.needShipping; + }; + + const shouldskipFinalConfirmation = () => { + if ( config.finalReviewEnabled ) { + return false; + } + + return ( + window.ppcpFundingSource !== 'venmo' || + ! config.scriptData.vaultingEnabled + ); + }; + + let handleShippingOptionsChange = null; + let handleShippingAddressChange = null; + + if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) { + handleShippingOptionsChange = async ( data, actions ) => { + try { + const shippingOptionId = data.selectedShippingOption?.id; + if ( shippingOptionId ) { + await wp.data + .dispatch( 'wc/store/cart' ) + .selectShippingRate( shippingOptionId ); + await shippingData.setSelectedRates( shippingOptionId ); + } + + const res = await fetch( config.ajax.update_shipping.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify( { + nonce: config.ajax.update_shipping.nonce, + order_id: data.orderID, + } ), + } ); + + const json = await res.json(); + + if ( ! json.success ) { + throw new Error( json.data.message ); + } + } catch ( e ) { + console.error( e ); + + actions.reject(); + } + }; + + handleShippingAddressChange = async ( data, actions ) => { + try { + const address = paypalAddressToWc( + convertKeysToSnakeCase( data.shippingAddress ) + ); + + await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { + shipping_address: address, + } ); + + await shippingData.setShippingAddress( address ); + + const res = await fetch( config.ajax.update_shipping.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify( { + nonce: config.ajax.update_shipping.nonce, + order_id: data.orderID, + } ), + } ); + + const json = await res.json(); + + if ( ! json.success ) { + throw new Error( json.data.message ); + } + } catch ( e ) { + console.error( e ); + + actions.reject(); + } + }; + } + + useEffect( () => { + if ( activePaymentMethod !== methodId ) { + return; + } + + const unsubscribeProcessing = onPaymentSetup( () => { + if ( + cartHasSubscriptionProducts( config.scriptData ) && + config.scriptData.is_free_trial_cart + ) { + return { + type: responseTypes.SUCCESS, + }; + } + + if ( config.scriptData.continuation ) { + return { + type: responseTypes.SUCCESS, + meta: { + paymentMethodData: { + paypal_order_id: + config.scriptData.continuation.order_id, + funding_source: + window.ppcpFundingSource ?? 'paypal', + }, + }, + }; + } + + const addresses = paypalOrderToWcAddresses( paypalOrder ); + + return { + type: responseTypes.SUCCESS, + meta: { + paymentMethodData: { + paypal_order_id: paypalOrder.id, + funding_source: window.ppcpFundingSource ?? 'paypal', + }, + ...addresses, + }, + }; + } ); + return () => { + unsubscribeProcessing(); + }; + }, [ onPaymentSetup, paypalOrder, activePaymentMethod ] ); + + useEffect( () => { + if ( activePaymentMethod !== methodId ) { + return; + } + const unsubscribe = onCheckoutFail( ( { processingResponse } ) => { + console.error( processingResponse ); + if ( onClose ) { + onClose(); + } + if ( config.scriptData.continuation ) { + return true; + } + if ( shouldskipFinalConfirmation() ) { + location.href = getCheckoutRedirectUrl(); + } + return true; + } ); + return unsubscribe; + }, [ onCheckoutFail, onClose, activePaymentMethod ] ); + + if ( config.scriptData.continuation ) { + return ( +
+ ); + } + + if ( ! registeredContext ) { + buttonModuleWatcher.registerContextBootstrap( + config.scriptData.context, + { + createOrder: ( data ) => { + return createOrder( data, config, onError, onClose ); + }, + onApprove: ( data, actions ) => { + return handleApprove( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose + ); + }, + } + ); + registeredContext = true; + } + + const style = normalizeStyleForFundingSource( + config.scriptData.button.style, + fundingSource + ); + + if ( typeof buttonAttributes !== 'undefined' ) { + style.height = buttonAttributes?.height + ? Number( buttonAttributes.height ) + : style.height; + style.borderRadius = buttonAttributes?.borderRadius + ? Number( buttonAttributes.borderRadius ) + : style.borderRadius; + } + + if ( ! paypalScriptLoaded ) { + return null; + } + + const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver( + 'react', + { React, ReactDOM } + ); + + const getOnShippingOptionsChange = ( fundingSource ) => { + if ( fundingSource === 'venmo' ) { + return null; + } + + return ( data, actions ) => { + shouldHandleShippingInPayPal() + ? handleShippingOptionsChange( data, actions ) + : null; + }; + }; + + const getOnShippingAddressChange = ( fundingSource ) => { + if ( fundingSource === 'venmo' ) { + return null; + } + + return ( data, actions ) => { + const shippingAddressChange = shouldHandleShippingInPayPal() + ? handleShippingAddressChange( data, actions ) + : null; + + return shippingAddressChange; + }; + }; + + if ( + cartHasSubscriptionProducts( config.scriptData ) && + config.scriptData.is_free_trial_cart + ) { + return ( + createVaultSetupToken( config ) } + onApprove={ ( { vaultSetupToken } ) => + onApproveSavePayment( vaultSetupToken, config, onSubmit ) + } + /> + ); + } + + if ( isPayPalSubscription( config.scriptData ) ) { + return ( + + createSubscription( data, actions, config ) + } + onApprove={ ( data, actions ) => + handleApproveSubscription( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose + ) + } + onShippingOptionsChange={ getOnShippingOptionsChange( + fundingSource + ) } + onShippingAddressChange={ getOnShippingAddressChange( + fundingSource + ) } + /> + ); + } + + return ( + + createOrder( data, config, onError, onClose ) + } + onApprove={ ( data, actions ) => + handleApprove( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose + ) + } + onShippingOptionsChange={ getOnShippingOptionsChange( + fundingSource + ) } + onShippingAddressChange={ getOnShippingAddressChange( + fundingSource + ) } + /> + ); +}; diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index b0f971dd2..3ddc2cd92 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -1,750 +1,26 @@ -import { useEffect, useState, useMemo } from '@wordpress/element'; import { registerExpressPaymentMethod, registerPaymentMethod, } from '@woocommerce/blocks-registry'; import { __ } from '@wordpress/i18n'; -import { - mergeWcAddress, - paypalAddressToWc, - paypalOrderToWcAddresses, - paypalSubscriptionToWcAddresses, -} from './Helper/Address'; -import { convertKeysToSnakeCase } from './Helper/Helper'; import { cartHasSubscriptionProducts, isPayPalSubscription, } from './Helper/Subscription'; import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; -import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; -import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style'; -import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap'; +import { PayPalComponent } from './Components/paypal'; +import { BlockEditorPayPalComponent } from './Components/block-editor-paypal'; +import { PaypalLabel } from './Components/paypal-label'; const namespace = 'ppcpBlocksPaypalExpressButtons'; const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); window.ppcpFundingSource = config.fundingSource; -let registeredContext = false; let paypalScriptPromise = null; -const PAYPAL_GATEWAY_ID = 'ppcp-gateway'; - -const PayPalComponent = ( { - onClick, - onClose, - onSubmit, - onError, - eventRegistration, - emitResponse, - activePaymentMethod, - shippingData, - isEditing, - fundingSource, - buttonAttributes, -} ) => { - const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } = - eventRegistration; - const { responseTypes } = emitResponse; - - const [ paypalOrder, setPaypalOrder ] = useState( null ); - const [ continuationFilled, setContinuationFilled ] = useState( false ); - const [ gotoContinuationOnError, setGotoContinuationOnError ] = - useState( false ); - - const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false ); - - if ( ! paypalScriptLoaded ) { - if ( ! paypalScriptPromise ) { - // for editor, since canMakePayment was not called - paypalScriptPromise = loadPayPalScript( - namespace, - config.scriptData - ); - } - paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) ); - } - - const methodId = fundingSource - ? `${ config.id }-${ fundingSource }` - : config.id; - - /** - * The block cart displays express checkout buttons. Those buttons are handled by the - * PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons"). - * - * A possible bug in WooCommerce does not use the correct payment method ID for the express - * payment buttons inside the cart, but sends the ID of the _first_ active payment method. - * - * This function uses an internal WooCommerce dispatcher method to set the correct method ID. - */ - const enforcePaymentMethodForCart = () => { - // Do nothing, unless we're handling block cart express payment buttons. - if ( 'cart-block' !== config.scriptData.context ) { - return; - } - - // Set the active payment method to PAYPAL_GATEWAY_ID. - wp.data - .dispatch( 'wc/store/payment' ) - .__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} ); - }; - - useEffect( () => { - // fill the form if in continuation (for product or mini-cart buttons) - if ( continuationFilled || ! config.scriptData.continuation?.order ) { - return; - } - - try { - const paypalAddresses = paypalOrderToWcAddresses( - config.scriptData.continuation.order - ); - const wcAddresses = wp.data - .select( 'wc/store/cart' ) - .getCustomerData(); - const addresses = mergeWcAddress( wcAddresses, paypalAddresses ); - - wp.data - .dispatch( 'wc/store/cart' ) - .setBillingAddress( addresses.billingAddress ); - - if ( shippingData.needsShipping ) { - wp.data - .dispatch( 'wc/store/cart' ) - .setShippingAddress( addresses.shippingAddress ); - } - } catch ( err ) { - // sometimes the PayPal address is missing, skip in this case. - console.log( err ); - } - - // this useEffect should run only once, but adding this in case of some kind of full re-rendering - setContinuationFilled( true ); - }, [ shippingData, continuationFilled ] ); - - const createOrder = async ( data, actions ) => { - try { - const requestBody = { - nonce: config.scriptData.ajax.create_order.nonce, - bn_code: '', - context: config.scriptData.context, - payment_method: 'ppcp-gateway', - funding_source: window.ppcpFundingSource ?? 'paypal', - createaccount: false, - ...( data?.paymentSource && { - payment_source: data.paymentSource, - } ), - }; - - const res = await fetch( - config.scriptData.ajax.create_order.endpoint, - { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( requestBody ), - } - ); - - const json = await res.json(); - - if ( ! json.success ) { - if ( json.data?.details?.length > 0 ) { - throw new Error( - json.data.details - .map( ( d ) => `${ d.issue } ${ d.description }` ) - .join( '
' ) - ); - } else if ( json.data?.message ) { - throw new Error( json.data.message ); - } - - throw new Error( config.scriptData.labels.error.generic ); - } - return json.data.id; - } catch ( err ) { - console.error( err ); - - onError( err.message ); - - onClose(); - - throw err; - } - }; - - const createSubscription = async ( data, actions ) => { - let planId = config.scriptData.subscription_plan_id; - if ( - config.scriptData - .variable_paypal_subscription_variation_from_cart !== '' - ) { - planId = - config.scriptData - .variable_paypal_subscription_variation_from_cart; - } - - return actions.subscription.create( { - plan_id: planId, - } ); - }; - - const handleApproveSubscription = async ( data, actions ) => { - try { - const subscription = await actions.subscription.get(); - - if ( subscription ) { - const addresses = - paypalSubscriptionToWcAddresses( subscription ); - - const promises = [ - // save address on server - wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { - billing_address: addresses.billingAddress, - shipping_address: addresses.shippingAddress, - } ), - ]; - if ( shouldHandleShippingInPayPal() ) { - // set address in UI - promises.push( - wp.data - .dispatch( 'wc/store/cart' ) - .setBillingAddress( addresses.billingAddress ) - ); - if ( shippingData.needsShipping ) { - promises.push( - wp.data - .dispatch( 'wc/store/cart' ) - .setShippingAddress( addresses.shippingAddress ) - ); - } - } - await Promise.all( promises ); - } - - setPaypalOrder( subscription ); - - const res = await fetch( - config.scriptData.ajax.approve_subscription.endpoint, - { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( { - nonce: config.scriptData.ajax.approve_subscription - .nonce, - order_id: data.orderID, - subscription_id: data.subscriptionID, - } ), - } - ); - - const json = await res.json(); - - if ( ! json.success ) { - if ( - typeof actions !== 'undefined' && - typeof actions.restart !== 'undefined' - ) { - return actions.restart(); - } - if ( json.data?.message ) { - throw new Error( json.data.message ); - } - - throw new Error( config.scriptData.labels.error.generic ); - } - - if ( ! shouldskipFinalConfirmation() ) { - location.href = getCheckoutRedirectUrl(); - } else { - setGotoContinuationOnError( true ); - enforcePaymentMethodForCart(); - onSubmit(); - } - } catch ( err ) { - console.error( err ); - - onError( err.message ); - - onClose(); - - throw err; - } - }; - - const getCheckoutRedirectUrl = () => { - const checkoutUrl = new URL( config.scriptData.redirect ); - // sometimes some browsers may load some kind of cached version of the page, - // so adding a parameter to avoid that - checkoutUrl.searchParams.append( - 'ppcp-continuation-redirect', - new Date().getTime().toString() - ); - return checkoutUrl.toString(); - }; - - const handleApprove = async ( data, actions ) => { - try { - const order = await actions.order.get(); - - if ( order ) { - const addresses = paypalOrderToWcAddresses( order ); - - const promises = [ - // save address on server - wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { - billing_address: addresses.billingAddress, - shipping_address: addresses.shippingAddress, - } ), - ]; - if ( shouldHandleShippingInPayPal() ) { - // set address in UI - promises.push( - wp.data - .dispatch( 'wc/store/cart' ) - .setBillingAddress( addresses.billingAddress ) - ); - if ( shippingData.needsShipping ) { - promises.push( - wp.data - .dispatch( 'wc/store/cart' ) - .setShippingAddress( addresses.shippingAddress ) - ); - } - } - await Promise.all( promises ); - } - - setPaypalOrder( order ); - - const res = await fetch( - config.scriptData.ajax.approve_order.endpoint, - { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( { - nonce: config.scriptData.ajax.approve_order.nonce, - order_id: data.orderID, - funding_source: window.ppcpFundingSource ?? 'paypal', - } ), - } - ); - - const json = await res.json(); - - if ( ! json.success ) { - if ( - typeof actions !== 'undefined' && - typeof actions.restart !== 'undefined' - ) { - return actions.restart(); - } - if ( json.data?.message ) { - throw new Error( json.data.message ); - } - - throw new Error( config.scriptData.labels.error.generic ); - } - - if ( ! shouldskipFinalConfirmation() ) { - location.href = getCheckoutRedirectUrl(); - } else { - setGotoContinuationOnError( true ); - enforcePaymentMethodForCart(); - onSubmit(); - } - } catch ( err ) { - console.error( err ); - - onError( err.message ); - - onClose(); - - throw err; - } - }; - - useEffect( () => { - const unsubscribe = onCheckoutValidation( () => { - if ( config.scriptData.continuation ) { - return true; - } - if ( - gotoContinuationOnError && - wp.data.select( 'wc/store/validation' ).hasValidationErrors() - ) { - location.href = getCheckoutRedirectUrl(); - return { type: responseTypes.ERROR }; - } - - return true; - } ); - return unsubscribe; - }, [ onCheckoutValidation, gotoContinuationOnError ] ); - - const handleClick = ( data, actions ) => { - if ( isEditing ) { - return actions.reject(); - } - - window.ppcpFundingSource = data.fundingSource; - - onClick(); - }; - - const shouldHandleShippingInPayPal = () => { - return shouldskipFinalConfirmation() && config.needShipping; - }; - - const shouldskipFinalConfirmation = () => { - if ( config.finalReviewEnabled ) { - return false; - } - - return ( - window.ppcpFundingSource !== 'venmo' || - ! config.scriptData.vaultingEnabled - ); - }; - - let handleShippingOptionsChange = null; - let handleShippingAddressChange = null; - let handleSubscriptionShippingOptionsChange = null; - let handleSubscriptionShippingAddressChange = null; - - if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) { - handleShippingOptionsChange = async ( data, actions ) => { - try { - const shippingOptionId = data.selectedShippingOption?.id; - if ( shippingOptionId ) { - await wp.data - .dispatch( 'wc/store/cart' ) - .selectShippingRate( shippingOptionId ); - await shippingData.setSelectedRates( shippingOptionId ); - } - - const res = await fetch( config.ajax.update_shipping.endpoint, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( { - nonce: config.ajax.update_shipping.nonce, - order_id: data.orderID, - } ), - } ); - - const json = await res.json(); - - if ( ! json.success ) { - throw new Error( json.data.message ); - } - } catch ( e ) { - console.error( e ); - - actions.reject(); - } - }; - - handleShippingAddressChange = async ( data, actions ) => { - try { - const address = paypalAddressToWc( - convertKeysToSnakeCase( data.shippingAddress ) - ); - - await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { - shipping_address: address, - } ); - - await shippingData.setShippingAddress( address ); - - const res = await fetch( config.ajax.update_shipping.endpoint, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( { - nonce: config.ajax.update_shipping.nonce, - order_id: data.orderID, - } ), - } ); - - const json = await res.json(); - - if ( ! json.success ) { - throw new Error( json.data.message ); - } - } catch ( e ) { - console.error( e ); - - actions.reject(); - } - }; - - handleSubscriptionShippingOptionsChange = async ( data, actions ) => { - try { - const shippingOptionId = data.selectedShippingOption?.id; - if ( shippingOptionId ) { - await wp.data - .dispatch( 'wc/store/cart' ) - .selectShippingRate( shippingOptionId ); - await shippingData.setSelectedRates( shippingOptionId ); - } - } catch ( e ) { - console.error( e ); - - actions.reject(); - } - }; - - handleSubscriptionShippingAddressChange = async ( data, actions ) => { - try { - const address = paypalAddressToWc( - convertKeysToSnakeCase( data.shippingAddress ) - ); - - await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { - shipping_address: address, - } ); - - await shippingData.setShippingAddress( address ); - - const res = await fetch( config.ajax.update_shipping.endpoint, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( { - nonce: config.ajax.update_shipping.nonce, - order_id: data.orderID, - } ), - } ); - - const json = await res.json(); - - if ( ! json.success ) { - throw new Error( json.data.message ); - } - } catch ( e ) { - console.error( e ); - - actions.reject(); - } - }; - } - - useEffect( () => { - if ( activePaymentMethod !== methodId ) { - return; - } - - const unsubscribeProcessing = onPaymentSetup( () => { - if ( config.scriptData.continuation ) { - return { - type: responseTypes.SUCCESS, - meta: { - paymentMethodData: { - paypal_order_id: - config.scriptData.continuation.order_id, - funding_source: - window.ppcpFundingSource ?? 'paypal', - }, - }, - }; - } - - const addresses = paypalOrderToWcAddresses( paypalOrder ); - - return { - type: responseTypes.SUCCESS, - meta: { - paymentMethodData: { - paypal_order_id: paypalOrder.id, - funding_source: window.ppcpFundingSource ?? 'paypal', - }, - ...addresses, - }, - }; - } ); - return () => { - unsubscribeProcessing(); - }; - }, [ onPaymentSetup, paypalOrder, activePaymentMethod ] ); - - useEffect( () => { - if ( activePaymentMethod !== methodId ) { - return; - } - const unsubscribe = onCheckoutFail( ( { processingResponse } ) => { - console.error( processingResponse ); - if ( onClose ) { - onClose(); - } - if ( config.scriptData.continuation ) { - return true; - } - if ( shouldskipFinalConfirmation() ) { - location.href = getCheckoutRedirectUrl(); - } - return true; - } ); - return unsubscribe; - }, [ onCheckoutFail, onClose, activePaymentMethod ] ); - - if ( config.scriptData.continuation ) { - return ( -
- ); - } - - if ( ! registeredContext ) { - buttonModuleWatcher.registerContextBootstrap( - config.scriptData.context, - { - createOrder: () => { - return createOrder(); - }, - onApprove: ( data, actions ) => { - return handleApprove( data, actions ); - }, - } - ); - registeredContext = true; - } - - const style = normalizeStyleForFundingSource( - config.scriptData.button.style, - fundingSource - ); - - if ( typeof buttonAttributes !== 'undefined' ) { - style.height = buttonAttributes?.height - ? Number( buttonAttributes.height ) - : style.height; - style.borderRadius = buttonAttributes?.borderRadius - ? Number( buttonAttributes.borderRadius ) - : style.borderRadius; - } - - if ( ! paypalScriptLoaded ) { - return null; - } - - const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver( - 'react', - { React, ReactDOM } - ); - - const getOnShippingOptionsChange = ( fundingSource ) => { - if ( fundingSource === 'venmo' ) { - return null; - } - - return ( data, actions ) => { - shouldHandleShippingInPayPal() - ? handleShippingOptionsChange( data, actions ) - : null; - }; - }; - - const getOnShippingAddressChange = ( fundingSource ) => { - if ( fundingSource === 'venmo' ) { - return null; - } - - return ( data, actions ) => { - const shippingAddressChange = shouldHandleShippingInPayPal() - ? handleShippingAddressChange( data, actions ) - : null; - - return shippingAddressChange; - }; - }; - - if ( isPayPalSubscription( config.scriptData ) ) { - return ( - - ); - } - - return ( - - ); -}; - -const BlockEditorPayPalComponent = ( { fundingSource, buttonAttributes } ) => { - const urlParams = useMemo( - () => ( { - clientId: 'test', - ...config.scriptData.url_params, - dataNamespace: 'ppcp-blocks-editor-paypal-buttons', - components: 'buttons', - } ), - [] - ); - - const style = useMemo( () => { - const configStyle = normalizeStyleForFundingSource( - config.scriptData.button.style, - fundingSource - ); - - if ( buttonAttributes ) { - return { - ...configStyle, - height: buttonAttributes.height - ? Number( buttonAttributes.height ) - : configStyle.height, - borderRadius: buttonAttributes.borderRadius - ? Number( buttonAttributes.borderRadius ) - : configStyle.borderRadius, - }; - } - - return configStyle; - }, [ fundingSource, buttonAttributes ] ); - - return ( - - false } - /> - - ); -}; - const features = [ 'products' ]; -let block_enabled = true; +let blockEnabled = true; if ( cartHasSubscriptionProducts( config.scriptData ) ) { // Don't show buttons on block cart page if using vault v2 and user is not logged in @@ -754,7 +30,17 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) { ! isPayPalSubscription( config.scriptData ) && // using vaulting ! config.scriptData?.save_payment_methods?.id_token // not vault v3 ) { - block_enabled = false; + blockEnabled = false; + } + + // Don't show buttons on block cart page if user is not logged in and cart contains free trial product + if ( + ! config.scriptData.user.is_logged && + config.scriptData.context === 'cart-block' && + cartHasSubscriptionProducts( config.scriptData ) && + config.scriptData.is_free_trial_cart + ) { + blockEnabled = false; } // Don't render if vaulting disabled and is in vault subscription mode @@ -762,7 +48,7 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) { ! isPayPalSubscription( config.scriptData ) && ! config.scriptData.can_save_vault_token ) { - block_enabled = false; + blockEnabled = false; } // Don't render buttons if in subscription mode and product not associated with a PayPal subscription @@ -770,13 +56,21 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) { isPayPalSubscription( config.scriptData ) && ! config.scriptData.subscription_product_allowed ) { - block_enabled = false; + blockEnabled = false; + } + + // Don't show buttons if cart contains free trial product and the stroe is not eligible for saving payment methods. + if ( + ! config.scriptData.vault_v3_enabled && + config.scriptData.is_free_trial_cart + ) { + blockEnabled = false; } features.push( 'subscriptions' ); } -if ( block_enabled ) { +if ( blockEnabled ) { if ( config.placeOrderEnabled && ! config.scriptData.continuation ) { let descriptionElement = (
{ - const { PaymentMethodIcons } = components; - - return ( - <> - - - - ); - }; - registerPaymentMethod( { name: config.id, label: , @@ -837,8 +116,13 @@ if ( block_enabled ) { registerPaymentMethod( { name: config.id, label:
, - content: , - edit: , + content: , + edit: ( + + ), ariaLabel: config.title, canMakePayment: () => { return true; @@ -848,10 +132,11 @@ if ( block_enabled ) { }, } ); } else if ( config.smartButtonsEnabled ) { - for ( const fundingSource of [ - 'paypal', - ...config.enabledFundingSources, - ] ) { + const fundingSources = config.scriptData.is_free_trial_cart + ? [ 'paypal' ] + : [ 'paypal', ...config.enabledFundingSources ]; + + for ( const fundingSource of fundingSources ) { registerExpressPaymentMethod( { name: `${ config.id }-${ fundingSource }`, title: 'PayPal', @@ -866,12 +151,14 @@ if ( block_enabled ) { ), content: ( ), edit: ( ), diff --git a/modules/ppcp-blocks/resources/js/paypal-config.js b/modules/ppcp-blocks/resources/js/paypal-config.js new file mode 100644 index 000000000..d78ee14db --- /dev/null +++ b/modules/ppcp-blocks/resources/js/paypal-config.js @@ -0,0 +1,316 @@ +import { + paypalOrderToWcAddresses, + paypalSubscriptionToWcAddresses, +} from './Helper/Address'; + +export const createOrder = async ( data, config, onError, onClose ) => { + try { + const requestBody = { + nonce: config.scriptData.ajax.create_order.nonce, + bn_code: '', + context: config.scriptData.context, + payment_method: 'ppcp-gateway', + funding_source: window.ppcpFundingSource ?? 'paypal', + createaccount: false, + ...( data?.paymentSource && { + payment_source: data.paymentSource, + } ), + }; + + const res = await fetch( config.scriptData.ajax.create_order.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify( requestBody ), + } ); + + const json = await res.json(); + + if ( ! json.success ) { + if ( json.data?.details?.length > 0 ) { + throw new Error( + json.data.details + .map( ( d ) => `${ d.issue } ${ d.description }` ) + .join( '
' ) + ); + } else if ( json.data?.message ) { + throw new Error( json.data.message ); + } + + throw new Error( config.scriptData.labels.error.generic ); + } + + return json.data.id; + } catch ( err ) { + console.error( err ); + + onError( err.message ); + + onClose(); + + throw err; + } +}; + +export const handleApprove = async ( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose +) => { + try { + const order = await actions.order.get(); + + if ( order ) { + const addresses = paypalOrderToWcAddresses( order ); + + const promises = [ + // save address on server + wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { + billing_address: addresses.billingAddress, + shipping_address: addresses.shippingAddress, + } ), + ]; + if ( shouldHandleShippingInPayPal() ) { + // set address in UI + promises.push( + wp.data + .dispatch( 'wc/store/cart' ) + .setBillingAddress( addresses.billingAddress ) + ); + if ( shippingData.needsShipping ) { + promises.push( + wp.data + .dispatch( 'wc/store/cart' ) + .setShippingAddress( addresses.shippingAddress ) + ); + } + } + await Promise.all( promises ); + } + + setPaypalOrder( order ); + + const res = await fetch( + config.scriptData.ajax.approve_order.endpoint, + { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify( { + nonce: config.scriptData.ajax.approve_order.nonce, + order_id: data.orderID, + funding_source: window.ppcpFundingSource ?? 'paypal', + } ), + } + ); + + const json = await res.json(); + + if ( ! json.success ) { + if ( + typeof actions !== 'undefined' && + typeof actions.restart !== 'undefined' + ) { + return actions.restart(); + } + if ( json.data?.message ) { + throw new Error( json.data.message ); + } + + throw new Error( config.scriptData.labels.error.generic ); + } + + if ( ! shouldskipFinalConfirmation() ) { + location.href = getCheckoutRedirectUrl(); + } else { + setGotoContinuationOnError( true ); + enforcePaymentMethodForCart(); + onSubmit(); + } + } catch ( err ) { + console.error( err ); + + onError( err.message ); + + onClose(); + + throw err; + } +}; + +export const createSubscription = async ( data, actions, config ) => { + let planId = config.scriptData.subscription_plan_id; + if ( + config.scriptData.variable_paypal_subscription_variation_from_cart !== + '' + ) { + planId = + config.scriptData.variable_paypal_subscription_variation_from_cart; + } + + return actions.subscription.create( { + plan_id: planId, + } ); +}; + +export const handleApproveSubscription = async ( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose +) => { + try { + const subscription = await actions.subscription.get(); + + if ( subscription ) { + const addresses = paypalSubscriptionToWcAddresses( subscription ); + + const promises = [ + // save address on server + wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( { + billing_address: addresses.billingAddress, + shipping_address: addresses.shippingAddress, + } ), + ]; + if ( shouldHandleShippingInPayPal() ) { + // set address in UI + promises.push( + wp.data + .dispatch( 'wc/store/cart' ) + .setBillingAddress( addresses.billingAddress ) + ); + if ( shippingData.needsShipping ) { + promises.push( + wp.data + .dispatch( 'wc/store/cart' ) + .setShippingAddress( addresses.shippingAddress ) + ); + } + } + await Promise.all( promises ); + } + + setPaypalOrder( subscription ); + + const res = await fetch( + config.scriptData.ajax.approve_subscription.endpoint, + { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify( { + nonce: config.scriptData.ajax.approve_subscription.nonce, + order_id: data.orderID, + subscription_id: data.subscriptionID, + } ), + } + ); + + const json = await res.json(); + + if ( ! json.success ) { + if ( + typeof actions !== 'undefined' && + typeof actions.restart !== 'undefined' + ) { + return actions.restart(); + } + if ( json.data?.message ) { + throw new Error( json.data.message ); + } + + throw new Error( config.scriptData.labels.error.generic ); + } + + if ( ! shouldskipFinalConfirmation() ) { + location.href = getCheckoutRedirectUrl(); + } else { + setGotoContinuationOnError( true ); + enforcePaymentMethodForCart(); + onSubmit(); + } + } catch ( err ) { + console.error( err ); + + onError( err.message ); + + onClose(); + + throw err; + } +}; + +export const createVaultSetupToken = async ( config ) => { + return fetch( config.scriptData.ajax.create_setup_token.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: config.scriptData.ajax.create_setup_token.nonce, + payment_method: 'ppcp-gateway', + } ), + } ) + .then( ( response ) => response.json() ) + .then( ( result ) => { + return result.data.id; + } ) + .catch( ( err ) => { + console.error( err ); + } ); +}; + +export const onApproveSavePayment = async ( + vaultSetupToken, + config, + onSubmit +) => { + let endpoint = + config.scriptData.ajax.create_payment_token_for_guest.endpoint; + let bodyContent = { + nonce: config.scriptData.ajax.create_payment_token_for_guest.nonce, + vault_setup_token: vaultSetupToken, + }; + + if ( config.scriptData.user.is_logged_in ) { + endpoint = config.scriptData.ajax.create_payment_token.endpoint; + + bodyContent = { + nonce: config.scriptData.ajax.create_payment_token.nonce, + vault_setup_token: vaultSetupToken, + is_free_trial_cart: config.scriptData.is_free_trial_cart, + }; + } + + const response = await fetch( endpoint, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( bodyContent ), + } ); + + const result = await response.json(); + if ( result.success === true ) { + onSubmit(); + } + + console.error( result ); +}; diff --git a/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php b/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php index c52d7e601..222d53e22 100644 --- a/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php +++ b/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php @@ -97,11 +97,16 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType { wp_register_script( 'ppcp-advanced-card-checkout-block', trailingslashit( $this->module_url ) . 'assets/js/advanced-card-checkout-block.js', - array(), + array( 'wp-i18n' ), $this->version, true ); + wp_set_script_translations( + 'ppcp-advanced-card-checkout-block', + 'woocommerce-paypal-payments' + ); + return array( 'ppcp-advanced-card-checkout-block' ); } diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index a08c20b27..fd1b93404 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -1910,7 +1910,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages $in_stock = $product->is_in_stock(); - if ( $product->is_type( 'variable' ) ) { + if ( ! $in_stock && $product->is_type( 'variable' ) ) { /** * The method is defined in WC_Product_Variable class. * diff --git a/modules/ppcp-compat/src/SettingsMapHelper.php b/modules/ppcp-compat/src/SettingsMapHelper.php index 474c6f533..8cedc104c 100644 --- a/modules/ppcp-compat/src/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/SettingsMapHelper.php @@ -5,66 +5,153 @@ * @package WooCommerce\PayPalCommerce\Compat */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Compat; +use RuntimeException; + /** - * A helper for mapping the new/old settings. + * A helper class to manage the transition between legacy and new settings. + * + * This utility provides mapping from old setting keys to new ones and retrieves + * their corresponding values from the appropriate models. The class uses lazy + * loading and caching to optimize performance during runtime. */ class SettingsMapHelper { /** - * A list of mapped settings. + * A list of settings maps containing mapping definitions. * * @var SettingsMap[] */ protected array $settings_map; + /** + * Indexed map for faster lookups, initialized lazily. + * + * @var array|null Associative array where old keys map to metadata. + */ + protected ?array $key_to_model = null; + + /** + * Cache for results of `to_array()` calls on models. + * + * @var array Associative array where keys are model IDs. + */ + protected array $model_cache = array(); + /** * Constructor. * - * @param SettingsMap[] $settings_map A list of mapped settings. + * @param SettingsMap[] $settings_map A list of settings maps containing key definitions. + * @throws RuntimeException When an old key has multiple mappings. */ public function __construct( array $settings_map ) { + $this->validate_settings_map( $settings_map ); $this->settings_map = $settings_map; } /** - * Retrieves the mapped value from the new settings. + * Validates the settings map for duplicate keys. * - * @param string $key The key. - * @return ?mixed the mapped value or Null if it doesn't exist. + * @param SettingsMap[] $settings_map The settings map to validate. + * @throws RuntimeException When an old key has multiple mappings. */ - public function mapped_value( string $key ) { - if ( ! $this->has_mapped_key( $key ) ) { - return null; - } + protected function validate_settings_map( array $settings_map ) : void { + $seen_keys = array(); - foreach ( $this->settings_map as $settings_map ) { - $mapped_key = array_search( $key, $settings_map->get_map(), true ); - $new_settings = $settings_map->get_model()->to_array(); - if ( ! empty( $new_settings[ $mapped_key ] ) ) { - return $new_settings[ $mapped_key ]; + foreach ( $settings_map as $settings_map_instance ) { + foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) { + if ( isset( $seen_keys[ $old_key ] ) ) { + throw new RuntimeException( "Duplicate mapping for legacy key '$old_key'." ); + } + $seen_keys[ $old_key ] = true; } } - - return null; } /** - * Checks if the given key exists in the new settings. + * Retrieves the value of a mapped key from the new settings. * - * @param string $key The key. - * @return bool true if the given key exists in the new settings, otherwise false. + * @param string $old_key The key from the legacy settings. + * + * @return mixed|null The value of the mapped setting, or null if not found. */ - public function has_mapped_key( string $key ) : bool { - foreach ( $this->settings_map as $settings_map ) { - if ( in_array( $key, $settings_map->get_map(), true ) ) { - return true; - } + public function mapped_value( string $old_key ) { + $this->ensure_map_initialized(); + + if ( ! isset( $this->key_to_model[ $old_key ] ) ) { + return null; } - return false; + $mapping = $this->key_to_model[ $old_key ]; + $model_id = spl_object_id( $mapping['model'] ); + + return $this->get_cached_model_value( $model_id, $mapping['new_key'], $mapping['model'] ); + } + + /** + * Determines if a given legacy key exists in the new settings. + * + * @param string $old_key The key from the legacy settings. + * + * @return bool True if the key exists in the new settings, false otherwise. + */ + public function has_mapped_key( string $old_key ) : bool { + $this->ensure_map_initialized(); + + return isset( $this->key_to_model[ $old_key ] ); + } + + /** + * Retrieves a cached model value or caches it if not already cached. + * + * @param int $model_id The unique identifier for the model object. + * @param string $new_key The key in the new settings structure. + * @param object $model The model object. + * + * @return mixed|null The value of the key in the model, or null if not found. + */ + protected function get_cached_model_value( int $model_id, string $new_key, object $model ) { + if ( ! isset( $this->model_cache[ $model_id ] ) ) { + $this->model_cache[ $model_id ] = $model->to_array(); + } + + return $this->model_cache[ $model_id ][ $new_key ] ?? null; + } + + /** + * Ensures the map of old-to-new settings is initialized. + * + * This method initializes the `key_to_model` array lazily to improve performance. + * + * @return void + */ + protected function ensure_map_initialized() : void { + if ( $this->key_to_model === null ) { + $this->initialize_key_map(); + } + } + + /** + * Initializes the indexed map of old-to-new settings keys. + * + * This method processes the provided settings maps and indexes the legacy + * keys to their corresponding metadata for efficient lookup. + * + * @return void + */ + protected function initialize_key_map() : void { + $this->key_to_model = array(); + + foreach ( $this->settings_map as $settings_map_instance ) { + foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) { + $this->key_to_model[ $old_key ] = array( + 'new_key' => $new_key, + 'model' => $settings_map_instance->get_model(), + ); + } + } } } diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index a13ebf498..01d5f8fae 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -232,6 +232,26 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul 2 ); + add_filter( + 'woocommerce_paypal_payments_rest_common_merchant_data', + function ( array $merchant_data ) use ( $c ): array { + if ( ! isset( $merchant_data['features'] ) ) { + $merchant_data['features'] = array(); + } + + $product_status = $c->get( 'googlepay.helpers.apm-product-status' ); + assert( $product_status instanceof ApmProductStatus ); + + $google_pay_enabled = $product_status->is_active(); + + $merchant_data['features']['google_pay'] = array( + 'enabled' => $google_pay_enabled, + ); + + return $merchant_data; + } + ); + return true; } } diff --git a/modules/ppcp-onboarding/resources/js/onboarding.js b/modules/ppcp-onboarding/resources/js/onboarding.js index 5a6ab333a..06197afd6 100644 --- a/modules/ppcp-onboarding/resources/js/onboarding.js +++ b/modules/ppcp-onboarding/resources/js/onboarding.js @@ -345,6 +345,10 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) { const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' ); + sandboxSwitchElement?.addEventListener( 'click', () => { + document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' ); + }); + const validate = () => { const selectors = sandboxSwitchElement.checked ? sandboxCredentialElementsSelectors diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index 434a08925..b89d17e53 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php @@ -96,7 +96,7 @@ class CreatePaymentToken implements EndpointInterface { $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); - $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, $customer_id ); + $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, (string) $customer_id ); if ( is_user_logged_in() && isset( $result->customer->id ) ) { $current_user_id = get_current_user_id(); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php index 6952feb43..0b53f0828 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php @@ -105,7 +105,7 @@ class CreateSetupToken implements EndpointInterface { $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); - $result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, $customer_id ); + $result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, (string) $customer_id ); wp_send_json_success( $result ); return true; diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index c76162193..c5a8776d0 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -66,396 +66,382 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'woocommerce_paypal_payments_gateway_migrate_on_update', function() use ( $c ) { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); - $reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); if ( $reference_transaction_enabled !== true ) { - $c->get( 'wcgateway.settings' )->set( 'vault_enabled', false ); - $c->get( 'wcgateway.settings' )->persist(); + $settings->set( 'vault_enabled', false ); + $settings->persist(); } } ); - add_filter( - 'woocommerce_paypal_payments_localized_script_data', - function( array $localized_script_data ) use ( $c ) { - if ( ! self::vault_enabled( $c ) ) { - return $localized_script_data; - } - $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); - assert( $subscriptions_helper instanceof SubscriptionHelper ); - if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) { - return $localized_script_data; - } - - $api = $c->get( 'api.user-id-token' ); - assert( $api instanceof UserIdToken ); - - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); - - return $this->add_id_token_to_script_data( $api, $logger, $localized_script_data ); - } - ); - - // Adds attributes needed to save payment method. - add_filter( - 'ppcp_create_order_request_body_data', - function( array $data, string $payment_method, array $request_data ) use ( $c ): array { - if ( ! self::vault_enabled( $c ) ) { - return $data; - } - if ( $payment_method === CreditCardGateway::ID ) { - $save_payment_method = $request_data['save_payment_method'] ?? false; - if ( $save_payment_method ) { - $data['payment_source'] = array( - 'card' => array( - 'attributes' => array( - 'vault' => array( - 'store_in_vault' => 'ON_SUCCESS', - ), - ), - ), - ); - - $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); - if ( ! $target_customer_id ) { - $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); - } - - if ( $target_customer_id ) { - $data['payment_source']['card']['attributes']['customer'] = array( - 'id' => $target_customer_id, - ); - } - } - } - - if ( $payment_method === PayPalGateway::ID ) { - $funding_source = $request_data['funding_source'] ?? null; - - if ( $funding_source && $funding_source === 'venmo' ) { - $data['payment_source'] = array( - 'venmo' => array( - 'attributes' => array( - 'vault' => array( - 'store_in_vault' => 'ON_SUCCESS', - 'usage_type' => 'MERCHANT', - 'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ), - ), - ), - ), - ); - } elseif ( $funding_source && $funding_source === 'apple_pay' ) { - $data['payment_source'] = array( - 'apple_pay' => array( - 'stored_credential' => array( - 'payment_initiator' => 'CUSTOMER', - 'payment_type' => 'RECURRING', - ), - 'attributes' => array( - 'vault' => array( - 'store_in_vault' => 'ON_SUCCESS', - ), - ), - ), - ); - } else { - $data['payment_source'] = array( - 'paypal' => array( - 'attributes' => array( - 'vault' => array( - 'store_in_vault' => 'ON_SUCCESS', - 'usage_type' => 'MERCHANT', - 'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ), - ), - ), - ), - ); - } - } - - return $data; - }, - 10, - 3 - ); - - add_action( - 'woocommerce_paypal_payments_after_order_processor', - function( WC_Order $wc_order, Order $order ) use ( $c ) { - if ( ! self::vault_enabled( $c ) ) { - return; - } - $payment_source = $order->payment_source(); - assert( $payment_source instanceof PaymentSource ); - - $payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null; - if ( $payment_vault_attributes ) { - $customer_id = $payment_vault_attributes->customer->id ?? ''; - $token_id = $payment_vault_attributes->id ?? ''; - if ( ! $customer_id || ! $token_id ) { - return; - } - - update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id ); - - $wc_payment_tokens = $c->get( 'vaulting.wc-payment-tokens' ); - assert( $wc_payment_tokens instanceof WooCommercePaymentTokens ); - - if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { - $token = new \WC_Payment_Token_CC(); - $token->set_token( $token_id ); - $token->set_user_id( $wc_order->get_customer_id() ); - $token->set_gateway_id( CreditCardGateway::ID ); - - $token->set_last4( $payment_source->properties()->last_digits ?? '' ); - $expiry = explode( '-', $payment_source->properties()->expiry ?? '' ); - $token->set_expiry_year( $expiry[0] ?? '' ); - $token->set_expiry_month( $expiry[1] ?? '' ); - $token->set_card_type( $payment_source->properties()->brand ?? '' ); - - $token->save(); - } - - if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { - switch ( $payment_source->name() ) { - case 'venmo': - $wc_payment_tokens->create_payment_token_venmo( - $wc_order->get_customer_id(), - $token_id, - $payment_source->properties()->email_address ?? '' - ); - break; - case 'apple_pay': - $wc_payment_tokens->create_payment_token_applepay( - $wc_order->get_customer_id(), - $token_id - ); - break; - case 'paypal': - default: - $wc_payment_tokens->create_payment_token_paypal( - $wc_order->get_customer_id(), - $token_id, - $payment_source->properties()->email_address ?? '' - ); - break; - } - } - } - }, - 10, - 2 - ); - - add_filter( - 'woocommerce_paypal_payments_disable_add_payment_method', - function ( bool $value ) use ( $c ): bool { - if ( ! self::vault_enabled( $c ) ) { - return $value; - } - return false; - } - ); - - add_filter( - 'woocommerce_paypal_payments_should_render_card_custom_fields', - function ( bool $value ) use ( $c ): bool { - if ( ! self::vault_enabled( $c ) ) { - return $value; - } - return false; - } - ); - add_action( - 'wp_enqueue_scripts', - function() use ( $c ) { - if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) || ! self::vault_enabled( $c ) ) { - return; + 'after_setup_theme', + function () use ( $c ) { + $settings = $c->get( 'wcgateway.settings' ); + if ( + ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) + && ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) + ) { + return true; } - $module_url = $c->get( 'save-payment-methods.module.url' ); - wp_enqueue_script( - 'ppcp-add-payment-method', - untrailingslashit( $module_url ) . '/assets/js/add-payment-method.js', - array( 'jquery' ), - $c->get( 'ppcp.asset-version' ), - true + add_filter( + 'woocommerce_paypal_payments_localized_script_data', + function ( array $localized_script_data ) use ( $c ) { + $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); + assert( $subscriptions_helper instanceof SubscriptionHelper ); + if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) { + return $localized_script_data; + } + + $api = $c->get( 'api.user-id-token' ); + assert( $api instanceof UserIdToken ); + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + return $this->add_id_token_to_script_data( $api, $logger, $localized_script_data ); + } ); - $api = $c->get( 'api.user-id-token' ); - assert( $api instanceof UserIdToken ); + // Adds attributes needed to save payment method. + add_filter( + 'ppcp_create_order_request_body_data', + function ( array $data, string $payment_method, array $request_data ) use ( $c ): array { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + if ( $payment_method === CreditCardGateway::ID ) { + if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) { + return $data; + } - try { - $target_customer_id = ''; - if ( is_user_logged_in() ) { - $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); - if ( ! $target_customer_id ) { - $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); + $save_payment_method = $request_data['save_payment_method'] ?? false; + if ( $save_payment_method ) { + $data['payment_source'] = array( + 'card' => array( + 'attributes' => array( + 'vault' => array( + 'store_in_vault' => 'ON_SUCCESS', + ), + ), + ), + ); + + $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + if ( ! $target_customer_id ) { + $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); + } + + if ( $target_customer_id ) { + $data['payment_source']['card']['attributes']['customer'] = array( + 'id' => $target_customer_id, + ); + } + } + } + + if ( $payment_method === PayPalGateway::ID ) { + if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) { + return $data; + } + + $funding_source = $request_data['funding_source'] ?? null; + + if ( $funding_source && $funding_source === 'venmo' ) { + $data['payment_source'] = array( + 'venmo' => array( + 'attributes' => array( + 'vault' => array( + 'store_in_vault' => 'ON_SUCCESS', + 'usage_type' => 'MERCHANT', + 'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ), + ), + ), + ), + ); + } elseif ( $funding_source && $funding_source === 'apple_pay' ) { + $data['payment_source'] = array( + 'apple_pay' => array( + 'stored_credential' => array( + 'payment_initiator' => 'CUSTOMER', + 'payment_type' => 'RECURRING', + ), + 'attributes' => array( + 'vault' => array( + 'store_in_vault' => 'ON_SUCCESS', + ), + ), + ), + ); + } else { + $data['payment_source'] = array( + 'paypal' => array( + 'attributes' => array( + 'vault' => array( + 'store_in_vault' => 'ON_SUCCESS', + 'usage_type' => 'MERCHANT', + 'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ), + ), + ), + ), + ); + } + } + + return $data; + }, + 10, + 3 + ); + + add_action( + 'woocommerce_paypal_payments_after_order_processor', + function ( WC_Order $wc_order, Order $order ) use ( $c ) { + $payment_source = $order->payment_source(); + assert( $payment_source instanceof PaymentSource ); + + $payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null; + if ( $payment_vault_attributes ) { + $customer_id = $payment_vault_attributes->customer->id ?? ''; + $token_id = $payment_vault_attributes->id ?? ''; + if ( ! $customer_id || ! $token_id ) { + return; + } + + update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id ); + + $wc_payment_tokens = $c->get( 'vaulting.wc-payment-tokens' ); + assert( $wc_payment_tokens instanceof WooCommercePaymentTokens ); + + if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { + $token = new \WC_Payment_Token_CC(); + $token->set_token( $token_id ); + $token->set_user_id( $wc_order->get_customer_id() ); + $token->set_gateway_id( CreditCardGateway::ID ); + + $token->set_last4( $payment_source->properties()->last_digits ?? '' ); + $expiry = explode( '-', $payment_source->properties()->expiry ?? '' ); + $token->set_expiry_year( $expiry[0] ?? '' ); + $token->set_expiry_month( $expiry[1] ?? '' ); + $token->set_card_type( $payment_source->properties()->brand ?? '' ); + + $token->save(); + } + + if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { + switch ( $payment_source->name() ) { + case 'venmo': + $wc_payment_tokens->create_payment_token_venmo( + $wc_order->get_customer_id(), + $token_id, + $payment_source->properties()->email_address ?? '' + ); + break; + case 'apple_pay': + $wc_payment_tokens->create_payment_token_applepay( + $wc_order->get_customer_id(), + $token_id + ); + break; + case 'paypal': + default: + $wc_payment_tokens->create_payment_token_paypal( + $wc_order->get_customer_id(), + $token_id, + $payment_source->properties()->email_address ?? '' + ); + break; + } + } + } + }, + 10, + 2 + ); + + add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' ); + add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' ); + + add_action( + 'wp_enqueue_scripts', + function () use ( $c ) { + if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) ) { + return; + } + + $module_url = $c->get( 'save-payment-methods.module.url' ); + wp_enqueue_script( + 'ppcp-add-payment-method', + untrailingslashit( $module_url ) . '/assets/js/add-payment-method.js', + array( 'jquery' ), + $c->get( 'ppcp.asset-version' ), + true + ); + + $api = $c->get( 'api.user-id-token' ); + assert( $api instanceof UserIdToken ); + + try { + $target_customer_id = ''; + if ( is_user_logged_in() ) { + $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + if ( ! $target_customer_id ) { + $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); + } + } + + $id_token = $api->id_token( $target_customer_id ); + + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $verification_method = + $settings->has( '3d_secure_contingency' ) + ? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) ) + : ''; + + $change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification + + wp_localize_script( + 'ppcp-add-payment-method', + 'ppcp_add_payment_method', + array( + 'client_id' => $c->get( 'button.client_id' ), + 'merchant_id' => $c->get( 'api.merchant_id' ), + 'id_token' => $id_token, + 'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ), + 'view_subscriptions_page' => wc_get_account_endpoint_url( 'view-subscription' ), + 'is_subscription_change_payment_page' => $this->is_subscription_change_payment_method_page(), + 'subscription_id_to_change_payment' => $this->is_subscription_change_payment_method_page() ? (int) $change_payment_method : 0, + 'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ), + 'verification_method' => $verification_method, + 'ajax' => array( + 'create_setup_token' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ), + 'nonce' => wp_create_nonce( CreateSetupToken::nonce() ), + ), + 'create_payment_token' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ), + 'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ), + ), + 'subscription_change_payment_method' => array( + 'endpoint' => \WC_AJAX::get_endpoint( SubscriptionChangePaymentMethod::ENDPOINT ), + 'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ), + ), + ), + 'labels' => array( + 'error' => array( + 'generic' => __( + 'Something went wrong. Please try again or choose another payment source.', + 'woocommerce-paypal-payments' + ), + ), + ), + ) + ); + } catch ( RuntimeException $exception ) { + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger->error( $error ); } } + ); - $id_token = $api->id_token( $target_customer_id ); + add_action( + 'woocommerce_add_payment_method_form_bottom', + function () { + if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) { + return; + } - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - - $verification_method = - $settings->has( '3d_secure_contingency' ) - ? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) ) - : ''; - - $change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification - - wp_localize_script( - 'ppcp-add-payment-method', - 'ppcp_add_payment_method', - array( - 'client_id' => $c->get( 'button.client_id' ), - 'merchant_id' => $c->get( 'api.merchant_id' ), - 'id_token' => $id_token, - 'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ), - 'view_subscriptions_page' => wc_get_account_endpoint_url( 'view-subscription' ), - 'is_subscription_change_payment_page' => $this->is_subscription_change_payment_method_page(), - 'subscription_id_to_change_payment' => $this->is_subscription_change_payment_method_page() ? (int) $change_payment_method : 0, - 'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ), - 'verification_method' => $verification_method, - 'ajax' => array( - 'create_setup_token' => array( - 'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ), - 'nonce' => wp_create_nonce( CreateSetupToken::nonce() ), - ), - 'create_payment_token' => array( - 'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ), - 'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ), - ), - 'subscription_change_payment_method' => array( - 'endpoint' => \WC_AJAX::get_endpoint( SubscriptionChangePaymentMethod::ENDPOINT ), - 'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ), - ), - ), - 'labels' => array( - 'error' => array( - 'generic' => __( - 'Something went wrong. Please try again or choose another payment source.', - 'woocommerce-paypal-payments' - ), - ), - ), - ) - ); - } catch ( RuntimeException $exception ) { - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); - - $error = $exception->getMessage(); - if ( is_a( $exception, PayPalApiException::class ) ) { - $error = $exception->get_details( $error ); + echo '
'; } + ); - $logger->error( $error ); - } - } - ); + add_action( + 'wc_ajax_' . CreateSetupToken::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' ); + assert( $endpoint instanceof CreateSetupToken ); - add_action( - 'woocommerce_add_payment_method_form_bottom', - function () use ( $c ) { - if ( ! is_user_logged_in() || ! is_add_payment_method_page() || ! self::vault_enabled( $c ) ) { - return; - } - - echo '
'; - } - ); - - add_action( - 'wc_ajax_' . CreateSetupToken::ENDPOINT, - static function () use ( $c ) { - if ( ! self::vault_enabled( $c ) ) { - return; - } - $endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' ); - assert( $endpoint instanceof CreateSetupToken ); - - $endpoint->handle_request(); - } - ); - - add_action( - 'wc_ajax_' . CreatePaymentToken::ENDPOINT, - static function () use ( $c ) { - if ( ! self::vault_enabled( $c ) ) { - return; - } - $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' ); - assert( $endpoint instanceof CreatePaymentToken ); - - $endpoint->handle_request(); - } - ); - - add_action( - 'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT, - static function () use ( $c ) { - if ( ! self::vault_enabled( $c ) ) { - return; - } - $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' ); - assert( $endpoint instanceof CreatePaymentTokenForGuest ); - - $endpoint->handle_request(); - } - ); - - add_action( - 'woocommerce_paypal_payments_before_delete_payment_token', - function( string $token_id ) use ( $c ) { - if ( ! self::vault_enabled( $c ) ) { - return; - } - try { - $endpoint = $c->get( 'api.endpoint.payment-tokens' ); - assert( $endpoint instanceof PaymentTokensEndpoint ); - - $endpoint->delete( $token_id ); - } catch ( RuntimeException $exception ) { - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); - - $error = $exception->getMessage(); - if ( is_a( $exception, PayPalApiException::class ) ) { - $error = $exception->get_details( $error ); + $endpoint->handle_request(); } + ); - $logger->error( $error ); - } - } - ); + add_action( + 'wc_ajax_' . CreatePaymentToken::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' ); + assert( $endpoint instanceof CreatePaymentToken ); - add_filter( - 'woocommerce_paypal_payments_credit_card_gateway_supports', - function( array $supports ) use ( $c ): array { - if ( ! self::vault_enabled( $c ) ) { - return $supports; - } - $supports[] = 'tokenization'; - $supports[] = 'add_payment_method'; + $endpoint->handle_request(); + } + ); - return $supports; - } - ); + add_action( + 'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' ); + assert( $endpoint instanceof CreatePaymentTokenForGuest ); - add_filter( - 'woocommerce_paypal_payments_save_payment_methods_eligible', - function( bool $value ) use ( $c ): bool { - if ( ! self::vault_enabled( $c ) ) { - return $value; - } - return true; + $endpoint->handle_request(); + } + ); + + add_action( + 'woocommerce_paypal_payments_before_delete_payment_token', + function( string $token_id ) use ( $c ) { + try { + $endpoint = $c->get( 'api.endpoint.payment-tokens' ); + assert( $endpoint instanceof PaymentTokensEndpoint ); + + $endpoint->delete( $token_id ); + } catch ( RuntimeException $exception ) { + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger->error( $error ); + } + } + ); + + add_filter( + 'woocommerce_paypal_payments_credit_card_gateway_supports', + function( array $supports ) use ( $c ): array { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof ContainerInterface ); + + if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ) { + $supports[] = 'tokenization'; + $supports[] = 'add_payment_method'; + } + + return $supports; + } + ); + + add_filter( + 'woocommerce_paypal_payments_save_payment_methods_eligible', + function() { + return true; + } + ); } ); @@ -502,24 +488,4 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut return $localized_script_data; } - - /** - * Checks whether the vault functionality is enabled based on configuration settings. - * - * @param ContainerInterface $container The dependency injection container from which settings can be retrieved. - * - * @return bool Returns true if either 'vault_enabled' or 'vault_enabled_dcc' settings are enabled; otherwise, false. - */ - private static function vault_enabled( ContainerInterface $container ): bool { - $settings = $container->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - - if ( - ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) - && ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) - ) { - return false; - } - return true; - } } diff --git a/modules/ppcp-settings/package.json b/modules/ppcp-settings/package.json index 0a1391b88..47e69347c 100644 --- a/modules/ppcp-settings/package.json +++ b/modules/ppcp-settings/package.json @@ -7,14 +7,13 @@ "build": "wp-scripts build --webpack-src-dir=resources/js --output-path=assets" }, "devDependencies": { - "@woocommerce/navigation": "~8.1.0", "@wordpress/data": "^10.10.0", "@wordpress/data-controls": "^4.10.0", - "@wordpress/scripts": "^30.3.0" + "@wordpress/scripts": "^30.3.0", + "classnames": "^2.5.1" }, "dependencies": { "@paypal/react-paypal-js": "^8.7.0", - "@woocommerce/settings": "^1.0.0", "react-select": "^5.8.3" } } diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss index 73b656f3a..10f427ea9 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -10,6 +10,7 @@ $color-gray-500: #BBBBBB; $color-gray-400: #CCCCCC; $color-gray-300: #EBEBEB; $color-gray-200: #E0E0E0; +$color-gray-100: #F0F0F0; $color-gray: #646970; $color-text-tertiary: #505050; $color-text-text: #070707; @@ -27,6 +28,8 @@ $max-width-settings: 938px; $card-vertical-gap: 48px; +/* define custom theming options */ + :root { --ppcp-color-app-bg: #{$color-white}; } @@ -37,4 +40,18 @@ $card-vertical-gap: 48px; --max-width-onboarding-content: #{$max-width-onboarding-content}; --max-container-width: var(--max-width-settings); + + --color-black: #{$color-black}; + --color-white: #{$color-white}; + --color-blueberry: #{$color-blueberry}; + --color-gray-900: #{$color-gray-900}; + --color-gray-800: #{$color-gray-800}; + --color-gray-700: #{$color-gray-700}; + --color-gray-600: #{$color-gray-600}; + --color-gray-500: #{$color-gray-500}; + --color-gray-400: #{$color-gray-400}; + --color-gray-300: #{$color-gray-300}; + --color-gray-200: #{$color-gray-200}; + --color-gray-100: #{$color-gray-100}; + --color-gradient-dark: #{$color-gradient-dark}; } diff --git a/modules/ppcp-settings/resources/css/components/_app.scss b/modules/ppcp-settings/resources/css/components/_app.scss new file mode 100644 index 000000000..7e69cbada --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/_app.scss @@ -0,0 +1,22 @@ +/** + * Global app-level styles + */ + +.ppcp-r-app.loading { + height: 400px; + width: 400px; + position: absolute; + left: 50%; + transform: translate(-50%, 0); + text-align: center; + + .ppcp-r-spinner-overlay { + display: flex; + flex-direction: column; + justify-content: center; + } + + .ppcp-r-spinner-overlay__message { + transform: translate(0, 32px) + } +} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss new file mode 100644 index 000000000..4254320aa --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss @@ -0,0 +1,10 @@ +.ppcp-r-busy-wrapper { + position: relative; + + &.ppcp--is-loading { + pointer-events: none; + user-select: none; + + --spinner-overlay-color: #fff4; + } +} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 4174e6a23..558ccaaf2 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -1,48 +1,102 @@ +%button-style-default { + background-color: var(--button-background); + color: var(--button-color); + box-shadow: inset 0 0 0 1px var(--button-border-color); +} + +%button-style-hover { + background-color: var(--button-hover-background); + color: var(--button-hover-color); + box-shadow: inset 0 0 0 1px var(--button-hover-border-color); +} + +%button-style-disabled { + background-color: var(--button-disabled-background); + color: var(--button-disabled-color); + box-shadow: inset 0 0 0 1px var(--button-disabled-border-color); +} + +%button-shape-pill { + border-radius: 50px; + padding: 15px 32px; + height: auto; +} + button.components-button, a.components-button { - &.is-primary, &.is-secondary { - &:not(:disabled) { - background-color: $color-black; - } + /* default theme */ + --button-color: var(--color-gray-900); + --button-background: transparent; + --button-border-color: transparent; - &:disabled { - color: $color-gray-700; - } + --button-hover-color: var(--button-color); + --button-hover-background: var(--button-background); + --button-hover-border-color: var(--button-border-color); - border-radius: 50px; - padding: 15px 32px; - height: auto; + --button-disabled-color: var(--color-gray-500); + --button-disabled-background: transparent; + --button-disabled-border-color: transparent; + + /* style the button template */ + + &:not(:disabled) { + @extend %button-style-default; + } + + &:hover { + @extend %button-style-hover; + } + + &:disabled { + @extend %button-style-disabled; + } + + /* + ---------------------------------------------- + Customize variants using the theming variables + */ + + &.is-primary, + &.is-secondary { + @extend %button-shape-pill; } &.is-primary { @include font(14, 18, 900); - &:not(:disabled) { - background-color: $color-blueberry; - color: $color-white; - } + --button-color: #{$color-white}; + --button-background: #{$color-blueberry}; + + --button-disabled-color: #{$color-gray-100}; + --button-disabled-background: #{$color-gray-500}; } - &.is-secondary:not(:disabled) { - border-color: $color-blueberry; - background-color: $color-white; - color: $color-blueberry; + &.is-secondary { + --button-color: #{$color-blueberry}; + --button-background: #{$color-white}; + --button-border-color: #{$color-blueberry}; - &:hover { - background-color: $color-white; - background: none; - } + --button-disabled-color: #{$color-gray-600}; + --button-disabled-background: #{$color-gray-100}; + --button-disabled-border-color: #{$color-gray-400}; } &.is-tertiary { - color: $color-blueberry; - - &:hover { - color: $color-gradient-dark; - } + --button-color: #{$color-blueberry}; + --button-hover-color: #{$color-gradient-dark}; &:focus:not(:disabled) { border: none; box-shadow: none; } } + + &.small-button { + @include small-button; + } +} + +.ppcp--is-loading { + button.components-button, a.components-button { + @extend %button-style-disabled; + } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_select-box.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_select-box.scss index fbfb2c7e0..410d9e9d0 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_select-box.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_select-box.scss @@ -57,6 +57,14 @@ &__content { display: flex; + position: relative; + pointer-events: none; + *:not(a){ + pointer-events: none; + } + a { + pointer-events: all; + } } &__title { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss index e5157a862..af4d264ad 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss @@ -31,10 +31,4 @@ &__toggled-content { margin-top: 24px; } - - &.ppcp--is-loading { - pointer-events: none; - - --spinner-overlay-color: #fff4; - } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss index 2f32b118f..8f5e136e9 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss @@ -12,5 +12,6 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); + margin: 0; } } diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 1d8852152..cf1b48975 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -11,10 +11,6 @@ } // Todo List and Feature Items -.ppcp-r-tab-overview-todo { - margin: 0 0 48px 0; -} - .ppcp-r-todo-item { position: relative; display: flex; @@ -109,6 +105,8 @@ span { font-weight: 500; } + + margin-top:24px; } } @@ -239,6 +237,10 @@ } // Settings Card and Block Styles +.ppcp-r-settings-card { + margin: 0 0 48px 0; +} + .ppcp-r-settings-card__content { > .ppcp-r-settings-block { &:not(:last-child) { diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss index 47af9c99a..450251b6f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss @@ -20,12 +20,6 @@ margin: 0 0 24px 0; } - .ppcp-r-toggle-block__toggled-content > button{ - @include small-button; - color: $color-white; - border: none; - } - .client-id-error { color: #cc1818; margin: -16px 0 24px; diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 7ad7aa7a4..56fe55a62 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -3,10 +3,11 @@ #ppcp-settings-container { @import './global'; - @import './components/reusable-components/onboarding-header'; + @import './components/reusable-components/busy-state'; @import './components/reusable-components/button'; - @import './components/reusable-components/settings-toggle-block'; @import './components/reusable-components/separator'; + @import './components/reusable-components/onboarding-header'; + @import './components/reusable-components/settings-toggle-block'; @import './components/reusable-components/payment-method-icons'; @import "./components/reusable-components/payment-method-item"; @import './components/reusable-components/settings-wrapper'; @@ -22,6 +23,7 @@ @import './components/screens/onboarding'; @import './components/screens/settings'; @import './components/screens/overview/tab-styling'; + @import './components/app'; } @import './components/reusable-components/payment-method-modal'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index f5b071945..aff2d9997 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,8 +1,6 @@ import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; - import classNames from 'classnames'; - import { useAccordionState } from '../../hooks/useAccordionState'; // Provide defaults for all layout components so the generic version just works. @@ -24,6 +22,13 @@ const DefaultDescription = ( { children } ) => (
{ children }
); +const AccordionContent = ( { isOpen, children } ) => { + if ( ! isOpen || ! children ) { + return null; + } + return
{ children }
; +}; + const Accordion = ( { title, id = '', @@ -65,9 +70,7 @@ const Accordion = ( { ) } - { isOpen && children && ( -
{ children }
- ) } + { children }
); }; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js index 5a257b22e..24dc36134 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js @@ -1,6 +1,4 @@ import data from '../../utils/data'; -import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge'; -import { __ } from '@wordpress/i18n'; const BadgeBox = ( props ) => { const titleSize = @@ -29,12 +27,7 @@ const BadgeBox = ( props ) => {
) } - { props.textBadge && ( - - ) } + { props.textBadge }
{ props?.description && ( diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js new file mode 100644 index 000000000..959b71bfe --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js @@ -0,0 +1,68 @@ +import { + Children, + isValidElement, + cloneElement, + useMemo, + createContext, + useContext, +} from '@wordpress/element'; +import classNames from 'classnames'; + +import { CommonHooks } from '../../data'; +import SpinnerOverlay from './SpinnerOverlay'; + +// Create context to track the busy state across nested wrappers +const BusyContext = createContext( false ); + +/** + * Wraps interactive child elements and modifies their behavior based on the global `isBusy` state. + * Allows custom processing of child props via the `onBusy` callback. + * + * @param {Object} props - Component properties. + * @param {Children} props.children - Child components to wrap. + * @param {boolean} props.enabled - Enables or disables the busy-state logic. + * @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state. + * @param {string} props.className - Additional class names for the wrapper. + * @param {Function} props.onBusy - Callback to process child props when busy. + */ +const BusyStateWrapper = ( { + children, + enabled = true, + busySpinner = true, + className = '', + onBusy = () => ( { disabled: true } ), +} ) => { + const { isBusy } = CommonHooks.useBusyState(); + const hasBusyParent = useContext( BusyContext ); + + const isBusyComponent = isBusy && enabled; + const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent; + + const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, { + 'ppcp--is-loading': isBusyComponent, + } ); + + const memoizedChildren = useMemo( + () => + Children.map( children, ( child ) => + isValidElement( child ) + ? cloneElement( + child, + isBusyComponent ? onBusy( child.props ) : {} + ) + : child + ), + [ children, isBusyComponent, onBusy ] + ); + + return ( + +
+ { showSpinner && } + { memoizedChildren } +
+
+ ); +}; + +export default BusyStateWrapper; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js new file mode 100644 index 000000000..ddf3606dc --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js @@ -0,0 +1,3 @@ +export { default as openSignup } from './Icons/open-signup'; +export const NOTIFICATION_SUCCESS = '✔️'; +export const NOTIFICATION_ERROR = '❌'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js new file mode 100644 index 000000000..83c792f22 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const openSignup = ( + + + +); + +export default openSignup; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js index 733d410de..6066ac470 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js @@ -1,14 +1,13 @@ -import BadgeBox from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; + +import BadgeBox from '../BadgeBox'; import Separator from '../Separator'; -import generatePriceText from '../../../utils/badgeBoxUtils'; -import { countryPriceInfo } from '../../../utils/countryPriceInfo'; +import PricingTitleBadge from '../PricingTitleBadge'; const AcdcOptionalPaymentMethods = ( { isFastlane, isPayLater, storeCountry, - storeCurrency, } ) => { if ( isFastlane && isPayLater && storeCountry === 'US' ) { return ( @@ -24,11 +23,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-amex.svg', 'icon-button-discover.svg', ] } - textBadge={ generatePriceText( - 'ccf', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -48,11 +43,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-apple-pay.svg', 'icon-button-google-pay.svg', ] } - textBadge={ generatePriceText( - 'dw', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -69,16 +60,11 @@ const AcdcOptionalPaymentMethods = ( { 'woocommerce-paypal-payments' ) } imageBadge={ [ - 'icon-button-sepa.svg', 'icon-button-ideal.svg', 'icon-button-blik.svg', 'icon-button-bancontact.svg', ] } - textBadge={ generatePriceText( - 'apm', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -92,11 +78,9 @@ const AcdcOptionalPaymentMethods = ( { + } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -124,11 +108,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-amex.svg', 'icon-button-discover.svg', ] } - textBadge={ generatePriceText( - 'ccf', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -148,11 +128,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-apple-pay.svg', 'icon-button-google-pay.svg', ] } - textBadge={ generatePriceText( - 'dw', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -174,11 +150,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-blik.svg', 'icon-button-bancontact.svg', ] } - textBadge={ generatePriceText( - 'apm', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -205,11 +177,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-amex.svg', 'icon-button-discover.svg', ] } - textBadge={ generatePriceText( - 'ccf', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -226,11 +194,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-apple-pay.svg', 'icon-button-google-pay.svg', ] } - textBadge={ generatePriceText( - 'dw', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -252,11 +216,7 @@ const AcdcOptionalPaymentMethods = ( { 'icon-button-blik.svg', 'icon-button-bancontact.svg', ] } - textBadge={ generatePriceText( - 'apm', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js index b5e4b7d2e..4307636e2 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js @@ -1,13 +1,9 @@ -import BadgeBox from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; -import generatePriceText from '../../../utils/badgeBoxUtils'; -import { countryPriceInfo } from '../../../utils/countryPriceInfo'; -const BcdcOptionalPaymentMethods = ( { - isPayLater, - storeCountry, - storeCurrency, -} ) => { +import BadgeBox from '../BadgeBox'; +import PricingTitleBadge from '../PricingTitleBadge'; + +const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => { if ( isPayLater && storeCountry === 'us' ) { return (
@@ -22,11 +18,9 @@ const BcdcOptionalPaymentMethods = ( { 'icon-button-amex.svg', 'icon-button-discover.svg', ] } - textBadge={ generatePriceText( - 'standardCardFields', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ + + } description={ sprintf( // translators: %s: Link to PayPal REST application guide __( @@ -53,11 +47,7 @@ const BcdcOptionalPaymentMethods = ( { 'icon-button-amex.svg', 'icon-button-discover.svg', ] } - textBadge={ generatePriceText( - 'standardCardFields', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal REST application guide __( diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js index 129088f59..b83fad366 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js @@ -6,7 +6,6 @@ const OptionalPaymentMethods = ( { isFastlane, isPayLater, storeCountry, - storeCurrency, } ) => { return (
@@ -15,13 +14,11 @@ const OptionalPaymentMethods = ( { isFastlane={ isFastlane } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } /> ) : ( ) }
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodIcons.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodIcons.js index cd8016a8c..9eaac5d3f 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodIcons.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodIcons.js @@ -11,7 +11,6 @@ const PaymentMethodIcons = ( props ) => { -
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PricingDescription.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PricingDescription.js new file mode 100644 index 000000000..a41ad8f3a --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PricingDescription.js @@ -0,0 +1,38 @@ +import { __, sprintf } from '@wordpress/i18n'; + +import { countryPriceInfo } from '../../utils/countryPriceInfo'; +import { CommonHooks } from '../../data'; + +const PricingDescription = () => { + const { storeCountry } = CommonHooks.useWooSettings(); + + if ( ! countryPriceInfo[ storeCountry ] ) { + return null; + } + + const lastDate = 'October 25th, 2024'; // TODO -- needs to be the last plugin update date. + const detailsUrl = + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input'; + + const label = sprintf( + // translators: %1$s: Pricing date, %2$s Link to PayPal price-details page. + __( + 'Prices based on domestic transactions as of %1$s. Click here for full pricing details.', + 'woocommerce-paypal-payments' + ), + lastDate, + detailsUrl + ); + + return ( +

+ 1 + +

+ ); +}; + +export default PricingDescription; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PricingTitleBadge.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PricingTitleBadge.js new file mode 100644 index 000000000..5603d10e4 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PricingTitleBadge.js @@ -0,0 +1,46 @@ +import { __, sprintf } from '@wordpress/i18n'; + +import { CommonHooks } from '../../data'; +import { countryPriceInfo } from '../../utils/countryPriceInfo'; +import { formatPrice } from '../../utils/formatPrice'; +import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge'; + +const getFixedAmount = ( currency, priceList, itemFixedAmount ) => { + if ( priceList[ currency ] ) { + const sum = priceList[ currency ] + itemFixedAmount; + return formatPrice( sum, currency ); + } + + const [ defaultCurrency, defaultPrice ] = Object.entries( priceList )[ 0 ]; + const sum = defaultPrice + itemFixedAmount; + return formatPrice( sum, defaultCurrency ); +}; + +const PricingTitleBadge = ( { item } ) => { + const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + const infos = countryPriceInfo[ storeCountry ]; + const itemKey = item.split(' ')[0]; // Extract the first word, fastlane has more than one + + if ( ! infos || ! infos[ itemKey ] ) { + return null; + } + + const percentage = typeof infos[itemKey] === 'number' ? infos[itemKey].toFixed(2) : infos[itemKey]['percentage'].toFixed(2); + const itemFixedAmount = infos[itemKey]['fixedFee'] ? infos[itemKey]['fixedFee'] : 0; + const fixedAmount = getFixedAmount( storeCurrency, infos.fixedFee, itemFixedAmount ); + + const label = sprintf( + __( 'from %1$s%% + %2$s', 'woocommerce-paypal-payments' ), + percentage, + fixedAmount + ); + + return ( + 1` } + /> + ); +}; + +export default PricingTitleBadge; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/AccordionSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/AccordionSettingsBlock.js index 8a953c805..cf7f2cee4 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/AccordionSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/AccordionSettingsBlock.js @@ -9,25 +9,19 @@ import { } from './SettingsBlockElements'; const SettingsAccordion = ( { title, description, children, ...props } ) => ( - ( - - { children } - - ), - ] } - /> + + + { children } + + ); export default SettingsAccordion; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js index e2b2aeb53..6bc1521ee 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js @@ -1,34 +1,27 @@ import { Button } from '@wordpress/components'; import SettingsBlock from './SettingsBlock'; -import { Header, Title, Action, Description } from './SettingsBlockElements'; +import { Action, Description, Header, Title } from './SettingsBlockElements'; const ButtonSettingsBlock = ( { title, description, ...props } ) => ( - ( - <> -
- { title } - { description } -
- - - - - ), - ] } - /> + +
+ { title } + { description } +
+ + + +
); export default ButtonSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js index 7b6010de6..0d104a70c 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js @@ -11,56 +11,43 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => { } return ( - <> - - { notes.map( ( note, index ) => ( - { note } - ) ) } - - + + { notes.map( ( note, index ) => ( + { note } + ) ) } + ); }; return ( - ( - <> -
- - { title } - { props.actionProps?.featureStatus && ( - <TitleBadge - { ...props.actionProps?.badge } - /> - ) } - - - { description } - { printNotes() } - -
- -
- { props.actionProps?.buttons.map( - ( button ) => ( - - ) - ) } -
-
- - ), - ] } - /> + +
+ + { title } + { props.actionProps?.enabled && ( + <TitleBadge { ...props.actionProps?.badge } /> + ) } + + + { description } + { printNotes() } + +
+ +
+ { props.actionProps?.buttons.map( ( button ) => ( + + ) ) } +
+
+
); }; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/InputSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/InputSettingsBlock.js index 6f8a06e3b..3470e8b60 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/InputSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/InputSettingsBlock.js @@ -42,28 +42,20 @@ const InputSettingsBlock = ( { order = DEFAULT_ELEMENT_ORDER, ...props } ) => ( - ( - <> - { order.map( ( elementKey ) => { - const RenderElement = ELEMENT_RENDERERS[ elementKey ]; - return RenderElement ? ( - - ) : null; - } ) } - - ), - ] } - /> + + { order.map( ( elementKey ) => { + const RenderElement = ELEMENT_RENDERERS[ elementKey ]; + return RenderElement ? ( + + ) : null; + } ) } + ); export default InputSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js index 3f3827f76..cdad46b3e 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js @@ -5,56 +5,43 @@ import PaymentMethodIcon from '../PaymentMethodIcon'; import data from '../../../utils/data'; const PaymentMethodItemBlock = ( props ) => { - const [ paymentMethodState, setPaymentMethodState ] = useState(); + const [ toggleIsChecked, setToggleIsChecked ] = useState( false ); const [ modalIsVisible, setModalIsVisible ] = useState( false ); const Modal = props?.modal; - const handleCheckboxState = ( checked ) => { - setPaymentMethodState( checked ? props.id : null ); - }; - return ( <> - ( -
-
- - - { props.title } - + +
+
+ + + { props.title } + +
+

+ { props.description } +

+
+ + { Modal && ( +
setModalIsVisible( true ) } + > + { data().getImage( 'icon-settings.svg' ) }
-

- { props.description } -

-
- - { Modal && ( -
- setModalIsVisible( true ) - } - > - { data().getImage( - 'icon-settings.svg' - ) } -
- ) } -
-
- ), - ] } - /> + ) } +
+
+ { Modal && modalIsVisible && ( ) } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js index 68a18ad4b..3ffb91051 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js @@ -1,7 +1,14 @@ +import { useState, useCallback } from '@wordpress/element'; import SettingsBlock from './SettingsBlock'; import PaymentMethodItemBlock from './PaymentMethodItemBlock'; const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => { + const [ selectedMethod, setSelectedMethod ] = useState( null ); + + const handleSelect = useCallback( ( methodId, isSelected ) => { + setSelectedMethod( isSelected ? methodId : null ); + }, [] ); + if ( paymentMethods.length === 0 ) { return null; } @@ -9,19 +16,18 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => { return ( ( - <> - { paymentMethods.map( ( paymentMethod ) => ( - - ) ) } - - ), - ] } - /> + > + { paymentMethods.map( ( paymentMethod ) => ( + + handleSelect( paymentMethod.id, checked ) + } + /> + ) ) } + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/RadioSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/RadioSettingsBlock.js index 9519f127f..44270d874 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/RadioSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/RadioSettingsBlock.js @@ -11,37 +11,32 @@ const RadioSettingsBlock = ( { ( - <> -
- { title } - { description } -
- { options.map( ( option ) => ( - - props.actionProps?.callback( - props.actionProps?.key, - newValue - ) - } - label={ option.label } - description={ option.description } - toggleAdditionalContent={ true } - > - { option.additionalContent } - - ) ) } - - ), - ] } - /> + > +
+ { title } + { description } +
+ { options.map( ( option ) => ( + + props.actionProps?.callback( + props.actionProps?.key, + newValue + ) + } + label={ option.label } + description={ option.description } + toggleAdditionalContent={ true } + > + { option.additionalContent } + + ) ) } +
); export default RadioSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SelectSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SelectSettingsBlock.js index acef1d29b..5436d0921 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SelectSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SelectSettingsBlock.js @@ -35,27 +35,19 @@ const SelectSettingsBlock = ( { order = DEFAULT_ELEMENT_ORDER, ...props } ) => ( - ( - <> - { order.map( ( elementKey ) => { - const RenderElement = ELEMENT_RENDERERS[ elementKey ]; - return RenderElement ? ( - - ) : null; - } ) } - - ), - ] } - /> + + { order.map( ( elementKey ) => { + const RenderElement = ELEMENT_RENDERERS[ elementKey ]; + return RenderElement ? ( + + ) : null; + } ) } + ); export default SelectSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js index 5e4985104..768fa9387 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js @@ -1,15 +1,9 @@ -const SettingsBlock = ( { className, components = [] } ) => { +const SettingsBlock = ( { className, children } ) => { const blockClassName = [ 'ppcp-r-settings-block', className ].filter( Boolean ); - return ( -
- { components.map( ( Component, index ) => ( - - ) ) } -
- ); + return
{ children }
; }; export default SettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js index 3e0c0eac6..b66536b00 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js @@ -3,35 +3,25 @@ import SettingsBlock from './SettingsBlock'; import { Header, Title, Action, Description } from './SettingsBlockElements'; const ToggleSettingsBlock = ( { title, description, ...props } ) => ( - ( - - - props.actionProps?.callback( - props.actionProps?.key, - newValue - ) - } - /> - - ), - () => ( -
- { title && { title } } - { description && ( - { description } - ) } -
- ), - ] } - /> + + + + props.actionProps?.callback( + props.actionProps?.key, + newValue + ) + } + /> + +
+ { title && { title } } + { description && { description } } +
+
); export default ToggleSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js index d8dda1cfb..4a7cf1a20 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js @@ -1,23 +1,17 @@ import { ToggleControl } from '@wordpress/components'; import { useRef } from '@wordpress/element'; -import SpinnerOverlay from './SpinnerOverlay'; - const SettingsToggleBlock = ( { isToggled, setToggled, - isLoading = false, + disabled = false, ...props } ) => { const toggleRef = useRef( null ); const blockClasses = [ 'ppcp-r-toggle-block' ]; - if ( isLoading ) { - blockClasses.push( 'ppcp--is-loading' ); - } - const handleLabelClick = () => { - if ( ! toggleRef.current || isLoading ) { + if ( ! toggleRef.current || disabled ) { return; } @@ -52,13 +46,12 @@ const SettingsToggleBlock = ( { ref={ toggleRef } checked={ isToggled } onChange={ ( newState ) => setToggled( newState ) } - disabled={ isLoading } + disabled={ disabled } />
{ props.children && isToggled && (
- { isLoading && } { props.children }
) } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js index dec732a3e..b4165b5ba 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js @@ -1,8 +1,13 @@ import { Spinner } from '@wordpress/components'; -const SpinnerOverlay = () => { +const SpinnerOverlay = ( { message = '' } ) => { return (
+ { message && ( + + { message } + + ) }
); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabNavigation.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabNavigation.js index 4c5d40675..e22711255 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabNavigation.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabNavigation.js @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from '@wordpress/element'; import { TabPanel } from '@wordpress/components'; -import { getQuery, updateQueryString } from '@woocommerce/navigation'; + +import { getQuery, updateQueryString } from '../../utils/navigation'; const TabNavigation = ( { tabs } ) => { const { panel } = getQuery(); @@ -30,7 +31,7 @@ const TabNavigation = ( { tabs } ) => { ); useEffect( () => { - updateQueryString( { panel: activePanel }, '/', getQuery() ); + updateQueryString( { panel: activePanel } ); }, [ activePanel ] ); return ( diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js index 8d4964ce9..3f4a6ff02 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js @@ -1,17 +1,11 @@ -import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; + +import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; import Separator from '../Separator'; -import generatePriceText from '../../../utils/badgeBoxUtils'; -import { countryPriceInfo } from '../../../utils/countryPriceInfo'; - import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods'; +import PricingTitleBadge from '../PricingTitleBadge'; -const AcdcFlow = ( { - isFastlane, - isPayLater, - storeCountry, - storeCurrency, -} ) => { +const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => { if ( isFastlane && isPayLater && storeCountry === 'US' ) { return (
@@ -22,11 +16,7 @@ const AcdcFlow = ( { 'woocommerce-paypal-payments' ) } titleType={ BADGE_BOX_TITLE_BIG } - textBadge={ generatePriceText( - 'checkout', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', 'woocommerce-paypal-payments' @@ -63,10 +53,13 @@ const AcdcFlow = ( { imageBadge={ [ 'icon-payment-method-paypal-small.svg', ] } + textBadge={ + + } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://www.paypal.com/us/business/paypal-business-fees' @@ -116,7 +109,6 @@ const AcdcFlow = ( { isFastlane={ isFastlane } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } />
@@ -133,11 +125,7 @@ const AcdcFlow = ( { 'woocommerce-paypal-payments' ) } titleType={ BADGE_BOX_TITLE_BIG } - textBadge={ generatePriceText( - 'checkout', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', 'woocommerce-paypal-payments' @@ -201,7 +189,6 @@ const AcdcFlow = ( { isFastlane={ isFastlane } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } /> @@ -217,11 +204,7 @@ const AcdcFlow = ( { 'woocommerce-paypal-payments' ) } titleType={ BADGE_BOX_TITLE_BIG } - textBadge={ generatePriceText( - 'checkout', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', 'woocommerce-paypal-payments' @@ -256,7 +239,7 @@ const AcdcFlow = ( { description={ sprintf( // translators: %s: Link to PayPal REST application guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' @@ -280,7 +263,6 @@ const AcdcFlow = ( { isFastlane={ isFastlane } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } /> diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js index 325e40a5e..591412be3 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js @@ -1,11 +1,11 @@ -import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; -import Separator from '../Separator'; -import generatePriceText from '../../../utils/badgeBoxUtils'; -import { countryPriceInfo } from '../../../utils/countryPriceInfo'; -import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods'; -const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { +import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; +import Separator from '../Separator'; +import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods'; +import PricingTitleBadge from '../PricingTitleBadge'; + +const BcdcFlow = ( { isPayLater, storeCountry } ) => { if ( isPayLater && storeCountry === 'US' ) { return (
@@ -16,11 +16,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { 'woocommerce-paypal-payments' ) } titleType={ BADGE_BOX_TITLE_BIG } - textBadge={ generatePriceText( - 'checkout', - countryPriceInfo[ storeCountry ], - storeCurrency - ) } + textBadge={ } description={ __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', 'woocommerce-paypal-payments' @@ -60,7 +56,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { description={ sprintf( // translators: %s: Link to PayPal REST application guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' @@ -110,7 +106,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { isFastlane={ false } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } />
@@ -122,11 +117,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { } description={ __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', 'woocommerce-paypal-payments' @@ -158,7 +149,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { description={ sprintf( // translators: %s: Link to PayPal REST application guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' @@ -181,7 +172,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { isFastlane={ false } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } /> ); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js index b3b60fee1..cb8d2fe7b 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js @@ -1,24 +1,10 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; + +import PricingDescription from '../PricingDescription'; import AcdcFlow from './AcdcFlow'; import BcdcFlow from './BcdcFlow'; -import { Button } from '@wordpress/components'; - -const WelcomeDocs = ( { - useAcdc, - isFastlane, - isPayLater, - storeCountry, - storeCurrency, -} ) => { - const pricesBasedDescription = sprintf( - // translators: %s: Link to PayPal REST application guide - __( - '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - ); +const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => { return (

@@ -32,19 +18,14 @@ const WelcomeDocs = ( { isFastlane={ isFastlane } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } /> ) : ( ) } -

+

); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index e7891955b..6aabd15fd 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -1,96 +1,118 @@ import { __, sprintf } from '@wordpress/i18n'; import { Button, TextControl } from '@wordpress/components'; -import { useRef, useMemo } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; -import { store as noticesStore } from '@wordpress/notices'; +import { + useRef, + useState, + useEffect, + useMemo, + useCallback, +} from '@wordpress/element'; + +import classNames from 'classnames'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; import { CommonHooks } from '../../../../data'; -import { openPopup } from '../../../../utils/window'; +import { + useSandboxConnection, + useManualConnection, +} from '../../../../hooks/useHandleConnections'; + +import ConnectionButton from './ConnectionButton'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; + +const FORM_ERRORS = { + noClientId: __( + 'Please enter your Client ID', + 'woocommerce-paypal-payments' + ), + noClientSecret: __( + 'Please enter your Secret Key', + 'woocommerce-paypal-payments' + ), + invalidClientId: __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ), +}; + +const AdvancedOptionsForm = () => { + const [ clientValid, setClientValid ] = useState( false ); + const [ secretValid, setSecretValid ] = useState( false ); -const AdvancedOptionsForm = ( { setCompleted } ) => { const { isBusy } = CommonHooks.useBusyState(); - const { isSandboxMode, setSandboxMode, connectViaSandbox } = - CommonHooks.useSandbox(); + const { isSandboxMode, setSandboxMode } = useSandboxConnection(); const { + handleConnectViaIdAndSecret, isManualConnectionMode, setManualConnectionMode, clientId, setClientId, clientSecret, setClientSecret, - connectViaIdAndSecret, - } = CommonHooks.useManualConnection(); + } = useManualConnection(); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); const refClientId = useRef( null ); const refClientSecret = useRef( null ); - const isValidClientId = useMemo( () => { - return /^A[\w-]{79}$/.test( clientId ); - }, [ clientId ] ); + const validateManualConnectionForm = useCallback( () => { + const checks = [ + { + ref: refClientId, + valid: () => clientId, + errorMessage: FORM_ERRORS.noClientId, + }, + { + ref: refClientId, + valid: () => clientValid, + errorMessage: FORM_ERRORS.invalidClientId, + }, + { + ref: refClientSecret, + valid: () => clientSecret && secretValid, + errorMessage: FORM_ERRORS.noClientSecret, + }, + ]; - const isFormValid = useMemo( () => { - return isValidClientId && clientId && clientSecret; - }, [ isValidClientId, clientId, clientSecret ] ); + for ( const { ref, valid, errorMessage } of checks ) { + if ( valid() ) { + continue; + } - const handleServerError = ( res, genericMessage ) => { - console.error( 'Connection error', res ); - createErrorNotice( res?.message ?? genericMessage ); - }; - - const handleServerSuccess = () => { - createSuccessNotice( - __( 'Connected to PayPal', 'woocommerce-paypal-payments' ) - ); - setCompleted( true ); - }; - - const handleSandboxConnect = async () => { - const res = await connectViaSandbox(); - - if ( ! res.success || ! res.data ) { - handleServerError( - res, - __( - 'Could not generate a Sandbox login link.', - 'woocommerce-paypal-payments' - ) - ); - return; + ref?.current?.focus(); + throw new Error( errorMessage ); } + }, [ clientId, clientSecret, clientValid, secretValid ] ); - const connectionUrl = res.data; - const popup = openPopup( connectionUrl ); + const handleManualConnect = useCallback( + () => + handleConnectViaIdAndSecret( { + validation: validateManualConnectionForm, + } ), + [ handleConnectViaIdAndSecret, validateManualConnectionForm ] + ); - if ( ! popup ) { - createErrorNotice( - __( - 'Popup blocked. Please allow popups for this site to connect to PayPal.', - 'woocommerce-paypal-payments' - ) - ); - } - }; + useEffect( () => { + setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); + setSecretValid( clientSecret && clientSecret.length > 0 ); + }, [ clientId, clientSecret ] ); - const handleManualConnect = async () => { - const res = await connectViaIdAndSecret(); + const clientIdLabel = useMemo( + () => + isSandboxMode + ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' ) + : __( 'Live Client ID', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); - if ( res.success ) { - handleServerSuccess(); - } else { - handleServerError( - res, - __( - 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', - 'woocommerce-paypal-payments' - ) - ); - } - }; + const secretKeyLabel = useMemo( + () => + isSandboxMode + ? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ) + : __( 'Live Secret Key', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); const advancedUsersDescription = sprintf( // translators: %s: Link to PayPal REST application guide @@ -103,88 +125,84 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { return ( <> - - - - - - - { clientId && ! isValidClientId && ( -

- { __( - 'Please enter a valid Client ID', + + + - ) } - - - + + { clientValid || ( +

+ { FORM_ERRORS.invalidClientId } +

+ ) } + + +
+ ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js new file mode 100644 index 000000000..ad6a7dcef --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -0,0 +1,49 @@ +import { Button } from '@wordpress/components'; + +import classNames from 'classnames'; + +import { CommonHooks } from '../../../../data'; +import { openSignup } from '../../../ReusableComponents/Icons'; +import { + useProductionConnection, + useSandboxConnection, +} from '../../../../hooks/useHandleConnections'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; + +const ConnectionButton = ( { + title, + isSandbox = false, + variant = 'primary', + showIcon = true, + className = '', +} ) => { + const { handleSandboxConnect } = useSandboxConnection(); + const { handleProductionConnect } = useProductionConnection(); + const buttonClassName = classNames( 'ppcp-r-connection-button', className, { + 'sandbox-mode': isSandbox, + 'live-mode': ! isSandbox, + } ); + + const handleConnectClick = async () => { + if ( isSandbox ) { + await handleSandboxConnect(); + } else { + await handleProductionConnect(); + } + }; + + return ( + + + + ); +}; + +export default ConnectionButton; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js index 82bfdb656..3c12e1206 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { OnboardingHooks } from '../../../../data'; import useIsScrolled from '../../../../hooks/useIsScrolled'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { const { title, isFirst, percentage, showNext, canProceed } = stepDetails; @@ -20,7 +21,11 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { return (
-
+ -
+ { ! isFirst && NextButton( { showNext, isDisabled, onNext, onExit } ) } @@ -42,7 +47,10 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { return ( -
+ @@ -55,7 +63,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { { __( 'Continue', 'woocommerce-paypal-payments' ) } ) } -
+ ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index e59bcdeeb..225527053 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -5,8 +5,7 @@ import { getSteps, getCurrentStep } from './availableSteps'; import Navigation from './Components/Navigation'; const Onboarding = () => { - const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps(); - + const { step, setStep, flags } = OnboardingHooks.useSteps(); const Steps = getSteps( flags ); const currentStep = getCurrentStep( step, Steps ); @@ -30,7 +29,6 @@ const Onboarding = () => {
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index 5f63f923e..ed2001ac2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -1,28 +1,9 @@ import { __ } from '@wordpress/i18n'; -import { Button, Icon } from '@wordpress/components'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; +import ConnectionButton from './Components/ConnectionButton'; -const StepCompleteSetup = ( { setCompleted } ) => { - const ButtonIcon = () => ( - ( - - - - ) } - /> - ); - +const StepCompleteSetup = () => { return (
{ />
-
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js index 83ca5540f..ac56180a3 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js @@ -1,10 +1,11 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; +import { CommonHooks, OnboardingHooks } from '../../../data'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBox from '../../ReusableComponents/SelectBox'; -import { CommonHooks, OnboardingHooks } from '../../../data'; import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods'; +import PricingDescription from '../../ReusableComponents/PricingDescription'; const OPM_RADIO_GROUP_NAME = 'optional-payment-methods'; @@ -16,15 +17,6 @@ const StepPaymentMethods = ( {} ) => { const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); - const pricesBasedDescription = sprintf( - // translators: %s: Link to PayPal REST application guide - __( - '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - ); - return (
{ type="radio" > -

+
); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index 761093b24..f9b7ddea4 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -9,9 +9,11 @@ import AccordionSection from '../../ReusableComponents/AccordionSection'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; import { CommonHooks } from '../../../data'; +import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper'; + +const StepWelcome = ( { setStep, currentStep } ) => { + const { storeCountry } = CommonHooks.useWooSettings(); -const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { - const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); return (
{ 'woocommerce-paypal-payments' ) }

- + + +
{ isFastlane={ true } isPayLater={ true } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } /> { className="onboarding-advanced-options" id="advanced-options" > - + ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js index fe3e64218..07e70efea 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js @@ -1,15 +1,49 @@ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; -import { Button } from '@wordpress/components'; +import { Button, Icon } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { reusableBlock } from '@wordpress/icons'; + import SettingsCard from '../../ReusableComponents/SettingsCard'; import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock'; import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock'; import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge'; -import data from '../../../utils/data'; +import { useMerchantInfo } from '../../../data/common/hooks'; +import { STORE_NAME } from '../../../data/common'; const TabOverview = () => { const [ todos, setTodos ] = useState( [] ); const [ todosData, setTodosData ] = useState( todosDataDefault ); + const [ isRefreshing, setIsRefreshing ] = useState( false ); + + const { merchant } = useMerchantInfo(); + const { refreshFeatureStatuses } = useDispatch( STORE_NAME ); + + const features = featuresDefault.map( ( feature ) => { + const merchantFeature = merchant?.features?.[ feature.id ]; + return { + ...feature, + enabled: merchantFeature?.enabled ?? false, + }; + } ); + + const refreshHandler = async () => { + setIsRefreshing( true ); + + const result = await refreshFeatureStatuses(); + + // TODO: Implement the refresh logic, remove this debug code -- PCP-4024 + if ( result && ! result.success ) { + console.error( + 'Failed to refresh features:', + result.message || 'Unknown error' + ); + } else { + console.log( 'Features refreshed successfully.' ); + } + + setIsRefreshing( false ); + }; return (
@@ -38,39 +72,118 @@ const TabOverview = () => { className="ppcp-r-tab-overview-features" title={ __( 'Features', 'woocommerce-paypal-payments' ) } description={ -
-

{ __( 'Enable additional features…' ) }

-

{ __( 'Click Refresh…' ) }

- -
+ } - contentItems={ featuresDefault.map( ( feature ) => ( + contentItems={ features.map( ( feature ) => ( ) ) } /> + + , + , + ] } + />
); }; +// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js const todosDataDefault = [ { value: 'paypal_later_messaging', @@ -106,6 +219,7 @@ const todosDataDefault = [ }, ]; +// TODO: Hardcoding this list here is not the best idea. Can we move this to a REST API response? const featuresDefault = [ { id: 'save_paypal_and_venmo', @@ -117,6 +231,7 @@ const featuresDefault = [ buttons: [ { type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, @@ -133,7 +248,6 @@ const featuresDefault = [ 'Advanced Credit and Debit Cards', 'woocommerce-paypal-payments' ), - featureStatus: true, description: __( 'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.', 'woocommerce-paypal-payments' @@ -141,6 +255,7 @@ const featuresDefault = [ buttons: [ { type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, @@ -164,6 +279,7 @@ const featuresDefault = [ buttons: [ { type: 'secondary', + class: 'small-button', text: __( 'Apply', 'woocommerce-paypal-payments' ), url: '#', }, @@ -181,10 +297,10 @@ const featuresDefault = [ 'Let customers pay using their Google Pay wallet.', 'woocommerce-paypal-payments' ), - featureStatus: true, buttons: [ { type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, @@ -194,9 +310,6 @@ const featuresDefault = [ url: '#', }, ], - notes: [ - __( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ), - ], }, { id: 'apple_pay', @@ -208,6 +321,7 @@ const featuresDefault = [ buttons: [ { type: 'secondary', + class: 'small-button', text: __( 'Domain registration', 'woocommerce-paypal-payments' @@ -231,6 +345,7 @@ const featuresDefault = [ buttons: [ { type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, @@ -240,6 +355,9 @@ const featuresDefault = [ url: '#', }, ], + notes: [ + __( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ), + ], }, ]; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OrderIntent.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OrderIntent.js index 1d1b632fe..a53090ea1 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OrderIntent.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OrderIntent.js @@ -9,55 +9,40 @@ import { const OrderIntent = ( { updateFormValue, settings } ) => { return ( - ( - <> -
- - { __( - 'Order Intent', - 'woocommerce-paypal-payments' - ) } - - - { __( - 'Choose between immediate capture or authorization-only, with manual capture in the Order section.', - 'woocommerce-paypal-payments' - ) } - -
- - ), - () => ( - <> - + +
+ + { __( 'Order Intent', 'woocommerce-paypal-payments' ) } + + + { __( + 'Choose between immediate capture or authorization-only, with manual capture in the Order section.', + 'woocommerce-paypal-payments' + ) } + +
- - - ), - ] } - /> + + + +
); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js index f10907c4c..aefa9f44c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js @@ -1,82 +1,73 @@ import { __, sprintf } from '@wordpress/i18n'; import { + Header, SettingsBlock, ToggleSettingsBlock, Title, Description, } from '../../../../ReusableComponents/SettingsBlocks'; -import { Header } from '../../../../ReusableComponents/SettingsBlocks/SettingsBlockElements'; const SavePaymentMethods = ( { updateFormValue, settings } ) => { return ( - ( - <> -
- - { __( - 'Save payment methods', + <SettingsBlock className="ppcp-r-settings-block--save-payment-methods"> + <Header> + <Title> + { __( + 'Save payment methods', + 'woocommerce-paypal-payments' + ) } + + + { __( + "Securely store customers' payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.", + 'woocommerce-paypal-payments' + ) } + +
+ + This will disable all Pay Later features and Alternative Payment Methods on your site.', 'woocommerce-paypal-payments' - ) } - - - { __( - 'Securely store customers’ payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.', - 'woocommerce-paypal-payments' - ) } - - - - ), - () => ( - This will disable all Pay Later features and Alternative Payment Methods on your site.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', - 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' - ), - } } - /> - } - actionProps={ { - value: settings.savePaypalAndVenmo, - callback: updateFormValue, - key: 'savePaypalAndVenmo', + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', + 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' + ), } } /> - ), - () => ( - - ), - ] } - /> + } + actionProps={ { + value: settings.savePaypalAndVenmo, + callback: updateFormValue, + key: 'savePaypalAndVenmo', + } } + /> + + +
); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js deleted file mode 100644 index f53a360c7..000000000 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js +++ /dev/null @@ -1,174 +0,0 @@ -import { __ } from '@wordpress/i18n'; -import { - Header, - Title, - Description, - AccordionSettingsBlock, - ToggleSettingsBlock, - ButtonSettingsBlock, -} from '../../../../ReusableComponents/SettingsBlocks'; -import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock'; - -const Troubleshooting = ( { updateFormValue, settings } ) => { - return ( - - - ( - <> -
- - { __( - 'Subscribed PayPal webhooks', - 'woocommerce-paypal-payments' - ) } - - - { __( - 'The following PayPal webhooks are subscribed. More information about the webhooks is available in the', - 'woocommerce-paypal-payments' - ) }{ ' ' } - - { __( - 'Webhook Status documentation', - 'woocommerce-paypal-payments' - ) } - - . - -
- - - ), - ] } - /> - - - console.log( - 'Resubscribe webhooks', - 'woocommerce-paypal-payments' - ), - value: __( - 'Resubscribe webhooks', - 'woocommerce-paypal-payments' - ), - } } - /> - - - console.log( - 'Simulate webhooks', - 'woocommerce-paypal-payments' - ), - value: __( - 'Simulate webhooks', - 'woocommerce-paypal-payments' - ), - } } - /> -
- ); -}; - -const hooksExampleData = () => { - return { - url: 'https://www.rt3.tech/wordpress/paypal-ux-testin/index.php?rest_route=/paypal/v1/incoming', - hooks: [ - 'billing plan pricing-change activated', - 'billing plan updated', - 'billing subscription cancelled', - 'catalog product updated', - 'checkout order approved', - 'checkout order completed', - 'checkout payment-approval reversed', - 'payment authorization voided', - 'payment capture completed', - 'payment capture denied', - 'payment capture pending', - 'payment capture refunded', - 'payment capture reversed', - 'payment order cancelled', - 'payment sale completed', - 'payment sale refunded', - 'vault payment-token created', - 'vault payment-token deleted', - ], - }; -}; - -const HooksTable = ( { data } ) => { - return ( - - - - - - - - - - - - - -
- { __( 'URL', 'woocommerce-paypal-payments' ) } - - { __( - 'Tracked events', - 'woocommerce-paypal-payments' - ) } -
{ data?.url } - { data.hooks.map( ( hook, index ) => ( - - { hook }{ ' ' } - { index !== data.hooks.length - 1 && ',' } - - ) ) } -
- ); -}; - -export default Troubleshooting; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/HooksTableBlock.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/HooksTableBlock.js new file mode 100644 index 000000000..c86309ddd --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/HooksTableBlock.js @@ -0,0 +1,37 @@ +import { __ } from '@wordpress/i18n'; +import { CommonHooks } from '../../../../../../data'; + +const HooksTableBlock = () => { + const { webhooks } = CommonHooks.useWebhooks(); + + return ( + + + + + + + + + + + + + +
+ { __( 'URL', 'woocommerce-paypal-payments' ) } + + { __( + 'Tracked events', + 'woocommerce-paypal-payments' + ) } +
+ { webhooks?.url } +
+ ); +}; + +export default HooksTableBlock; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/ResubscribeBlock.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/ResubscribeBlock.js new file mode 100644 index 000000000..373ec57c4 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/ResubscribeBlock.js @@ -0,0 +1,72 @@ +import { useState } from '@wordpress/element'; +import { STORE_NAME } from '../../../../../../data/common'; +import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks'; +import { __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { + NOTIFICATION_ERROR, + NOTIFICATION_SUCCESS, +} from '../../../../../ReusableComponents/Icons'; + +const ResubscribeBlock = () => { + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const [ resubscribing, setResubscribing ] = useState( false ); + + const { resubscribeWebhooks } = useDispatch( STORE_NAME ); + + const startResubscribingWebhooks = async () => { + setResubscribing( true ); + try { + await resubscribeWebhooks(); + } catch ( error ) { + setResubscribing( false ); + createErrorNotice( + __( + 'Operation failed. Check WooCommerce logs for more details.', + 'woocommerce-paypal-payments' + ), + { + icon: NOTIFICATION_ERROR, + } + ); + return; + } + + setResubscribing( false ); + createSuccessNotice( + __( + 'Webhooks were successfully re-subscribed.', + 'woocommerce-paypal-payments' + ), + { + icon: NOTIFICATION_SUCCESS, + } + ); + }; + + return ( + startResubscribingWebhooks(), + value: __( + 'Resubscribe webhooks', + 'woocommerce-paypal-payments' + ), + } } + /> + ); +}; + +export default ResubscribeBlock; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/SimulationBlock.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/SimulationBlock.js new file mode 100644 index 000000000..2ae430e76 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/SimulationBlock.js @@ -0,0 +1,129 @@ +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { CommonHooks } from '../../../../../../data'; +import { + NOTIFICATION_ERROR, + NOTIFICATION_SUCCESS, +} from '../../../../../ReusableComponents/Icons'; + +const SimulationBlock = () => { + const { + createSuccessNotice, + createInfoNotice, + createErrorNotice, + removeNotice, + } = useDispatch( noticesStore ); + const { startWebhookSimulation, checkWebhookSimulationState } = + CommonHooks.useWebhooks(); + const [ simulating, setSimulating ] = useState( false ); + const sleep = ( ms ) => { + return new Promise( ( resolve ) => setTimeout( resolve, ms ) ); + }; + const startSimulation = async ( maxRetries ) => { + const webhookInfoNoticeId = 'paypal-webhook-simulation-info-notice'; + const triggerWebhookInfoNotice = () => { + createInfoNotice( + __( + 'Waiting for the webhook to arrive…', + 'woocommerce-paypal-payments' + ), + { + id: webhookInfoNoticeId, + } + ); + }; + + const stopSimulation = () => { + removeNotice( webhookInfoNoticeId ); + setSimulating( false ); + }; + + setSimulating( true ); + + triggerWebhookInfoNotice(); + + try { + await startWebhookSimulation(); + } catch ( error ) { + console.error( error ); + setSimulating( false ); + createErrorNotice( + __( + 'Operation failed. Check WooCommerce logs for more details.', + 'woocommerce-paypal-payments' + ), + { + icon: NOTIFICATION_ERROR, + } + ); + return; + } + + for ( let i = 0; i < maxRetries; i++ ) { + await sleep( 2000 ); + + const simulationStateResponse = await checkWebhookSimulationState(); + try { + if ( ! simulationStateResponse.success ) { + console.error( + 'Simulation state query failed: ' + + simulationStateResponse?.data + ); + continue; + } + + if ( simulationStateResponse?.data?.state === 'received' ) { + createSuccessNotice( + __( + 'The webhook was received successfully.', + 'woocommerce-paypal-payments' + ), + { + icon: NOTIFICATION_SUCCESS, + } + ); + stopSimulation(); + return; + } + removeNotice( webhookInfoNoticeId ); + triggerWebhookInfoNotice(); + } catch ( error ) { + console.error( error ); + } + } + stopSimulation(); + createErrorNotice( + __( + 'Looks like the webhook cannot be received. Check that your website is accessible from the internet.', + 'woocommerce-paypal-payments' + ), + { + icon: NOTIFICATION_ERROR, + } + ); + }; + + return ( + <> + startSimulation( 30 ), + value: __( + 'Simulate webhooks', + 'woocommerce-paypal-payments' + ), + } } + /> + + ); +}; +export default SimulationBlock; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/Troubleshooting.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/Troubleshooting.js new file mode 100644 index 000000000..546659b1f --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/Troubleshooting.js @@ -0,0 +1,72 @@ +import { __ } from '@wordpress/i18n'; +import { + AccordionSettingsBlock, + Description, + Header, + Title, + ToggleSettingsBlock, +} from '../../../../../ReusableComponents/SettingsBlocks'; +import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock'; + +import SimulationBlock from './SimulationBlock'; +import ResubscribeBlock from './ResubscribeBlock'; +import HooksTableBlock from './HooksTableBlock'; + +const Troubleshooting = ( { updateFormValue, settings } ) => { + return ( + + + +
+ + { __( + 'Subscribed PayPal webhooks', + 'woocommerce-paypal-payments' + ) } + + + { __( + 'The following PayPal webhooks are subscribed. More information about the webhooks is available in the', + 'woocommerce-paypal-payments' + ) }{ ' ' } + + { __( + 'Webhook Status documentation', + 'woocommerce-paypal-payments' + ) } + + . + +
+ + + +
+
+ ); +}; + +export default Troubleshooting; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js index 56a8e63c6..fd949b8c2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js @@ -5,7 +5,7 @@ import { ContentWrapper, } from '../../../ReusableComponents/SettingsBlocks'; import Sandbox from './Blocks/Sandbox'; -import Troubleshooting from './Blocks/Troubleshooting'; +import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting'; import PaypalSettings from './Blocks/PaypalSettings'; import OtherSettings from './Blocks/OtherSettings'; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js index df2983aea..7ddd8be7a 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js @@ -90,7 +90,7 @@ const TabStyling = () => { return (
- + { ); }; -const SectionIntro = () => { - const buttonStyleDescription = sprintf( - // translators: %s: Link to Classic checkout page - __( - 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Classic Checkout page. Checkout Buttons must be enabled to display the PayPal gateway on the Checkout page.' - ), - '#' - ); +const SectionIntro = ( { location } ) => { + const { description, descriptionLink } = + defaultLocationSettings[ location ]; + const buttonStyleDescription = sprintf( description, descriptionLink ); + return ( + /> ); }; @@ -321,6 +318,7 @@ const SectionButtonPreview = ( { locationSettings } ) => { clientId: 'test', merchantId: 'QTQX5NP6N9WZU', components: 'buttons,googlepay', + 'disable-funding': 'card', 'buyer-country': 'US', currency: 'USD', } } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index e0634343c..bc79b34b3 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -1,20 +1,51 @@ +import { useEffect, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; + import { OnboardingHooks } from '../../data'; +import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay'; + import Onboarding from './Onboarding/Onboarding'; import SettingsScreen from './SettingsScreen'; const Settings = () => { const onboardingProgress = OnboardingHooks.useSteps(); - if ( ! onboardingProgress.isReady ) { - // TODO: Use better loading state indicator. - return
Loading...
; - } + // Disable the "Changes you made might not be saved" browser warning. + useEffect( () => { + const suppressBeforeUnload = ( event ) => { + event.stopImmediatePropagation(); + return undefined; + }; - if ( ! onboardingProgress.completed ) { - return ; - } + window.addEventListener( 'beforeunload', suppressBeforeUnload ); - return ; + return () => { + window.removeEventListener( 'beforeunload', suppressBeforeUnload ); + }; + }, [] ); + + const wrapperClass = classNames( 'ppcp-r-app', { + loading: ! onboardingProgress.isReady, + } ); + + const Content = useMemo( () => { + if ( ! onboardingProgress.isReady ) { + return ( + + ); + } + + if ( ! onboardingProgress.completed ) { + return ; + } + + return ; + }, [ onboardingProgress ] ); + + return
{ Content }
; }; export default Settings; diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 47de76afe..ffafa7984 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -10,10 +10,22 @@ export default { // Persistent data. SET_PERSISTENT: 'COMMON:SET_PERSISTENT', + RESET: 'COMMON:RESET', HYDRATE: 'COMMON:HYDRATE', + // Activity management (advanced solution that replaces the isBusy state). + START_ACTIVITY: 'COMMON:START_ACTIVITY', + STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY', + // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN', + DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN', + DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', + DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES', + DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS', + DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION', + DO_CHECK_WEBHOOK_SIMULATION_STATE: + 'COMMON:DO_CHECK_WEBHOOK_SIMULATION_STATE', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 619aaca5f..202e3378f 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -18,6 +18,13 @@ import { STORE_NAME } from './constants'; * @property {Object?} payload - Optional payload for the action. */ +/** + * Special. Resets all values in the onboarding store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { type: ACTION_TYPES.RESET } ); + /** * Persistent. Set the full onboarding details, usually during app initialization. * @@ -52,14 +59,35 @@ export const setIsSaving = ( isSaving ) => ( { } ); /** - * Transient. Changes the "manual connection is busy" flag. + * Transient (Activity): Marks the start of an async activity + * Think of it as "setIsBusy(true)" * - * @param {boolean} isBusy + * @param {string} id Internal ID/key of the action, used to stop it again. + * @param {?string} description Optional, description for logging/debugging + * @return {?Action} The action. + */ +export const startActivity = ( id, description = null ) => { + if ( ! id || 'string' !== typeof id ) { + console.warn( 'Activity ID must be a non-empty string' ); + return null; + } + + return { + type: ACTION_TYPES.START_ACTIVITY, + payload: { id, description }, + }; +}; + +/** + * Transient (Activity): Marks the end of an async activity. + * Think of it as "setIsBusy(false)" + * + * @param {string} id Internal ID/key of the action, used to stop it again. * @return {Action} The action. */ -export const setIsBusy = ( isBusy ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isBusy }, +export const stopActivity = ( id ) => ( { + type: ACTION_TYPES.STOP_ACTIVITY, + payload: { id }, } ); /** @@ -118,17 +146,22 @@ export const persist = function* () { }; /** - * Side effect. Initiates the sandbox login ISU. + * Side effect. Fetches the ISU-login URL for a sandbox account. * * @return {Action} The action. */ -export const connectViaSandbox = function* () { - yield setIsBusy( true ); +export const connectToSandbox = function* () { + return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; +}; - const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; - yield setIsBusy( false ); - - return result; +/** + * Side effect. Fetches the ISU-login URL for a production account. + * + * @param {string[]} products Which products/features to display in the ISU popup. + * @return {Action} The action. + */ +export const connectToProduction = function* ( products = [] ) { + return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products }; }; /** @@ -140,15 +173,89 @@ export const connectViaIdAndSecret = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); - yield setIsBusy( true ); - - const result = yield { + return yield { type: ACTION_TYPES.DO_MANUAL_CONNECTION, clientId, clientSecret, useSandbox, }; - yield setIsBusy( false ); +}; + +/** + * Side effect. Clears and refreshes the merchant data via a REST request. + * + * @return {Action} The action. + */ +export const refreshMerchantData = function* () { + const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT }; + + if ( result.success && result.merchant ) { + yield hydrate( result ); + } return result; }; + +/** + * Side effect. + * Purges all feature status data via a REST request. + * Refreshes the merchant data via a REST request. + * + * @return {Action} The action. + */ +export const refreshFeatureStatuses = function* () { + const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES }; + + if ( result && result.success ) { + // TODO: Review if we can get the updated feature details in the result.data instead of + // doing a second refreshMerchantData() request. + yield refreshMerchantData(); + } + + return result; +}; + +/** + * Persistent. Changes the "webhooks" value. + * + * @param {string} webhooks + * @return {Action} The action. + */ +export const setWebhooks = ( webhooks ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { webhooks }, +} ); + +/** + * Side effect + * Refreshes subscribed webhooks via a REST request + * + * @return {Action} The action. + */ +export const resubscribeWebhooks = function* () { + const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS }; + + if ( result && result.success ) { + yield hydrate( result ); + } + + return result; +}; + +/** + * Side effect. Starts webhook simulation. + * + * @return {Action} The action. + */ +export const startWebhookSimulation = function* () { + return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION }; +}; + +/** + * Side effect. Checks webhook simulation. + * + * @return {Action} The action. + */ +export const checkWebhookSimulationState = function* () { + return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE }; +}; diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index c7ea9b4c1..a44d6f295 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -8,7 +8,7 @@ export const STORE_NAME = 'wc/paypal/common'; /** - * REST path to hydrate data of this module by loading data from the WP DB.. + * REST path to hydrate data of this module by loading data from the WP DB. * * Used by resolvers. * @@ -16,6 +16,15 @@ export const STORE_NAME = 'wc/paypal/common'; */ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common'; +/** + * REST path to fetch merchant details from the WordPress DB. + * + * Used by controls. + * + * @type {string} + */ +export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant'; + /** * REST path to persist data of this module to the WP DB. * @@ -36,11 +45,42 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual'; /** - * REST path to generate an ISU URL for the sandbox-login. + * REST path to generate an ISU URL for the PayPal-login. * * Used by: Controls * See: LoginLinkRestEndpoint.php * * @type {string} */ -export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link'; +export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link'; + +/** + * REST path to fetch webhooks data or resubscribe webhooks, + * + * Used by: Controls + * See: WebhookSettingsEndpoint.php + * + * @type {string} + */ +export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhook_settings'; + +/** + * REST path to start webhook simulation and observe the state, + * + * Used by: Controls + * See: WebhookSettingsEndpoint.php + * + * @type {string} + */ +export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhook_simulate'; + +/** + * REST path to refresh the feature status. + * + * Used by: Controls + * See: RefreshFeatureStatusEndpoint.php + * + * @type {string} + */ +export const REST_REFRESH_FEATURES_PATH = + '/wc/v3/wc_paypal/refresh-feature-status'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 6de513e0b..1bb48c334 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -10,16 +10,20 @@ import apiFetch from '@wordpress/api-fetch'; import { - REST_PERSIST_PATH, + REST_CONNECTION_URL_PATH, + REST_HYDRATE_MERCHANT_PATH, REST_MANUAL_CONNECTION_PATH, - REST_SANDBOX_CONNECTION_PATH, + REST_PERSIST_PATH, + REST_REFRESH_FEATURES_PATH, + REST_WEBHOOKS, + REST_WEBHOOKS_SIMULATE, } from './constants'; import ACTION_TYPES from './action-types'; export const controls = { async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { try { - return await apiFetch( { + await apiFetch( { path: REST_PERSIST_PATH, method: 'POST', data, @@ -30,25 +34,39 @@ export const controls = { }, async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() { - let result = null; - try { - result = await apiFetch( { - path: REST_SANDBOX_CONNECTION_PATH, + return apiFetch( { + path: REST_CONNECTION_URL_PATH, method: 'POST', data: { environment: 'sandbox', - products: [ 'EXPRESS_CHECKOUT' ], + products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT. }, } ); } catch ( e ) { - result = { + return { success: false, error: e, }; } + }, - return result; + async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) { + try { + return apiFetch( { + path: REST_CONNECTION_URL_PATH, + method: 'POST', + data: { + environment: 'production', + products, + }, + } ); + } catch ( e ) { + return { + success: false, + error: e, + }; + } }, async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( { @@ -56,10 +74,8 @@ export const controls = { clientSecret, useSandbox, } ) { - let result = null; - try { - result = await apiFetch( { + return await apiFetch( { path: REST_MANUAL_CONNECTION_PATH, method: 'POST', data: { @@ -69,12 +85,56 @@ export const controls = { }, } ); } catch ( e ) { - result = { + return { success: false, error: e, }; } + }, - return result; + async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() { + try { + return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } ); + } catch ( e ) { + return { + success: false, + error: e, + }; + } + }, + + async [ ACTION_TYPES.DO_REFRESH_FEATURES ]() { + try { + return await apiFetch( { + path: REST_REFRESH_FEATURES_PATH, + method: 'POST', + } ); + } catch ( e ) { + return { + success: false, + error: e, + message: e.message, + }; + } + }, + + async [ ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS ]() { + return await apiFetch( { + method: 'POST', + path: REST_WEBHOOKS, + } ); + }, + + async [ ACTION_TYPES.DO_START_WEBHOOK_SIMULATION ]() { + return await apiFetch( { + method: 'POST', + path: REST_WEBHOOKS_SIMULATE, + } ); + }, + + async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE ]() { + return await apiFetch( { + path: REST_WEBHOOKS_SIMULATE, + } ); }, }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index fbe6a4842..a9974eb51 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -9,7 +9,6 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { useCallback } from '@wordpress/element'; - import { STORE_NAME } from './constants'; const useTransient = ( key ) => @@ -31,8 +30,11 @@ const useHooks = () => { setManualConnectionMode, setClientId, setClientSecret, - connectViaSandbox, + connectToSandbox, + connectToProduction, connectViaIdAndSecret, + startWebhookSimulation, + checkWebhookSimulationState, } = useDispatch( STORE_NAME ); // Transient accessors. @@ -43,7 +45,11 @@ const useHooks = () => { const clientSecret = usePersistent( 'clientSecret' ); const isSandboxMode = usePersistent( 'useSandbox' ); const isManualConnectionMode = usePersistent( 'useManualConnection' ); - + const webhooks = usePersistent( 'webhooks' ); + const merchant = useSelect( + ( select ) => select( STORE_NAME ).merchant(), + [] + ); const wooSettings = useSelect( ( select ) => select( STORE_NAME ).wooSettings(), [] @@ -72,26 +78,27 @@ const useHooks = () => { setClientSecret: ( value ) => { return savePersistent( setClientSecret, value ); }, - connectViaSandbox, + connectToSandbox, + connectToProduction, connectViaIdAndSecret, + merchant, wooSettings, - }; -}; - -export const useBusyState = () => { - const { setIsBusy } = useDispatch( STORE_NAME ); - const isBusy = useTransient( 'isBusy' ); - - return { - isBusy, - setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ), + webhooks, + startWebhookSimulation, + checkWebhookSimulationState, }; }; export const useSandbox = () => { - const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks(); + const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks(); - return { isSandboxMode, setSandboxMode, connectViaSandbox }; + return { isSandboxMode, setSandboxMode, connectToSandbox }; +}; + +export const useProduction = () => { + const { connectToProduction } = useHooks(); + + return { connectToProduction }; }; export const useManualConnection = () => { @@ -118,5 +125,77 @@ export const useManualConnection = () => { export const useWooSettings = () => { const { wooSettings } = useHooks(); + return wooSettings; }; + +export const useWebhooks = () => { + const { + webhooks, + setWebhooks, + registerWebhooks, + startWebhookSimulation, + checkWebhookSimulationState, + } = useHooks(); + return { + webhooks, + setWebhooks, + registerWebhooks, + startWebhookSimulation, + checkWebhookSimulationState, + }; +}; +export const useMerchantInfo = () => { + const { merchant } = useHooks(); + const { refreshMerchantData } = useDispatch( STORE_NAME ); + + const verifyLoginStatus = useCallback( async () => { + const result = await refreshMerchantData(); + + if ( ! result.success ) { + throw new Error( result?.message || result?.error?.message ); + } + + // Verify if the server state is "connected" and we have a merchant ID. + return merchant?.isConnected && merchant?.id; + }, [ refreshMerchantData, merchant ] ); + + return { + merchant, // Merchant details + verifyLoginStatus, // Callback + }; +}; + +// -- Not using the `useHooks()` data provider -- + +export const useBusyState = () => { + const { startActivity, stopActivity } = useDispatch( STORE_NAME ); + + // Resolved value (object), contains a list of all running actions. + const activities = useSelect( + ( select ) => select( STORE_NAME ).getActivityList(), + [] + ); + + // Derive isBusy state from activities + const isBusy = Object.keys( activities ).length > 0; + + // HOC that starts and stops an activity while the callback is executed. + const withActivity = useCallback( + async ( id, description, asyncFn ) => { + startActivity( id, description ); + try { + return await asyncFn(); + } finally { + stopActivity( id ); + } + }, + [ startActivity, stopActivity ] + ); + + return { + withActivity, // HOC + isBusy, // Boolean. + activities, // Object. + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 771dfa8f5..8b5cfb9b3 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -12,23 +12,31 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, - isBusy: false, + activities: new Map(), // Read only values, provided by the server via hydrate. - wooSettings: { + merchant: Object.freeze( { + isConnected: false, + isSandbox: false, + id: '', + email: '', + } ), + + wooSettings: Object.freeze( { storeCountry: '', storeCurrency: '', - }, -}; + } ), +} ); -const defaultPersistent = { +const defaultPersistent = Object.freeze( { useSandbox: false, useManualConnection: false, clientId: '', clientSecret: '', -}; + webhooks: [], +} ); // Reducer logic. @@ -44,15 +52,53 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) => setPersistent( state, action ), + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.wooSettings = { ...state.wooSettings }; + cleanState.isReady = true; + + return cleanState; + }, + + [ ACTION_TYPES.START_ACTIVITY ]: ( state, payload ) => { + return setTransient( state, { + activities: new Map( state.activities ).set( + payload.id, + payload.description + ), + } ); + }, + + [ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload ) => { + const newActivities = new Map( state.activities ); + newActivities.delete( payload.id ); + return setTransient( state, { activities: newActivities } ); + }, + + [ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( { + ...state, + merchant: Object.freeze( { ...defaultTransient.merchant } ), + } ), + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); - if ( payload.wooSettings ) { - newState.wooSettings = { - ...newState.wooSettings, - ...payload.wooSettings, - }; - } + // Populate read-only properties. + [ 'wooSettings', 'merchant' ].forEach( ( key ) => { + if ( ! payload[ key ] ) { + return; + } + + newState[ key ] = Object.freeze( { + ...newState[ key ], + ...payload[ key ], + } ); + } ); return newState; }, diff --git a/modules/ppcp-settings/resources/js/data/common/resolvers.js b/modules/ppcp-settings/resources/js/data/common/resolvers.js index ceebca53f..157c6d8d4 100644 --- a/modules/ppcp-settings/resources/js/data/common/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/common/resolvers.js @@ -12,7 +12,7 @@ import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; -import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; +import { STORE_NAME, REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants'; export const resolvers = { /** @@ -21,6 +21,9 @@ export const resolvers = { *persistentData() { try { const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); + const webhooks = yield apiFetch( { path: REST_WEBHOOKS } ); + + result.data = { ...result.data, ...webhooks.data }; yield dispatch( STORE_NAME ).hydrate( result ); yield dispatch( STORE_NAME ).setIsReady( true ); diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 7f0b3ee20..96393942a 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -16,10 +16,24 @@ export const persistentData = ( state ) => { }; export const transientData = ( state ) => { - const { data, wooSettings, ...transientState } = getState( state ); + const { data, merchant, wooSettings, ...transientState } = + getState( state ); return transientState || EMPTY_OBJ; }; +export const getActivityList = ( state ) => { + const { activities = new Map() } = state; + return Object.fromEntries( activities ); +}; + +export const merchant = ( state ) => { + return getState( state ).merchant || EMPTY_OBJ; +}; + export const wooSettings = ( state ) => { return getState( state ).wooSettings || EMPTY_OBJ; }; + +export const webhooks = ( state ) => { + return getState( state ).webhooks || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index b292d1920..6380c6d6a 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -1,4 +1,4 @@ -import { OnboardingStoreName } from './index'; +import { OnboardingStoreName, CommonStoreName } from './index'; export const addDebugTools = ( context, modules ) => { if ( ! context || ! context?.debug ) { @@ -33,9 +33,14 @@ export const addDebugTools = ( context, modules ) => { }; context.resetStore = () => { - const onboarding = wp.data.dispatch( OnboardingStoreName ); - onboarding.reset(); - onboarding.persist(); + const stores = [ OnboardingStoreName, CommonStoreName ]; + + stores.forEach( ( storeName ) => { + const store = wp.data.dispatch( storeName ); + + store.reset(); + store.persist(); + } ); }; context.startOnboarding = () => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index b0f41d450..e8582821e 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -34,8 +34,12 @@ const useHooks = () => { setProducts, } = useDispatch( STORE_NAME ); - // Read-only flags. + // Read-only flags and derived state. const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); + const determineProducts = useSelect( + ( select ) => select( STORE_NAME ).determineProducts(), + [] + ); // Transient accessors. const isReady = useTransient( 'isReady' ); @@ -80,6 +84,7 @@ const useHooks = () => { ); return savePersistent( setProducts, validProducts ); }, + determineProducts, }; }; @@ -124,6 +129,12 @@ export const useNavigationState = () => { }; }; +export const useDetermineProducts = () => { + const { determineProducts } = useHooks(); + + return determineProducts; +}; + export const useFlags = () => { const { flags } = useHooks(); return flags; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 4ceb53f20..2b16e2416 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -12,25 +12,25 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, // Read only values, provided by the server. - flags: { + flags: Object.freeze( { canUseCasualSelling: false, canUseVaulting: false, canUseCardPayments: false, canUseSubscriptions: false, - }, -}; + } ), +} ); -const defaultPersistent = { +const defaultPersistent = Object.freeze( { completed: false, step: 0, isCasualSeller: null, // null value will uncheck both options in the UI. areOptionalPaymentMethodsEnabled: null, products: [], -}; +} ); // Reducer logic. @@ -46,15 +46,28 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => setPersistent( state, payload ), - [ ACTION_TYPES.RESET ]: ( state ) => - setPersistent( state, defaultPersistent ), + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.flags = { ...state.flags }; + cleanState.isReady = true; + + return cleanState; + }, [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); // Flags are not updated by `setPersistent()`. if ( payload.flags ) { - newState.flags = { ...newState.flags, ...payload.flags }; + newState.flags = Object.freeze( { + ...newState.flags, + ...payload.flags, + } ); } return newState; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index d4d57ef4d..2e0953437 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -23,3 +23,50 @@ export const transientData = ( state ) => { export const flags = ( state ) => { return getState( state ).flags || EMPTY_OBJ; }; + +/** + * Returns the products that we use for the production login link in the last onboarding step. + * + * This selector does not return state-values, but uses the state to derive the products-array + * that should be returned. + * + * @param {{}} state + * @return {string[]} The ISU products, based on choices made in the onboarding wizard. + */ +export const determineProducts = ( state ) => { + const derivedProducts = []; + + const { isCasualSeller, areOptionalPaymentMethodsEnabled } = + persistentData( state ); + const { canUseVaulting, canUseCardPayments } = flags( state ); + + if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) { + /** + * Branch 1: Credit Card Payments not available. + * The store uses the Express-checkout product. + */ + derivedProducts.push( 'EXPRESS_CHECKOUT' ); + } else if ( isCasualSeller ) { + /** + * Branch 2: Merchant has no business. + * The store uses the Express-checkout product. + */ + derivedProducts.push( 'EXPRESS_CHECKOUT' ); + + // TODO: Add the "BCDC" product/feature + // Requirement: "EXPRESS_CHECKOUT with BCDC" + } else { + /** + * Branch 3: Merchant is business, and can use CC payments. + * The store uses the advanced PPCP product. + */ + derivedProducts.push( 'PPCP' ); + } + + if ( canUseVaulting ) { + // TODO: Add the "Vaulting" product/feature + // Requirement: "... with Vault" + } + + return derivedProducts; +}; diff --git a/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js b/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js index 92bb32015..6bdb4f643 100644 --- a/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js +++ b/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js @@ -25,26 +25,56 @@ export const defaultLocationSettings = { value: 'cart', label: __( 'Cart', 'woocommerce-paypal-payments' ), settings: { ...cartAndExpressCheckoutSettings }, + // translators: %s: Link to Cart page + description: __( + 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Cart page and select which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + descriptionLink: '#', }, 'classic-checkout': { value: 'classic-checkout', label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), settings: { ...settings }, + // translators: %s: Link to Classic Checkout page + description: __( + 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Classic Checkout page and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + descriptionLink: '#', }, 'express-checkout': { value: 'express-checkout', label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), settings: { ...cartAndExpressCheckoutSettings }, + // translators: %s: Link to Express Checkout location + description: __( + 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Express Checkout location and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + descriptionLink: '#', }, 'mini-cart': { value: 'mini-cart', label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), settings: { ...settings }, + // translators: %s: Link to Mini Cart + description: __( + 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Mini Cart and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + descriptionLink: '#', }, 'product-page': { value: 'product-page', label: __( 'Product Page', 'woocommerce-paypal-payments' ), settings: { ...settings }, + // translators: %s: Link to Product Page + description: __( + 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Product Page and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + descriptionLink: '#', }, }; @@ -57,10 +87,6 @@ export const paymentMethodOptions = [ value: 'paylater', label: __( 'Pay Later', 'woocommerce-paypal-payments' ), }, - { - value: 'card', - label: __( 'Debit or Credit Card', 'woocommerce-paypal-payments' ), - }, { value: 'googlepay', label: __( 'Google Pay', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js new file mode 100644 index 000000000..d34e74f42 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -0,0 +1,214 @@ +import { __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; + +import { CommonHooks, OnboardingHooks } from '../data'; +import { openPopup } from '../utils/window'; + +const MESSAGES = { + CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ), + POPUP_BLOCKED: __( + 'Popup blocked. Please allow popups for this site to connect to PayPal.', + 'woocommerce-paypal-payments' + ), + SANDBOX_ERROR: __( + 'Could not generate a Sandbox login link.', + 'woocommerce-paypal-payments' + ), + PRODUCTION_ERROR: __( + 'Could not generate a login link.', + 'woocommerce-paypal-payments' + ), + MANUAL_ERROR: __( + 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', + 'woocommerce-paypal-payments' + ), + LOGIN_FAILED: __( + 'Login was not successful. Please try again.', + 'woocommerce-paypal-payments' + ), +}; + +const ACTIVITIES = { + CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX', + CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION', + CONNECT_MANUAL: 'MANUAL_LOGIN', +}; + +const handlePopupWithCompletion = ( url, onError ) => { + return new Promise( ( resolve ) => { + const popup = openPopup( url ); + + if ( ! popup ) { + onError( MESSAGES.POPUP_BLOCKED ); + resolve( false ); + return; + } + + // Check popup state every 500ms + const checkPopup = setInterval( () => { + if ( popup.closed ) { + clearInterval( checkPopup ); + resolve( true ); + } + }, 500 ); + + return () => { + clearInterval( checkPopup ); + + if ( popup && ! popup.closed ) { + popup.close(); + } + }; + } ); +}; + +const useConnectionBase = () => { + const { setCompleted } = OnboardingHooks.useSteps(); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { verifyLoginStatus } = CommonHooks.useMerchantInfo(); + + return { + handleFailed: ( res, genericMessage ) => { + console.error( 'Connection error', res ); + createErrorNotice( res?.message ?? genericMessage ); + }, + handleCompleted: async () => { + try { + const loginSuccessful = await verifyLoginStatus(); + + if ( loginSuccessful ) { + createSuccessNotice( MESSAGES.CONNECTED ); + await setCompleted( true ); + } else { + createErrorNotice( MESSAGES.LOGIN_FAILED ); + } + } catch ( error ) { + createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED ); + } + }, + createErrorNotice, + }; +}; + +const useConnectionAttempt = ( connectFn, errorMessage ) => { + const { handleFailed, createErrorNotice, handleCompleted } = + useConnectionBase(); + + return async ( ...args ) => { + const res = await connectFn( ...args ); + + if ( ! res.success || ! res.data ) { + handleFailed( res, errorMessage ); + return false; + } + + const popupClosed = await handlePopupWithCompletion( + res.data, + createErrorNotice + ); + + if ( popupClosed ) { + await handleCompleted(); + } + + return popupClosed; + }; +}; + +export const useSandboxConnection = () => { + const { connectToSandbox, isSandboxMode, setSandboxMode } = + CommonHooks.useSandbox(); + const { withActivity } = CommonHooks.useBusyState(); + const connectionAttempt = useConnectionAttempt( + connectToSandbox, + MESSAGES.SANDBOX_ERROR + ); + + const handleSandboxConnect = async () => { + return withActivity( + ACTIVITIES.CONNECT_SANDBOX, + 'Connecting to sandbox account', + connectionAttempt + ); + }; + + return { + handleSandboxConnect, + isSandboxMode, + setSandboxMode, + }; +}; + +export const useProductionConnection = () => { + const { connectToProduction } = CommonHooks.useProduction(); + const { withActivity } = CommonHooks.useBusyState(); + const products = OnboardingHooks.useDetermineProducts(); + const connectionAttempt = useConnectionAttempt( + () => connectToProduction( products ), + MESSAGES.PRODUCTION_ERROR + ); + + const handleProductionConnect = async () => { + return withActivity( + ACTIVITIES.CONNECT_PRODUCTION, + 'Connecting to production account', + connectionAttempt + ); + }; + + return { handleProductionConnect }; +}; + +export const useManualConnection = () => { + const { handleFailed, handleCompleted, createErrorNotice } = + useConnectionBase(); + const { withActivity } = CommonHooks.useBusyState(); + const { + connectViaIdAndSecret, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + } = CommonHooks.useManualConnection(); + + const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { + return withActivity( + ACTIVITIES.CONNECT_MANUAL, + 'Connecting manually via Client ID and Secret', + async () => { + if ( 'function' === typeof validation ) { + try { + validation(); + } catch ( exception ) { + createErrorNotice( exception.message ); + return; + } + } + + const res = await connectViaIdAndSecret(); + + if ( res.success ) { + await handleCompleted(); + } else { + handleFailed( res, MESSAGES.MANUAL_ERROR ); + } + + return res.success; + } + ); + }; + + return { + handleConnectViaIdAndSecret, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + }; +}; diff --git a/modules/ppcp-settings/resources/js/utils/badgeBoxUtils.js b/modules/ppcp-settings/resources/js/utils/badgeBoxUtils.js deleted file mode 100644 index 60c4da274..000000000 --- a/modules/ppcp-settings/resources/js/utils/badgeBoxUtils.js +++ /dev/null @@ -1,18 +0,0 @@ -import { __ } from '@wordpress/i18n'; - -const generatePriceText = ( type, selectedCountryPrice, storeCurrency ) => { - if ( ! selectedCountryPrice || ! selectedCountryPrice[ type ] ) { - console.warn( `Invalid type or price data for: ${ type }` ); - return ''; - } - - const percentage = selectedCountryPrice[ type ].toFixed( 2 ); - const fixedFee = `${ selectedCountryPrice.currencySymbol }${ selectedCountryPrice.fixedFee }`; - - return __( - `from ${ percentage }% + ${ fixedFee } ${ storeCurrency }1`, - 'woocommerce-paypal-payments' - ); -}; - -export default generatePriceText; diff --git a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js index 34bfc8e7f..c5cf52a3e 100644 --- a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js +++ b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js @@ -1,74 +1,140 @@ export const countryPriceInfo = { US: { - currencySymbol: '$', - fixedFee: 0.49, + fixedFee: { + USD: 0.49, + GBP: 0.39, + CAD: 0.59, + AUD: 0.59, + EUR: 0.39, + }, checkout: 3.49, - ccf: 2.59, - dw: 2.59, - apm: 2.59, - fastlane: 2.59, + plater: 4.99, + ccf: { + percentage: 2.59, + fixedFee: 0.29, + }, + dw: { + percentage: 2.59, + fixedFee: 0.29, + }, + apm: { + percentage: 2.89, + fixedFee: 0.29, + }, + fast: { + percentage: 2.59, + fixedFee: 0.29, + }, standardCardFields: 2.99, }, UK: { - currencySymbol: '£', - fixedFee: 0.3, + fixedFee: { + GPB: 0.3, + USD: 0.3, + CAD: 0.3, + AUD: 0.3, + EUR: 0.35, + }, checkout: 2.9, + plater: 2.9, ccf: 1.2, dw: 1.2, + fast: 1.2, apm: 1.2, standardCardFields: 1.2, }, CA: { - currencySymbol: '$', - fixedFee: 0.3, + fixedFee: { + CAD: 0.3, + USD: 0.3, + GBP: 0.2, + AUD: 0.3, + EUR: 0.35, + }, checkout: 2.9, ccf: 2.7, dw: 2.7, + fast: 2.7, apm: 2.9, standardCardFields: 2.9, }, AU: { - currencySymbol: '$', - fixedFee: 0.3, + fixedFee: { + AUD: 0.3, + USD: 0.3, + GBP: 0.2, + CAD: 0.3, + EUR: 0.35, + }, checkout: 2.6, + plater: 2.6, ccf: 1.75, dw: 1.75, + fast: 1.75, apm: 2.6, standardCardFields: 2.6, }, FR: { - currencySymbol: '€', - fixedFee: 0.35, + fixedFee: { + EUR: 0.35, + USD: 0.3, + GBP: 0.3, + CAD: 0.3, + AUD: 0.3, + }, checkout: 2.9, + plater: 2.9, ccf: 1.2, dw: 1.2, + fast: 1.2, apm: 1.2, standardCardFields: 1.2, }, IT: { - currencySymbol: '€', - fixedFee: 0.35, + fixedFee: { + EUR: 0.35, + USD: 0.3, + GBP: 0.3, + CAD: 0.3, + AUD: 0.3, + }, checkout: 3.4, + plater: 3.4, ccf: 1.2, dw: 1.2, + fast: 1.2, apm: 1.2, standardCardFields: 1.2, }, DE: { - currencySymbol: '€', - fixedFee: 0.39, + fixedFee: { + EUR: 0.39, + USD: 0.49, + GBP: 0.29, + CAD: 0.59, + AUD: 0.59, + }, checkout: 2.99, + plater: 2.99, ccf: 2.99, dw: 2.99, + fast: 2.99, apm: 2.99, standardCardFields: 2.99, }, ES: { - currencySymbol: '€', - fixedFee: 0.35, + fixedFee: { + EUR: 0.35, + USD: 0.3, + GBP: 0.3, + CAD: 0.3, + AUD: 0.3, + }, checkout: 2.9, + plater: 2.9, ccf: 1.2, dw: 1.2, + fast: 1.2, apm: 1.2, standardCardFields: 1.2, }, diff --git a/modules/ppcp-settings/resources/js/utils/formatPrice.js b/modules/ppcp-settings/resources/js/utils/formatPrice.js new file mode 100644 index 000000000..56e6b85d9 --- /dev/null +++ b/modules/ppcp-settings/resources/js/utils/formatPrice.js @@ -0,0 +1,34 @@ +const priceFormatInfo = { + USD: { + prefix: '$', + suffix: 'USD', + }, + CAD: { + prefix: '$', + suffix: 'CAD', + }, + AUD: { + prefix: '$', + suffix: 'AUD', + }, + EUR: { + prefix: '€', + suffix: '', + }, + GPB: { + prefix: '£', + suffix: '', + }, +}; + +export const formatPrice = ( value, currency ) => { + const currencyInfo = priceFormatInfo[ currency ]; + const amount = value.toFixed( 2 ); + + if ( ! currencyInfo ) { + console.error( `Unsupported currency: ${ currency }` ); + return amount; + } + + return `${ currencyInfo.prefix }${ amount } ${ currencyInfo.suffix }`; +}; diff --git a/modules/ppcp-settings/resources/js/utils/navigation.js b/modules/ppcp-settings/resources/js/utils/navigation.js new file mode 100644 index 000000000..f2106bc1d --- /dev/null +++ b/modules/ppcp-settings/resources/js/utils/navigation.js @@ -0,0 +1,39 @@ +import { addQueryArgs } from '@wordpress/url'; + +const getLocation = () => window.location; + +const pushHistory = ( path ) => window.history.pushState( { path }, '', path ); + +/** + * Get the current path from the browser. + * + * @return {string} Current path. + */ +export const getPath = () => getLocation().pathname; + +/** + * Get the current query string, parsed into an object, from history. + * + * @return {Object} Current query object, defaults to empty object. + */ +export const getQuery = () => + Object.fromEntries( new URLSearchParams( getLocation().search ) ); + +/** + * Updates the query parameters of the current page. + * + * @param {Object} query Object of params to be updated. + * @throws {TypeError} If the query is not an object. + */ +export const updateQueryString = ( query ) => + pushHistory( getNewPath( query ) ); + +/** + * Return a URL with set query parameters. + * + * @param {Object} query Object of params to be updated. + * @param {string} basePath Optional. Define the path for the new URL. + * @return {string} Updated URL merging query params into existing params. + */ +export const getNewPath = ( query, basePath = getPath() ) => + addQueryArgs( basePath, { ...getQuery(), ...query } ); diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 31880dca0..a3a2872b5 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -17,9 +17,13 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; +use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -37,7 +41,8 @@ return array( $can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' ); $can_use_vaulting = $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' ); $can_use_card_payments = $container->has( 'card-fields.eligible' ) && $container->get( 'card-fields.eligible' ); - $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' )->plugin_is_active(); + $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' ) + ->plugin_is_active(); // Card payments are disabled for this plugin when WooPayments is active. // TODO: Move this condition to the card-fields.eligible service? @@ -67,6 +72,13 @@ return array( 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { return new CommonRestEndpoint( $container->get( 'settings.data.common' ) ); }, + 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint { + return new RefreshFeatureStatusEndpoint( + $container->get( 'wcgateway.settings' ), + new Cache( 'ppcp-timeout' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint { return new ConnectManualRestEndpoint( $container->get( 'api.paypal-host-production' ), @@ -80,6 +92,13 @@ return array( $container->get( 'settings.service.connection-url-generators' ), ); }, + 'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint { + return new WebhookSettingsEndpoint( + $container->get( 'api.endpoint.webhook' ), + $container->get( 'webhook.registrar' ), + $container->get( 'webhook.status.simulation' ) + ); + }, 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array { return array( 'AR', @@ -136,9 +155,25 @@ return array( return in_array( $country, $eligible_countries, true ); }, + 'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener { + $page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : ''; + + return new ConnectionListener( + $page_id, + $container->get( 'settings.data.common' ), + $container->get( 'settings.service.onboarding-url-manager' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache { return new Cache( 'ppcp-paypal-signup-link' ); }, + 'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager { + return new OnboardingUrlManager( + $container->get( 'settings.service.signup-link-cache' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array { // Define available environments. $environments = array( @@ -157,8 +192,8 @@ return array( $generators[ $environment ] = new ConnectionUrlGenerator( $config['partner_referrals'], $container->get( 'api.repository.partner-referrals-data' ), - $container->get( 'settings.service.signup-link-cache' ), $environment, + $container->get( 'settings.service.onboarding-url-manager' ), $container->get( 'woocommerce.logger.woocommerce' ) ); } diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index b377b66aa..1894255ff 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -59,6 +59,12 @@ class CommonSettings extends AbstractDataModel { 'use_manual_connection' => false, 'client_id' => '', 'client_secret' => '', + + // Details about connected merchant account. + 'merchant_connected' => false, + 'sandbox_merchant' => false, + 'merchant_id' => '', + 'merchant_email' => '', ); } @@ -144,4 +150,58 @@ class CommonSettings extends AbstractDataModel { public function get_woo_settings() : array { return $this->woo_settings; } + + /** + * Setter to update details of the connected merchant account. + * + * Those details cannot be changed individually. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id The merchant ID. + * @param string $merchant_email The merchant's email. + * + * @return void + */ + public function set_merchant_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->data['sandbox_merchant'] = $is_sandbox; + $this->data['merchant_id'] = sanitize_text_field( $merchant_id ); + $this->data['merchant_email'] = sanitize_email( $merchant_email ); + $this->data['merchant_connected'] = true; + } + + /** + * Whether the currently connected merchant is a sandbox account. + * + * @return bool + */ + public function is_sandbox_merchant() : bool { + return $this->data['sandbox_merchant']; + } + + /** + * Whether the merchant successfully logged into their PayPal account. + * + * @return bool + */ + public function is_merchant_connected() : bool { + return $this->data['merchant_connected'] && $this->data['merchant_id'] && $this->data['merchant_email']; + } + + /** + * Gets the currently connected merchant ID. + * + * @return string + */ + public function get_merchant_id() : string { + return $this->data['merchant_id']; + } + + /** + * Gets the currently connected merchant's email. + * + * @return string + */ + public function get_merchant_email() : string { + return $this->data['merchant_email']; + } } diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 721c07e11..054f3e918 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -58,10 +58,33 @@ class CommonRestEndpoint extends RestEndpoint { 'js_name' => 'clientSecret', 'sanitize' => 'sanitize_text_field', ), + 'webhooks' => array( + 'js_name' => 'webhooks', + ), ); /** - * Map the internal flags to JS names. + * Map merchant details to JS names. + * + * @var array + */ + private array $merchant_info_map = array( + 'merchant_connected' => array( + 'js_name' => 'isConnected', + ), + 'sandbox_merchant' => array( + 'js_name' => 'isSandbox', + ), + 'merchant_id' => array( + 'js_name' => 'id', + ), + 'merchant_email' => array( + 'js_name' => 'email', + ), + ); + + /** + * Map woo-settings to JS names. * * @var array */ @@ -110,6 +133,18 @@ class CommonRestEndpoint extends RestEndpoint { ), ) ); + + register_rest_route( + $this->namespace, + "/$this->rest_base/merchant", + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_merchant_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); } /** @@ -123,17 +158,10 @@ class CommonRestEndpoint extends RestEndpoint { $this->field_map ); - $js_woo_settings = $this->sanitize_for_javascript( - $this->settings->get_woo_settings(), - $this->woo_settings_map - ); + $extra_data = $this->add_woo_settings( array() ); + $extra_data = $this->add_merchant_info( $extra_data ); - return $this->return_success( - $js_data, - array( - 'wooSettings' => $js_woo_settings, - ) - ); + return $this->return_success( $js_data, $extra_data ); } /** @@ -154,4 +182,55 @@ class CommonRestEndpoint extends RestEndpoint { return $this->get_details(); } + + /** + * Returns only the (read-only) merchant details from the DB. + * + * @return WP_REST_Response Merchant details. + */ + public function get_merchant_details() : WP_REST_Response { + $js_data = array(); // No persistent data. + $extra_data = $this->add_merchant_info( array() ); + + return $this->return_success( $js_data, $extra_data ); + } + + /** + * Appends the "merchant" attribute to the extra_data collection, which + * contains details about the merchant's PayPal account, like the merchant ID. + * + * @param array $extra_data Initial extra_data collection. + * + * @return array Updated extra_data collection. + */ + protected function add_merchant_info( array $extra_data ) : array { + $extra_data['merchant'] = $this->sanitize_for_javascript( + $this->settings->to_array(), + $this->merchant_info_map + ); + + $extra_data['merchant'] = apply_filters( + 'woocommerce_paypal_payments_rest_common_merchant_data', + $extra_data['merchant'], + ); + + return $extra_data; + } + + /** + * Appends the "wooSettings" attribute to the extra_data collection to + * provide WooCommerce store details, like the store country and currency. + * + * @param array $extra_data Initial extra_data collection. + * + * @return array Updated extra_data collection. + */ + protected function add_woo_settings( array $extra_data ) : array { + $extra_data['wooSettings'] = $this->sanitize_for_javascript( + $this->settings->get_woo_settings(), + $this->woo_settings_map + ); + + return $extra_data; + } } diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php new file mode 100644 index 000000000..d8fc2760e --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -0,0 +1,132 @@ +settings = $settings; + $this->cache = $cache; + $this->logger = $logger; + } + + /** + * Configure REST API routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'refresh_status' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + } + + /** + * Handles the refresh status request. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response + */ + public function refresh_status( WP_REST_Request $request ): WP_REST_Response { + $now = time(); + $last_request_time = $this->cache->get( self::CACHE_KEY ) ?: 0; + $seconds_missing = $last_request_time + self::TIMEOUT - $now; + + if ( $seconds_missing > 0 ) { + return $this->return_error( + sprintf( + // translators: %1$s is the number of seconds remaining. + __( 'Wait %1$s seconds before trying again.', 'woocommerce-paypal-payments' ), + $seconds_missing + ) + ); + } + + $this->cache->set( self::CACHE_KEY, $now, self::TIMEOUT ); + + do_action( 'woocommerce_paypal_payments_clear_apm_product_status', $this->settings ); + + $this->logger->info( 'Feature status refreshed successfully' ); + + return $this->return_success( + array( + 'message' => __( 'Feature status refreshed successfully.', 'woocommerce-paypal-payments' ), + ) + ); + } +} diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php new file mode 100644 index 000000000..c3116a1ed --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -0,0 +1,185 @@ +webhook_endpoint = $webhook_endpoint; + $this->webhook_registrar = $webhook_registrar; + $this->webhook_simulation = $webhook_simulation; + } + + /** + * Configure REST API routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_webhooks' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'resubscribe_webhooks' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_simulate_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'check_simulated_webhook_state' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'simulate_webhooks_start' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + } + + /** + * Returns a webhook endpoint URL and list of subscribed webhooks + * + * @return WP_REST_Response + */ + public function get_webhooks(): WP_REST_Response { + try { + $webhook_list = ( $this->webhook_endpoint->list() )[0]; + $webhook_events = array_map( + function ( stdClass $webhook ) { + return strtolower( $webhook->name ); + }, + $webhook_list->event_types() + ); + + return $this->return_success( + array( + 'webhooks' => array( + 'url' => $webhook_list->url(), + 'events' => implode( ', ', $webhook_events ), + ), + ) + ); + } catch ( \Exception $error ) { + return $this->return_error( 'Problem while fetching webhooks data' ); + } + } + + /** + * Re-subscribes webhooks and returns webhooks + * + * @return WP_REST_Response + */ + public function resubscribe_webhooks(): WP_REST_Response { + if ( ! $this->webhook_registrar->register() ) { + return $this->return_error( 'Webhook subscription failed.' ); + } + return $this->get_webhooks(); + } + + /** + * Starts webhook simulation + * + * @return WP_REST_Response + */ + public function simulate_webhooks_start(): WP_REST_Response { + try { + $this->webhook_simulation->start(); + return $this->return_success( array() ); + } catch ( \Exception $error ) { + return $this->return_error( $error->getMessage() ); + } + } + + /** + * Checks webhook simulation state + * + * @return WP_REST_Response + */ + public function check_simulated_webhook_state(): WP_REST_Response { + try { + $state = $this->webhook_simulation->get_state(); + + return $this->return_success( + array( + 'state' => $state, + ) + ); + + } catch ( \Exception $error ) { + return $this->return_error( $error->getMessage() ); + } + } +} diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php new file mode 100644 index 000000000..a24a82231 --- /dev/null +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -0,0 +1,241 @@ +settings_page_id = $settings_page_id; + $this->settings = $settings; + $this->url_manager = $url_manager; + $this->logger = $logger ?: new NullLogger(); + + // Initialize as "guest", the real ID is provided via process(). + $this->user_id = 0; + } + + /** + * Process the request data, and extract connection details, if present. + * + * @param int $user_id The current user ID. + * @param array $request Request details to process. + */ + public function process( int $user_id, array $request ) : void { + $this->user_id = $user_id; + + if ( ! $this->is_valid_request( $request ) ) { + return; + } + + $token = $this->get_token_from_request( $request ); + if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) { + return; + } + + $data = $this->extract_data( $request ); + if ( ! $data ) { + return; + } + + $this->logger->info( 'Found merchant data in request', $data ); + + $this->store_data( + $data['is_sandbox'], + $data['merchant_id'], + $data['merchant_email'] + ); + } + + /** + * Determine, if the request details contain connection data that should be + * extracted and stored. + * + * @param array $request Request details to verify. + * + * @return bool True, if the request contains valid connection details. + */ + protected function is_valid_request( array $request ) : bool { + if ( $this->user_id < 1 || ! $this->settings_page_id ) { + return false; + } + + if ( ! user_can( $this->user_id, 'manage_woocommerce' ) ) { + return false; + } + + $required_params = array( + 'merchantIdInPayPal', + 'merchantId', + 'ppcpToken', + ); + + foreach ( $required_params as $param ) { + if ( empty( $request[ $param ] ) ) { + return false; + } + } + + return true; + } + + /** + * Extract the merchant details (ID & email) from the request details. + * + * @param array $request The full request details. + * + * @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys, + * or an empty array on failure. + */ + protected function extract_data( array $request ) : array { + $this->logger->info( 'Extracting connection data from request...' ); + + $merchant_id = $this->get_merchant_id_from_request( $request ); + $merchant_email = $this->get_merchant_email_from_request( $request ); + + if ( ! $merchant_id || ! $merchant_email ) { + return array(); + } + + return array( + 'is_sandbox' => $this->settings->get_sandbox(), + 'merchant_id' => $merchant_id, + 'merchant_email' => $merchant_email, + ); + } + + /** + * Persist the merchant details to the database. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id The anonymized merchant ID. + * @param string $merchant_email The merchant's email. + */ + protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" ); + + $this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->settings->save(); + } + + /** + * Returns the sanitized connection token from the incoming request. + * + * @param array $request Full request details. + * + * @return string The sanitized token, or an empty string. + */ + protected function get_token_from_request( array $request ) : string { + return $this->sanitize_string( $request['ppcpToken'] ?? '' ); + } + + /** + * Returns the sanitized merchant ID from the incoming request. + * + * @param array $request Full request details. + * + * @return string The sanitized merchant ID, or an empty string. + */ + protected function get_merchant_id_from_request( array $request ) : string { + return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' ); + } + + /** + * Returns the sanitized merchant email from the incoming request. + * + * Note that the email is provided via the argument "merchantId", which + * looks incorrect at first, but PayPal uses the email address as merchant + * IDm and offers a more anonymous ID via the "merchantIdInPayPal" argument. + * + * @param array $request Full request details. + * + * @return string The sanitized merchant email, or an empty string. + */ + protected function get_merchant_email_from_request( array $request ) : string { + return $this->sanitize_merchant_email( $request['merchantId'] ?? '' ); + } + + /** + * Sanitizes a request-argument for processing. + * + * @param string $value Value from the request argument. + * + * @return string Sanitized value. + */ + protected function sanitize_string( string $value ) : string { + return trim( sanitize_text_field( wp_unslash( $value ) ) ); + } + + /** + * Sanitizes the merchant's email address for processing. + * + * @param string $email The plain email. + * + * @return string Sanitized email address. + */ + protected function sanitize_merchant_email( string $email ) : string { + return sanitize_text_field( str_replace( ' ', '+', $email ) ); + } +} diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php index 6e91aba3a..028740cb9 100644 --- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -14,9 +14,11 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; -use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; +// TODO: Replace the OnboardingUrl with a new implementation for this module. +use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; + /** * Generator that builds the ISU connection URL. */ @@ -36,11 +38,11 @@ class ConnectionUrlGenerator { protected PartnerReferralsData $referrals_data; /** - * The cache + * Manages access to OnboardingUrl instances * - * @var Cache + * @var OnboardingUrlManager */ - protected Cache $cache; + protected OnboardingUrlManager $url_manager; /** * Which environment is used for the connection URL. @@ -54,7 +56,7 @@ class ConnectionUrlGenerator { * * @var LoggerInterface */ - private $logger; + private LoggerInterface $logger; /** * Constructor for the ConnectionUrlGenerator class. @@ -63,23 +65,22 @@ class ConnectionUrlGenerator { * * @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation. * @param PartnerReferralsData $referrals_data Default partner referrals data. - * @param Cache $cache The cache object used for storing and - * retrieving data. * @param string $environment Environment that is used to generate the URL. * ['production'|'sandbox']. + * @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances. * @param ?LoggerInterface $logger The logger object for logging messages. */ public function __construct( PartnerReferrals $partner_referrals, PartnerReferralsData $referrals_data, - Cache $cache, string $environment, + OnboardingUrlManager $url_manager, ?LoggerInterface $logger = null ) { $this->partner_referrals = $partner_referrals; $this->referrals_data = $referrals_data; - $this->cache = $cache; $this->environment = $environment; + $this->url_manager = $url_manager; $this->logger = $logger ?: new NullLogger(); } @@ -107,7 +108,7 @@ class ConnectionUrlGenerator { public function generate( array $products = array() ) : string { $cache_key = $this->cache_key( $products ); $user_id = get_current_user_id(); - $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, $user_id ); + $onboarding_url = $this->url_manager->get( $cache_key, $user_id ); $cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key ); if ( $cached_url ) { diff --git a/modules/ppcp-settings/src/Service/OnboardingUrlManager.php b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php new file mode 100644 index 000000000..f2463af46 --- /dev/null +++ b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php @@ -0,0 +1,101 @@ +cache = $cache; + $this->logger = $logger ?: new NullLogger(); + } + + /** + * Returns a new Onboarding Url instance. + * + * @param string $cache_key_prefix The prefix for the cache entry. + * @param int $user_id User ID to associate the link with. + * + * @return OnboardingUrl + */ + public function get( string $cache_key_prefix, int $user_id ) : OnboardingUrl { + return new OnboardingUrl( $this->cache, $cache_key_prefix, $user_id ); + } + + /** + * Validates the authentication token; if it's valid, the token is instantly + * invalidated (deleted), so it cannot be validated again. + * + * @param string $token The token to validate. + * @param int $user_id User ID who generated the token. + * + * @return bool True, if the token is valid. False otherwise. + */ + public function validate_token_and_delete( string $token, int $user_id ) : bool { + if ( $user_id < 1 || strlen( $token ) < 10 ) { + return false; + } + + $log_token = ( (string) substr( $token, 0, 2 ) ) . '...' . ( (string) substr( $token, - 6 ) ); + $this->logger->debug( 'Validating onboarding ppcpToken: ' . $log_token ); + + if ( OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id ) ) { + $this->logger->info( 'Validated onboarding ppcpToken: ' . $log_token ); + + return true; + } + + if ( OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id ) ) { + // TODO: Do we need this here? Previous logic was to reload the page without doing anything in this case. + $this->logger->info( 'Validated previous token, silently redirecting: ' . $log_token ); + + return true; + } + + $this->logger->error( 'Failed to validate onboarding ppcpToken: ' . $log_token ); + + return false; + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 7c9dca2f8..b97db27ec 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings; use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; +use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; @@ -85,7 +86,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); - $endpoint = $container->get( 'settings.switch-ui.endpoint' ); + $endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null; assert( $endpoint instanceof SwitchSettingsUiEndpoint ); add_action( @@ -180,6 +181,8 @@ class SettingsModule implements ServiceModule, ExecutableModule { $container->get( 'settings.rest.common' ), $container->get( 'settings.rest.connect_manual' ), $container->get( 'settings.rest.login_link' ), + $container->get( 'settings.rest.webhooks' ), + $container->get( 'settings.rest.refresh_feature_status' ), ); foreach ( $endpoints as $endpoint ) { @@ -189,6 +192,17 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); + add_action( + 'admin_init', + static function () use ( $container ) : void { + $connection_handler = $container->get( 'settings.handler.connection-listener' ); + assert( $connection_handler instanceof ConnectionListener ); + + // @phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce; sanitation done by the handler + $connection_handler->process( get_current_user_id(), $_GET ); + } + ); + return true; } diff --git a/modules/ppcp-settings/yarn.lock b/modules/ppcp-settings/yarn.lock index a3b2a6adb..6b623d53a 100644 --- a/modules/ppcp-settings/yarn.lock +++ b/modules/ppcp-settings/yarn.lock @@ -1117,14 +1117,6 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime-corejs2@7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2" - integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A== - dependencies: - core-js "^2.6.5" - regenerator-runtime "^0.13.2" - "@babel/runtime@7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" @@ -1132,7 +1124,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -1221,7 +1213,7 @@ source-map "^0.5.7" stylis "4.2.0" -"@emotion/cache@^11.13.0", "@emotion/cache@^11.4.0", "@emotion/cache@^11.7.1": +"@emotion/cache@^11.13.0", "@emotion/cache@^11.4.0": version "11.13.1" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== @@ -1232,47 +1224,17 @@ "@emotion/weak-memoize" "^0.4.0" stylis "4.2.0" -"@emotion/css@^11.7.1": - version "11.13.4" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.4.tgz#a5128e34a23f5e2c891970b8ec98a60c5a2395e1" - integrity sha512-CthbOD5EBw+iN0rfM96Tuv5kaZN4nxPyYDvGUs0bc7wZBBiU/0mse+l+0O9RshW2d+v5HH1cme+BAbLJ/3Folw== - dependencies: - "@emotion/babel-plugin" "^11.12.0" - "@emotion/cache" "^11.13.0" - "@emotion/serialize" "^1.3.0" - "@emotion/sheet" "^1.4.0" - "@emotion/utils" "^1.4.0" - "@emotion/hash@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== -"@emotion/is-prop-valid@^0.8.2": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" - integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== - dependencies: - "@emotion/memoize" "0.7.4" - -"@emotion/is-prop-valid@^1.3.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz#8d5cf1132f836d7adbe42cf0b49df7816fc88240" - integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== - dependencies: - "@emotion/memoize" "^0.9.0" - -"@emotion/memoize@0.7.4": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" - integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== - "@emotion/memoize@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== -"@emotion/react@^11.7.1", "@emotion/react@^11.8.1": +"@emotion/react@^11.8.1": version "11.13.3" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== @@ -1286,7 +1248,7 @@ "@emotion/weak-memoize" "^0.4.0" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^1.0.2", "@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": +"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.1": version "1.3.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== @@ -1302,18 +1264,6 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== -"@emotion/styled@^11.6.0": - version "11.13.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.13.0.tgz#633fd700db701472c7a5dbef54d6f9834e9fb190" - integrity sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.12.0" - "@emotion/is-prop-valid" "^1.3.0" - "@emotion/serialize" "^1.3.0" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" - "@emotion/utils" "^1.4.0" - "@emotion/unitless@^0.10.0": version "0.10.0" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" @@ -1324,11 +1274,6 @@ resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== -"@emotion/utils@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" - integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== - "@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" @@ -1380,11 +1325,6 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@floating-ui/core@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.6.2.tgz#f2813f0e5f3d5ed7af5029e1a082203dadf02b7d" - integrity sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg== - "@floating-ui/core@^1.6.0": version "1.6.8" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" @@ -1392,13 +1332,6 @@ dependencies: "@floating-ui/utils" "^0.2.8" -"@floating-ui/dom@^0.4.5": - version "0.4.5" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.4.5.tgz#2e88d16646119cc67d44683f75ee99840475bbfa" - integrity sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw== - dependencies: - "@floating-ui/core" "^0.6.2" - "@floating-ui/dom@^1.0.1": version "1.6.12" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556" @@ -1407,14 +1340,6 @@ "@floating-ui/core" "^1.6.0" "@floating-ui/utils" "^0.2.8" -"@floating-ui/react-dom@0.6.3": - version "0.6.3" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.6.3.tgz#7b64cfd4fd12e4a0515dbf1b2be16e48c9a06c5a" - integrity sha512-hC+pS5D6AgS2wWjbmSQ6UR6Kpy+drvWGJIri6e1EDGADTPsCaa4KzCgmCczHrQeInx9tqs81EyDmbKJYY2swKg== - dependencies: - "@floating-ui/dom" "^0.4.5" - use-isomorphic-layout-effect "^1.1.1" - "@floating-ui/utils@^0.2.8": version "0.2.8" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" @@ -1704,59 +1629,6 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== -"@motionone/animation@^10.12.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.18.0.tgz#868d00b447191816d5d5cf24b1cafa144017922b" - integrity sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw== - dependencies: - "@motionone/easing" "^10.18.0" - "@motionone/types" "^10.17.1" - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/dom@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.12.0.tgz#ae30827fd53219efca4e1150a5ff2165c28351ed" - integrity sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw== - dependencies: - "@motionone/animation" "^10.12.0" - "@motionone/generators" "^10.12.0" - "@motionone/types" "^10.12.0" - "@motionone/utils" "^10.12.0" - hey-listen "^1.0.8" - tslib "^2.3.1" - -"@motionone/easing@^10.18.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/easing/-/easing-10.18.0.tgz#7b82f6010dfee3a1bb0ee83abfbaff6edae0c708" - integrity sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg== - dependencies: - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/generators@^10.12.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.18.0.tgz#fe09ab5cfa0fb9a8884097feb7eb60abeb600762" - integrity sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg== - dependencies: - "@motionone/types" "^10.17.1" - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/types@^10.12.0", "@motionone/types@^10.17.1": - version "10.17.1" - resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.17.1.tgz#cf487badbbdc9da0c2cb86ffc1e5d11147c6e6fb" - integrity sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A== - -"@motionone/utils@^10.12.0", "@motionone/utils@^10.18.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.18.0.tgz#a59ff8932ed9009624bca07c56b28ef2bb2f885e" - integrity sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw== - dependencies: - "@motionone/types" "^10.17.1" - hey-listen "^1.0.8" - tslib "^2.3.1" - "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -1919,11 +1791,6 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== -"@popperjs/core@^2.5.4": - version "2.11.8" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - "@puppeteer/browsers@1.4.6": version "1.4.6" resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-1.4.6.tgz#1f70fd23d5d2ccce9d29b038e5039d7a1049ca77" @@ -2456,13 +2323,6 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@^17.0.11": - version "17.0.25" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" - integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== - dependencies: - "@types/react" "^17" - "@types/react-dom@^18.2.25": version "18.3.1" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" @@ -2485,25 +2345,11 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@^17", "@types/react@^17.0.37": - version "17.0.83" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.83.tgz#b477c56387b74279281149dcf5ba2a1e2216d131" - integrity sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "^0.16" - csstype "^3.0.2" - "@types/retry@0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/scheduler@^0.16": - version "0.16.8" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" - integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== - "@types/semver@^7.3.12", "@types/semver@^7.5.0": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -2753,18 +2599,6 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@use-gesture/core@10.3.1": - version "10.3.1" - resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.3.1.tgz#976c9421e905f0079d49822cfd5c2e56b808fc56" - integrity sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw== - -"@use-gesture/react@^10.2.6": - version "10.3.1" - resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.3.1.tgz#17a743a894d9bd9a0d1980c618f37f0164469867" - integrity sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g== - dependencies: - "@use-gesture/core" "10.3.1" - "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" @@ -2901,37 +2735,6 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== -"@woocommerce/navigation@~8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@woocommerce/navigation/-/navigation-8.1.0.tgz#dc0183e61d0fb139844f5471839b723dbb289b4a" - integrity sha512-Ifl8IYRLYlbxk6RNuuVorMaCoOs8aFWEo8oSU++SqFfyjPi893Nuk6NJYVvAVhxFdwPfw9RptvQ/q8sIusPihA== - dependencies: - "@wordpress/api-fetch" "^6.0.1" - "@wordpress/components" "^19.5.0" - "@wordpress/compose" "^5.1.2" - "@wordpress/element" "^4.1.1" - "@wordpress/hooks" "^3.5.0" - "@wordpress/notices" "^3.3.2" - "@wordpress/url" "^3.4.1" - history "^5.3.0" - qs "^6.10.3" - -"@woocommerce/settings@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@woocommerce/settings/-/settings-1.0.0.tgz#c35f85d1c9c03f68c77541bf323a77fa31720312" - integrity sha512-BjrT56Cz8XTRHw2JNPmANRkYh2rzdF33wOa56lah1qb/MjHUKuVJ0PTSZ19S5Trb92IkxfcIVB26CSdxXnf5Og== - dependencies: - "@babel/runtime-corejs2" "7.5.5" - -"@wordpress/a11y@^3.15.0", "@wordpress/a11y@^3.22.0", "@wordpress/a11y@^3.31.0": - version "3.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/a11y/-/a11y-3.58.0.tgz#8e8853709061b3042ca6cb9ac1bc6a5ec2bc9232" - integrity sha512-7NnJKl4+pxP6kV/jvXaJcZZCGzW7zaj6YeMnyjUd96cH4ta1ykBIveWgejerFOGsbK+88FnStcxSFj+dbDXs/w== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/dom-ready" "^3.58.0" - "@wordpress/i18n" "^4.58.0" - "@wordpress/api-fetch@*": version "7.11.0" resolved "https://registry.yarnpkg.com/@wordpress/api-fetch/-/api-fetch-7.11.0.tgz#83c7158cb45c5b2390be0383c680a797a5e72452" @@ -2941,15 +2744,6 @@ "@wordpress/i18n" "*" "@wordpress/url" "*" -"@wordpress/api-fetch@^6.0.1": - version "6.55.0" - resolved "https://registry.yarnpkg.com/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz#a28883cfa3a31590838cb1f0ae863d7c3d391499" - integrity sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/i18n" "^4.58.0" - "@wordpress/url" "^3.59.0" - "@wordpress/babel-preset-default@*": version "8.11.0" resolved "https://registry.yarnpkg.com/@wordpress/babel-preset-default/-/babel-preset-default-8.11.0.tgz#603e773093729542a893c91faf9b58b133bc2e0a" @@ -2977,52 +2771,6 @@ resolved "https://registry.yarnpkg.com/@wordpress/browserslist-config/-/browserslist-config-6.11.0.tgz#4637f0f1336309e519e858347480c01bf2fa8c83" integrity sha512-wUDbJ3x7c8iMZLtwo+7VlWZ/vDc47PDW2eSAKW18RrQBSTdaNmWi4qiyFYi7Ye2XkyfUd2gp71MTJjZi6n/V2A== -"@wordpress/components@^19.5.0": - version "19.17.0" - resolved "https://registry.yarnpkg.com/@wordpress/components/-/components-19.17.0.tgz#c15b1467aaa7056d3fbd74c04644074ef43d49de" - integrity sha512-6FsLq1WS924fjZjRGSuen3Tzaa4mEWRtCTHM2JS5eE5+rnuhddiHNNgvw26IZCwhQYQwIvIKq9m9in0F0fSOzg== - dependencies: - "@babel/runtime" "^7.16.0" - "@emotion/cache" "^11.7.1" - "@emotion/css" "^11.7.1" - "@emotion/react" "^11.7.1" - "@emotion/serialize" "^1.0.2" - "@emotion/styled" "^11.6.0" - "@emotion/utils" "1.0.0" - "@floating-ui/react-dom" "0.6.3" - "@use-gesture/react" "^10.2.6" - "@wordpress/a11y" "^3.15.0" - "@wordpress/compose" "^5.13.0" - "@wordpress/date" "^4.15.0" - "@wordpress/deprecated" "^3.15.0" - "@wordpress/dom" "^3.15.0" - "@wordpress/element" "^4.13.0" - "@wordpress/escape-html" "^2.15.0" - "@wordpress/hooks" "^3.15.0" - "@wordpress/i18n" "^4.15.0" - "@wordpress/icons" "^9.6.0" - "@wordpress/is-shallow-equal" "^4.15.0" - "@wordpress/keycodes" "^3.15.0" - "@wordpress/primitives" "^3.13.0" - "@wordpress/rich-text" "^5.13.0" - "@wordpress/warning" "^2.15.0" - classnames "^2.3.1" - colord "^2.7.0" - dom-scroll-into-view "^1.2.1" - downshift "^6.0.15" - framer-motion "^6.2.8" - gradient-parser "^0.1.5" - highlight-words-core "^1.2.2" - lodash "^4.17.21" - memize "^1.1.0" - moment "^2.26.0" - re-resizable "^6.4.0" - react-colorful "^5.3.1" - react-dates "^21.8.0" - reakit "^1.3.8" - remove-accents "^0.4.2" - uuid "^8.3.0" - "@wordpress/compose@*": version "7.11.0" resolved "https://registry.yarnpkg.com/@wordpress/compose/-/compose-7.11.0.tgz#a57702e9529aa842ce45968edb286cc0ef22299e" @@ -3042,43 +2790,6 @@ mousetrap "^1.6.5" use-memo-one "^1.1.1" -"@wordpress/compose@^5.1.2", "@wordpress/compose@^5.13.0", "@wordpress/compose@^5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@wordpress/compose/-/compose-5.20.0.tgz#e5c5181bca058f73b13fb7007d96f800b6501f71" - integrity sha512-IcmXeAIgZoJUFIO3bxBpPYfAre41H6zxQTC5N6nqhGqpISvbO1SsAIikd6B4AoSHUZmYV5UoTxk9kECqZZGVOw== - dependencies: - "@babel/runtime" "^7.16.0" - "@types/mousetrap" "^1.6.8" - "@wordpress/deprecated" "^3.22.0" - "@wordpress/dom" "^3.22.0" - "@wordpress/element" "^4.20.0" - "@wordpress/is-shallow-equal" "^4.22.0" - "@wordpress/keycodes" "^3.22.0" - "@wordpress/priority-queue" "^2.22.0" - change-case "^4.1.2" - clipboard "^2.0.8" - mousetrap "^1.6.5" - use-memo-one "^1.1.1" - -"@wordpress/compose@^6.35.0": - version "6.35.0" - resolved "https://registry.yarnpkg.com/@wordpress/compose/-/compose-6.35.0.tgz#411a1929bb28102cf4c508a13dc4d46812bbc871" - integrity sha512-PfruhCxxxJokDQHc2YBgerEiHV7BIxQk9g5vU4/f9X/0PBQWUTuxOzSFcAba03vnjfAgtPTSMp50T50hcJwXfA== - dependencies: - "@babel/runtime" "^7.16.0" - "@types/mousetrap" "^1.6.8" - "@wordpress/deprecated" "^3.58.0" - "@wordpress/dom" "^3.58.0" - "@wordpress/element" "^5.35.0" - "@wordpress/is-shallow-equal" "^4.58.0" - "@wordpress/keycodes" "^3.58.0" - "@wordpress/priority-queue" "^2.58.0" - "@wordpress/undo-manager" "^0.18.0" - change-case "^4.1.2" - clipboard "^2.0.11" - mousetrap "^1.6.5" - use-memo-one "^1.1.1" - "@wordpress/data-controls@^4.10.0": version "4.11.0" resolved "https://registry.yarnpkg.com/@wordpress/data-controls/-/data-controls-4.11.0.tgz#48894aac92e5d517eb729f718d5861bd28cd7128" @@ -3110,57 +2821,6 @@ rememo "^4.0.2" use-memo-one "^1.1.1" -"@wordpress/data@^7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@wordpress/data/-/data-7.6.0.tgz#16e5d03653e527baeb00607d8c9cdacbb6c59bcc" - integrity sha512-Og+oinEpJzd2rI4cFQGJBtSNzSVEa1sDWje1dYc3Jm7t2/NpkGk/YXn0PlVhkakA7YCGBy2OhX122flgZBuaBw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/compose" "^5.20.0" - "@wordpress/deprecated" "^3.22.0" - "@wordpress/element" "^4.20.0" - "@wordpress/is-shallow-equal" "^4.22.0" - "@wordpress/priority-queue" "^2.22.0" - "@wordpress/redux-routine" "^4.22.0" - equivalent-key-map "^0.2.2" - is-plain-object "^5.0.0" - is-promise "^4.0.0" - lodash "^4.17.21" - redux "^4.1.2" - turbo-combine-reducers "^1.0.2" - use-memo-one "^1.1.1" - -"@wordpress/data@^9.1.0": - version "9.28.0" - resolved "https://registry.yarnpkg.com/@wordpress/data/-/data-9.28.0.tgz#64efd691384ba26faa1b460549fad9a237728818" - integrity sha512-EDPpZdkngdoW7EMzPpGj0BmNcr7syJO67pgTODtN/4XFIdYL2RKzFyn3nlLBKhX17UsE/ALq9WdijacH4QJ9qw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/compose" "^6.35.0" - "@wordpress/deprecated" "^3.58.0" - "@wordpress/element" "^5.35.0" - "@wordpress/is-shallow-equal" "^4.58.0" - "@wordpress/priority-queue" "^2.58.0" - "@wordpress/private-apis" "^0.40.0" - "@wordpress/redux-routine" "^4.58.0" - deepmerge "^4.3.0" - equivalent-key-map "^0.2.2" - is-plain-object "^5.0.0" - is-promise "^4.0.0" - redux "^4.1.2" - rememo "^4.0.2" - use-memo-one "^1.1.1" - -"@wordpress/date@^4.15.0": - version "4.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/date/-/date-4.58.0.tgz#6803e0bde8e8ccb62ebf57554f9543a14cc4433c" - integrity sha512-yFT7DU0H9W0lsDytMaVMmjho08X1LeBMIQMppxdtKB04Ujx58hVh7gtunOsstUQ7pVg23nE2eLaVfx5JOdjzAw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/deprecated" "^3.58.0" - moment "^2.29.4" - moment-timezone "^0.5.40" - "@wordpress/dependency-extraction-webpack-plugin@*": version "6.11.0" resolved "https://registry.yarnpkg.com/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.11.0.tgz#11ad1ab4700f33c1e80d7b8c2a81a4690afc06ad" @@ -3176,21 +2836,6 @@ "@babel/runtime" "7.25.7" "@wordpress/hooks" "*" -"@wordpress/deprecated@^3.15.0", "@wordpress/deprecated@^3.22.0", "@wordpress/deprecated@^3.58.0": - version "3.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/deprecated/-/deprecated-3.58.0.tgz#c8b9442167bc20aef4888af4a4d081b4553adb4c" - integrity sha512-knweE2lLEUxWRr6A48sHiO0ww5pPybGe2NVIZVq/y7EaYCMdpy6gYA0ZdVqMKZvtxKKqicJfwigcn+hinsTvUQ== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/hooks" "^3.58.0" - -"@wordpress/dom-ready@^3.58.0": - version "3.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/dom-ready/-/dom-ready-3.58.0.tgz#73453c803638438e10c003f68ce9ebc27138335f" - integrity sha512-sDgRPjNBToRKgYrpwvMRv2Yc7/17+sp8hm/rRnbubwb+d/DbGkK4Tc/r4sNLSZCqUAtcBXq9uk1lzvhge3QUSg== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/dom@*": version "4.11.0" resolved "https://registry.yarnpkg.com/@wordpress/dom/-/dom-4.11.0.tgz#c4cc6bdc6374112139c305a642a46ce1995e88a9" @@ -3199,14 +2844,6 @@ "@babel/runtime" "7.25.7" "@wordpress/deprecated" "*" -"@wordpress/dom@^3.15.0", "@wordpress/dom@^3.22.0", "@wordpress/dom@^3.58.0": - version "3.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/dom/-/dom-3.58.0.tgz#c9afe87ce29d00a2baecfcf61a284232ce821612" - integrity sha512-t3xSr/nqekj2qwUGRAqSeGx6116JOBxzI+VBiUfZrjGEnuyKdLelXDEeYtcwbb7etMkj/6F60/NB7GTl5IwizQ== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/deprecated" "^3.58.0" - "@wordpress/e2e-test-utils-playwright@*": version "1.11.0" resolved "https://registry.yarnpkg.com/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.11.0.tgz#00dee8a1d945ecf9354a3f5eb6f56d4dfd4f38c5" @@ -3233,34 +2870,6 @@ react "^18.3.0" react-dom "^18.3.0" -"@wordpress/element@^4.1.1", "@wordpress/element@^4.13.0", "@wordpress/element@^4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-4.20.0.tgz#d78499521cbb471b97e011a81ec6236daeac24ad" - integrity sha512-Ou7EoGtGe4FUL6fKALINXJLKoSfyWTBJzkJfN2HzSgM1wira9EuWahl8MQN0HAUaWeOoDqMKPvnglfS+kC8JLA== - dependencies: - "@babel/runtime" "^7.16.0" - "@types/react" "^17.0.37" - "@types/react-dom" "^17.0.11" - "@wordpress/escape-html" "^2.22.0" - change-case "^4.1.2" - is-plain-object "^5.0.0" - react "^17.0.2" - react-dom "^17.0.2" - -"@wordpress/element@^5.35.0": - version "5.35.0" - resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-5.35.0.tgz#c0629200aa5f3644e921fc569cb53a8fb470af34" - integrity sha512-puswpGcIdS+0A2g28uHriMkZqqRCmzFczue5Tk99VNtzBdehyk7Ae+DZ4xw5yT6GqYai8NTqv6MRwCB78uh5Mw== - dependencies: - "@babel/runtime" "^7.16.0" - "@types/react" "^18.2.79" - "@types/react-dom" "^18.2.25" - "@wordpress/escape-html" "^2.58.0" - change-case "^4.1.2" - is-plain-object "^5.0.0" - react "^18.3.0" - react-dom "^18.3.0" - "@wordpress/escape-html@*": version "3.11.0" resolved "https://registry.yarnpkg.com/@wordpress/escape-html/-/escape-html-3.11.0.tgz#44850f394981a27c511b0eb5b28f8851b938a056" @@ -3268,13 +2877,6 @@ dependencies: "@babel/runtime" "7.25.7" -"@wordpress/escape-html@^2.15.0", "@wordpress/escape-html@^2.22.0", "@wordpress/escape-html@^2.58.0": - version "2.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/escape-html/-/escape-html-2.58.0.tgz#37fa8c74c31b7a481d56bab6a8f0bfa415311b2f" - integrity sha512-9YJXMNfzkrhHEVP1jFEhgijbZqW8Mt3NHIMZjIQoWtBf7QE86umpYpGGBXzYC0YlpGTRGzZTBwYaqFKxjeaSgA== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/eslint-plugin@*": version "21.4.0" resolved "https://registry.yarnpkg.com/@wordpress/eslint-plugin/-/eslint-plugin-21.4.0.tgz#9499d73c1930b0ba3c598b22f76ac3589ec683de" @@ -3305,13 +2907,6 @@ dependencies: "@babel/runtime" "7.25.7" -"@wordpress/hooks@^3.15.0", "@wordpress/hooks@^3.5.0", "@wordpress/hooks@^3.58.0": - version "3.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/hooks/-/hooks-3.58.0.tgz#68094f7e7e3f8cbc3ab68a0fe9ac2a9b3cfe55d6" - integrity sha512-9LB0ZHnZRQlORttux9t/xbAskF+dk2ujqzPGsVzc92mSKpQP3K2a5Wy74fUnInguB1vLUNHT6nrNdkVom5qX1Q== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/i18n@*": version "5.11.0" resolved "https://registry.yarnpkg.com/@wordpress/i18n/-/i18n-5.11.0.tgz#95a8d645df23600d8f40e3d719c9c8b2db1e238f" @@ -3324,27 +2919,6 @@ sprintf-js "^1.1.1" tannin "^1.2.0" -"@wordpress/i18n@^4.15.0", "@wordpress/i18n@^4.22.0", "@wordpress/i18n@^4.58.0": - version "4.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/i18n/-/i18n-4.58.0.tgz#d4327fa4dee4f82be7753e900700670fea316d30" - integrity sha512-VfvS3BWv/RDjRKD6PscIcvYfWKnGJcI/DEqyDgUMhxCM6NRwoL478CsUKTiGJIymeyRodNRfprdcF086DpGKYw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/hooks" "^3.58.0" - gettext-parser "^1.3.1" - memize "^2.1.0" - sprintf-js "^1.1.1" - tannin "^1.2.0" - -"@wordpress/icons@^9.6.0": - version "9.49.0" - resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-9.49.0.tgz#3886fcb99c01caae97f25bfa15a7fd6b0a818d88" - integrity sha512-Z8F+ledkfkcKDuS1c/RkM0dEWdfv2AXs6bCgey89p0atJSscf7qYbMJR9zE5rZ5aqXyFfV0DAFKJEgayNqneNQ== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/element" "^5.35.0" - "@wordpress/primitives" "^3.56.0" - "@wordpress/is-shallow-equal@*": version "5.11.0" resolved "https://registry.yarnpkg.com/@wordpress/is-shallow-equal/-/is-shallow-equal-5.11.0.tgz#2f273d6d4de24a66a7a8316b770cf832d22bfc37" @@ -3352,13 +2926,6 @@ dependencies: "@babel/runtime" "7.25.7" -"@wordpress/is-shallow-equal@^4.15.0", "@wordpress/is-shallow-equal@^4.22.0", "@wordpress/is-shallow-equal@^4.58.0": - version "4.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/is-shallow-equal/-/is-shallow-equal-4.58.0.tgz#52f400dc9fac721a0763b1c3f2932d383286b581" - integrity sha512-NH2lbXo/6ix1t4Zu9UBXpXNtoLwSaYmIRSyDH34XNb0ic8a7yjEOhYWVW3LTfSCv9dJVyxlM5TJPtL85q7LdeQ== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/jest-console@*": version "8.11.0" resolved "https://registry.yarnpkg.com/@wordpress/jest-console/-/jest-console-8.11.0.tgz#a825f4d9ee4eb007c9b6329687f8507dc30b49d2" @@ -3383,23 +2950,6 @@ "@babel/runtime" "7.25.7" "@wordpress/i18n" "*" -"@wordpress/keycodes@^3.15.0", "@wordpress/keycodes@^3.22.0", "@wordpress/keycodes@^3.58.0": - version "3.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/keycodes/-/keycodes-3.58.0.tgz#cc4d2a7c2eb47c2b4718dd6ec0e47c1a77d1f8ba" - integrity sha512-Q/LRKpx8ndzuHlkxSQ2BD+NTYYKQPIneNNMng8hTAfyU7RFwXpqj06HpeOFGh4XIdPKCs/8hmucoLJRmmLmZJA== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/i18n" "^4.58.0" - -"@wordpress/notices@^3.3.2": - version "3.31.0" - resolved "https://registry.yarnpkg.com/@wordpress/notices/-/notices-3.31.0.tgz#a278767eaa2e7b704fe1f4d68f95c7984779737c" - integrity sha512-9WyaFaSr/vQc1K7cZLyPw1xBKqWfjpAKMJzWMzHYjwk1ldibhBWVLukicuolD6Y+9l+97IZHCoESBQwUeFo79Q== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/a11y" "^3.31.0" - "@wordpress/data" "^9.1.0" - "@wordpress/npm-package-json-lint-config@*": version "5.11.0" resolved "https://registry.yarnpkg.com/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.11.0.tgz#41fe1589927d4342b2e79268200279da0609daa7" @@ -3418,15 +2968,6 @@ resolved "https://registry.yarnpkg.com/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz#6b3f9aa7e2698c0d78e644037c6778b5c1da12ce" integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw== -"@wordpress/primitives@^3.13.0", "@wordpress/primitives@^3.56.0": - version "3.56.0" - resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-3.56.0.tgz#4513180bd783edeb09c4eb721fb1adff84c0e5bd" - integrity sha512-NXBq1ODjl6inMWx/l7KCbATcjdoeIOqYeL9i9alqdAfWeKx1EH9PIvKWylIkqZk7erXxCxldiRkuyjTtwjNBxw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/element" "^5.35.0" - clsx "^2.1.1" - "@wordpress/priority-queue@*": version "3.11.0" resolved "https://registry.yarnpkg.com/@wordpress/priority-queue/-/priority-queue-3.11.0.tgz#01e1570a7a29372bb1d07cd22fd9cbc5b5d03b09" @@ -3435,14 +2976,6 @@ "@babel/runtime" "7.25.7" requestidlecallback "^0.3.0" -"@wordpress/priority-queue@^2.22.0", "@wordpress/priority-queue@^2.58.0": - version "2.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/priority-queue/-/priority-queue-2.58.0.tgz#02564bbb0700d9fdd93039149e44e9992b16ebd3" - integrity sha512-W+qCS8HJWsXG8gE6yK/H/IObowcghPrQMM3cQHtfd/U05yFNU1Bd/fbj3AO1fVRztktS47lIpi9m3ll1evPEHA== - dependencies: - "@babel/runtime" "^7.16.0" - requestidlecallback "^0.3.0" - "@wordpress/private-apis@*": version "1.11.0" resolved "https://registry.yarnpkg.com/@wordpress/private-apis/-/private-apis-1.11.0.tgz#d2b2e61d0b5a45638d7fd5760ea2c3a644a72551" @@ -3450,13 +2983,6 @@ dependencies: "@babel/runtime" "7.25.7" -"@wordpress/private-apis@^0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@wordpress/private-apis/-/private-apis-0.40.0.tgz#0b2eb46599db5a669cf5d950a36745c7c17eb59b" - integrity sha512-ZX/9Y8eA3C3K6LOj32bHFj+9tNV819CBd8+chqMmmlvQRcTngiuXbMbnSdZnnAr1gLQgNpH9PJ60dIwJnGSEtQ== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/redux-routine@*": version "5.11.0" resolved "https://registry.yarnpkg.com/@wordpress/redux-routine/-/redux-routine-5.11.0.tgz#244e50a0007ac4cd8105b954bbf17bc418de714e" @@ -3467,33 +2993,6 @@ is-promise "^4.0.0" rungen "^0.3.2" -"@wordpress/redux-routine@^4.22.0", "@wordpress/redux-routine@^4.58.0": - version "4.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/redux-routine/-/redux-routine-4.58.0.tgz#b4d10267d196f9bb50059a322191c86684ef366d" - integrity sha512-r0mMWFeJr93yPy2uY/M5+gdUUYj0Zu8+21OFFb5hyQ0z7UHIa3IdgQxzCaTbV1LDA1ZYJrjHeCnA6s4gNHjA2Q== - dependencies: - "@babel/runtime" "^7.16.0" - is-plain-object "^5.0.0" - is-promise "^4.0.0" - rungen "^0.3.2" - -"@wordpress/rich-text@^5.13.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@wordpress/rich-text/-/rich-text-5.20.0.tgz#c1e367f3503b5e9d89e949afe60fa11cc4facc40" - integrity sha512-7W4PksJ6/SnQ+KuwvZ0dlKSwbaS6ejvWBm2N8R5S79AzbdmB69BpDCz0U/GUfGDXDhrU9dpzg5NIivoW2LC8Kg== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/a11y" "^3.22.0" - "@wordpress/compose" "^5.20.0" - "@wordpress/data" "^7.6.0" - "@wordpress/deprecated" "^3.22.0" - "@wordpress/element" "^4.20.0" - "@wordpress/escape-html" "^2.22.0" - "@wordpress/i18n" "^4.22.0" - "@wordpress/keycodes" "^3.22.0" - memize "^1.1.0" - rememo "^4.0.0" - "@wordpress/scripts@^30.3.0": version "30.4.0" resolved "https://registry.yarnpkg.com/@wordpress/scripts/-/scripts-30.4.0.tgz#c44d1877e0cd43b91583b95b7e6920615c6ade8f" @@ -3578,14 +3077,6 @@ "@babel/runtime" "7.25.7" "@wordpress/is-shallow-equal" "*" -"@wordpress/undo-manager@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@wordpress/undo-manager/-/undo-manager-0.18.0.tgz#f087eaf7c42b67f96af2d3bc90ccdf27c741c988" - integrity sha512-upbzPEToa095XG+2JXLHaolF1LfXEMFS0lNMYV37myoUS+eZ7/tl9Gx+yU2+OqWy57TMwx33NlWUX/n+ynzPRw== - dependencies: - "@babel/runtime" "^7.16.0" - "@wordpress/is-shallow-equal" "^4.58.0" - "@wordpress/url@*": version "4.11.0" resolved "https://registry.yarnpkg.com/@wordpress/url/-/url-4.11.0.tgz#d62bc612b45cf3776a00fabd6b4e0d0b264a4550" @@ -3594,24 +3085,11 @@ "@babel/runtime" "7.25.7" remove-accents "^0.5.0" -"@wordpress/url@^3.4.1", "@wordpress/url@^3.59.0": - version "3.59.0" - resolved "https://registry.yarnpkg.com/@wordpress/url/-/url-3.59.0.tgz#6453180452d2e00f3ba45177c4340cf0ca4ad90d" - integrity sha512-GxvoMjYCav0w4CiX0i0h3qflrE/9rhLIZg5aPCQjbrBdwTxYR3Exfw0IJYcmVaTKXQOUU8fOxlDxULsbLmKe9w== - dependencies: - "@babel/runtime" "^7.16.0" - remove-accents "^0.5.0" - "@wordpress/warning@*": version "3.11.0" resolved "https://registry.yarnpkg.com/@wordpress/warning/-/warning-3.11.0.tgz#36c5a1c024a96c712ce1e5746be08009d84213f6" integrity sha512-tXCsxlMAYXbRCgZmVHsBkoBGnrytZPGGezGXANRTsyJ00QoQJgxvnH6u22Rs/NOIVHQ5o65/9jKC3g0e6qn7PA== -"@wordpress/warning@^2.15.0": - version "2.58.0" - resolved "https://registry.yarnpkg.com/@wordpress/warning/-/warning-2.58.0.tgz#1f2f2cd10302daa4e6d391037a49f875e8d12fcc" - integrity sha512-9bZlORhyMY2nbWozeyC5kqJsFzEPP4DCLhGmjtbv+YWGHttUrxUZEfrKdqO+rUODA8rP5zeIly1nCQOUnkw4Lg== - "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -3679,21 +3157,6 @@ agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: dependencies: debug "^4.3.4" -airbnb-prop-types@^2.14.0, airbnb-prop-types@^2.15.0, airbnb-prop-types@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" - integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== - dependencies: - array.prototype.find "^2.1.1" - function.prototype.name "^1.1.2" - is-regex "^1.1.0" - object-is "^1.1.2" - object.assign "^4.1.0" - object.entries "^1.1.2" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.13.1" - ajv-errors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -3859,17 +3322,6 @@ array-uniq@^1.0.1: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== -array.prototype.find@^2.1.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.2.3.tgz#675a233dbcd9b65ecf1fb3f915741aebc45461e6" - integrity sha512-fO/ORdOELvjbbeIfZfzrXFMhYHGofRGqd+am9zm3tZ4GlJINj/pA2eITyfd65Vg6+ZbHd/Cys7stpoRSWtQFdA== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" - array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" @@ -3894,7 +3346,7 @@ array.prototype.findlastindex@^1.2.5: es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" -array.prototype.flat@^1.2.1, array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== @@ -4212,11 +3664,6 @@ body-parser@1.20.3: type-is "~1.6.18" unpipe "1.0.0" -body-scroll-lock@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec" - integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg== - bonjour-service@^1.0.11: version "1.2.1" resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" @@ -4252,11 +3699,6 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -brcast@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.2.tgz#2db16de44140e418dc37fab10beec0369e78dcef" - integrity sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg== - browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" @@ -4483,7 +3925,7 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== -classnames@^2.3.1: +classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -4496,7 +3938,7 @@ clean-webpack-plugin@^3.0.0: "@types/webpack" "^4.4.31" del "^4.1.1" -clipboard@^2.0.11, clipboard@^2.0.8: +clipboard@^2.0.11: version "2.0.11" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== @@ -4534,11 +3976,6 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4561,7 +3998,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colord@^2.7.0, colord@^2.9.3: +colord@^2.9.3: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== @@ -4633,11 +4070,6 @@ compression@^1.7.4: safe-buffer "5.2.1" vary "~1.1.2" -compute-scroll-into-view@^1.0.17: - version "1.0.20" - resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43" - integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -4660,11 +4092,6 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -"consolidated-events@^1.1.1 || ^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91" - integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ== - constant-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" @@ -4735,7 +4162,7 @@ core-js-pure@^3.23.3: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.39.0.tgz#aa0d54d70a15bdc13e7c853db87c10abc30d68f3" integrity sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg== -core-js@^2.4.0, core-js@^2.6.5: +core-js@^2.4.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== @@ -5096,11 +4523,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" - integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== - deepmerge@^4.2.2, deepmerge@^4.3.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -5127,7 +4549,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -5225,11 +4647,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -direction@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/direction/-/direction-1.0.4.tgz#2b86fb686967e987088caf8b89059370d4837442" - integrity sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ== - dns-packet@^5.2.2: version "5.6.1" resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" @@ -5251,13 +4668,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -document.contains@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/document.contains/-/document.contains-1.0.2.tgz#4260abad67a6ae9e135c1be83d68da0db169d5f0" - integrity sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q== - dependencies: - define-properties "^1.1.3" - dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -5266,11 +4676,6 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dom-scroll-into-view@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e" - integrity sha512-LwNVg3GJOprWDO+QhLL1Z9MMgWe/KAFLxVWKzjRTxNSPn8/LLDIfmuG71YHznXCqaqTjvHJDYO1MEAgX6XCNbQ== - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -5323,17 +4728,6 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" -downshift@^6.0.15: - version "6.1.12" - resolved "https://registry.yarnpkg.com/downshift/-/downshift-6.1.12.tgz#f14476b41a6f6fd080c340bad1ddf449f7143f6f" - integrity sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA== - dependencies: - "@babel/runtime" "^7.14.8" - compute-scroll-into-view "^1.0.17" - prop-types "^15.7.2" - react-is "^17.0.2" - tslib "^2.3.0" - duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -5429,14 +4823,6 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== -enzyme-shallow-equal@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.7.tgz#4e3aa678022387a68e6c47aff200587851885b5e" - integrity sha512-/um0GFqUXnpM9SvKtje+9Tjoz3f1fpBC3eXRFrNs8kpYn69JljciYP7KZTqM/YQbUY9KUjvKB4jo/q+L6WGGvg== - dependencies: - hasown "^2.0.0" - object-is "^1.1.5" - equivalent-key-map@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/equivalent-key-map/-/equivalent-key-map-0.2.2.tgz#be4d57049bb8d46a81d6e256c1628465620c2a13" @@ -5553,7 +4939,7 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.0.1, es-set-tostringtag@^2.0.3: +es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== @@ -6227,27 +5613,6 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== -framer-motion@^6.2.8: - version "6.5.1" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7" - integrity sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw== - dependencies: - "@motionone/dom" "10.12.0" - framesync "6.0.1" - hey-listen "^1.0.8" - popmotion "11.0.3" - style-value-types "5.0.0" - tslib "^2.1.0" - optionalDependencies: - "@emotion/is-prop-valid" "^0.8.2" - -framesync@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20" - integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA== - dependencies: - tslib "^2.1.0" - fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -6287,7 +5652,7 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.2, function.prototype.name@^1.1.6: +function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== @@ -6408,14 +5773,6 @@ glob@^7.0.3, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -global-cache@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/global-cache/-/global-cache-1.2.1.tgz#39ca020d3dd7b3f0934c52b75363f8d53312c16d" - integrity sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA== - dependencies: - define-properties "^1.1.2" - is-symbol "^1.0.1" - global-modules@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" @@ -6529,11 +5886,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -gradient-parser@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/gradient-parser/-/gradient-parser-0.1.5.tgz#0c7e2179559e5ce7d8d71f4423af937100b2248c" - integrity sha512-+uPlcVbjrKOnTzvz0MjTj7BfACj8OmxIa1moIjJV7btvhUMSJk0D47RfDCgDrZE3dYMz9Cf5xKJwnrKLjUq0KQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -6605,29 +5957,12 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" -hey-listen@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" - integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== - hi-base32@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA== -highlight-words-core@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.3.tgz#781f37b2a220bf998114e4ef8c8cb6c7a4802ea8" - integrity sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ== - -history@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" - integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== - dependencies: - "@babel/runtime" "^7.7.6" - -hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7147,7 +6482,7 @@ is-promise@^4.0.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== -is-regex@^1.1.0, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -7179,18 +6514,13 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" -is-symbol@^1.0.1, is-symbol@^1.0.2, is-symbol@^1.0.3: +is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: has-symbols "^1.0.2" -is-touch-device@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-touch-device/-/is-touch-device-1.0.1.tgz#9a2fd59f689e9a9bf6ae9a86924c4ba805a42eab" - integrity sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw== - is-typed-array@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" @@ -8089,7 +7419,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.1.1, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8275,11 +7605,6 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" -memize@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/memize/-/memize-1.1.0.tgz#4a5a684ac6992a13b1299043f3e49b1af6a0b0d3" - integrity sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg== - memize@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/memize/-/memize-2.1.0.tgz#6ddd4717887d94825748149ece00d04cf868ce0d" @@ -8463,18 +7788,6 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -moment-timezone@^0.5.40: - version "0.5.46" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.46.tgz#a21aa6392b3c6b3ed916cd5e95858a28d893704a" - integrity sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw== - dependencies: - moment "^2.29.4" - -moment@>=1.6.0, moment@^2.26.0, moment@^2.29.4: - version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== - mousetrap@^1.6.5: version "1.6.5" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" @@ -8682,20 +7995,12 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== -object-is@^1.1.2, object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.4, object.assign@^4.1.5: +object.assign@^4.1.4, object.assign@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== @@ -8705,7 +8010,7 @@ object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.4, object.assign@ has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.2, object.entries@^1.1.8: +object.entries@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== @@ -8733,7 +8038,7 @@ object.groupby@^1.0.3: define-properties "^1.2.1" es-abstract "^1.23.2" -object.values@^1.1.0, object.values@^1.1.5, object.values@^1.1.6, object.values@^1.2.0: +object.values@^1.1.6, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== @@ -8994,11 +8299,6 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -9057,16 +8357,6 @@ plur@^4.0.0: dependencies: irregular-plurals "^3.2.0" -popmotion@11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9" - integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA== - dependencies: - framesync "6.0.1" - hey-listen "^1.0.8" - style-value-types "5.0.0" - tslib "^2.1.0" - possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -9405,19 +8695,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types-exact@^1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.5.tgz#f275e7dc0d629c2f7414782e8189b3e2d2e9e158" - integrity sha512-wHDhA5TSSvU07gdzsdeT/FZg6zay94K4Y7swSK4YsRG3moWB0Qsp9g1Y5BBausP1HF8K4UeVe2Xt7ZFJByKp6A== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - hasown "^2.0.2" - isarray "^2.0.5" - object.assign "^4.1.5" - reflect.ownkeys "^1.1.4" - -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -9526,7 +8804,7 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -qs@6.13.0, qs@^6.10.3: +qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== @@ -9553,13 +8831,6 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -9582,46 +8853,6 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" -re-resizable@^6.4.0: - version "6.10.1" - resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.10.1.tgz#d062ca50bbc4ec7ae940f756cba36479e9015bc5" - integrity sha512-m33nSWRH57UZLmep5M/LatkZ2NRqimVD/bOOpvymw5Zf33+eTSEixsUugscOZzAtK0/nx+OSuOf8VbKJx/4ptw== - -react-colorful@^5.3.1: - version "5.6.1" - resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" - integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== - -react-dates@^21.8.0: - version "21.8.0" - resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.8.0.tgz#355c3c7a243a7c29568fe00aca96231e171a5e94" - integrity sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw== - dependencies: - airbnb-prop-types "^2.15.0" - consolidated-events "^1.1.1 || ^2.0.0" - enzyme-shallow-equal "^1.0.0" - is-touch-device "^1.0.1" - lodash "^4.1.1" - object.assign "^4.1.0" - object.values "^1.1.0" - prop-types "^15.7.2" - raf "^3.4.1" - react-moment-proptypes "^1.6.0" - react-outside-click-handler "^1.2.4" - react-portal "^4.2.0" - react-with-direction "^1.3.1" - react-with-styles "^4.1.0" - react-with-styles-interface-css "^6.0.0" - -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - react-dom@^18.3.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -9635,41 +8866,11 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^18.0.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-moment-proptypes@^1.6.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz#7ba4076147f6b5998f0d4f51d302d6d8c62049fd" - integrity sha512-Er940DxWoObfIqPrZNfwXKugjxMIuk1LAuEzn23gytzV6hKS/sw108wibi9QubfMN4h+nrlje8eUCSbQRJo2fQ== - dependencies: - moment ">=1.6.0" - -react-outside-click-handler@^1.2.4: - version "1.3.0" - resolved "https://registry.yarnpkg.com/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#3831d541ac059deecd38ec5423f81e80ad60e115" - integrity sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ== - dependencies: - airbnb-prop-types "^2.15.0" - consolidated-events "^1.1.1 || ^2.0.0" - document.contains "^1.0.1" - object.values "^1.1.0" - prop-types "^15.7.2" - -react-portal@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.2.tgz#bff1e024147d6041ba8c530ffc99d4c8248f49fa" - integrity sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q== - dependencies: - prop-types "^15.5.8" - react-refresh@^0.14.0: version "0.14.2" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" @@ -9700,47 +8901,6 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-with-direction@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-with-direction/-/react-with-direction-1.4.0.tgz#ebdf64d685d0650ce966e872e6431ad5a2485444" - integrity sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg== - dependencies: - airbnb-prop-types "^2.16.0" - brcast "^2.0.2" - deepmerge "^1.5.2" - direction "^1.0.4" - hoist-non-react-statics "^3.3.2" - object.assign "^4.1.2" - object.values "^1.1.5" - prop-types "^15.7.2" - -react-with-styles-interface-css@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/react-with-styles-interface-css/-/react-with-styles-interface-css-6.0.0.tgz#b53da7fa8359d452cb934cface8738acaef7b5fe" - integrity sha512-6khSG1Trf4L/uXOge/ZAlBnq2O2PEXlQEqAhCRbvzaQU4sksIkdwpCPEl6d+DtP3+IdhyffTWuHDO9lhe1iYvA== - dependencies: - array.prototype.flat "^1.2.1" - global-cache "^1.2.1" - -react-with-styles@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-4.2.0.tgz#0b8a8e5d94d082518b9f564f6fcf6103e28096c5" - integrity sha512-tZCTY27KriRNhwHIbg1NkSdTTOSfXDg6Z7s+Q37mtz0Ym7Sc7IOr3PzVt4qJhJMW6Nkvfi3g34FuhtiGAJCBQA== - dependencies: - airbnb-prop-types "^2.14.0" - hoist-non-react-statics "^3.2.1" - object.assign "^4.1.0" - prop-types "^15.7.2" - react-with-direction "^1.3.1" - -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - react@^18.3.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -9808,36 +8968,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reakit-system@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/reakit-system/-/reakit-system-0.15.2.tgz#a485fab84b3942acbed6212c3b56a6ef8611c457" - integrity sha512-TvRthEz0DmD0rcJkGamMYx+bATwnGNWJpe/lc8UV2Js8nnPvkaxrHk5fX9cVASFrWbaIyegZHCWUBfxr30bmmA== - dependencies: - reakit-utils "^0.15.2" - -reakit-utils@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/reakit-utils/-/reakit-utils-0.15.2.tgz#b4d5836e534576bfd175171541d43182ad97f2d2" - integrity sha512-i/RYkq+W6hvfFmXw5QW7zvfJJT/K8a4qZ0hjA79T61JAFPGt23DsfxwyBbyK91GZrJ9HMrXFVXWMovsKBc1qEQ== - -reakit-warning@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/reakit-warning/-/reakit-warning-0.6.2.tgz#9c346ae483eb1f284f2088653f90cabd26dbee56" - integrity sha512-z/3fvuc46DJyD3nJAUOto6inz2EbSQTjvI/KBQDqxwB0y02HDyeP8IWOJxvkuAUGkWpeSx+H3QWQFSNiPcHtmw== - dependencies: - reakit-utils "^0.15.2" - -reakit@^1.3.8: - version "1.3.11" - resolved "https://registry.yarnpkg.com/reakit/-/reakit-1.3.11.tgz#c15360ac43e94fbe4291d233af3ac5040428252e" - integrity sha512-mYxw2z0fsJNOQKAEn5FJCPTU3rcrY33YZ/HzoWqZX0G7FwySp1wkCYW79WhuYMNIUFQ8s3Baob1RtsEywmZSig== - dependencies: - "@popperjs/core" "^2.5.4" - body-scroll-lock "^3.1.5" - reakit-system "^0.15.2" - reakit-utils "^0.15.2" - reakit-warning "^0.6.2" - rechoir@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" @@ -9873,17 +9003,6 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" -reflect.ownkeys@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-1.1.4.tgz#3cf21da448f2aff8aba63ca601f65c99482e692c" - integrity sha512-iUNmtLgzudssL+qnTUosCmnq3eczlrVd1wXrgx/GhiI/8FvwrTYWtCJ9PNvWIRX+4ftupj2WUfB5mu5s9t6LnA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-set-tostringtag "^2.0.1" - globalthis "^1.0.3" - regenerate-unicode-properties@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" @@ -9901,11 +9020,6 @@ regenerator-runtime@^0.10.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" integrity sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w== -regenerator-runtime@^0.13.2: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -9952,16 +9066,11 @@ regjsparser@^0.11.0: dependencies: jsesc "~3.0.2" -rememo@^4.0.0, rememo@^4.0.2: +rememo@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/rememo/-/rememo-4.0.2.tgz#8af1f09fd3bf5809ca0bfd0b803926c67ead8c1e" integrity sha512-NVfSP9NstE3QPNs/TnegQY0vnJnstKQSpcrsI2kBTB3dB2PkdfKdTa+abbjMIDqpc63fE5LfjLgfMst0ULMFxQ== -remove-accents@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.4.tgz#73704abf7dae3764295d475d2b6afac4ea23e4d9" - integrity sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg== - remove-accents@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" @@ -10183,14 +9292,6 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler@^0.23.2: version "0.23.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" @@ -10780,14 +9881,6 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== -style-value-types@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad" - integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA== - dependencies: - hey-listen "^1.0.8" - tslib "^2.1.0" - stylehacks@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.1.1.tgz#543f91c10d17d00a440430362d419f79c25545a6" @@ -11129,7 +10222,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.6.2: +tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -11141,11 +10234,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -turbo-combine-reducers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/turbo-combine-reducers/-/turbo-combine-reducers-1.0.2.tgz#aa3650b3c63daa6804d35a4042014f6d31df1e47" - integrity sha512-gHbdMZlA6Ym6Ur5pSH/UWrNQMIM9IqTH6SoL1DbHpqEdQ8i+cFunSmSlFykPt0eGQwZ4d/XTHOl74H0/kFBVWw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -11376,7 +10464,7 @@ urlpattern-polyfill@10.0.0: resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec" integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg== -use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.1.2: +use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== @@ -11396,7 +10484,7 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^8.3.0, uuid@^8.3.2: +uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index 5082bd86f..c461c3611 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -466,7 +466,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { continue; } - $custom_id = $wc_order->get_order_number(); + $custom_id = (string) $wc_order->get_id(); $invoice_id = $this->prefix . $wc_order->get_order_number(); $create_order = $this->capture_card_payment->create_order( $token->get_token(), $custom_id, $invoice_id, $wc_order ); diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php index 51a3a741c..c730136ab 100644 --- a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php @@ -96,52 +96,35 @@ trait CreditCardOrderInfoHandlingTrait { return; } - $fraud_responses = $fraud->to_array(); $card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' ); $card_last_digits = $payment_source->properties()->last_digits ?? __( 'N/A', 'woocommerce-paypal-payments' ); - $avs_response_order_note_title = __( 'Address Verification Result', 'woocommerce-paypal-payments' ); + $response_order_note_title = __( 'PayPal Advanced Card Processing Verification:', 'woocommerce-paypal-payments' ); /* translators: %1$s is AVS order note title, %2$s is AVS order note result markup */ - $avs_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' ); - $avs_response_order_note_result_format = '
    -
  • %1$s
  • -
      -
    • %2$s
    • -
    • %3$s
    • -
    -
  • %4$s
  • -
  • %5$s
  • -
'; - $avs_response_order_note_result = sprintf( - $avs_response_order_note_result_format, - /* translators: %s is fraud AVS code */ - sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['avs_code'] ) ), - /* translators: %s is fraud AVS address match */ - sprintf( __( 'Address Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['address_match'] ) ), - /* translators: %s is fraud AVS postal match */ - sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) ), - /* translators: %s is card brand */ - sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) ), - /* translators: %s card last digits */ - sprintf( __( 'Card Last Digits: %s', 'woocommerce-paypal-payments' ), esc_html( $card_last_digits ) ) + $response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' ); + $response_order_note_result_format = '
    +
  • %1$s
  • +
  • %2$s
  • +
  • %3$s
  • +
'; + $response_order_note_result = sprintf( + $response_order_note_result_format, + /* translators: %1$s is card brand and %2$s card last 4 digits */ + sprintf( __( 'Card: %1$s (%2$s)', 'woocommerce-paypal-payments' ), $card_brand, $card_last_digits ), + /* translators: %s is fraud AVS message */ + sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), $fraud->get_avs_code_message() ), + /* translators: %s is fraud CVV message */ + sprintf( __( 'CVV: %s', 'woocommerce-paypal-payments' ), $fraud->get_cvv2_code_message() ), ); - $avs_response_order_note = sprintf( - $avs_response_order_note_format, - esc_html( $avs_response_order_note_title ), - wp_kses_post( $avs_response_order_note_result ) + $response_order_note = sprintf( + $response_order_note_format, + esc_html( $response_order_note_title ), + wp_kses_post( $response_order_note_result ) ); - $wc_order->add_order_note( $avs_response_order_note ); - - $cvv_response_order_note_format = '
  • %1$s
'; - $cvv_response_order_note = sprintf( - $cvv_response_order_note_format, - /* translators: %s is fraud CVV match */ - sprintf( __( 'CVV2 Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['cvv_match'] ) ) - ); - $wc_order->add_order_note( $cvv_response_order_note ); + $wc_order->add_order_note( $response_order_note ); $meta_details = array_merge( - $fraud_responses, + $fraud->to_array(), array( 'card_brand' => $card_brand, 'card_last_digits' => $card_last_digits, diff --git a/modules/ppcp-wc-gateway/src/Settings/Settings.php b/modules/ppcp-wc-gateway/src/Settings/Settings.php index d5465eb72..182fba281 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Settings.php +++ b/modules/ppcp-wc-gateway/src/Settings/Settings.php @@ -5,7 +5,7 @@ * @package WooCommerce\PayPalCommerce\WcGateway\Settings */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; @@ -18,44 +18,46 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; */ class Settings implements ContainerInterface { - const KEY = 'woocommerce-ppcp-settings'; + const KEY = 'woocommerce-ppcp-settings'; + const CONNECTION_TAB_ID = 'ppcp-connection'; - const PAY_LATER_TAB_ID = 'ppcp-pay-later'; + + const PAY_LATER_TAB_ID = 'ppcp-pay-later'; /** * The settings. * * @var array */ - private $settings = array(); + private array $settings = array(); /** * The list of selected default button locations. * * @var string[] */ - protected $default_button_locations; + protected array $default_button_locations; /** * The list of selected default pay later button locations. * * @var string[] */ - protected $default_pay_later_button_locations; + protected array $default_pay_later_button_locations; /** * The list of selected default pay later messaging locations. * * @var string[] */ - protected $default_pay_later_messaging_locations; + protected array $default_pay_later_messaging_locations; /** * The default ACDC gateway title. * * @var string */ - protected $default_dcc_gateway_title; + protected string $default_dcc_gateway_title; /** * A helper for mapping the new/old settings. @@ -67,11 +69,17 @@ class Settings implements ContainerInterface { /** * Settings constructor. * - * @param string[] $default_button_locations The list of selected default button locations. - * @param string $default_dcc_gateway_title The default ACDC gateway title. - * @param string[] $default_pay_later_button_locations The list of selected default pay later button locations. - * @param string[] $default_pay_later_messaging_locations The list of selected default pay later messaging locations. - * @param SettingsMapHelper $settings_map_helper A helper for mapping the new/old settings. + * @param string[] $default_button_locations The list of selected default + * button locations. + * @param string $default_dcc_gateway_title The default ACDC gateway + * title. + * @param string[] $default_pay_later_button_locations The list of selected default + * pay later button locations. + * @param string[] $default_pay_later_messaging_locations The list of selected default + * pay later messaging + * locations. + * @param SettingsMapHelper $settings_map_helper A helper for mapping the + * new/old settings. */ public function __construct( array $default_button_locations, @@ -90,10 +98,11 @@ class Settings implements ContainerInterface { /** * Returns the value for an id. * - * @param string $id The value identificator. + * @throws NotFoundException When nothing was found. + * + * @param string $id The value identifier. * * @return mixed - * @throws NotFoundException When nothing was found. */ public function get( $id ) { if ( ! $this->has( $id ) ) { @@ -106,23 +115,24 @@ class Settings implements ContainerInterface { /** * Whether a value exists. * - * @param string $id The value identificator. + * @param string $id The value identifier. * * @return bool */ - public function has( $id ) { + public function has( string $id ) { if ( $this->settings_map_helper->has_mapped_key( $id ) ) { return true; } $this->load(); + return array_key_exists( $id, $this->settings ); } /** * Sets a value. * - * @param string $id The value identificator. + * @param string $id The value identifier. * @param mixed $value The value. */ public function set( $id, $value ) { @@ -142,7 +152,7 @@ class Settings implements ContainerInterface { * * @return bool */ - private function load(): bool { + private function load() : bool { if ( $this->settings ) { return false; } @@ -175,6 +185,7 @@ class Settings implements ContainerInterface { } $this->settings[ $key ] = $value; } + return true; } } diff --git a/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrar.php b/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrar.php index 174e60e6b..a65443b80 100644 --- a/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrar.php +++ b/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrar.php @@ -21,9 +21,14 @@ class TaskRegistrar implements TaskRegistrarInterface { * * @throws RuntimeException If problem registering. */ - public function register( array $tasks ): void { + public function register( string $list_id, array $tasks ): void { + $task_lists = TaskLists::get_lists(); + if ( ! isset( $task_lists[ $list_id ] ) ) { + return; + } + foreach ( $tasks as $task ) { - $added_task = TaskLists::add_task( 'extended', $task ); + $added_task = TaskLists::add_task( $list_id, $task ); if ( $added_task instanceof WP_Error ) { throw new RuntimeException( $added_task->get_error_message() ); } diff --git a/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrarInterface.php b/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrarInterface.php index 992b41150..0f8ee555d 100644 --- a/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrarInterface.php +++ b/modules/ppcp-wc-gateway/src/Settings/WcTasks/Registrar/TaskRegistrarInterface.php @@ -15,11 +15,12 @@ use RuntimeException; interface TaskRegistrarInterface { /** - * Registers the tasks inside "Things to do next" WC section. + * Registers the tasks inside the section with given list ID. * + * @param string $list_id The list ID. * @param Task[] $tasks The list of tasks. * @return void * @throws RuntimeException If problem registering. */ - public function register( array $tasks ): void; + public function register( string $list_id, array $tasks ): void; } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 9d57421ae..f754a3cbe 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -13,6 +13,7 @@ use Exception; use Psr\Log\LoggerInterface; use Throwable; use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -547,6 +548,33 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul } ); + add_filter( + 'woocommerce_paypal_payments_rest_common_merchant_data', + function( array $merchant_data ) use ( $c ): array { + if ( ! isset( $merchant_data['features'] ) ) { + $merchant_data['features'] = array(); + } + + $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); + assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + + $reference_transactions_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); + $merchant_data['features']['save_paypal_and_venmo'] = array( + 'enabled' => $reference_transactions_enabled, + ); + + $dcc_product_status = $c->get( 'wcgateway.helper.dcc-product-status' ); + assert( $dcc_product_status instanceof DCCProductStatus ); + + $dcc_enabled = $dcc_product_status->dcc_is_active(); + $merchant_data['features']['advanced_credit_and_debit_cards'] = array( + 'enabled' => $dcc_enabled, + ); + + return $merchant_data; + } + ); + return true; } @@ -653,7 +681,10 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul $listener = $container->get( 'wcgateway.settings.listener' ); assert( $listener instanceof SettingsListener ); - $listener->listen_for_merchant_id(); + $use_new_ui = $container->get( 'wcgateway.settings.admin-settings-enabled' ); + if ( ! $use_new_ui ) { + $listener->listen_for_merchant_id(); + } try { $listener->listen_for_vaulting_enabled(); @@ -880,10 +911,11 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul if ( empty( $simple_redirect_tasks ) ) { return; } + $task_registrar = $container->get( 'wcgateway.settings.wc-tasks.task-registrar' ); assert( $task_registrar instanceof TaskRegistrarInterface ); - $task_registrar->register( $simple_redirect_tasks ); + $task_registrar->register( 'extended', $simple_redirect_tasks ); } catch ( Exception $exception ) { $logger->error( "Failed to create a task in the 'Things to do next' section of WC. " . $exception->getMessage() ); } diff --git a/package.json b/package.json index 6dc952728..cf4a58b7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.9.5", + "version": "2.9.6", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index ca2b8a27c..c35d7f244 100644 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ === WooCommerce PayPal Payments === Contributors: woocommerce, automattic, syde Tags: woocommerce, paypal, payments, ecommerce, credit card -Requires at least: 6.3 +Requires at least: 6.5 Tested up to: 6.7 Requires PHP: 7.4 -Stable tag: 2.9.5 +Stable tag: 2.9.6 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -179,23 +179,35 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == -= 2.9.5 - xxxx-xx-xx = -Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 -Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 -Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 -Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752 -Fix - Additional payment tokens saved with new customer_id #2820 -Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809 -Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845 -Fix - Restore accidentally removed ACDC currencies #2838 -Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712 -Enhancement - Allow disabling specific card types for Fastlane #2704 -Enhancement - Fastlane Insights SDK implementation for block Checkout #2737 -Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703 -Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792 -Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772 -Enhancement - Disable “Add payment method” button while saving ACDC payment #2794 -Enhancement - Sanitize soft_descriptor field #2846 #2854 += 2.9.6 - XXXX-XX-XX = +* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955 +* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872 +* Fix - String translations not applied to Card Fields on Block Checkout #2934 +* Fix - Fastlane component included in script when Fastlane is disabled #2911 +* Fix - Zero amount line items may trigger CANNOT_BE_ZERO_OR_NEGATIVE error after rounding error #2906 +* Fix - “Save changes” is grey and unclickable when switching from Sandbox to Live #2895 +* Fix - plugin queries variations when button/messaging is disabled on single product page #2896 +* Fix - Use get_id instead of get_order_number on setting custom_id (author @0verscore) #2930 +* Enhancement - Improve fraud response order notes for Advanced Card Processing transactions #2905 +* Tweak - Update the minimum plugin requirements to WordPress 6.5 & WooCommerce 9.2 #2920 + += 2.9.5 - 2024-12-10 = +* Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 +* Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 +* Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 +* Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752 +* Fix - Additional payment tokens saved with new customer_id #2820 +* Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809 +* Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845 +* Fix - Restore accidentally removed ACDC currencies #2838 +* Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712 +* Enhancement - Allow disabling specific card types for Fastlane #2704 +* Enhancement - Fastlane Insights SDK implementation for block Checkout #2737 +* Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703 +* Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792 +* Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772 +* Enhancement - Disable “Add payment method” button while saving ACDC payment #2794 +* Enhancement - Sanitize soft_descriptor field #2846 #2854 = 2.9.4 - 2024-11-11 = * Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755 diff --git a/src/FilePathPluginFactory.php b/src/FilePathPluginFactory.php index e62fe1add..34f028ffa 100644 --- a/src/FilePathPluginFactory.php +++ b/src/FilePathPluginFactory.php @@ -85,7 +85,7 @@ class FilePathPluginFactory implements FilePathPluginFactoryInterface { 'Title' => '', 'Description' => '', 'TextDomain' => '', - 'RequiresWP' => '6.3', + 'RequiresWP' => '6.5', 'RequiresPHP' => '7.4', ), $plugin_data diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 1544895ca..d9e1a2277 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,14 +3,15 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.9.5 + * Version: 2.9.6 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.4 * Requires Plugins: woocommerce - * WC requires at least: 6.9 - * WC tested up to: 9.4 + * Requires at least: 6.5 + * WC requires at least: 9.2 + * WC tested up to: 9.5 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -26,7 +27,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); define( 'PAYPAL_URL', 'https://www.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2024-12-02' ); +define( 'PAYPAL_INTEGRATION_DATE', '2024-12-31' ); define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );