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 b0ffc609c..1efeb16a3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,22 +1,34 @@ *** Changelog *** += 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 +* 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/images/icon-button-payment-method-multibanco.svg b/modules/ppcp-settings/images/icon-button-payment-method-multibanco.svg new file mode 100644 index 000000000..9d423223c --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-multibanco.svg @@ -0,0 +1 @@ +Logo_Multibanco diff --git a/modules/ppcp-settings/images/icon-button-payment-method-mybank.svg b/modules/ppcp-settings/images/icon-button-payment-method-mybank.svg new file mode 100644 index 000000000..82dd40ca4 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-mybank.svg @@ -0,0 +1,28 @@ + + + + MyBank + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-oxxo.svg b/modules/ppcp-settings/images/icon-button-payment-method-oxxo.svg new file mode 100644 index 000000000..4f69e152d --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-oxxo.svg @@ -0,0 +1,18 @@ + + + + logo OXXO + Created with Sketch. + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-przelewy24.svg b/modules/ppcp-settings/images/icon-button-payment-method-przelewy24.svg new file mode 100644 index 000000000..3ab7a31be --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-przelewy24.svg @@ -0,0 +1,38 @@ + + + + logo P24 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-ratepay.svg b/modules/ppcp-settings/images/icon-button-payment-method-ratepay.svg new file mode 100644 index 000000000..f0da1b689 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-ratepay.svg @@ -0,0 +1,3 @@ + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-trustly.svg b/modules/ppcp-settings/images/icon-button-payment-method-trustly.svg new file mode 100644 index 000000000..85bfacbe0 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-trustly.svg @@ -0,0 +1 @@ + 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 613403b67..10f427ea9 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -10,10 +10,12 @@ $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; -$color-border:#AEAEAE; +$color-border: #AEAEAE; +$color-divider: #F0F0F0; $shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15); $shadow-selection-box: 0 2px 4px 0 rgba(0, 0, 0, 0.1); @@ -24,10 +26,32 @@ $max-width-onboarding: 1024px; $max-width-onboarding-content: 500px; $max-width-settings: 938px; +$card-vertical-gap: 48px; + +/* define custom theming options */ + +:root { + --ppcp-color-app-bg: #{$color-white}; +} + #ppcp-settings-container { --max-width-settings: #{$max-width-settings}; --max-width-onboarding: #{$max-width-onboarding}; --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/_accordion-section.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss index 28ac713ce..d4894abd8 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss @@ -2,26 +2,27 @@ margin-left: auto; margin-right: auto; - &--title { + &__toggler { + display: block; + cursor: pointer; + + background: transparent; + border: 0; + box-shadow: none; + padding: 0; + margin: 24px auto; + } + + &__title-wrapper { @include font(14, 32, 450); color: $color-gray-900; display: flex; align-items: center; gap: 16px; - margin: 24px auto; - border: 0; - background: transparent; - cursor: pointer; } - &--content { + &__content { margin: 24px 0 0; } - - &.ppcp--is-open { - .ppcp-r-accordion--icon { - transform: rotate(180deg); - } - } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss index 427b4b1fb..74fb531ee 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss @@ -33,5 +33,20 @@ margin: 6px 0px 0px 0px; width: fit-content; } + + @media screen and (max-width: 480px) { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + flex-direction: column; + + .ppcp-r-badge-box__title-text:not(:empty) + .ppcp-r-badge-box__title-image-badge { + margin: 0px; + img:first-of-type { + margin: 0px; + } + } + } } } 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 0afb0a304..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,47 +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-black; - } + --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/_fields.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss index 4b6cadfd8..1a9cf102c 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss @@ -58,13 +58,13 @@ position: relative; label { - @include font(14, 20, 400); + @include font(13, 20, 400); color: $color-gray-800; } } &__radio-description { - @include font(14, 20, 400); + @include font(13, 20, 400); margin: 0; color: $color-gray-800; } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss index 09be265c1..a766433e0 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss @@ -1,64 +1,111 @@ .ppcp-r-navigation-container { - padding: 24px 48px; + position: sticky; + top: var(--wp-admin--admin-bar--height); + z-index: 10; + + padding: 10px 48px; margin: 0 -20px 48px -20px; - border-bottom: 1px solid $color-gray-300; - position: relative; + + box-shadow: 0 -1px 0 0 $color-gray-300 inset; + background: var(--ppcp-color-app-bg); + transition: box-shadow 0.3s; + + --wp-components-color-accent: #{$color-blueberry}; + --color-text: #{$color-gray-900}; + --color-disabled: #CCC; .ppcp-r-navigation { display: flex; justify-content: space-between; align-items: center; + height: 40px; - button.is-primary { - padding: 10px 18px; - justify-content: center; - margin: 0 0 0 12px; - &:not(:disabled) { - background-color: $color-blueberry; + .components-button { + @include font(13, 20, 400); + + &.is-primary { + background-color: var(--wp-components-color-accent); + padding: 10px 16px; + justify-content: center; + margin: 0 0 0 12px; + border-radius: 2px; + + &:disabled { + background-color: var(--color-disabled); + } } - } - button.is-tertiary { - @include font(16, 24, 600); - color: $color-gray-900; - &:hover{ - background-color:none; - background:none; + &.is-link { + color: var(--wp-components-color-accent); + text-decoration: none; + + &:disabled { + color: var(--color-disabled); + } + } + + &.is-title { + @include font(16, 24, 600); + color: var(--color-text); + + .title { + margin-left: 18px; + } + + .big { + @include font(20, 28, 400); + } } } &--left { - &__link { - @include font(20, 28, 400); - color: $color-gray-900; - text-decoration: none; - padding: 0 0 0 18px; - } + align-items: center; + display: inline-flex; } - &--right a{ - @include font(13, 20, 400); - color: $color-blueberry; - text-decoration: none; + &--right { + .is-link { + padding: 10px 16px; + } } &--progress-bar { position: absolute; - bottom: 0px; + bottom: 0; left: 0; - background-color: $color-blueberry; + background-color: var(--wp-components-color-accent); height: 4px; + transition: width 0.3s; } } - @media screen and (max-width: 480px) { - padding: 24px 35px; + &.is-scrolled { + box-shadow: 0 -1px 0 0 $color-gray-300 inset, 0 8px 8px 0 rgba(85, 93, 102, .3); + } + + @media screen and (max-width: 782px) { + padding: 10px 12px; + .ppcp-r-navigation { - flex-wrap: wrap; row-gap: 8px; + white-space: nowrap; + + &--right { + position: absolute; + right: 10px; + z-index: 10; + background: var(--ppcp-color-app-bg); + box-shadow: -5px 0 8px var(--ppcp-color-app-bg); + } &--progress-bar { - display: none; + height: 2px; + } + + .components-button.is-title { + .title { + margin-left: 4px; + } } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss index d6d8cf4f3..70348532e 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss @@ -31,5 +31,9 @@ @include font(14, 22, 400); margin: 0 20%; text-align: center; + + @media screen and (max-width: 480px) { + margin: 0px; + } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss index 3ca44193c..c85c5162e 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss @@ -1,75 +1,78 @@ -.ppcp-r-payment-method-item-list { - display: flex; - flex-wrap: wrap; - gap: 16px; -} - -.ppcp-r-payment-method-item { - display: flex; - align-items: flex-start; - width: calc(100% / 3 - 32px / 3); - border: 1px solid $color-gray-300; - padding: 16px; - border-radius: 8px; - min-height: 200px; - - @media screen and (max-width: 767px) { - width: calc(50% - 8px); - } - - @media screen and (max-width: 480px) { - width: 100%; - } - - &__wrap { +.ppcp-r-settings-block__payment-methods { + &.ppcp-r-settings-block { display: flex; - flex-direction: column; - height: 100%; + flex-wrap: wrap; + flex-direction: row; + gap: 16px; } - &__title-wrap { + &__item { display: flex; - align-items: center; - margin: 0 0 8px 0; - gap: 12px; - } + align-items: flex-start; + width: calc(100% / 3 - 32px / 3); + border: 1px solid $color-gray-300; + padding: 16px; + border-radius: 8px; + min-height: 200px; - &__content { - p { - margin: 0; - color: $color-text-tertiary; - @include font(13, 20, 400); + @media screen and (max-width: 767px) { + width: calc(50% - 8px); } - margin: 0 0 12px 0; - } + @media screen and (max-width: 480px) { + width: 100%; + } - &__title { - @include font(13, 20, 500); - color: $color-black; - display: block; - } + &__inner { + display: flex; + flex-direction: column; + height: 100%; + } - &__settings-button { - line-height: 0; - transition: 0.2s ease-out transform; - transform: rotate(0deg); - zoom: 1.005; + &__title-wrapper { + display: flex; + align-items: center; + margin: 0 0 8px 0; + gap: 12px; + } - &:hover { - transform: rotate(45deg); - cursor: pointer; + &__description { + p { + margin: 0; + color: $color-text-tertiary; + @include font(13, 20, 400); + } + + margin: 0 0 12px 0; + } + + &__title { + @include font(13, 20, 500); + color: $color-black; + display: block; + } + + &__settings { + line-height: 0; + transition: 0.2s ease-out transform; + transform: rotate(0deg); + zoom: 1.005; + + &:hover { + transform: rotate(45deg); + cursor: pointer; + } + } + + button.is-secondary { + @include small-button; + } + + &__footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; } } - - button.is-secondary { - @include small-button; - } - - &__footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: auto; - } } 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/_settings-wrapper.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss index dd83011c0..a13df6e77 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss @@ -16,6 +16,17 @@ } } + &-settings { + > * { + margin-bottom: $card-vertical-gap; + } + + > *:not(:last-child) { + padding-bottom: $card-vertical-gap; + border-bottom: 1px solid $color-gray-200; + } + } + &-settings-card { @media screen and (min-width: 960px) { display: flex; @@ -26,6 +37,12 @@ padding: 24px; } + &__content-wrapper { + display: flex; + flex-direction: column; + gap: 24px; + } + &__header { display: flex; gap: 18px; @@ -43,21 +60,25 @@ } &__content { + border: 1px solid $color-gray-200; + border-radius: 4px; + padding: 24px; @media screen and (min-width: 960px) { flex: 1; } } &__title { - @include font(16, 24, 600); - color: $color-blueberry; + @include font(13, 24, 600); + color: $color-text-text; margin: 0 0 4px 0; display: block; } + &__description { - @include font(14, 20, 400); - color: $color-gray-800; + @include font(13, 20, 400); + color: $color-text-tertiary; margin: 0; } } 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/reusable-components/_title-badge.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss index 2abd25541..38429a8f7 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss @@ -1,12 +1,11 @@ .ppcp-r-title-badge{ @include font(12, 16, 400); - margin-left:12px; - padding:4px 8px; + padding: 4px 8px; border-radius: 2px; white-space: nowrap; &--positive{ - color:#005C12; - background-color: #EDFAEF; + color: #144722; + background-color: #DAFFE0; } &--negative{ color:#5c0000; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss index 411d5a987..f6dce1407 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss @@ -20,8 +20,7 @@ justify-content: center; @media screen and (max-width: 480px) { - flex-wrap: wrap; - row-gap: 8px; + display: block; } } } diff --git a/modules/ppcp-settings/resources/css/components/screens/_fullscreen.scss b/modules/ppcp-settings/resources/css/components/screens/_fullscreen.scss new file mode 100644 index 000000000..214cc11d7 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/_fullscreen.scss @@ -0,0 +1,18 @@ +body:has(.ppcp-r-container--settings), +body:has(.ppcp-r-container--onboarding) { + background-color: var(--ppcp-color-app-bg) !important; + + .woocommerce-layout, + #woocommerce-layout__primary { + padding: 0 !important; + } + + .notice, + .nav-tab-wrapper.woo-nav-tab-wrapper, + .woocommerce-layout__header, + .wrap.woocommerce form > h2, + #screen-meta-links { + display: none !important; + visibility: hidden; + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss b/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss deleted file mode 100644 index 7878ef729..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss +++ /dev/null @@ -1,8 +0,0 @@ -body:has(.ppcp-r-container--onboarding) { - background-color: #fff !important; - - .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout__header, .wrap.woocommerce form > h2, #screen-meta-links { - display: none !important; - visibility: hidden; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings-global.scss b/modules/ppcp-settings/resources/css/components/screens/_settings-global.scss deleted file mode 100644 index 629d89d76..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/_settings-global.scss +++ /dev/null @@ -1,7 +0,0 @@ -body:has(.ppcp-r-container--settings) { - background-color: #fff !important; - - .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout, .wrap.woocommerce form > h2, #screen-meta-links { - display: none !important; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index c3879a3db..b2f587d6b 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -1,3 +1,6 @@ +@import "./settings/block-accordion"; + +// Container and Tab Settings .ppcp-r-tabs.settings, .ppcp-r-container--settings { --max-container-width: var(--max-width-settings); @@ -6,3 +9,529 @@ max-width: var(--max-container-width); } } + +// Todo List and Feature Items +.ppcp-r-todo-item { + position: relative; + display: flex; + align-items: center; + gap: 18px; + width: 100%; + + &:not(:last-child) { + border-bottom: 1px solid $color-gray-400; + padding-bottom: 16px; + } + + &:not(:first-child) { + padding-top: 16px; + } + + p { + @include font(14, 20, 400); + } + + &__inner { + position: relative; + display: flex; + align-items: center; + gap: 18px; + } + + &__close { + margin-left: auto; + + &:hover { + cursor: pointer; + color: $color-blueberry; + } + } + + .ppcp-r__checkbox { + .components-flex { + gap: 12px; + } + + label { + @include font(13, 20, 400); + color: $color-blueberry; + } + } + + &__description { + @include font(13, 20, 400); + color: $color-blueberry; + } +} + +.ppcp-r-feature-item { + padding-top: 32px; + border-top: 1px solid $color-gray-400; + + &__title { + @include font(16, 20, 600); + color: $color-black; + display: block; + margin: 0 0 8px 0; + } + + &__description { + @include font(14, 20, 400); + color: $color-gray-800; + margin: 0 0 18px 0; + } + + &:not(:last-child) { + padding-bottom: 32px; + } + + &__buttons { + display: flex; + gap: 18px; + } + + &__notes { + display: flex; + flex-direction: column; + + span { + font-weight: 500; + } + + margin-top:24px; + } +} + +// Connection Status +.ppcp-r-connection-status { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + &__status-status { + margin: 0 0 8px 0; + + strong { + @include font(14, 24, 700); + color: $color-black; + } + } + + &__show-all-data { + margin-left: 12px; + } + + &__status-label { + @include font(11, 22, 600); + color: $color-gray-900; + display: block; + text-transform: uppercase; + } + + &__status-value { + @include font(13, 26, 400); + color: $color-text-tertiary; + } + + &__data { + display: flex; + flex-direction: column; + gap: 12px; + } + + &__status-toggle--toggled { + .ppcp-r-connection-status__show-all-data { + transform: rotate(180deg); + } + } + + &__status-row { + display: flex; + flex-direction: column; + * { + user-select: none; + } + strong { + @include font(14, 24, 600); + color: $color-gray-800; + margin-right: 12px; + white-space: nowrap; + } + + .ppcp-r-connection-status__status-toggle { + line-height: 0; + } + &--first { + &:hover { + cursor: pointer; + } + } + } + + @media screen and (max-width: 767px) { + flex-wrap: wrap; + &__status { + width: 100%; + } + &__status-row { + flex-wrap: wrap; + strong { + width: 100%; + } + span { + word-break: break-all; + } + } + } +} + +// Feature Refresh +.ppcp-r-feature-refresh { + display: flex; + gap: 12px; + margin-bottom: 24px; + + &__row { + display: flex; + align-items: center; + } + + &__content { + width: 100%; + + &-title { + @include font(16, 20, 700); + color: $color-black; + display: block; + margin: 0 0 4px 0; + } + + p { + @include font(12, 20, 400); + color: $color-gray-700; + margin: 0; + } + } + + button { + display: flex; + gap: 4px; + @include font(13, 20, 400); + } +} + +// Payment Methods +.ppcp-r-payment-methods { + display: flex; + flex-direction: column; + gap: 48px; +} + +// 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) { + border-bottom: 1px solid $color-divider; + } + } +} + +.ppcp-r-settings-block { + display: flex; + flex-direction: column; + gap: 16px 0; + + &.ppcp-r-settings-block__input, + &.ppcp-r-settings-block__select { + gap: 6px 0; + } + + .ppcp-r-settings-block__header { + display: flex; + flex-direction: column; + gap: 6px; + + &:not(:last-child):not(.ppcp-r-settings-block--accordion__header) { + padding-bottom: 6px; + } + } + + .ppcp-r-settings-block__title { + @include font(11, 22, 600); + color: $color-gray-900; + display: block; + text-transform: uppercase; + + .ppcp-r-title-badge { + text-transform: none; + margin-left: 6px; + } + } + + .ppcp-r-settings-block__title-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + } + + &.ppcp-r-settings-block__feature { + .ppcp-r-settings-block__title { + @include font(13, 20, 600); + color: $color-text-text; + text-transform: none; + } + + .ppcp-r-settings-block__feature__description { + color: $color-gray-700; + @include font(13, 20, 400); + } + } + + &.ppcp-r-settings-block__toggle { + display: flex; + flex-direction: row; + + .ppcp-r-settings-block__title { + color: $color-text-text; + @include font(13, 20, 400); + text-transform: none; + } + } + + .ppcp-r-settings-block__description { + margin: 0; + @include font(13, 20, 400); + color: $color-gray-800; + + &:not(:last-child) { + padding-bottom: 1em; + } + + a { + color: $color-blueberry; + } + + strong { + color: $color-gray-800; + } + } + + .ppcp-r-settings-block__supplementary-title-label { + @include font(13, 20, 400); + color: $color-text-tertiary; + text-transform: none; + margin-left: 5px; + } + + // Types + &--toggle-content { + &.ppcp-r-settings-block--content-visible { + .ppcp-r-settings-block__toggle-content { + transform: rotate(180deg); + } + } + + .ppcp-r-settings-block__header { + user-select: none; + + &:hover { + cursor: pointer; + } + } + } + + &--sandbox-connected { + .ppcp-r-settings-block__content { + margin-top: 24px; + } + + .ppcp-r-connection-status__data { + margin-bottom: 20px; + } + } + + &--connect-sandbox { + button.components-button { + @include small-button; + } + + .ppcp-r__radio-content-additional { + .ppcp-r-vertical-text-control { + width: 100%; + } + + @include vertical-layout-event-gap(24px); + align-items: flex-start; + + input[type='text'] { + width: 100%; + } + } + } + + &--troubleshooting, + &--settings { + > .ppcp-r-settings-block__content > *:not(:last-child) { + padding-bottom: 32px; + margin-bottom: 32px; + border-bottom: 1px solid $color-gray-500; + } + } + + // Fields + input[type='text'] { + border-color: $color-gray-700; + width: 100%; + max-width: 100%; + color: $color-gray-800; + + &::placeholder { + color: $color-gray-700; + } + } + + // MultiSelect control + .ppcp-r { + &__radio-wrapper { + align-items: flex-start; + gap: 12px; + } + + &__radio-content { + display: flex; + flex-direction: column; + gap: 4px; + + label { + font-weight: 600; + } + } + + &__radio-content-additional { + padding-left: 32px; + } + + // Select control styles + &__control { + border-radius: 2px; + border-color: $color-gray-700; + min-height: auto; + padding: 0; + } + + &__input-container { + padding: 0; + margin: 0; + } + + &__value-container { + padding: 0 0 0 7px; + } + + &__indicator { + padding: 5px; + } + + &__indicator-separator { + display: none; + } + + &__value-container--has-value { + .ppcp-r__single-value { + color: $color-gray-800; + } + } + + &__placeholder, + &__single-value { + @include font(13, 20, 400); + } + + &__option { + &--is-selected { + background-color: $color-gray-200; + } + } + } +} + +// Hooks table +.ppcp-r-table { + &__hooks-url { + width: 70%; + padding-right: 20%; + text-align: left; + vertical-align: top; + } + + &__hooks-events { + vertical-align: top; + text-align: left; + width: 40%; + + span { + display: block; + } + } + + td.ppcp-r-table__hooks-url, + td.ppcp-r-table__hooks-events { + padding-top: 12px; + color: $color-gray-800; + @include font(14, 20, 400); + + span { + color: inherit; + @include font(14, 20, 400); + } + } + + th.ppcp-r-table__hooks-url, + th.ppcp-r-table__hooks-events { + @include font(14, 20, 700); + color: $color-gray-800; + border-bottom: 1px solid $color-gray-600; + padding-bottom: 4px; + } +} + +// Settings specific styles +.ppcp-r-settings-card--common-settings .ppcp-r-settings-card__content, +.ppcp-r-settings-card--expert-settings .ppcp-r-settings-card__content { + > .ppcp-r-settings-block { + &:not(:last-child) { + padding-bottom: 32px; + margin-bottom: 32px; + } + } +} + +.ppcp-r-settings-block { + &--order-intent, + &--save-payment-methods { + @include vertical-layout-event-gap(24px); + + > .ppcp-r-settings-block__content { + @include vertical-layout-event-gap(24px); + } + } +} + +.ppcp-r-settings-block--toggle-content { + .ppcp-r-settings-block__content { + margin-top: 32px; + } +} + +.ppcp-r-settings-block__button { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 50px; +} 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 3399a1bc9..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; @@ -78,6 +72,7 @@ border-right: 0; padding-right: 0; padding-bottom: 8px; + margin: 0px; } } } diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-overview.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-overview.scss deleted file mode 100644 index 5d1027043..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-overview.scss +++ /dev/null @@ -1,205 +0,0 @@ -.ppcp-r-tab-overview-todo { - margin: 0 0 48px 0; -} - -.ppcp-r-todo-item { - position: relative; - display: flex; - align-items: center; - gap: 18px; - width: 100%; - - &:not(:last-child) { - border-bottom: 1px solid $color-gray-400; - padding-bottom: 24px; - } - - &:not(:first-child) { - padding-top: 24px; - } - - &__inner { - position: relative; - display: flex; - align-items: center; - gap: 18px; - } - - &__close { - margin-left: auto; - - &:hover { - cursor: pointer; - color: $color-blueberry; - } - } - - .ppcp-r__checkbox { - .components-flex { - gap: 12px; - } - label{ - @include font(13, 20, 400); - color:$color-blueberry; - } - } -} - -.ppcp-r-feature-item { - padding-top: 32px; - border-top: 1px solid $color-gray-400; - - &__title { - @include font(16, 20, 600); - color: $color-black; - display: block; - margin: 0 0 8px 0; - } - - &__description { - @include font(14, 20, 400); - color: $color-gray-800; - margin: 0 0 18px 0; - } - - &:not(:last-child) { - padding-bottom: 32px; - } - - &__buttons { - display: flex; - gap: 18px; - } - - &__notes { - display: flex; - flex-direction: column; - - span { - font-weight: 500; - } - } -} - -.ppcp-r-connection-status { - display: flex; - gap: 32px; - padding-bottom: 48px; - margin-bottom: 48px; - border-bottom: 2px solid $color-gray-500; - - &__status-status { - margin: 0 0 8px 0; - - strong { - @include font(14, 24, 700); - color: $color-black; - } - } - - &__show-all-data { - margin-left: 12px; - } - - &__status-label { - span { - @include font(12, 16, 400); - color: $color-gray-700; - } - } - - &__data { - display: flex; - flex-direction: column; - gap: 12px; - } - - &__status-toggle--toggled { - .ppcp-r-connection-status__show-all-data { - transform: rotate(180deg); - } - } - - &__status-row { - display: flex; - align-items: center; - - * { - user-select: none; - } - - strong { - @include font(14, 24, 600); - color: $color-gray-800; - margin-right: 12px; - white-space: nowrap; - } - - span:not(.ppcp-r-connection-status__status-toggle) { - @include font(14, 24, 400); - color: $color-gray-800; - } - - .ppcp-r-connection-status__status-toggle { - line-height: 0; - } - - &--first { - &:hover { - cursor: pointer; - } - } - } - - @media screen and (max-width: 767px) { - flex-wrap: wrap; - &__status { - width: 100%; - } - &__status-row { - flex-wrap: wrap; - - strong { - width: 100%; - } - - span { - word-break: break-all; - } - } - } -} - -.ppcp-r-feature-refresh { - display: flex; - gap: 12px; - margin-bottom: 24px; - - &__row { - display: flex; - align-items: center; - } - - &__content { - width: 100%; - - &-title { - @include font(16, 20, 700); - color: $color-black; - display: block; - margin: 0 0 4px 0; - } - - p { - @include font(12, 20, 400); - color: $color-gray-700; - margin: 0; - } - } - - button { - display: flex; - gap: 4px; - @include font(13, 20, 400); - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-payment-methods.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-payment-methods.scss deleted file mode 100644 index 556589d03..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-payment-methods.scss +++ /dev/null @@ -1,5 +0,0 @@ -.ppcp-r-payment-methods{ - display: flex; - flex-direction: column; - gap:48px; -} diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-settings.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-settings.scss deleted file mode 100644 index 197d575ea..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-settings.scss +++ /dev/null @@ -1,312 +0,0 @@ -// Global settings styles -.ppcp-r-settings { - @include vertical-layout-event-gap(48px); -} - - -.ppcp-r-settings-card__content { - > .ppcp-r-settings-block { - &:not(:last-child) { - border-bottom: 1.5px solid $color-gray-700; - } - } -} - -.ppcp-r-settings-block { - .ppcp-r-settings-block__header { - display: flex; - gap: 48px; - - &-inner { - display: flex; - flex-direction: column; - gap: 4px; - } - } - - &__action { - margin-left: auto; - } - - &--primary { - > .ppcp-r-settings-block__header { - .ppcp-r-settings-block__title { - @include font(16, 20, 700); - color: $color-black; - } - } - } - - &--secondary { - > .ppcp-r-settings-block__header { - .ppcp-r-settings-block__title { - @include font(16, 20, 600); - color: $color-gray-800; - } - } - } - - &--tertiary { - padding-bottom: 0; - margin-bottom: 24px; - - > .ppcp-r-settings-block__header { - align-items: center; - - .ppcp-r-settings-block__title { - color: $color-gray-800; - @include font(14, 20, 400); - } - } - } - - .ppcp-r-settings-block__description { - margin: 0; - @include font(14, 20, 400); - color: $color-gray-800; - - a { - color: $color-blueberry; - } - - strong { - color: $color-gray-800; - } - } - - // Types - &--toggle-content { - &.ppcp-r-settings-block--content-visible { - .ppcp-r-settings-block__toggle-content { - transform: rotate(180deg); - } - } - - .ppcp-r-settings-block__header { - user-select: none; - - &:hover { - cursor: pointer; - } - } - } - - &--sandbox-connected { - .ppcp-r-settings-block__content { - margin-top: 24px; - } - - button.is-secondary { - @include small-button; - } - - .ppcp-r-connection-status__data { - margin-bottom: 20px; - } - } - - &--expert-rdb{ - @include vertical-layout-event-gap(24px); - } - &--connect-sandbox { - button.components-button { - @include small-button; - } - - .ppcp-r__radio-content-additional { - .ppcp-r-vertical-text-control { - width: 100%; - } - - @include vertical-layout-event-gap(24px); - align-items: flex-start; - - input[type='text'] { - width: 100%; - } - } - } - - &--troubleshooting { - > .ppcp-r-settings-block__content > *:not(:last-child) { - padding-bottom: 32px; - margin-bottom: 32px; - border-bottom: 1px solid $color-gray-500; - } - } - - &--settings{ - > .ppcp-r-settings-block__content > *:not(:last-child){ - padding-bottom: 32px; - margin-bottom: 32px; - border-bottom: 1px solid $color-gray-500; - } - } - - // Fields - input[type='text'] { - border-color: $color-gray-700; - width: 282px; - max-width: 100%; - color: $color-gray-800; - } - - input[type='text'] { - &::placeholder { - color: $color-gray-700; - } - } - - .ppcp-r { - &__radio-wrapper { - align-items: flex-start; - gap: 12px; - } - - &__radio-content { - display: flex; - flex-direction: column; - gap: 4px; - - label { - font-weight: 600; - } - } - - &__radio-content-additional { - padding-left: 32px; - } - } - - // MultiSelect control - .ppcp-r { - &__control { - border-radius: 2px; - border-color: $color-gray-700; - width: 282px; - min-height: auto; - padding: 0; - } - - &__input-container { - padding: 0; - margin: 0; - } - - &__value-container { - padding: 0 0 0 7px; - } - - &__indicator { - padding: 5px; - } - - &__indicator-separator { - display: none; - } - - &__value-container--has-value { - .ppcp-r__single-value { - color: $color-gray-800; - } - } - - &__placeholde, &__single-value { - @include font(13, 20, 400); - } - - &__option { - &--is-selected { - background-color: $color-gray-200; - } - } - } -} - -// Special settings styles - -// Hooks table -.ppcp-r-table { - &__hooks-url { - width: 70%; - padding-right: 20%; - text-align: left; - vertical-align: top; - } - - &__hooks-events { - vertical-align: top; - text-align: left; - width: 40%; - - span { - display: block; - } - } - - td.ppcp-r-table__hooks-url, td.ppcp-r-table__hooks-events { - padding-top: 12px; - color: $color-gray-800; - @include font(14, 20, 400); - - span { - color: inherit; - @include font(14, 20, 400); - } - } - - th.ppcp-r-table__hooks-url, th.ppcp-r-table__hooks-events { - @include font(14, 20, 700); - color: $color-gray-800; - border-bottom: 1px solid $color-gray-600; - padding-bottom: 4px; - } -} - -// Common settings have 48px margin&padding bottom between blocks -.ppcp-r-settings-card--common-settings .ppcp-r-settings-card__content { - > .ppcp-r-settings-block { - &:not(:last-child) { - padding-bottom: 48px; - margin-bottom: 48px; - } - } -} - -// Expert settings have 32px margin&padding bottom between blocks -.ppcp-r-settings-card--expert-settings .ppcp-r-settings-card__content { - > .ppcp-r-settings-block { - &:not(:last-child) { - padding-bottom: 32px; - margin-bottom: 32px; - } - } -} - - -// Order intent block has 32px gap and no lines in between -// Save payment methods block has 32px gap and no lines in between -.ppcp-r-settings-block { - &--order-intent, &--save-payment-methods { - @include vertical-layout-event-gap(32px); - - > .ppcp-r-settings-block__content { - @include vertical-layout-event-gap(32px); - } - } -} - - -// Most primary settings block in the expert settings have 32px space after description -.ppcp-r-settings-block--toggle-content { - .ppcp-r-settings-block__content { - margin-top: 32px; - } -} - -// Common settings have actions aligned top with the text, Expert settings have actions alligned middle with the text -.ppcp-r-settings-card--expert-settings { - .ppcp-r-settings-block__header { - align-items: center; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss new file mode 100644 index 000000000..c77a3eb91 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss @@ -0,0 +1,38 @@ +.ppcp-r-settings-block__accordion { + > .ppcp-r-accordion { + width: 100%; + + .ppcp-r-accordion__toggler { + width: 100%; + margin: 0; + text-align: unset; + } + } + + &.ppcp-r-settings-block { + gap: 0; + + .ppcp-r-settings-block__title { + @include font(13, 20, 600); + color: $color-text-text; + text-transform: none; + } + + .ppcp-r-settings-block--accordion__title { + @include font(14, 20, 600); + } + + .ppcp-r-settings-block--accordion__description { + color: $color-gray-700; + @include font(13, 20, 400); + } + + .ppcp-r-settings-block:not(:last-child) { + &:not(.ppcp-r__radio-content-additional .ppcp-r-settings-block) { + padding-bottom: 32px; + margin-bottom: 32px; + border-bottom: 1px solid $color-divider; + } + } + } +} diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index a1f5b390b..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'; @@ -21,12 +22,9 @@ @import './components/reusable-components/welcome-docs'; @import './components/screens/onboarding'; @import './components/screens/settings'; - @import './components/screens/overview/tab-overview'; - @import './components/screens/overview/tab-payment-methods'; - @import './components/screens/overview/tab-settings'; @import './components/screens/overview/tab-styling'; + @import './components/app'; } @import './components/reusable-components/payment-method-modal'; -@import './components/screens/onboarding-global'; -@import './components/screens/settings-global'; +@import './components/screens/fullscreen'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index 23f01a09c..aff2d9997 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,66 +1,76 @@ -import { useEffect } from '@wordpress/element'; import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; +import classNames from 'classnames'; +import { useAccordionState } from '../../hooks/useAccordionState'; -import { useState } from 'react'; +// Provide defaults for all layout components so the generic version just works. +const DefaultHeader = ( { children, className = '' } ) => ( +
+ { children } +
+); +const DefaultTitleWrapper = ( { children } ) => ( +
{ children }
+); +const DefaultTitle = ( { children } ) => ( + { children } +); +const DefaultAction = ( { children } ) => ( + { children } +); +const DefaultDescription = ( { children } ) => ( +
{ children }
+); + +const AccordionContent = ( { isOpen, children } ) => { + if ( ! isOpen || ! children ) { + return null; + } + return
{ children }
; +}; const Accordion = ( { title, - initiallyOpen = null, - className = '', id = '', - children, + initiallyOpen = null, + description = '', + children = null, + className = '', + + // Layout components can be overridden by the caller + Header = DefaultHeader, + TitleWrapper = DefaultTitleWrapper, + Title = DefaultTitle, + Action = DefaultAction, + Description = DefaultDescription, } ) => { - const determineInitialState = () => { - if ( id && initiallyOpen === null ) { - return window.location.hash === `#${ id }`; - } - return !! initiallyOpen; - }; + const { isOpen, toggleOpen } = useAccordionState( { id, initiallyOpen } ); + const wrapperClasses = classNames( 'ppcp-r-accordion', className, { + 'ppcp--is-open': isOpen, + } ); - const [ isOpen, setIsOpen ] = useState( determineInitialState ); - - useEffect( () => { - const handleHashChange = () => { - if ( id && window.location.hash === `#${ id }` ) { - setIsOpen( true ); - } - }; - - window.addEventListener( 'hashchange', handleHashChange ); - - return () => { - window.removeEventListener( 'hashchange', handleHashChange ); - }; - }, [ id ] ); - - const toggleOpen = ( ev ) => { - setIsOpen( ! isOpen ); - ev?.preventDefault(); - return false; - }; - - const wrapperClasses = [ 'ppcp-r-accordion' ]; - if ( className ) { - wrapperClasses.push( className ); - } - if ( isOpen ) { - wrapperClasses.push( 'ppcp--is-open' ); - } + const icon = isOpen ? chevronUp : chevronDown; return ( -
+
- { isOpen && ( -
{ 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/ConnectionInfo.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js index bfa45013e..5fbfdb1f1 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js @@ -1,5 +1,4 @@ import { __ } from '@wordpress/i18n'; -import data from '../../utils/data'; import { useState } from '@wordpress/element'; const ConnectionInfo = ( { connectionStatusDataDefault } ) => { @@ -7,13 +6,6 @@ const ConnectionInfo = ( { connectionStatusDataDefault } ) => { ...connectionStatusDataDefault, } ); - const showAllData = () => { - setConnectionData( { - ...connectionData, - showAllData: ! connectionData.showAllData, - } ); - }; - const toggleStatusClassName = [ 'ppcp-r-connection-status__status-toggle' ]; if ( connectionData.showAllData ) { @@ -24,43 +16,30 @@ const ConnectionInfo = ( { connectionStatusDataDefault } ) => { return (
-
showAllData() } - > - - { __( 'Email address:', 'woocommerce-paypal-payments' ) } - - { connectionData.email } - - { data().getImage( - 'icon-arrow-down.svg', - 'ppcp-r-connection-status__show-all-data' - ) } +
+ + { __( 'Merchant ID', 'woocommerce-paypal-payments' ) } + + + { connectionData.merchantId } + +
+
+ + { __( 'Email address', 'woocommerce-paypal-payments' ) } + + + { connectionData.email } + +
+
+ + { __( 'Client ID', 'woocommerce-paypal-payments' ) } + + + { connectionData.clientId }
- { connectionData.showAllData && ( - <> -
- - { __( - 'Merchant ID:', - 'woocommerce-paypal-payments' - ) } - - { connectionData.merchantId } -
-
- - { __( - 'Client ID:', - 'woocommerce-paypal-payments' - ) } - - { connectionData.clientId } -
- - ) }
); }; 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 f316a9a90..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,16 +1,15 @@ -import BadgeBox, { BADGE_BOX_TITLE_BIG } 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' ) { + if ( isFastlane && isPayLater && storeCountry === 'US' ) { return (
} 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/PaymentMethodItem.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodItem.js deleted file mode 100644 index dbe9c6971..000000000 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodItem.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Button } from '@wordpress/components'; -import PaymentMethodIcon from './PaymentMethodIcon'; -import { PayPalCheckbox } from './Fields'; -import { useState } from '@wordpress/element'; -import { ToggleControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import data from '../../utils/data'; - -const PaymentMethodItem = ( props ) => { - const [ paymentMethodState, setPaymentMethodState ] = useState(); - const [ modalIsVisible, setModalIsVisible ] = useState( false ); - let Modal = null; - if ( props?.modal ) { - Modal = props.modal; - } - const handleCheckboxState = ( checked ) => { - if ( checked ) { - setPaymentMethodState( props.payment_method_id ); - } else { - setPaymentMethodState( null ); - } - }; - return ( - <> -
-
-
- - - { props.title } - -
-
-

{ props.description }

-
-
- - handleCheckboxState( newValue ) - } - /> -
setModalIsVisible( true ) } - > - { Modal && data().getImage( 'icon-settings.svg' ) } -
-
-
-
- { Modal && modalIsVisible && ( - - ) } - - ); -}; - -export default PaymentMethodItem; 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/SettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlock.js deleted file mode 100644 index d23860b38..000000000 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlock.js +++ /dev/null @@ -1,146 +0,0 @@ -import { Button, ToggleControl, TextControl } from '@wordpress/components'; -import data from '../../utils/data'; -import { useState } from '@wordpress/element'; -import Select, { components } from 'react-select'; - -export const SETTINGS_BLOCK_TYPE_EMPTY = 'empty'; -export const SETTINGS_BLOCK_TYPE_TOGGLE = 'toggle'; -export const SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT = 'toggle-content'; -export const SETTINGS_BLOCK_TYPE_INPUT = 'input'; -export const SETTINGS_BLOCK_TYPE_BUTTON = 'button'; -export const SETTINGS_BLOCK_TYPE_SELECT = 'select'; - -export const SETTINGS_BLOCK_STYLING_TYPE_PRIMARY = 'primary'; -export const SETTINGS_BLOCK_STYLING_TYPE_SECONDARY = 'secondary'; -export const SETTINGS_BLOCK_STYLING_TYPE_TERTIARY = 'tertiary'; - -const SettingsBlock = ( { - className, - title, - description, - children, - style, - actionProps, - tag, -} ) => { - const [ toggleContentVisible, setToggleContentVisible ] = useState( - actionProps?.type !== SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT - ); - - const toggleContent = () => { - if ( actionProps?.type !== SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT ) { - return; - } - setToggleContentVisible( ! toggleContentVisible ); - }; - - const blockClassName = [ 'ppcp-r-settings-block' ]; - - blockClassName.push( 'ppcp-r-settings-block--' + style ); - blockClassName.push( 'ppcp-r-settings-block--' + actionProps?.type ); - - if ( className ) { - blockClassName.push( className ); - } - - if ( - toggleContentVisible && - actionProps?.type === SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT - ) { - blockClassName.push( 'ppcp-r-settings-block--content-visible' ); - } - - return ( -
-
toggleContent() } - > -
- - { title } - { tag && tag } - -

-

- { actionProps?.type !== SETTINGS_BLOCK_TYPE_EMPTY && ( -
- { actionProps?.type === SETTINGS_BLOCK_TYPE_TOGGLE && ( - - actionProps?.callback( - actionProps?.key, - newValue - ) - } - /> - ) } - { actionProps?.type === SETTINGS_BLOCK_TYPE_INPUT && ( - <> - - actionProps?.callback( - actionProps?.key, - newValue - ) - } - /> - - ) } - { actionProps?.type === SETTINGS_BLOCK_TYPE_BUTTON && ( - - ) } - { actionProps?.type === - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT && ( -
- { data().getImage( 'icon-arrow-down.svg' ) } -
- ) } - { actionProps?.type === SETTINGS_BLOCK_TYPE_SELECT && ( - + + ), + description: ( { description } ) => ( + { description } + ), +}; + +const SelectSettingsBlock = ( { + title, + description, + order = DEFAULT_ELEMENT_ORDER, + ...props +} ) => ( + + { 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 new file mode 100644 index 000000000..768fa9387 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js @@ -0,0 +1,9 @@ +const SettingsBlock = ( { className, children } ) => { + const blockClassName = [ 'ppcp-r-settings-block', className ].filter( + Boolean + ); + + return
{ children }
; +}; + +export default SettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js new file mode 100644 index 000000000..296c2c2ad --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js @@ -0,0 +1,42 @@ +// Block Elements +export const Title = ( { children, className = '' } ) => ( + + { children } + +); +export const TitleWrapper = ( { children } ) => ( + { children } +); + +export const SupplementaryLabel = ( { children } ) => ( + + { children } + +); + +export const Description = ( { children, className = '' } ) => ( + + { children } + +); + +export const Action = ( { children } ) => ( +
{ children }
+); + +export const Header = ( { children, className = '' } ) => ( +
+ { children } +
+); + +// Card Elements +export const Content = ( { children } ) => ( +
{ children }
+); + +export const ContentWrapper = ( { children } ) => ( +
{ children }
+); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js new file mode 100644 index 000000000..4f9b01644 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js @@ -0,0 +1,69 @@ +import { PayPalCheckbox, handleCheckboxState } from '../Fields'; +import data from '../../../utils/data'; + +const TodoSettingsBlock = ( { + todos, + setTodos, + todosData, + setTodosData, + className = '', +} ) => { + if ( todosData.length === 0 ) { + return null; + } + + return ( +
+ { todosData.map( ( todo ) => ( + + ) ) } +
+ ); +}; + +const TodoItem = ( props ) => { + return ( +
+
+ +
+ { props.description } +
+
+
+ removeTodo( + props.value, + props.todosData, + props.changeTodos + ) + } + > + { data().getImage( 'icon-close.svg' ) } +
+
+ ); +}; + +const removeTodo = ( todoValue, todosData, changeTodos ) => { + changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) ); +}; + +export default TodoSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js new file mode 100644 index 000000000..b66536b00 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js @@ -0,0 +1,27 @@ +import { ToggleControl } from '@wordpress/components'; +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 } } +
+
+); + +export default ToggleSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/index.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/index.js new file mode 100644 index 000000000..80a5db448 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/index.js @@ -0,0 +1,20 @@ +export { default as SettingsBlock } from './SettingsBlock'; +export { default as ButtonSettingsBlock } from './ButtonSettingsBlock'; +export { default as InputSettingsBlock } from './InputSettingsBlock'; +export { default as SelectSettingsBlock } from './SelectSettingsBlock'; +export { default as AccordionSettingsBlock } from './AccordionSettingsBlock'; +export { default as ToggleSettingsBlock } from './ToggleSettingsBlock'; +export { default as RadioSettingsBlock } from './RadioSettingsBlock'; +export { default as PaymentMethodsBlock } from './PaymentMethodsBlock'; +export { default as PaymentMethodItemBlock } from './PaymentMethodItemBlock'; + +export { + Title, + TitleWrapper, + SupplementaryLabel, + Description, + Action, + Content, + ContentWrapper, + Header, +} from './SettingsBlockElements'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js index 11693d172..aeb0e3561 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js @@ -1,26 +1,50 @@ -import data from '../../utils/data'; +import { Content, ContentWrapper } from './SettingsBlocks'; -const SettingsCard = ( props ) => { - let className = 'ppcp-r-settings-card'; +const SettingsCard = ( { + className: extraClassName, + title, + description, + children, + contentItems, + contentContainer = true, +} ) => { + const className = [ 'ppcp-r-settings-card', extraClassName ] + .filter( Boolean ) + .join( ' ' ); + + const renderContent = () => { + // If contentItems array is provided, wrap each item in Content component + if ( contentItems ) { + return ( + + { contentItems.map( ( item, index ) => ( + { item } + ) ) } + + ); + } + + // Otherwise handle regular children with contentContainer prop + if ( contentContainer ) { + return { children }; + } + + return children; + }; - if ( props?.className ) { - className += ' ' + props.className; - } return (
- { props.title } + { title }

- { props.description } + { description }

-
- { props.children } -
+ { renderContent() }
); }; 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/TitleBadge.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js index dc9cdad71..9b493e735 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js @@ -1,11 +1,13 @@ const TitleBadge = ( { text, type } ) => { const className = 'ppcp-r-title-badge ' + `ppcp-r-title-badge--${ type }`; - return ; + return ( + + ); }; export const TITLE_BADGE_POSITIVE = 'positive'; 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 1c167d756..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,18 +1,12 @@ -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, -} ) => { - if ( isFastlane && isPayLater && storeCountry === 'us' ) { +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,14 +109,13 @@ const AcdcFlow = ( { isFastlane={ isFastlane } isPayLater={ isPayLater } storeCountry={ storeCountry } - storeCurrency={ storeCurrency } />
); } - if ( isPayLater && storeCountry === 'uk' ) { + if ( isPayLater && storeCountry === 'UK' ) { return (
@@ -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 6c984cfc1..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,12 +1,12 @@ -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 } ) => { - if ( isPayLater && storeCountry === 'us' ) { +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 5a1da25cb..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 @@ -1,130 +1,81 @@ -import { Button } from '@wordpress/components'; +import { Button, Icon } from '@wordpress/components'; +import { chevronLeft } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; + import { OnboardingHooks } from '../../../../data'; -import data from '../../../../utils/data'; +import useIsScrolled from '../../../../hooks/useIsScrolled'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; -const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { - const isLastStep = () => currentStep + 1 === stepperOrder.length; - const isFistStep = () => currentStep === 0; - const navigateBy = ( stepDirection ) => { - let newStep = currentStep + stepDirection; +const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { + const { title, isFirst, percentage, showNext, canProceed } = stepDetails; + const { isScrolled } = useIsScrolled(); - if ( isNaN( newStep ) || newStep < 0 ) { - console.warn( 'Invalid next step:', newStep ); - newStep = 0; - } - - if ( newStep >= stepperOrder.length ) { - setCompleted( true ); - } else { - setStep( newStep ); - } - }; - - const { products } = OnboardingHooks.useProducts(); - const { isCasualSeller } = OnboardingHooks.useBusiness(); - - let navigationTitle = ''; - let disabled = false; - - switch ( currentStep ) { - case 1: - navigationTitle = __( - 'Set up store type', - 'woocommerce-paypal-payments' - ); - disabled = isCasualSeller === null; - break; - case 2: - navigationTitle = __( - 'Select product types', - 'woocommerce-paypal-payments' - ); - disabled = products.length < 1; - break; - case 3: - navigationTitle = __( - 'Choose checkout options', - 'woocommerce-paypal-payments' - ); - case 4: - navigationTitle = __( - 'Connect your PayPal account', - 'woocommerce-paypal-payments' - ); - break; - default: - navigationTitle = __( - 'PayPal Payments', - 'woocommerce-paypal-payments' - ); - } + const state = OnboardingHooks.useNavigationState(); + const isDisabled = ! canProceed( state ); + const className = classNames( 'ppcp-r-navigation-container', { + 'is-scrolled': isScrolled, + } ); return ( -
+
-
- { data().getImage( 'icon-arrow-left.svg' ) } - { ! isFistStep() ? ( - - ) : ( - - { navigationTitle } - - ) } -
- { ! isFistStep() && ( -
- - { __( - 'Save and exit', - 'woocommerce-paypal-payments' - ) } - - { ! isLastStep() && ( - - ) } -
- ) } -
+ + + + { ! isFirst && + NextButton( { showNext, isDisabled, onNext, onExit } ) } +
); }; +const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { + return ( + + + { showNext && ( + + ) } + + ); +}; + +const ProgressBar = ( { percent } ) => { + percent = Math.min( Math.max( percent, 0 ), 100 ); + + return ( +
+ ); +}; + export default Navigation; 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 30cd52ffe..225527053 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -1,40 +1,35 @@ import Container from '../../ReusableComponents/Container'; import { OnboardingHooks } from '../../../data'; -import { getSteps } from './availableSteps'; + +import { getSteps, getCurrentStep } from './availableSteps'; import Navigation from './Components/Navigation'; -const getCurrentStep = ( requestedStep, steps ) => { - const isValidStep = ( step ) => - typeof step === 'number' && - Number.isInteger( step ) && - step >= 0 && - step < steps.length; - - const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0; - return steps[ safeCurrentStep ]; -}; - const Onboarding = () => { - const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps(); - const steps = getSteps( flags ); + const { step, setStep, flags } = OnboardingHooks.useSteps(); + const Steps = getSteps( flags ); + const currentStep = getCurrentStep( step, Steps ); - const CurrentStepComponent = getCurrentStep( step, steps ); + const handleNext = () => setStep( currentStep.nextStep ); + const handlePrev = () => setStep( currentStep.prevStep ); + const handleExit = () => { + window.location.href = window.ppcpSettings.wcPaymentsTabUrl; + }; return ( <> +
-
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js index a223686ff..dd9a1dcd5 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js @@ -10,9 +10,8 @@ const BUSINESS_RADIO_GROUP_NAME = 'business'; const StepBusiness = ( {} ) => { const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness(); - const handleSellerTypeChange = ( value ) => { + const handleSellerTypeChange = ( value ) => setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value ); - }; const getCurrentValue = () => { if ( isCasualSeller === null ) { 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 a9d2f6b9e..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 { OnboardingHooks } from '../../../data'; import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods'; +import PricingDescription from '../../ReusableComponents/PricingDescription'; const OPM_RADIO_GROUP_NAME = 'optional-payment-methods'; @@ -13,14 +14,8 @@ const StepPaymentMethods = ( {} ) => { areOptionalPaymentMethodsEnabled, setAreOptionalPaymentMethodsEnabled, } = OnboardingHooks.useOptionalPaymentMethods(); - 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 { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); return (
@@ -42,8 +37,8 @@ const StepPaymentMethods = ( {} ) => { useAcdc={ true } isFastlane={ true } isPayLater={ true } - storeCountry={ 'us' } - storeCurrency={ 'usd' } + storeCountry={ storeCountry } + storeCurrency={ storeCurrency } /> } name={ OPM_RADIO_GROUP_NAME } @@ -64,12 +59,7 @@ const StepPaymentMethods = ( {} ) => { type="radio" > -

+
); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index cbd642327..ee99f4acf 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -9,6 +9,7 @@ const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; const StepProducts = () => { const { products, setProducts } = OnboardingHooks.useProducts(); + const { canUseSubscriptions } = OnboardingHooks.useFlags(); return (
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 c94c84935..f9b7ddea4 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -8,8 +8,12 @@ import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs'; 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 } ) => { return (
{ 'woocommerce-paypal-payments' ) }

- + + +
{ className="onboarding-advanced-options" id="advanced-options" > - +
); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js index 7e8ea1556..e14e66231 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js @@ -1,21 +1,86 @@ +import { __ } from '@wordpress/i18n'; + import StepWelcome from './StepWelcome'; import StepBusiness from './StepBusiness'; import StepProducts from './StepProducts'; import StepPaymentMethods from './StepPaymentMethods'; import StepCompleteSetup from './StepCompleteSetup'; +/** + * List of all onboarding screens that are available. + * + * The screens are displayed in the order in which they appear in this array + * + * @type {[{id, StepComponent, title}]} + */ +const ALL_STEPS = [ + { + id: 'welcome', + title: __( 'PayPal Payments', 'woocommerce-paypal-payments' ), + StepComponent: StepWelcome, + canProceed: () => true, + }, + { + id: 'business', + title: __( 'Set up store type', 'woocommerce-paypal-payments' ), + StepComponent: StepBusiness, + canProceed: ( { business } ) => business.isCasualSeller !== null, + }, + { + id: 'products', + title: __( 'Select product types', 'woocommerce-paypal-payments' ), + StepComponent: StepProducts, + canProceed: ( { products } ) => products.products.length > 0, + }, + { + id: 'methods', + title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ), + StepComponent: StepPaymentMethods, + canProceed: () => true, + }, + { + id: 'complete', + title: __( + 'Connect your PayPal account', + 'woocommerce-paypal-payments' + ), + StepComponent: StepCompleteSetup, + canProceed: () => true, + }, +]; + export const getSteps = ( flags ) => { - const allSteps = [ - StepWelcome, - StepBusiness, - StepProducts, - StepPaymentMethods, - StepCompleteSetup, - ]; + const steps = flags.canUseCasualSelling + ? ALL_STEPS + : ALL_STEPS.filter( ( step ) => step.id !== 'business' ); - if ( ! flags.canUseCasualSelling ) { - return allSteps.filter( ( step ) => step !== StepBusiness ); - } + const totalStepsCount = steps.length; - return allSteps; + return steps.map( ( step, index ) => ( { + ...step, + isFirst: index === 0, + isLast: index === totalStepsCount - 1, + showNext: index < totalStepsCount - 1, + percentage: 100 * ( index / ( totalStepsCount - 1 ) ), + nextStep: index < totalStepsCount - 1 ? index + 1 : index, + prevStep: index > 0 ? index - 1 : 0, + } ) ); +}; + +/** + * Returns the screen-details of the current step, based on the numeric step-index. + * + * @param {number} requestedStep Index of the screen to display. + * @param {[]} steps List of all available steps (see `getSteps()`) + * @return {{id, StepComponent, title}} The requested screen details, or the first welcome screen. + */ +export const getCurrentStep = ( requestedStep, steps ) => { + const isValidStep = ( step ) => + typeof step === 'number' && + Number.isInteger( step ) && + step >= 0 && + step < steps.length; + + const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0; + return steps[ safeCurrentStep ]; }; 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 4bac75c9a..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,22 +1,49 @@ -import SettingsCard from '../../ReusableComponents/SettingsCard'; import { __ } from '@wordpress/i18n'; -import { - PayPalCheckbox, -} from '../../ReusableComponents/Fields'; import { useState } from '@wordpress/element'; -import data from '../../../utils/data'; -import { Button } from '@wordpress/components'; -import TitleBadge, { - TITLE_BADGE_NEGATIVE, - TITLE_BADGE_POSITIVE, -} from '../../ReusableComponents/TitleBadge'; -import ConnectionInfo, { - connectionStatusDataDefault, -} from '../../ReusableComponents/ConnectionInfo'; +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 { 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 (
@@ -32,198 +59,131 @@ const TabOverview = () => { 'woocommerce-paypal-payments' ) } > -
- { todosData.map( ( todo ) => ( - - ) ) } -
+ ) } + +

+ { __( + 'Enable additional features and capabilities on your WooCommerce store.', + 'woocommerce-paypal-payments' + ) } +

+

+ { __( + 'Click Refresh to update your current features after making changes.', + 'woocommerce-paypal-payments' + ) } +

+ + + } + contentItems={ features.map( ( feature ) => ( + + ) ) } + /> + + - - - { featuresDefault.map( ( feature ) => { - return ( - - ); - } ) } - -
- ); -}; - -const ConnectionStatus = ( { connectionData } ) => { - return ( -
-
-
- - { __( 'Connection', 'woocommerce-paypal-payments' ) } - - { connectionData.connectionStatus ? ( - - ) : ( - - ) } -
-
- - { __( - 'PayPal Account Details', + contentItems={ [ + -
-
- { connectionData.connectionStatus && ( - - ) } -
- ); -}; - -const FeaturesRefresh = () => { - return ( -
-
- - { __( 'Features', 'woocommerce-paypal-payments' ) } - -

- { __( - 'After making changes to your PayPal account, click Refresh to update your store features.', - 'woocommerce-paypal-payments' - ) } -

-
- -
- ); -}; - -const TodoItem = ( props ) => { - return ( -
-
- { ' ' } -
-
- removeTodo( - props.value, - props.todosData, - props.changeTodos - ) - } - > - { data().getImage( 'icon-close.svg' ) } -
-
- ); -}; - -const FeatureItem = ( { feature } ) => { - const printNotes = () => { - if ( ! feature?.notes ) { - return null; - } - - if ( Array.isArray( feature.notes ) && feature.notes.length === 0 ) { - return null; - } - - return ( - <> -
-
- - { feature.notes.map( ( note, index ) => { - return { note }; - } ) } - - - ); - }; - - return ( -
- - { feature.title } - { feature?.featureStatus && ( - - ) } - -

- { feature.description } - { printNotes() } -

-
- { feature.buttons.map( ( button ) => { - return ( - - ); - } ) } -
+ actionProps={ { + buttons: [ + { + type: 'tertiary', + text: __( + 'View full documentation', + 'woocommerce-paypal-payments' + ), + url: '#', + }, + ], + } } + />, + , + ] } + />
); }; -const removeTodo = ( todoValue, todosData, changeTodos ) => { - changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) ); -}; - +// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js const todosDataDefault = [ { value: 'paypal_later_messaging', @@ -259,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', @@ -269,12 +230,13 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -286,19 +248,19 @@ 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' ), buttons: [ { - type: 'primary', + type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -316,12 +278,13 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', + class: 'small-button', text: __( 'Apply', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -334,22 +297,19 @@ const featuresDefault = [ 'Let customers pay using their Google Pay wallet.', 'woocommerce-paypal-payments' ), - featureStatus: true, buttons: [ { - type: 'primary', + type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, ], - notes: [ - __( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ), - ], }, { id: 'apple_pay', @@ -360,7 +320,8 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', + class: 'small-button', text: __( 'Domain registration', 'woocommerce-paypal-payments' @@ -368,7 +329,7 @@ const featuresDefault = [ url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -383,16 +344,21 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', + class: 'small-button', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, ], + notes: [ + __( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ), + ], }, ]; + export default TabOverview; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js index 453a34426..c1576da10 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js @@ -1,23 +1,34 @@ -import SettingsCard from '../../ReusableComponents/SettingsCard'; import { __ } from '@wordpress/i18n'; -import PaymentMethodItem from '../../ReusableComponents/PaymentMethodItem'; +import { useMemo } from '@wordpress/element'; + +import SettingsCard from '../../ReusableComponents/SettingsCard'; +import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock'; +import { CommonHooks } from '../../../data'; import ModalPayPal from './Modals/ModalPayPal'; import ModalFastlane from './Modals/ModalFastlane'; import ModalAcdc from './Modals/ModalAcdc'; const TabPaymentMethods = () => { - const renderPaymentMethods = ( data ) => { - return ( -
- { data.map( ( paymentMethod ) => ( - - ) ) } -
- ); - }; + const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + + const filteredPaymentMethods = useMemo( () => { + const contextProps = { storeCountry, storeCurrency }; + + return { + payPalCheckout: filterPaymentMethods( + paymentMethodsPayPalCheckout, + contextProps + ), + onlineCardPayments: filterPaymentMethods( + paymentMethodsOnlineCardPayments, + contextProps + ), + alternative: filterPaymentMethods( + paymentMethodsAlternative, + contextProps + ), + }; + }, [ storeCountry, storeCurrency ] ); return (
@@ -28,8 +39,11 @@ const TabPaymentMethods = () => { 'woocommerce-paypal-payments' ) } icon="icon-checkout-standard.svg" + contentContainer={ false } > - { renderPaymentMethods( paymentMethodsPayPalCheckoutDefault ) } + { 'woocommerce-paypal-payments' ) } icon="icon-checkout-online-methods.svg" + contentContainer={ false } > - { renderPaymentMethods( - paymentMethodsOnlineCardPaymentsDefault - ) } + { 'woocommerce-paypal-payments' ) } icon="icon-checkout-alternative-methods.svg" + contentContainer={ false } > - { renderPaymentMethods( paymentMethodsAlternativeDefault ) } +
); }; -const paymentMethodsPayPalCheckoutDefault = [ +function filterPaymentMethods( paymentMethods, contextProps ) { + return paymentMethods.filter( ( method ) => + typeof method.condition === 'function' + ? method.condition( contextProps ) + : true + ); +} + +const paymentMethodsPayPalCheckout = [ { id: 'paypal', title: __( 'PayPal', 'woocommerce-paypal-payments' ), @@ -106,7 +132,7 @@ const paymentMethodsPayPalCheckoutDefault = [ }, ]; -const paymentMethodsOnlineCardPaymentsDefault = [ +const paymentMethodsOnlineCardPayments = [ { id: 'advanced_credit_and_debit_card_payments', title: __( @@ -124,7 +150,7 @@ const paymentMethodsOnlineCardPaymentsDefault = [ id: 'fastlane', title: __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ), description: __( - 'Tap into the scale and trust of PayPal’s customer network to recognize shoppers and make guest checkout more seamless than ever.', + "Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.", 'woocommerce-paypal-payments' ), icon: 'payment-method-fastlane', @@ -150,7 +176,7 @@ const paymentMethodsOnlineCardPaymentsDefault = [ }, ]; -const paymentMethodsAlternativeDefault = [ +const paymentMethodsAlternative = [ { id: 'bancontact', title: __( 'Bancontact', 'woocommerce-paypal-payments' ), @@ -173,7 +199,7 @@ const paymentMethodsAlternativeDefault = [ id: 'eps', title: __( 'eps', 'woocommerce-paypal-payments' ), description: __( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum porttitor massa ex, eget luctus lacus iaculis at.', + 'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', 'woocommerce-paypal-payments' ), icon: 'payment-method-eps', @@ -182,11 +208,69 @@ const paymentMethodsAlternativeDefault = [ id: 'blik', title: __( 'BLIK', 'woocommerce-paypal-payments' ), description: __( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum porttitor massa ex, eget luctus lacus iaculis at.', + 'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.', 'woocommerce-paypal-payments' ), icon: 'payment-method-blik', }, + { + id: 'mybank', + title: __( 'MyBank', 'woocommerce-paypal-payments' ), + description: __( + 'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-mybank', + }, + { + id: 'przelewy24', + title: __( 'Przelewy24', 'woocommerce-paypal-payments' ), + description: __( + 'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-przelewy24', + }, + { + id: 'trustly', + title: __( 'Trustly', 'woocommerce-paypal-payments' ), + description: __( + 'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-trustly', + }, + { + id: 'multibanco', + title: __( 'Multibanco', 'woocommerce-paypal-payments' ), + description: __( + 'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-multibanco', + }, + { + id: 'pui', + title: __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), + description: __( + 'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-ratepay', + condition: ( { storeCountry, storeCurrency } ) => + storeCountry === 'DE' && storeCurrency === 'EUR', + }, + { + id: 'oxxo', + title: __( 'OXXO', 'woocommerce-paypal-payments' ), + description: __( + 'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 800–925–0304', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-oxxo', + condition: ( { storeCountry, storeCurrency } ) => + storeCountry === 'MX' && storeCurrency === 'MXN', + }, ]; export default TabPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js index 5505fac8d..1b471fe1e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js @@ -1,4 +1,5 @@ import { useState } from '@wordpress/element'; +import ConnectionStatus from './TabSettingsElements/ConnectionStatus'; import CommonSettings from './TabSettingsElements/CommonSettings'; import ExpertSettings from './TabSettingsElements/ExpertSettings'; @@ -30,6 +31,7 @@ const TabSettings = () => { return ( <>
+ { return ( - - +
+ + { __( 'Order Intent', 'woocommerce-paypal-payments' ) } + + + { __( + 'Choose between immediate capture or authorization-only, with manual capture in the Order section.', + 'woocommerce-paypal-payments' + ) } + +
+ + - { - return ( - - - - ); -}; +import { + AccordionSettingsBlock, + SelectSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; const creditCardExamples = [ { value: '', label: __( 'Select', 'woocommerce-paypal-payments' ) }, @@ -63,4 +22,38 @@ const creditCardExamples = [ }, ]; +const OtherSettings = ( { settings, updateFormValue } ) => { + return ( + + + + ); +}; + export default OtherSettings; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js index 7b01ea203..f8d68881e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js @@ -1,33 +1,28 @@ import { __ } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_INPUT, - SETTINGS_BLOCK_TYPE_SELECT, - SETTINGS_BLOCK_TYPE_TOGGLE, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; -import { PayPalRdbWithContent } from '../../../../ReusableComponents/Fields'; +import { + AccordionSettingsBlock, + RadioSettingsBlock, + ToggleSettingsBlock, + InputSettingsBlock, + SelectSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; const PaypalSettings = ( { updateFormValue, settings } ) => { return ( - - { 'Due to differences in how WooCommerce and PayPal calculates taxes, some transactions may fail due to a rounding error. This settings determines the fallback behavior.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - } } - > -
- - updateFormValue( - 'subtotalMismatchFallback', - newValue - ) - } - label={ __( + options={ [ + { + id: 'add_a_correction', + value: 'add_a_correction', + label: __( 'Add a correction', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Adds an additional line item with the missing amount.', 'woocommerce-paypal-payments' - ) } - /> - - updateFormValue( - 'subtotalMismatchFallback', - newValue - ) - } - label={ __( + ), + }, + { + id: 'do_not_send_line_items', + value: 'do_not_send_line_items', + label: __( 'Do not send line items', 'woocommerce-paypal-payments' - ) } - description={ __( - 'Resubmit the transaction without line item details', + ), + description: __( + 'Resubmit the transaction without line item details.', 'woocommerce-paypal-payments' - ) } - /> -
-
+ ), + }, + ] } + actionProps={ { + name: 'paypal_settings_mismatch', + key: 'subtotalMismatchFallback', + currentValue: settings.subtotalMismatchFallback, + callback: updateFormValue, + } } + /> - { 'If enabled, PayPal will not allow buyers to use funding sources that take additional time to complete, such as eChecks.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE, value: settings.savePaypalAndVenmo, callback: updateFormValue, key: 'savePaypalAndVenmo', } } /> - { 'woocommerce-paypal-payments' ), } } + order={ [ 'title', 'description', 'action' ] } /> - { 'woocommerce-paypal-payments' ), } } + order={ [ 'title', 'description', 'action' ] } /> - { 'Determine which experience a buyer sees when they click the PayPal button.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - } } - > -
- - updateFormValue( 'paypalLandingPage', newValue ) - } - label={ __( + options={ [ + { + id: 'no_preference', + value: 'no_reference', + label: __( 'No preference', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Shows the buyer the PayPal login for a recognized PayPal buyer.', 'woocommerce-paypal-payments' - ) } - /> - - updateFormValue( 'paypalLandingPage', newValue ) - } - label={ __( + ), + }, + { + id: 'login_page', + value: 'login_page', + label: __( 'Login page', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Always show the buyer the PayPal login screen.', 'woocommerce-paypal-payments' - ) } - /> - - updateFormValue( 'paypalLandingPage', newValue ) - } - label={ __( + ), + }, + { + id: 'guest_checkout_page', + value: 'guest_checkout_page', + label: __( 'Guest checkout page', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Always show the buyer the guest checkout fields first.', 'woocommerce-paypal-payments' - ) } - /> -
-
- + + { 'woocommerce-paypal-payments' ), } } + order={ [ 'title', 'description', 'action' ] } /> - + ); }; @@ -235,4 +200,5 @@ const languagesExample = [ { value: 'es', label: 'Spanish' }, { value: 'it', label: 'Italian' }, ]; + export default PaypalSettings; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js index f47711098..93a4a7d0d 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js @@ -1,43 +1,40 @@ import { __, sprintf } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_STYLING_TYPE_TERTIARY, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_TOGGLE, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; +import { Button } from '@wordpress/components'; +import { + AccordionSettingsBlock, + ButtonSettingsBlock, + RadioSettingsBlock, + ToggleSettingsBlock, + InputSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; import TitleBadge, { TITLE_BADGE_POSITIVE, } from '../../../../ReusableComponents/TitleBadge'; import ConnectionInfo, { connectionStatusDataDefault, } from '../../../../ReusableComponents/ConnectionInfo'; -import { Button, TextControl } from '@wordpress/components'; -import { PayPalRdbWithContent } from '../../../../ReusableComponents/Fields'; const Sandbox = ( { settings, updateFormValue } ) => { const className = settings.sandboxConnected ? 'ppcp-r-settings-block--sandbox-connected' : 'ppcp-r-settings-block--sandbox-disconnected'; + return ( - Note: No real payments/money movement occur in Sandbox mode. Do not ship orders made in this mode.", + "Test your site in PayPal's Sandbox environment.", 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, callback: updateFormValue, key: 'payNowExperience', value: settings.payNowExperience, } } > { settings.sandboxConnected && ( - { ) } /> } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - callback: updateFormValue, - key: 'sandboxAccountCredentials', - value: settings.sandboxAccountCredentials, - } } >
- { ) }
-
+ ) } { ! settings.sandboxConnected && ( - { 'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - callback: updateFormValue, - key: 'sandboxAccountCredentials', - value: settings.sandboxAccountCredentials, - } } - > -
- - updateFormValue( 'sandboxMode', newValue ) - } - label={ __( + options={ [ + { + id: 'sandbox_mode', + value: 'sandbox_mode', + label: __( 'Sandbox Mode', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.', 'woocommerce-paypal-payments' - ) } - > - - - - updateFormValue( 'sandboxMode', newValue ) - } - label={ __( + ), + additionalContent: ( + + ), + }, + { + id: 'manual_connect', + value: 'manual_connect', + label: __( 'Manual Connect', 'woocommerce-paypal-payments' - ) } - description={ sprintf( - // translators: %s: Link to creating PayPal REST application + ), + description: sprintf( __( 'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, click here.', 'woocommerce-paypal-payments' ), '#' - ) } - > - - - - -
-
+ ), + additionalContent: ( + <> + + + + + ), + }, + ] } + actionProps={ { + name: 'paypal_connect_sandbox', + key: 'sandboxMode', + currentValue: settings.sandboxMode, + callback: updateFormValue, + } } + /> ) } -
+ ); }; + export default Sandbox; 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 8fdefbb9c..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,58 +1,58 @@ -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_TOGGLE, -} from '../../../../ReusableComponents/SettingsBlock'; import { __, sprintf } from '@wordpress/i18n'; +import { + Header, + SettingsBlock, + ToggleSettingsBlock, + Title, + Description, +} from '../../../../ReusableComponents/SettingsBlocks'; const SavePaymentMethods = ( { updateFormValue, settings } ) => { return ( - future payments[MISSING_LINK] and subscriptions[MISSING_LINK], simplifying checkout and enabling recurring transactions.', - 'woocommerce-paypal-payments' - ), - '#', - '#' - ) } - type={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY } - style={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - } } - > - +
+ + { __( + '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' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', - 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' - ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - value={ settings.savePaypalAndVenmo } + description={ +
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={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE, value: settings.savePaypalAndVenmo, callback: updateFormValue, key: 'savePaypalAndVenmo', } } /> - { "Securely store your customer's credit card.", 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } actionProps={ { - type: { SETTINGS_BLOCK_TYPE_TOGGLE }, callback: updateFormValue, key: 'saveCreditCardAndDebitCard', value: settings.saveCreditCardAndDebitCard, @@ -72,4 +70,5 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => { ); }; + export default SavePaymentMethods; 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 fdc4ad28f..000000000 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js +++ /dev/null @@ -1,169 +0,0 @@ -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_BUTTON, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_TOGGLE, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; -import { __ } from '@wordpress/i18n'; - -const Troubleshooting = ( { updateFormValue, settings } ) => { - return ( - - - - - - - - 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/CommonSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js index 0066a5fcd..dad5da83e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js @@ -1,10 +1,8 @@ import { __ } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_INPUT, - SETTINGS_BLOCK_TYPE_TOGGLE, -} from '../../../ReusableComponents/SettingsBlock'; +import { + InputSettingsBlock, + ToggleSettingsBlock, +} from '../../../ReusableComponents/SettingsBlocks'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; import OrderIntent from './Blocks/OrderIntent'; import SavePaymentMethods from './Blocks/SavePaymentMethods'; @@ -13,19 +11,21 @@ const CommonSettings = ( { updateFormValue, settings } ) => { return ( - { ), } } /> + + - { 'Let PayPal customers skip the Order Review page by selecting shipping options directly within PayPal.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE, callback: updateFormValue, key: 'payNowExperience', value: settings.payNowExperience, diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js new file mode 100644 index 000000000..b1018d44c --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js @@ -0,0 +1,53 @@ +import { __ } from '@wordpress/i18n'; +import SettingsCard from '../../../ReusableComponents/SettingsCard'; +import ConnectionInfo, { + connectionStatusDataDefault, +} from '../../../ReusableComponents/ConnectionInfo'; +import TitleBadge, { + TITLE_BADGE_NEGATIVE, + TITLE_BADGE_POSITIVE, +} from '../../../ReusableComponents/TitleBadge'; +const ConnectionStatus = () => { + return ( + +
+
+
+ { connectionStatusDataDefault.connectionStatus ? ( + + ) : ( + + ) } +
+
+ { connectionStatusDataDefault.connectionStatus && ( + + ) } +
+
+ ); +}; +export default ConnectionStatus; 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 78e8bcd20..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 @@ -1,13 +1,11 @@ import { __ } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_SELECT, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../ReusableComponents/SettingsBlock'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; +import { + Content, + 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'; @@ -25,25 +23,37 @@ const ExpertSettings = ( { updateFormValue, settings } ) => { callback: updateFormValue, key: 'payNowExperience', } } + contentContainer={ false } > - + + + + - + + + - - + + + + + + + +
); }; 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 8be3857b0..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,6 +45,15 @@ 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(), + [] + ); const savePersistent = async ( setter, value ) => { setter( value ); @@ -67,25 +78,27 @@ const useHooks = () => { setClientSecret: ( value ) => { return savePersistent( setClientSecret, value ); }, - connectViaSandbox, + connectToSandbox, + connectToProduction, connectViaIdAndSecret, - }; -}; - -export const useBusyState = () => { - const { setIsBusy } = useDispatch( STORE_NAME ); - const isBusy = useTransient( 'isBusy' ); - - return { - isBusy, - setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ), + merchant, + wooSettings, + 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 = () => { @@ -109,3 +122,80 @@ export const useManualConnection = () => { connectViaIdAndSecret, }; }; + +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 3f822468b..8b5cfb9b3 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -12,17 +12,31 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, - isBusy: false, -}; + activities: new Map(), -const defaultPersistent = { + // Read only values, provided by the server via hydrate. + merchant: Object.freeze( { + isConnected: false, + isSandbox: false, + id: '', + email: '', + } ), + + wooSettings: Object.freeze( { + storeCountry: '', + storeCurrency: '', + } ), +} ); + +const defaultPersistent = Object.freeze( { useSandbox: false, useManualConnection: false, clientId: '', clientSecret: '', -}; + webhooks: [], +} ); // Reducer logic. @@ -38,8 +52,56 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) => setPersistent( state, action ), - [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => - setPersistent( state, payload.data ), + [ 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 ); + + // Populate read-only properties. + [ 'wooSettings', 'merchant' ].forEach( ( key ) => { + if ( ! payload[ key ] ) { + return; + } + + newState[ key ] = Object.freeze( { + ...newState[ key ], + ...payload[ key ], + } ); + } ); + + return newState; + }, } ); export default commonReducer; 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 14334fcf3..96393942a 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -16,6 +16,24 @@ export const persistentData = ( state ) => { }; export const transientData = ( state ) => { - const { data, ...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 4ae5bd947..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, }; }; @@ -113,3 +118,24 @@ export const useSteps = () => { return { flags, isReady, step, setStep, completed, setCompleted }; }; + +export const useNavigationState = () => { + const products = useProducts(); + const business = useBusiness(); + + return { + products, + business, + }; +}; + +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 176d4875d..2b16e2416 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -12,24 +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: true, + areOptionalPaymentMethodsEnabled: null, products: [], -}; +} ); // Reducer logic. @@ -45,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/useAccordionState.js b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js new file mode 100644 index 000000000..f54018262 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js @@ -0,0 +1,39 @@ +import { useEffect, useState } from '@wordpress/element'; + +const checkIfCurrentTab = ( id ) => { + return id && window.location.hash === `#${ id }`; +}; + +const determineInitialState = ( id, initiallyOpen ) => { + if ( initiallyOpen !== null ) { + return initiallyOpen; + } + return checkIfCurrentTab( id ); +}; + +export function useAccordionState( { id = '', initiallyOpen = null } ) { + const [ isOpen, setIsOpen ] = useState( + determineInitialState( id, initiallyOpen ) + ); + + useEffect( () => { + const handleHashChange = () => { + if ( checkIfCurrentTab( id ) ) { + setIsOpen( true ); + } + }; + + window.addEventListener( 'hashchange', handleHashChange ); + return () => { + window.removeEventListener( 'hashchange', handleHashChange ); + }; + }, [ id ] ); + + const toggleOpen = ( ev ) => { + setIsOpen( ! isOpen ); + ev?.preventDefault(); + return false; + }; + + return { isOpen, toggleOpen }; +} 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/hooks/useIsScrolled.js b/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js new file mode 100644 index 000000000..2b40aa3e9 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js @@ -0,0 +1,44 @@ +/** + * Taken from WooCommerce core: + * https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/client/hooks/useIsScrolled.js + */ + +import { useEffect, useRef, useState } from '@wordpress/element'; + +const isAtBottom = () => + window.innerHeight + window.scrollY >= document.body.scrollHeight; + +const useIsScrolled = () => { + const [ isScrolled, setIsScrolled ] = useState( false ); + const [ atBottom, setAtBottom ] = useState( isAtBottom() ); + const rafHandle = useRef( null ); + useEffect( () => { + const updateIsScrolled = () => { + setIsScrolled( window.pageYOffset > 20 ); + setAtBottom( isAtBottom() ); + }; + + const scrollListener = () => { + rafHandle.current = + window.requestAnimationFrame( updateIsScrolled ); + }; + + window.addEventListener( 'scroll', scrollListener ); + + window.addEventListener( 'resize', scrollListener ); + + return () => { + window.removeEventListener( 'scroll', scrollListener ); + window.removeEventListener( 'resize', scrollListener ); + window.cancelAnimationFrame( rafHandle.current ); + }; + }, [] ); + + return { + isScrolled, + atBottom, + atTop: ! isScrolled, + }; +}; + +export default useIsScrolled; 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 193efd584..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, + US: { + 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, + UK: { + 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, + CA: { + 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, + AU: { + 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, + FR: { + 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, + IT: { + 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, + DE: { + 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, + ES: { + 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 d213aa4c0..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,6 +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(); // Card payments are disabled for this plugin when WooPayments is active. // TODO: Move this condition to the card-fields.eligible service? @@ -47,14 +53,18 @@ return array( return new OnboardingProfile( $can_use_casual_selling, $can_use_vaulting, - $can_use_card_payments + $can_use_card_payments, + $can_use_subscriptions ); }, 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings { return new GeneralSettings(); }, 'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings { - return new CommonSettings(); + return new CommonSettings( + $container->get( 'api.shop.country' ), + $container->get( 'api.shop.currency.getter' )->get(), + ); }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); @@ -62,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' ), @@ -75,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', @@ -131,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( @@ -152,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 8f7dd1ddf..1894255ff 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -29,6 +29,25 @@ class CommonSettings extends AbstractDataModel { */ protected const OPTION_KEY = 'woocommerce-ppcp-data-common'; + /** + * List of customization flags, provided by the server (read-only). + * + * @var array + */ + protected array $woo_settings = array(); + + /** + * Constructor. + * + * @param string $country WooCommerce store country. + * @param string $currency WooCommerce store currency. + */ + public function __construct( string $country, string $currency ) { + parent::__construct(); + $this->woo_settings['country'] = $country; + $this->woo_settings['currency'] = $currency; + } + /** * Get default values for the model. * @@ -40,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' => '', ); } @@ -116,4 +141,67 @@ class CommonSettings extends AbstractDataModel { public function set_client_secret( string $client_secret ) : void { $this->data['client_secret'] = sanitize_text_field( $client_secret ); } + + /** + * Returns the list of read-only customization flags. + * + * @return array + */ + 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/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index 03a0a7d1c..a2d8e6c36 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -42,19 +42,22 @@ class OnboardingProfile extends AbstractDataModel { * @param bool $can_use_casual_selling Whether casual selling is enabled in the store's country. * @param bool $can_use_vaulting Whether vaulting is enabled in the store's country. * @param bool $can_use_card_payments Whether credit card payments are possible. + * @param bool $can_use_subscriptions Whether WC Subscriptions plugin is active. * * @throws RuntimeException If the OPTION_KEY is not defined in the child class. */ public function __construct( bool $can_use_casual_selling = false, bool $can_use_vaulting = false, - bool $can_use_card_payments = false + bool $can_use_card_payments = false, + bool $can_use_subscriptions = false ) { parent::__construct(); $this->flags['can_use_casual_selling'] = $can_use_casual_selling; $this->flags['can_use_vaulting'] = $can_use_vaulting; $this->flags['can_use_card_payments'] = $can_use_card_payments; + $this->flags['can_use_subscriptions'] = $can_use_subscriptions; } /** @@ -67,7 +70,7 @@ class OnboardingProfile extends AbstractDataModel { 'completed' => false, 'step' => 0, 'is_casual_seller' => null, - 'are_optional_payment_methods_enabled' => true, + 'are_optional_payment_methods_enabled' => null, 'products' => array(), ); } diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index c7345148e..054f3e918 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -58,6 +58,43 @@ class CommonRestEndpoint extends RestEndpoint { 'js_name' => 'clientSecret', 'sanitize' => 'sanitize_text_field', ), + 'webhooks' => array( + 'js_name' => 'webhooks', + ), + ); + + /** + * 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 + */ + private array $woo_settings_map = array( + 'country' => array( + 'js_name' => 'storeCountry', + ), + 'currency' => array( + 'js_name' => 'storeCurrency', + ), ); /** @@ -96,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' ), + ), + ) + ); } /** @@ -109,7 +158,10 @@ class CommonRestEndpoint extends RestEndpoint { $this->field_map ); - return $this->return_success( $js_data ); + $extra_data = $this->add_woo_settings( array() ); + $extra_data = $this->add_merchant_info( $extra_data ); + + return $this->return_success( $js_data, $extra_data ); } /** @@ -130,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/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 02e7c80cd..d4273228f 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -77,6 +77,9 @@ class OnboardingRestEndpoint extends RestEndpoint { 'can_use_card_payments' => array( 'js_name' => 'canUseCardPayments', ), + 'can_use_subscriptions' => array( + 'js_name' => 'canUseSubscriptions', + ), ); /** 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 8bf6b5404..45aa2371e 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -454,7 +454,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { $tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() ); foreach ( $tokens as $token ) { if ( $token->get_id() === (int) $card_payment_token_id ) { - $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 e11d40263..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.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 +* 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' );