From ff2219af24ee5741ca3b0824711c95394d65a186 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 24 Sep 2024 15:13:10 +0200 Subject: [PATCH 001/298] Do not use wpdb to get wc_orders table --- modules/ppcp-compat/src/PPEC/PPECHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-compat/src/PPEC/PPECHelper.php b/modules/ppcp-compat/src/PPEC/PPECHelper.php index a01c5eea7..c74aac61d 100644 --- a/modules/ppcp-compat/src/PPEC/PPECHelper.php +++ b/modules/ppcp-compat/src/PPEC/PPECHelper.php @@ -75,10 +75,10 @@ class PPECHelper { } global $wpdb; - if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() && isset( $wpdb->wc_orders ) ) { + if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) { $result = $wpdb->get_var( $wpdb->prepare( - "SELECT 1 FROM {$wpdb->wc_orders} WHERE payment_method = %s", + "SELECT 1 FROM {$wpdb->prefix}wc_orders WHERE payment_method = %s", self::PPEC_GATEWAY_ID ) ); From 7e175a888d5c6a347f29dc538bbbf2a367f4aac7 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 14 Oct 2024 16:36:54 +0200 Subject: [PATCH 002/298] Add `ppec_paypal` payment to vault v2 order renewal handler --- modules/ppcp-wc-subscriptions/src/RenewalHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php index 695bce551..88bf229f7 100644 --- a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php @@ -406,7 +406,7 @@ class RenewalHandler { return; } - if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { + if ( $wc_order->get_payment_method() === PayPalGateway::ID || $wc_order->get_payment_method() === 'ppec_paypal' ) { $order = $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference, From 809b2f6577217ce83b149cc6f0030e5c4f574c06 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 14 Oct 2024 16:49:55 +0200 Subject: [PATCH 003/298] Ensure transient for checking if ppec subscriptions exist is deleted on plugin upgrade --- modules/ppcp-compat/src/CompatModule.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index b698cd62a..1641f6574 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -79,6 +79,13 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule { $this->initialize_wc_bookings_compat_layer( $logger ); } + add_action( + 'woocommerce_paypal_payments_gateway_migrate', + function() { + delete_transient( 'ppcp_has_ppec_subscriptions' ); + } + ); + return true; } From 8da36b61208a19b969d8c5058a709acf7c4bac98 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 14 Oct 2024 17:44:49 +0200 Subject: [PATCH 004/298] Refactor --- modules/ppcp-compat/src/CompatModule.php | 7 +------ modules/ppcp-wc-subscriptions/src/RenewalHandler.php | 11 ++++++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index 1641f6574..2544b7185 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -79,12 +79,7 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule { $this->initialize_wc_bookings_compat_layer( $logger ); } - add_action( - 'woocommerce_paypal_payments_gateway_migrate', - function() { - delete_transient( 'ppcp_has_ppec_subscriptions' ); - } - ); + add_action( 'woocommerce_paypal_payments_gateway_migrate', static fn() => delete_transient( 'ppcp_has_ppec_subscriptions' ) ); return true; } diff --git a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php index 88bf229f7..e82491904 100644 --- a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php @@ -257,7 +257,8 @@ class RenewalHandler { // Vault v3. $payment_source = null; - if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { + $payment_method = $wc_order->get_payment_method(); + if ( $payment_method === PayPalGateway::ID ) { $customer_tokens = $this->wc_payment_tokens->customer_tokens( $user_id ); $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, PayPalGateway::ID ); @@ -309,7 +310,7 @@ class RenewalHandler { } } - if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { + if ( $payment_method === CreditCardGateway::ID ) { $customer_tokens = $this->wc_payment_tokens->customer_tokens( $user_id ); $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); @@ -352,7 +353,7 @@ class RenewalHandler { $this->handle_paypal_order( $wc_order, $order ); - if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { + if ( $payment_method === CreditCardGateway::ID ) { $card_payment_source = $order->payment_source(); if ( $card_payment_source ) { $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); @@ -379,7 +380,7 @@ class RenewalHandler { // Vault v2. $token = $this->get_token_for_customer( $customer, $wc_order ); if ( $token ) { - if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { + if ( $payment_method === CreditCardGateway::ID ) { $payment_source = $this->card_payment_source( $token->id(), $wc_order ); $order = $this->order_endpoint->create( @@ -406,7 +407,7 @@ class RenewalHandler { return; } - if ( $wc_order->get_payment_method() === PayPalGateway::ID || $wc_order->get_payment_method() === 'ppec_paypal' ) { + if ( $payment_method === PayPalGateway::ID || $payment_method === 'ppec_paypal' ) { $order = $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference, From add776c976fd6300e88df7ab0559f0114f586b7e Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 3 Dec 2024 12:27:34 +0100 Subject: [PATCH 005/298] Extract components --- .../js/Components/block-editor-paypal.js | 52 ++ .../resources/js/Components/paypal-label.js | 14 + .../resources/js/Components/paypal.js | 687 ++++++++++++++++ .../resources/js/checkout-block.js | 766 +----------------- 4 files changed, 770 insertions(+), 749 deletions(-) create mode 100644 modules/ppcp-blocks/resources/js/Components/block-editor-paypal.js create mode 100644 modules/ppcp-blocks/resources/js/Components/paypal-label.js create mode 100644 modules/ppcp-blocks/resources/js/Components/paypal.js 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/paypal-label.js b/modules/ppcp-blocks/resources/js/Components/paypal-label.js new file mode 100644 index 000000000..0cb9eb95d --- /dev/null +++ b/modules/ppcp-blocks/resources/js/Components/paypal-label.js @@ -0,0 +1,14 @@ +export const PaypalLabel = ( { components, config } ) => { + 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..577fe3914 --- /dev/null +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -0,0 +1,687 @@ +import { useEffect, useState } from '@wordpress/element'; +import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; +import { + mergeWcAddress, + paypalAddressToWc, + paypalOrderToWcAddresses, + paypalSubscriptionToWcAddresses, +} 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 { isPayPalSubscription } from '../Helper/Subscription'; + +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 createOrder = async ( data ) => { + 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 ( + + ); +}; diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index b0f971dd2..c02015833 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,7 @@ 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 render if vaulting disabled and is in vault subscription mode @@ -762,7 +38,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 +46,13 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) { isPayPalSubscription( config.scriptData ) && ! config.scriptData.subscription_product_allowed ) { - block_enabled = false; + 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 +98,13 @@ if ( block_enabled ) { registerPaymentMethod( { name: config.id, label:
, - content: , - edit: , + content: , + edit: ( + + ), ariaLabel: config.title, canMakePayment: () => { return true; @@ -866,12 +132,14 @@ if ( block_enabled ) { ), content: ( ), edit: ( ), From d500c4dc180cd020527b5ab68c94e018ddfa96c3 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 3 Dec 2024 16:13:36 +0100 Subject: [PATCH 006/298] Add save payment button configuration (WIP) --- .../resources/js/Components/card-fields.js | 54 ++++++++----- .../resources/js/Components/paypal.js | 77 ++++++++++++++++++- 2 files changed, 111 insertions(+), 20 deletions(-) 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 ); } } > - - -
-
- -
-
- -
-
+ + +
+
+ +
+
+ +
+
{ + 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 ); + } ); + }; + + const onApproveSavePayment = async ( { vaultSetupToken } ) => { + 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 ); + }; + + if ( + cartHasSubscriptionProducts( config.scriptData ) && + config.scriptData.is_free_trial_cart + ) { + return ( + + ); + } + if ( isPayPalSubscription( config.scriptData ) ) { return ( Date: Tue, 3 Dec 2024 16:29:37 +0100 Subject: [PATCH 007/298] Add save payment button configuration (WIP) --- modules/ppcp-blocks/resources/js/Components/paypal.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/ppcp-blocks/resources/js/Components/paypal.js b/modules/ppcp-blocks/resources/js/Components/paypal.js index 0daee0f19..b65a22c58 100644 --- a/modules/ppcp-blocks/resources/js/Components/paypal.js +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -525,6 +525,15 @@ export const PayPalComponent = ( { } 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, From eac06cf045ce115482278468d88793770627be21 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 3 Dec 2024 17:30:36 +0100 Subject: [PATCH 008/298] Ensure `customer_id` is string --- .../src/Endpoint/CreatePaymentToken.php | 2 +- .../ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index 434a08925..e5235eb17 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; From ee336293c1314ac962f71af30d0571f5cedd1eaf Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 4 Dec 2024 12:16:38 +0100 Subject: [PATCH 009/298] Do not display PayPal button for guest with free trial subscription --- modules/ppcp-blocks/resources/js/checkout-block.js | 9 +++++++++ .../src/Endpoint/CreatePaymentToken.php | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index c02015833..1aabdaca0 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -160,6 +160,15 @@ if ( blockEnabled ) { } await paypalScriptPromise; + if ( + ! config.scriptData.user.is_logged && + config.scriptData.context === 'cart-block' && + cartHasSubscriptionProducts( config.scriptData ) && + config.scriptData.is_free_trial_cart + ) { + return false; + } + return ppcpBlocksPaypalExpressButtons .Buttons( { fundingSource } ) .isEligible(); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index e5235eb17..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, (string)$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(); From 855a9caef6b800157bdafcaadb7a5c5c49fa13e9 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 4 Dec 2024 12:42:41 +0100 Subject: [PATCH 010/298] Move guest free trial conditional at the top of the file --- .../resources/js/checkout-block.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 1aabdaca0..fc7720f93 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -33,6 +33,16 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) { 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 if ( ! isPayPalSubscription( config.scriptData ) && @@ -160,15 +170,6 @@ if ( blockEnabled ) { } await paypalScriptPromise; - if ( - ! config.scriptData.user.is_logged && - config.scriptData.context === 'cart-block' && - cartHasSubscriptionProducts( config.scriptData ) && - config.scriptData.is_free_trial_cart - ) { - return false; - } - return ppcpBlocksPaypalExpressButtons .Buttons( { fundingSource } ) .isEligible(); From e37d36230293d17ec7a589207b2506fcffa91b43 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 4 Dec 2024 16:28:59 +0100 Subject: [PATCH 011/298] Extract actions from button components --- .../resources/js/Components/paypal.js | 365 ++++-------------- .../ppcp-blocks/resources/js/paypal-config.js | 316 +++++++++++++++ 2 files changed, 385 insertions(+), 296 deletions(-) create mode 100644 modules/ppcp-blocks/resources/js/paypal-config.js diff --git a/modules/ppcp-blocks/resources/js/Components/paypal.js b/modules/ppcp-blocks/resources/js/Components/paypal.js index b65a22c58..f0dc84f13 100644 --- a/modules/ppcp-blocks/resources/js/Components/paypal.js +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -4,7 +4,6 @@ import { mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses, - paypalSubscriptionToWcAddresses, } from '../Helper/Address'; import { convertKeysToSnakeCase } from '../Helper/Helper'; import buttonModuleWatcher from '../../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; @@ -13,6 +12,14 @@ import { cartHasSubscriptionProducts, isPayPalSubscription, } from '../Helper/Subscription'; +import { + createOrder, + createSubscription, + createVaultSetupToken, + handleApprove, + handleApproveSubscription, + onApproveSavePayment, +} from '../paypal-config'; const PAYPAL_GATEWAY_ID = 'ppcp-gateway'; @@ -114,156 +121,6 @@ export const PayPalComponent = ( { setContinuationFilled( true ); }, [ shippingData, continuationFilled ] ); - const createOrder = async ( data ) => { - 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, @@ -275,87 +132,6 @@ export const PayPalComponent = ( { 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 ) { @@ -600,11 +376,25 @@ export const PayPalComponent = ( { buttonModuleWatcher.registerContextBootstrap( config.scriptData.context, { - createOrder: () => { - return createOrder(); + createOrder: ( data ) => { + return createOrder( data, config, onError, onClose ); }, onApprove: ( data, actions ) => { - return handleApprove( data, actions ); + return handleApprove( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose + ); }, } ); @@ -660,61 +450,6 @@ export const PayPalComponent = ( { }; }; - const createVaultSetupToken = async () => { - 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 ); - } ); - }; - - const onApproveSavePayment = async ( { vaultSetupToken } ) => { - 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 ); - }; - if ( cartHasSubscriptionProducts( config.scriptData ) && config.scriptData.is_free_trial_cart @@ -725,8 +460,10 @@ export const PayPalComponent = ( { onClick={ handleClick } onCancel={ onClose } onError={ onClose } - createVaultSetupToken={ createVaultSetupToken } - onApprove={ onApproveSavePayment } + createVaultSetupToken={ () => createVaultSetupToken( config ) } + onApprove={ ( { vaultSetupToken } ) => + onApproveSavePayment( vaultSetupToken, config, onSubmit ) + } /> ); } @@ -739,8 +476,26 @@ export const PayPalComponent = ( { onClick={ handleClick } onCancel={ onClose } onError={ onClose } - createSubscription={ createSubscription } - onApprove={ handleApproveSubscription } + createSubscription={ ( data, actions ) => + createSubscription( data, actions, config ) + } + onApprove={ ( data, actions ) => + handleApproveSubscription( + data, + actions, + config, + shouldHandleShippingInPayPal, + shippingData, + setPaypalOrder, + shouldskipFinalConfirmation, + getCheckoutRedirectUrl, + setGotoContinuationOnError, + enforcePaymentMethodForCart, + onSubmit, + onError, + onClose + ) + } onShippingOptionsChange={ getOnShippingOptionsChange( fundingSource ) } @@ -758,8 +513,26 @@ export const PayPalComponent = ( { onClick={ handleClick } onCancel={ onClose } onError={ onClose } - createOrder={ createOrder } - onApprove={ handleApprove } + createOrder={ ( data ) => + 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 ) } 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 ); +}; From 21eb11aee892ec5af835afbc1d88e203cfcb6f6d Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 4 Dec 2024 16:34:29 +0100 Subject: [PATCH 012/298] Remove not used methods --- .../resources/js/Components/paypal.js | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/modules/ppcp-blocks/resources/js/Components/paypal.js b/modules/ppcp-blocks/resources/js/Components/paypal.js index f0dc84f13..c9bb1ad88 100644 --- a/modules/ppcp-blocks/resources/js/Components/paypal.js +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -177,8 +177,6 @@ export const PayPalComponent = ( { let handleShippingOptionsChange = null; let handleShippingAddressChange = null; - let handleSubscriptionShippingOptionsChange = null; - let handleSubscriptionShippingAddressChange = null; if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) { handleShippingOptionsChange = async ( data, actions ) => { @@ -244,55 +242,6 @@ export const PayPalComponent = ( { 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( () => { From c65ca2a00b0f2fa2650e8026225f788d74b5517a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Tue, 10 Dec 2024 12:31:55 +0100 Subject: [PATCH 013/298] removed disabled from button when sandbox is changed --- modules/ppcp-onboarding/resources/js/onboarding.js | 4 ++++ 1 file changed, 4 insertions(+) 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 From 14f91218f6c720d0132bd44ad377fe2c91f7917a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Tue, 10 Dec 2024 13:23:20 +0100 Subject: [PATCH 014/298] when variable product is in stock there is no need to query all variations --- modules/ppcp-button/src/Assets/SmartButton.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. * From 149c5f5b38ebc7a8c1491f75cc212ea9e78872e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 09:01:44 +0100 Subject: [PATCH 015/298] add cvv and avs error messages to order note and removed old address match fields --- .../src/Entity/FraudProcessorResponse.php | 102 +++++++++++++++--- .../CreditCardOrderInfoHandlingTrait.php | 60 ++++------- 2 files changed, 109 insertions(+), 53 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index 2cc7c5480..de254f088 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -17,32 +17,32 @@ 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 { return $this->avs_code; @@ -51,10 +51,10 @@ class FraudProcessorResponse { /** * Returns the CVV response code. * - * @return string|null + * @return string */ public function cvv_code(): ?string { - return $this->cvv_code; + return $this->cvv2_code; } /** @@ -64,11 +64,83 @@ class FraudProcessorResponse { */ public function to_array(): array { return array( - 'avs_code' => $this->avs_code() ?: '', - 'address_match' => $this->avs_code() === 'M' ? 'Y' : 'N', - 'postal_match' => $this->avs_code() === 'M' ? 'Y' : 'N', - 'cvv_match' => $this->cvv_code() === 'M' ? 'Y' : 'N', + 'avs_code' => $this->avs_code(), + 'cvv2_code' => $this->cvv_code(), ); } + /** + * 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_messages(): string { + if ( $this->avs_code() ) { + return ''; + } + $messages = array( + /* Visa, Mastercard, Discover, American Express */ + 'A' => __( 'A: Address - Address only (no ZIP code)', 'woocommerce-paypal-payments' ), + 'B' => __( 'B: International "A" - Address only (no ZIP code)', 'woocommerce-paypal-payments' ), + 'C' => __( 'C: International "N" - None. The transaction is declined.', 'woocommerce-paypal-payments' ), + 'D' => __( 'D: International "X" - Address and Postal Code', 'woocommerce-paypal-payments' ), + 'E' => __( 'E: Not allowed for MOTO (Internet/Phone) transactions - Not applicable. The transaction is declined.', 'woocommerce-paypal-payments' ), + 'F' => __( 'F: UK-specific "X" - Address and Postal Code', 'woocommerce-paypal-payments' ), + 'G' => __( 'G: Global Unavailable - Not applicable', 'woocommerce-paypal-payments' ), + 'I' => __( 'I: International Unavailable - Not applicable', 'woocommerce-paypal-payments' ), + 'M' => __( 'M: Address - Address and Postal Code', 'woocommerce-paypal-payments' ), + 'N' => __( 'N: No - None. The transaction is declined.', 'woocommerce-paypal-payments' ), + 'P' => __( 'P: Postal (International "Z") - Postal Code only (no Address)', 'woocommerce-paypal-payments' ), + 'R' => __( 'R: Retry - Not applicable', 'woocommerce-paypal-payments' ), + 'S' => __( 'S: Service not Supported - Not applicable', 'woocommerce-paypal-payments' ), + 'U' => __( 'U: Unavailable / Address not checked, or acquirer had no response. Service not available.', 'woocommerce-paypal-payments' ), + 'W' => __( 'W: Whole ZIP - Nine-digit ZIP code (no Address)', 'woocommerce-paypal-payments' ), + 'X' => __( 'X: Exact match - Address and nine-digit ZIP code)', 'woocommerce-paypal-payments' ), + 'Y' => __( 'Y: Yes - Address and five-digit ZIP', 'woocommerce-paypal-payments' ), + 'Z' => __( 'Z: ZIP - Five-digit ZIP code (no Address)', 'woocommerce-paypal-payments' ), + /* Maestro */ + '0' => __( '0: All the address information matched.', 'woocommerce-paypal-payments' ), + '1' => __( '1: None of the address information matched. The transaction is declined.', 'woocommerce-paypal-payments' ), + '2' => __( '2: Part of the address information matched.', 'woocommerce-paypal-payments' ), + '3' => __( '3: The merchant did not provide AVS information. Not processed.', 'woocommerce-paypal-payments' ), + '4' => __( '4: Address not checked, or acquirer had no response. Service not available.', 'woocommerce-paypal-payments' ), + ); + /* translators: %s is fraud AVS code */ + return $messages[ $this->avs_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $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_messages(): string { + if ( $this->cvv_code() ) { + return ''; + } + $messages = array( + /* Visa, Mastercard, Discover, American Express */ + 'E' => __( 'E: Error - Unrecognized or Unknown response', 'woocommerce-paypal-payments' ), + 'I' => __( 'I: Invalid or Null', 'woocommerce-paypal-payments' ), + 'M' => __( 'M: Match or CSC', 'woocommerce-paypal-payments' ), + 'N' => __( 'N: No match', 'woocommerce-paypal-payments' ), + 'P' => __( 'P: Not processed', 'woocommerce-paypal-payments' ), + 'S' => __( 'S: Service not supported', 'woocommerce-paypal-payments' ), + 'U' => __( 'U: Unknown - Issuer is not certified', 'woocommerce-paypal-payments' ), + 'X' => __( 'X: No response / Service not available', 'woocommerce-paypal-payments' ), + /* Maestro */ + '0' => __( '0: Matched CVV2', 'woocommerce-paypal-payments' ), + '1' => __( '1: No match', 'woocommerce-paypal-payments' ), + '2' => __( '2: The merchant has not implemented CVV2 code handling', 'woocommerce-paypal-payments' ), + '3' => __( '3: Merchant has indicated that CVV2 is not present on card', 'woocommerce-paypal-payments' ), + '4' => __( '4: Service not available', 'woocommerce-paypal-payments' ), + ); + /* translators: %s is fraud CVV2 code */ + return $messages[ $this->cvv_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->cvv_code() ); + } } diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php index 51a3a741c..c909e5981 100644 --- a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php @@ -96,52 +96,36 @@ 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 = __( 'Card decline errors', '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
  • +
  • %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_messages() ), + /* translators: %s is fraud CVV message */ + sprintf( __( 'CVV: %s', 'woocommerce-paypal-payments' ), $fraud->get_cvv2_code_messages() ), ); - $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, From 2d27f5e99d7eeaf979d311f77e03957c6695e587 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 11 Dec 2024 09:23:01 +0100 Subject: [PATCH 016/298] Add todo item icon --- .../css/components/screens/_settings.scss | 8 ++++++ .../SettingsBlocks/TodoSettingsBlock.js | 26 +------------------ 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index b98a5f7e1..1d8852152 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -66,6 +66,14 @@ @include font(13, 20, 400); color: $color-blueberry; } + + &__icon { + border: 1px dashed #949494; + background: #fff; + border-radius: 50%; + width: 32px; + height: 32px; + } } .ppcp-r-feature-item { diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js index 4f9b01644..1378c72a5 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js @@ -1,6 +1,3 @@ -import { PayPalCheckbox, handleCheckboxState } from '../Fields'; -import data from '../../../utils/data'; - const TodoSettingsBlock = ( { todos, setTodos, @@ -36,34 +33,13 @@ 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; From b2aff305808025f7cd92d5af119ff6cd3f5a8547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 09:27:59 +0100 Subject: [PATCH 017/298] fix psalm error --- .../src/Entity/FraudProcessorResponse.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index de254f088..fb0db00ba 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -108,7 +108,12 @@ class FraudProcessorResponse { '3' => __( '3: The merchant did not provide AVS information. Not processed.', 'woocommerce-paypal-payments' ), '4' => __( '4: Address not checked, or acquirer had no response. Service not available.', 'woocommerce-paypal-payments' ), ); - /* translators: %s is fraud AVS code */ + /** + * Translators: %s is fraud AVS code + * + * @psalm-suppress PossiblyNullArrayOffset + * @psalm-suppress PossiblyNullArgument + */ return $messages[ $this->avs_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->avs_code() ); } @@ -140,7 +145,12 @@ class FraudProcessorResponse { '3' => __( '3: Merchant has indicated that CVV2 is not present on card', 'woocommerce-paypal-payments' ), '4' => __( '4: Service not available', 'woocommerce-paypal-payments' ), ); - /* translators: %s is fraud CVV2 code */ + /** + * Translators: %s is fraud CVV2 code + * + * @psalm-suppress PossiblyNullArrayOffset + * @psalm-suppress PossiblyNullArgument + */ return $messages[ $this->cvv_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->cvv_code() ); } } From 3a63a8e7d76ac885d382f2c6875700526d8eaa3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 09:57:48 +0100 Subject: [PATCH 018/298] fix phpcs error --- .../ppcp-api-client/src/Entity/FraudProcessorResponse.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index fb0db00ba..7a9d89666 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -108,12 +108,12 @@ class FraudProcessorResponse { '3' => __( '3: The merchant did not provide AVS information. Not processed.', 'woocommerce-paypal-payments' ), '4' => __( '4: Address not checked, or acquirer had no response. Service not available.', 'woocommerce-paypal-payments' ), ); + /** - * Translators: %s is fraud AVS code - * * @psalm-suppress PossiblyNullArrayOffset * @psalm-suppress PossiblyNullArgument */ + /* translators: %s is fraud AVS code */ return $messages[ $this->avs_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->avs_code() ); } @@ -145,12 +145,12 @@ class FraudProcessorResponse { '3' => __( '3: Merchant has indicated that CVV2 is not present on card', 'woocommerce-paypal-payments' ), '4' => __( '4: Service not available', 'woocommerce-paypal-payments' ), ); + /** - * Translators: %s is fraud CVV2 code - * * @psalm-suppress PossiblyNullArrayOffset * @psalm-suppress PossiblyNullArgument */ + /* translators: %s is fraud CVV2 code */ return $messages[ $this->cvv_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->cvv_code() ); } } From c92e2455e12f2f927b9ec83887ea34ea8322597d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 10:24:03 +0100 Subject: [PATCH 019/298] fix phpcs error --- modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index 7a9d89666..a1c281e93 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -110,6 +110,8 @@ class FraudProcessorResponse { ); /** + * Psalm suppress + * * @psalm-suppress PossiblyNullArrayOffset * @psalm-suppress PossiblyNullArgument */ @@ -147,6 +149,8 @@ class FraudProcessorResponse { ); /** + * Psalm suppress + * * @psalm-suppress PossiblyNullArrayOffset * @psalm-suppress PossiblyNullArgument */ From 486c0e683987a2d1fd4e88e1f5cf120702e93a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 14:16:09 +0100 Subject: [PATCH 020/298] removed translation, improved wordings and fixes --- .../src/Entity/FraudProcessorResponse.php | 94 ++++++++++--------- .../CreditCardOrderInfoHandlingTrait.php | 7 +- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index a1c281e93..f77326812 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -64,8 +64,12 @@ class FraudProcessorResponse { */ public function to_array(): array { return array( - 'avs_code' => $this->avs_code(), - 'cvv2_code' => $this->cvv_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', ); } @@ -77,36 +81,36 @@ class FraudProcessorResponse { * * @return string The AVS response code message. If the code is not found, an error message is returned. */ - public function get_avs_code_messages(): string { - if ( $this->avs_code() ) { + 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)', 'woocommerce-paypal-payments' ), - 'B' => __( 'B: International "A" - Address only (no ZIP code)', 'woocommerce-paypal-payments' ), - 'C' => __( 'C: International "N" - None. The transaction is declined.', 'woocommerce-paypal-payments' ), - 'D' => __( 'D: International "X" - Address and Postal Code', 'woocommerce-paypal-payments' ), - 'E' => __( 'E: Not allowed for MOTO (Internet/Phone) transactions - Not applicable. The transaction is declined.', 'woocommerce-paypal-payments' ), - 'F' => __( 'F: UK-specific "X" - Address and Postal Code', 'woocommerce-paypal-payments' ), - 'G' => __( 'G: Global Unavailable - Not applicable', 'woocommerce-paypal-payments' ), - 'I' => __( 'I: International Unavailable - Not applicable', 'woocommerce-paypal-payments' ), - 'M' => __( 'M: Address - Address and Postal Code', 'woocommerce-paypal-payments' ), - 'N' => __( 'N: No - None. The transaction is declined.', 'woocommerce-paypal-payments' ), - 'P' => __( 'P: Postal (International "Z") - Postal Code only (no Address)', 'woocommerce-paypal-payments' ), - 'R' => __( 'R: Retry - Not applicable', 'woocommerce-paypal-payments' ), - 'S' => __( 'S: Service not Supported - Not applicable', 'woocommerce-paypal-payments' ), - 'U' => __( 'U: Unavailable / Address not checked, or acquirer had no response. Service not available.', 'woocommerce-paypal-payments' ), - 'W' => __( 'W: Whole ZIP - Nine-digit ZIP code (no Address)', 'woocommerce-paypal-payments' ), - 'X' => __( 'X: Exact match - Address and nine-digit ZIP code)', 'woocommerce-paypal-payments' ), - 'Y' => __( 'Y: Yes - Address and five-digit ZIP', 'woocommerce-paypal-payments' ), - 'Z' => __( 'Z: ZIP - Five-digit ZIP code (no Address)', 'woocommerce-paypal-payments' ), + '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.', 'woocommerce-paypal-payments' ), - '1' => __( '1: None of the address information matched. The transaction is declined.', 'woocommerce-paypal-payments' ), - '2' => __( '2: Part of the address information matched.', 'woocommerce-paypal-payments' ), - '3' => __( '3: The merchant did not provide AVS information. Not processed.', 'woocommerce-paypal-payments' ), - '4' => __( '4: Address not checked, or acquirer had no response. Service not available.', 'woocommerce-paypal-payments' ), + '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.', ); /** @@ -115,8 +119,7 @@ class FraudProcessorResponse { * @psalm-suppress PossiblyNullArrayOffset * @psalm-suppress PossiblyNullArgument */ - /* translators: %s is fraud AVS code */ - return $messages[ $this->avs_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->avs_code() ); + return $messages[ $this->avs_code() ] ?? sprintf( '%s: Error', $this->avs_code() ); } /** @@ -126,26 +129,26 @@ class FraudProcessorResponse { * * @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_messages(): string { - if ( $this->cvv_code() ) { + 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', 'woocommerce-paypal-payments' ), - 'I' => __( 'I: Invalid or Null', 'woocommerce-paypal-payments' ), - 'M' => __( 'M: Match or CSC', 'woocommerce-paypal-payments' ), - 'N' => __( 'N: No match', 'woocommerce-paypal-payments' ), - 'P' => __( 'P: Not processed', 'woocommerce-paypal-payments' ), - 'S' => __( 'S: Service not supported', 'woocommerce-paypal-payments' ), - 'U' => __( 'U: Unknown - Issuer is not certified', 'woocommerce-paypal-payments' ), - 'X' => __( 'X: No response / Service not available', 'woocommerce-paypal-payments' ), + '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', 'woocommerce-paypal-payments' ), - '1' => __( '1: No match', 'woocommerce-paypal-payments' ), - '2' => __( '2: The merchant has not implemented CVV2 code handling', 'woocommerce-paypal-payments' ), - '3' => __( '3: Merchant has indicated that CVV2 is not present on card', 'woocommerce-paypal-payments' ), - '4' => __( '4: Service not available', 'woocommerce-paypal-payments' ), + '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', ); /** @@ -154,7 +157,6 @@ class FraudProcessorResponse { * @psalm-suppress PossiblyNullArrayOffset * @psalm-suppress PossiblyNullArgument */ - /* translators: %s is fraud CVV2 code */ - return $messages[ $this->cvv_code() ] ?? sprintf( __( '%s: Error', 'woocommerce-paypal-payments' ), $this->cvv_code() ); + return $messages[ $this->cvv_code() ] ?? sprintf( '%s: Error', $this->cvv_code() ); } } diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php index c909e5981..c730136ab 100644 --- a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php @@ -99,23 +99,22 @@ trait CreditCardOrderInfoHandlingTrait { $card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' ); $card_last_digits = $payment_source->properties()->last_digits ?? __( 'N/A', 'woocommerce-paypal-payments' ); - $response_order_note_title = __( 'Card decline errors', '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 */ $response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' ); $response_order_note_result_format = '
  • %1$s
  • %2$s
  • %3$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_messages() ), + 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_messages() ), + sprintf( __( 'CVV: %s', 'woocommerce-paypal-payments' ), $fraud->get_cvv2_code_message() ), ); $response_order_note = sprintf( $response_order_note_format, From 9c447835ab12c956dd51718ccd52e5f0392f3b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 15:31:12 +0100 Subject: [PATCH 021/298] Fix negative unit amount adjustments in item sanitization --- modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php | 5 +++++ 1 file changed, 5 insertions(+) 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'] From ae275210ca7e525b2389bb51cd32a90238d88c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 11 Dec 2024 15:50:12 +0100 Subject: [PATCH 022/298] fix null return type --- modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php index f77326812..baecabf73 100644 --- a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -44,7 +44,7 @@ class FraudProcessorResponse { * * @return string */ - public function avs_code(): ?string { + public function avs_code(): string { return $this->avs_code; } @@ -53,7 +53,7 @@ class FraudProcessorResponse { * * @return string */ - public function cvv_code(): ?string { + public function cvv_code(): string { return $this->cvv2_code; } From 5e78085a7ae0f7a695245c692637ff3c05837e0e Mon Sep 17 00:00:00 2001 From: inpsyde-maticluznar Date: Thu, 12 Dec 2024 06:04:03 +0100 Subject: [PATCH 023/298] Copy endpoint --- .../src/Endpoint/WebhookSettingsEndpoint.php | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php new file mode 100644 index 000000000..328ff54c4 --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -0,0 +1,167 @@ + array( + 'js_name' => 'completed', + 'sanitize' => 'to_boolean', + ), + 'step' => array( + 'js_name' => 'step', + 'sanitize' => 'to_number', + ), + 'is_casual_seller' => array( + 'js_name' => 'isCasualSeller', + 'sanitize' => 'to_boolean', + ), + 'are_optional_payment_methods_enabled' => array( + 'js_name' => 'areOptionalPaymentMethodsEnabled', + 'sanitize' => 'to_boolean', + ), + 'products' => array( + 'js_name' => 'products', + ), + ); + + /** + * Map the internal flags to JS names. + * + * @var array + */ + private array $flag_map = array( + 'can_use_casual_selling' => array( + 'js_name' => 'canUseCasualSelling', + ), + 'can_use_vaulting' => array( + 'js_name' => 'canUseVaulting', + ), + 'can_use_card_payments' => array( + 'js_name' => 'canUseCardPayments', + ), + 'can_use_subscriptions' => array( + 'js_name' => 'canUseSubscriptions', + ), + ); + + /** + * Constructor. + * + * @param OnboardingProfile $profile The settings instance. + */ + public function __construct( OnboardingProfile $profile ) { + $this->profile = $profile; + + $this->field_map['products']['sanitize'] = fn( $list ) => array_map( 'sanitize_text_field', $list ); + } + + /** + * 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_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + } + + /** + * Returns all details of the current onboarding wizard progress. + * + * @return WP_REST_Response The current state of the onboarding wizard. + */ + public function get_details() : WP_REST_Response { + $js_data = $this->sanitize_for_javascript( + $this->profile->to_array(), + $this->field_map + ); + + $js_flags = $this->sanitize_for_javascript( + $this->profile->get_flags(), + $this->flag_map + ); + + return $this->return_success( + $js_data, + array( + 'flags' => $js_flags, + ) + ); + } + + /** + * Updates onboarding details based on the request. + * + * @param WP_REST_Request $request Full data about the request. + * + * @return WP_REST_Response The updated state of the onboarding wizard. + */ + public function update_details( WP_REST_Request $request ) : WP_REST_Response { + $wp_data = $this->sanitize_for_wordpress( + $request->get_params(), + $this->field_map + ); + + $this->profile->from_array( $wp_data ); + $this->profile->save(); + + return $this->get_details(); + } +} From 947f20a2a5e3543293daf7ae55f851f69fdd0c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Thu, 12 Dec 2024 16:03:32 +0100 Subject: [PATCH 024/298] check if axo is enabled or not for buttons --- modules/ppcp-axo-block/src/AxoBlockModule.php | 5 ++++- modules/ppcp-axo/services.php | 4 +++- modules/ppcp-axo/src/AxoModule.php | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) 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..8be0f3c22 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -246,7 +246,10 @@ class AxoModule 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; } From 3b85c5037ff98a218e43dfb4d552d606ebd3bac3 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 12 Dec 2024 19:09:14 +0400 Subject: [PATCH 025/298] Don't show buttons if cart contains free trial product and the stroe is not eligible for saving payment methods. --- modules/ppcp-blocks/resources/js/checkout-block.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index fc7720f93..03c60745a 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -59,6 +59,14 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) { 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' ); } From 0b48d9c2272c9093cfa9c07c2e0a716aa30fda91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Fri, 13 Dec 2024 13:04:41 +0100 Subject: [PATCH 026/298] avoid adding JS when fastlane is disabled --- modules/ppcp-axo/src/AxoModule.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 8be0f3c22..21ee84a35 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -247,7 +247,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { add_filter( 'woocommerce_paypal_payments_sdk_components_hook', function( $components ) use ( $c ) { - if ( ! $c->has( 'axo.available' ) || ! $c->get( 'axo.available' ) ) { + $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); + assert( $dcc_configuration instanceof DCCGatewayConfiguration ); + + if ( ! $dcc_configuration->use_fastlane() ) { return $components; } $components[] = 'fastlane'; @@ -258,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 ); + } } ); From 83b637cf5e7cd536c3a0ea39585557e6513fea25 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 13 Dec 2024 16:12:27 +0400 Subject: [PATCH 027/298] If free trial is in the product the fundingSource should be only "paypal" --- modules/ppcp-blocks/resources/js/checkout-block.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 03c60745a..3ddc2cd92 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -132,10 +132,11 @@ if ( blockEnabled ) { }, } ); } 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', From eb2298fcae3bde5474dd8cb8605c1011d1944dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Mon, 16 Dec 2024 08:42:20 +0100 Subject: [PATCH 028/298] only use counties for saved payments and updated the list --- .../ppcp-save-payment-methods/services.php | 101 +++++++----------- .../src/Helper/SavePaymentMethodsApplies.php | 36 ++----- 2 files changed, 52 insertions(+), 85 deletions(-) diff --git a/modules/ppcp-save-payment-methods/services.php b/modules/ppcp-save-payment-methods/services.php index f0c5802ba..e59e35d1c 100644 --- a/modules/ppcp-save-payment-methods/services.php +++ b/modules/ppcp-save-payment-methods/services.php @@ -20,75 +20,56 @@ return array( $save_payment_methods_applies = $container->get( 'save-payment-methods.helpers.save-payment-methods-applies' ); assert( $save_payment_methods_applies instanceof SavePaymentMethodsApplies ); - return $save_payment_methods_applies->for_country_currency(); + return $save_payment_methods_applies->for_country(); }, 'save-payment-methods.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : SavePaymentMethodsApplies { return new SavePaymentMethodsApplies( - $container->get( 'save-payment-methods.supported-country-currency-matrix' ), - $container->get( 'api.shop.currency.getter' ), + $container->get( 'save-payment-methods.supported-countries' ), $container->get( 'api.shop.country' ) ); }, - 'save-payment-methods.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { - $default_currencies = array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'THB', - 'TWD', - 'USD', - ); + 'save-payment-methods.supported-countries' => static function ( ContainerInterface $container ) : array { + if ( has_filter( 'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix' ) ) { + _deprecated_hook( 'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix', '3.0.0', 'woocommerce_paypal_payments_save_payment_methods_supported_countries', esc_attr__( 'Please use the new Hook to filer countries for saved payments in PayPal Payments.', 'woocommerce-paypal-payments' ) ); + } return apply_filters( - 'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix', + 'woocommerce_paypal_payments_save_payment_methods_supported_countries', array( - 'AU' => $default_currencies, - 'AT' => $default_currencies, - 'BE' => $default_currencies, - 'BG' => $default_currencies, - 'CA' => $default_currencies, - 'CN' => $default_currencies, - 'CY' => $default_currencies, - 'CZ' => $default_currencies, - 'DK' => $default_currencies, - 'EE' => $default_currencies, - 'FI' => $default_currencies, - 'FR' => $default_currencies, - 'DE' => $default_currencies, - 'GR' => $default_currencies, - 'HU' => $default_currencies, - 'IE' => $default_currencies, - 'IT' => $default_currencies, - 'LV' => $default_currencies, - 'LI' => $default_currencies, - 'LT' => $default_currencies, - 'LU' => $default_currencies, - 'MT' => $default_currencies, - 'NO' => $default_currencies, - 'NL' => $default_currencies, - 'PL' => $default_currencies, - 'PT' => $default_currencies, - 'RO' => $default_currencies, - 'SK' => $default_currencies, - 'SI' => $default_currencies, - 'ES' => $default_currencies, - 'SE' => $default_currencies, - 'GB' => $default_currencies, - 'US' => $default_currencies, + 'AU', + 'AT', + 'BE', + 'BG', + 'CA', + 'CN', + 'CY', + 'CZ', + 'DK', + 'EE', + 'FI', + 'FR', + 'DE', + 'HK', + 'HU', + 'IE', + 'IT', + 'LV', + 'LI', + 'LT', + 'LU', + 'MT', + 'NO', + 'NL', + 'PL', + 'PT', + 'RO', + 'SG', + 'SK', + 'SI', + 'ES', + 'SE', + 'GB', + 'US', ) ); }, diff --git a/modules/ppcp-save-payment-methods/src/Helper/SavePaymentMethodsApplies.php b/modules/ppcp-save-payment-methods/src/Helper/SavePaymentMethodsApplies.php index c7f4d32d7..7bea44c3c 100644 --- a/modules/ppcp-save-payment-methods/src/Helper/SavePaymentMethodsApplies.php +++ b/modules/ppcp-save-payment-methods/src/Helper/SavePaymentMethodsApplies.php @@ -9,49 +9,37 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Helper; -use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; - /** * Class SavePaymentMethodsApplies */ class SavePaymentMethodsApplies { /** - * The matrix which countries and currency combinations can be used for Save Payment Methods. + * The countries can be used for Save Payment Methods. * * @var array */ - private $allowed_country_currency_matrix; - - /** - * The getter of the 3-letter currency code of the shop. - * - * @var CurrencyGetter - */ - private CurrencyGetter $currency; + private array $allowed_country_currencies; /** * 2-letter country code of the shop. * * @var string */ - private $country; + private string $country; /** * SavePaymentMethodsApplies constructor. * - * @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for Save Payment Methods. - * @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop. - * @param string $country 2-letter country code of the shop. + * @param array $allowed_country_currencies The matrix which countries and currency combinations can be used for Save Payment Methods. + * @param string $country 2-letter country code of the shop. */ public function __construct( - array $allowed_country_currency_matrix, - CurrencyGetter $currency, + array $allowed_country_currencies, string $country ) { - $this->allowed_country_currency_matrix = $allowed_country_currency_matrix; - $this->currency = $currency; - $this->country = $country; + $this->allowed_country_currencies = $allowed_country_currencies; + $this->country = $country; } /** @@ -59,10 +47,8 @@ class SavePaymentMethodsApplies { * * @return bool */ - public function for_country_currency(): bool { - if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { - return false; - } - return in_array( $this->currency->get(), $this->allowed_country_currency_matrix[ $this->country ], true ); + public function for_country(): bool { + + return in_array( $this->country, $this->allowed_country_currencies, true ); } } From dec6e040e678397b4b08111376e4eeb5cdcc6941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Mon, 16 Dec 2024 14:05:35 +0100 Subject: [PATCH 029/298] Update required WP Version to 6.5 and WC version to 9.2 --- readme.txt | 2 +- src/FilePathPluginFactory.php | 2 +- woocommerce-paypal-payments.php | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.txt b/readme.txt index e11d40263..93baa02a9 100644 --- a/readme.txt +++ b/readme.txt @@ -1,7 +1,7 @@ === 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 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..36beac2f3 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -9,7 +9,8 @@ * License: GPL-2.0 * Requires PHP: 7.4 * Requires Plugins: woocommerce - * WC requires at least: 6.9 + * Requires at least: 6.5 + * WC requires at least: 9.2 * WC tested up to: 9.4 * Text Domain: woocommerce-paypal-payments * From a5bbee307d732573eabbf39f906c9f1f5b56765a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 16:04:36 +0100 Subject: [PATCH 030/298] =?UTF-8?q?=F0=9F=9A=9A=20Move=20ajax=20handler=20?= =?UTF-8?q?to=20separate=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 2 +- .../src/{Endpoint => Ajax}/SwitchSettingsUiEndpoint.php | 4 ++-- modules/ppcp-settings/src/SettingsModule.php | 2 +- modules/ppcp-uninstall/services.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename modules/ppcp-settings/src/{Endpoint => Ajax}/SwitchSettingsUiEndpoint.php (94%) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 349c13350..f1c1730c4 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -18,7 +18,7 @@ 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\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; diff --git a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php similarity index 94% rename from modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php rename to modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php index 244c26dfe..04a6cc86e 100644 --- a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php @@ -1,13 +1,13 @@ Date: Tue, 17 Dec 2024 16:08:09 +0100 Subject: [PATCH 031/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adjust=20service?= =?UTF-8?q?=20name=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 2 +- modules/ppcp-settings/src/SettingsModule.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index f1c1730c4..c785111aa 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -192,7 +192,7 @@ return array( return $generators; }, - 'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { + 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { return new SwitchSettingsUiEndpoint( $container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'button.request-data' ), diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 3197b1cba..931a9782e 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -86,7 +86,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); - $endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null; + $endpoint = $container->get( 'settings.ajax.switch_ui' ) ? $container->get( 'settings.ajax.switch_ui' ) : null; assert( $endpoint instanceof SwitchSettingsUiEndpoint ); add_action( From a2221e52337ea7f90ec9d22ecb42f4c0cce3018d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 19:34:22 +0100 Subject: [PATCH 032/298] =?UTF-8?q?=E2=9C=A8=20Introduce=20new=20connectio?= =?UTF-8?q?n=20manager=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 9 + .../ppcp-settings/src/Data/CommonSettings.php | 14 + .../src/Service/ConnectionManager.php | 242 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/ConnectionManager.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index c785111aa..1397b137b 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -23,6 +23,7 @@ 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; +use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -192,6 +193,14 @@ return array( return $generators; }, + 'settings.service.connection_manager' => static function ( ContainerInterface $container ) : ConnectionManager { + return new ConnectionManager( + $container->get( 'settings.data.common' ), + $container->get( 'api.paypal-host-production' ), + $container->get( 'api.paypal-host-sandbox' ), + $container->get( 'woocommerce.logger.woocommerce' ), + ); + }, 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { return new SwitchSettingsUiEndpoint( $container->get( 'woocommerce.logger.woocommerce' ), diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 1894255ff..0935734b8 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -169,6 +169,20 @@ class CommonSettings extends AbstractDataModel { $this->data['merchant_connected'] = true; } + /** + * Reset all connection details to the initial, disconnected state. + * + * @return void + */ + public function reset_merchant_data() : void { + $defaults = $this->get_defaults(); + + $this->data['sandbox_merchant'] = $defaults['sandbox_merchant']; + $this->data['merchant_id'] = $defaults['merchant_id']; + $this->data['merchant_email'] = $defaults['merchant_email']; + $this->data['merchant_connected'] = $defaults['merchant_connected']; + } + /** * Whether the currently connected merchant is a sandbox account. * diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php new file mode 100644 index 000000000..1cffb155c --- /dev/null +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -0,0 +1,242 @@ + + */ + private array $connection_hosts; + + /** + * Constructor. + * + * @param CommonSettings $common_settings Data model that stores the connection details. + * @param string $live_host The API host for the live mode. + * @param string $sandbox_host The API host for the sandbox mode. + * @param LoggerInterface $logger Logging instance. + */ + public function __construct( CommonSettings $common_settings, string $live_host, string $sandbox_host, LoggerInterface $logger ) { + $this->common_settings = $common_settings; + $this->logger = $logger; + $this->connection_hosts = array( + 'live' => $live_host, + 'sandbox' => $sandbox_host, + ); + } + + /** + * Returns details about the currently connected merchant. + * + * @return array + */ + public function get_account_details() : array { + return array( + 'is_sandbox' => $this->common_settings->is_sandbox_merchant(), + 'is_connected' => $this->common_settings->is_merchant_connected(), + 'merchant_id' => $this->common_settings->get_merchant_id(), + 'merchant_email' => $this->common_settings->get_merchant_email(), + ); + } + + /** + * Removes any connection details we currently have stored. + * + * @return void + */ + public function disconnect() : void { + $this->logger->info( 'Disconnecting merchant from PayPal...' ); + + $this->common_settings->reset_merchant_data(); + $this->common_settings->save(); + } + + /** + * Checks if the provided ID and secret have a valid format. + * + * On failure, an Exception is thrown, while a successful check does not + * generate any return value. + * + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @return void + * @throws RuntimeException When invalid client ID or secret provided. + */ + public function validate_id_and_secret( string $client_id, string $client_secret ) : void { + if ( empty( $client_id ) ) { + throw new RuntimeException( 'No client ID provided.' ); + } + + if ( false === preg_match( '/^A[\w-]{79}$/', $client_secret ) ) { + throw new RuntimeException( 'Invalid client ID provided.' ); + } + + if ( empty( $client_secret ) ) { + throw new RuntimeException( 'No client secret provided.' ); + } + } + + /** + * Disconnects the current merchant, and then attempts to connect to a + * PayPal account using a client ID and secret. + * + * @param bool $use_sandbox Whether to use the sandbox mode. + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @return void + * @throws RuntimeException When failed to retrieve payee. + */ + public function connect_via_secret( bool $use_sandbox, string $client_id, string $client_secret ) : void { + $this->disconnect(); + + $this->logger->info( + 'Attempting manual connection to PayPal...', + array( + 'sandbox' => $use_sandbox, + 'client_id' => $client_id, + ) + ); + + $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); + + $this->update_connection_details( $use_sandbox, $payee['merchant_id'], $payee['email_address'] ); + } + + + // ---------------------------------------------------------------------------- + // Internal helper methods + + + /** + * Returns the API host for the relevant environment. + * + * @param bool $for_sandbox Whether to return the sandbox API host. + * @return string + */ + private function get_host( bool $for_sandbox = false ) : string { + return $for_sandbox ? $this->connection_hosts['sandbox'] : $this->connection_hosts['live']; + } + + /** + * Retrieves the payee object with the merchant data by creating a minimal PayPal order. + * + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @param bool $use_sandbox Whether to use the sandbox mode. + * + * @return array Payee details, containing 'merchant_id' and 'merchant_email' keys. + * @throws RuntimeException When failed to retrieve payee. + */ + private function request_payee( + string $client_id, + string $client_secret, + bool $use_sandbox + ) : array { + $host = $this->get_host( $use_sandbox ); + + $bearer = new PayPalBearer( + new InMemoryCache(), + $host, + $client_id, + $client_secret, + $this->logger, + null + ); + + $orders = new Orders( + $host, + $bearer, + $this->logger + ); + + $request_body = array( + 'intent' => 'CAPTURE', + 'purchase_units' => array( + array( + 'amount' => array( + 'currency_code' => 'USD', + 'value' => 1.0, + ), + ), + ), + ); + + $response = $orders->create( $request_body ); + $body = json_decode( $response['body'] ); + + $order_id = $body->id; + + $order_response = $orders->order( $order_id ); + $order_body = json_decode( $order_response['body'] ); + + $pu = $order_body->purchase_units[0]; + $payee = $pu->payee; + + if ( ! is_object( $payee ) ) { + throw new RuntimeException( 'Payee not found.' ); + } + if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) { + throw new RuntimeException( 'Payee info not found.' ); + } + + return array( + 'merchant_id' => $payee->merchant_id, + 'email_address' => $payee->email_address, + ); + } + + /** + * Stores the provided details in the data model. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id PayPal's internal merchant ID. + * @param string $merchant_email Email address associated with the PayPal account. + * @return void + */ + private function update_connection_details( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->logger->info( + 'Updating connection details', + array( + 'sandbox' => $is_sandbox, + 'merchant_id' => $merchant_id, + 'merchant_email' => $merchant_email, + ) + ); + + $this->common_settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->common_settings->save(); + } +} From 45db5abb34422dc25b656bcc975742fb1455bde0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 19:35:43 +0100 Subject: [PATCH 033/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20new=20connec?= =?UTF-8?q?tion=20manager=20in=20REST=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 5 +- .../Endpoint/ConnectManualRestEndpoint.php | 161 +++--------------- 2 files changed, 24 insertions(+), 142 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 1397b137b..6b676850d 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -81,10 +81,7 @@ return array( }, 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint { return new ConnectManualRestEndpoint( - $container->get( 'api.paypal-host-production' ), - $container->get( 'api.paypal-host-sandbox' ), - $container->get( 'woocommerce.logger.woocommerce' ), - $container->get( 'settings.data.general' ) + $container->get( 'settings.service.connection_manager' ), ); }, 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint { diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php index 7046342a2..efeda393d 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php @@ -20,33 +20,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; /** * REST controller for connection via manual credentials input. */ class ConnectManualRestEndpoint extends RestEndpoint { - - /** - * The API host for the live mode. - * - * @var string - */ - private string $live_host; - - /** - * The API host for the sandbox mode. - * - * @var string - */ - private string $sandbox_host; - - /** - * The logger. - * - * @var LoggerInterface - */ - private $logger; - /** * The base path for this REST controller. * @@ -54,13 +33,6 @@ class ConnectManualRestEndpoint extends RestEndpoint { */ protected $rest_base = 'connect_manual'; - /** - * Settings instance. - * - * @var GeneralSettings - */ - private $settings = null; - /** * Field mapping for request. * @@ -81,24 +53,27 @@ class ConnectManualRestEndpoint extends RestEndpoint { ), ); + /** + * Defines the JSON response format (when connection was successful). + * + * @var array + */ + private array $response_map = array( + 'merchant_id' => array( + 'js_name' => 'merchantId', + ), + 'merchant_email' => array( + 'js_name' => 'email', + ), + ); + /** * ConnectManualRestEndpoint constructor. * - * @param string $live_host The API host for the live mode. - * @param string $sandbox_host The API host for the sandbox mode. - * @param LoggerInterface $logger The logger. - * @param GeneralSettings $settings Settings instance. + * @param ConnectionManager $connection_manager The connection manager. */ - public function __construct( - string $live_host, - string $sandbox_host, - LoggerInterface $logger, - GeneralSettings $settings - ) { - $this->live_host = $live_host; - $this->sandbox_host = $sandbox_host; - $this->logger = $logger; - $this->settings = $settings; + public function __construct( ConnectionManager $connection_manager ) { + $this->connection_manager = $connection_manager; } /** @@ -133,106 +108,16 @@ class ConnectManualRestEndpoint extends RestEndpoint { $client_secret = $data['client_secret'] ?? ''; $use_sandbox = (bool) ( $data['use_sandbox'] ?? false ); - if ( empty( $client_id ) || empty( $client_secret ) ) { - return $this->return_error( 'No client ID or secret provided.' ); - } - try { - $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); + $this->connection_manager->validate_id_and_secret( $client_id, $client_secret ); + $this->connection_manager->connect_via_secret( $use_sandbox, $client_id, $client_secret ); } catch ( Exception $exception ) { return $this->return_error( $exception->getMessage() ); } - if ( $use_sandbox ) { - $this->settings->set_is_sandbox( true ); - $this->settings->set_sandbox_client_id( $client_id ); - $this->settings->set_sandbox_client_secret( $client_secret ); - $this->settings->set_sandbox_merchant_id( $payee->merchant_id ); - $this->settings->set_sandbox_merchant_email( $payee->email_address ); - } else { - $this->settings->set_is_sandbox( false ); - $this->settings->set_live_client_id( $client_id ); - $this->settings->set_live_client_secret( $client_secret ); - $this->settings->set_live_merchant_id( $payee->merchant_id ); - $this->settings->set_live_merchant_email( $payee->email_address ); - } - $this->settings->save(); + $account = $this->connection_manager->get_account_details(); + $response = $this->sanitize_for_javascript( $this->response_map, $account ); - return $this->return_success( - array( - 'merchantId' => $payee->merchant_id, - 'email' => $payee->email_address, - ) - ); - } - - /** - * Retrieves the payee object with the merchant data - * by creating a minimal PayPal order. - * - * @throws Exception When failed to retrieve payee. - * - * phpcs:disable Squiz.Commenting - * phpcs:disable Generic.Commenting - * - * @param string $client_secret The client secret. - * @param bool $use_sandbox Whether to use the sandbox mode. - * @param string $client_id The client ID. - * - * @return stdClass The payee object. - */ - private function request_payee( - string $client_id, - string $client_secret, - bool $use_sandbox - ) : stdClass { - - $host = $use_sandbox ? $this->sandbox_host : $this->live_host; - - $bearer = new PayPalBearer( - new InMemoryCache(), - $host, - $client_id, - $client_secret, - $this->logger, - null - ); - - $orders = new Orders( - $host, - $bearer, - $this->logger - ); - - $request_body = array( - 'intent' => 'CAPTURE', - 'purchase_units' => array( - array( - 'amount' => array( - 'currency_code' => 'USD', - 'value' => 1.0, - ), - ), - ), - ); - - $response = $orders->create( $request_body ); - $body = json_decode( $response['body'] ); - - $order_id = $body->id; - - $order_response = $orders->order( $order_id ); - $order_body = json_decode( $order_response['body'] ); - - $pu = $order_body->purchase_units[0]; - $payee = $pu->payee; - if ( ! is_object( $payee ) ) { - throw new RuntimeException( 'Payee not found.' ); - } - if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) { - throw new RuntimeException( 'Payee info not found.' ); - } - - return $payee; + return $this->return_success( $response ); } } From 08f5b4fba56f4308c309aaaa14fbe60fbac1a113 Mon Sep 17 00:00:00 2001 From: inpsyde-maticluznar Date: Wed, 18 Dec 2024 07:00:47 +0100 Subject: [PATCH 034/298] Get and resubscribe webhooks --- .../SettingsBlocks/ButtonSettingsBlock.js | 1 + .../Blocks/Troubleshooting.js | 68 +++----- .../resources/js/data/common/action-types.js | 1 + .../resources/js/data/common/actions.js | 5 + .../resources/js/data/common/constants.js | 2 + .../resources/js/data/common/hooks.js | 30 +++- .../resources/js/data/common/reducer.js | 1 + .../resources/js/data/common/resolvers.js | 4 +- .../resources/js/data/common/selectors.js | 4 + modules/ppcp-settings/services.php | 8 + .../src/Endpoint/CommonRestEndpoint.php | 3 + .../src/Endpoint/WebhookSettingsEndpoint.php | 152 ++++-------------- modules/ppcp-settings/src/SettingsModule.php | 1 + 13 files changed, 113 insertions(+), 167 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js index e2b2aeb53..6cdadb43c 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js @@ -15,6 +15,7 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => ( -
+ } contentItems={ features.map( ( feature ) => ( { /> ) ) } /> + + , + , + ] } + />
); }; From 0e469db58df123106e20d47d6dca1ae0972fc186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 18 Dec 2024 12:15:33 +0100 Subject: [PATCH 039/298] script translations must be configured additional when not used directly in a block --- modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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' ); } From 607021c9ec2cd76e6e3ca46cacd5e339e9a45754 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 18 Dec 2024 12:51:22 +0100 Subject: [PATCH 040/298] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unnecessary=20"?= =?UTF-8?q?use"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php index 028740cb9..b2483160a 100644 --- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -12,7 +12,6 @@ namespace WooCommerce\PayPalCommerce\Settings\Service; use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; -use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; From 09149f18797afd4b38c9102fe8c4f9f6dce11812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 18 Dec 2024 14:21:57 +0100 Subject: [PATCH 041/298] Not display Button Gateway when only a free trial product is in cart --- .../src/Gateway/CardButtonGateway.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php index 3ba41da15..ffb0fe55c 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php @@ -362,4 +362,19 @@ class CardButtonGateway extends \WC_Payment_Gateway { protected function settings_renderer(): SettingsRenderer { return $this->settings_renderer; } + + /** + * Determines if the Gateway is available for use. + * + * @return bool + */ + public function is_available(): bool { + $is_available = parent::is_available(); + + if ( $is_available && $this->is_free_trial_cart() ) { + $is_available = false; + } + + return $is_available; + } } From cb8c3f8726fd8895acf6fc87e4650246e1b8e653 Mon Sep 17 00:00:00 2001 From: Wesley Rosa Date: Wed, 18 Dec 2024 13:40:58 -0300 Subject: [PATCH 042/298] Updating GitHub upload action due deprecation (#2921) --- .github/workflows/package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From ed77ad63ca7bedf13a7aaec15ecf0af83ac928ce Mon Sep 17 00:00:00 2001 From: inpsyde-maticluznar Date: Thu, 19 Dec 2024 13:10:19 +0100 Subject: [PATCH 043/298] Add messages, separate components, finish actions --- .../SettingsBlocks/ButtonSettingsBlock.js | 5 +- .../Blocks/Troubleshooting.js | 150 ------------------ .../Blocks/Troubleshooting/Troubleshooting.js | 75 +++++++++ .../TroubleshootingResubscribeBlock.js | 67 ++++++++ .../TroubleshootingSimulationBlock.js | 130 +++++++++++++++ .../TroubleshootingTableBlock.js | 34 ++++ .../TabSettingsElements/ExpertSettings.js | 2 +- .../resources/js/data/common/action-types.js | 3 + .../resources/js/data/common/actions.js | 34 +++- .../resources/js/data/common/constants.js | 20 ++- .../resources/js/data/common/controls.js | 12 +- .../resources/js/data/common/hooks.js | 33 ++-- .../src/Endpoint/WebhookSettingsEndpoint.php | 39 ++++- 13 files changed, 417 insertions(+), 187 deletions(-) delete mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/Troubleshooting.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/TroubleshootingResubscribeBlock.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/TroubleshootingSimulationBlock.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting/TroubleshootingTableBlock.js diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js index 1242df032..3b48b820b 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js @@ -1,6 +1,6 @@ import { Button } from '@wordpress/components'; import SettingsBlock from './SettingsBlock'; -import { Header, Title, Action, Description } from './SettingsBlockElements'; +import { Action, Description, Header, Title } from './SettingsBlockElements'; const ButtonSettingsBlock = ( { title, description, ...props } ) => ( @@ -9,6 +9,9 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => ( { description } + { props.actionProps?.message && ( +

{ props.actionProps.message }

+ ) } + + + + { children } + + + { showProgressBar && ( + + ) } + + + ); +}; + +const ProgressBar = ( { percent } ) => { + percent = Math.min( Math.max( percent, 0 ), 100 ); + + return ( +
+ ); +}; + +export default TopNavigation; diff --git a/modules/ppcp-settings/resources/js/hooks/useNavigation.js b/modules/ppcp-settings/resources/js/hooks/useNavigation.js new file mode 100644 index 000000000..f477a0b91 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useNavigation.js @@ -0,0 +1,10 @@ +/** + * Navigate to the WooCommerce "Payments" settings tab, i.e. exit the settings app. + */ +const goToWooCommercePaymentsTab = () => { + window.location.href = window.ppcpSettings.wcPaymentsTabUrl; +}; + +export const useNavigation = () => { + return { goToWooCommercePaymentsTab }; +}; From 47be154307295b6a1de69cd69a5d3459130e3f00 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 15:14:15 +0100 Subject: [PATCH 168/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20Setting?= =?UTF-8?q?s=20to=20use=20new=20TopNavigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Settings/Components/SettingsNavigation.js | 39 +++++-------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/SettingsNavigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/SettingsNavigation.js index 8eb6b12c5..f195ea1b1 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/SettingsNavigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/SettingsNavigation.js @@ -1,36 +1,17 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import data from '../../../../utils/data'; -const SettingsNavigation = ( {} ) => { +import TopNavigation from '../../../ReusableComponents/TopNavigation'; + +const SettingsNavigation = () => { + const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); + return ( -
-
-
- { data().getImage( 'icon-arrow-left.svg' ) } - - { __( - 'PayPal Payments', - 'woocommerce-paypal-payments' - ) } - -
- { -
- -
- } -
-
+ + + ); }; From 0a5fb316c4cb763a5d8e97cd58db0d7c2b369489 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 15:14:38 +0100 Subject: [PATCH 169/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20Onboard?= =?UTF-8?q?ing=20to=20use=20new=20TopNavigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components/Navigation.js | 68 ++++--------------- .../js/Components/Screens/Onboarding/index.js | 4 -- 2 files changed, 14 insertions(+), 58 deletions(-) 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 817b26f3e..f03abb383 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,56 +1,27 @@ -import { Button, Icon } from '@wordpress/components'; -import { chevronLeft } from '@wordpress/icons'; +import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import classNames from 'classnames'; import { OnboardingHooks } from '../../../../data'; -import useIsScrolled from '../../../../hooks/useIsScrolled'; -import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; +import { useNavigation } from '../../../../hooks/useNavigation'; +import TopNavigation from '../../../ReusableComponents/TopNavigation'; -const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { +const Navigation = ( { stepDetails, onNext, onPrev } ) => { + const { goToWooCommercePaymentsTab } = useNavigation(); const { title, isFirst, percentage, showNext, canProceed } = stepDetails; - const { isScrolled } = useIsScrolled(); const state = OnboardingHooks.useNavigationState(); const isDisabled = ! canProceed( state ); - const className = classNames( 'ppcp-r-navigation-container', { - 'is-scrolled': isScrolled, - } ); return ( -
-
- - - - { ! isFirst && - NextButton( { showNext, isDisabled, onNext, onExit } ) } - -
-
- ); -}; - -const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { - return ( - - { showNext && ( @@ -62,18 +33,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { { __( 'Continue', 'woocommerce-paypal-payments' ) } ) } - - ); -}; - -const ProgressBar = ( { percent } ) => { - percent = Math.min( Math.max( percent, 0 ), 100 ); - - return ( -
+ ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js index a58062006..69ffb6bfd 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js @@ -11,9 +11,6 @@ const OnboardingScreen = () => { const handleNext = () => setStep( currentStep.nextStep ); const handlePrev = () => setStep( currentStep.prevStep ); - const handleExit = () => { - window.location.href = window.ppcpSettings.wcPaymentsTabUrl; - }; return ( <> @@ -21,7 +18,6 @@ const OnboardingScreen = () => { stepDetails={ currentStep } onNext={ handleNext } onPrev={ handlePrev } - onExit={ handleExit } /> From 2a1da98b5f663a8f25afcb3e3f771525f3a10d59 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 15:16:05 +0100 Subject: [PATCH 170/298] =?UTF-8?q?=F0=9F=9A=9A=20Consistent=20file-=20and?= =?UTF-8?q?=20component-naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/Screens/Onboarding/Components/Navigation.js | 4 ++-- .../resources/js/Components/Screens/Onboarding/index.js | 4 ++-- .../Components/{SettingsNavigation.js => Navigation.js} | 0 .../resources/js/Components/Screens/Settings/index.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/{SettingsNavigation.js => Navigation.js} (100%) 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 f03abb383..52d496b33 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 @@ -5,7 +5,7 @@ import { OnboardingHooks } from '../../../../data'; import { useNavigation } from '../../../../hooks/useNavigation'; import TopNavigation from '../../../ReusableComponents/TopNavigation'; -const Navigation = ( { stepDetails, onNext, onPrev } ) => { +const OnboardingNavigation = ( { stepDetails, onNext, onPrev } ) => { const { goToWooCommercePaymentsTab } = useNavigation(); const { title, isFirst, percentage, showNext, canProceed } = stepDetails; @@ -37,4 +37,4 @@ const Navigation = ( { stepDetails, onNext, onPrev } ) => { ); }; -export default Navigation; +export default OnboardingNavigation; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js index 69ffb6bfd..27bdaab9c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js @@ -2,7 +2,7 @@ import Container from '../../ReusableComponents/Container'; import { OnboardingHooks } from '../../../data'; import { getSteps, getCurrentStep } from './Steps'; -import Navigation from './Components/Navigation'; +import OnboardingNavigation from './Components/Navigation'; const OnboardingScreen = () => { const { step, setStep, flags } = OnboardingHooks.useSteps(); @@ -14,7 +14,7 @@ const OnboardingScreen = () => { return ( <> - { const tabs = getSettingsTabs(); From 0ea57ea06f7df2921990e1d384bfafedb9cc785e Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Fri, 10 Jan 2025 16:43:01 +0100 Subject: [PATCH 171/298] Refactor ConnectionInfo component to use merchant data from CommonHooks --- .../ReusableComponents/ConnectionInfo.js | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js index 5fbfdb1f1..0a10b4c17 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js @@ -1,10 +1,17 @@ import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; +import {CommonHooks} from "../../data"; -const ConnectionInfo = ( { connectionStatusDataDefault } ) => { - const [ connectionData, setConnectionData ] = useState( { - ...connectionStatusDataDefault, - } ); + +const ConnectionInfo = () => { + const { merchant } = CommonHooks.useMerchantInfo(); + const [connectionData, setConnectionData] = useState(getDefaultConnectionStatusData(merchant)); + + useEffect(() => { + if (merchant) { + setConnectionData(getDefaultConnectionStatusData(merchant)); + } + }, [merchant]); const toggleStatusClassName = [ 'ppcp-r-connection-status__status-toggle' ]; @@ -15,40 +22,47 @@ const ConnectionInfo = ( { connectionStatusDataDefault } ) => { } return ( -
-
- - { __( 'Merchant ID', 'woocommerce-paypal-payments' ) } - - - { connectionData.merchantId } - -
-
- - { __( 'Email address', 'woocommerce-paypal-payments' ) } - - - { connectionData.email } - -
-
- - { __( 'Client ID', 'woocommerce-paypal-payments' ) } - - - { connectionData.clientId } - -
-
- ); +
+ {renderStatusRow( + __('Merchant ID', 'woocommerce-paypal-payments'), + connectionData.merchantId + )} + {renderStatusRow( + __('Email address', 'woocommerce-paypal-payments'), + connectionData.email + )} + {renderStatusRow( + __('Client ID', 'woocommerce-paypal-payments'), + connectionData.clientId + )} +
+ ); }; export default ConnectionInfo; +export const getDefaultConnectionStatusData = (merchant = null) => { + if (!merchant) { + const contextMerchant = CommonHooks.useMerchantInfo()?.merchant || {}; + return { + connectionStatus: contextMerchant.isConnected || false, + showAllData: false, + email: contextMerchant.email || '', + merchantId: contextMerchant.id || '', + clientId: contextMerchant.clientId || '', + }; + } -export const connectionStatusDataDefault = { - connectionStatus: true, - showAllData: false, - email: 'bt_us@woocommerce.com', - merchantId: 'AT45V2DGMKLRY', - clientId: 'BAARTJLxtUNN4d2GMB6Eut3suMDYad72xQA-FntdIFuJ6FmFJITxAY8', + return { + connectionStatus: merchant.isConnected || false, + showAllData: false, + email: merchant.email || '', + merchantId: merchant.id || '', + clientId: merchant.clientId || '', + }; }; + +const renderStatusRow = (label, value) => ( +
+ {label} + {value} +
+); From 84fd823d84df1da3c67f35d5bb0ca0744fc244ce Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Fri, 10 Jan 2025 16:43:54 +0100 Subject: [PATCH 172/298] Use getDefaultConnectionStatusData and ConnectionInfo for displaying connection status and details --- .../Overview/TabSettingsElements/ConnectionStatus.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index b1018d44c..fa264a6ee 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js @@ -1,7 +1,7 @@ import { __ } from '@wordpress/i18n'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; import ConnectionInfo, { - connectionStatusDataDefault, + getDefaultConnectionStatusData, } from '../../../ReusableComponents/ConnectionInfo'; import TitleBadge, { TITLE_BADGE_NEGATIVE, @@ -20,7 +20,7 @@ const ConnectionStatus = () => {
- { connectionStatusDataDefault.connectionStatus ? ( + { getDefaultConnectionStatusData().connectionStatus ? ( { ) }
- { connectionStatusDataDefault.connectionStatus && ( + { getDefaultConnectionStatusData().connectionStatus && ( ) } From 8b88eaed4b1e8c6414045fd91a174ed4a4a48a58 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Fri, 10 Jan 2025 17:06:32 +0100 Subject: [PATCH 173/298] Remove unused showAllData --- .../js/Components/ReusableComponents/ConnectionInfo.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js index 0a10b4c17..4d06f16e4 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js @@ -13,14 +13,6 @@ const ConnectionInfo = () => { } }, [merchant]); - const toggleStatusClassName = [ 'ppcp-r-connection-status__status-toggle' ]; - - if ( connectionData.showAllData ) { - toggleStatusClassName.push( - 'ppcp-r-connection-status__status-toggle--toggled' - ); - } - return (
{renderStatusRow( @@ -44,7 +36,6 @@ export const getDefaultConnectionStatusData = (merchant = null) => { const contextMerchant = CommonHooks.useMerchantInfo()?.merchant || {}; return { connectionStatus: contextMerchant.isConnected || false, - showAllData: false, email: contextMerchant.email || '', merchantId: contextMerchant.id || '', clientId: contextMerchant.clientId || '', @@ -53,7 +44,6 @@ export const getDefaultConnectionStatusData = (merchant = null) => { return { connectionStatus: merchant.isConnected || false, - showAllData: false, email: merchant.email || '', merchantId: merchant.id || '', clientId: merchant.clientId || '', From 25887024a6a5a2c4095bf156b0e079889dedf3ca Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 18:20:00 +0100 Subject: [PATCH 174/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Start=20to=20unclu?= =?UTF-8?q?tter=20SCSS=20a=20bit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/css/_variables.scss | 5 + .../resources/css/components/_reusable.scss | 19 ++ .../reusable-components/_settings-block.scss | 176 ++++++++++ .../reusable-components/_settings-card.scss | 63 ++++ .../_settings-wrapper.scss | 56 ---- .../css/components/screens/_settings.scss | 310 +----------------- .../screens/settings/_block-accordion.scss | 38 --- .../components/screens/settings/_input.scss | 77 +++++ .../{overview => settings}/_tab-styling.scss | 0 .../ppcp-settings/resources/css/style.scss | 19 +- .../SettingsBlocks/SettingsBlock.js | 10 +- .../ReusableComponents/SettingsCard.js | 6 +- 12 files changed, 351 insertions(+), 428 deletions(-) create mode 100644 modules/ppcp-settings/resources/css/components/_reusable.scss create mode 100644 modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss create mode 100644 modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss delete mode 100644 modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss create mode 100644 modules/ppcp-settings/resources/css/components/screens/settings/_input.scss rename modules/ppcp-settings/resources/css/components/screens/{overview => settings}/_tab-styling.scss (100%) diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss index 10f427ea9..5ccf191e5 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -54,4 +54,9 @@ $card-vertical-gap: 48px; --color-gray-200: #{$color-gray-200}; --color-gray-100: #{$color-gray-100}; --color-gradient-dark: #{$color-gradient-dark}; + + --color-text-title: #{$color-gray-900}; + --color-text-main: #{$color-text-text}; + --color-text-teriary: #{$color-text-tertiary}; + --color-text-description: #{$color-gray-700}; } diff --git a/modules/ppcp-settings/resources/css/components/_reusable.scss b/modules/ppcp-settings/resources/css/components/_reusable.scss new file mode 100644 index 000000000..4d4f5c1ba --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/_reusable.scss @@ -0,0 +1,19 @@ +@import "./reusable-components/payment-method-item"; +@import './reusable-components/accordion-section'; +@import './reusable-components/badge-box'; +@import './reusable-components/busy-state'; +@import './reusable-components/button'; +@import './reusable-components/fields'; +@import './reusable-components/navigation'; +@import './reusable-components/onboarding-header'; +@import './reusable-components/payment-method-icons'; +@import './reusable-components/select-box'; +@import './reusable-components/separator'; +@import './reusable-components/settings-block'; +@import './reusable-components/settings-card'; +@import './reusable-components/settings-toggle-block'; +@import './reusable-components/settings-wrapper'; +@import './reusable-components/spinner-overlay'; +@import './reusable-components/tab-navigation'; +@import './reusable-components/title-badge'; +@import './reusable-components/welcome-docs'; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss new file mode 100644 index 000000000..829be1977 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss @@ -0,0 +1,176 @@ +/* + Styles the `SettingsBlock` and all its derived components. + */ +.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: var(--color-text-title); + 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: var(--color-text-main); + text-transform: none; + } + + .ppcp-r-settings-block__feature__description { + @include font(13, 20, 400); + color: var(--color-text-description); + } + } + + &.ppcp-r-settings-block__toggle { + display: flex; + flex-direction: row; + + .ppcp-r-settings-block__title { + @include font(13, 20, 400); + color: var(--color-text-main); + text-transform: none; + } + } + + .ppcp-r-settings-block__description { + @include font(13, 20, 400); + margin: 0; + color: var(--color-text-description); + + &:not(:last-child) { + padding-bottom: 1em; + } + + a { + color: var(--color-blueberry); + } + + strong { + color: var(--color-gray-800); + } + } + + .ppcp-r-settings-block__supplementary-title-label { + @include font(13, 20, 400); + color: var(--color-text-teriary); + text-transform: none; + margin-left: 5px; + } + + + .ppcp-r-settings-block { + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid var(--color-gray-200); + } + + // 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 { + @include vertical-layout-event-gap(24px); + align-items: flex-start; + + .ppcp-r-vertical-text-control, + input[type='text'] { + width: 100%; + } + } + } +} + +.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; +} + +.ppcp-r-settings-block__accordion { + > .ppcp-r-accordion { + width: 100%; + + .ppcp-r-accordion__toggler { + width: 100%; + margin: 0; + text-align: unset; + } + } +} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss new file mode 100644 index 000000000..634e3a8b7 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss @@ -0,0 +1,63 @@ +/* + Styles the `SettingsCard` layout component. + + This is a 2-column row that displays a title + description on the + left side, and a "card" with settings content on the right side. + */ +.ppcp-r-settings-card { + + // -- Theming + + --card-width-header: 100%; + --card-width-content: 100%; + --card-gap: 0; + --card-layout: block; + + @media screen and (min-width: 960px) { + --card-width-header: 280px; + --card-width-content: 610px; + --card-gap: 48px; + --card-layout: flex; + } + + // -- Styling + + display: var(--card-layout); + gap: var(--card-gap); + margin: 0 0 var(--card-gap) 0; + + .ppcp-r-settings-card__header { + display: var(--card-layout); + width: var(--card-width-header); + flex: 0 0 var(--card-width-header); + gap: 18px; + padding-bottom: 18px; + } + + .ppcp-r-settings-card__content-wrapper { + display: flex; + flex-direction: column; + gap: 24px; + } + + .ppcp-r-settings-card__content { + flex: 1; + max-width: var(--card-width-content); + border: 1px solid var(--color-gray-200); + border-radius: 4px; + padding: 24px; + } + + .ppcp-r-settings-card__title { + @include font(13, 24, 600); + color: var(--color-text-main); + margin: 0 0 4px 0; + display: block; + } + + .ppcp-r-settings-card__description { + @include font(13, 20, 400); + color: var(--color-text-teriary); + margin: 0; + } +} 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 a13df6e77..342e4be1d 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 @@ -26,60 +26,4 @@ border-bottom: 1px solid $color-gray-200; } } - - &-settings-card { - @media screen and (min-width: 960px) { - display: flex; - gap: 48px; - } - - @media screen and (max-width: 480px) { - padding: 24px; - } - - &__content-wrapper { - display: flex; - flex-direction: column; - gap: 24px; - } - - &__header { - display: flex; - gap: 18px; - padding-bottom: 18px; - border-bottom: 2px solid $color-gray-700; - margin-bottom: 32px; - - @media screen and (min-width: 960px) { - width: 280px; - flex-shrink: 0; - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; - } - } - - &__content { - border: 1px solid $color-gray-200; - border-radius: 4px; - padding: 24px; - @media screen and (min-width: 960px) { - flex: 1; - } - } - - &__title { - @include font(13, 24, 600); - color: $color-text-text; - margin: 0 0 4px 0; - display: block; - } - - - &__description { - @include font(13, 20, 400); - color: $color-text-tertiary; - margin: 0; - } - } } diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 4315d3f1a..c5b10be9c 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -1,4 +1,5 @@ -@import "./settings/block-accordion"; +@import './settings/input'; +@import './settings/tab-styling'; // Container and Tab Settings .ppcp-r-tabs.settings, @@ -245,310 +246,3 @@ 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/settings/_block-accordion.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss deleted file mode 100644 index c77a3eb91..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss +++ /dev/null @@ -1,38 +0,0 @@ -.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/components/screens/settings/_input.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_input.scss new file mode 100644 index 000000000..d71a52150 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_input.scss @@ -0,0 +1,77 @@ +// Fields +.ppcp-r { + 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; + } + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss similarity index 100% rename from modules/ppcp-settings/resources/css/components/screens/overview/_tab-styling.scss rename to modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 56fe55a62..0875dcad5 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -3,26 +3,9 @@ #ppcp-settings-container { @import './global'; - @import './components/reusable-components/busy-state'; - @import './components/reusable-components/button'; - @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'; - @import './components/reusable-components/select-box'; - @import './components/reusable-components/tab-navigation'; - @import './components/reusable-components/navigation'; - @import './components/reusable-components/fields'; - @import './components/reusable-components/title-badge'; - @import './components/reusable-components/accordion-section'; - @import './components/reusable-components/badge-box'; - @import './components/reusable-components/spinner-overlay'; - @import './components/reusable-components/welcome-docs'; + @import './components/reusable'; @import './components/screens/onboarding'; @import './components/screens/settings'; - @import './components/screens/overview/tab-styling'; @import './components/app'; } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js index 768fa9387..2ded62c11 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js @@ -1,9 +1,9 @@ -const SettingsBlock = ( { className, children } ) => { - const blockClassName = [ 'ppcp-r-settings-block', className ].filter( - Boolean - ); +import classNames from 'classnames'; - return
{ children }
; +const SettingsBlock = ( { className, children } ) => { + const blockClassName = classNames( 'ppcp-r-settings-block', className ); + + return
{ children }
; }; export default SettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js index a72253301..f209daec1 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js @@ -1,3 +1,5 @@ +import classNames from 'classnames'; + import { Content, ContentWrapper } from './SettingsBlocks'; const SettingsCard = ( { @@ -9,9 +11,7 @@ const SettingsCard = ( { contentItems, contentContainer = true, } ) => { - const className = [ 'ppcp-r-settings-card', extraClassName ] - .filter( Boolean ) - .join( ' ' ); + const className = classNames( 'ppcp-r-settings-card', extraClassName ); const renderContent = () => { // If contentItems array is provided, wrap each item in Content component From 406e8ebc98401c5a15e901fc0fa9f07e023b7211 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 18:29:54 +0100 Subject: [PATCH 175/298] =?UTF-8?q?=E2=9C=A8=20Add=20generic=20option=20to?= =?UTF-8?q?=20suppress=20separator=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../css/components/reusable-components/_settings-block.scss | 2 +- .../ReusableComponents/SettingsBlocks/SettingsBlock.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss index 829be1977..f65c5c9d5 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss @@ -88,7 +88,7 @@ margin-left: 5px; } - + .ppcp-r-settings-block { + + .ppcp-r-settings-block:not(.no-gap) { margin-top: 32px; padding-top: 32px; border-top: 1px solid var(--color-gray-200); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js index 2ded62c11..e033a69df 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js @@ -1,7 +1,9 @@ import classNames from 'classnames'; -const SettingsBlock = ( { className, children } ) => { - const blockClassName = classNames( 'ppcp-r-settings-block', className ); +const SettingsBlock = ( { className, children, separatorAndGap = true } ) => { + const blockClassName = classNames( 'ppcp-r-settings-block', className, { + 'no-gap': ! separatorAndGap, + } ); return
{ children }
; }; From 1c33c4250783bc2cf0328e608606f7c360880fef Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 18:30:11 +0100 Subject: [PATCH 176/298] =?UTF-8?q?=F0=9F=92=84=20Improve=20UI=20of=20webh?= =?UTF-8?q?ook=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Blocks/Troubleshooting/HooksTableBlock.js | 9 ++++-- .../Troubleshooting/ResubscribeBlock.js | 1 + .../Blocks/Troubleshooting/SimulationBlock.js | 31 +++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) 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 index 856f1969d..5a057023c 100644 --- 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 @@ -1,6 +1,9 @@ import { __ } from '@wordpress/i18n'; import { CommonHooks } from '../../../../../../data'; -import { Title } from '../../../../../ReusableComponents/SettingsBlocks'; +import { + SettingsBlock, + Title, +} from '../../../../../ReusableComponents/SettingsBlocks'; const HooksTableBlock = () => { const { webhooks } = CommonHooks.useWebhooks(); @@ -11,10 +14,10 @@ const HooksTableBlock = () => { } return ( - <> + - + ); }; 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 index 373ec57c4..c309681bc 100644 --- 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 @@ -56,6 +56,7 @@ const ResubscribeBlock = () => { 'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.', 'woocommerce-paypal-payments' ) } + separatorAndGap={ false } actionProps={ { buttonType: 'secondary', isBusy: resubscribing, 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 index 2ae430e76..075113ddf 100644 --- 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 @@ -107,23 +107,20 @@ const SimulationBlock = () => { }; return ( - <> - startSimulation( 30 ), - value: __( - 'Simulate webhooks', - 'woocommerce-paypal-payments' - ), - } } - /> - + startSimulation( 30 ), + value: __( 'Simulate webhooks', 'woocommerce-paypal-payments' ), + } } + /> ); }; export default SimulationBlock; From 0735cae04fc5cfb6406673045d9de7ac8661bbe1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 19:12:22 +0100 Subject: [PATCH 177/298] =?UTF-8?q?=F0=9F=92=84=20Make=20the=20send-only?= =?UTF-8?q?=20message=20look=20a=20bit=20nicer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/Screens/SendOnlyMessage.js | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js b/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js index 884f790fc..56d780250 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js @@ -1,18 +1,54 @@ import { __, sprintf } from '@wordpress/i18n'; +import Container from '../ReusableComponents/Container'; +import SettingsCard from '../ReusableComponents/SettingsCard'; +import SettingsNavigation from './SettingsNavigation'; + const SendOnlyMessage = () => { const settingsPageUrl = '/wp-admin/admin.php?page=wc-settings'; - const message = sprintf( - /* translators: 1: URL to the WooCommerce store location settings */ - __( - 'Your current WooCommerce store location is in a "send-only" country, according to PayPal\'s policies. Sellers in these countries are unable to receive payments via PayPal. Since receiving payments is essential for using the PayPal Payments extension, you will not be able to connect your PayPal account while operating from a "send-only" country. To activate PayPal, please update your WooCommerce store location to a supported region and connect a PayPal account eligible for receiving payments.', - 'woocommerce-paypal-payments' - ), - settingsPageUrl + return ( + <> + + + +

+ { __( + 'Your current WooCommerce store location is in a "send-only" country, according to PayPal\'s policies', + 'woocommerce-paypal-payments' + ) } +

+

+ { __( + 'Since receiving payments is essential for using the PayPal Payments extension, you are unable to connect your PayPal account while operating from a "send-only" country.', + 'woocommerce-paypal-payments' + ) } +

+

WooCommerce store location to a supported region and connect a PayPal account eligible for receiving payments.', + 'woocommerce-paypal-payments' + ), + settingsPageUrl + ), + } } + /> + + + ); - - return

; }; export default SendOnlyMessage; From 54e2258d62f8e6f199da5e676e7b5da496508a35 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 19:15:02 +0100 Subject: [PATCH 178/298] =?UTF-8?q?=E2=9C=A8=20Improve=20the=20loading=20e?= =?UTF-8?q?xperience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, the settings screen was briefly visible, before the merchant details were loaded from the REST endpoint --- .../resources/js/Components/Screens/Settings.js | 16 ++++++++++++---- .../resources/js/data/common/hooks.js | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index 112200e87..128884f10 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -12,7 +12,10 @@ import SendOnlyMessage from './SendOnlyMessage'; const Settings = () => { const onboardingProgress = OnboardingHooks.useSteps(); - const { merchant } = useMerchantInfo(); + const { + isReady: merchantIsReady, + merchant: { isSendOnlyCountry }, + } = useMerchantInfo(); // Disable the "Changes you made might not be saved" browser warning. useEffect( () => { @@ -33,7 +36,7 @@ const Settings = () => { } ); const Content = useMemo( () => { - if ( ! onboardingProgress.isReady ) { + if ( ! onboardingProgress.isReady || ! merchantIsReady ) { return ( { ); } - if ( merchant.isCurrentCountrySendOnly ) { + if ( isSendOnlyCountry ) { return ; } @@ -50,7 +53,12 @@ const Settings = () => { } return ; - }, [ onboardingProgress ] ); + }, [ + isSendOnlyCountry, + merchantIsReady, + onboardingProgress.completed, + onboardingProgress.isReady, + ] ); return

{ Content }
; }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 4aea08caa..8fcf3f6a9 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -144,7 +144,7 @@ export const useWebhooks = () => { }; }; export const useMerchantInfo = () => { - const { merchant, features } = useHooks(); + const { isReady, merchant, features } = useHooks(); const { refreshMerchantData } = useDispatch( STORE_NAME ); const verifyLoginStatus = useCallback( async () => { @@ -159,6 +159,7 @@ export const useMerchantInfo = () => { }, [ refreshMerchantData, merchant ] ); return { + isReady, merchant, // Merchant details features, // Eligible merchant features verifyLoginStatus, // Callback From 4fa6c78f83aa359c84c9925cd1be0bb8787514f8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 19:15:48 +0100 Subject: [PATCH 179/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20a=20shorter?= =?UTF-8?q?=20attribute=20name=20for=20send-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Data/GeneralSettings.php | 22 +++++++++---------- .../src/Endpoint/CommonRestEndpoint.php | 16 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-settings/src/Data/GeneralSettings.php b/modules/ppcp-settings/src/Data/GeneralSettings.php index 8970c6c05..94e8a380f 100644 --- a/modules/ppcp-settings/src/Data/GeneralSettings.php +++ b/modules/ppcp-settings/src/Data/GeneralSettings.php @@ -53,8 +53,8 @@ class GeneralSettings extends AbstractDataModel { $this->woo_settings['country'] = $country; $this->woo_settings['currency'] = $currency; - $this->data['is_current_country_send_only'] = $is_send_only_country; - $this->data['merchant_connected'] = $this->is_merchant_connected(); + $this->data['is_send_only_country'] = $is_send_only_country; + $this->data['merchant_connected'] = $this->is_merchant_connected(); } /** @@ -64,17 +64,17 @@ class GeneralSettings extends AbstractDataModel { */ protected function get_defaults() : array { return array( - 'use_sandbox' => false, // UI state, not a connection detail. - 'use_manual_connection' => false, // UI state, not a connection detail. - 'is_current_country_send_only' => false, // Read-only flag. + 'use_sandbox' => false, // UI state, not a connection detail. + 'use_manual_connection' => false, // UI state, not a connection detail. + 'is_send_only_country' => false, // Read-only flag. // Details about connected merchant account. - 'merchant_connected' => false, - 'sandbox_merchant' => false, - 'merchant_id' => '', - 'merchant_email' => '', - 'client_id' => '', - 'client_secret' => '', + 'merchant_connected' => false, + 'sandbox_merchant' => false, + 'merchant_id' => '', + 'merchant_email' => '', + 'client_id' => '', + 'client_secret' => '', ); } diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 589e0ea01..d99204c80 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -61,26 +61,26 @@ class CommonRestEndpoint extends RestEndpoint { * @var array */ private array $merchant_info_map = array( - 'merchant_connected' => array( + 'merchant_connected' => array( 'js_name' => 'isConnected', ), - 'sandbox_merchant' => array( + 'sandbox_merchant' => array( 'js_name' => 'isSandbox', ), - 'merchant_id' => array( + 'merchant_id' => array( 'js_name' => 'id', ), - 'merchant_email' => array( + 'merchant_email' => array( 'js_name' => 'email', ), - 'client_id' => array( + 'client_id' => array( 'js_name' => 'clientId', ), - 'client_secret' => array( + 'client_secret' => array( 'js_name' => 'clientSecret', ), - 'is_current_country_send_only' => array( - 'js_name' => 'isCurrentCountrySendOnly', + 'is_send_only_country' => array( + 'js_name' => 'isSendOnlyCountry', ), ); From 3d17fcbad9239b11aedf01723f559eb054cf1517 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 10 Jan 2025 20:40:36 +0100 Subject: [PATCH 180/298] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20phpcs=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/PayLaterConfiguratorModule.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php index 6c05696a4..8e46bcf33 100644 --- a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php +++ b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php @@ -162,10 +162,10 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec * The notice appears on any PayPal-Settings page, except for the Pay-Later settings page, * when no Pay-Later messaging is used yet. * - * @param array $message_locations PayLater messaging locations. - * @param bool $is_settings_page Whether the current page is a WC settings page. - * @param string $current_page_id ID of current settings page tab. - * @param OnboardingProfile $onboarding_profile Onboarding profile. + * @param array $message_locations PayLater messaging locations. + * @param bool $is_settings_page Whether the current page is a WC settings page. + * @param string $current_page_id ID of current settings page tab. + * @param OnboardingProfile $onboarding_profile Onboarding profile. * * @return void */ From 15782945e29e8132c2e0bf6eb55ccd6649f181ff Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 13 Jan 2025 14:13:55 +0400 Subject: [PATCH 181/298] Add 'pay-later-messaging' tab ID --- modules/ppcp-settings/resources/js/utils/tabSelector.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-settings/resources/js/utils/tabSelector.js b/modules/ppcp-settings/resources/js/utils/tabSelector.js index c8b1c5350..5c7105ffc 100644 --- a/modules/ppcp-settings/resources/js/utils/tabSelector.js +++ b/modules/ppcp-settings/resources/js/utils/tabSelector.js @@ -4,6 +4,7 @@ export const TAB_IDS = { PAYMENT_METHODS: 'tab-panel-0-payment-methods', SETTINGS: 'tab-panel-0-settings', STYLING: 'tab-panel-0-styling', + PAY_LATER_MESSAGING: 'tab-panel-0-pay-later-messaging', }; /** From 6a7eb8fa1f9e80a652e1e51f325a63ac89eba963 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 13 Jan 2025 14:14:17 +0400 Subject: [PATCH 182/298] Create 'pay-later-messaging' empty tab --- .../js/Components/Screens/Overview/TabPayLaterMessaging.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js new file mode 100644 index 000000000..b388a6bee --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js @@ -0,0 +1,5 @@ +const TabPayLaterMessaging = () => { + return
; +}; + +export default TabPayLaterMessaging; From 140e028054a8f091581abc6737a3f47745136cae Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 13 Jan 2025 14:14:35 +0400 Subject: [PATCH 183/298] Add 'pay-later-messaging' tab into array of tabs --- .../ppcp-settings/resources/js/Components/Screens/tabs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/tabs.js b/modules/ppcp-settings/resources/js/Components/Screens/tabs.js index c2f3b25e6..e828dab9f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/tabs.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/tabs.js @@ -3,6 +3,7 @@ import TabOverview from './Overview/TabOverview'; import TabPaymentMethods from './Overview/TabPaymentMethods'; import TabSettings from './Overview/TabSettings'; import TabStyling from './Overview/TabStyling'; +import TabPayLaterMessaging from './Overview/TabPayLaterMessaging'; export const getSettingsTabs = () => { const tabs = []; @@ -31,5 +32,11 @@ export const getSettingsTabs = () => { component: , } ); + tabs.push( { + name: 'pay-later-messaging', + title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ), + component: , + } ); + return tabs; }; From 879f9b94e1115a71f685c94f5ff92d3b226011ff Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Mon, 13 Jan 2025 11:29:11 +0100 Subject: [PATCH 184/298] Change wording Advanced to Expanded --- .../ReusableComponents/WelcomeDocs/AcdcFlow.js | 15 +++++++++------ .../ReusableComponents/WelcomeDocs/BcdcFlow.js | 10 ++++++---- .../Screens/Onboarding/StepPaymentMethods.js | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) 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 3f4a6ff02..87e6b6861 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js @@ -95,12 +95,13 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
@@ -175,12 +176,13 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
@@ -249,12 +251,13 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
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 591412be3..7fa58bca4 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js @@ -92,12 +92,13 @@ const BcdcFlow = ( { isPayLater, storeCountry } ) => {
@@ -158,12 +159,13 @@ const BcdcFlow = ( { isPayLater, storeCountry } ) => { 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 ac56180a3..b6badf36c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js @@ -21,7 +21,7 @@ const StepPaymentMethods = ( {} ) => {
From e5023b50075b6dc0900b492346683c215fe381b9 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 13:46:04 +0100 Subject: [PATCH 185/298] =?UTF-8?q?=E2=9C=A8=20New=20REST=20endpoint=20to?= =?UTF-8?q?=20disconnect=20merchant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoint/AuthenticationRestEndpoint.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 9d72ff88c..a558f6e71 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -123,6 +123,15 @@ class AuthenticationRestEndpoint extends RestEndpoint { ), ) ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/disconnect', + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'disconnect' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); } /** @@ -176,4 +185,15 @@ class AuthenticationRestEndpoint extends RestEndpoint { return $this->return_success( $response ); } + + /** + * Disconnect the merchant and clear the authentication details. + * + * @return WP_REST_Response + */ + public function disconnect() : WP_REST_Response { + $this->authentication_manager->disconnect(); + + return $this->return_success( 'OK' ); + } } From 4f61251ae7ab8c438a007c15335ad6b3c187698f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 13:56:41 +0100 Subject: [PATCH 186/298] =?UTF-8?q?=F0=9F=92=A1=20Document=20REST=20paths?= =?UTF-8?q?=20in=20source=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoint/AuthenticationRestEndpoint.php | 20 +++++++++++++++++++ .../src/Endpoint/CommonRestEndpoint.php | 12 +++++++++++ .../src/Endpoint/LoginLinkRestEndpoint.php | 7 +++++++ .../src/Endpoint/OnboardingRestEndpoint.php | 9 +++++++++ .../Endpoint/RefreshFeatureStatusEndpoint.php | 3 +++ .../src/Endpoint/WebhookSettingsEndpoint.php | 8 ++++++++ 6 files changed, 59 insertions(+) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index a558f6e71..5ffd48baa 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -67,6 +67,14 @@ class AuthenticationRestEndpoint extends RestEndpoint { * Configure REST API routes. */ public function register_routes() : void { + /** + * POST /wp-json/wc/v3/wc_paypal/authenticate/direct + * { + * clientId + * clientSecret + * useSandbox + * } + */ register_rest_route( $this->namespace, '/' . $this->rest_base . '/direct', @@ -97,6 +105,14 @@ class AuthenticationRestEndpoint extends RestEndpoint { ) ); + /** + * POST /wp-json/wc/v3/wc_paypal/authenticate/isu + * { + * sharedId + * authCode + * useSandbox + * } + */ register_rest_route( $this->namespace, '/' . $this->rest_base . '/isu', @@ -123,6 +139,10 @@ class AuthenticationRestEndpoint extends RestEndpoint { ), ) ); + + /** + * POST /wp-json/wc/v3/wc_paypal/authenticate/disconnect + */ register_rest_route( $this->namespace, '/' . $this->rest_base . '/disconnect', diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index d99204c80..6da060f28 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -111,6 +111,9 @@ class CommonRestEndpoint extends RestEndpoint { * Configure REST API routes. */ public function register_routes() { + /** + * GET /wp-json/wc/v3/wc_paypal/common + */ register_rest_route( $this->namespace, '/' . $this->rest_base, @@ -121,6 +124,12 @@ class CommonRestEndpoint extends RestEndpoint { ) ); + /** + * POST /wp-json/wc/v3/wc_paypal/common + * { + * // Fields mentioned in $field_map[]['js_name'] + * } + */ register_rest_route( $this->namespace, '/' . $this->rest_base, @@ -131,6 +140,9 @@ class CommonRestEndpoint extends RestEndpoint { ) ); + /** + * GET /wp-json/wc/v3/wc_paypal/common/merchant + */ register_rest_route( $this->namespace, "/$this->rest_base/merchant", diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index 7ddee27a5..c2b0e9ff3 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -52,6 +52,13 @@ class LoginLinkRestEndpoint extends RestEndpoint { * Configure REST API routes. */ public function register_routes() : void { + /** + * POST /wp-json/wc/v3/wc_paypal/login_link + * { + * useSandbox + * products + * } + */ register_rest_route( $this->namespace, '/' . $this->rest_base, diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 018ab2dc2..19cdcd530 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -97,6 +97,9 @@ class OnboardingRestEndpoint extends RestEndpoint { * Configure REST API routes. */ public function register_routes() { + /** + * GET /wp-json/wc/v3/wc_paypal/onboarding + */ register_rest_route( $this->namespace, '/' . $this->rest_base, @@ -107,6 +110,12 @@ class OnboardingRestEndpoint extends RestEndpoint { ) ); + /** + * POST /wp-json/wc/v3/wc_paypal/onboarding + * { + * // Fields mentioned in $field_map[]['js_name'] + * } + */ register_rest_route( $this->namespace, '/' . $this->rest_base, diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php index dfbfc3a3a..7e8335e00 100644 --- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -83,6 +83,9 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { * Configure REST API routes. */ public function register_routes() { + /** + * POST /wp-json/wc/v3/wc_paypal/refresh-feature-status + */ register_rest_route( $this->namespace, '/' . $this->rest_base, diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php index df227264a..603496194 100644 --- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -77,6 +77,10 @@ class WebhookSettingsEndpoint extends RestEndpoint { * Configure REST API routes. */ public function register_routes() : void { + /** + * GET /wp-json/wc/v3/wc_paypal/webhook_settings + * POST /wp-json/wc/v3/wc_paypal/webhook_settings + */ register_rest_route( $this->namespace, '/' . $this->rest_base, @@ -94,6 +98,10 @@ class WebhookSettingsEndpoint extends RestEndpoint { ) ); + /** + * GET /wp-json/wc/v3/wc_paypal/webhook_simulate + * POST /wp-json/wc/v3/wc_paypal/webhook_simulate + */ register_rest_route( $this->namespace, '/' . $this->rest_simulate_base, From 1bee5f1c46ae0a1c7ddb579f1f1a7baa9313cf5d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 13:57:42 +0100 Subject: [PATCH 187/298] =?UTF-8?q?=F0=9F=8E=A8=20Minor=20code-style=20cha?= =?UTF-8?q?nges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php | 2 +- .../ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php | 2 +- .../src/Endpoint/RefreshFeatureStatusEndpoint.php | 2 +- .../ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php | 6 +++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 6da060f28..35af6c093 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -110,7 +110,7 @@ class CommonRestEndpoint extends RestEndpoint { /** * Configure REST API routes. */ - public function register_routes() { + public function register_routes() : void { /** * GET /wp-json/wc/v3/wc_paypal/common */ diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 19cdcd530..54aced7ef 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -96,7 +96,7 @@ class OnboardingRestEndpoint extends RestEndpoint { /** * Configure REST API routes. */ - public function register_routes() { + public function register_routes() : void { /** * GET /wp-json/wc/v3/wc_paypal/onboarding */ diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php index 7e8335e00..7f1d4468c 100644 --- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -82,7 +82,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { /** * Configure REST API routes. */ - public function register_routes() { + public function register_routes() : void { /** * POST /wp-json/wc/v3/wc_paypal/refresh-feature-status */ diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php index 603496194..5e56de9e3 100644 --- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -67,7 +67,11 @@ class WebhookSettingsEndpoint extends RestEndpoint { * @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks. * @param WebhookSimulation $webhook_simulation A service that allows webhook simulations. */ - public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) { + public function __construct( + WebhookEndpoint $webhook_endpoint, + WebhookRegistrar $webhook_registrar, + WebhookSimulation $webhook_simulation + ) { $this->webhook_endpoint = $webhook_endpoint; $this->webhook_registrar = $webhook_registrar; $this->webhook_simulation = $webhook_simulation; From 8f38bac810e84155f87c11ab6c73cb38a7b40a0e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:06:24 +0100 Subject: [PATCH 188/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20REST?= =?UTF-8?q?=20endpoints:=20Webhooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/constants.js | 8 ++++---- .../src/Endpoint/WebhookSettingsEndpoint.php | 19 ++++++------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 49abba6db..40c01b6e3 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -66,24 +66,24 @@ export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu'; export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link'; /** - * REST path to fetch webhooks data or resubscribe webhooks, + * 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'; +export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhooks'; /** - * REST path to start webhook simulation and observe the state, + * 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'; +export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhooks/simulate'; /** * REST path to refresh the feature status. diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php index 5e56de9e3..81e8f4335 100644 --- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -29,14 +29,7 @@ class WebhookSettingsEndpoint extends RestEndpoint { * * @var string */ - protected $rest_base = 'webhook_settings'; - - /** - * Endpoint base to start webhook simulation and check the state - * - * @var string - */ - protected string $rest_simulate_base = 'webhook_simulate'; + protected $rest_base = 'webhooks'; /** * Application webhook endpoint @@ -82,8 +75,8 @@ class WebhookSettingsEndpoint extends RestEndpoint { */ public function register_routes() : void { /** - * GET /wp-json/wc/v3/wc_paypal/webhook_settings - * POST /wp-json/wc/v3/wc_paypal/webhook_settings + * GET /wp-json/wc/v3/wc_paypal/webhooks + * POST /wp-json/wc/v3/wc_paypal/webhooks */ register_rest_route( $this->namespace, @@ -103,12 +96,12 @@ class WebhookSettingsEndpoint extends RestEndpoint { ); /** - * GET /wp-json/wc/v3/wc_paypal/webhook_simulate - * POST /wp-json/wc/v3/wc_paypal/webhook_simulate + * GET /wp-json/wc/v3/wc_paypal/webhooks/simulate + * POST /wp-json/wc/v3/wc_paypal/webhooks/simulate */ register_rest_route( $this->namespace, - '/' . $this->rest_simulate_base, + '/' . $this->rest_base . '/simulate', array( array( 'methods' => WP_REST_Server::READABLE, From 53ce535f4a93a9809463b121957c0c49a77ad41f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:08:20 +0100 Subject: [PATCH 189/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Shorten=20REST=20e?= =?UTF-8?q?ndpoints:=20Features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/constants.js | 3 +-- .../src/Endpoint/RefreshFeatureStatusEndpoint.php | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 40c01b6e3..6101ed31a 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -93,5 +93,4 @@ export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhooks/simulate'; * * @type {string} */ -export const REST_REFRESH_FEATURES_PATH = - '/wc/v3/wc_paypal/refresh-feature-status'; +export const REST_REFRESH_FEATURES_PATH = '/wc/v3/wc_paypal/refresh-features'; diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php index 7f1d4468c..3b17b84ed 100644 --- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -25,7 +25,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { * * @var string */ - protected $rest_base = 'refresh-feature-status'; + protected $rest_base = 'refresh-features'; /** * Cache timeout in seconds. @@ -84,7 +84,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { */ public function register_routes() : void { /** - * POST /wp-json/wc/v3/wc_paypal/refresh-feature-status + * POST /wp-json/wc/v3/wc_paypal/refresh-features */ register_rest_route( $this->namespace, From ec029dd4c5cfa547f63a954c635b32c51eb56e4c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:09:04 +0100 Subject: [PATCH 190/298] =?UTF-8?q?=E2=9C=A8=20Flush=20APM=20feature=20cac?= =?UTF-8?q?he=20on=20merchant=20disconnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/AuthenticationManager.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 96d7f3bc1..f722ab297 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -122,6 +122,11 @@ class AuthenticationManager { * is no need for it here, it's good house-keeping practice to clean up. */ do_action( 'woocommerce_paypal_payments_flush_api_cache' ); + + /** + * Clear the APM eligibility flags from the default settings object. + */ + do_action( 'woocommerce_paypal_payments_clear_apm_product_status' ); } /** @@ -420,6 +425,11 @@ class AuthenticationManager { */ do_action( 'woocommerce_paypal_payments_authenticated_merchant' ); + /** + * Clear the APM eligibility flags from the default settings object. + */ + do_action( 'woocommerce_paypal_payments_clear_apm_product_status' ); + /** * Subscribe the new merchant to relevant PayPal webhooks. */ From 30ab1bd51f5e1aadf081b009efcbc3a9cc086cd5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:11:24 +0100 Subject: [PATCH 191/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20auth?= =?UTF-8?q?entication=20name:=20OAuth,=20not=20ISU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/constants.js | 5 +++-- .../resources/js/data/common/controls.js | 4 ++-- .../src/Endpoint/AuthenticationRestEndpoint.php | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 6101ed31a..9faa698e5 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -46,14 +46,15 @@ export const REST_DIRECT_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/direct'; /** - * REST path to perform the ISU authentication check, using shared ID and authCode. + * REST path to perform the OAuth authentication check, using shared ID and authCode. * * Used by: Controls * See: AuthenticateRestEndpoint.php * * @type {string} */ -export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu'; +export const REST_OAUTH_AUTHENTICATION_PATH = + '/wc/v3/wc_paypal/authenticate/oauth'; /** * REST path to generate an ISU URL for the PayPal-login. diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 62d4a8f84..dc2f04710 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -15,7 +15,7 @@ import { REST_CONNECTION_URL_PATH, REST_HYDRATE_MERCHANT_PATH, REST_REFRESH_FEATURES_PATH, - REST_ISU_AUTHENTICATION_PATH, + REST_OAUTH_AUTHENTICATION_PATH, REST_WEBHOOKS, REST_WEBHOOKS_SIMULATE, } from './constants'; @@ -82,7 +82,7 @@ export const controls = { } ) { try { return await apiFetch( { - path: REST_ISU_AUTHENTICATION_PATH, + path: REST_OAUTH_AUTHENTICATION_PATH, method: 'POST', data: { sharedId, diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 5ffd48baa..7ac964b2d 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -106,7 +106,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { ); /** - * POST /wp-json/wc/v3/wc_paypal/authenticate/isu + * POST /wp-json/wc/v3/wc_paypal/authenticate/oauth * { * sharedId * authCode @@ -115,10 +115,10 @@ class AuthenticationRestEndpoint extends RestEndpoint { */ register_rest_route( $this->namespace, - '/' . $this->rest_base . '/isu', + '/' . $this->rest_base . '/oauth', array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'connect_isu' ), + 'callback' => array( $this, 'connect_oauth' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'sharedId' => array( @@ -181,14 +181,14 @@ class AuthenticationRestEndpoint extends RestEndpoint { } /** - * ISU login: Retrieves clientId and clientSecret using a sharedId and authCode. + * OAuth login: Retrieves clientId and clientSecret using a sharedId and authCode. * - * This is the final step in the UI-driven login via the ISU popup, which + * This is the final step in the UI-driven login via the OAuth popup, which * is triggered by the LoginLinkRestEndpoint URL. * * @param WP_REST_Request $request Full data about the request. */ - public function connect_isu( WP_REST_Request $request ) : WP_REST_Response { + public function connect_oauth( WP_REST_Request $request ) : WP_REST_Response { $shared_id = $request->get_param( 'sharedId' ); $auth_code = $request->get_param( 'authCode' ); $use_sandbox = $request->get_param( 'useSandbox' ); From bc08aa74da195446a6f85936a2d7651892212c62 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:12:57 +0100 Subject: [PATCH 192/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Shorten=20action?= =?UTF-8?q?=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/action-types.js | 3 +-- modules/ppcp-settings/resources/js/data/common/actions.js | 2 +- modules/ppcp-settings/resources/js/data/common/controls.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) 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 8ae56b20c..91f350482 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -26,6 +26,5 @@ export default { DO_REFRESH_FEATURES: 'COMMON: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', + DO_CHECK_WEBHOOK_SIMULATION: 'COMMON:DO_CHECK_WEBHOOK_SIMULATION', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index c859fe0be..91652f56e 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -288,5 +288,5 @@ export const startWebhookSimulation = function* () { * @return {Action} The action. */ export const checkWebhookSimulationState = function* () { - return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE }; + return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION }; }; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index dc2f04710..a7e53a8da 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -138,7 +138,7 @@ export const controls = { } ); }, - async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE ]() { + async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION ]() { return await apiFetch( { path: REST_WEBHOOKS_SIMULATE, } ); From 401faf6975bc5d4696b46c86ff0c60c439fc8414 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:16:46 +0100 Subject: [PATCH 193/298] =?UTF-8?q?=E2=9C=A8=20New=20Redux=20action=20to?= =?UTF-8?q?=20disconnect=20merchant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 1 + .../ppcp-settings/resources/js/data/common/actions.js | 9 +++++++++ .../resources/js/data/common/constants.js | 11 +++++++++++ .../resources/js/data/common/controls.js | 8 ++++++++ 4 files changed, 29 insertions(+) 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 91f350482..f33e8a9ee 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -21,6 +21,7 @@ export default { DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_DIRECT_API_AUTHENTICATION: 'COMMON:DO_DIRECT_API_AUTHENTICATION', DO_OAUTH_AUTHENTICATION: 'COMMON:DO_OAUTH_AUTHENTICATION', + DO_DISCONNECT_MERCHANT: 'COMMON:DO_DISCONNECT_MERCHANT', DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES', diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 91652f56e..0c4cb4af1 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -212,6 +212,15 @@ export const authenticateWithOAuth = function* ( }; }; +/** + * Side effect. Checks webhook simulation. + * + * @return {Action} The action. + */ +export const disconnectMerchant = function () { + return { type: ACTION_TYPES.DO_DISCONNECT_MERCHANT }; +}; + /** * Side effect. Clears and refreshes the merchant data via a REST request. * diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 9faa698e5..86bd670db 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -56,6 +56,17 @@ export const REST_DIRECT_AUTHENTICATION_PATH = export const REST_OAUTH_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/oauth'; +/** + * REST path to disconnect the current merchant from PayPal. + * + * Used by: Controls + * See: AuthenticateRestEndpoint.php + * + * @type {string} + */ +export const REST_DISCONNECT_MERCHANT_PATH = + '/wc/v3/wc_paypal/authenticate/disconnect'; + /** * REST path to generate an ISU URL for the PayPal-login. * diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index a7e53a8da..6a5b4f9bc 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -16,6 +16,7 @@ import { REST_HYDRATE_MERCHANT_PATH, REST_REFRESH_FEATURES_PATH, REST_OAUTH_AUTHENTICATION_PATH, + REST_DISCONNECT_MERCHANT_PATH, REST_WEBHOOKS, REST_WEBHOOKS_SIMULATE, } from './constants'; @@ -98,6 +99,13 @@ export const controls = { } }, + async [ ACTION_TYPES.DO_DISCONNECT ]() { + return await apiFetch( { + path: REST_DISCONNECT_MERCHANT_PATH, + method: 'POST', + } ); + }, + async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() { try { return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } ); From 4b83d588ec76d1620a4ebdbcdd2ee810a2c5a467 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:21:52 +0100 Subject: [PATCH 194/298] =?UTF-8?q?=F0=9F=90=9B=20Fix=20a=20PHP=20error=20?= =?UTF-8?q?during=20disconnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Service/AuthenticationManager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index f722ab297..95acf068b 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -126,7 +126,7 @@ class AuthenticationManager { /** * Clear the APM eligibility flags from the default settings object. */ - do_action( 'woocommerce_paypal_payments_clear_apm_product_status' ); + do_action( 'woocommerce_paypal_payments_clear_apm_product_status', null ); } /** @@ -428,7 +428,7 @@ class AuthenticationManager { /** * Clear the APM eligibility flags from the default settings object. */ - do_action( 'woocommerce_paypal_payments_clear_apm_product_status' ); + do_action( 'woocommerce_paypal_payments_clear_apm_product_status', null ); /** * Subscribe the new merchant to relevant PayPal webhooks. From 364c1b55c5f2d883b2d83c7d7b83bd1473bb8ca2 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:41:08 +0100 Subject: [PATCH 195/298] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=20disconnect?= =?UTF-8?q?=20Redux=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/actions.js | 4 ++-- modules/ppcp-settings/resources/js/data/common/controls.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 0c4cb4af1..8f2b14812 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -217,8 +217,8 @@ export const authenticateWithOAuth = function* ( * * @return {Action} The action. */ -export const disconnectMerchant = function () { - return { type: ACTION_TYPES.DO_DISCONNECT_MERCHANT }; +export const disconnectMerchant = function* () { + return yield { type: ACTION_TYPES.DO_DISCONNECT_MERCHANT }; }; /** diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 6a5b4f9bc..a318e2116 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -11,10 +11,10 @@ import apiFetch from '@wordpress/api-fetch'; import { REST_PERSIST_PATH, - REST_DIRECT_AUTHENTICATION_PATH, REST_CONNECTION_URL_PATH, REST_HYDRATE_MERCHANT_PATH, REST_REFRESH_FEATURES_PATH, + REST_DIRECT_AUTHENTICATION_PATH, REST_OAUTH_AUTHENTICATION_PATH, REST_DISCONNECT_MERCHANT_PATH, REST_WEBHOOKS, @@ -99,7 +99,7 @@ export const controls = { } }, - async [ ACTION_TYPES.DO_DISCONNECT ]() { + async [ ACTION_TYPES.DO_DISCONNECT_MERCHANT ]() { return await apiFetch( { path: REST_DISCONNECT_MERCHANT_PATH, method: 'POST', From 5173954776714c3dbcaccdf72a14ad6a72ac391d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:42:05 +0100 Subject: [PATCH 196/298] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20debug=20tool=20to=20disconnect=20merchant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/debug.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index 6380c6d6a..a57c62a05 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -5,6 +5,7 @@ export const addDebugTools = ( context, modules ) => { return; } + // Dump the current state of all our Redux stores. context.dumpStore = async () => { /* eslint-disable no-console */ if ( ! console?.groupCollapsed ) { @@ -43,10 +44,15 @@ export const addDebugTools = ( context, modules ) => { } ); }; - context.startOnboarding = () => { - const onboarding = wp.data.dispatch( OnboardingStoreName ); - onboarding.setCompleted( false ); - onboarding.setStep( 0 ); - onboarding.persist(); + // Disconnect the merchant and display the onboarding wizard. + context.disconnect = () => { + const common = wp.data.dispatch( CommonStoreName ); + + common.disconnectMerchant(); + + // eslint-disable-next-line no-console + console.log( 'Disconnected from PayPal. Reloading the page...' ); + + window.location.reload(); }; }; From 99d93888e1c3caa0ace72cd9db6b3ac567c42c0d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 14:42:59 +0100 Subject: [PATCH 197/298] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Deb?= =?UTF-8?q?ug=20tool=20=E2=80=9Creset=E2=80=9D=20respects=20onboarding=20s?= =?UTF-8?q?tate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/reducer.js | 2 ++ .../ppcp-settings/resources/js/data/debug.js | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index e9ff90a42..559024728 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -77,6 +77,8 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { // Keep "read-only" details and initialization flags. cleanState.wooSettings = { ...state.wooSettings }; + cleanState.merchant = { ...state.merchant }; + cleanState.features = { ...state.features }; cleanState.isReady = true; return cleanState; diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index a57c62a05..861af8de9 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -33,12 +33,32 @@ export const addDebugTools = ( context, modules ) => { /* eslint-enable no-console */ }; + // Reset all Redux stores to their initial state. context.resetStore = () => { - const stores = [ OnboardingStoreName, CommonStoreName ]; + const stores = []; + const { isConnected } = wp.data.select( CommonStoreName ).merchant(); + + if ( isConnected ) { + // Make sure the Onboarding wizard is "completed". + const onboarding = wp.data.dispatch( OnboardingStoreName ); + onboarding.setCompleted( true ); + onboarding.persist(); + + // Reset all stores, except for the onboarding store. + stores.push( CommonStoreName ); + // TODO: Add other stores here once they are available. + } else { + // Only reset the common & onboarding stores to restart the onboarding wizard. + stores.push( CommonStoreName ); + stores.push( OnboardingStoreName ); + } stores.forEach( ( storeName ) => { const store = wp.data.dispatch( storeName ); + // eslint-disable-next-line no-console + console.log( `Reset store: ${ storeName }...` ); + store.reset(); store.persist(); } ); From c2c4347ce2491ab9faec1b2bcf55d3a155806e3a Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Mon, 13 Jan 2025 15:21:36 +0100 Subject: [PATCH 198/298] use merchant directly --- .../ReusableComponents/ConnectionInfo.js | 23 ++----------------- .../TabSettingsElements/ConnectionStatus.js | 16 +++++-------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js index 4d06f16e4..0c90bf58f 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js @@ -1,17 +1,8 @@ import { __ } from '@wordpress/i18n'; -import { useState, useEffect } from '@wordpress/element'; -import {CommonHooks} from "../../data"; - +import { CommonHooks } from '../../data'; const ConnectionInfo = () => { - const { merchant } = CommonHooks.useMerchantInfo(); - const [connectionData, setConnectionData] = useState(getDefaultConnectionStatusData(merchant)); - - useEffect(() => { - if (merchant) { - setConnectionData(getDefaultConnectionStatusData(merchant)); - } - }, [merchant]); + const { merchant } = CommonHooks.useMerchantInfo(); return (
@@ -31,16 +22,6 @@ const ConnectionInfo = () => { ); }; export default ConnectionInfo; -export const getDefaultConnectionStatusData = (merchant = null) => { - if (!merchant) { - const contextMerchant = CommonHooks.useMerchantInfo()?.merchant || {}; - return { - connectionStatus: contextMerchant.isConnected || false, - email: contextMerchant.email || '', - merchantId: contextMerchant.id || '', - clientId: contextMerchant.clientId || '', - }; - } return { connectionStatus: merchant.isConnected || false, 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 index fa264a6ee..c765d8738 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js @@ -1,13 +1,13 @@ import { __ } from '@wordpress/i18n'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; -import ConnectionInfo, { - getDefaultConnectionStatusData, -} from '../../../ReusableComponents/ConnectionInfo'; +import { CommonHooks } from '../../../../data'; import TitleBadge, { TITLE_BADGE_NEGATIVE, TITLE_BADGE_POSITIVE, } from '../../../ReusableComponents/TitleBadge'; +import ConnectionInfo from '../../../ReusableComponents/ConnectionInfo'; const ConnectionStatus = () => { + const { merchant } = CommonHooks.useMerchantInfo(); return ( {
- { getDefaultConnectionStatusData().connectionStatus ? ( + { merchant.isConnected ? ( { ) }
- { getDefaultConnectionStatusData().connectionStatus && ( - + { merchant.isConnected && ( + ) }
From f004be97ffc0b38c4cb9d880f24b8b00a5cb1d40 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Mon, 13 Jan 2025 15:21:55 +0100 Subject: [PATCH 199/298] use component instead of function --- .../ReusableComponents/ConnectionInfo.js | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js index 0c90bf58f..201681a5f 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js @@ -5,35 +5,31 @@ const ConnectionInfo = () => { const { merchant } = CommonHooks.useMerchantInfo(); return ( -
- {renderStatusRow( - __('Merchant ID', 'woocommerce-paypal-payments'), - connectionData.merchantId - )} - {renderStatusRow( - __('Email address', 'woocommerce-paypal-payments'), - connectionData.email - )} - {renderStatusRow( - __('Client ID', 'woocommerce-paypal-payments'), - connectionData.clientId - )} -
- ); +
+ + + +
+ ); }; export default ConnectionInfo; - return { - connectionStatus: merchant.isConnected || false, - email: merchant.email || '', - merchantId: merchant.id || '', - clientId: merchant.clientId || '', - }; -}; - -const renderStatusRow = (label, value) => ( -
- {label} - {value} -
+const StatusRow = ( { label, value } ) => ( +
+ + { label } + + + { value } + +
); From 98ccecab194d17def353002b134fd8fa9a93487c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Mon, 13 Jan 2025 16:05:34 +0100 Subject: [PATCH 200/298] Remove tokens from Block Checkout page --- modules/ppcp-vaulting/src/VaultingModule.php | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 37074b41e..673543dc7 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -136,6 +136,34 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule 3 ); + // Remove tokens from Block checkout page. + add_filter( + 'woocommerce_saved_payment_methods_list', + function( $methods ) { + + // Found no other way to manipulate the data only on that place + $exception = new \Exception(); + $trace = $exception->getTrace(); + $found = false; + foreach ( $trace as $value ) { + if ( $value['function'] === 'hydrate_customer_payment_methods' && $value['class'] === \Automattic\WooCommerce\Blocks\BlockTypes\Checkout::class ) { + $found = true; + break; + } + } + + if ( ! $found ) { + return $methods; + } + + unset( $methods['paypal'] ); + unset( $methods['venmo'] ); + unset( $methods['applepay'] ); + + return $methods; + } + ); + add_filter( 'woocommerce_payment_methods_list_item', /** From 11ec10a6d63885c8e1bfe8ef1d443a37c4ef9815 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 16:16:03 +0100 Subject: [PATCH 201/298] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20Redux=20store=20template=20with=20instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/_template/README.md | 45 ++++++++++++ .../js/data/_template/action-types.js | 18 +++++ .../resources/js/data/_template/actions.js | 71 +++++++++++++++++++ .../resources/js/data/_template/constants.js | 28 ++++++++ .../resources/js/data/_template/controls.js | 23 ++++++ .../resources/js/data/_template/hooks.js | 71 +++++++++++++++++++ .../resources/js/data/_template/index.js | 24 +++++++ .../resources/js/data/_template/reducer.js | 56 +++++++++++++++ .../resources/js/data/_template/resolvers.js | 37 ++++++++++ .../resources/js/data/_template/selectors.js | 21 ++++++ 10 files changed, 394 insertions(+) create mode 100644 modules/ppcp-settings/resources/js/data/_template/README.md create mode 100644 modules/ppcp-settings/resources/js/data/_template/action-types.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/actions.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/constants.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/controls.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/hooks.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/index.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/reducer.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/resolvers.js create mode 100644 modules/ppcp-settings/resources/js/data/_template/selectors.js diff --git a/modules/ppcp-settings/resources/js/data/_template/README.md b/modules/ppcp-settings/resources/js/data/_template/README.md new file mode 100644 index 000000000..b97f6ca4c --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/README.md @@ -0,0 +1,45 @@ +# Store template + +This template contains all files for a Redux store. + +## New Store: Redux integration + +1. Copy this folder, give it a correct name. +2. Check each file for `` placeholders and `TODO` remarks. +3. Edit the main store-index file and add the relevant store integration there. +4. Check the debug-module, and add relevant debug code. + - Register the store in the `reset()` method. + +--- + +Main store-index: +`modules/ppcp-settings/resources/js/data/index.js` + +Sample store integration: +```js +import * as YourStore from './yourStore'; +// ... +YourStore.initStore(); +// ... +export const YourStoreHooks = YourStore.hooks; +// ... +export const YourStoreName = YourStore.STORE_NAME; +// ... +addDebugTools( window.ppcpSettings, [ ..., YourStoreName ] ); +``` + +--- + +### New Store: PHP integration + +1. Create the **REST endpoint** for hydrating and persisting data. + - `modules/ppcp-settings/src/Endpoint/YourStoreRestEndpoint.php` + - Extend from base class `RestEndpoint` +2. Create the **data model** class to manage the DB interaction. + - `modules/ppcp-settings/src/Data/YourStoreSettings.php` + - Extend from base class `AbstractDataModel` +3. Create relevant **DI services** for both files. + - `modules/ppcp-settings/services.php` +4. Register the REST endpoint in the **service module**. + - `modules/ppcp-settings/src/SettingsModule.php` + - Find the action `rest_api_init` diff --git a/modules/ppcp-settings/resources/js/data/_template/action-types.js b/modules/ppcp-settings/resources/js/data/_template/action-types.js new file mode 100644 index 000000000..6341087d6 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/action-types.js @@ -0,0 +1,18 @@ +/** + * Action Types: Define unique identifiers for actions across all store modules. + * + * @file + */ + +export default { + // Transient data. + SET_TRANSIENT: ':SET_TRANSIENT', + + // Persistent data. + SET_PERSISTENT: ':SET_PERSISTENT', + RESET: ':RESET', + HYDRATE: ':HYDRATE', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: ':DO_PERSIST_DATA', +}; diff --git a/modules/ppcp-settings/resources/js/data/_template/actions.js b/modules/ppcp-settings/resources/js/data/_template/actions.js new file mode 100644 index 000000000..7360424f4 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/actions.js @@ -0,0 +1,71 @@ +/** + * Action Creators: Define functions to create action objects. + * + * These functions update state or trigger side effects (e.g., async operations). + * Actions are categorized as Transient, Persistent, or Side effect. + * + * @file + */ + +import { select } from '@wordpress/data'; + +import ACTION_TYPES from './action-types'; +import { STORE_NAME } from './constants'; + +/** + * @typedef {Object} Action An action object that is handled by a reducer or control. + * @property {string} type - The action type. + * @property {Object?} payload - Optional payload for the action. + */ + +/** + * Special. Resets all values in the store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { type: ACTION_TYPES.RESET } ); + +/** + * Persistent. Set the full store details during app initialization. + * + * @param {{data: {}, flags?: {}}} payload + * @return {Action} The action. + */ +export const hydrate = ( payload ) => ( { + type: ACTION_TYPES.HYDRATE, + payload, +} ); + +/** + * Transient. Marks the store as "ready", i.e., fully initialized. + * + * @param {boolean} isReady + * @return {Action} The action. + */ +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); + +/** + * Persistent. Sets a sample value. + * TODO: Replace with a real action/property. + * + * @param {string} value + * @return {Action} The action. + */ +export const setSampleValue = ( value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { sampleValue: value }, +} ); + +/** + * Side effect. Triggers the persistence of store data to the server. + * + * @return {Action} The action. + */ +export const persist = function* () { + const data = yield select( STORE_NAME ).persistentData(); + + yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; +}; diff --git a/modules/ppcp-settings/resources/js/data/_template/constants.js b/modules/ppcp-settings/resources/js/data/_template/constants.js new file mode 100644 index 000000000..9caae616f --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/constants.js @@ -0,0 +1,28 @@ +/** + * Name of the Redux store module. + * + * Used by: Reducer, Selector, Index + * + * @type {string} + */ +export const STORE_NAME = 'wc/paypal/'; + +/** + * REST path to hydrate data of this module by loading data from the WP DB. + * + * Used by: Resolvers + * See: .php + * + * @type {string} + */ +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/'; + +/** + * REST path to persist data of this module to the WP DB. + * + * Used by: Controls + * See: .php + * + * @type {string} + */ +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/'; diff --git a/modules/ppcp-settings/resources/js/data/_template/controls.js b/modules/ppcp-settings/resources/js/data/_template/controls.js new file mode 100644 index 000000000..9295b62bc --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/controls.js @@ -0,0 +1,23 @@ +/** + * Controls: Implement side effects, typically asynchronous operations. + * + * Controls use ACTION_TYPES keys as identifiers. + * They are triggered by corresponding actions and handle external interactions. + * + * @file + */ + +import apiFetch from '@wordpress/api-fetch'; + +import { REST_PERSIST_PATH } from './constants'; +import ACTION_TYPES from './action-types'; + +export const controls = { + async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { + return await apiFetch( { + path: REST_PERSIST_PATH, + method: 'POST', + data, + } ); + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/_template/hooks.js b/modules/ppcp-settings/resources/js/data/_template/hooks.js new file mode 100644 index 000000000..e1cf3852e --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/hooks.js @@ -0,0 +1,71 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * + * @file + */ + +import { useSelect, useDispatch } from '@wordpress/data'; + +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); + +const useHooks = () => { + const { + persist, + + // TODO: Replace with real property. + setSampleValue, + } = useDispatch( STORE_NAME ); + + // Read-only flags and derived state. + // Nothing here yet. + + // Transient accessors. + const isReady = useTransient( 'isReady' ); + + // Persistent accessors. + // TODO: Replace with real property. + const sampleValue = usePersistent( 'sampleValue' ); + + const savePersistent = async ( setter, value ) => { + setter( value ); + await persist(); + }; + + return { + isReady, + sampleValue, + setSampleValue: ( value ) => { + return savePersistent( setSampleValue, value ); + }, + }; +}; + +export const useState = () => { + const { isReady } = useHooks(); + return isReady; +}; + +// TODO: Replace with real hook. +export const useSampleValue = () => { + const { sampleValue, setSampleValue } = useHooks(); + + return { + sampleValue, + setSampleValue, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/_template/index.js b/modules/ppcp-settings/resources/js/data/_template/index.js new file mode 100644 index 000000000..28c162f98 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/index.js @@ -0,0 +1,24 @@ +import { createReduxStore, register } from '@wordpress/data'; +import { controls as wpControls } from '@wordpress/data-controls'; + +import { STORE_NAME } from './constants'; +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as hooks from './hooks'; +import { resolvers } from './resolvers'; +import { controls } from './controls'; + +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/_template/reducer.js b/modules/ppcp-settings/resources/js/data/_template/reducer.js new file mode 100644 index 000000000..a858b719a --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/reducer.js @@ -0,0 +1,56 @@ +/** + * Reducer: Defines store structure and state updates for this module. + * + * Manages both transient (temporary) and persistent (saved) state. + * The initial state must define all properties, as dynamic additions are not supported. + * + * @file + */ + +import { createReducer, createSetters } from '../utils'; +import ACTION_TYPES from './action-types'; + +// Store structure. + +// Transient: Values that are _not_ saved to the DB (like app lifecycle-flags). +const defaultTransient = Object.freeze( { + isReady: false, +} ); + +// Persistent: Values that are loaded from the DB. +const defaultPersistent = Object.freeze( { + // TODO: Add real DB properties here. + sampleValue: 'foo', +} ); + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +const reducer = createReducer( defaultTransient, defaultPersistent, { + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => + setTransient( state, payload ), + + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => + setPersistent( state, payload ), + + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.isReady = true; + + return cleanState; + }, + + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => + setPersistent( state, payload.data ), +} ); + +export default reducer; diff --git a/modules/ppcp-settings/resources/js/data/_template/resolvers.js b/modules/ppcp-settings/resources/js/data/_template/resolvers.js new file mode 100644 index 000000000..84ff1766a --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/resolvers.js @@ -0,0 +1,37 @@ +/** + * Resolvers: Handle asynchronous data fetching for the store. + * + * These functions update store state with data from external sources. + * Each resolver corresponds to a specific selector (selector with same name must exist). + * Resolvers are called automatically when selectors request unavailable data. + * + * @file + */ + +import { dispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { apiFetch } from '@wordpress/data-controls'; + +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; + +export const resolvers = { + /** + * Retrieve settings from the site's REST API. + */ + *persistentData() { + try { + const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); + + yield dispatch( STORE_NAME ).hydrate( result ); + yield dispatch( STORE_NAME ).setIsReady( true ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + // TODO: Add the module name to the error message. + __( + 'Error retrieving details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/_template/selectors.js b/modules/ppcp-settings/resources/js/data/_template/selectors.js new file mode 100644 index 000000000..14334fcf3 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/_template/selectors.js @@ -0,0 +1,21 @@ +/** + * Selectors: Extract specific pieces of state from the store. + * + * These functions provide a consistent interface for accessing store data. + * They allow components to retrieve data without knowing the store structure. + * + * @file + */ + +const EMPTY_OBJ = Object.freeze( {} ); + +const getState = ( state ) => state || EMPTY_OBJ; + +export const persistentData = ( state ) => { + return getState( state ).data || EMPTY_OBJ; +}; + +export const transientData = ( state ) => { + const { data, ...transientState } = getState( state ); + return transientState || EMPTY_OBJ; +}; From 5b192f3cc70c91cd21bea9ddcd8fe53c4ae6cc98 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 16:27:53 +0100 Subject: [PATCH 202/298] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20Redux=20templat?= =?UTF-8?q?e=20folder=20to=20`=5Fexample`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/{_template => _example}/README.md | 0 .../resources/js/data/{_template => _example}/action-types.js | 0 .../resources/js/data/{_template => _example}/actions.js | 0 .../resources/js/data/{_template => _example}/constants.js | 0 .../resources/js/data/{_template => _example}/controls.js | 0 .../resources/js/data/{_template => _example}/hooks.js | 0 .../resources/js/data/{_template => _example}/index.js | 0 .../resources/js/data/{_template => _example}/reducer.js | 0 .../resources/js/data/{_template => _example}/resolvers.js | 0 .../resources/js/data/{_template => _example}/selectors.js | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename modules/ppcp-settings/resources/js/data/{_template => _example}/README.md (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/action-types.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/actions.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/constants.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/controls.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/hooks.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/index.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/reducer.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/resolvers.js (100%) rename modules/ppcp-settings/resources/js/data/{_template => _example}/selectors.js (100%) diff --git a/modules/ppcp-settings/resources/js/data/_template/README.md b/modules/ppcp-settings/resources/js/data/_example/README.md similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/README.md rename to modules/ppcp-settings/resources/js/data/_example/README.md diff --git a/modules/ppcp-settings/resources/js/data/_template/action-types.js b/modules/ppcp-settings/resources/js/data/_example/action-types.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/action-types.js rename to modules/ppcp-settings/resources/js/data/_example/action-types.js diff --git a/modules/ppcp-settings/resources/js/data/_template/actions.js b/modules/ppcp-settings/resources/js/data/_example/actions.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/actions.js rename to modules/ppcp-settings/resources/js/data/_example/actions.js diff --git a/modules/ppcp-settings/resources/js/data/_template/constants.js b/modules/ppcp-settings/resources/js/data/_example/constants.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/constants.js rename to modules/ppcp-settings/resources/js/data/_example/constants.js diff --git a/modules/ppcp-settings/resources/js/data/_template/controls.js b/modules/ppcp-settings/resources/js/data/_example/controls.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/controls.js rename to modules/ppcp-settings/resources/js/data/_example/controls.js diff --git a/modules/ppcp-settings/resources/js/data/_template/hooks.js b/modules/ppcp-settings/resources/js/data/_example/hooks.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/hooks.js rename to modules/ppcp-settings/resources/js/data/_example/hooks.js diff --git a/modules/ppcp-settings/resources/js/data/_template/index.js b/modules/ppcp-settings/resources/js/data/_example/index.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/index.js rename to modules/ppcp-settings/resources/js/data/_example/index.js diff --git a/modules/ppcp-settings/resources/js/data/_template/reducer.js b/modules/ppcp-settings/resources/js/data/_example/reducer.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/reducer.js rename to modules/ppcp-settings/resources/js/data/_example/reducer.js diff --git a/modules/ppcp-settings/resources/js/data/_template/resolvers.js b/modules/ppcp-settings/resources/js/data/_example/resolvers.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/resolvers.js rename to modules/ppcp-settings/resources/js/data/_example/resolvers.js diff --git a/modules/ppcp-settings/resources/js/data/_template/selectors.js b/modules/ppcp-settings/resources/js/data/_example/selectors.js similarity index 100% rename from modules/ppcp-settings/resources/js/data/_template/selectors.js rename to modules/ppcp-settings/resources/js/data/_example/selectors.js From b77058e2aa063f4402aa1c6049ea3907f78166f0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 17:08:59 +0100 Subject: [PATCH 203/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20=E2=80=9C?= =?UTF-8?q?auto-persist=E2=80=9D=20from=20the=20sample=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/_example/hooks.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/_example/hooks.js b/modules/ppcp-settings/resources/js/data/_example/hooks.js index e1cf3852e..394fceb7e 100644 --- a/modules/ppcp-settings/resources/js/data/_example/hooks.js +++ b/modules/ppcp-settings/resources/js/data/_example/hooks.js @@ -41,23 +41,17 @@ const useHooks = () => { // TODO: Replace with real property. const sampleValue = usePersistent( 'sampleValue' ); - const savePersistent = async ( setter, value ) => { - setter( value ); - await persist(); - }; - return { + persist, isReady, sampleValue, - setSampleValue: ( value ) => { - return savePersistent( setSampleValue, value ); - }, + setSampleValue, }; }; export const useState = () => { - const { isReady } = useHooks(); - return isReady; + const { persist, isReady } = useHooks(); + return { persist, isReady }; }; // TODO: Replace with real hook. From 63f417d7d29a04ad4143d6c2acaff562d97ec053 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 17:06:05 +0100 Subject: [PATCH 204/298] =?UTF-8?q?=E2=9C=A8=20Brand=20new,=20empty=20?= =?UTF-8?q?=E2=80=9CStyling=E2=80=9D=20store=20in=20Redux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/index.js | 6 +- .../resources/js/data/styling/action-types.js | 18 +++ .../resources/js/data/styling/actions.js | 70 ++++++++++ .../resources/js/data/styling/constants.js | 28 ++++ .../resources/js/data/styling/controls.js | 23 ++++ .../resources/js/data/styling/hooks.js | 48 +++++++ .../resources/js/data/styling/index.js | 24 ++++ .../resources/js/data/styling/reducer.js | 55 ++++++++ .../resources/js/data/styling/resolvers.js | 36 +++++ .../resources/js/data/styling/selectors.js | 21 +++ modules/ppcp-settings/services.php | 8 ++ .../src/Data/StylingSettings.php | 14 +- .../src/Endpoint/StylingRestEndpoint.php | 126 ++++++++++++++++++ modules/ppcp-settings/src/SettingsModule.php | 1 + 14 files changed, 471 insertions(+), 7 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/styling/action-types.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/actions.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/constants.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/controls.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/hooks.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/index.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/reducer.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/resolvers.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/selectors.js create mode 100644 modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 274aac790..e447ff770 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -1,16 +1,20 @@ import { addDebugTools } from './debug'; import * as Onboarding from './onboarding'; import * as Common from './common'; +import * as Styling from './styling'; Onboarding.initStore(); Common.initStore(); +Styling.initStore(); export const OnboardingHooks = Onboarding.hooks; export const CommonHooks = Common.hooks; +export const StylingHooks = Styling.hooks; export const OnboardingStoreName = Onboarding.STORE_NAME; export const CommonStoreName = Common.STORE_NAME; +export const StylingStoreName = Styling.STORE_NAME; export * from './constants'; -addDebugTools( window.ppcpSettings, [ Onboarding, Common ] ); +addDebugTools( window.ppcpSettings, [ Onboarding, Common, Styling ] ); diff --git a/modules/ppcp-settings/resources/js/data/styling/action-types.js b/modules/ppcp-settings/resources/js/data/styling/action-types.js new file mode 100644 index 000000000..d487b1f5f --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/action-types.js @@ -0,0 +1,18 @@ +/** + * Action Types: Define unique identifiers for actions across all store modules. + * + * @file + */ + +export default { + // Transient data. + SET_TRANSIENT: 'STYLE:SET_TRANSIENT', + + // Persistent data. + SET_PERSISTENT: 'STYLE:SET_PERSISTENT', + RESET: 'STYLE:RESET', + HYDRATE: 'STYLE:HYDRATE', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: 'STYLE:DO_PERSIST_DATA', +}; diff --git a/modules/ppcp-settings/resources/js/data/styling/actions.js b/modules/ppcp-settings/resources/js/data/styling/actions.js new file mode 100644 index 000000000..25cf6f04b --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/actions.js @@ -0,0 +1,70 @@ +/** + * Action Creators: Define functions to create action objects. + * + * These functions update state or trigger side effects (e.g., async operations). + * Actions are categorized as Transient, Persistent, or Side effect. + * + * @file + */ + +import { select } from '@wordpress/data'; + +import ACTION_TYPES from './action-types'; +import { STORE_NAME } from './constants'; + +/** + * @typedef {Object} Action An action object that is handled by a reducer or control. + * @property {string} type - The action type. + * @property {Object?} payload - Optional payload for the action. + */ + +/** + * Special. Resets all values in the store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { type: ACTION_TYPES.RESET } ); + +/** + * Persistent. Set the full store details during app initialization. + * + * @param {{data: {}, flags?: {}}} payload + * @return {Action} The action. + */ +export const hydrate = ( payload ) => ( { + type: ACTION_TYPES.HYDRATE, + payload, +} ); + +/** + * Transient. Marks the store as "ready", i.e., fully initialized. + * + * @param {boolean} isReady + * @return {Action} The action. + */ +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); + +/** + * Persistent. + * + * @param {string} shape + * @return {Action} The action. + */ +export const setShape = ( shape ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { shape }, +} ); + +/** + * Side effect. Triggers the persistence of store data to the server. + * + * @return {Action} The action. + */ +export const persist = function* () { + const data = yield select( STORE_NAME ).persistentData(); + + yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; +}; diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js new file mode 100644 index 000000000..db1082f33 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -0,0 +1,28 @@ +/** + * Name of the Redux store module. + * + * Used by: Reducer, Selector, Index + * + * @type {string} + */ +export const STORE_NAME = 'wc/paypal/style'; + +/** + * REST path to hydrate data of this module by loading data from the WP DB. + * + * Used by: Resolvers + * See: StylingRestEndpoint.php + * + * @type {string} + */ +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/styling'; + +/** + * REST path to persist data of this module to the WP DB. + * + * Used by: Controls + * See: StylingRestEndpoint.php + * + * @type {string} + */ +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/styling'; diff --git a/modules/ppcp-settings/resources/js/data/styling/controls.js b/modules/ppcp-settings/resources/js/data/styling/controls.js new file mode 100644 index 000000000..9295b62bc --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/controls.js @@ -0,0 +1,23 @@ +/** + * Controls: Implement side effects, typically asynchronous operations. + * + * Controls use ACTION_TYPES keys as identifiers. + * They are triggered by corresponding actions and handle external interactions. + * + * @file + */ + +import apiFetch from '@wordpress/api-fetch'; + +import { REST_PERSIST_PATH } from './constants'; +import ACTION_TYPES from './action-types'; + +export const controls = { + async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { + return await apiFetch( { + path: REST_PERSIST_PATH, + method: 'POST', + data, + } ); + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js new file mode 100644 index 000000000..de08f1124 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -0,0 +1,48 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * + * @file + */ + +import { useSelect, useDispatch } from '@wordpress/data'; + +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); + +const useHooks = () => { + const { persist, setShape } = useDispatch( STORE_NAME ); + + // Read-only flags and derived state. + + // Transient accessors. + const isReady = useTransient( 'isReady' ); + + // Persistent accessors. + const shape = usePersistent( 'shape' ); + + return { + persist, + isReady, + shape, + setShape, + }; +}; + +export const useState = () => { + const { persist, isReady } = useHooks(); + return { persist, isReady }; +}; diff --git a/modules/ppcp-settings/resources/js/data/styling/index.js b/modules/ppcp-settings/resources/js/data/styling/index.js new file mode 100644 index 000000000..28c162f98 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/index.js @@ -0,0 +1,24 @@ +import { createReduxStore, register } from '@wordpress/data'; +import { controls as wpControls } from '@wordpress/data-controls'; + +import { STORE_NAME } from './constants'; +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as hooks from './hooks'; +import { resolvers } from './resolvers'; +import { controls } from './controls'; + +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js new file mode 100644 index 000000000..9e505dd24 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -0,0 +1,55 @@ +/** + * Reducer: Defines store structure and state updates for this module. + * + * Manages both transient (temporary) and persistent (saved) state. + * The initial state must define all properties, as dynamic additions are not supported. + * + * @file + */ + +import { createReducer, createSetters } from '../utils'; +import ACTION_TYPES from './action-types'; + +// Store structure. + +// Transient: Values that are _not_ saved to the DB (like app lifecycle-flags). +const defaultTransient = Object.freeze( { + isReady: false, +} ); + +// Persistent: Values that are loaded from the DB. +const defaultPersistent = Object.freeze( { + shape: 'rect', +} ); + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +const reducer = createReducer( defaultTransient, defaultPersistent, { + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => + setTransient( state, payload ), + + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => + setPersistent( state, payload ), + + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.isReady = true; + + return cleanState; + }, + + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => + setPersistent( state, payload.data ), +} ); + +export default reducer; diff --git a/modules/ppcp-settings/resources/js/data/styling/resolvers.js b/modules/ppcp-settings/resources/js/data/styling/resolvers.js new file mode 100644 index 000000000..e59794746 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/resolvers.js @@ -0,0 +1,36 @@ +/** + * Resolvers: Handle asynchronous data fetching for the store. + * + * These functions update store state with data from external sources. + * Each resolver corresponds to a specific selector (selector with same name must exist). + * Resolvers are called automatically when selectors request unavailable data. + * + * @file + */ + +import { dispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { apiFetch } from '@wordpress/data-controls'; + +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; + +export const resolvers = { + /** + * Retrieve settings from the site's REST API. + */ + *persistentData() { + try { + const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); + + yield dispatch( STORE_NAME ).hydrate( result ); + yield dispatch( STORE_NAME ).setIsReady( true ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + __( + 'Error retrieving style-details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/styling/selectors.js b/modules/ppcp-settings/resources/js/data/styling/selectors.js new file mode 100644 index 000000000..14334fcf3 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/selectors.js @@ -0,0 +1,21 @@ +/** + * Selectors: Extract specific pieces of state from the store. + * + * These functions provide a consistent interface for accessing store data. + * They allow components to retrieve data without knowing the store structure. + * + * @file + */ + +const EMPTY_OBJ = Object.freeze( {} ); + +const getState = ( state ) => state || EMPTY_OBJ; + +export const persistentData = ( state ) => { + return getState( state ).data || EMPTY_OBJ; +}; + +export const transientData = ( state ) => { + const { data, ...transientState } = getState( state ); + return transientState || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 6fc9d67e3..50afb3fdb 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -24,6 +24,8 @@ use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -64,12 +66,18 @@ return array( $container->get( 'wcgateway.is-send-only-country' ) ); }, + 'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings { + return new StylingSettings(); + }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); }, 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); }, + 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint { + return new StylingRestEndpoint( $container->get( 'settings.data.styling' ) ); + }, 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint { return new RefreshFeatureStatusEndpoint( $container->get( 'wcgateway.settings' ), diff --git a/modules/ppcp-settings/src/Data/StylingSettings.php b/modules/ppcp-settings/src/Data/StylingSettings.php index 481625ca7..ee942693c 100644 --- a/modules/ppcp-settings/src/Data/StylingSettings.php +++ b/modules/ppcp-settings/src/Data/StylingSettings.php @@ -1,6 +1,6 @@ 'rect', + ); } } diff --git a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php new file mode 100644 index 000000000..97b1158f2 --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php @@ -0,0 +1,126 @@ + array( + 'js_name' => 'shape', + ), + ); + + /** + * Constructor. + * + * @param StylingSettings $settings The settings instance. + */ + public function __construct( StylingSettings $settings ) { + $this->settings = $settings; + } + + /** + * Configure REST API routes. + */ + public function register_routes() : void { + /** + * GET wc/v3/wc_paypal/styling + */ + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + + /** + * POST wc/v3/wc_paypal/styling + * { + * // Fields mentioned in $field_map[]['js_name'] + * } + */ + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + } + + /** + * Returns all styling details. + * + * @return WP_REST_Response The current styling details. + */ + public function get_details() : WP_REST_Response { + $js_data = $this->sanitize_for_javascript( + $this->settings->to_array(), + $this->field_map + ); + + return $this->return_success( + $js_data + ); + } + + /** + * Updates styling details based on the request. + * + * @param WP_REST_Request $request Full data about the request. + * + * @return WP_REST_Response The updated styling details. + */ + public function update_details( WP_REST_Request $request ) : WP_REST_Response { + $wp_data = $this->sanitize_for_wordpress( + $request->get_params(), + $this->field_map + ); + + $this->settings->from_array( $wp_data ); + $this->settings->save(); + + return $this->get_details(); + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 59f752545..5bc032dfb 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -180,6 +180,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { $endpoints = array( $container->get( 'settings.rest.onboarding' ), $container->get( 'settings.rest.common' ), + $container->get( 'settings.rest.styling' ), $container->get( 'settings.rest.connect_manual' ), $container->get( 'settings.rest.login_link' ), $container->get( 'settings.rest.webhooks' ), From 0b624eb8bc3f53ae47f81071d18775b8ff2ca3b3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 19:03:33 +0100 Subject: [PATCH 205/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20JS=20config?= =?UTF-8?q?uration=20for=20styling=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Screens/Overview/TabStyling.js | 20 +-- .../resources/js/data/constants.js | 17 ++- .../resources/js/data/onboarding/constants.js | 23 +++- .../js/data/settings/tab-styling-data.js | 64 ---------- .../resources/js/data/styling/constants.js | 114 ++++++++++++++++++ 5 files changed, 155 insertions(+), 83 deletions(-) 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 7ddd8be7a..ad931c844 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js @@ -7,11 +7,13 @@ import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { defaultLocationSettings, paymentMethodOptions, - colorOptions, - shapeOptions, - buttonLayoutOptions, - buttonLabelOptions, } from '../../../data/settings/tab-styling-data'; +import { + STYLING_LABELS, + STYLING_COLORS, + STYLING_LAYOUTS, + STYLING_SHAPES, +} from '../../../data'; const TabStyling = () => { const [ location, setLocation ] = useState( 'cart' ); @@ -52,7 +54,7 @@ const TabStyling = () => { }, [] ); - }, [] ); + }, [ locationSettings ] ); const updateButtonSettings = ( key, value ) => { setLocationSettings( { @@ -223,7 +225,7 @@ const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => { updateButtonStyle( 'layout', newValue ) } selected={ locationSettings.settings.style.layout } - options={ buttonLayoutOptions } + options={ Object.values( STYLING_LAYOUTS ) } /> ) @@ -242,7 +244,7 @@ const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => { updateButtonStyle( 'shape', newValue ) } selected={ locationSettings.settings.style.shape } - options={ shapeOptions } + options={ Object.values( STYLING_SHAPES ) } /> ); @@ -258,7 +260,7 @@ const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => { } value={ locationSettings.settings.style.label } label={ __( 'Button Label', 'woocommerce-paypal-payments' ) } - options={ buttonLabelOptions } + options={ Object.values( STYLING_LABELS ) } /> ); @@ -274,7 +276,7 @@ const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => { updateButtonStyle( 'color', newValue ) } value={ locationSettings.settings.style.color } - options={ colorOptions } + options={ Object.values( STYLING_COLORS ) } /> ); diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js index 5654ad476..fbc8e8e11 100644 --- a/modules/ppcp-settings/resources/js/data/constants.js +++ b/modules/ppcp-settings/resources/js/data/constants.js @@ -1,10 +1,9 @@ -export const BUSINESS_TYPES = { - CASUAL_SELLER: 'casual_seller', - BUSINESS: 'business', -}; +export { BUSINESS_TYPES, PRODUCT_TYPES } from './onboarding/constants'; -export const PRODUCT_TYPES = { - VIRTUAL: 'virtual', - PHYSICAL: 'physical', - SUBSCRIPTIONS: 'subscriptions', -}; +export { + STYLING_LOCATIONS, + STYLING_LABELS, + STYLING_COLORS, + STYLING_LAYOUTS, + STYLING_SHAPES, +} from './styling/constants'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js index 4b33c6701..7c35ee693 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -8,7 +8,7 @@ export const STORE_NAME = 'wc/paypal/onboarding'; /** - * 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 * See: OnboardingRestEndpoint.php @@ -26,3 +26,24 @@ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/onboarding'; * @type {string} */ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/onboarding'; + +/** + * Onboarding options for StepBusiness + * + * @type {Object} + */ +export const BUSINESS_TYPES = { + CASUAL_SELLER: 'casual_seller', + BUSINESS: 'business', +}; + +/** + * Onboarding options for StepProducts + * + * @type {Object} + */ +export const PRODUCT_TYPES = { + VIRTUAL: 'virtual', + PHYSICAL: 'physical', + SUBSCRIPTIONS: 'subscriptions', +}; 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 6bdb4f643..ae0636481 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 @@ -96,67 +96,3 @@ export const paymentMethodOptions = [ label: __( 'Apple Pay', 'woocommerce-paypal-payments' ), }, ]; - -export const buttonLabelOptions = [ - { - value: 'paypal', - label: __( 'PayPal', 'woocommerce-paypal-payments' ), - }, - { - value: 'checkout', - label: __( 'Checkout', 'woocommerce-paypal-payments' ), - }, - { - value: 'buynow', - label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ), - }, - { - value: 'pay', - label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ), - }, -]; - -export const colorOptions = [ - { - value: 'gold', - label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ), - }, - { - value: 'blue', - label: __( 'Blue', 'woocommerce-paypal-payments' ), - }, - { - value: 'silver', - label: __( 'Silver', 'woocommerce-paypal-payments' ), - }, - { - value: 'black', - label: __( 'Black', 'woocommerce-paypal-payments' ), - }, - { - value: 'white', - label: __( 'White', 'woocommerce-paypal-payments' ), - }, -]; - -export const buttonLayoutOptions = [ - { - label: __( 'Vertical', 'woocommerce-paypal-payments' ), - value: 'vertical', - }, - { - label: __( 'Horizontal', 'woocommerce-paypal-payments' ), - value: 'horizontal', - }, -]; - -export const shapeOptions = [ - { - value: 'pill', - label: __( 'Pill', 'woocommerce-paypal-payments' ), - }, - { - value: 'rect', - label: __( 'Rectangle', 'woocommerce-paypal-payments' ), - }, -]; diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js index db1082f33..e5e32efab 100644 --- a/modules/ppcp-settings/resources/js/data/styling/constants.js +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -1,3 +1,5 @@ +import { __ } from '@wordpress/i18n'; + /** * Name of the Redux store module. * @@ -26,3 +28,115 @@ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/styling'; * @type {string} */ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/styling'; + +export const STYLING_LOCATIONS = { + cart: { + value: 'cart', + label: __( 'Cart', 'woocommerce-paypal-payments' ), + description: __( + 'Customize the appearance of the PayPal smart buttons on the Cart page and select which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'classic-checkout': { + value: 'classic-checkout', + label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), + description: __( + 'Customize the appearance of the PayPal smart buttons on the Classic Checkout page and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'express-checkout': { + value: 'express-checkout', + label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), + description: __( + 'Customize the appearance of the PayPal smart buttons on the Express Checkout location and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'mini-cart': { + value: 'mini-cart', + label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), + description: __( + 'Customize the appearance of the PayPal smart buttons on the Mini Cart and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'product-page': { + value: 'product-page', + label: __( 'Product Page', 'woocommerce-paypal-payments' ), + description: __( + 'Customize the appearance of the PayPal smart buttons on the Product Page and choose which additional payment buttons to display in this location.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, +}; + +export const STYLING_LABELS = { + paypal: { + value: 'paypal', + label: __( 'PayPal', 'woocommerce-paypal-payments' ), + }, + checkout: { + value: 'checkout', + label: __( 'Checkout', 'woocommerce-paypal-payments' ), + }, + buynow: { + value: 'buynow', + label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ), + }, + pay: { + value: 'pay', + label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_COLORS = { + gold: { + value: 'gold', + label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ), + }, + blue: { + value: 'blue', + label: __( 'Blue', 'woocommerce-paypal-payments' ), + }, + silver: { + value: 'silver', + label: __( 'Silver', 'woocommerce-paypal-payments' ), + }, + black: { + value: 'black', + label: __( 'Black', 'woocommerce-paypal-payments' ), + }, + white: { + value: 'white', + label: __( 'White', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_LAYOUTS = { + vertical: { + value: 'vertical', + label: __( 'Vertical', 'woocommerce-paypal-payments' ), + }, + horizontal: { + value: 'horizontal', + label: __( 'Horizontal', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_SHAPES = { + pill: { + value: 'pill', + label: __( 'Pill', 'woocommerce-paypal-payments' ), + }, + rect: { + value: 'rect', + label: __( 'Rectangle', 'woocommerce-paypal-payments' ), + }, +}; From fdffbee0aba883d57990ad97bade50a7b6cdd221 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 13 Jan 2025 19:04:02 +0100 Subject: [PATCH 206/298] =?UTF-8?q?=E2=9C=A8=20New=20DTO=20to=20hold=20sty?= =?UTF-8?q?ling=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DTO/LocationStylingDTO.php | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 modules/ppcp-settings/src/DTO/LocationStylingDTO.php diff --git a/modules/ppcp-settings/src/DTO/LocationStylingDTO.php b/modules/ppcp-settings/src/DTO/LocationStylingDTO.php new file mode 100644 index 000000000..14e4471ea --- /dev/null +++ b/modules/ppcp-settings/src/DTO/LocationStylingDTO.php @@ -0,0 +1,85 @@ +location = $location; + $this->enabled = $enabled; + $this->methods = $methods; + $this->shape = $shape; + $this->label = $label; + $this->color = $color; + } +} From 5d13bef912138a734ece1a3561f7821bfa2ae87b Mon Sep 17 00:00:00 2001 From: Himad M Date: Tue, 14 Jan 2025 07:16:40 +0100 Subject: [PATCH 207/298] Fix fatal error in PayLaterConfigurator --- .../src/PayLaterConfiguratorModule.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php index 8e46bcf33..a73efef26 100644 --- a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php +++ b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php @@ -70,9 +70,15 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec $current_page_id = $c->get( 'wcgateway.current-ppcp-settings-page-id' ); $is_wc_settings_page = $c->get( 'wcgateway.is-wc-settings-page' ); $messaging_locations = $c->get( 'paylater-configurator.messaging-locations' ); - $onboarding_profile = $c->get( 'settings.data.onboarding' ); - self::add_paylater_update_notice( $messaging_locations, $is_wc_settings_page, $current_page_id, $onboarding_profile ); + if ( $c->has( 'settings.data.onboarding' ) && + $c->get( 'settings.data.onboarding' )->get_completed() === true ) { + self::add_paylater_update_notice( + $messaging_locations, + $is_wc_settings_page, + $current_page_id + ); + } $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); @@ -162,19 +168,13 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec * The notice appears on any PayPal-Settings page, except for the Pay-Later settings page, * when no Pay-Later messaging is used yet. * - * @param array $message_locations PayLater messaging locations. - * @param bool $is_settings_page Whether the current page is a WC settings page. - * @param string $current_page_id ID of current settings page tab. - * @param OnboardingProfile $onboarding_profile Onboarding profile. + * @param array $message_locations PayLater messaging locations. + * @param bool $is_settings_page Whether the current page is a WC settings page. + * @param string $current_page_id ID of current settings page tab. * * @return void */ - private static function add_paylater_update_notice( array $message_locations, bool $is_settings_page, string $current_page_id, OnboardingProfile $onboarding_profile ) : void { - // Don't display the notice if the user has not completed the onboarding process. - if ( $onboarding_profile->get_completed() !== true ) { - return; - } - + private static function add_paylater_update_notice( array $message_locations, bool $is_settings_page, string $current_page_id ) : void { // The message must be registered on any WC-Settings page, except for the Pay Later page. if ( ! $is_settings_page || Settings::PAY_LATER_TAB_ID === $current_page_id ) { return; From 4d8f418eae24e64e80b859b4b58b36093116bdf7 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 11:37:05 +0400 Subject: [PATCH 208/298] Do not add a "PayLater" tab if configurator is not eligible --- .../resources/js/Components/Screens/tabs.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/tabs.js b/modules/ppcp-settings/resources/js/Components/Screens/tabs.js index e828dab9f..9d743d754 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/tabs.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/tabs.js @@ -32,11 +32,13 @@ export const getSettingsTabs = () => { component: , } ); - tabs.push( { - name: 'pay-later-messaging', - title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ), - component: , - } ); + if ( window.ppcpSettings.isPayLaterConfiguratorAvailable ) { + tabs.push( { + name: 'pay-later-messaging', + title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ), + component: , + } ); + } return tabs; }; From 16146be506f8e934455cc629ed423d8359aa4525 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 11:37:39 +0400 Subject: [PATCH 209/298] Send the necessary data(PayLater config) to JS --- modules/ppcp-settings/src/SettingsModule.php | 44 ++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 59f752545..1855856a5 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -9,6 +9,8 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; +use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig; +use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; @@ -17,6 +19,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; /** * Class SettingsModule @@ -146,19 +149,46 @@ class SettingsModule implements ServiceModule, ExecutableModule { $style_asset_file['version'] ); + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + wp_enqueue_style( 'ppcp-admin-settings' ); wp_enqueue_style( 'ppcp-admin-settings-font', 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', array(), $style_asset_file['version'] ); + + $is_pay_later_configurator_available = $container->get( 'paylater-configurator.is-available' ); + + $script_data = array( + 'assets' => array( + 'imagesUrl' => $module_url . '/images/', + ), + 'wcPaymentsTabUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout' ), + 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'isPayLaterConfiguratorAvailable' => $is_pay_later_configurator_available, + ); + + if ( $is_pay_later_configurator_available ) { + wp_enqueue_script( + 'ppcp-paylater-configurator-lib', + 'https://www.paypalobjects.com/merchant-library/merchant-configurator.js', + array(), + $script_asset_file['version'], + true + ); + + $script_data['PcpPayLaterConfigurator'] = array( + 'config' => array(), + 'merchantClientId' => $settings->get( 'client_id' ), + 'merchantClientId' => 'BAAIfEA1GuGq3IB27pvY0vRNkeL0ZxB2NczyPIl4cqA7OFYRUQsObu77A-1z-s4ZjZxvtQJ4R4Jz0nLYTE', + 'partnerClientId' => $container->get( 'api.partner_merchant_id' ), + 'bnCode' => PPCP_PAYPAL_BN_CODE, + ); + } + wp_localize_script( 'ppcp-admin-settings', 'ppcpSettings', - array( - 'assets' => array( - 'imagesUrl' => $module_url . '/images/', - ), - 'wcPaymentsTabUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout' ), - 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, - ) + $script_data ); } ); From b84287337080ea2ddc1e4eb0f28a50ce1b6b5bb4 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 11:37:57 +0400 Subject: [PATCH 210/298] Create the configurator --- .../Screens/Overview/TabPayLaterMessaging.js | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js index b388a6bee..a3a3083c9 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js @@ -1,5 +1,50 @@ +import React, { useEffect } from 'react'; + const TabPayLaterMessaging = () => { - return
; + const config = {}; // Replace with the appropriate/saved configuration. + const PcpPayLaterConfigurator = + window.ppcpSettings?.PcpPayLaterConfigurator; + + useEffect( () => { + if ( window.merchantConfigurators && PcpPayLaterConfigurator ) { + window.merchantConfigurators.Messaging( { + config, + merchantClientId: PcpPayLaterConfigurator.merchantClientId, + partnerClientId: PcpPayLaterConfigurator.partnerClientId, + partnerName: 'WooCommerce', + bnCode: PcpPayLaterConfigurator.bnCode, + placements: [ + 'cart', + 'checkout', + 'product', + 'shop', + 'home', + 'custom_placement', + ], + styleOverrides: { + button: 'ppcp-r-paylater-configurator__publish-button', + header: 'ppcp-r-paylater-configurator__header', + subheader: 'ppcp-r-paylater-configurator__subheader', + }, + onSave: ( data ) => { + /* + TODO: + - The saving will be handled in a separate PR. + - One option could be: + - When saving the settings, programmatically click on the configurator's + "Save Changes" button and send the request to PHP. + */ + }, + } ); + } + }, [ PcpPayLaterConfigurator ] ); + + return ( +
+ ); }; export default TabPayLaterMessaging; From 7483fd63c9588668f6a4c91fdb41e290794858b2 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 11:38:25 +0400 Subject: [PATCH 211/298] Override the css to match the "styling" tab styles --- .../overview/_tab-paylater-configurator.scss | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 modules/ppcp-settings/resources/css/components/screens/overview/_tab-paylater-configurator.scss diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-paylater-configurator.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-paylater-configurator.scss new file mode 100644 index 000000000..ae49bed82 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-paylater-configurator.scss @@ -0,0 +1,89 @@ +.ppcp-r-paylater-configurator { + display: flex; + border: 1px solid $color-gray-200; + border-radius: 8px; + overflow: hidden; + + .css-1snxoyf.eolpigi0 { + margin: 0; + } + + #configurator-eligibleContainer.css-4nclxm.e1vy3g880 { + width: 100%; + max-width: 100%; + padding: 48px 0px 48px 48px; + + #configurator-controlPanelContainer.css-5urmrq.e1vy3g880 { + width: 374px; + padding-right: 48px; + } + + #configurator-previewSectionContainer.css-vojyxx.e1vy3g880 { + width: calc(100% - 374px); + + .css-7xkxom, .css-8tvj6u { + height: auto; + } + + .css-10nkerk.ej6n7t60 { + align-items: flex-start; + } + + .css-1sgwra0-svg-size_sm { + height: 1.2rem; + width: 1.2rem; + } + + .css-1vc34jy-handler { + height: 1.6rem; + width: 1.6rem; + } + + .css-8vwtr6-state { + height: 1.6rem; + } + } + } + + &__subheader, #configurator-controlPanelSubHeader { + @include font(13, 20, 400); + color: $color-gray-800; + margin: 0 0 18px 0; + } + + .css-1caaugt-links_base-text_body_strong, .css-dpyjrq-text_body { + @include font(13, 20, 400); + } + + &__header, #configurator-controlPanelHeader, #configurator-previewSectionSubHeaderText.css-14ujlqd-text_body, .css-16jt5za-text_body { + @include font(14, 20, 600); + color: $color-gray-800; + margin: 0 0 8px 0; + display: block; + } + + .css-1yo2lxy-text_body_strong { + @include font(13, 16, 600); + color: $color-black; + margin: 0; + text-transform: none; + } + + .css-rok10q { + margin-top: 0; + } + + &__publish-button { + display: none; + } + + .css-1oxdnb3-dropdown_menu_button-text_field_value_sm-active, .css-1wvwydd-dropdown_menu_button-text_field_value_sm-active-active, .css-16jt5za-text_body { + font-size: 13px; + line-height: 1.5384615385; + font-weight: 400; + } + + .css-udzaps { + padding: 0px; + } +} From e5fe9053d6e8d57af168ff79be8d70212d9fd913 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 11:38:46 +0400 Subject: [PATCH 212/298] import the configurator css --- modules/ppcp-settings/resources/css/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 56fe55a62..dc698aec0 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -23,6 +23,7 @@ @import './components/screens/onboarding'; @import './components/screens/settings'; @import './components/screens/overview/tab-styling'; + @import './components/screens/overview/tab-paylater-configurator'; @import './components/app'; } From 42c9c32b45e8286dc86b191e8cc626e72aade919 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 11:39:24 +0400 Subject: [PATCH 213/298] import the leftover hardcode --- modules/ppcp-settings/src/SettingsModule.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 1855856a5..4f5842ce0 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -179,7 +179,6 @@ class SettingsModule implements ServiceModule, ExecutableModule { $script_data['PcpPayLaterConfigurator'] = array( 'config' => array(), 'merchantClientId' => $settings->get( 'client_id' ), - 'merchantClientId' => 'BAAIfEA1GuGq3IB27pvY0vRNkeL0ZxB2NczyPIl4cqA7OFYRUQsObu77A-1z-s4ZjZxvtQJ4R4Jz0nLYTE', 'partnerClientId' => $container->get( 'api.partner_merchant_id' ), 'bnCode' => PPCP_PAYPAL_BN_CODE, ); From e2ce69fd0b28498ec192bb6a12a5b588c9e6211b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Tue, 14 Jan 2025 08:43:34 +0100 Subject: [PATCH 214/298] use existing code to remove tokens --- modules/ppcp-vaulting/src/VaultingModule.php | 30 +------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 673543dc7..c6d52c12d 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -124,7 +124,7 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule && ! $is_post // Don't check on POST so we have all payment methods on form submissions. ) { foreach ( $tokens as $index => $token ) { - if ( $token instanceof PaymentTokenApplePay ) { + if ( $token instanceof PaymentTokenApplePay || $token instanceof PaymentTokenPayPal || $token instanceof PaymentTokenVenmo ) { unset( $tokens[ $index ] ); } } @@ -136,34 +136,6 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule 3 ); - // Remove tokens from Block checkout page. - add_filter( - 'woocommerce_saved_payment_methods_list', - function( $methods ) { - - // Found no other way to manipulate the data only on that place - $exception = new \Exception(); - $trace = $exception->getTrace(); - $found = false; - foreach ( $trace as $value ) { - if ( $value['function'] === 'hydrate_customer_payment_methods' && $value['class'] === \Automattic\WooCommerce\Blocks\BlockTypes\Checkout::class ) { - $found = true; - break; - } - } - - if ( ! $found ) { - return $methods; - } - - unset( $methods['paypal'] ); - unset( $methods['venmo'] ); - unset( $methods['applepay'] ); - - return $methods; - } - ); - add_filter( 'woocommerce_payment_methods_list_item', /** From bcbcdab08b024934b884e6697aaeb1a7e40172cd Mon Sep 17 00:00:00 2001 From: Himad M Date: Tue, 14 Jan 2025 10:06:37 +0100 Subject: [PATCH 215/298] Revert PayLaterConfigurator notice changes --- .../src/PayLaterConfiguratorModule.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php index a73efef26..71f33e1da 100644 --- a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php +++ b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php @@ -9,11 +9,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\PayLaterConfigurator; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig; use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig; use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory; -use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -71,14 +69,11 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec $is_wc_settings_page = $c->get( 'wcgateway.is-wc-settings-page' ); $messaging_locations = $c->get( 'paylater-configurator.messaging-locations' ); - if ( $c->has( 'settings.data.onboarding' ) && - $c->get( 'settings.data.onboarding' )->get_completed() === true ) { - self::add_paylater_update_notice( - $messaging_locations, - $is_wc_settings_page, - $current_page_id - ); - } + self::add_paylater_update_notice( + $messaging_locations, + $is_wc_settings_page, + $current_page_id + ); $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); From 98a626dfa355cafe6332c65de854c32071fe5fdc Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 10:28:10 +0100 Subject: [PATCH 216/298] =?UTF-8?q?=F0=9F=9A=A7=20Sample=20reducer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/reducer.js | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index 9e505dd24..3ac82ab6d 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -19,7 +19,41 @@ const defaultTransient = Object.freeze( { // Persistent: Values that are loaded from the DB. const defaultPersistent = Object.freeze( { - shape: 'rect', + cart: { + enabled: true, + methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], + shape: 'rect', + label: 'Pay', + color: 'gold', + }, + 'classic-checkout': { + enabled: true, + methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], + shape: 'rect', + label: 'Checkout', + color: 'gold', + }, + 'express-checkout': { + enabled: true, + methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], + shape: 'rect', + label: 'Checkout', + color: 'gold', + }, + 'mini-cart': { + enabled: true, + methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], + shape: 'rect', + label: 'Pay', + color: 'gold', + }, + product: { + enabled: true, + methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], + shape: 'rect', + label: 'Buy', + color: 'gold', + }, } ); // Reducer logic. From c633dd51ea43d82ee1340fd6ddb4ede41a415c20 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 14 Jan 2025 13:46:57 +0400 Subject: [PATCH 217/298] Remove unnecessary uses --- modules/ppcp-settings/src/SettingsModule.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 4f5842ce0..2d9356ab7 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -9,8 +9,6 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; -use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig; -use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; From 9d494a4a0c4261db2a70cfeed984248c51c0b1a7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 11:29:50 +0100 Subject: [PATCH 218/298] =?UTF-8?q?=F0=9F=8C=90=20Use=20new=20wording=20on?= =?UTF-8?q?ly=20for=20US=20stores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WelcomeDocs/AcdcFlow.js | 17 ++++++----------- .../WelcomeDocs/BcdcFlow.js | 8 +++----- .../Screens/Onboarding/StepPaymentMethods.js | 19 +++++++++++++------ 3 files changed, 22 insertions(+), 22 deletions(-) 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 87e6b6861..87488ed74 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js @@ -53,9 +53,7 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => { imageBadge={ [ 'icon-payment-method-paypal-small.svg', ] } - textBadge={ - - } + textBadge={ } description={ sprintf( // translators: %s: Link to PayPal business fees guide __( @@ -100,8 +98,7 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => { ) } titleType={ BADGE_BOX_TITLE_BIG } description={ __( - 'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more.\n' + - 'Note: Additional application required for more methods', + 'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods', 'woocommerce-paypal-payments' ) } /> @@ -176,13 +173,12 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
@@ -251,13 +247,12 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
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 7fa58bca4..6856e7caf 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js @@ -97,8 +97,7 @@ const BcdcFlow = ( { isPayLater, storeCountry } ) => { ) } titleType={ BADGE_BOX_TITLE_BIG } description={ __( - 'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more.\n' + - 'Note: Additional application required for more methods', + 'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods', 'woocommerce-paypal-payments' ) } /> @@ -159,13 +158,12 @@ const BcdcFlow = ( { isPayLater, storeCountry } ) => { 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 b6badf36c..5eeb51c5f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js @@ -17,14 +17,21 @@ const StepPaymentMethods = ( {} ) => { const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + let screenTitle = __( + 'Add optional payment methods to your Checkout', + 'woocommerce-paypal-payments' + ); + + if ( 'US' === storeCountry ) { + screenTitle = __( + 'Add Expanded Checkout for More Ways to Pay', + 'woocommerce-paypal-payments' + ); + } + return (
- +
Date: Tue, 14 Jan 2025 12:04:20 +0100 Subject: [PATCH 219/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Slightly=20improve?= =?UTF-8?q?=20the=20BadgeBox=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/ReusableComponents/BadgeBox.js | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js index 24dc36134..337e5626c 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BadgeBox.js @@ -1,40 +1,62 @@ import data from '../../utils/data'; -const BadgeBox = ( props ) => { - const titleSize = - props.titleType && props.titleType === BADGE_BOX_TITLE_BIG - ? BADGE_BOX_TITLE_BIG - : BADGE_BOX_TITLE_SMALL; +const ImageBadge = ( { images } ) => { + if ( ! images || ! images.length ) { + return null; + } + + return ( + + + { images.map( ( badge ) => data().getImage( badge ) ) } + + + ); +}; + +// If `children` is not empty, it's output and wrapped in spaces. +const BadgeContent = ( { children } ) => { + if ( ! children ) { + return null; + } + return <> { children } ; +}; + +const BadgeBox = ( { + title, + textBadge, + imageBadge = [], + titleType = BADGE_BOX_TITLE_BIG, + description = '', +} ) => { + let titleSize = BADGE_BOX_TITLE_SMALL; + if ( BADGE_BOX_TITLE_BIG === titleType ) { + titleSize = BADGE_BOX_TITLE_BIG; + } const titleTextClassName = 'ppcp-r-badge-box__title-text ' + `ppcp-r-badge-box__title-text--${ titleSize }`; const titleBaseClassName = 'ppcp-r-badge-box__title'; - const titleClassName = props.imageBadge + const titleClassName = imageBadge.length ? `${ titleBaseClassName } ppcp-r-badge-box__title--has-image-badge` : titleBaseClassName; + return (
- { props.title } + { title } - { props.imageBadge && ( - - { props.imageBadge.map( ( badge ) => - data().getImage( badge ) - ) } - - ) } - - { props.textBadge } + + { textBadge }
- { props?.description && ( + { description && (

) } From ed66f2bc05f423b541ac4dde57ce32285c2b4f90 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 12:36:03 +0100 Subject: [PATCH 220/298] =?UTF-8?q?=F0=9F=90=9B=20Fix=20incorrect=20compon?= =?UTF-8?q?ent=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/Screens/SendOnlyMessage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js b/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js index 56d780250..676b691eb 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/SendOnlyMessage.js @@ -2,7 +2,7 @@ import { __, sprintf } from '@wordpress/i18n'; import Container from '../ReusableComponents/Container'; import SettingsCard from '../ReusableComponents/SettingsCard'; -import SettingsNavigation from './SettingsNavigation'; +import SettingsNavigation from './Settings/Components/Navigation'; const SendOnlyMessage = () => { const settingsPageUrl = '/wp-admin/admin.php?page=wc-settings'; @@ -38,7 +38,7 @@ const SendOnlyMessage = () => { __html: sprintf( /* translators: 1: URL to the WooCommerce store location settings */ __( - 'To activate PayPal, please update your WooCommerce store location to a supported region and connect a PayPal account eligible for receiving payments.', + 'To activate PayPal, please update your WooCommerce store location to a supported region and connect a PayPal account eligible for receiving payments.', 'woocommerce-paypal-payments' ), settingsPageUrl From ffc8dc8d6b3d0a288a4faa24433fb20f4418e859 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 12:36:34 +0100 Subject: [PATCH 221/298] =?UTF-8?q?=F0=9F=92=84=20Remove=20the=20gap=20at?= =?UTF-8?q?=20the=20top=20of=20the=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/ReusableComponents/TopNavigation.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TopNavigation.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TopNavigation.js index 60bd19641..ee3ca7aaa 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TopNavigation.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TopNavigation.js @@ -1,4 +1,4 @@ -import { useCallback } from '@wordpress/element'; +import { useCallback, useLayoutEffect } from '@wordpress/element'; import { Button, Icon } from '@wordpress/components'; import { chevronLeft } from '@wordpress/icons'; import classNames from 'classnames'; @@ -34,6 +34,11 @@ const TopNavigation = ( { } }, [ exitOnTitleClick, goToWooCommercePaymentsTab, onTitleClick ] ); + // Removes the excess padding at the top of the navigation bar. + useLayoutEffect( () => { + window.dispatchEvent( new Event( 'resize' ) ); + }, [] ); + return (
From ef57724040c8f173e2221d204ac92836649611b1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 13:12:37 +0100 Subject: [PATCH 222/298] =?UTF-8?q?=F0=9F=9A=9A=20Move=20the=20TabStyling?= =?UTF-8?q?=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/{Overview => Settings/Tabs}/TabStyling.js | 6 +++--- .../resources/js/Components/Screens/Settings/Tabs/index.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename modules/ppcp-settings/resources/js/Components/Screens/{Overview => Settings/Tabs}/TabStyling.js (98%) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js similarity index 98% rename from modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js index ad931c844..671e133bc 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js @@ -1,19 +1,19 @@ import { __, sprintf } from '@wordpress/i18n'; import { SelectControl, RadioControl } from '@wordpress/components'; -import { PayPalCheckboxGroup } from '../../ReusableComponents/Fields'; +import { PayPalCheckboxGroup } from '../../../ReusableComponents/Fields'; import { useState, useMemo, useEffect } from '@wordpress/element'; import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { defaultLocationSettings, paymentMethodOptions, -} from '../../../data/settings/tab-styling-data'; +} from '../../../../data/settings/tab-styling-data'; import { STYLING_LABELS, STYLING_COLORS, STYLING_LAYOUTS, STYLING_SHAPES, -} from '../../../data'; +} from '../../../../data'; const TabStyling = () => { const [ location, setLocation ] = useState( 'cart' ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js index fa19dc6e4..85d4cf5bd 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js @@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n'; import TabOverview from '../../Overview/TabOverview'; import TabPaymentMethods from '../../Overview/TabPaymentMethods'; import TabSettings from '../../Overview/TabSettings'; -import TabStyling from '../../Overview/TabStyling'; +import TabStyling from './TabStyling'; import TabPayLaterMessaging from '../../Overview/TabPayLaterMessaging'; /** From fa67abc8e4c048dbfdd127b8b2cc99c45a4a8cce Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 16:18:54 +0100 Subject: [PATCH 223/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20the=20bu?= =?UTF-8?q?tton-preview=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/PaymentButtonPreview.js | 28 +++++++++++++++++++ .../Screens/Settings/Tabs/TabStyling.js | 28 ++----------------- 2 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js new file mode 100644 index 000000000..0c43ba902 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js @@ -0,0 +1,28 @@ +import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js'; + +const PREVIEW_CLIENT_ID = 'test'; +const PREVIEW_MERCHANT_ID = 'QTQX5NP6N9WZU'; + +const PaymentButtonPreview = ( { + style, + components = [ 'buttons', 'googlepay' ], +} ) => { + return ( + + + Error + + + ); +}; + +export default PaymentButtonPreview; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js index 671e133bc..98019c93c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js @@ -2,7 +2,6 @@ import { __, sprintf } from '@wordpress/i18n'; import { SelectControl, RadioControl } from '@wordpress/components'; import { PayPalCheckboxGroup } from '../../../ReusableComponents/Fields'; import { useState, useMemo, useEffect } from '@wordpress/element'; -import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { defaultLocationSettings, @@ -14,6 +13,7 @@ import { STYLING_LAYOUTS, STYLING_SHAPES, } from '../../../../data'; +import PaymentButtonPreview from '../Components/PaymentButtonPreview'; const TabStyling = () => { const [ location, setLocation ] = useState( 'cart' ); @@ -127,8 +127,8 @@ const TabStyling = () => {
-
@@ -313,26 +313,4 @@ const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => { ); }; -const SectionButtonPreview = ( { locationSettings } ) => { - return ( - - - Error - - - ); -}; - export default TabStyling; From e9644ba02602af6d100c9ef200b98078e712c629 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 14 Jan 2025 16:19:18 +0100 Subject: [PATCH 224/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20the=20re?= =?UTF-8?q?ducer=20by=20using=20defined=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/constants.js | 5 +++ .../resources/js/data/styling/reducer.js | 31 ++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js index e5e32efab..357fe43a1 100644 --- a/modules/ppcp-settings/resources/js/data/styling/constants.js +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -33,6 +33,7 @@ export const STYLING_LOCATIONS = { cart: { value: 'cart', label: __( 'Cart', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. description: __( 'Customize the appearance of the PayPal smart buttons on the Cart page and select which additional payment buttons to display in this location.', 'wooocommerce-paypal-payments' @@ -42,6 +43,7 @@ export const STYLING_LOCATIONS = { 'classic-checkout': { value: 'classic-checkout', label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. description: __( 'Customize the appearance of the PayPal smart buttons on the Classic Checkout page and choose which additional payment buttons to display in this location.', 'wooocommerce-paypal-payments' @@ -51,6 +53,7 @@ export const STYLING_LOCATIONS = { 'express-checkout': { value: 'express-checkout', label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. description: __( 'Customize the appearance of the PayPal smart buttons on the Express Checkout location and choose which additional payment buttons to display in this location.', 'wooocommerce-paypal-payments' @@ -60,6 +63,7 @@ export const STYLING_LOCATIONS = { 'mini-cart': { value: 'mini-cart', label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), + // translators: %s is the URL to a documentation page. description: __( 'Customize the appearance of the PayPal smart buttons on the Mini Cart and choose which additional payment buttons to display in this location.', 'wooocommerce-paypal-payments' @@ -69,6 +73,7 @@ export const STYLING_LOCATIONS = { 'product-page': { value: 'product-page', label: __( 'Product Page', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. description: __( 'Customize the appearance of the PayPal smart buttons on the Product Page and choose which additional payment buttons to display in this location.', 'wooocommerce-paypal-payments' diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index 3ac82ab6d..b28c3f265 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -9,6 +9,7 @@ import { createReducer, createSetters } from '../utils'; import ACTION_TYPES from './action-types'; +import { STYLING_COLORS, STYLING_SHAPES } from './constants'; // Store structure. @@ -21,38 +22,38 @@ const defaultTransient = Object.freeze( { const defaultPersistent = Object.freeze( { cart: { enabled: true, - methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], - shape: 'rect', + methods: [], label: 'Pay', - color: 'gold', + shape: STYLING_SHAPES.rect.value, + color: STYLING_COLORS.gold.value, }, 'classic-checkout': { enabled: true, - methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], - shape: 'rect', + methods: [], label: 'Checkout', - color: 'gold', + shape: STYLING_SHAPES.rect.value, + color: STYLING_COLORS.gold.value, }, 'express-checkout': { enabled: true, - methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], - shape: 'rect', + methods: [], label: 'Checkout', - color: 'gold', + shape: STYLING_SHAPES.rect.value, + color: STYLING_COLORS.gold.value, }, 'mini-cart': { enabled: true, - methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], - shape: 'rect', + methods: [], label: 'Pay', - color: 'gold', + shape: STYLING_SHAPES.rect.value, + color: STYLING_COLORS.gold.value, }, product: { enabled: true, - methods: [ 'venmo', 'applepay', 'googlepay', 'credit card' ], - shape: 'rect', + methods: [], label: 'Buy', - color: 'gold', + shape: STYLING_SHAPES.rect.value, + color: STYLING_COLORS.gold.value, }, } ); From 47ae8ff670e084ab51da6000fa4b7ac8ca3aeb9b Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 14 Jan 2025 17:59:35 +0100 Subject: [PATCH 225/298] Add data store boilerplace and default data --- .../ppcp-settings/resources/js/data/debug.js | 12 +- .../ppcp-settings/resources/js/data/index.js | 6 +- .../resources/js/data/payment/README.md | 45 +++++ .../resources/js/data/payment/action-types.js | 18 ++ .../resources/js/data/payment/actions.js | 71 +++++++ .../resources/js/data/payment/constants.js | 28 +++ .../resources/js/data/payment/controls.js | 23 +++ .../resources/js/data/payment/hooks.js | 65 +++++++ .../resources/js/data/payment/index.js | 24 +++ .../resources/js/data/payment/reducer.js | 56 ++++++ .../resources/js/data/payment/resolvers.js | 37 ++++ .../resources/js/data/payment/selectors.js | 21 ++ modules/ppcp-settings/services.php | 8 + .../src/Data/PaymentSettings.php | 182 +++++++++++++++++- .../src/Endpoint/PaymentRestEndpoint.php | 132 +++++++++++++ modules/ppcp-settings/src/SettingsModule.php | 1 + 16 files changed, 723 insertions(+), 6 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/payment/README.md create mode 100644 modules/ppcp-settings/resources/js/data/payment/action-types.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/actions.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/constants.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/controls.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/hooks.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/index.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/reducer.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/resolvers.js create mode 100644 modules/ppcp-settings/resources/js/data/payment/selectors.js create mode 100644 modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index 6380c6d6a..56fc31993 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -1,4 +1,8 @@ -import { OnboardingStoreName, CommonStoreName } from './index'; +import { + OnboardingStoreName, + CommonStoreName, + PaymentStoreName, +} from './index'; export const addDebugTools = ( context, modules ) => { if ( ! context || ! context?.debug ) { @@ -33,7 +37,11 @@ export const addDebugTools = ( context, modules ) => { }; context.resetStore = () => { - const stores = [ OnboardingStoreName, CommonStoreName ]; + const stores = [ + OnboardingStoreName, + CommonStoreName, + PaymentStoreName, + ]; stores.forEach( ( storeName ) => { const store = wp.data.dispatch( storeName ); diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 274aac790..587ef036e 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -1,16 +1,20 @@ import { addDebugTools } from './debug'; import * as Onboarding from './onboarding'; import * as Common from './common'; +import * as Payment from './payment'; Onboarding.initStore(); Common.initStore(); +Payment.initStore(); export const OnboardingHooks = Onboarding.hooks; export const CommonHooks = Common.hooks; +export const PaymentHooks = Payment.hooks; export const OnboardingStoreName = Onboarding.STORE_NAME; export const CommonStoreName = Common.STORE_NAME; +export const PaymentStoreName = Payment.STORE_NAME; export * from './constants'; -addDebugTools( window.ppcpSettings, [ Onboarding, Common ] ); +addDebugTools( window.ppcpSettings, [ Onboarding, Common, Payment ] ); diff --git a/modules/ppcp-settings/resources/js/data/payment/README.md b/modules/ppcp-settings/resources/js/data/payment/README.md new file mode 100644 index 000000000..b97f6ca4c --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/README.md @@ -0,0 +1,45 @@ +# Store template + +This template contains all files for a Redux store. + +## New Store: Redux integration + +1. Copy this folder, give it a correct name. +2. Check each file for `` placeholders and `TODO` remarks. +3. Edit the main store-index file and add the relevant store integration there. +4. Check the debug-module, and add relevant debug code. + - Register the store in the `reset()` method. + +--- + +Main store-index: +`modules/ppcp-settings/resources/js/data/index.js` + +Sample store integration: +```js +import * as YourStore from './yourStore'; +// ... +YourStore.initStore(); +// ... +export const YourStoreHooks = YourStore.hooks; +// ... +export const YourStoreName = YourStore.STORE_NAME; +// ... +addDebugTools( window.ppcpSettings, [ ..., YourStoreName ] ); +``` + +--- + +### New Store: PHP integration + +1. Create the **REST endpoint** for hydrating and persisting data. + - `modules/ppcp-settings/src/Endpoint/YourStoreRestEndpoint.php` + - Extend from base class `RestEndpoint` +2. Create the **data model** class to manage the DB interaction. + - `modules/ppcp-settings/src/Data/YourStoreSettings.php` + - Extend from base class `AbstractDataModel` +3. Create relevant **DI services** for both files. + - `modules/ppcp-settings/services.php` +4. Register the REST endpoint in the **service module**. + - `modules/ppcp-settings/src/SettingsModule.php` + - Find the action `rest_api_init` diff --git a/modules/ppcp-settings/resources/js/data/payment/action-types.js b/modules/ppcp-settings/resources/js/data/payment/action-types.js new file mode 100644 index 000000000..e68253c5d --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/action-types.js @@ -0,0 +1,18 @@ +/** + * Action Types: Define unique identifiers for actions across all store modules. + * + * @file + */ + +export default { + // Transient data. + SET_TRANSIENT: 'PAYMENT:SET_TRANSIENT', + + // Persistent data. + SET_PERSISTENT: 'PAYMENT:SET_PERSISTENT', + RESET: 'PAYMENT:RESET', + HYDRATE: 'PAYMENT:HYDRATE', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: 'PAYMENT:DO_PERSIST_DATA', +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/actions.js b/modules/ppcp-settings/resources/js/data/payment/actions.js new file mode 100644 index 000000000..7360424f4 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/actions.js @@ -0,0 +1,71 @@ +/** + * Action Creators: Define functions to create action objects. + * + * These functions update state or trigger side effects (e.g., async operations). + * Actions are categorized as Transient, Persistent, or Side effect. + * + * @file + */ + +import { select } from '@wordpress/data'; + +import ACTION_TYPES from './action-types'; +import { STORE_NAME } from './constants'; + +/** + * @typedef {Object} Action An action object that is handled by a reducer or control. + * @property {string} type - The action type. + * @property {Object?} payload - Optional payload for the action. + */ + +/** + * Special. Resets all values in the store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { type: ACTION_TYPES.RESET } ); + +/** + * Persistent. Set the full store details during app initialization. + * + * @param {{data: {}, flags?: {}}} payload + * @return {Action} The action. + */ +export const hydrate = ( payload ) => ( { + type: ACTION_TYPES.HYDRATE, + payload, +} ); + +/** + * Transient. Marks the store as "ready", i.e., fully initialized. + * + * @param {boolean} isReady + * @return {Action} The action. + */ +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); + +/** + * Persistent. Sets a sample value. + * TODO: Replace with a real action/property. + * + * @param {string} value + * @return {Action} The action. + */ +export const setSampleValue = ( value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { sampleValue: value }, +} ); + +/** + * Side effect. Triggers the persistence of store data to the server. + * + * @return {Action} The action. + */ +export const persist = function* () { + const data = yield select( STORE_NAME ).persistentData(); + + yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/constants.js b/modules/ppcp-settings/resources/js/data/payment/constants.js new file mode 100644 index 000000000..82c428074 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/constants.js @@ -0,0 +1,28 @@ +/** + * Name of the Redux store module. + * + * Used by: Reducer, Selector, Index + * + * @type {string} + */ +export const STORE_NAME = 'wc/paypal/payment'; + +/** + * REST path to hydrate data of this module by loading data from the WP DB. + * + * Used by: Resolvers + * See: payment.php + * + * @type {string} + */ +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/payment'; + +/** + * REST path to persist data of this module to the WP DB. + * + * Used by: Controls + * See: payment.php + * + * @type {string} + */ +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/payment'; diff --git a/modules/ppcp-settings/resources/js/data/payment/controls.js b/modules/ppcp-settings/resources/js/data/payment/controls.js new file mode 100644 index 000000000..9295b62bc --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/controls.js @@ -0,0 +1,23 @@ +/** + * Controls: Implement side effects, typically asynchronous operations. + * + * Controls use ACTION_TYPES keys as identifiers. + * They are triggered by corresponding actions and handle external interactions. + * + * @file + */ + +import apiFetch from '@wordpress/api-fetch'; + +import { REST_PERSIST_PATH } from './constants'; +import ACTION_TYPES from './action-types'; + +export const controls = { + async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { + return await apiFetch( { + path: REST_PERSIST_PATH, + method: 'POST', + data, + } ); + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js new file mode 100644 index 000000000..394fceb7e --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js @@ -0,0 +1,65 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * + * @file + */ + +import { useSelect, useDispatch } from '@wordpress/data'; + +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); + +const useHooks = () => { + const { + persist, + + // TODO: Replace with real property. + setSampleValue, + } = useDispatch( STORE_NAME ); + + // Read-only flags and derived state. + // Nothing here yet. + + // Transient accessors. + const isReady = useTransient( 'isReady' ); + + // Persistent accessors. + // TODO: Replace with real property. + const sampleValue = usePersistent( 'sampleValue' ); + + return { + persist, + isReady, + sampleValue, + setSampleValue, + }; +}; + +export const useState = () => { + const { persist, isReady } = useHooks(); + return { persist, isReady }; +}; + +// TODO: Replace with real hook. +export const useSampleValue = () => { + const { sampleValue, setSampleValue } = useHooks(); + + return { + sampleValue, + setSampleValue, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/index.js b/modules/ppcp-settings/resources/js/data/payment/index.js new file mode 100644 index 000000000..28c162f98 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/index.js @@ -0,0 +1,24 @@ +import { createReduxStore, register } from '@wordpress/data'; +import { controls as wpControls } from '@wordpress/data-controls'; + +import { STORE_NAME } from './constants'; +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as hooks from './hooks'; +import { resolvers } from './resolvers'; +import { controls } from './controls'; + +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/payment/reducer.js b/modules/ppcp-settings/resources/js/data/payment/reducer.js new file mode 100644 index 000000000..a858b719a --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/reducer.js @@ -0,0 +1,56 @@ +/** + * Reducer: Defines store structure and state updates for this module. + * + * Manages both transient (temporary) and persistent (saved) state. + * The initial state must define all properties, as dynamic additions are not supported. + * + * @file + */ + +import { createReducer, createSetters } from '../utils'; +import ACTION_TYPES from './action-types'; + +// Store structure. + +// Transient: Values that are _not_ saved to the DB (like app lifecycle-flags). +const defaultTransient = Object.freeze( { + isReady: false, +} ); + +// Persistent: Values that are loaded from the DB. +const defaultPersistent = Object.freeze( { + // TODO: Add real DB properties here. + sampleValue: 'foo', +} ); + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +const reducer = createReducer( defaultTransient, defaultPersistent, { + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => + setTransient( state, payload ), + + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => + setPersistent( state, payload ), + + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.isReady = true; + + return cleanState; + }, + + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => + setPersistent( state, payload.data ), +} ); + +export default reducer; diff --git a/modules/ppcp-settings/resources/js/data/payment/resolvers.js b/modules/ppcp-settings/resources/js/data/payment/resolvers.js new file mode 100644 index 000000000..321ddfee8 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/resolvers.js @@ -0,0 +1,37 @@ +/** + * Resolvers: Handle asynchronous data fetching for the store. + * + * These functions update store state with data from external sources. + * Each resolver corresponds to a specific selector (selector with same name must exist). + * Resolvers are called automatically when selectors request unavailable data. + * + * @file + */ + +import { dispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { apiFetch } from '@wordpress/data-controls'; + +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; + +export const resolvers = { + /** + * Retrieve settings from the site's REST API. + */ + *persistentData() { + try { + const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); + + yield dispatch( STORE_NAME ).hydrate( result ); + yield dispatch( STORE_NAME ).setIsReady( true ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + // TODO: Add the module name to the error message. + __( + 'Error retrieving payment details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/selectors.js b/modules/ppcp-settings/resources/js/data/payment/selectors.js new file mode 100644 index 000000000..14334fcf3 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/selectors.js @@ -0,0 +1,21 @@ +/** + * Selectors: Extract specific pieces of state from the store. + * + * These functions provide a consistent interface for accessing store data. + * They allow components to retrieve data without knowing the store structure. + * + * @file + */ + +const EMPTY_OBJ = Object.freeze( {} ); + +const getState = ( state ) => state || EMPTY_OBJ; + +export const persistentData = ( state ) => { + return getState( state ).data || EMPTY_OBJ; +}; + +export const transientData = ( state ) => { + const { data, ...transientState } = getState( state ); + return transientState || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 6fc9d67e3..838ebe3d5 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -13,10 +13,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\PaymentRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; @@ -64,12 +66,18 @@ return array( $container->get( 'wcgateway.is-send-only-country' ) ); }, + 'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings { + return new PaymentSettings(); + }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); }, 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); }, + 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { + return new PaymentRestEndpoint( $container->get( 'settings.data.payment' ) ); + }, 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint { return new RefreshFeatureStatusEndpoint( $container->get( 'wcgateway.settings' ), diff --git a/modules/ppcp-settings/src/Data/PaymentSettings.php b/modules/ppcp-settings/src/Data/PaymentSettings.php index 0180150a2..d603a082f 100644 --- a/modules/ppcp-settings/src/Data/PaymentSettings.php +++ b/modules/ppcp-settings/src/Data/PaymentSettings.php @@ -1,6 +1,6 @@ array( + array( + 'id' => 'paypal', + 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-paypal', + ), + array( + 'id' => 'venmo', + 'title' => __( 'Venmo', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Offer Venmo at checkout to millions of active users.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-venmo', + ), + array( + 'id' => 'paypal_credit', + 'title' => __( 'Pay Later', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-paypal', + ), + array( + 'id' => 'credit_and_debit_card_payments', + 'title' => __( + 'Credit and debit card payments', + 'woocommerce-paypal-payments' + ), + 'description' => __( + "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-cards', + ), + ), + 'paymentMethodsOnlineCardPayments' => array( + array( + 'id' => 'advanced_credit_and_debit_card_payments', + 'title' => __( + 'Advanced Credit and Debit Card Payments', + 'woocommerce-paypal-payments' + ), + 'description' => __( + "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-advanced-cards', + ), + array( + '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.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-fastlane', + ), + array( + 'id' => 'apple_pay', + 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Allow customers to pay via their Apple Pay digital wallet.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-apple-pay', + ), + array( + 'id' => 'google_pay', + 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Allow customers to pay via their Google Pay digital wallet.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-google-pay', + ), + ), + 'paymentMethodsAlternative' => array( + array( + 'id' => 'bancontact', + 'title' => __( 'Bancontact', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-bancontact', + ), + array( + 'id' => 'ideal', + 'title' => __( 'iDEAL', 'woocommerce-paypal-payments' ), + 'description' => __( + 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-ideal', + ), + array( + 'id' => 'eps', + 'title' => __( 'eps', 'woocommerce-paypal-payments' ), + 'description' => __( + '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', + ), + array( + 'id' => 'blik', + 'title' => __( 'BLIK', 'woocommerce-paypal-payments' ), + 'description' => __( + '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', + ), + array( + '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', + ), + array( + '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', + ), + array( + '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', + ), + array( + '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', + ), + array( + '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', + + ), + array( + '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', + ), + ), + ); } } diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php new file mode 100644 index 000000000..4df0b2607 --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -0,0 +1,132 @@ + array( + 'js_name' => 'paymentMethodsPayPalCheckout', + ), + 'paymentMethodsOnlineCardPayments' => array( + 'js_name' => 'paymentMethodsOnlineCardPayments', + ), + 'paymentMethodsAlternative' => array( + 'js_name' => 'paymentMethodsAlternative', + ), + ); + + /** + * Constructor. + * + * @param PaymentSettings $settings The settings instance. + */ + public function __construct( PaymentSettings $settings ) { + $this->settings = $settings; + } + + /** + * Configure REST API routes. + */ + public function register_routes() : void { + /** + * GET wc/v3/wc_paypal/payment + */ + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + + /** + * POST wc/v3/wc_paypal/payment + * { + * // Fields mentioned in $field_map[]['js_name'] + * } + */ + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + } + + /** + * Returns all payment methods details. + * + * @return WP_REST_Response The current payment methods details. + */ + public function get_details() : WP_REST_Response { + $js_data = $this->sanitize_for_javascript( + $this->settings->to_array(), + $this->field_map + ); + + return $this->return_success( + $js_data + ); + } + + /** + * Updates payment methods details based on the request. + * + * @param WP_REST_Request $request Full data about the request. + * + * @return WP_REST_Response The updated payment methods details. + */ + public function update_details( WP_REST_Request $request ) : WP_REST_Response { + $wp_data = $this->sanitize_for_wordpress( + $request->get_params(), + $this->field_map + ); + + $this->settings->from_array( $wp_data ); + $this->settings->save(); + + return $this->get_details(); + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 2d9356ab7..ce8146a21 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -207,6 +207,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { $endpoints = array( $container->get( 'settings.rest.onboarding' ), $container->get( 'settings.rest.common' ), + $container->get( 'settings.rest.payment' ), $container->get( 'settings.rest.connect_manual' ), $container->get( 'settings.rest.login_link' ), $container->get( 'settings.rest.webhooks' ), From 99161983cfdf83664d0be79d1d9d9ffbe97a93cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 15 Jan 2025 12:31:21 +0100 Subject: [PATCH 226/298] Remove ACDC tokens in continuation mode --- modules/ppcp-vaulting/src/VaultingModule.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 37074b41e..258ae3708 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -13,6 +13,8 @@ use Psr\Log\LoggerInterface; use RuntimeException; use WC_Payment_Token; use WC_Payment_Tokens; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; +use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -28,7 +30,14 @@ use WP_User_Query; * Class StatusReportModule */ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule { - use ModuleClassNameIdTrait; + use ModuleClassNameIdTrait, ContextTrait; + + /** + * Session handler. + * + * @var SessionHandler + */ + protected $session_handler; /** * {@inheritDoc} @@ -103,6 +112,7 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule } ); + $this->session_handler = $container->get( 'session.handler' ); add_filter( 'woocommerce_get_customer_payment_tokens', /** @@ -130,6 +140,14 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule } } + if ( is_checkout() && ! $is_post && $this->is_paypal_continuation() ) { + foreach ( $tokens as $index => $token ) { + if ( $token instanceof \WC_Payment_Token_CC && $token->get_gateway_id() === CreditCardGateway::ID ) { + unset( $tokens[ $index ] ); + } + } + } + return $tokens; }, 10, From a14a87b05c39a43b6d86eda98e6edff2f51f335e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 15 Jan 2025 12:55:30 +0100 Subject: [PATCH 227/298] =?UTF-8?q?=F0=9F=9A=A7=20Idea=20for=20REST=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/PaymentRestEndpoint.php | 97 ++++++++++++------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index 4df0b2607..2786c6510 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -12,7 +12,8 @@ namespace WooCommerce\PayPalCommerce\Settings\Endpoint; use WP_REST_Server; use WP_REST_Response; use WP_REST_Request; -use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway; /** * REST controller for the "Payment Methods" settings tab. @@ -28,37 +29,24 @@ class PaymentRestEndpoint extends RestEndpoint { */ protected $rest_base = 'payment'; - /** - * The settings instance. - * - * @var PaymentSettings - */ - protected PaymentSettings $settings; - /** * Field mapping for request to profile transformation. * * @var array */ - private array $field_map = array( - 'paymentMethodsPayPalCheckout' => array( - 'js_name' => 'paymentMethodsPayPalCheckout', - ), - 'paymentMethodsOnlineCardPayments' => array( - 'js_name' => 'paymentMethodsOnlineCardPayments', - ), - 'paymentMethodsAlternative' => array( - 'js_name' => 'paymentMethodsAlternative', - ), + private array $gateway_ids = array( + 'ppcp-gateway', + 'ppcp-credit-card-gateway', + ApplePayGateway::ID, + EPSGateway::ID, + // Todo: Add all payment methods. Maybe via a filter instead of hard-coding it? ); /** * Constructor. - * - * @param PaymentSettings $settings The settings instance. */ - public function __construct( PaymentSettings $settings ) { - $this->settings = $settings; + public function __construct() { + // Todo: Add DI instead of using `WC()->payment_gateways->payment_gateways()`? } /** @@ -81,7 +69,11 @@ class PaymentRestEndpoint extends RestEndpoint { /** * POST wc/v3/wc_paypal/payment * { - * // Fields mentioned in $field_map[]['js_name'] + * [gateway_id]: { + * enabled + * title + * description + * } * } */ register_rest_route( @@ -101,14 +93,28 @@ class PaymentRestEndpoint extends RestEndpoint { * @return WP_REST_Response The current payment methods details. */ public function get_details() : WP_REST_Response { - $js_data = $this->sanitize_for_javascript( - $this->settings->to_array(), - $this->field_map - ); + // Todo: Change this to DI? + $all_gateways = WC()->payment_gateways->payment_gateways(); - return $this->return_success( - $js_data - ); + $gateway_settings = array(); + + foreach ( $this->gateway_ids as $gateway_id ) { + if ( ! isset( $all_gateways[ $gateway_id ] ) ) { + continue; + } + + $gateway = $all_gateways[ $gateway_id ]; + + $gateway_settings[ $gateway_id ] = array( + 'enabled' => 'yes' === $gateway->enabled, + 'title' => $gateway->get_title(), + 'description' => $gateway->get_description(), + 'method_title' => $gateway->get_method_title(), + 'icon' => $gateway->get_icon(), + ); + } + + return $this->return_success( $gateway_settings ); } /** @@ -119,13 +125,32 @@ class PaymentRestEndpoint extends RestEndpoint { * @return WP_REST_Response The updated payment methods details. */ public function update_details( WP_REST_Request $request ) : WP_REST_Response { - $wp_data = $this->sanitize_for_wordpress( - $request->get_params(), - $this->field_map - ); + // Todo: Change this to DI? + $all_gateways = WC()->payment_gateways->payment_gateways(); - $this->settings->from_array( $wp_data ); - $this->settings->save(); + $request_data = $request->get_params(); + + foreach ( $this->gateway_ids as $gateway_id ) { + // Check if the REST body contains details for this gateway. + if ( ! isset( $request_data[ $gateway_id ] ) || ! isset( $all_gateways[ $gateway_id ] ) ) { + continue; + } + + $gateway = $all_gateways[ $gateway_id ]; + $new_data = $request_data[ $gateway_id ]; + + if ( isset( $new_data['enabled'] ) ) { + $gateway->update_option( 'enabled', $new_data['enabled'] ? 'yes' : 'no' ); + } + if ( isset( $new_data['title'] ) ) { + $gateway->update_option( 'title', sanitize_text_field( $new_data['title'] ) ); + } + if ( isset( $new_data['description'] ) ) { + $gateway->update_option( 'description', wp_kses_post( $new_data['description'] ) ); + } + + $gateway->process_admin_options(); + } return $this->get_details(); } From 95721f5f8f6e07ba44cc75c5b161ab484ad37eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 15 Jan 2025 13:08:05 +0100 Subject: [PATCH 228/298] fixed psalm --- modules/ppcp-vaulting/src/VaultingModule.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 258ae3708..03780cba6 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -28,16 +28,15 @@ use WP_User_Query; /** * Class StatusReportModule + * @psalm-suppress MissingConstructor */ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule { use ModuleClassNameIdTrait, ContextTrait; /** - * Session handler. - * * @var SessionHandler */ - protected $session_handler; + protected SessionHandler $session_handler; /** * {@inheritDoc} From c69f4cdc5017697f27cf8817861444d5853bce6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 15 Jan 2025 13:09:43 +0100 Subject: [PATCH 229/298] changed comment --- modules/ppcp-vaulting/src/VaultingModule.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 03780cba6..aff22dc50 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -34,6 +34,8 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule use ModuleClassNameIdTrait, ContextTrait; /** + * Session Handler + * * @var SessionHandler */ protected SessionHandler $session_handler; From ba15caaa81a3b6120460b2a5f9794084f374f9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 15 Jan 2025 13:22:17 +0100 Subject: [PATCH 230/298] fix phpcs --- modules/ppcp-vaulting/src/VaultingModule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index aff22dc50..d7a20ac49 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -28,6 +28,7 @@ use WP_User_Query; /** * Class StatusReportModule + * * @psalm-suppress MissingConstructor */ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule { From d51577723fe8377f25515e2556833b45557a8a68 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 15 Jan 2025 15:10:24 +0100 Subject: [PATCH 231/298] Add more payment gateways --- .../resources/js/data/payment/resolvers.js | 1 - .../src/Endpoint/PaymentRestEndpoint.php | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/payment/resolvers.js b/modules/ppcp-settings/resources/js/data/payment/resolvers.js index 321ddfee8..ebc6832bb 100644 --- a/modules/ppcp-settings/resources/js/data/payment/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/payment/resolvers.js @@ -26,7 +26,6 @@ export const resolvers = { yield dispatch( STORE_NAME ).setIsReady( true ); } catch ( e ) { yield dispatch( 'core/notices' ).createErrorNotice( - // TODO: Add the module name to the error message. __( 'Error retrieving payment details.', 'woocommerce-paypal-payments' diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index 2786c6510..d985f07c1 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -9,6 +9,17 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\IDealGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MultibancoGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WP_REST_Server; use WP_REST_Response; use WP_REST_Request; @@ -35,11 +46,21 @@ class PaymentRestEndpoint extends RestEndpoint { * @var array */ private array $gateway_ids = array( - 'ppcp-gateway', - 'ppcp-credit-card-gateway', + PayPalGateway::ID, + CardButtonGateway::ID, + + CreditCardGateway::ID, ApplePayGateway::ID, + GooglePayGateway::ID, + + BancontactGateway::ID, + BlikGateway::ID, EPSGateway::ID, - // Todo: Add all payment methods. Maybe via a filter instead of hard-coding it? + IDealGateway::ID, + MyBankGateway::ID, + P24Gateway::ID, + TrustlyGateway::ID, + MultibancoGateway::ID, ); /** From 491352710e83280b0de67a3fb5c0bc2c2134934a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 15 Jan 2025 12:25:37 +0100 Subject: [PATCH 232/298] =?UTF-8?q?=F0=9F=9A=A7=20Refactor=20the=20Styling?= =?UTF-8?q?=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/css/_variables.scss | 2 + .../settings/_tab-paylater-configurator.scss | 2 +- .../screens/settings/_tab-styling.scss | 65 ++-- .../Components/Styling/LocationSelector.js | 35 ++ .../Components/Styling/PreviewPanel.js | 11 + .../Components/Styling/SettingsPanel.js | 184 +++++++++++ .../Components/Styling/StylingSection.js | 29 ++ .../Screens/Settings/Tabs/TabStyling.js | 308 +----------------- .../resources/js/data/styling/constants.js | 10 +- 9 files changed, 301 insertions(+), 345 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss index 5ccf191e5..53483adb6 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -55,6 +55,8 @@ $card-vertical-gap: 48px; --color-gray-100: #{$color-gray-100}; --color-gradient-dark: #{$color-gradient-dark}; + --color-preview-background: #FAF8F5; + --color-separators: #{$color-gray-200}; --color-text-title: #{$color-gray-900}; --color-text-main: #{$color-text-text}; --color-text-teriary: #{$color-text-tertiary}; diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss index ae49bed82..9444f4e18 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss @@ -1,6 +1,6 @@ .ppcp-r-paylater-configurator { display: flex; - border: 1px solid $color-gray-200; + border: 1px solid var(--color-separators); border-radius: 8px; overflow: hidden; diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index c75d6a4c9..87472e377 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -1,61 +1,49 @@ +$width-settings-panel: 422px; + .ppcp-r-styling { display: flex; - border: 1px solid $color-gray-200; + border: 1px solid var(--color-separators); border-radius: 8px; overflow: hidden; - &__section:not(:last-child) { - border-bottom: 1px solid black; - padding-bottom: 24px; - margin-bottom: 28px; - border-bottom: 1px solid $color-gray-600; - } + .ppcp-r-styling__title { + @include font(14, 16, 600); - &__main-title { - @include font(14, 20, 600); - color: $color-gray-800; - margin: 0 0 8px 0; + color: var(--color-text-title); display: block; - } - - &__description { - @include font(13, 20, 400); - color: $color-gray-800; margin: 0 0 18px 0; } - &__settings { - width: 422px; - background-color: $color-white; - padding: 48px; + .header-section .ppcp-r-styling__title { + @include font(16, 20, 600); } - &__preview { - width: calc(100% - 422px); - background-color: #FAF8F5; + /* The settings-panel (left side) */ + .settings-panel { + width: $width-settings-panel; + padding: 48px; + + .ppcp-r-styling__section { + padding-bottom: 24px; + margin-bottom: 28px; + border-bottom: 1px solid var(--color-separators); + } + } + + /* The preview area (right side) */ + .preview-panel { + width: calc(100% - $width-settings-panel); + background-color: var(--color-preview-background); display: flex; align-items: center; - &-inner { + .preview-panel-inner { width: 100%; padding: 24px; } } - &__section--rc { - .ppcp-r-styling__title { - @include font(13, 20, 600); - color: $color-black; - display: block; - margin: 0 0 18px 0; - } - } - - &__section--empty.ppcp-r-styling__section { - padding-bottom: 0; - margin-bottom: 0; - border-bottom: none; - } + /* --- * &__select { label { @@ -118,4 +106,5 @@ } } } + // */ } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js new file mode 100644 index 000000000..af0b83840 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js @@ -0,0 +1,35 @@ +import { SelectControl } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +import StylingSection from './StylingSection'; + +const LocationSelector = ( { choices = [], location, setLocation } ) => { + // TODO. move to store/hook. + const locationData = choices.find( + ( choice ) => choice.value === location + ); + const { description, link } = locationData || {}; + const locationDescription = sprintf( description, link ); + + return ( + + setLocation( choice ) } + /> +

+ + ); +}; + +export default LocationSelector; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js new file mode 100644 index 000000000..66658f2d6 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js @@ -0,0 +1,11 @@ +import PaymentButtonPreview from '../PaymentButtonPreview'; + +const PreviewPanel = ( { settings } ) => ( +

+
+ +
+
+); + +export default PreviewPanel; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js new file mode 100644 index 000000000..d6571a4c5 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -0,0 +1,184 @@ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { RadioControl, SelectControl } from '@wordpress/components'; + +import { STYLING_LOCATIONS } from '../../../../../data'; +import { PayPalCheckboxGroup } from '../../../../ReusableComponents/Fields'; +import LocationSelector from './LocationSelector'; +import StylingSection from './StylingSection'; + +const SettingsPanel = () => { + const { location, setLocation } = useState( 'cart' ); + + const currentLocationSettings = { + settings: { shape: '', label: '', color: '' }, + }; + const handleChange = () => {}; + + return ( +
+ + + + + + + + + + +
+ ); +}; + +export default SettingsPanel; + +// ----- +const SectionPaymentMethods = ( { + locationSettings, + updateButtonSettings, +} ) => { + const paymentMethodOptions = []; + + return ( + +
+ + updateButtonSettings( 'paymentMethods', newValue ) + } + currentValue={ locationSettings.paymentMethods } + /> +
+
+ ); +}; + +const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => { + const buttonLayoutIsAllowed = + locationSettings.layout && locationSettings.tagline === false; + return ( + buttonLayoutIsAllowed && ( + + + updateButtonStyle( 'layout', newValue ) + } + selected={ locationSettings.layout } + options={ [] } + /> + + ) + ); +}; + +const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => { + return ( + + + updateButtonStyle( 'shape', newValue ) + } + selected={ locationSettings.shape } + options={ [] } + /> + + ); +}; + +const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => { + return ( + + + updateButtonStyle( 'label', newValue ) + } + value={ locationSettings.label } + label={ __( 'Button Label', 'woocommerce-paypal-payments' ) } + options={ [] } + /> + + ); +}; + +const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => { + return ( + + + updateButtonStyle( 'color', newValue ) + } + value={ locationSettings.color } + options={ [] } + /> + + ); +}; + +const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => { + const taglineIsAllowed = + locationSettings.hasOwnProperty( 'tagline' ) && + locationSettings.layout === 'horizontal'; + + return ( + taglineIsAllowed && ( + + { + updateButtonStyle( 'tagline', newValue ); + } } + currentValue={ locationSettings.tagline } + /> + + ) + ); +}; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js new file mode 100644 index 000000000..ae72e5b03 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js @@ -0,0 +1,29 @@ +import classnames from 'classnames'; + +const StylingSection = ( { + title, + className = '', + description = '', + children, +} ) => { + const sectionClasses = classnames( 'ppcp-r-styling__section', className ); + + return ( +
+ { title } + + { description && ( +

+ ) } + + { children } +

+ ); +}; + +export default StylingSection; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js index 98019c93c..4a0b8f015 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js @@ -1,316 +1,22 @@ -import { __, sprintf } from '@wordpress/i18n'; -import { SelectControl, RadioControl } from '@wordpress/components'; -import { PayPalCheckboxGroup } from '../../../ReusableComponents/Fields'; -import { useState, useMemo, useEffect } from '@wordpress/element'; +import { useState } from '@wordpress/element'; -import { - defaultLocationSettings, - paymentMethodOptions, -} from '../../../../data/settings/tab-styling-data'; -import { - STYLING_LABELS, - STYLING_COLORS, - STYLING_LAYOUTS, - STYLING_SHAPES, -} from '../../../../data'; -import PaymentButtonPreview from '../Components/PaymentButtonPreview'; +import { defaultLocationSettings } from '../../../../data/settings/tab-styling-data'; + +import PreviewPanel from '../Components/Styling/PreviewPanel'; +import SettingsPanel from '../Components/Styling/SettingsPanel'; const TabStyling = () => { - const [ location, setLocation ] = useState( 'cart' ); - const [ canRender, setCanRender ] = useState( false ); const [ locationSettings, setLocationSettings ] = useState( { ...defaultLocationSettings, } ); - // Sometimes buttons won't render. This fixes the timing problem. - useEffect( () => { - const handleDOMContentLoaded = () => setCanRender( true ); - if ( - document.readyState === 'interactive' || - document.readyState === 'complete' - ) { - handleDOMContentLoaded(); - } else { - document.addEventListener( - 'DOMContentLoaded', - handleDOMContentLoaded - ); - } - }, [] ); - - const currentLocationSettings = useMemo( () => { - return locationSettings[ location ]; - }, [ location, locationSettings ] ); - - const locationOptions = useMemo( () => { - return Object.keys( locationSettings ).reduce( - ( locationOptionsData, key ) => { - locationOptionsData.push( { - value: locationSettings[ key ].value, - label: locationSettings[ key ].label, - } ); - - return locationOptionsData; - }, - [] - ); - }, [ locationSettings ] ); - - const updateButtonSettings = ( key, value ) => { - setLocationSettings( { - ...locationSettings, - [ location ]: { - ...currentLocationSettings, - settings: { - ...currentLocationSettings.settings, - [ key ]: value, - }, - }, - } ); - }; - - const updateButtonStyle = ( key, value ) => { - setLocationSettings( { - ...locationSettings, - [ location ]: { - ...currentLocationSettings, - settings: { - ...currentLocationSettings.settings, - style: { - ...currentLocationSettings.settings.style, - [ key ]: value, - }, - }, - }, - } ); - }; - - if ( ! canRender ) { - return <>; - } - return (
-
- - - + - - - - - - -
-
-
- -
-
+
); }; -const TabStylingSection = ( props ) => { - let sectionTitleClassName = 'ppcp-r-styling__section'; - - if ( props?.className ) { - sectionTitleClassName += ` ${ props.className }`; - } - - return ( -
- { props.title } - { props?.description && ( -

- ) } - { props.children } -

- ); -}; - -const SectionIntro = ( { location } ) => { - const { description, descriptionLink } = - defaultLocationSettings[ location ]; - const buttonStyleDescription = sprintf( description, descriptionLink ); - - return ( - - ); -}; - -const SectionLocations = ( { locationOptions, location, setLocation } ) => { - return ( - - setLocation( newLocation ) } - label={ __( 'Locations', 'woocommerce-paypal-payments' ) } - options={ locationOptions } - /> - - ); -}; - -const SectionPaymentMethods = ( { - locationSettings, - updateButtonSettings, -} ) => { - return ( - -
- - updateButtonSettings( 'paymentMethods', newValue ) - } - currentValue={ locationSettings.settings.paymentMethods } - /> -
-
- ); -}; - -const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => { - const buttonLayoutIsAllowed = - locationSettings.settings.style?.layout && - locationSettings.settings.style?.tagline === false; - return ( - buttonLayoutIsAllowed && ( - - - updateButtonStyle( 'layout', newValue ) - } - selected={ locationSettings.settings.style.layout } - options={ Object.values( STYLING_LAYOUTS ) } - /> - - ) - ); -}; - -const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => { - return ( - - - updateButtonStyle( 'shape', newValue ) - } - selected={ locationSettings.settings.style.shape } - options={ Object.values( STYLING_SHAPES ) } - /> - - ); -}; - -const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => { - return ( - - - updateButtonStyle( 'label', newValue ) - } - value={ locationSettings.settings.style.label } - label={ __( 'Button Label', 'woocommerce-paypal-payments' ) } - options={ Object.values( STYLING_LABELS ) } - /> - - ); -}; - -const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => { - return ( - - - updateButtonStyle( 'color', newValue ) - } - value={ locationSettings.settings.style.color } - options={ Object.values( STYLING_COLORS ) } - /> - - ); -}; - -const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => { - const taglineIsAllowed = - locationSettings.settings.style.hasOwnProperty( 'tagline' ) && - locationSettings.settings.style?.layout === 'horizontal'; - - return ( - taglineIsAllowed && ( - - { - updateButtonStyle( 'tagline', newValue ); - } } - currentValue={ locationSettings.settings.style.tagline } - /> - - ) - ); -}; - export default TabStyling; diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js index 357fe43a1..92d3714da 100644 --- a/modules/ppcp-settings/resources/js/data/styling/constants.js +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -35,7 +35,7 @@ export const STYLING_LOCATIONS = { label: __( 'Cart', 'woocommerce-paypal-payments' ), // translators: %s is the URL to a documentation page. description: __( - 'Customize the appearance of the PayPal smart buttons on the Cart page and select which additional payment buttons to display in this location.', + 'More details on the Cart page.', 'wooocommerce-paypal-payments' ), link: '#', @@ -45,7 +45,7 @@ export const STYLING_LOCATIONS = { label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), // translators: %s is the URL to a documentation page. description: __( - 'Customize the appearance of the PayPal smart buttons on the Classic Checkout page and choose which additional payment buttons to display in this location.', + 'More details on the Classic Checkout page.', 'wooocommerce-paypal-payments' ), link: '#', @@ -55,7 +55,7 @@ export const STYLING_LOCATIONS = { label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), // translators: %s is the URL to a documentation page. description: __( - 'Customize the appearance of the PayPal smart buttons on the Express Checkout location and choose which additional payment buttons to display in this location.', + 'More details on the Express Checkout location.', 'wooocommerce-paypal-payments' ), link: '#', @@ -65,7 +65,7 @@ export const STYLING_LOCATIONS = { label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), // translators: %s is the URL to a documentation page. description: __( - 'Customize the appearance of the PayPal smart buttons on the Mini Cart and choose which additional payment buttons to display in this location.', + 'More details on the Mini Cart.', 'wooocommerce-paypal-payments' ), link: '#', @@ -75,7 +75,7 @@ export const STYLING_LOCATIONS = { label: __( 'Product Page', 'woocommerce-paypal-payments' ), // translators: %s is the URL to a documentation page. description: __( - 'Customize the appearance of the PayPal smart buttons on the Product Page and choose which additional payment buttons to display in this location.', + 'More details on the Product Page.', 'wooocommerce-paypal-payments' ), link: '#', From 55d1fd3699d18efacd51445934021146930a4666 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 15 Jan 2025 18:43:12 +0100 Subject: [PATCH 233/298] =?UTF-8?q?=F0=9F=9A=A7=20Move=20styling=20data=20?= =?UTF-8?q?to=20a=20dummy=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/PaymentButtonPreview.js | 28 --- .../Components/Styling/LocationSelector.js | 16 +- .../Components/Styling/PreviewPanel.js | 36 +++- .../Components/Styling/SettingsPanel.js | 200 +++++++----------- .../Screens/Settings/Tabs/TabStyling.js | 121 +++++++++-- .../js/data/settings/tab-styling-data.js | 98 --------- .../resources/js/data/styling/constants.js | 19 ++ 7 files changed, 242 insertions(+), 276 deletions(-) delete mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js delete mode 100644 modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js deleted file mode 100644 index 0c43ba902..000000000 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/PaymentButtonPreview.js +++ /dev/null @@ -1,28 +0,0 @@ -import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js'; - -const PREVIEW_CLIENT_ID = 'test'; -const PREVIEW_MERCHANT_ID = 'QTQX5NP6N9WZU'; - -const PaymentButtonPreview = ( { - style, - components = [ 'buttons', 'googlepay' ], -} ) => { - return ( - - - Error - - - ); -}; - -export default PaymentButtonPreview; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js index af0b83840..c05b68727 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js @@ -1,14 +1,14 @@ import { SelectControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +// Dummy hook. +import { useStylingProps } from '../../Tabs/TabStyling'; + import StylingSection from './StylingSection'; -const LocationSelector = ( { choices = [], location, setLocation } ) => { - // TODO. move to store/hook. - const locationData = choices.find( - ( choice ) => choice.value === location - ); - const { description, link } = locationData || {}; +const LocationSelector = ( { location, setLocation } ) => { + const { locationChoices, locationDetails } = useStylingProps( location ); + const { description, link } = locationDetails || {}; const locationDescription = sprintf( description, link ); return ( @@ -23,9 +23,9 @@ const LocationSelector = ( { choices = [], location, setLocation } ) => { setLocation( choice ) } + onChange={ setLocation } />

diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js index 66658f2d6..8b83785b7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js @@ -1,11 +1,33 @@ -import PaymentButtonPreview from '../PaymentButtonPreview'; +import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js'; -const PreviewPanel = ( { settings } ) => ( -

-
- +const PREVIEW_CLIENT_ID = 'test'; +const PREVIEW_MERCHANT_ID = 'QTQX5NP6N9WZU'; + +const PreviewPanel = () => { + // TODO: Make those props dynamic based on location style settings. + const style = {}; + const components = [ 'buttons', 'googlepay' ]; + + const providerOptions = { + clientId: PREVIEW_CLIENT_ID, + merchantId: PREVIEW_MERCHANT_ID, + components: components.join( ',' ), + 'disable-funding': 'card', + 'buyer-country': 'US', + currency: 'USD', + }; + + return ( +
+
+ + + Error + + +
-
-); + ); +}; export default PreviewPanel; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js index d6571a4c5..e739ab629 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -1,54 +1,28 @@ import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; import { RadioControl, SelectControl } from '@wordpress/components'; -import { STYLING_LOCATIONS } from '../../../../../data'; +// Dummy hook. +import { useStylingLocation, useStylingProps } from '../../Tabs/TabStyling'; + import { PayPalCheckboxGroup } from '../../../../ReusableComponents/Fields'; import LocationSelector from './LocationSelector'; import StylingSection from './StylingSection'; const SettingsPanel = () => { - const { location, setLocation } = useState( 'cart' ); - - const currentLocationSettings = { - settings: { shape: '', label: '', color: '' }, - }; - const handleChange = () => {}; + const { location, setLocation } = useStylingLocation(); return (
- - - - - - - - - + + + + + +
); }; @@ -56,129 +30,115 @@ const SettingsPanel = () => { export default SettingsPanel; // ----- -const SectionPaymentMethods = ( { - locationSettings, - updateButtonSettings, -} ) => { - const paymentMethodOptions = []; +const SectionPaymentMethods = ( { location } ) => { + const { paymentMethods, setPaymentMethods, paymentMethodChoices } = + useStylingProps( location ); return ( -
- - updateButtonSettings( 'paymentMethods', newValue ) - } - currentValue={ locationSettings.paymentMethods } - /> -
+
); }; -const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => { - const buttonLayoutIsAllowed = - locationSettings.layout && locationSettings.tagline === false; - return ( - buttonLayoutIsAllowed && ( - - - updateButtonStyle( 'layout', newValue ) - } - selected={ locationSettings.layout } - options={ [] } - /> - - ) - ); -}; +const SectionButtonLayout = ( { location } ) => { + const { supportsLayout, layout, setLayout, layoutChoices } = + useStylingProps( location ); + + if ( ! supportsLayout ) { + return null; + } -const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => { return ( - updateButtonStyle( 'shape', newValue ) - } - selected={ locationSettings.shape } - options={ [] } + options={ layoutChoices } + selected={ layout } + onChange={ setLayout } /> ); }; -const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => { +const SectionButtonShape = ( { location } ) => { + const { shape, setShape, shapeChoices } = useStylingProps( location ); + + return ( + + + + ); +}; + +const SectionButtonLabel = ( { location } ) => { + const { label, setLabel, labelChoices } = useStylingProps( location ); + return ( - updateButtonStyle( 'label', newValue ) - } - value={ locationSettings.label } label={ __( 'Button Label', 'woocommerce-paypal-payments' ) } - options={ [] } + className="ppcp-r-styling__select" + options={ labelChoices } + value={ label } + onChange={ setLabel } /> ); }; -const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => { +const SectionButtonColor = ( { location } ) => { + const { color, setColor, colorChoices } = useStylingProps( location ); + return ( - updateButtonStyle( 'color', newValue ) - } - value={ locationSettings.color } - options={ [] } + className=" ppcp-r-styling__select" + options={ colorChoices } + value={ color } + onChange={ setColor } /> ); }; -const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => { - const taglineIsAllowed = - locationSettings.hasOwnProperty( 'tagline' ) && - locationSettings.layout === 'horizontal'; +const SectionButtonTagline = ( { location } ) => { + const { supportsTagline, tagline, setTagline, taglineChoices } = + useStylingProps( location ); + + if ( ! supportsTagline ) { + return null; + } return ( - taglineIsAllowed && ( - - { - updateButtonStyle( 'tagline', newValue ); - } } - currentValue={ locationSettings.tagline } - /> - - ) + + + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js index 4a0b8f015..07f5ad4d9 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js @@ -1,22 +1,113 @@ -import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useCallback, useState } from '@wordpress/element'; -import { defaultLocationSettings } from '../../../../data/settings/tab-styling-data'; +import { + STYLING_COLORS, + STYLING_LABELS, + STYLING_LAYOUTS, + STYLING_LOCATIONS, + STYLING_PAYMENT_METHODS, + STYLING_SHAPES, +} from '../../../../data/styling/constants'; import PreviewPanel from '../Components/Styling/PreviewPanel'; import SettingsPanel from '../Components/Styling/SettingsPanel'; -const TabStyling = () => { - const [ locationSettings, setLocationSettings ] = useState( { - ...defaultLocationSettings, - } ); - - return ( -
- - - -
- ); -}; +const TabStyling = () => ( +
+ + +
+); export default TabStyling; + +// ---------------------------------------------------------------------------- + +// Temporary "hook" to extract logic before moving it to the Redux store. +export const useStylingLocation = () => { + const [ location, setLocation ] = useState( 'cart' ); + + return { location, setLocation }; +}; + +export const useStylingProps = ( location ) => { + const defaultStyle = { + paymentMethods: [], + color: 'gold', + shape: 'rect', + label: 'paypal', + layout: 'vertical', + tagline: false, + }; + + const [ styles, setStyles ] = useState( { + cart: { ...defaultStyle, label: 'checkout' }, + 'classic-checkout': { ...defaultStyle }, + 'express-checkout': { ...defaultStyle, label: 'pay' }, + 'mini-cart': { ...defaultStyle, label: 'pay' }, + 'product-page': { ...defaultStyle }, + } ); + + const getLocationStyle = useCallback( + ( prop ) => styles[ location ]?.[ prop ], + [ location, styles ] + ); + + const setLocationStyle = useCallback( + ( prop, value ) => { + setStyles( ( prevState ) => ( { + ...prevState, + [ location ]: { + ...prevState[ location ], + [ prop ]: value, + }, + } ) ); + }, + [ location ] + ); + + return { + // Location (drop down). + locationChoices: Object.values( STYLING_LOCATIONS ), + locationDetails: STYLING_LOCATIONS[ location ], + + // Payment methods (checkboxes). + paymentMethodChoices: Object.values( STYLING_PAYMENT_METHODS ), + paymentMethods: getLocationStyle( 'paymentMethods' ), + setPaymentMethods: ( methods ) => + setLocationStyle( 'paymentMethods', methods ), + + // Color (dropdown). + colorChoices: Object.values( STYLING_COLORS ), + color: getLocationStyle( 'color' ), + setColor: ( color ) => setLocationStyle( 'color', color ), + + // Shape (radio). + shapeChoices: Object.values( STYLING_SHAPES ), + shape: getLocationStyle( 'shape' ), + setShape: ( shape ) => setLocationStyle( 'shape', shape ), + + // Label (dropdown). + labelChoices: Object.values( STYLING_LABELS ), + label: getLocationStyle( 'label' ), + setLabel: ( label ) => setLocationStyle( 'label', label ), + + // Layout (radio). + layoutChoices: Object.values( STYLING_LAYOUTS ), + supportsLayout: true, + layout: getLocationStyle( 'layout' ), + setLayout: ( layout ) => setLocationStyle( 'layout', layout ), + + // Tagline (checkbox). + taglineChoices: [ + { + value: 'tagline', + label: __( 'Enable Tagline', 'woocommerce-paypal-payments' ), + }, + ], + supportsTagline: true, + tagline: getLocationStyle( 'tagline' ), + setTagline: ( tagline ) => setLocationStyle( 'tagline', tagline ), + }; +}; 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 deleted file mode 100644 index ae0636481..000000000 --- a/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js +++ /dev/null @@ -1,98 +0,0 @@ -import { __ } from '@wordpress/i18n'; - -const cartAndExpressCheckoutSettings = { - paymentMethods: [], - style: { - shape: 'pill', - label: 'paypal', - color: 'gold', - }, -}; - -const settings = { - paymentMethods: [], - style: { - layout: 'vertical', - shape: cartAndExpressCheckoutSettings.style.shape, - label: cartAndExpressCheckoutSettings.style.label, - color: cartAndExpressCheckoutSettings.style.color, - tagline: false, - }, -}; - -export const defaultLocationSettings = { - cart: { - 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: '#', - }, -}; - -export const paymentMethodOptions = [ - { - value: 'venmo', - label: __( 'Venmo', 'woocommerce-paypal-payments' ), - }, - { - value: 'paylater', - label: __( 'Pay Later', 'woocommerce-paypal-payments' ), - }, - { - value: 'googlepay', - label: __( 'Google Pay', 'woocommerce-paypal-payments' ), - }, - { - value: 'applepay', - label: __( 'Apple Pay', 'woocommerce-paypal-payments' ), - }, -]; diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js index 92d3714da..395d80cdb 100644 --- a/modules/ppcp-settings/resources/js/data/styling/constants.js +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -145,3 +145,22 @@ export const STYLING_SHAPES = { label: __( 'Rectangle', 'woocommerce-paypal-payments' ), }, }; + +export const STYLING_PAYMENT_METHODS = { + venmo: { + value: 'venmo', + label: __( 'Venmo', 'woocommerce-paypal-payments' ), + }, + paylater: { + value: 'paylater', + label: __( 'Pay Later', 'woocommerce-paypal-payments' ), + }, + googlepay: { + value: 'googlepay', + label: __( 'Google Pay', 'woocommerce-paypal-payments' ), + }, + applepay: { + value: 'applepay', + label: __( 'Apple Pay', 'woocommerce-paypal-payments' ), + }, +}; From 85794e77ce2a492cdd3b926df8d8ff5fd8ea6211 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 15 Jan 2025 19:41:46 +0100 Subject: [PATCH 234/298] =?UTF-8?q?=F0=9F=92=84=20Improve=20the=20(broken)?= =?UTF-8?q?=20UI=20in=20the=20Styling-tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/css/components/_reusable.scss | 1 + .../reusable-components/_hstack.scss | 20 +++ .../screens/settings/_tab-styling.scss | 51 ++---- .../Components/ReusableComponents/Fields.js | 161 +++++++++++------- .../Components/ReusableComponents/HStack.js | 26 +++ .../Components/Styling/SettingsPanel.js | 68 ++++---- .../resources/js/data/styling/constants.js | 6 + 7 files changed, 198 insertions(+), 135 deletions(-) create mode 100644 modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js diff --git a/modules/ppcp-settings/resources/css/components/_reusable.scss b/modules/ppcp-settings/resources/css/components/_reusable.scss index 4d4f5c1ba..2de887f57 100644 --- a/modules/ppcp-settings/resources/css/components/_reusable.scss +++ b/modules/ppcp-settings/resources/css/components/_reusable.scss @@ -4,6 +4,7 @@ @import './reusable-components/busy-state'; @import './reusable-components/button'; @import './reusable-components/fields'; +@import './reusable-components/hstack'; @import './reusable-components/navigation'; @import './reusable-components/onboarding-header'; @import './reusable-components/payment-method-icons'; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss new file mode 100644 index 000000000..e55584d1c --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss @@ -0,0 +1,20 @@ +.components-flex { + display: flex; + -webkit-box-align: stretch; + align-items: stretch; + flex-direction: column; + -webkit-box-pack: center; + justify-content: center; + + .components-h-stack { + flex-direction: row; + justify-content: flex-start; + gap: 32px; + } + + // Fix layout for checkboxes inside a flex-stack. + .components-checkbox-control >.components-base-control__field > .components-flex { + flex-direction: row; + gap: 12px; + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index 87472e377..61534cc04 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -27,6 +27,16 @@ $width-settings-panel: 422px; padding-bottom: 24px; margin-bottom: 28px; border-bottom: 1px solid var(--color-separators); + + &:last-child { + padding-bottom: 0; + margin-bottom: 0; + border-bottom-style: none; + } + } + + .components-radio-control__option { + min-width: 100px; } } @@ -65,46 +75,7 @@ $width-settings-panel: 422px; color: $color-black; } } - - .components-flex { - gap: 12px; - } - } - - &__payment-method-checkboxes { - display: flex; - flex-direction: column; - gap: 24px; } } - -.ppcp-r { - &__horizontal-control { - .components-flex { - flex-direction: row; - justify-content: flex-start; - gap: 32px; - } - - - .components-radio-control { - &__option { - gap: 12px; - - input { - margin: 0; - } - - label { - @include font(13, 20, 400); - color: $color-black; - } - } - - input { - margin: 0; - } - } - } - // */ +//*/ } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js index 74336951f..862e057bb 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js @@ -1,107 +1,136 @@ import { CheckboxControl } from '@wordpress/components'; +import classNames from 'classnames'; -export const PayPalCheckbox = ( props ) => { - let isChecked = null; +export const PayPalCheckbox = ( { + currentValue, + label, + value, + checked = null, + disabled = null, + changeCallback, +} ) => { + let isChecked = checked; - if ( Array.isArray( props.currentValue ) ) { - isChecked = props.currentValue.includes( props.value ); - } else { - isChecked = props.currentValue; + if ( null === isChecked ) { + if ( Array.isArray( currentValue ) ) { + isChecked = currentValue.includes( value ); + } else { + isChecked = currentValue; + } } + const onChange = ( newState ) => { + let newValue; + + if ( ! Array.isArray( currentValue ) ) { + newValue = newState; + } else if ( newState ) { + newValue = [ ...currentValue, value ]; + } else { + newValue = currentValue.filter( + ( optionValue ) => optionValue !== value + ); + } + + changeCallback( newValue ); + }; + return (
+ { /* todo: Can we remove the wrapper div? */ } - handleCheckboxState( checked, props ) - } + disabled={ disabled } + onChange={ onChange } />
); }; -export const PayPalCheckboxGroup = ( props ) => { - const renderCheckboxGroup = () => { - return props.value.map( ( checkbox ) => { - return ( - - ); - } ); - }; +export const CheckboxGroup = ( { options, value, onChange } ) => ( + <> + { options.map( ( checkbox ) => ( + + ) ) } + +); - return <>{ renderCheckboxGroup() }; -}; - -export const PayPalRdb = ( props ) => { +export const PayPalRdb = ( { + id, + name, + value, + currentValue, + handleRdbState, +} ) => { return (
+ { /* todo: Can we remove the wrapper div? */ } props.handleRdbState( props.value ) } + id={ id } + checked={ value === currentValue } + name={ name } + value={ value } + onChange={ () => handleRdbState( value ) } />
); }; -export const PayPalRdbWithContent = ( props ) => { - const className = [ 'ppcp-r__radio-wrapper' ]; - - if ( props?.className ) { - className.push( props.className ); - } +export const PayPalRdbWithContent = ( { + className, + id, + name, + label, + description, + value, + currentValue, + handleRdbState, + toggleAdditionalContent, + children, +} ) => { + const wrapperClasses = classNames( 'ppcp-r__radio-wrapper', className ); return (
-
- +
+ +
- - { props.description && ( + + { description && (

) }

- { props?.toggleAdditionalContent && - props.children && - props.value === props.currentValue && ( -
- { props.children } -
- ) } + { toggleAdditionalContent && children && value === currentValue && ( +
+ { children } +
+ ) }
); }; - -export const handleCheckboxState = ( checked, props ) => { - let newValue = null; - if ( ! Array.isArray( props.currentValue ) ) { - newValue = checked; - } else if ( checked ) { - newValue = [ ...props.currentValue, props.value ]; - } else { - newValue = props.currentValue.filter( - ( value ) => value !== props.value - ); - } - props.changeCallback( newValue ); -}; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js new file mode 100644 index 000000000..2c54ac7da --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js @@ -0,0 +1,26 @@ +/** + * Temporary component, until the experimental HStack block editor component is stable. + * + * @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs + * @file + */ +import classNames from 'classnames'; + +const HStack = ( { className, spacing = 3, children } ) => { + const wrapperClass = classNames( + 'components-flex components-h-stack', + className + ); + + const styles = { + gap: `calc(${ 4 * spacing }px)`, + }; + + return ( +
+ { children } +
+ ); +}; + +export default HStack; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js index e739ab629..f357bb287 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -4,7 +4,8 @@ import { RadioControl, SelectControl } from '@wordpress/components'; // Dummy hook. import { useStylingLocation, useStylingProps } from '../../Tabs/TabStyling'; -import { PayPalCheckboxGroup } from '../../../../ReusableComponents/Fields'; +import { CheckboxGroup } from '../../../../ReusableComponents/Fields'; +import HStack from '../../../../ReusableComponents/HStack'; import LocationSelector from './LocationSelector'; import StylingSection from './StylingSection'; @@ -30,6 +31,7 @@ const SettingsPanel = () => { export default SettingsPanel; // ----- + const SectionPaymentMethods = ( { location } ) => { const { paymentMethods, setPaymentMethods, paymentMethodChoices } = useStylingProps( location ); @@ -39,11 +41,13 @@ const SectionPaymentMethods = ( { location } ) => { title={ __( 'Payment Methods', 'woocommerce-paypal-payments' ) } className="payment-methods" > - + + + ); }; @@ -61,12 +65,13 @@ const SectionButtonLayout = ( { location } ) => { className="button-layout" title={ __( 'Button Layout', 'woocommerce-paypal-payments' ) } > - + + + ); }; @@ -79,12 +84,13 @@ const SectionButtonShape = ( { location } ) => { title={ __( 'Shape', 'woocommerce-paypal-payments' ) } className="button-shape" > - + + + ); }; @@ -93,10 +99,11 @@ const SectionButtonLabel = ( { location } ) => { const { label, setLabel, labelChoices } = useStylingProps( location ); return ( - + { const { color, setColor, colorChoices } = useStylingProps( location ); return ( - + { title={ __( 'Tagline', 'woocommerce-paypal-payments' ) } className="tagline" > - + + + ); }; diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js index 395d80cdb..f21ac7adf 100644 --- a/modules/ppcp-settings/resources/js/data/styling/constants.js +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -147,6 +147,12 @@ export const STYLING_SHAPES = { }; export const STYLING_PAYMENT_METHODS = { + paypal: { + value: '', + label: __( 'PayPal', 'woocommerce-paypal-payments' ), + checked: true, + disabled: true, + }, venmo: { value: 'venmo', label: __( 'Venmo', 'woocommerce-paypal-payments' ), From 1cac69ce990c35c730496413209b10e07f06ccd1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 14:37:44 +0100 Subject: [PATCH 235/298] =?UTF-8?q?=E2=9C=A8=20Make=20some=20generic=20UI?= =?UTF-8?q?=20components=20themeable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/css/_mixins.scss | 11 ++ .../reusable-components/_fields.scss | 186 +++++++++--------- .../reusable-components/_settings-block.scss | 19 +- .../Components/ReusableComponents/Fields.js | 22 ++- .../SettingsBlocks/SettingsBlockElements.js | 47 +++-- 5 files changed, 169 insertions(+), 116 deletions(-) diff --git a/modules/ppcp-settings/resources/css/_mixins.scss b/modules/ppcp-settings/resources/css/_mixins.scss index d2fac6cd2..29cb3af1c 100644 --- a/modules/ppcp-settings/resources/css/_mixins.scss +++ b/modules/ppcp-settings/resources/css/_mixins.scss @@ -43,3 +43,14 @@ display: flex; gap: $gap; } + +@mixin disabled-state($control-type) { + .components-#{$control-type}-control.is-disabled { + .components-#{$control-type}-control__input, + .components-#{$control-type}-control__label, + .components-base-control__help { + opacity: 0.5; + cursor: default; + } + } +} 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 1a9cf102c..195367dfb 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss @@ -1,27 +1,96 @@ -.ppcp-r { +.ppcp-r__radio-value { + @include hide-input-field; - &__radio-value { - @include hide-input-field; + &:checked + .ppcp-r__radio-presentation { + position: relative; - &:checked + .ppcp-r__radio-presentation { - position: relative; + &::before { + content: ''; + width: 12px; + height: 12px; + border-radius: 12px; + background-color: $color-blueberry; + display: block; + position: absolute; + transform: translate(-50%, -50%); + left: 50%; + top: 50%; + } + } +} - &::before { - content: ''; - width: 12px; - height: 12px; - border-radius: 12px; - background-color: $color-blueberry; - display: block; - position: absolute; - transform: translate(-50%, -50%); - left: 50%; - top: 50%; - } +.ppcp-r__radio-presentation { + @include fake-input-field(20px); +} + +.ppcp-r__checkbox-presentation { + @include fake-input-field(2px); +} + +.ppcp-r__radio-wrapper { + display: flex; + gap: 18px; + align-items: center; + position: relative; + + label { + @include font(13, 20, 400); + color: $color-gray-800; + } +} + +.ppcp-r__radio-description { + @include font(13, 20, 400); + margin: 0; + color: $color-gray-800; +} + +.ppcp-r__radio-content-additional { + padding-left: 38px; + padding-top: 18px; +} + + +.ppcp-r-app { + @include disabled-state('base'); + @include disabled-state('checkbox'); + + .components-base-control__label { + @include font(13, 16, 600); + color: $color-gray-900; + text-transform: none; + } + + .components-base-control__input { + border: 1px solid $color-gray-700; + border-radius: 2px; + box-shadow: none; + + &:focus { + border-color: $color-blueberry; } } - &__checkbox { + .components-base-control__help { + margin-bottom: 0; + } + + // Text input fields. + input[type='text'] { + @include font(14, 20, 400); + @include primaryFont; + padding: 7px 11px; + border-radius: 2px; + } + + // Select lists. + select { + @include font(14, 20, 400); + padding: 7px 27px 7px 11px; + } + + // Checkboxes. + .components-checkbox-control { position: relative; input { @@ -30,7 +99,7 @@ &:checked { background-color: $color-blueberry; - border-color:$color-blueberry; + border-color: $color-blueberry; } } @@ -43,78 +112,17 @@ } } - &__radio-presentation { - @include fake-input-field(20px); + // Custom styles. + .components-form-toggle.is-checked > .components-form-toggle__track { + background-color: $color-blueberry; } - &__checkbox-presentation { - @include fake-input-field(2px); - } - - &__radio-wrapper { - display: flex; - gap: 18px; - align-items: center; - position: relative; - - label { - @include font(13, 20, 400); - color: $color-gray-800; - } - } - - &__radio-description { - @include font(13, 20, 400); - margin: 0; - color: $color-gray-800; - } - - &__radio-content-additional { - padding-left: 38px; - padding-top: 18px; - } -} - -.components-base-control { - &__label { - color: $color-gray-900; - @include font(13, 16, 600); - text-transform: none; - } - - &__input { - border: 1px solid $color-gray-700; - border-radius: 2px; - box-shadow: none; - - &:focus { - border-color: $color-blueberry; + .ppcp-r-vertical-text-control { + .components-base-control__field { + display: flex; + flex-direction: column; + gap: 0; + margin: 0; } } } - - -input[type='text'] { - padding: 7px 11px; - @include font(14, 20, 400); - @include primaryFont; - border-radius: 2px; -} - -select { - padding: 7px 27px 7px 11px; - @include font(14, 20, 400); -} - -.components-form-toggle.is-checked > .components-form-toggle__track { - background-color: $color-blueberry; -} - -.ppcp-r-vertical-text-control { - .components-base-control__field { - display: flex; - flex-direction: column; - gap: 0; - margin: 0; - } -} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss index f65c5c9d5..f5ddb5ea1 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss @@ -4,7 +4,7 @@ .ppcp-r-settings-block { display: flex; flex-direction: column; - gap: 16px 0; + gap: var(--block-item-gap, 16px) 0; &.ppcp-r-settings-block__input, &.ppcp-r-settings-block__select { @@ -16,8 +16,8 @@ flex-direction: column; gap: 6px; - &:not(:last-child):not(.ppcp-r-settings-block--accordion__header) { - padding-bottom: 6px; + &:not(:last-child) { + padding-bottom: var(--block-header-gap, 6px); } } @@ -27,6 +27,15 @@ display: block; text-transform: uppercase; + &.style-alt { + @include font(14, 16, 600); + text-transform: none; + } + + &.style-big { + @include font(16, 20, 600); + } + .ppcp-r-title-badge { text-transform: none; margin-left: 6px; @@ -89,8 +98,8 @@ } + .ppcp-r-settings-block:not(.no-gap) { - margin-top: 32px; - padding-top: 32px; + margin-top: var(--block-separator-gap, 32px); + padding-top: var(--block-separator-gap, 32px); border-top: 1px solid var(--color-gray-200); } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js index 862e057bb..d979cffba 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields.js @@ -19,6 +19,8 @@ export const PayPalCheckbox = ( { } } + const className = classNames( { 'is-disabled': disabled } ); + const onChange = ( newState ) => { let newValue; @@ -36,16 +38,14 @@ export const PayPalCheckbox = ( { }; return ( -
- { /* todo: Can we remove the wrapper div? */ } - -
+ ); }; @@ -58,6 +58,8 @@ export const CheckboxGroup = ( { options, value, onChange } ) => ( value={ checkbox.value } checked={ checkbox.checked } disabled={ checkbox.disabled } + description={ checkbox.description } + tooltip={ checkbox.tooltip } currentValue={ value } changeCallback={ onChange } /> diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js index ec3ba31ae..de45a601b 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js @@ -1,9 +1,20 @@ +import classNames from 'classnames'; + // Block Elements -export const Title = ( { children, className = '' } ) => ( - - { children } - -); +export const Title = ( { + children, + altStyle = false, + big = false, + className = '', +} ) => { + className = classNames( 'ppcp-r-settings-block__title', className, { + 'style-alt': altStyle, + 'style-big': big, + } ); + + return { children }; +}; + export const TitleWrapper = ( { children } ) => ( { children } ); @@ -14,13 +25,25 @@ export const SupplementaryLabel = ( { children } ) => ( ); -export const Description = ( { children, className = '' } ) => ( - - { children } - -); +export const Description = ( { children, asHtml = false, className = '' } ) => { + // Don't output anything if description is empty. + if ( ! children ) { + return null; + } + + className = classNames( 'ppcp-r-settings-block__description', className ); + + if ( ! asHtml ) { + return { children }; + } + + return ( + + ); +}; export const Action = ( { children } ) => (
{ children }
From e5eb1a4435c463a251362a95677d4a43e33691f3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 14:39:27 +0100 Subject: [PATCH 236/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Switch=20Styling?= =?UTF-8?q?=20UI=20to=20use=20generic=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/settings/_tab-styling.scss | 52 +++++++------------ .../Components/Styling/StylingSection.js | 34 ++++++------ 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index 61534cc04..d88fe64f7 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -1,21 +1,29 @@ $width-settings-panel: 422px; .ppcp-r-styling { + --block-item-gap: 0; + --block-separator-gap: 24px; + --block-header-gap: 18px; + display: flex; border: 1px solid var(--color-separators); border-radius: 8px; overflow: hidden; - .ppcp-r-styling__title { - @include font(14, 16, 600); + .ppcp-r-settings-block { + &.header-section { + margin-bottom: 22px + } - color: var(--color-text-title); - display: block; - margin: 0 0 18px 0; - } + // Select-fields have a smaller gap between the header and input field. + &.has-select { + --block-header-gap: 8px; + } - .header-section .ppcp-r-styling__title { - @include font(16, 20, 600); + // Above the payment methods is a slightly larger gap. + &.payment-methods { + --block-separator-gap: 28px; + } } /* The settings-panel (left side) */ @@ -28,6 +36,7 @@ $width-settings-panel: 422px; margin-bottom: 28px; border-bottom: 1px solid var(--color-separators); + &.no-gap, &:last-child { padding-bottom: 0; margin-bottom: 0; @@ -35,6 +44,7 @@ $width-settings-panel: 422px; } } + // Horizontal radio buttons have a width of 100px. .components-radio-control__option { min-width: 100px; } @@ -52,30 +62,4 @@ $width-settings-panel: 422px; padding: 24px; } } - - /* --- * - - &__select { - label { - @include font(13, 16, 600); - color: $color-black; - margin: 0; - text-transform: none; - } - - select { - @include font(13, 20, 400); - } - } - - .ppcp-r__checkbox { - .components-checkbox-control { - &__label { - @include font(13, 20, 400); - color: $color-black; - } - } - } -} -//*/ } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js index ae72e5b03..51290831a 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js @@ -1,28 +1,32 @@ -import classnames from 'classnames'; +import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock'; +import { + Description, + Header, + Title, +} from '../../../../ReusableComponents/SettingsBlocks'; const StylingSection = ( { title, + bigTitle = false, className = '', description = '', + separatorAndGap = true, children, } ) => { - const sectionClasses = classnames( 'ppcp-r-styling__section', className ); - return ( -
- { title } - - { description && ( -

- ) } + +

+ + { title } + + { description } +
{ children } -
+ ); }; From 11e6624dfc280bf1a829133e58c42cd879bf954d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 14:39:55 +0100 Subject: [PATCH 237/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20common?= =?UTF-8?q?=20Styling=20components=20to=20own=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Styling/LocationSelector.js | 36 ++++--- .../Components/Styling/SettingsPanel.js | 93 +++++++------------ .../Styling/StylingSectionWithCheckboxes.js | 39 ++++++++ .../Styling/StylingSectionWithRadiobuttons.js | 39 ++++++++ .../Styling/StylingSectionWithSelect.js | 36 +++++++ 5 files changed, 169 insertions(+), 74 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithSelect.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js index c05b68727..01745fbdc 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js @@ -4,7 +4,9 @@ import { __, sprintf } from '@wordpress/i18n'; // Dummy hook. import { useStylingProps } from '../../Tabs/TabStyling'; +import { Description } from '../../../../ReusableComponents/SettingsBlocks'; import StylingSection from './StylingSection'; +import StylingSectionWithSelect from './StylingSectionWithSelect'; const LocationSelector = ( { location, setLocation } ) => { const { locationChoices, locationDetails } = useStylingProps( location ); @@ -12,23 +14,29 @@ const LocationSelector = ( { location, setLocation } ) => { const locationDescription = sprintf( description, link ); return ( - - + + -

- + > + + { locationDescription } + + + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js index f357bb287..40d418276 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -1,13 +1,12 @@ import { __ } from '@wordpress/i18n'; -import { RadioControl, SelectControl } from '@wordpress/components'; // Dummy hook. import { useStylingLocation, useStylingProps } from '../../Tabs/TabStyling'; -import { CheckboxGroup } from '../../../../ReusableComponents/Fields'; -import HStack from '../../../../ReusableComponents/HStack'; import LocationSelector from './LocationSelector'; -import StylingSection from './StylingSection'; +import StylingSectionWithSelect from './StylingSectionWithSelect'; +import StylingSectionWithCheckboxes from './StylingSectionWithCheckboxes'; +import StylingSectionWithRadiobuttons from './StylingSectionWithRadiobuttons'; const SettingsPanel = () => { const { location, setLocation } = useStylingLocation(); @@ -37,18 +36,13 @@ const SectionPaymentMethods = ( { location } ) => { useStylingProps( location ); return ( - - - - - + options={ paymentMethodChoices } + value={ paymentMethods } + onChange={ setPaymentMethods } + /> ); }; @@ -61,18 +55,13 @@ const SectionButtonLayout = ( { location } ) => { } return ( - - - - - + options={ layoutChoices } + selected={ layout } + onChange={ setLayout } + /> ); }; @@ -80,18 +69,13 @@ const SectionButtonShape = ( { location } ) => { const { shape, setShape, shapeChoices } = useStylingProps( location ); return ( - - - - - + options={ shapeChoices } + selected={ shape } + onChange={ setShape } + /> ); }; @@ -99,16 +83,13 @@ const SectionButtonLabel = ( { location } ) => { const { label, setLabel, labelChoices } = useStylingProps( location ); return ( - - - + options={ labelChoices } + value={ label } + onChange={ setLabel } + /> ); }; @@ -116,16 +97,13 @@ const SectionButtonColor = ( { location } ) => { const { color, setColor, colorChoices } = useStylingProps( location ); return ( - - - + options={ colorChoices } + value={ color } + onChange={ setColor } + /> ); }; @@ -138,17 +116,12 @@ const SectionButtonTagline = ( { location } ) => { } return ( - - - - - + options={ taglineChoices } + value={ tagline } + onChange={ setTagline } + /> ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js new file mode 100644 index 000000000..cc0b259d6 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js @@ -0,0 +1,39 @@ +import classNames from 'classnames'; + +import { CheckboxGroup } from '../../../../ReusableComponents/Fields'; +import HStack from '../../../../ReusableComponents/HStack'; +import StylingSection from './StylingSection'; + +const StylingSectionWithCheckboxes = ( { + title, + className = '', + description = '', + separatorAndGap = true, + options, + value, + onChange, + children, +} ) => { + className = classNames( 'has-checkboxes', className ); + + return ( + + + + + + { children } + + ); +}; + +export default StylingSectionWithCheckboxes; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js new file mode 100644 index 000000000..4bc33326f --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js @@ -0,0 +1,39 @@ +import { RadioControl } from '@wordpress/components'; +import classNames from 'classnames'; + +import HStack from '../../../../ReusableComponents/HStack'; +import StylingSection from './StylingSection'; + +const StylingSectionWithRadiobuttons = ( { + title, + className = '', + description = '', + separatorAndGap = true, + options, + selected, + onChange, + children, +} ) => { + className = classNames( 'has-radio-buttons', className ); + + return ( + + + + + + { children } + + ); +}; + +export default StylingSectionWithRadiobuttons; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithSelect.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithSelect.js new file mode 100644 index 000000000..c4164d6b8 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithSelect.js @@ -0,0 +1,36 @@ +import { SelectControl } from '@wordpress/components'; +import classNames from 'classnames'; + +import StylingSection from './StylingSection'; + +const StylingSectionWithSelect = ( { + title, + className = '', + description = '', + separatorAndGap = true, + options, + value, + onChange, + children, +} ) => { + className = classNames( 'has-select', className ); + + return ( + + + + { children } + + ); +}; + +export default StylingSectionWithSelect; From 5243ef10dbfcf1c41f78323b8e9acca86f5112c0 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 16 Jan 2025 14:48:04 +0100 Subject: [PATCH 238/298] Get payment methods default data from data store --- .../Screens/Overview/TabPaymentMethods.js | 231 ++---------------- .../resources/js/data/payment/hooks.js | 75 +++++- .../resources/js/data/payment/reducer.js | 18 +- .../src/Endpoint/PaymentRestEndpoint.php | 80 ++++-- 4 files changed, 164 insertions(+), 240 deletions(-) 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 63d627692..3216faf40 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js @@ -1,44 +1,30 @@ import { __ } from '@wordpress/i18n'; -import { useMemo } from '@wordpress/element'; import SettingsCard from '../../ReusableComponents/SettingsCard'; import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock'; -import { CommonHooks } from '../../../data'; +import { PaymentHooks } from '../../../data'; import { useActiveModal } from '../../../data/common/hooks'; import Modal from './TabSettingsElements/Blocks/Modal'; const TabPaymentMethods = () => { - const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + const { paymentMethodsPayPalCheckout } = + PaymentHooks.usePaymentMethodsPayPalCheckout(); + const { paymentMethodsOnlineCardPayments } = + PaymentHooks.usePaymentMethodsOnlineCardPayments(); + const { paymentMethodsAlternative } = + PaymentHooks.usePaymentMethodsAlternative(); + const { activeModal, setActiveModal } = useActiveModal(); - const filteredPaymentMethods = useMemo( () => { - const contextProps = { storeCountry, storeCurrency }; - - return { - payPalCheckout: filterPaymentMethods( - paymentMethodsPayPalCheckout, - contextProps - ), - onlineCardPayments: filterPaymentMethods( - paymentMethodsOnlineCardPayments, - contextProps - ), - alternative: filterPaymentMethods( - paymentMethodsAlternative, - contextProps - ), - }; - }, [ storeCountry, storeCurrency ] ); - const getActiveMethod = () => { if ( ! activeModal ) { return null; } const allMethods = [ - ...filteredPaymentMethods.payPalCheckout, - ...filteredPaymentMethods.onlineCardPayments, - ...filteredPaymentMethods.alternative, + ...paymentMethodsPayPalCheckout, + ...paymentMethodsOnlineCardPayments, + ...paymentMethodsAlternative, ]; return allMethods.find( ( method ) => method.id === activeModal ); @@ -57,7 +43,7 @@ const TabPaymentMethods = () => { contentContainer={ false } > @@ -75,7 +61,7 @@ const TabPaymentMethods = () => { contentContainer={ false } > @@ -93,7 +79,7 @@ const TabPaymentMethods = () => { contentContainer={ false } > @@ -116,193 +102,4 @@ const TabPaymentMethods = () => { ); }; -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' ), - description: __( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-paypal', - }, - { - id: 'venmo', - title: __( 'Venmo', 'woocommerce-paypal-payments' ), - description: __( - 'Offer Venmo at checkout to millions of active users.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-venmo', - }, - { - id: 'paypal_credit', - title: __( 'Pay Later', 'woocommerce-paypal-payments' ), - description: __( - 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-paypal', - }, - { - id: 'credit_and_debit_card_payments', - title: __( - 'Credit and debit card payments', - 'woocommerce-paypal-payments' - ), - description: __( - "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-cards', - }, -]; - -const paymentMethodsOnlineCardPayments = [ - { - id: 'advanced_credit_and_debit_card_payments', - title: __( - 'Advanced Credit and Debit Card Payments', - 'woocommerce-paypal-payments' - ), - description: __( - "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-advanced-cards', - }, - { - 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.", - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-fastlane', - }, - { - id: 'apple_pay', - title: __( 'Apple Pay', 'woocommerce-paypal-payments' ), - description: __( - 'Allow customers to pay via their Apple Pay digital wallet.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-apple-pay', - }, - { - id: 'google_pay', - title: __( 'Google Pay', 'woocommerce-paypal-payments' ), - description: __( - 'Allow customers to pay via their Google Pay digital wallet.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-google-pay', - }, -]; - -const paymentMethodsAlternative = [ - { - id: 'bancontact', - title: __( 'Bancontact', 'woocommerce-paypal-payments' ), - description: __( - 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-bancontact', - }, - { - id: 'ideal', - title: __( 'iDEAL', 'woocommerce-paypal-payments' ), - description: __( - 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-ideal', - }, - { - id: 'eps', - title: __( 'eps', 'woocommerce-paypal-payments' ), - description: __( - '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', - }, - { - id: 'blik', - title: __( 'BLIK', 'woocommerce-paypal-payments' ), - description: __( - '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/data/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js index 394fceb7e..fbae1e603 100644 --- a/modules/ppcp-settings/resources/js/data/payment/hooks.js +++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js @@ -38,14 +38,37 @@ const useHooks = () => { const isReady = useTransient( 'isReady' ); // Persistent accessors. - // TODO: Replace with real property. const sampleValue = usePersistent( 'sampleValue' ); + const paypal = usePersistent( 'ppcp-gateway' ); + const advancedCreditCard = usePersistent( 'ppcp-credit-card-gateway' ); + const bancontact = usePersistent( 'ppcp-bancontact' ); + const blik = usePersistent( 'ppcp-blik' ); + const eps = usePersistent( 'ppcp-eps' ); + const ideal = usePersistent( 'ppcp-ideal' ); + const mybank = usePersistent( 'ppcp-mybank' ); + const p24 = usePersistent( 'ppcp-p24' ); + const trustly = usePersistent( 'ppcp-trustly' ); + const multibanco = usePersistent( 'ppcp-multibanco' ); + const pui = usePersistent( 'ppcp-pay-upon-invoice-gateway' ); + const oxxo = usePersistent( 'ppcp-oxxo-gateway' ); return { persist, isReady, sampleValue, setSampleValue, + paypal, + advancedCreditCard, + bancontact, + blik, + eps, + ideal, + mybank, + p24, + trustly, + multibanco, + pui, + oxxo, }; }; @@ -63,3 +86,53 @@ export const useSampleValue = () => { setSampleValue, }; }; + +export const usePaymentMethodsPayPalCheckout = () => { + const { paypal } = useHooks(); + const paymentMethodsPayPalCheckout = [ paypal ]; + + return { + paymentMethodsPayPalCheckout, + }; +}; + +export const usePaymentMethodsOnlineCardPayments = () => { + const { advancedCreditCard } = useHooks(); + const paymentMethodsOnlineCardPayments = [ advancedCreditCard ]; + + return { + paymentMethodsOnlineCardPayments, + }; +}; + +export const usePaymentMethodsAlternative = () => { + const { + bancontact, + blik, + eps, + ideal, + mybank, + p24, + trustly, + multibanco, + pui, + oxxo, + } = useHooks(); + + const paymentMethodsAlternative = [ + bancontact, + blik, + eps, + ideal, + mybank, + p24, + trustly, + multibanco, + pui, + oxxo, + ]; + + return { + paymentMethodsAlternative, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/reducer.js b/modules/ppcp-settings/resources/js/data/payment/reducer.js index a858b719a..0ce37c18c 100644 --- a/modules/ppcp-settings/resources/js/data/payment/reducer.js +++ b/modules/ppcp-settings/resources/js/data/payment/reducer.js @@ -19,8 +19,22 @@ const defaultTransient = Object.freeze( { // Persistent: Values that are loaded from the DB. const defaultPersistent = Object.freeze( { - // TODO: Add real DB properties here. - sampleValue: 'foo', + 'ppcp-gateway': {}, + 'ppcp-card-button-gateway': {}, + 'ppcp-credit-card-gateway': {}, + 'ppcp-axo-gateway': {}, + 'ppcp-applepay': {}, + 'ppcp-googlepay': {}, + 'ppcp-bancontact': {}, + 'ppcp-blik': {}, + 'ppcp-eps': {}, + 'ppcp-ideal': {}, + 'ppcp-mybank': {}, + 'ppcp-p24': {}, + 'ppcp-trustly': {}, + 'ppcp-multibanco': {}, + 'ppcp-pay-upon-invoice-gateway': {}, + 'ppcp-oxxo-gateway': {}, } ); // Reducer logic. diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index d985f07c1..52f083228 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -9,6 +9,7 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; +use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway; @@ -19,7 +20,9 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WP_REST_Server; use WP_REST_Response; use WP_REST_Request; @@ -45,22 +48,58 @@ class PaymentRestEndpoint extends RestEndpoint { * * @var array */ - private array $gateway_ids = array( - PayPalGateway::ID, - CardButtonGateway::ID, + private array $gateways = array( + PayPalGateway::ID => array( + 'id' => 'paypal', + 'icon' => 'payment-method-paypal', + ), + CardButtonGateway::ID => array( + 'icon' => 'payment-method-cards', + ), - CreditCardGateway::ID, - ApplePayGateway::ID, - GooglePayGateway::ID, + CreditCardGateway::ID => array( + 'icon' => 'payment-method-advanced-cards', + ), + AxoGateway::ID => array( + 'icon' => 'payment-method-fastlane', + ), + ApplePayGateway::ID => array( + 'icon' => 'payment-method-apple-pay', + ), + GooglePayGateway::ID => array( + 'icon' => 'payment-method-google-pay', + ), - BancontactGateway::ID, - BlikGateway::ID, - EPSGateway::ID, - IDealGateway::ID, - MyBankGateway::ID, - P24Gateway::ID, - TrustlyGateway::ID, - MultibancoGateway::ID, + BancontactGateway::ID => array( + 'icon' => 'payment-method-bancontact', + ), + BlikGateway::ID => array( + 'icon' => 'payment-method-blik', + ), + EPSGateway::ID => array( + 'icon' => 'payment-method-eps', + ), + IDealGateway::ID => array( + 'icon' => 'payment-method-ideal', + ), + MyBankGateway::ID => array( + 'icon' => 'payment-method-mybank', + ), + P24Gateway::ID => array( + 'icon' => 'payment-method-przelewy24', + ), + TrustlyGateway::ID => array( + 'icon' => 'payment-method-trustly', + ), + MultibancoGateway::ID => array( + 'icon' => 'payment-method-multibanco', + ), + PayUponInvoiceGateway::ID => array( + 'icon' => 'payment-method-multibanco', + ), + OXXO::ID => array( + 'icon' => 'payment-method-multibanco', + ), ); /** @@ -119,19 +158,20 @@ class PaymentRestEndpoint extends RestEndpoint { $gateway_settings = array(); - foreach ( $this->gateway_ids as $gateway_id ) { - if ( ! isset( $all_gateways[ $gateway_id ] ) ) { + foreach ( $this->gateways as $key => $value ) { + if ( ! isset( $all_gateways[ $key ] ) ) { continue; } - $gateway = $all_gateways[ $gateway_id ]; + $gateway = $all_gateways[ $key ]; - $gateway_settings[ $gateway_id ] = array( + $gateway_settings[ $key ] = array( 'enabled' => 'yes' === $gateway->enabled, 'title' => $gateway->get_title(), 'description' => $gateway->get_description(), 'method_title' => $gateway->get_method_title(), - 'icon' => $gateway->get_icon(), + 'id' => $this->gateways[ $key ]['id'] ?? $key, + 'icon' => $this->gateways[ $key ]['icon'] ?? '', ); } @@ -151,7 +191,7 @@ class PaymentRestEndpoint extends RestEndpoint { $request_data = $request->get_params(); - foreach ( $this->gateway_ids as $gateway_id ) { + foreach ( $this->gateways as $gateway_id ) { // Check if the REST body contains details for this gateway. if ( ! isset( $request_data[ $gateway_id ] ) || ! isset( $all_gateways[ $gateway_id ] ) ) { continue; From 560f6ed30df17e5ba8b3b24f9319371b19c62f41 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 14:53:58 +0100 Subject: [PATCH 239/298] =?UTF-8?q?=F0=9F=92=84=20Fix=20some=20minor=20CSS?= =?UTF-8?q?=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reusable-components/_settings-block.scss | 11 +++++++++- .../css/components/screens/_settings.scss | 22 ++++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss index f5ddb5ea1..8888bf2c0 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss @@ -4,7 +4,7 @@ .ppcp-r-settings-block { display: flex; flex-direction: column; - gap: var(--block-item-gap, 16px) 0; + gap: var(--block-item-gap, 16px); &.ppcp-r-settings-block__input, &.ppcp-r-settings-block__select { @@ -97,6 +97,15 @@ margin-left: 5px; } + .ppcp-r-settings-block__action { + display: flex; + align-items: center; + + .components-flex { + row-gap: 0; + } + } + + .ppcp-r-settings-block:not(.no-gap) { margin-top: var(--block-separator-gap, 32px); padding-top: var(--block-separator-gap, 32px); diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 2f536ac96..207817cb4 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -22,6 +22,7 @@ &:hover { cursor: pointer; + .ppcp-r-todo-item__inner { .ppcp-r-todo-item__description { color: $color-text-text; @@ -117,7 +118,7 @@ font-weight: 500; } - margin-top:24px; + margin-top: 24px; } } @@ -137,10 +138,6 @@ } } - &__show-all-data { - margin-left: 12px; - } - &__status-label { @include font(11, 22, 600); color: $color-gray-900; @@ -151,12 +148,16 @@ &__status-value { @include font(13, 26, 400); color: $color-text-tertiary; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &__data { display: flex; flex-direction: column; gap: 12px; + width: 100%; } &__status-toggle--toggled { @@ -168,9 +169,7 @@ &__status-row { display: flex; flex-direction: column; - * { - user-select: none; - } + strong { @include font(14, 24, 600); color: $color-gray-800; @@ -181,11 +180,6 @@ .ppcp-r-connection-status__status-toggle { line-height: 0; } - &--first { - &:hover { - cursor: pointer; - } - } } @media screen and (max-width: 767px) { @@ -195,9 +189,11 @@ } &__status-row { flex-wrap: wrap; + strong { width: 100%; } + span { word-break: break-all; } From 59a4991cf3d2481c995c69d34ad7cf52883ca005 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 14:55:13 +0100 Subject: [PATCH 240/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20a=20comp?= =?UTF-8?q?onent=20from=20settings.scss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../css/components/screens/_settings.scss | 80 +------------------ .../screens/settings/_connection-status.scss | 78 ++++++++++++++++++ 2 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 207817cb4..0b8802138 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -1,4 +1,5 @@ @import './settings/input'; +@import './settings/connection-status'; @import './settings/tab-styling'; @import './settings/tab-paylater-configurator'; @@ -122,85 +123,6 @@ } } -// 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; - } - } - - &__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; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__data { - display: flex; - flex-direction: column; - gap: 12px; - width: 100%; - } - - &__status-toggle--toggled { - .ppcp-r-connection-status__show-all-data { - transform: rotate(180deg); - } - } - - &__status-row { - display: flex; - flex-direction: column; - - 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; - } - } - - @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; diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss new file mode 100644 index 000000000..2bf1417e6 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss @@ -0,0 +1,78 @@ +// 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; + } + } + + &__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; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__data { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + } + + &__status-toggle--toggled { + .ppcp-r-connection-status__show-all-data { + transform: rotate(180deg); + } + } + + &__status-row { + display: flex; + flex-direction: column; + + 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; + } + } + + @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; + } + } + } +} From 484356dcc00d015ef76320fd0783e8b05b97ba70 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 15:02:25 +0100 Subject: [PATCH 241/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Shorten=20the=20im?= =?UTF-8?q?port=20path=20for=20styling=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/Screens/Settings/Tabs/TabStyling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js index 07f5ad4d9..f0ee60151 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js @@ -8,7 +8,7 @@ import { STYLING_LOCATIONS, STYLING_PAYMENT_METHODS, STYLING_SHAPES, -} from '../../../../data/styling/constants'; +} from '../../../../data'; import PreviewPanel from '../Components/Styling/PreviewPanel'; import SettingsPanel from '../Components/Styling/SettingsPanel'; From c16e5e4c58e17e3c800c3eb14f30bf7876140fca Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 15:03:46 +0100 Subject: [PATCH 242/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20UI=20con?= =?UTF-8?q?figuration=20to=20separate=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/configuration.js | 10 ++ .../resources/js/data/constants.js | 9 -- .../ppcp-settings/resources/js/data/index.js | 2 +- .../js/data/onboarding/configuration.js | 26 +++ .../resources/js/data/onboarding/constants.js | 21 --- .../resources/js/data/onboarding/hooks.js | 2 +- .../js/data/styling/configuration.js | 149 ++++++++++++++++++ .../resources/js/data/styling/constants.js | 144 ----------------- 8 files changed, 187 insertions(+), 176 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/configuration.js delete mode 100644 modules/ppcp-settings/resources/js/data/constants.js create mode 100644 modules/ppcp-settings/resources/js/data/onboarding/configuration.js create mode 100644 modules/ppcp-settings/resources/js/data/styling/configuration.js diff --git a/modules/ppcp-settings/resources/js/data/configuration.js b/modules/ppcp-settings/resources/js/data/configuration.js new file mode 100644 index 000000000..0f3608552 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/configuration.js @@ -0,0 +1,10 @@ +export { BUSINESS_TYPES, PRODUCT_TYPES } from './onboarding/configuration'; + +export { + STYLING_LOCATIONS, + STYLING_PAYMENT_METHODS, + STYLING_LABELS, + STYLING_COLORS, + STYLING_LAYOUTS, + STYLING_SHAPES, +} from './styling/configuration'; diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js deleted file mode 100644 index fbc8e8e11..000000000 --- a/modules/ppcp-settings/resources/js/data/constants.js +++ /dev/null @@ -1,9 +0,0 @@ -export { BUSINESS_TYPES, PRODUCT_TYPES } from './onboarding/constants'; - -export { - STYLING_LOCATIONS, - STYLING_LABELS, - STYLING_COLORS, - STYLING_LAYOUTS, - STYLING_SHAPES, -} from './styling/constants'; diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index e447ff770..959c5f187 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -15,6 +15,6 @@ export const OnboardingStoreName = Onboarding.STORE_NAME; export const CommonStoreName = Common.STORE_NAME; export const StylingStoreName = Styling.STORE_NAME; -export * from './constants'; +export * from './configuration'; addDebugTools( window.ppcpSettings, [ Onboarding, Common, Styling ] ); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/configuration.js b/modules/ppcp-settings/resources/js/data/onboarding/configuration.js new file mode 100644 index 000000000..4b31689b5 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/onboarding/configuration.js @@ -0,0 +1,26 @@ +/** + * Configuration for UI components. + * + * @file + */ + +/** + * Onboarding options for StepBusiness + * + * @type {Object} + */ +export const BUSINESS_TYPES = { + CASUAL_SELLER: 'casual_seller', + BUSINESS: 'business', +}; + +/** + * Onboarding options for StepProducts + * + * @type {Object} + */ +export const PRODUCT_TYPES = { + VIRTUAL: 'virtual', + PHYSICAL: 'physical', + SUBSCRIPTIONS: 'subscriptions', +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js index 7c35ee693..396726199 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -26,24 +26,3 @@ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/onboarding'; * @type {string} */ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/onboarding'; - -/** - * Onboarding options for StepBusiness - * - * @type {Object} - */ -export const BUSINESS_TYPES = { - CASUAL_SELLER: 'casual_seller', - BUSINESS: 'business', -}; - -/** - * Onboarding options for StepProducts - * - * @type {Object} - */ -export const PRODUCT_TYPES = { - VIRTUAL: 'virtual', - PHYSICAL: 'physical', - SUBSCRIPTIONS: 'subscriptions', -}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index c4308c0fa..2d1542f68 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -9,7 +9,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; -import { PRODUCT_TYPES } from '../constants'; +import { PRODUCT_TYPES } from './configuration'; import { STORE_NAME } from './constants'; const useTransient = ( key ) => diff --git a/modules/ppcp-settings/resources/js/data/styling/configuration.js b/modules/ppcp-settings/resources/js/data/styling/configuration.js new file mode 100644 index 000000000..861379e2d --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/styling/configuration.js @@ -0,0 +1,149 @@ +/** + * Configuration for UI components. + * + * @file + */ + +import { __ } from '@wordpress/i18n'; + +export const STYLING_LOCATIONS = { + cart: { + value: 'cart', + label: __( 'Cart', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. + description: __( + 'More details on the Cart page.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'classic-checkout': { + value: 'classic-checkout', + label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. + description: __( + 'More details on the Classic Checkout page.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'express-checkout': { + value: 'express-checkout', + label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. + description: __( + 'More details on the Express Checkout location.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'mini-cart': { + value: 'mini-cart', + label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), + // translators: %s is the URL to a documentation page. + description: __( + 'More details on the Mini Cart.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, + 'product-page': { + value: 'product-page', + label: __( 'Product Page', 'woocommerce-paypal-payments' ), + // translators: %s is the URL to a documentation page. + description: __( + 'More details on the Product Page.', + 'wooocommerce-paypal-payments' + ), + link: '#', + }, +}; + +export const STYLING_LABELS = { + paypal: { + value: 'paypal', + label: __( 'PayPal', 'woocommerce-paypal-payments' ), + }, + checkout: { + value: 'checkout', + label: __( 'Checkout', 'woocommerce-paypal-payments' ), + }, + buynow: { + value: 'buynow', + label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ), + }, + pay: { + value: 'pay', + label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_COLORS = { + gold: { + value: 'gold', + label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ), + }, + blue: { + value: 'blue', + label: __( 'Blue', 'woocommerce-paypal-payments' ), + }, + silver: { + value: 'silver', + label: __( 'Silver', 'woocommerce-paypal-payments' ), + }, + black: { + value: 'black', + label: __( 'Black', 'woocommerce-paypal-payments' ), + }, + white: { + value: 'white', + label: __( 'White', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_LAYOUTS = { + vertical: { + value: 'vertical', + label: __( 'Vertical', 'woocommerce-paypal-payments' ), + }, + horizontal: { + value: 'horizontal', + label: __( 'Horizontal', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_SHAPES = { + pill: { + value: 'pill', + label: __( 'Pill', 'woocommerce-paypal-payments' ), + }, + rect: { + value: 'rect', + label: __( 'Rectangle', 'woocommerce-paypal-payments' ), + }, +}; + +export const STYLING_PAYMENT_METHODS = { + paypal: { + value: '', + label: __( 'PayPal', 'woocommerce-paypal-payments' ), + checked: true, + disabled: true, + }, + venmo: { + value: 'venmo', + label: __( 'Venmo', 'woocommerce-paypal-payments' ), + }, + paylater: { + value: 'paylater', + label: __( 'Pay Later', 'woocommerce-paypal-payments' ), + }, + googlepay: { + value: 'googlepay', + label: __( 'Google Pay', 'woocommerce-paypal-payments' ), + }, + applepay: { + value: 'applepay', + label: __( 'Apple Pay', 'woocommerce-paypal-payments' ), + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/styling/constants.js b/modules/ppcp-settings/resources/js/data/styling/constants.js index f21ac7adf..db1082f33 100644 --- a/modules/ppcp-settings/resources/js/data/styling/constants.js +++ b/modules/ppcp-settings/resources/js/data/styling/constants.js @@ -1,5 +1,3 @@ -import { __ } from '@wordpress/i18n'; - /** * Name of the Redux store module. * @@ -28,145 +26,3 @@ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/styling'; * @type {string} */ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/styling'; - -export const STYLING_LOCATIONS = { - cart: { - value: 'cart', - label: __( 'Cart', 'woocommerce-paypal-payments' ), - // translators: %s is the URL to a documentation page. - description: __( - 'More details on the Cart page.', - 'wooocommerce-paypal-payments' - ), - link: '#', - }, - 'classic-checkout': { - value: 'classic-checkout', - label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), - // translators: %s is the URL to a documentation page. - description: __( - 'More details on the Classic Checkout page.', - 'wooocommerce-paypal-payments' - ), - link: '#', - }, - 'express-checkout': { - value: 'express-checkout', - label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), - // translators: %s is the URL to a documentation page. - description: __( - 'More details on the Express Checkout location.', - 'wooocommerce-paypal-payments' - ), - link: '#', - }, - 'mini-cart': { - value: 'mini-cart', - label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), - // translators: %s is the URL to a documentation page. - description: __( - 'More details on the Mini Cart.', - 'wooocommerce-paypal-payments' - ), - link: '#', - }, - 'product-page': { - value: 'product-page', - label: __( 'Product Page', 'woocommerce-paypal-payments' ), - // translators: %s is the URL to a documentation page. - description: __( - 'More details on the Product Page.', - 'wooocommerce-paypal-payments' - ), - link: '#', - }, -}; - -export const STYLING_LABELS = { - paypal: { - value: 'paypal', - label: __( 'PayPal', 'woocommerce-paypal-payments' ), - }, - checkout: { - value: 'checkout', - label: __( 'Checkout', 'woocommerce-paypal-payments' ), - }, - buynow: { - value: 'buynow', - label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ), - }, - pay: { - value: 'pay', - label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ), - }, -}; - -export const STYLING_COLORS = { - gold: { - value: 'gold', - label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ), - }, - blue: { - value: 'blue', - label: __( 'Blue', 'woocommerce-paypal-payments' ), - }, - silver: { - value: 'silver', - label: __( 'Silver', 'woocommerce-paypal-payments' ), - }, - black: { - value: 'black', - label: __( 'Black', 'woocommerce-paypal-payments' ), - }, - white: { - value: 'white', - label: __( 'White', 'woocommerce-paypal-payments' ), - }, -}; - -export const STYLING_LAYOUTS = { - vertical: { - value: 'vertical', - label: __( 'Vertical', 'woocommerce-paypal-payments' ), - }, - horizontal: { - value: 'horizontal', - label: __( 'Horizontal', 'woocommerce-paypal-payments' ), - }, -}; - -export const STYLING_SHAPES = { - pill: { - value: 'pill', - label: __( 'Pill', 'woocommerce-paypal-payments' ), - }, - rect: { - value: 'rect', - label: __( 'Rectangle', 'woocommerce-paypal-payments' ), - }, -}; - -export const STYLING_PAYMENT_METHODS = { - paypal: { - value: '', - label: __( 'PayPal', 'woocommerce-paypal-payments' ), - checked: true, - disabled: true, - }, - venmo: { - value: 'venmo', - label: __( 'Venmo', 'woocommerce-paypal-payments' ), - }, - paylater: { - value: 'paylater', - label: __( 'Pay Later', 'woocommerce-paypal-payments' ), - }, - googlepay: { - value: 'googlepay', - label: __( 'Google Pay', 'woocommerce-paypal-payments' ), - }, - applepay: { - value: 'applepay', - label: __( 'Apple Pay', 'woocommerce-paypal-payments' ), - }, -}; From 8755242530511672eea2d2e9fc7862fb4f339e7a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 15:05:59 +0100 Subject: [PATCH 243/298] =?UTF-8?q?=F0=9F=90=9B=20Fix=20incorrect=20import?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/styling/reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index b28c3f265..358d261ff 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -9,7 +9,7 @@ import { createReducer, createSetters } from '../utils'; import ACTION_TYPES from './action-types'; -import { STYLING_COLORS, STYLING_SHAPES } from './constants'; +import { STYLING_COLORS, STYLING_SHAPES } from './configuration'; // Store structure. From afad57da5dc659e71b90d8d1a19b29687cb2e818 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 16 Jan 2025 15:48:40 +0100 Subject: [PATCH 244/298] Add non payment gateway items --- .../resources/js/data/payment/hooks.js | 36 ++- .../resources/js/data/payment/reducer.js | 2 + .../src/Endpoint/PaymentRestEndpoint.php | 252 +++++++++++++----- 3 files changed, 222 insertions(+), 68 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js index fbae1e603..54b6d9863 100644 --- a/modules/ppcp-settings/resources/js/data/payment/hooks.js +++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js @@ -39,8 +39,20 @@ const useHooks = () => { // Persistent accessors. const sampleValue = usePersistent( 'sampleValue' ); + + // PayPal checkout. const paypal = usePersistent( 'ppcp-gateway' ); + const venmo = usePersistent( 'venmo' ); + const payLater = usePersistent( 'pay-later' ); + const creditCard = usePersistent( 'ppcp-card-button-gateway' ); + + // Online card Payments. const advancedCreditCard = usePersistent( 'ppcp-credit-card-gateway' ); + const fastlane = usePersistent( 'ppcp-axo-gateway' ); + const applePay = usePersistent( 'ppcp-applepay' ); + const googlePay = usePersistent( 'ppcp-googlepay' ); + + // Alternative payment methods. const bancontact = usePersistent( 'ppcp-bancontact' ); const blik = usePersistent( 'ppcp-blik' ); const eps = usePersistent( 'ppcp-eps' ); @@ -58,7 +70,13 @@ const useHooks = () => { sampleValue, setSampleValue, paypal, + venmo, + payLater, + creditCard, advancedCreditCard, + fastlane, + applePay, + googlePay, bancontact, blik, eps, @@ -88,8 +106,13 @@ export const useSampleValue = () => { }; export const usePaymentMethodsPayPalCheckout = () => { - const { paypal } = useHooks(); - const paymentMethodsPayPalCheckout = [ paypal ]; + const { paypal, venmo, payLater, creditCard } = useHooks(); + const paymentMethodsPayPalCheckout = [ + paypal, + venmo, + payLater, + creditCard, + ]; return { paymentMethodsPayPalCheckout, @@ -97,8 +120,13 @@ export const usePaymentMethodsPayPalCheckout = () => { }; export const usePaymentMethodsOnlineCardPayments = () => { - const { advancedCreditCard } = useHooks(); - const paymentMethodsOnlineCardPayments = [ advancedCreditCard ]; + const { advancedCreditCard, fastlane, applePay, googlePay } = useHooks(); + const paymentMethodsOnlineCardPayments = [ + advancedCreditCard, + fastlane, + applePay, + googlePay, + ]; return { paymentMethodsOnlineCardPayments, diff --git a/modules/ppcp-settings/resources/js/data/payment/reducer.js b/modules/ppcp-settings/resources/js/data/payment/reducer.js index 0ce37c18c..4894c9996 100644 --- a/modules/ppcp-settings/resources/js/data/payment/reducer.js +++ b/modules/ppcp-settings/resources/js/data/payment/reducer.js @@ -20,6 +20,8 @@ const defaultTransient = Object.freeze( { // Persistent: Values that are loaded from the DB. const defaultPersistent = Object.freeze( { 'ppcp-gateway': {}, + venmo: {}, + 'pay-later': {}, 'ppcp-card-button-gateway': {}, 'ppcp-credit-card-gateway': {}, 'ppcp-axo-gateway': {}, diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index 52f083228..19c5b7d08 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -46,67 +46,184 @@ class PaymentRestEndpoint extends RestEndpoint { /** * Field mapping for request to profile transformation. * - * @var array + * @return array[] */ - private array $gateways = array( - PayPalGateway::ID => array( - 'id' => 'paypal', - 'icon' => 'payment-method-paypal', - ), - CardButtonGateway::ID => array( - 'icon' => 'payment-method-cards', - ), + protected function gateways():array { + return array( + // PayPal checkout. + PayPalGateway::ID => array( + 'id' => 'paypal', + 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-paypal', + ), + 'venmo' => array( + 'id' => 'venmo', + 'title' => __( 'Venmo', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Offer Venmo at checkout to millions of active users.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-venmo', + ), + 'pay-later' => array( + 'id' => 'paypal_credit', + 'title' => __( 'Pay Later', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-paypal', + ), + CardButtonGateway::ID => array( + 'id' => 'credit_and_debit_card_payments', + 'title' => __( + 'Credit and debit card payments', + 'woocommerce-paypal-payments' + ), + 'description' => __( + "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-cards', + ), - CreditCardGateway::ID => array( - 'icon' => 'payment-method-advanced-cards', - ), - AxoGateway::ID => array( - 'icon' => 'payment-method-fastlane', - ), - ApplePayGateway::ID => array( - 'icon' => 'payment-method-apple-pay', - ), - GooglePayGateway::ID => array( - 'icon' => 'payment-method-google-pay', - ), + // Online card Payments. + CreditCardGateway::ID => array( + 'id' => 'advanced_credit_and_debit_card_payments', + 'title' => __( + 'Advanced Credit and Debit Card Payments', + 'woocommerce-paypal-payments' + ), + 'description' => __( + "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-advanced-cards', + ), + AxoGateway::ID => array( + '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.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-fastlane', + ), + ApplePayGateway::ID => array( + 'id' => 'apple_pay', + 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Allow customers to pay via their Apple Pay digital wallet.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-apple-pay', + ), + GooglePayGateway::ID => array( + 'id' => 'google_pay', + 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Allow customers to pay via their Google Pay digital wallet.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-google-pay', + ), - BancontactGateway::ID => array( - 'icon' => 'payment-method-bancontact', - ), - BlikGateway::ID => array( - 'icon' => 'payment-method-blik', - ), - EPSGateway::ID => array( - 'icon' => 'payment-method-eps', - ), - IDealGateway::ID => array( - 'icon' => 'payment-method-ideal', - ), - MyBankGateway::ID => array( - 'icon' => 'payment-method-mybank', - ), - P24Gateway::ID => array( - 'icon' => 'payment-method-przelewy24', - ), - TrustlyGateway::ID => array( - 'icon' => 'payment-method-trustly', - ), - MultibancoGateway::ID => array( - 'icon' => 'payment-method-multibanco', - ), - PayUponInvoiceGateway::ID => array( - 'icon' => 'payment-method-multibanco', - ), - OXXO::ID => array( - 'icon' => 'payment-method-multibanco', - ), - ); - - /** - * Constructor. - */ - public function __construct() { - // Todo: Add DI instead of using `WC()->payment_gateways->payment_gateways()`? + // Alternative payment methods. + BancontactGateway::ID => array( + 'id' => 'bancontact', + 'title' => __( 'Bancontact', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-bancontact', + ), + BlikGateway::ID => array( + 'id' => 'blik', + 'title' => __( 'BLIK', 'woocommerce-paypal-payments' ), + 'description' => __( + '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', + ), + EPSGateway::ID => array( + 'id' => 'eps', + 'title' => __( 'eps', 'woocommerce-paypal-payments' ), + 'description' => __( + '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', + ), + IDealGateway::ID => array( + 'id' => 'ideal', + 'title' => __( 'iDEAL', 'woocommerce-paypal-payments' ), + 'description' => __( + 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-ideal', + ), + MyBankGateway::ID => array( + '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', + ), + P24Gateway::ID => array( + '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', + ), + TrustlyGateway::ID => array( + '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', + ), + MultibancoGateway::ID => array( + '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', + ), + PayUponInvoiceGateway::ID => array( + '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' => '', + ), + OXXO::ID => array( + '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' => '', + ), + ); } /** @@ -153,13 +270,20 @@ class PaymentRestEndpoint extends RestEndpoint { * @return WP_REST_Response The current payment methods details. */ public function get_details() : WP_REST_Response { - // Todo: Change this to DI? $all_gateways = WC()->payment_gateways->payment_gateways(); $gateway_settings = array(); - foreach ( $this->gateways as $key => $value ) { + foreach ( $this->gateways() as $key => $value ) { if ( ! isset( $all_gateways[ $key ] ) ) { + $gateway_settings[ $key ] = array( + 'id' => $this->gateways()[ $key ]['id'] ?? '', + 'title' => $this->gateways()[ $key ]['title'] ?? '', + 'description' => $this->gateways()[ $key ]['description'] ?? '', + 'enabled' => false, + 'icon' => $this->gateways()[ $key ]['icon'] ?? '', + ); + continue; } @@ -167,11 +291,11 @@ class PaymentRestEndpoint extends RestEndpoint { $gateway_settings[ $key ] = array( 'enabled' => 'yes' === $gateway->enabled, - 'title' => $gateway->get_title(), - 'description' => $gateway->get_description(), + 'title' => $this->gateways()[ $key ]['title'] ?? $gateway->get_title(), + 'description' => $this->gateways()[ $key ]['description'] ?? $gateway->get_description(), 'method_title' => $gateway->get_method_title(), - 'id' => $this->gateways[ $key ]['id'] ?? $key, - 'icon' => $this->gateways[ $key ]['icon'] ?? '', + 'id' => $this->gateways()[ $key ]['id'] ?? $key, + 'icon' => $this->gateways()[ $key ]['icon'] ?? '', ); } From 58777d6f9137026b0a305c39cfdf24ce478ae7dd Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 16 Jan 2025 16:11:15 +0100 Subject: [PATCH 245/298] Fix update details endpoint --- .../ppcp-settings/src/Endpoint/PaymentRestEndpoint.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index 19c5b7d08..ea692242a 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -310,19 +310,18 @@ class PaymentRestEndpoint extends RestEndpoint { * @return WP_REST_Response The updated payment methods details. */ public function update_details( WP_REST_Request $request ) : WP_REST_Response { - // Todo: Change this to DI? $all_gateways = WC()->payment_gateways->payment_gateways(); $request_data = $request->get_params(); - foreach ( $this->gateways as $gateway_id ) { + foreach ( $this->gateways() as $key => $value) { // Check if the REST body contains details for this gateway. - if ( ! isset( $request_data[ $gateway_id ] ) || ! isset( $all_gateways[ $gateway_id ] ) ) { + if ( ! isset( $request_data[ $key ] ) || ! isset( $all_gateways[ $key ] ) ) { continue; } - $gateway = $all_gateways[ $gateway_id ]; - $new_data = $request_data[ $gateway_id ]; + $gateway = $all_gateways[ $key ]; + $new_data = $request_data[ $key ]; if ( isset( $new_data['enabled'] ) ) { $gateway->update_option( 'enabled', $new_data['enabled'] ? 'yes' : 'no' ); From bd14ea441ec2d38a6abed9861a5f2c5e9a6bb977 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 17:16:37 +0100 Subject: [PATCH 246/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20dummy=20hoo?= =?UTF-8?q?k=20into=20redux=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Styling/LocationSelector.js | 8 +- .../Components/Styling/SettingsPanel.js | 21 ++-- .../Screens/Settings/Tabs/TabStyling.js | 102 ------------------ .../resources/js/data/styling/hooks.js | 98 ++++++++++++++++- 4 files changed, 111 insertions(+), 118 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js index 01745fbdc..35c3a6efd 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js @@ -1,15 +1,13 @@ -import { SelectControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -// Dummy hook. -import { useStylingProps } from '../../Tabs/TabStyling'; - +import { StylingHooks } from '../../../../../data'; import { Description } from '../../../../ReusableComponents/SettingsBlocks'; import StylingSection from './StylingSection'; import StylingSectionWithSelect from './StylingSectionWithSelect'; const LocationSelector = ( { location, setLocation } ) => { - const { locationChoices, locationDetails } = useStylingProps( location ); + const { locationChoices, locationDetails } = + StylingHooks.useStylingProps( location ); const { description, link } = locationDetails || {}; const locationDescription = sprintf( description, link ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js index 40d418276..4826632a9 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -1,15 +1,13 @@ import { __ } from '@wordpress/i18n'; -// Dummy hook. -import { useStylingLocation, useStylingProps } from '../../Tabs/TabStyling'; - +import { StylingHooks } from '../../../../../data'; import LocationSelector from './LocationSelector'; import StylingSectionWithSelect from './StylingSectionWithSelect'; import StylingSectionWithCheckboxes from './StylingSectionWithCheckboxes'; import StylingSectionWithRadiobuttons from './StylingSectionWithRadiobuttons'; const SettingsPanel = () => { - const { location, setLocation } = useStylingLocation(); + const { location, setLocation } = StylingHooks.useStylingLocation(); return (

@@ -33,7 +31,7 @@ export default SettingsPanel; const SectionPaymentMethods = ( { location } ) => { const { paymentMethods, setPaymentMethods, paymentMethodChoices } = - useStylingProps( location ); + StylingHooks.useStylingProps( location ); return ( { const SectionButtonLayout = ( { location } ) => { const { supportsLayout, layout, setLayout, layoutChoices } = - useStylingProps( location ); + StylingHooks.useStylingProps( location ); if ( ! supportsLayout ) { return null; @@ -66,7 +64,8 @@ const SectionButtonLayout = ( { location } ) => { }; const SectionButtonShape = ( { location } ) => { - const { shape, setShape, shapeChoices } = useStylingProps( location ); + const { shape, setShape, shapeChoices } = + StylingHooks.useStylingProps( location ); return ( { }; const SectionButtonLabel = ( { location } ) => { - const { label, setLabel, labelChoices } = useStylingProps( location ); + const { label, setLabel, labelChoices } = + StylingHooks.useStylingProps( location ); return ( { }; const SectionButtonColor = ( { location } ) => { - const { color, setColor, colorChoices } = useStylingProps( location ); + const { color, setColor, colorChoices } = + StylingHooks.useStylingProps( location ); return ( { const SectionButtonTagline = ( { location } ) => { const { supportsTagline, tagline, setTagline, taglineChoices } = - useStylingProps( location ); + StylingHooks.useStylingProps( location ); if ( ! supportsTagline ) { return null; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js index f0ee60151..c1eb5eb19 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js @@ -1,15 +1,3 @@ -import { __ } from '@wordpress/i18n'; -import { useCallback, useState } from '@wordpress/element'; - -import { - STYLING_COLORS, - STYLING_LABELS, - STYLING_LAYOUTS, - STYLING_LOCATIONS, - STYLING_PAYMENT_METHODS, - STYLING_SHAPES, -} from '../../../../data'; - import PreviewPanel from '../Components/Styling/PreviewPanel'; import SettingsPanel from '../Components/Styling/SettingsPanel'; @@ -21,93 +9,3 @@ const TabStyling = () => ( ); export default TabStyling; - -// ---------------------------------------------------------------------------- - -// Temporary "hook" to extract logic before moving it to the Redux store. -export const useStylingLocation = () => { - const [ location, setLocation ] = useState( 'cart' ); - - return { location, setLocation }; -}; - -export const useStylingProps = ( location ) => { - const defaultStyle = { - paymentMethods: [], - color: 'gold', - shape: 'rect', - label: 'paypal', - layout: 'vertical', - tagline: false, - }; - - const [ styles, setStyles ] = useState( { - cart: { ...defaultStyle, label: 'checkout' }, - 'classic-checkout': { ...defaultStyle }, - 'express-checkout': { ...defaultStyle, label: 'pay' }, - 'mini-cart': { ...defaultStyle, label: 'pay' }, - 'product-page': { ...defaultStyle }, - } ); - - const getLocationStyle = useCallback( - ( prop ) => styles[ location ]?.[ prop ], - [ location, styles ] - ); - - const setLocationStyle = useCallback( - ( prop, value ) => { - setStyles( ( prevState ) => ( { - ...prevState, - [ location ]: { - ...prevState[ location ], - [ prop ]: value, - }, - } ) ); - }, - [ location ] - ); - - return { - // Location (drop down). - locationChoices: Object.values( STYLING_LOCATIONS ), - locationDetails: STYLING_LOCATIONS[ location ], - - // Payment methods (checkboxes). - paymentMethodChoices: Object.values( STYLING_PAYMENT_METHODS ), - paymentMethods: getLocationStyle( 'paymentMethods' ), - setPaymentMethods: ( methods ) => - setLocationStyle( 'paymentMethods', methods ), - - // Color (dropdown). - colorChoices: Object.values( STYLING_COLORS ), - color: getLocationStyle( 'color' ), - setColor: ( color ) => setLocationStyle( 'color', color ), - - // Shape (radio). - shapeChoices: Object.values( STYLING_SHAPES ), - shape: getLocationStyle( 'shape' ), - setShape: ( shape ) => setLocationStyle( 'shape', shape ), - - // Label (dropdown). - labelChoices: Object.values( STYLING_LABELS ), - label: getLocationStyle( 'label' ), - setLabel: ( label ) => setLocationStyle( 'label', label ), - - // Layout (radio). - layoutChoices: Object.values( STYLING_LAYOUTS ), - supportsLayout: true, - layout: getLocationStyle( 'layout' ), - setLayout: ( layout ) => setLocationStyle( 'layout', layout ), - - // Tagline (checkbox). - taglineChoices: [ - { - value: 'tagline', - label: __( 'Enable Tagline', 'woocommerce-paypal-payments' ), - }, - ], - supportsTagline: true, - tagline: getLocationStyle( 'tagline' ), - setTagline: ( tagline ) => setLocationStyle( 'tagline', tagline ), - }; -}; diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index de08f1124..45280de83 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -7,9 +7,19 @@ * @file */ +import { __ } from '@wordpress/i18n'; +import { useCallback, useState } from '@wordpress/element'; // Temporary import { useSelect, useDispatch } from '@wordpress/data'; import { STORE_NAME } from './constants'; +import { + STYLING_COLORS, + STYLING_LABELS, + STYLING_LAYOUTS, + STYLING_LOCATIONS, + STYLING_PAYMENT_METHODS, + STYLING_SHAPES, +} from './configuration'; const useTransient = ( key ) => useSelect( @@ -42,7 +52,93 @@ const useHooks = () => { }; }; -export const useState = () => { +export const useStore = () => { const { persist, isReady } = useHooks(); return { persist, isReady }; }; + +export const useStylingLocation = () => { + const [ location, setLocation ] = useState( 'cart' ); + return { location, setLocation }; +}; + +export const useStylingProps = ( location ) => { + const defaultStyle = { + paymentMethods: [], + color: 'gold', + shape: 'rect', + label: 'paypal', + layout: 'vertical', + tagline: false, + }; + + const [ styles, setStyles ] = useState( { + cart: { ...defaultStyle, label: 'checkout' }, + 'classic-checkout': { ...defaultStyle }, + 'express-checkout': { ...defaultStyle, label: 'pay' }, + 'mini-cart': { ...defaultStyle, label: 'pay' }, + 'product-page': { ...defaultStyle }, + } ); + + const getLocationStyle = useCallback( + ( prop ) => styles[ location ]?.[ prop ], + [ location, styles ] + ); + + const setLocationStyle = useCallback( + ( prop, value ) => { + setStyles( ( prevState ) => ( { + ...prevState, + [ location ]: { + ...prevState[ location ], + [ prop ]: value, + }, + } ) ); + }, + [ location ] + ); + + return { + // Location (drop down). + locationChoices: Object.values( STYLING_LOCATIONS ), + locationDetails: STYLING_LOCATIONS[ location ], + + // Payment methods (checkboxes). + paymentMethodChoices: Object.values( STYLING_PAYMENT_METHODS ), + paymentMethods: getLocationStyle( 'paymentMethods' ), + setPaymentMethods: ( methods ) => + setLocationStyle( 'paymentMethods', methods ), + + // Color (dropdown). + colorChoices: Object.values( STYLING_COLORS ), + color: getLocationStyle( 'color' ), + setColor: ( color ) => setLocationStyle( 'color', color ), + + // Shape (radio). + shapeChoices: Object.values( STYLING_SHAPES ), + shape: getLocationStyle( 'shape' ), + setShape: ( shape ) => setLocationStyle( 'shape', shape ), + + // Label (dropdown). + labelChoices: Object.values( STYLING_LABELS ), + label: getLocationStyle( 'label' ), + setLabel: ( label ) => setLocationStyle( 'label', label ), + + // Layout (radio). + layoutChoices: Object.values( STYLING_LAYOUTS ), + supportsLayout: true, + layout: getLocationStyle( 'layout' ), + setLayout: ( layout ) => setLocationStyle( 'layout', layout ), + + // Tagline (checkbox). + taglineChoices: [ + { + value: 'tagline', + label: __( 'Enable Tagline', 'woocommerce-paypal-payments' ), + }, + ], + supportsTagline: true, + tagline: getLocationStyle( 'tagline' ), + setTagline: ( tagline ) => setLocationStyle( 'tagline', tagline ), + }; +}; From f07d9bad82e7b0afcacf0cd5af47a6fe31df9411 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 18:20:03 +0100 Subject: [PATCH 247/298] =?UTF-8?q?=E2=9C=A8=20Add=20gemeric=20hook-genera?= =?UTF-8?q?tor=20for=20data=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/hooks.js | 20 ++---- .../ppcp-settings/resources/js/data/utils.js | 61 +++++++++++++++++++ 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 45280de83..6e6a502ab 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n'; import { useCallback, useState } from '@wordpress/element'; // Temporary import { useSelect, useDispatch } from '@wordpress/data'; +import { createHooksForStore } from '../utils'; import { STORE_NAME } from './constants'; import { STYLING_COLORS, @@ -21,28 +22,17 @@ import { STYLING_SHAPES, } from './configuration'; -const useTransient = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).transientData()?.[ key ], - [ key ] - ); - -const usePersistent = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).persistentData()?.[ key ], - [ key ] - ); - const useHooks = () => { - const { persist, setShape } = useDispatch( STORE_NAME ); + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + const { persist } = useDispatch( STORE_NAME ); // Read-only flags and derived state. // Transient accessors. - const isReady = useTransient( 'isReady' ); + const [ isReady ] = useTransient( 'isReady' ); // Persistent accessors. - const shape = usePersistent( 'shape' ); + const [ shape, setShape ] = usePersistent( 'shape' ); return { persist, diff --git a/modules/ppcp-settings/resources/js/data/utils.js b/modules/ppcp-settings/resources/js/data/utils.js index 45c652862..35d7bbbf9 100644 --- a/modules/ppcp-settings/resources/js/data/utils.js +++ b/modules/ppcp-settings/resources/js/data/utils.js @@ -1,3 +1,6 @@ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; + /** * Updates an object with new values, filtering based on allowed keys. * @@ -13,6 +16,10 @@ const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( { ...Object.keys( newValues ).reduce( ( acc, key ) => { if ( key in allowedKeys ) { acc[ key ] = newValues[ key ]; + } else { + console.warn( + `Ignoring unknown key "${ key }" - to use it, add it to the initial store properties in the reducer.` + ); } return acc; }, {} ), @@ -73,3 +80,57 @@ export const createReducer = ( return state; }; }; + +/** + * Returns an object with two hooks: + * - useTransient( prop ) + * - usePersistent( prop ) + * + * Both hooks have a similar syntax to the native "useState( prop )" hook, but provide access to + * a transient or persistent property in the relevant Redux store. + * + * Sample: + * + * const { useTransient } = createHooksForStore( STORE_NAME ); + * const [ isReady, setIsReady ] = useTransient( 'isReady' ); + * + * @param {string} storeName Store name. + * @return {{useTransient, usePersistent}} Store hooks. + */ +export const createHooksForStore = ( storeName ) => { + const createHook = ( selector, dispatcher ) => ( key ) => { + const value = useSelect( + ( select ) => { + const store = select( storeName ); + if ( ! store?.[ selector ] ) { + throw new Error( + `Please create the selector "${ selector }" for store "${ storeName }"` + ); + } + return store[ selector ]()?.[ key ]; + }, + [ key ] + ); + + const actions = useDispatch( storeName ); + + const setValue = useCallback( + ( newValue ) => { + if ( ! actions?.[ dispatcher ] ) { + throw new Error( + `Please create the action "${ dispatcher }" for store "${ storeName }"` + ); + } + actions[ dispatcher ]( key, newValue ); + }, + [ actions, key ] + ); + + return [ value, setValue ]; + }; + + return { + useTransient: createHook( 'transientData', 'setTransient' ), + usePersistent: createHook( 'persistentData', 'setPersistent' ), + }; +}; From 96128ee3e4e7280383b0fbeffe0f7068f75886f7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 18:25:54 +0100 Subject: [PATCH 248/298] =?UTF-8?q?=E2=9C=A8=20Create=20the=20generic=20se?= =?UTF-8?q?tter-actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/actions.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/styling/actions.js b/modules/ppcp-settings/resources/js/data/styling/actions.js index 25cf6f04b..435c4f2d9 100644 --- a/modules/ppcp-settings/resources/js/data/styling/actions.js +++ b/modules/ppcp-settings/resources/js/data/styling/actions.js @@ -36,6 +36,30 @@ export const hydrate = ( payload ) => ( { payload, } ); +/** + * Generic transient-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setTransient = ( prop, value ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { [ prop ]: value }, +} ); + +/** + * Generic persistent-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setPersistent = ( prop, value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { [ prop ]: value }, +} ); + /** * Transient. Marks the store as "ready", i.e., fully initialized. * From 209b7a7c885a8d3b6513f2172388ebbe09019341 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 18:27:20 +0100 Subject: [PATCH 249/298] =?UTF-8?q?=F0=9F=94=A5=20Clean=20up=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/actions.js | 22 ------------------- .../resources/js/data/styling/hooks.js | 4 +--- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/actions.js b/modules/ppcp-settings/resources/js/data/styling/actions.js index 435c4f2d9..095a54c91 100644 --- a/modules/ppcp-settings/resources/js/data/styling/actions.js +++ b/modules/ppcp-settings/resources/js/data/styling/actions.js @@ -60,28 +60,6 @@ export const setPersistent = ( prop, value ) => ( { payload: { [ prop ]: value }, } ); -/** - * Transient. Marks the store as "ready", i.e., fully initialized. - * - * @param {boolean} isReady - * @return {Action} The action. - */ -export const setIsReady = ( isReady ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, -} ); - -/** - * Persistent. - * - * @param {string} shape - * @return {Action} The action. - */ -export const setShape = ( shape ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { shape }, -} ); - /** * Side effect. Triggers the persistence of store data to the server. * diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 6e6a502ab..201136695 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n'; import { useCallback, useState } from '@wordpress/element'; // Temporary -import { useSelect, useDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { createHooksForStore } from '../utils'; import { STORE_NAME } from './constants'; @@ -37,8 +37,6 @@ const useHooks = () => { return { persist, isReady, - shape, - setShape, }; }; From 0673e5d813b6f7314fd47ee17e1761c662365142 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 18:28:11 +0100 Subject: [PATCH 250/298] =?UTF-8?q?=E2=9C=A8=20Move=20prop=20=E2=80=9Cloca?= =?UTF-8?q?tion=E2=80=9D=20into=20Redux=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/styling/hooks.js | 5 ++++- modules/ppcp-settings/resources/js/data/styling/reducer.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 201136695..e8863b3e3 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -30,6 +30,7 @@ const useHooks = () => { // Transient accessors. const [ isReady ] = useTransient( 'isReady' ); + const [ location, setLocation ] = useTransient( 'location' ); // Persistent accessors. const [ shape, setShape ] = usePersistent( 'shape' ); @@ -37,6 +38,8 @@ const useHooks = () => { return { persist, isReady, + location, + setLocation, }; }; @@ -46,7 +49,7 @@ export const useStore = () => { }; export const useStylingLocation = () => { - const [ location, setLocation ] = useState( 'cart' ); + const { location, setLocation } = useHooks(); return { location, setLocation }; }; diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index 358d261ff..ec432081f 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -16,6 +16,7 @@ import { STYLING_COLORS, STYLING_SHAPES } from './configuration'; // Transient: Values that are _not_ saved to the DB (like app lifecycle-flags). const defaultTransient = Object.freeze( { isReady: false, + location: 'cart', // Which location is selected in the Styling tab. } ); // Persistent: Values that are loaded from the DB. From 2112769de9a7a0bfcc20549d4b0400fd6d9ae71f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 19:29:16 +0100 Subject: [PATCH 251/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Restructure=20Styl?= =?UTF-8?q?ing=20component=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Styling/Content/ButtonColor.js | 21 +++ .../Components/Styling/Content/ButtonLabel.js | 21 +++ .../Styling/Content/ButtonLayout.js | 25 ++++ .../Components/Styling/Content/ButtonShape.js | 21 +++ .../Styling/{ => Content}/LocationSelector.js | 13 +- .../Styling/Content/PaymentMethods.js | 21 +++ .../Components/Styling/Content/TagLine.js | 25 ++++ .../Components/Styling/Content/index.js | 7 + .../Styling/{ => Layout}/StylingSection.js | 4 +- .../StylingSectionWithCheckboxes.js | 4 +- .../StylingSectionWithRadiobuttons.js | 2 +- .../{ => Layout}/StylingSectionWithSelect.js | 0 .../Components/Styling/Layout/index.js | 4 + .../Components/Styling/SettingsPanel.js | 127 +++--------------- 14 files changed, 171 insertions(+), 124 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js rename modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/{ => Content}/LocationSelector.js (72%) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js rename modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/{ => Layout}/StylingSection.js (76%) rename modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/{ => Layout}/StylingSectionWithCheckboxes.js (83%) rename modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/{ => Layout}/StylingSectionWithRadiobuttons.js (92%) rename modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/{ => Layout}/StylingSectionWithSelect.js (100%) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js new file mode 100644 index 000000000..acadc3943 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js @@ -0,0 +1,21 @@ +import { __ } from '@wordpress/i18n'; + +import { StylingHooks } from '../../../../../../data'; +import { SelectStylingSection } from '../Layout'; + +const SectionButtonColor = ( { location } ) => { + const { color, setColor, colorChoices } = + StylingHooks.useStylingProps( location ); + + return ( + + ); +}; + +export default SectionButtonColor; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js new file mode 100644 index 000000000..a0763a049 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js @@ -0,0 +1,21 @@ +import { __ } from '@wordpress/i18n'; + +import { StylingHooks } from '../../../../../../data'; +import { SelectStylingSection } from '../Layout'; + +const SectionButtonLabel = ( { location } ) => { + const { label, setLabel, labelChoices } = + StylingHooks.useStylingProps( location ); + + return ( + + ); +}; + +export default SectionButtonLabel; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js new file mode 100644 index 000000000..0b6407ef1 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js @@ -0,0 +1,25 @@ +import { __ } from '@wordpress/i18n'; + +import { StylingHooks } from '../../../../../../data'; +import { RadiobuttonStylingSection } from '../Layout'; + +const SectionButtonLayout = ( { location } ) => { + const { supportsLayout, layout, setLayout, layoutChoices } = + StylingHooks.useStylingProps( location ); + + if ( ! supportsLayout ) { + return null; + } + + return ( + + ); +}; + +export default SectionButtonLayout; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js new file mode 100644 index 000000000..68eaef1dc --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js @@ -0,0 +1,21 @@ +import { __ } from '@wordpress/i18n'; + +import { StylingHooks } from '../../../../../../data'; +import { RadiobuttonStylingSection } from '../Layout'; + +const SectionButtonShape = ( { location } ) => { + const { shape, setShape, shapeChoices } = + StylingHooks.useStylingProps( location ); + + return ( + + ); +}; + +export default SectionButtonShape; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js similarity index 72% rename from modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js index 35c3a6efd..2fd21caa0 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/LocationSelector.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js @@ -1,9 +1,8 @@ import { __, sprintf } from '@wordpress/i18n'; -import { StylingHooks } from '../../../../../data'; -import { Description } from '../../../../ReusableComponents/SettingsBlocks'; -import StylingSection from './StylingSection'; -import StylingSectionWithSelect from './StylingSectionWithSelect'; +import { StylingHooks } from '../../../../../../data'; +import { Description } from '../../../../../ReusableComponents/SettingsBlocks'; +import { SelectStylingSection, StylingSection } from '../Layout'; const LocationSelector = ( { location, setLocation } ) => { const { locationChoices, locationDetails } = @@ -22,9 +21,9 @@ const LocationSelector = ( { location, setLocation } ) => { 'woocommerce-paypal-payments' ) } > - { { locationDescription } - + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js new file mode 100644 index 000000000..e379f876b --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js @@ -0,0 +1,21 @@ +import { __ } from '@wordpress/i18n'; + +import { StylingHooks } from '../../../../../../data'; +import { CheckboxStylingSection } from '../Layout'; + +const SectionPaymentMethods = ( { location } ) => { + const { paymentMethods, setPaymentMethods, paymentMethodChoices } = + StylingHooks.useStylingProps( location ); + + return ( + + ); +}; + +export default SectionPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js new file mode 100644 index 000000000..66fa4cd1f --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js @@ -0,0 +1,25 @@ +import { __ } from '@wordpress/i18n'; + +import { StylingHooks } from '../../../../../../data'; +import { CheckboxStylingSection } from '../Layout'; + +const SectionTagline = ( { location } ) => { + const { supportsTagline, tagline, setTagline, taglineChoices } = + StylingHooks.useStylingProps( location ); + + if ( ! supportsTagline ) { + return null; + } + + return ( + + ); +}; + +export default SectionTagline; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js new file mode 100644 index 000000000..27a4e3d56 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js @@ -0,0 +1,7 @@ +export { default as LocationSelector } from './LocationSelector'; +export { default as ButtonColor } from './ButtonColor'; +export { default as ButtonLabel } from './ButtonLabel'; +export { default as ButtonLayout } from './ButtonLayout'; +export { default as ButtonShape } from './ButtonShape'; +export { default as PaymentMethods } from './PaymentMethods'; +export { default as TagLine } from './TagLine'; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js similarity index 76% rename from modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js index 51290831a..e528361dc 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSection.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js @@ -1,9 +1,9 @@ -import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock'; +import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock'; import { Description, Header, Title, -} from '../../../../ReusableComponents/SettingsBlocks'; +} from '../../../../../ReusableComponents/SettingsBlocks'; const StylingSection = ( { title, diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithCheckboxes.js similarity index 83% rename from modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithCheckboxes.js index cc0b259d6..d3ca92db4 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithCheckboxes.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithCheckboxes.js @@ -1,7 +1,7 @@ import classNames from 'classnames'; -import { CheckboxGroup } from '../../../../ReusableComponents/Fields'; -import HStack from '../../../../ReusableComponents/HStack'; +import { CheckboxGroup } from '../../../../../ReusableComponents/Fields'; +import HStack from '../../../../../ReusableComponents/HStack'; import StylingSection from './StylingSection'; const StylingSectionWithCheckboxes = ( { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithRadiobuttons.js similarity index 92% rename from modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithRadiobuttons.js index 4bc33326f..337e2b30f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithRadiobuttons.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithRadiobuttons.js @@ -1,7 +1,7 @@ import { RadioControl } from '@wordpress/components'; import classNames from 'classnames'; -import HStack from '../../../../ReusableComponents/HStack'; +import HStack from '../../../../../ReusableComponents/HStack'; import StylingSection from './StylingSection'; const StylingSectionWithRadiobuttons = ( { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithSelect.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js similarity index 100% rename from modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/StylingSectionWithSelect.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js new file mode 100644 index 000000000..ea856552d --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js @@ -0,0 +1,4 @@ +export { default as StylingSection } from './StylingSection'; +export { default as CheckboxStylingSection } from './StylingSectionWithCheckboxes'; +export { default as RadiobuttonStylingSection } from './StylingSectionWithRadiobuttons'; +export { default as SelectStylingSection } from './StylingSectionWithSelect'; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js index 4826632a9..ad65185cf 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -1,10 +1,13 @@ -import { __ } from '@wordpress/i18n'; - import { StylingHooks } from '../../../../../data'; -import LocationSelector from './LocationSelector'; -import StylingSectionWithSelect from './StylingSectionWithSelect'; -import StylingSectionWithCheckboxes from './StylingSectionWithCheckboxes'; -import StylingSectionWithRadiobuttons from './StylingSectionWithRadiobuttons'; +import { + LocationSelector, + PaymentMethods, + ButtonLayout, + ButtonShape, + ButtonLabel, + ButtonColor, + TagLine, +} from './Content'; const SettingsPanel = () => { const { location, setLocation } = StylingHooks.useStylingLocation(); @@ -15,114 +18,14 @@ const SettingsPanel = () => { location={ location } setLocation={ setLocation } /> - - - - - - + + + + + +
); }; export default SettingsPanel; - -// ----- - -const SectionPaymentMethods = ( { location } ) => { - const { paymentMethods, setPaymentMethods, paymentMethodChoices } = - StylingHooks.useStylingProps( location ); - - return ( - - ); -}; - -const SectionButtonLayout = ( { location } ) => { - const { supportsLayout, layout, setLayout, layoutChoices } = - StylingHooks.useStylingProps( location ); - - if ( ! supportsLayout ) { - return null; - } - - return ( - - ); -}; - -const SectionButtonShape = ( { location } ) => { - const { shape, setShape, shapeChoices } = - StylingHooks.useStylingProps( location ); - - return ( - - ); -}; - -const SectionButtonLabel = ( { location } ) => { - const { label, setLabel, labelChoices } = - StylingHooks.useStylingProps( location ); - - return ( - - ); -}; - -const SectionButtonColor = ( { location } ) => { - const { color, setColor, colorChoices } = - StylingHooks.useStylingProps( location ); - - return ( - - ); -}; - -const SectionButtonTagline = ( { location } ) => { - const { supportsTagline, tagline, setTagline, taglineChoices } = - StylingHooks.useStylingProps( location ); - - if ( ! supportsTagline ) { - return null; - } - - return ( - - ); -}; From f7f140875da7ae6f16b1905c6f47232fb11a6af8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 19:30:01 +0100 Subject: [PATCH 252/298] =?UTF-8?q?=F0=9F=92=84=20Improve=20UX=20and=20SCS?= =?UTF-8?q?S=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/settings/_tab-styling.scss | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index d88fe64f7..cc60e1619 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -1,18 +1,30 @@ -$width-settings-panel: 422px; - .ppcp-r-styling { --block-item-gap: 0; --block-separator-gap: 24px; --block-header-gap: 18px; + --panel-width: 422px; + --sticky-offset-top: 92px; // 32px admin-bar + 60px TopNavigation height + --preview-height-reduction: 236px; // 32px admin-bar + 60px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin display: flex; border: 1px solid var(--color-separators); border-radius: 8px; - overflow: hidden; .ppcp-r-settings-block { &.header-section { - margin-bottom: 22px + margin-bottom: 6px + } + + &.location-selector { + position: sticky; + top: var(--sticky-offset-top); + background: var(--ppcp-color-app-bg); + border-bottom: 1px solid var(--color-gray-200); + box-shadow: 0 5px 10px 5px var(--ppcp-color-app-bg); + z-index: 5; + padding-top: 16px; + padding-bottom: var(--block-separator-gap); + margin-bottom: -29px; } // Select-fields have a smaller gap between the header and input field. @@ -28,7 +40,7 @@ $width-settings-panel: 422px; /* The settings-panel (left side) */ .settings-panel { - width: $width-settings-panel; + width: var(--panel-width); padding: 48px; .ppcp-r-styling__section { @@ -52,14 +64,19 @@ $width-settings-panel: 422px; /* The preview area (right side) */ .preview-panel { - width: calc(100% - $width-settings-panel); + width: calc(100% - var(--panel-width)); background-color: var(--color-preview-background); - display: flex; - align-items: center; + z-index: 0; .preview-panel-inner { + position: sticky; + display: flex; + flex-direction: column; + justify-content: center; width: 100%; padding: 24px; + height: calc(100vh - var(--preview-height-reduction)); + top: var(--sticky-offset-top); } } } From 34cc8f8fab66ac1274c50bd631e801792f87b6ae Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 19:30:54 +0100 Subject: [PATCH 253/298] =?UTF-8?q?=F0=9F=9A=A7=20=20Prepare=20Redux=20int?= =?UTF-8?q?egration=20in=20hooks-file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/hooks.js | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index e8863b3e3..7db797d3a 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -33,13 +33,51 @@ const useHooks = () => { const [ location, setLocation ] = useTransient( 'location' ); // Persistent accessors. - const [ shape, setShape ] = usePersistent( 'shape' ); + // TODO - this is a dummy implementation. + const defaultStyle = { + paymentMethods: [], + color: 'gold', + shape: 'rect', + label: 'paypal', + layout: 'vertical', + tagline: false, + }; + + // This data-struct is already present in the Redux store via persistentData, e.g. "persistentData.cart.label" + const [ styles, setStyles ] = useState( { + cart: { ...defaultStyle, label: 'checkout' }, + 'classic-checkout': { ...defaultStyle }, + 'express-checkout': { ...defaultStyle, label: 'pay' }, + 'mini-cart': { ...defaultStyle, label: 'pay' }, + 'product-page': { ...defaultStyle }, + } ); + + const getLocationProp = useCallback( + ( prop ) => styles[ location ]?.[ prop ], + [ location, styles ] + ); + + const setLocationProp = useCallback( + ( prop, value ) => { + setStyles( ( prevState ) => ( { + ...prevState, + [ location ]: { + ...prevState[ location ], + [ prop ]: value, + }, + } ) ); + }, + [ location ] + ); + // end of dummy implementation return { persist, isReady, location, setLocation, + getLocationProp, + setLocationProp, }; }; @@ -54,40 +92,7 @@ export const useStylingLocation = () => { }; export const useStylingProps = ( location ) => { - const defaultStyle = { - paymentMethods: [], - color: 'gold', - shape: 'rect', - label: 'paypal', - layout: 'vertical', - tagline: false, - }; - - const [ styles, setStyles ] = useState( { - cart: { ...defaultStyle, label: 'checkout' }, - 'classic-checkout': { ...defaultStyle }, - 'express-checkout': { ...defaultStyle, label: 'pay' }, - 'mini-cart': { ...defaultStyle, label: 'pay' }, - 'product-page': { ...defaultStyle }, - } ); - - const getLocationStyle = useCallback( - ( prop ) => styles[ location ]?.[ prop ], - [ location, styles ] - ); - - const setLocationStyle = useCallback( - ( prop, value ) => { - setStyles( ( prevState ) => ( { - ...prevState, - [ location ]: { - ...prevState[ location ], - [ prop ]: value, - }, - } ) ); - }, - [ location ] - ); + const { getLocationProp, setLocationProp } = useHooks(); return { // Location (drop down). @@ -96,30 +101,30 @@ export const useStylingProps = ( location ) => { // Payment methods (checkboxes). paymentMethodChoices: Object.values( STYLING_PAYMENT_METHODS ), - paymentMethods: getLocationStyle( 'paymentMethods' ), + paymentMethods: getLocationProp( 'paymentMethods' ), setPaymentMethods: ( methods ) => - setLocationStyle( 'paymentMethods', methods ), + setLocationProp( 'paymentMethods', methods ), // Color (dropdown). colorChoices: Object.values( STYLING_COLORS ), - color: getLocationStyle( 'color' ), - setColor: ( color ) => setLocationStyle( 'color', color ), + color: getLocationProp( 'color' ), + setColor: ( color ) => setLocationProp( 'color', color ), // Shape (radio). shapeChoices: Object.values( STYLING_SHAPES ), - shape: getLocationStyle( 'shape' ), - setShape: ( shape ) => setLocationStyle( 'shape', shape ), + shape: getLocationProp( 'shape' ), + setShape: ( shape ) => setLocationProp( 'shape', shape ), // Label (dropdown). labelChoices: Object.values( STYLING_LABELS ), - label: getLocationStyle( 'label' ), - setLabel: ( label ) => setLocationStyle( 'label', label ), + label: getLocationProp( 'label' ), + setLabel: ( label ) => setLocationProp( 'label', label ), // Layout (radio). layoutChoices: Object.values( STYLING_LAYOUTS ), supportsLayout: true, - layout: getLocationStyle( 'layout' ), - setLayout: ( layout ) => setLocationStyle( 'layout', layout ), + layout: getLocationProp( 'layout' ), + setLayout: ( layout ) => setLocationProp( 'layout', layout ), // Tagline (checkbox). taglineChoices: [ @@ -129,7 +134,7 @@ export const useStylingProps = ( location ) => { }, ], supportsTagline: true, - tagline: getLocationStyle( 'tagline' ), - setTagline: ( tagline ) => setLocationStyle( 'tagline', tagline ), + tagline: getLocationProp( 'tagline' ), + setTagline: ( tagline ) => setLocationProp( 'tagline', tagline ), }; }; From 033b6e5b74ca2194e7c1b5bf470bf6d03610eb72 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 20:01:22 +0100 Subject: [PATCH 254/298] =?UTF-8?q?=E2=9C=A8=20Move=20styling=20options=20?= =?UTF-8?q?to=20Redux=20store!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/hooks.js | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 7db797d3a..5cf07655e 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -8,8 +8,8 @@ */ import { __ } from '@wordpress/i18n'; -import { useCallback, useState } from '@wordpress/element'; // Temporary -import { useDispatch } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; // Temporary +import { useDispatch, useSelect } from '@wordpress/data'; import { createHooksForStore } from '../utils'; import { STORE_NAME } from './constants'; @@ -33,43 +33,29 @@ const useHooks = () => { const [ location, setLocation ] = useTransient( 'location' ); // Persistent accessors. - // TODO - this is a dummy implementation. - const defaultStyle = { - paymentMethods: [], - color: 'gold', - shape: 'rect', - label: 'paypal', - layout: 'vertical', - tagline: false, - }; + const persistentData = useSelect( + ( select ) => select( STORE_NAME ).persistentData(), + [] + ); - // This data-struct is already present in the Redux store via persistentData, e.g. "persistentData.cart.label" - const [ styles, setStyles ] = useState( { - cart: { ...defaultStyle, label: 'checkout' }, - 'classic-checkout': { ...defaultStyle }, - 'express-checkout': { ...defaultStyle, label: 'pay' }, - 'mini-cart': { ...defaultStyle, label: 'pay' }, - 'product-page': { ...defaultStyle }, - } ); + const [ locationStyles, setLocationStyles ] = usePersistent( location ); const getLocationProp = useCallback( - ( prop ) => styles[ location ]?.[ prop ], - [ location, styles ] + ( prop ) => { + return persistentData[ location ]?.[ prop ]; + }, + [ location, persistentData ] ); const setLocationProp = useCallback( ( prop, value ) => { - setStyles( ( prevState ) => ( { - ...prevState, - [ location ]: { - ...prevState[ location ], - [ prop ]: value, - }, - } ) ); + setLocationStyles( { + ...locationStyles, + [ prop ]: value, + } ); }, - [ location ] + [ locationStyles, setLocationStyles ] ); - // end of dummy implementation return { persist, From dac414433fffc8feda3b224f6fd97afdfbf19438 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 20:02:37 +0100 Subject: [PATCH 255/298] =?UTF-8?q?=F0=9F=A5=85=20Add=20detection=20for=20?= =?UTF-8?q?accessing=20undefined=20props?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/styling/hooks.js | 5 +++++ modules/ppcp-settings/resources/js/data/utils.js | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 5cf07655e..cb0508247 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -42,6 +42,11 @@ const useHooks = () => { const getLocationProp = useCallback( ( prop ) => { + if ( undefined === persistentData[ location ]?.[ prop ] ) { + console.error( + `Trying to access non-existent style property: ${ location }.${ prop }. Possibly wrong style name - review the reducer.` + ); + } return persistentData[ location ]?.[ prop ]; }, [ location, persistentData ] diff --git a/modules/ppcp-settings/resources/js/data/utils.js b/modules/ppcp-settings/resources/js/data/utils.js index 35d7bbbf9..7d3a14af7 100644 --- a/modules/ppcp-settings/resources/js/data/utils.js +++ b/modules/ppcp-settings/resources/js/data/utils.js @@ -107,7 +107,13 @@ export const createHooksForStore = ( storeName ) => { `Please create the selector "${ selector }" for store "${ storeName }"` ); } - return store[ selector ]()?.[ key ]; + const selectorResult = store[ selector ](); + if ( undefined === selectorResult?.[ key ] ) { + console.error( + `Warning: ${ selector }()[${ key }] is undefined in store "${ storeName }". This may indicate a bug.` + ); + } + return selectorResult?.[ key ]; }, [ key ] ); From e5f882cc57904dba2d969226c2741b694cca47fe Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 20:04:20 +0100 Subject: [PATCH 256/298] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Fix=20issues=20?= =?UTF-8?q?in=20the=20defaultPersistent=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/reducer.js | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index ec432081f..7c553091e 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -9,53 +9,69 @@ import { createReducer, createSetters } from '../utils'; import ACTION_TYPES from './action-types'; -import { STYLING_COLORS, STYLING_SHAPES } from './configuration'; +import { + STYLING_COLORS, + STYLING_LABELS, + STYLING_LAYOUTS, + STYLING_LOCATIONS, + STYLING_SHAPES, +} from './configuration'; // Store structure. // Transient: Values that are _not_ saved to the DB (like app lifecycle-flags). const defaultTransient = Object.freeze( { isReady: false, - location: 'cart', // Which location is selected in the Styling tab. + location: STYLING_LOCATIONS.cart.value, // Which location is selected in the Styling tab. } ); // Persistent: Values that are loaded from the DB. const defaultPersistent = Object.freeze( { - cart: { + [ STYLING_LOCATIONS.cart.value ]: Object.freeze( { enabled: true, methods: [], - label: 'Pay', + label: STYLING_LABELS.pay.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - }, - 'classic-checkout': { + layout: STYLING_LAYOUTS.vertical.value, + tagline: false, + } ), + [ STYLING_LOCATIONS[ 'classic-checkout' ].value ]: Object.freeze( { enabled: true, methods: [], - label: 'Checkout', + label: STYLING_LABELS.checkout.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - }, - 'express-checkout': { + layout: STYLING_LAYOUTS.vertical.value, + tagline: false, + } ), + [ STYLING_LOCATIONS[ 'express-checkout' ].value ]: Object.freeze( { enabled: true, methods: [], - label: 'Checkout', + label: STYLING_LABELS.checkout.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - }, - 'mini-cart': { + layout: STYLING_LAYOUTS.vertical.value, + tagline: false, + } ), + [ STYLING_LOCATIONS[ 'mini-cart' ].value ]: Object.freeze( { enabled: true, methods: [], - label: 'Pay', + label: STYLING_LABELS.pay.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - }, - product: { + layout: STYLING_LAYOUTS.vertical.value, + tagline: false, + } ), + [ STYLING_LOCATIONS.product.value ]: Object.freeze( { enabled: true, methods: [], - label: 'Buy', + label: STYLING_LABELS.buynow.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - }, + layout: STYLING_LAYOUTS.vertical.value, + tagline: false, + } ), } ); // Reducer logic. From 510a711caa58fb142a827f73734d038b86b7d88a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 16 Jan 2025 20:04:47 +0100 Subject: [PATCH 257/298] =?UTF-8?q?=F0=9F=90=9B=20Fix=20inconsistent=20pro?= =?UTF-8?q?perty=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/configuration.js | 12 ++++++------ .../ppcp-settings/resources/js/data/styling/hooks.js | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/configuration.js b/modules/ppcp-settings/resources/js/data/styling/configuration.js index 861379e2d..ecb7a3765 100644 --- a/modules/ppcp-settings/resources/js/data/styling/configuration.js +++ b/modules/ppcp-settings/resources/js/data/styling/configuration.js @@ -47,8 +47,8 @@ export const STYLING_LOCATIONS = { ), link: '#', }, - 'product-page': { - value: 'product-page', + product: { + value: 'product', label: __( 'Product Page', 'woocommerce-paypal-payments' ), // translators: %s is the URL to a documentation page. description: __( @@ -113,14 +113,14 @@ export const STYLING_LAYOUTS = { }; export const STYLING_SHAPES = { - pill: { - value: 'pill', - label: __( 'Pill', 'woocommerce-paypal-payments' ), - }, rect: { value: 'rect', label: __( 'Rectangle', 'woocommerce-paypal-payments' ), }, + pill: { + value: 'pill', + label: __( 'Pill', 'woocommerce-paypal-payments' ), + }, }; export const STYLING_PAYMENT_METHODS = { diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index cb0508247..d81ee8746 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -92,9 +92,8 @@ export const useStylingProps = ( location ) => { // Payment methods (checkboxes). paymentMethodChoices: Object.values( STYLING_PAYMENT_METHODS ), - paymentMethods: getLocationProp( 'paymentMethods' ), - setPaymentMethods: ( methods ) => - setLocationProp( 'paymentMethods', methods ), + paymentMethods: getLocationProp( 'methods' ), + setPaymentMethods: ( methods ) => setLocationProp( 'methods', methods ), // Color (dropdown). colorChoices: Object.values( STYLING_COLORS ), From a5ceec2af43361920edac4dd50f8d1f12c168bb6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 13:04:21 +0100 Subject: [PATCH 258/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Location-prop=20ac?= =?UTF-8?q?cessors=20require=20a=20location=20arg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of using the “location” property, we want to explicitly specify which location props to access --- .../resources/js/data/styling/hooks.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index d81ee8746..946aa4cc7 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -23,8 +23,8 @@ import { } from './configuration'; const useHooks = () => { - const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); - const { persist } = useDispatch( STORE_NAME ); + const { useTransient } = createHooksForStore( STORE_NAME ); + const { persist, setPersistent } = useDispatch( STORE_NAME ); // Read-only flags and derived state. @@ -38,28 +38,28 @@ const useHooks = () => { [] ); - const [ locationStyles, setLocationStyles ] = usePersistent( location ); - const getLocationProp = useCallback( - ( prop ) => { - if ( undefined === persistentData[ location ]?.[ prop ] ) { + ( locatonId, prop ) => { + if ( undefined === persistentData[ locatonId ]?.[ prop ] ) { console.error( - `Trying to access non-existent style property: ${ location }.${ prop }. Possibly wrong style name - review the reducer.` + `Trying to access non-existent style property: ${ locatonId }.${ prop }. Possibly wrong style name - review the reducer.` ); } - return persistentData[ location ]?.[ prop ]; + return persistentData[ locatonId ]?.[ prop ]; }, - [ location, persistentData ] + [ persistentData ] ); const setLocationProp = useCallback( - ( prop, value ) => { - setLocationStyles( { - ...locationStyles, + ( locationId, prop, value ) => { + const updatedStyles = { + ...persistentData[ locationId ], [ prop ]: value, - } ); + }; + + setPersistent( locationId, updatedStyles ); }, - [ locationStyles, setLocationStyles ] + [ persistentData, setPersistent ] ); return { From 18429d91e5d87edd5613d760a0d3e15c94860510 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 13:05:25 +0100 Subject: [PATCH 259/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Split=20the=20styl?= =?UTF-8?q?ing=20hook=20into=20multiple=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Styling/Content/ButtonColor.js | 5 +- .../Components/Styling/Content/ButtonLabel.js | 5 +- .../Styling/Content/ButtonLayout.js | 8 +- .../Components/Styling/Content/ButtonShape.js | 5 +- .../Styling/Content/LocationSelector.js | 13 +-- .../Styling/Content/PaymentMethods.js | 6 +- .../Components/Styling/Content/TagLine.js | 8 +- .../Components/Styling/Content/index.js | 2 +- .../Components/Styling/SettingsPanel.js | 4 +- .../resources/js/data/styling/hooks.js | 97 +++++++++++++------ 10 files changed, 90 insertions(+), 63 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js index acadc3943..29b98069c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js @@ -4,14 +4,13 @@ import { StylingHooks } from '../../../../../../data'; import { SelectStylingSection } from '../Layout'; const SectionButtonColor = ( { location } ) => { - const { color, setColor, colorChoices } = - StylingHooks.useStylingProps( location ); + const { color, setColor, choices } = StylingHooks.useColorProps( location ); return ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js index a0763a049..de7c1a103 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js @@ -4,14 +4,13 @@ import { StylingHooks } from '../../../../../../data'; import { SelectStylingSection } from '../Layout'; const SectionButtonLabel = ( { location } ) => { - const { label, setLabel, labelChoices } = - StylingHooks.useStylingProps( location ); + const { label, setLabel, choices } = StylingHooks.useLabelProps( location ); return ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js index 0b6407ef1..1acf666f9 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js @@ -4,10 +4,10 @@ import { StylingHooks } from '../../../../../../data'; import { RadiobuttonStylingSection } from '../Layout'; const SectionButtonLayout = ( { location } ) => { - const { supportsLayout, layout, setLayout, layoutChoices } = - StylingHooks.useStylingProps( location ); + const { isAvailable, layout, setLayout, choices } = + StylingHooks.useLayoutProps( location ); - if ( ! supportsLayout ) { + if ( ! isAvailable ) { return null; } @@ -15,7 +15,7 @@ const SectionButtonLayout = ( { location } ) => { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js index 68eaef1dc..71bc5bebe 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js @@ -4,14 +4,13 @@ import { StylingHooks } from '../../../../../../data'; import { RadiobuttonStylingSection } from '../Layout'; const SectionButtonShape = ( { location } ) => { - const { shape, setShape, shapeChoices } = - StylingHooks.useStylingProps( location ); + const { shape, setShape, choices } = StylingHooks.useShapeProps( location ); return ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js index 2fd21caa0..f5d8231b8 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js @@ -1,14 +1,11 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { StylingHooks } from '../../../../../../data'; import { Description } from '../../../../../ReusableComponents/SettingsBlocks'; import { SelectStylingSection, StylingSection } from '../Layout'; const LocationSelector = ( { location, setLocation } ) => { - const { locationChoices, locationDetails } = - StylingHooks.useStylingProps( location ); - const { description, link } = locationDetails || {}; - const locationDescription = sprintf( description, link ); + const { choices, description } = StylingHooks.useLocationProps( location ); return ( <> @@ -25,13 +22,11 @@ const LocationSelector = ( { location, setLocation } ) => { className="location-selector" title={ __( 'Location', 'woocommerce-paypal-payments' ) } separatorAndGap={ false } - options={ locationChoices } + options={ choices } value={ location } onChange={ setLocation } > - - { locationDescription } - + { description } ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js index e379f876b..0397d34c3 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js @@ -4,14 +4,14 @@ import { StylingHooks } from '../../../../../../data'; import { CheckboxStylingSection } from '../Layout'; const SectionPaymentMethods = ( { location } ) => { - const { paymentMethods, setPaymentMethods, paymentMethodChoices } = - StylingHooks.useStylingProps( location ); + const { paymentMethods, setPaymentMethods, choices } = + StylingHooks.usePaymentMethodProps( location ); return ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js index 66fa4cd1f..ec292455c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js @@ -4,10 +4,10 @@ import { StylingHooks } from '../../../../../../data'; import { CheckboxStylingSection } from '../Layout'; const SectionTagline = ( { location } ) => { - const { supportsTagline, tagline, setTagline, taglineChoices } = - StylingHooks.useStylingProps( location ); + const { isAvailable, tagline, setTagline, choices } = + StylingHooks.useTaglineProps( location ); - if ( ! supportsTagline ) { + if ( ! isAvailable ) { return null; } @@ -15,7 +15,7 @@ const SectionTagline = ( { location } ) => { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js index 27a4e3d56..4529fb9eb 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js @@ -4,4 +4,4 @@ export { default as ButtonLabel } from './ButtonLabel'; export { default as ButtonLayout } from './ButtonLayout'; export { default as ButtonShape } from './ButtonShape'; export { default as PaymentMethods } from './PaymentMethods'; -export { default as TagLine } from './TagLine'; +export { default as Tagline } from './Tagline'; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js index ad65185cf..9151781f7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js @@ -6,7 +6,7 @@ import { ButtonShape, ButtonLabel, ButtonColor, - TagLine, + Tagline, } from './Content'; const SettingsPanel = () => { @@ -23,7 +23,7 @@ const SettingsPanel = () => { - +
); }; diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 946aa4cc7..2e77d41f2 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -7,7 +7,7 @@ * @file */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { useCallback } from '@wordpress/element'; // Temporary import { useDispatch, useSelect } from '@wordpress/data'; @@ -82,49 +82,84 @@ export const useStylingLocation = () => { return { location, setLocation }; }; -export const useStylingProps = ( location ) => { +export const useLocationProps = ( location ) => { + const details = STYLING_LOCATIONS[ location ] ?? {}; + + // eslint-disable-next-line @wordpress/valid-sprintf + const description = sprintf( details.description ?? '', details.link ); + + return { + choices: Object.values( STYLING_LOCATIONS ), + details, + description, + }; +}; + +export const usePaymentMethodProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); return { - // Location (drop down). - locationChoices: Object.values( STYLING_LOCATIONS ), - locationDetails: STYLING_LOCATIONS[ location ], + choices: Object.values( STYLING_PAYMENT_METHODS ), + paymentMethods: getLocationProp( location, 'methods' ), + setPaymentMethods: ( methods ) => + setLocationProp( location, 'methods', methods ), + }; +}; - // Payment methods (checkboxes). - paymentMethodChoices: Object.values( STYLING_PAYMENT_METHODS ), - paymentMethods: getLocationProp( 'methods' ), - setPaymentMethods: ( methods ) => setLocationProp( 'methods', methods ), +export const useColorProps = ( location ) => { + const { getLocationProp, setLocationProp } = useHooks(); - // Color (dropdown). - colorChoices: Object.values( STYLING_COLORS ), - color: getLocationProp( 'color' ), - setColor: ( color ) => setLocationProp( 'color', color ), + return { + choices: Object.values( STYLING_COLORS ), + color: getLocationProp( location, 'color' ), + setColor: ( color ) => setLocationProp( location, 'color', color ), + }; +}; - // Shape (radio). - shapeChoices: Object.values( STYLING_SHAPES ), - shape: getLocationProp( 'shape' ), - setShape: ( shape ) => setLocationProp( 'shape', shape ), +export const useShapeProps = ( location ) => { + const { getLocationProp, setLocationProp } = useHooks(); - // Label (dropdown). - labelChoices: Object.values( STYLING_LABELS ), - label: getLocationProp( 'label' ), - setLabel: ( label ) => setLocationProp( 'label', label ), + return { + choices: Object.values( STYLING_SHAPES ), + shape: getLocationProp( location, 'shape' ), + setShape: ( shape ) => setLocationProp( location, 'shape', shape ), + }; +}; - // Layout (radio). - layoutChoices: Object.values( STYLING_LAYOUTS ), - supportsLayout: true, - layout: getLocationProp( 'layout' ), - setLayout: ( layout ) => setLocationProp( 'layout', layout ), +export const useLabelProps = ( location ) => { + const { getLocationProp, setLocationProp } = useHooks(); - // Tagline (checkbox). - taglineChoices: [ + return { + choices: Object.values( STYLING_LABELS ), + label: getLocationProp( location, 'label' ), + setLabel: ( label ) => setLocationProp( location, 'label', label ), + }; +}; + +export const useLayoutProps = ( location ) => { + const { getLocationProp, setLocationProp } = useHooks(); + + return { + choices: Object.values( STYLING_LAYOUTS ), + isAvailable: true, + layout: getLocationProp( location, 'layout' ), + setLayout: ( layout ) => setLocationProp( location, 'layout', layout ), + }; +}; + +export const useTaglineProps = ( location ) => { + const { getLocationProp, setLocationProp } = useHooks(); + + return { + choices: [ { value: 'tagline', label: __( 'Enable Tagline', 'woocommerce-paypal-payments' ), }, ], - supportsTagline: true, - tagline: getLocationProp( 'tagline' ), - setTagline: ( tagline ) => setLocationProp( 'tagline', tagline ), + isAvailable: true, + tagline: getLocationProp( location, 'tagline' ), + setTagline: ( tagline ) => + setLocationProp( location, 'tagline', tagline ), }; }; From 4b47084aa20c96dcba52bc9e5e202fae5b61e1cf Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 13:20:15 +0100 Subject: [PATCH 260/298] =?UTF-8?q?=F0=9F=92=84=20Make=20disabled=20option?= =?UTF-8?q?=20more=20distinct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/css/_mixins.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/css/_mixins.scss b/modules/ppcp-settings/resources/css/_mixins.scss index 29cb3af1c..8c83eb436 100644 --- a/modules/ppcp-settings/resources/css/_mixins.scss +++ b/modules/ppcp-settings/resources/css/_mixins.scss @@ -49,7 +49,7 @@ .components-#{$control-type}-control__input, .components-#{$control-type}-control__label, .components-base-control__help { - opacity: 0.5; + opacity: 0.3; cursor: default; } } From 0428dd08aa0be410b500c6bf6f67fa79e6707165 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 13:20:58 +0100 Subject: [PATCH 261/298] =?UTF-8?q?=E2=9C=A8=20Add=20location-specific=20c?= =?UTF-8?q?onfiguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/configuration.js | 5 +++++ modules/ppcp-settings/resources/js/data/styling/hooks.js | 9 +++++++-- .../ppcp-settings/resources/js/data/styling/reducer.js | 4 ---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/configuration.js b/modules/ppcp-settings/resources/js/data/styling/configuration.js index ecb7a3765..eef2f90bf 100644 --- a/modules/ppcp-settings/resources/js/data/styling/configuration.js +++ b/modules/ppcp-settings/resources/js/data/styling/configuration.js @@ -16,6 +16,7 @@ export const STYLING_LOCATIONS = { 'wooocommerce-paypal-payments' ), link: '#', + props: { layout: false, tagline: false }, }, 'classic-checkout': { value: 'classic-checkout', @@ -26,6 +27,7 @@ export const STYLING_LOCATIONS = { 'wooocommerce-paypal-payments' ), link: '#', + props: { layout: true, tagline: true }, }, 'express-checkout': { value: 'express-checkout', @@ -36,6 +38,7 @@ export const STYLING_LOCATIONS = { 'wooocommerce-paypal-payments' ), link: '#', + props: { layout: false, tagline: false }, }, 'mini-cart': { value: 'mini-cart', @@ -46,6 +49,7 @@ export const STYLING_LOCATIONS = { 'wooocommerce-paypal-payments' ), link: '#', + props: { layout: true, tagline: true }, }, product: { value: 'product', @@ -56,6 +60,7 @@ export const STYLING_LOCATIONS = { 'wooocommerce-paypal-payments' ), link: '#', + props: { layout: true, tagline: true }, }, }; diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 2e77d41f2..f2bee3952 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -138,10 +138,11 @@ export const useLabelProps = ( location ) => { export const useLayoutProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); + const { details } = useLocationProps( location ); return { choices: Object.values( STYLING_LAYOUTS ), - isAvailable: true, + isAvailable: false !== details.props.layout, layout: getLocationProp( location, 'layout' ), setLayout: ( layout ) => setLocationProp( location, 'layout', layout ), }; @@ -149,6 +150,7 @@ export const useLayoutProps = ( location ) => { export const useTaglineProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); + const { details } = useLocationProps( location ); return { choices: [ @@ -157,7 +159,10 @@ export const useTaglineProps = ( location ) => { label: __( 'Enable Tagline', 'woocommerce-paypal-payments' ), }, ], - isAvailable: true, + isAvailable: + false !== details.props.tagline && + STYLING_LAYOUTS.horizontal.value === + getLocationProp( location, 'layout' ), tagline: getLocationProp( location, 'tagline' ), setTagline: ( tagline ) => setLocationProp( location, 'tagline', tagline ), diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index 7c553091e..e41cd1e40 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -33,8 +33,6 @@ const defaultPersistent = Object.freeze( { label: STYLING_LABELS.pay.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - layout: STYLING_LAYOUTS.vertical.value, - tagline: false, } ), [ STYLING_LOCATIONS[ 'classic-checkout' ].value ]: Object.freeze( { enabled: true, @@ -51,8 +49,6 @@ const defaultPersistent = Object.freeze( { label: STYLING_LABELS.checkout.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, - layout: STYLING_LAYOUTS.vertical.value, - tagline: false, } ), [ STYLING_LOCATIONS[ 'mini-cart' ].value ]: Object.freeze( { enabled: true, From 00c00bf9b1719caccef534cd6af8869ceef99df0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 13:27:31 +0100 Subject: [PATCH 262/298] =?UTF-8?q?=F0=9F=92=84=20Improvement=20UX=20for?= =?UTF-8?q?=20the=20=E2=80=9Ctagline=E2=80=9D=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/settings/_tab-styling.scss | 5 +++++ .../Components/Styling/Content/ButtonLayout.js | 18 +++++++++++------- .../Components/Styling/Content/TagLine.js | 2 +- .../Components/Styling/SettingsPanel.js | 2 -- .../resources/js/data/styling/hooks.js | 5 ++++- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index cc60e1619..071855a64 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -36,6 +36,11 @@ &.payment-methods { --block-separator-gap: 28px; } + + // It has no header; adjusts the gap to the control right above the tagline. + &.tagline { + --block-header-gap: 24px; + } } /* The settings-panel (left side) */ diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js index 1acf666f9..1a284f72a 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js @@ -2,6 +2,7 @@ import { __ } from '@wordpress/i18n'; import { StylingHooks } from '../../../../../../data'; import { RadiobuttonStylingSection } from '../Layout'; +import { Tagline } from './index'; const SectionButtonLayout = ( { location } ) => { const { isAvailable, layout, setLayout, choices } = @@ -12,13 +13,16 @@ const SectionButtonLayout = ( { location } ) => { } return ( - + <> + + + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js index ec292455c..54d823b22 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/TagLine.js @@ -13,8 +13,8 @@ const SectionTagline = ( { location } ) => { return ( { @@ -23,7 +22,6 @@ const SettingsPanel = () => { -
); }; diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index f2bee3952..2767698d5 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -156,7 +156,10 @@ export const useTaglineProps = ( location ) => { choices: [ { value: 'tagline', - label: __( 'Enable Tagline', 'woocommerce-paypal-payments' ), + label: __( + 'Show tagline below buttons', + 'woocommerce-paypal-payments' + ), }, ], isAvailable: From 6993038c053b158fe280b19028eb79edc55a7595 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Fri, 17 Jan 2025 13:59:17 +0100 Subject: [PATCH 263/298] Settings UI: Add Settings tab Redux store --- modules/ppcp-onboarding/services.php | 36 ++-- .../Screens/Overview/TabSettings.js | 29 +--- .../js/Components/Screens/Settings.js | 15 +- .../js/data/settings-tab/action-types.js | 40 +++++ .../resources/js/data/settings-tab/actions.js | 71 ++++++++ .../js/data/settings-tab/constants.js | 28 +++ .../js/data/settings-tab/controls.js | 34 ++++ .../resources/js/data/settings-tab/hooks.js | 55 ++++++ .../resources/js/data/settings-tab/index.js | 51 ++++++ .../resources/js/data/settings-tab/reducer.js | 107 ++++++++++++ .../js/data/settings-tab/resolvers.js | 57 ++++++ .../js/data/settings-tab/selectors.js | 46 +++++ modules/ppcp-settings/services.php | 11 ++ .../ppcp-settings/src/Data/SettingsModel.php | 101 +++++++++++ .../src/Endpoint/SettingsRestEndpoint.php | 163 ++++++++++++++++++ modules/ppcp-settings/src/SettingsModule.php | 13 +- .../ppcp-wc-gateway/src/WCGatewayModule.php | 4 +- 17 files changed, 811 insertions(+), 50 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/action-types.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/actions.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/constants.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/controls.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/hooks.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/index.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/reducer.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/resolvers.js create mode 100644 modules/ppcp-settings/resources/js/data/settings-tab/selectors.js create mode 100644 modules/ppcp-settings/src/Data/SettingsModel.php create mode 100644 modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index aca3bed0a..2a6de82d8 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -22,7 +22,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRendere use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; return array( - 'api.sandbox-host' => static function ( ContainerInterface $container ): string { + 'api.sandbox-host' => static function ( ContainerInterface $container ): string { $state = $container->get( 'onboarding.state' ); @@ -36,7 +36,7 @@ return array( } return CONNECT_WOO_SANDBOX_URL; }, - 'api.production-host' => static function ( ContainerInterface $container ): string { + 'api.production-host' => static function ( ContainerInterface $container ): string { $state = $container->get( 'onboarding.state' ); @@ -51,7 +51,7 @@ return array( } return CONNECT_WOO_URL; }, - 'api.host' => static function ( ContainerInterface $container ): string { + 'api.host' => static function ( ContainerInterface $container ): string { $environment = $container->get( 'onboarding.environment' ); /** @@ -63,7 +63,7 @@ return array( ? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' ); }, - 'api.paypal-host' => function( ContainerInterface $container ) : string { + 'api.paypal-host' => function( ContainerInterface $container ) : string { $environment = $container->get( 'onboarding.environment' ); /** * The current environment. @@ -76,7 +76,7 @@ return array( return $container->get( 'api.paypal-host-production' ); }, - 'api.paypal-website-url' => function( ContainerInterface $container ) : string { + 'api.paypal-website-url' => function( ContainerInterface $container ) : string { $environment = $container->get( 'onboarding.environment' ); assert( $environment instanceof Environment ); if ( $environment->current_environment_is( Environment::SANDBOX ) ) { @@ -86,7 +86,7 @@ return array( }, - 'api.bearer' => static function ( ContainerInterface $container ): Bearer { + 'api.bearer' => static function ( ContainerInterface $container ): Bearer { $state = $container->get( 'onboarding.state' ); @@ -113,16 +113,16 @@ return array( $settings ); }, - 'onboarding.state' => function( ContainerInterface $container ) : State { + 'onboarding.state' => function( ContainerInterface $container ) : State { $settings = $container->get( 'wcgateway.settings' ); return new State( $settings ); }, - 'onboarding.environment' => function( ContainerInterface $container ) : Environment { + 'onboarding.environment' => function( ContainerInterface $container ) : Environment { $settings = $container->get( 'wcgateway.settings' ); return new Environment( $settings ); }, - 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { + 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { $state = $container->get( 'onboarding.state' ); $login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' ); return new OnboardingAssets( @@ -135,14 +135,14 @@ return array( ); }, - 'onboarding.url' => static function ( ContainerInterface $container ): string { + 'onboarding.url' => static function ( ContainerInterface $container ): string { return plugins_url( '/modules/ppcp-onboarding/', dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { + 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { $request_data = $container->get( 'button.request-data' ); $login_seller_production = $container->get( 'api.endpoint.login-seller-production' ); @@ -162,7 +162,7 @@ return array( new Cache( 'ppcp-client-credentials-cache' ) ); }, - 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { + 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { return new UpdateSignupLinksEndpoint( $container->get( 'wcgateway.settings' ), $container->get( 'button.request-data' ), @@ -172,10 +172,10 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { + 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { return new Cache( 'ppcp-paypal-signup-link' ); }, - 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { + 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { return array( 'production-ppcp', 'production-express_checkout', @@ -183,12 +183,12 @@ return array( 'sandbox-express_checkout', ); }, - 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { + 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { return new OnboardingSendOnlyNoticeRenderer( $container->get( 'wcgateway.send-only-message' ) ); }, - 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { + 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { $partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' ); $partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' ); $partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' ); @@ -204,14 +204,14 @@ return array( $logger ); }, - 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { + 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { return new OnboardingOptionsRenderer( $container->get( 'onboarding.url' ), $container->get( 'api.shop.country' ), $container->get( 'wcgateway.settings' ) ); }, - 'onboarding.rest' => static function( $container ) : OnboardingRESTController { + 'onboarding.rest' => static function( $container ) : OnboardingRESTController { return new OnboardingRESTController( $container ); }, ); 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 1b471fe1e..80d025af8 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js @@ -1,31 +1,16 @@ -import { useState } from '@wordpress/element'; import ConnectionStatus from './TabSettingsElements/ConnectionStatus'; import CommonSettings from './TabSettingsElements/CommonSettings'; import ExpertSettings from './TabSettingsElements/ExpertSettings'; +import { useSettings } from '../../../data/settings-tab/hooks'; const TabSettings = () => { - const [ settings, setSettings ] = useState( { - invoicePrefix: '', - authorizeOnly: false, - captureVirtualOnlyOrders: false, - savePaypalAndVenmo: false, - saveCreditCardAndDebitCard: false, - payNowExperience: false, - sandboxAccountCredentials: false, - sandboxMode: null, - sandboxEnabled: false, - sandboxClientId: '', - sandboxSecretKey: '', - sandboxConnected: false, - logging: false, - subtotalMismatchFallback: null, - brandName: '', - softDescriptor: '', - paypalLandingPage: null, - buttonLanguage: '', - } ); + const { settings, setSettings } = useSettings(); + const updateFormValue = ( key, value ) => { - setSettings( { ...settings, [ key ]: value } ); + setSettings( { + ...settings, + [ key ]: value, + } ); }; return ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index 128884f10..55f60e390 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -8,10 +8,16 @@ import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay'; import Onboarding from './Onboarding/Onboarding'; import SettingsScreen from './SettingsScreen'; import { useMerchantInfo } from '../../data/common/hooks'; +import { initStore as initSettingsStore } from '../../data/settings-tab'; +import { useSettingsState } from '../../data/settings-tab/hooks'; import SendOnlyMessage from './SendOnlyMessage'; +// Initialize the settings store +initSettingsStore(); + const Settings = () => { const onboardingProgress = OnboardingHooks.useSteps(); + const { isReady: settingsIsReady } = useSettingsState(); const { isReady: merchantIsReady, merchant: { isSendOnlyCountry }, @@ -32,11 +38,15 @@ const Settings = () => { }, [] ); const wrapperClass = classNames( 'ppcp-r-app', { - loading: ! onboardingProgress.isReady, + loading: ! onboardingProgress.isReady || ! settingsIsReady, } ); const Content = useMemo( () => { - if ( ! onboardingProgress.isReady || ! merchantIsReady ) { + if ( + ! onboardingProgress.isReady || + ! merchantIsReady || + ! settingsIsReady + ) { return ( { merchantIsReady, onboardingProgress.completed, onboardingProgress.isReady, + settingsIsReady, ] ); return
{ Content }
; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/action-types.js b/modules/ppcp-settings/resources/js/data/settings-tab/action-types.js new file mode 100644 index 000000000..d13c0cbcf --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/action-types.js @@ -0,0 +1,40 @@ +/** + * Settings action types + * + * Defines the constants used for dispatching actions in the settings store. + * Each constant represents a unique action type that can be handled by reducers. + * + * @file + */ + +export default { + /** + * Represents setting transient (temporary) state data. + * These values are not persisted and will reset on page reload. + */ + SET_TRANSIENT: 'ppcp/settings/SET_TRANSIENT', + + /** + * Represents setting persistent state data. + * These values are meant to be saved to the server and persist between page loads. + */ + SET_PERSISTENT: 'ppcp/settings/SET_PERSISTENT', + + /** + * Resets the store state to its initial values. + * Used when needing to clear all settings data. + */ + RESET: 'ppcp/settings/RESET', + + /** + * Initializes the store with data, typically used during store initialization + * to set up the initial state with data from the server. + */ + HYDRATE: 'ppcp/settings/HYDRATE', + + /** + * Triggers the persistence of store data to the server. + * Used when changes need to be saved to the backend. + */ + DO_PERSIST_DATA: 'ppcp/settings/DO_PERSIST_DATA', +}; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/actions.js b/modules/ppcp-settings/resources/js/data/settings-tab/actions.js new file mode 100644 index 000000000..a13cedeab --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/actions.js @@ -0,0 +1,71 @@ +/** + * Action Creators: Define functions to create action objects. + * + * These functions update state or trigger side effects (e.g., async operations). + * Actions are categorized as Transient, Persistent, or Side effect. + * + * @file + */ + +import { select } from '@wordpress/data'; +import ACTION_TYPES from './action-types'; +import { STORE_NAME } from './constants'; + +/** + * @typedef {Object} Action An action object that is handled by a reducer or control. + * @property {string} type - The action type. + * @property {Object?} payload - Optional payload for the action. + */ + +/** + * Special. Resets all values in the store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { + type: ACTION_TYPES.RESET, +} ); + +/** + * Persistent. Sets the full store details during app initialization. + * + * @param {Object} payload Initial store data + * @return {Action} The action. + */ +export const hydrate = ( payload ) => ( { + type: ACTION_TYPES.HYDRATE, + payload, +} ); + +/** + * Transient. Marks the store as "ready", i.e., fully initialized. + * + * @param {boolean} isReady Whether the store is ready + * @return {Action} The action. + */ +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); + +/** + * Persistent. Updates the settings data in the store. + * + * @param {Object} settings The settings object to store + * @return {Action} The action. + */ +export const setSettings = ( settings ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: settings, +} ); + +/** + * Side effect. Triggers the persistence of store data to the server. + * Yields an action with the current persistent data to be saved. + * + * @return {Action} The action. + */ +export const persist = function* () { + const data = yield select( STORE_NAME ).persistentData(); + yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; +}; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/constants.js b/modules/ppcp-settings/resources/js/data/settings-tab/constants.js new file mode 100644 index 000000000..dd7faa120 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/constants.js @@ -0,0 +1,28 @@ +/** + * Name of the Redux store module. + * + * Used by: Reducer, Selector, Index + * + * @type {string} + */ +export const STORE_NAME = 'wc/paypal/settings'; + +/** + * REST path to hydrate data of this module by loading data from the WP DB. + * + * Used by: Resolvers + * See: SettingsRestEndpoint.php + * + * @type {string} + */ +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/settings'; + +/** + * REST path to persist data of this module to the WP DB. + * + * Used by: Controls + * See: SettingsRestEndpoint.php + * + * @type {string} + */ +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/settings'; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/controls.js b/modules/ppcp-settings/resources/js/data/settings-tab/controls.js new file mode 100644 index 000000000..11e5e6a04 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/controls.js @@ -0,0 +1,34 @@ +/** + * Controls: Implement side effects, typically asynchronous operations. + * + * Controls use ACTION_TYPES keys as identifiers. + * They are triggered by corresponding actions and handle external interactions. + * + * @file + */ + +import apiFetch from '@wordpress/api-fetch'; +import { REST_PERSIST_PATH } from './constants'; +import ACTION_TYPES from './action-types'; + +/** + * Control handlers for settings store actions. + * Each handler maps to an ACTION_TYPE and performs the corresponding async operation. + */ +export const controls = { + /** + * Persists settings data to the server via REST API. + * Triggered by the DO_PERSIST_DATA action to save settings changes. + * + * @param {Object} action The action object + * @param {Object} action.data The settings data to persist + * @return {Promise} The API response + */ + async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { + return await apiFetch( { + path: REST_PERSIST_PATH, + method: 'POST', + data, + } ); + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/hooks.js b/modules/ppcp-settings/resources/js/data/settings-tab/hooks.js new file mode 100644 index 000000000..89041b207 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/hooks.js @@ -0,0 +1,55 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * + * @file + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); + +const useHooks = () => { + const { persist, setSettings } = useDispatch( STORE_NAME ); + + // Read-only flags and derived state. + const isReady = useTransient( 'isReady' ); + + // Persistent accessors. + const settings = useSelect( + ( select ) => select( STORE_NAME ).persistentData(), + [] + ); + + return { + persist, + isReady, + settings, + setSettings, + }; +}; + +export const useSettingsState = () => { + const { persist, isReady } = useHooks(); + return { persist, isReady }; +}; + +export const useSettings = () => { + const { settings, setSettings } = useHooks(); + return { + settings, + setSettings, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/index.js b/modules/ppcp-settings/resources/js/data/settings-tab/index.js new file mode 100644 index 000000000..affca562e --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/index.js @@ -0,0 +1,51 @@ +/** + * Store Configuration: Defines and registers the settings data store. + * + * Creates a Redux-style store with WordPress data layer integration. + * Combines reducers, actions, selectors and controls into a unified store. + * + * @file + */ + +import { createReduxStore, register } from '@wordpress/data'; +import { controls as wpControls } from '@wordpress/data-controls'; +import { STORE_NAME } from './constants'; +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as hooks from './hooks'; +import { resolvers } from './resolvers'; +import { controls } from './controls'; + +/** + * Initializes and registers the settings store with WordPress data layer. + * Combines custom controls with WordPress data controls. + * + * @return {boolean} True if initialization succeeded, false otherwise. + */ +export const initStore = () => { + try { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + register( store ); + + // Verify store registration + const isStoreRegistered = Boolean( wp.data.select( STORE_NAME ) ); + if ( ! isStoreRegistered ) { + console.error( 'Store registration verification failed' ); + return false; + } + + return true; + } catch ( error ) { + console.error( 'Failed to initialize settings store:', error ); + return false; + } +}; + +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/reducer.js b/modules/ppcp-settings/resources/js/data/settings-tab/reducer.js new file mode 100644 index 000000000..a374f60af --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/reducer.js @@ -0,0 +1,107 @@ +/** + * Reducer: Defines store structure and state updates for this module. + * + * Manages both transient (temporary) and persistent (saved) state. + * The initial state must define all properties, as dynamic additions are not supported. + * + * @file + */ + +import { createReducer, createSetters } from '../utils'; +import ACTION_TYPES from './action-types'; + +// Store structure. + +/** + * Transient: Values that are _not_ saved to the DB (like app lifecycle-flags). + * These reset on page reload. + */ +const defaultTransient = Object.freeze( { + isReady: false, +} ); + +/** + * Persistent: Values that are loaded from and saved to the DB. + * These represent the core PayPal payment settings configuration. + */ +const defaultPersistent = Object.freeze( { + invoicePrefix: '', // Prefix for PayPal invoice IDs + authorizeOnly: false, // Whether to only authorize payments initially + captureVirtualOnlyOrders: false, // Auto-capture virtual-only orders + savePaypalAndVenmo: false, // Enable PayPal & Venmo vaulting + saveCreditCardAndDebitCard: false, // Enable card vaulting + payNowExperience: false, // Enable Pay Now experience + sandboxAccountCredentials: false, // Use sandbox credentials + sandboxMode: null, // Sandbox mode configuration + sandboxEnabled: false, // Whether sandbox mode is active + sandboxClientId: '', // Sandbox API client ID + sandboxSecretKey: '', // Sandbox API secret key + sandboxConnected: false, // Sandbox connection status + logging: false, // Enable debug logging + subtotalMismatchFallback: null, // Handling for subtotal mismatches + brandName: '', // Merchant brand name for PayPal + softDescriptor: '', // Payment descriptor on statements + paypalLandingPage: null, // PayPal checkout landing page + buttonLanguage: '', // Language for PayPal buttons +} ); + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +/** + * Reducer implementation mapping actions to state updates. + */ +const reducer = createReducer( defaultTransient, defaultPersistent, { + /** + * Updates temporary state values + * + * @param {Object} state Current state + * @param {Object} payload Update payload + * @return {Object} Updated state + */ + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => { + return setTransient( state, payload ); + }, + + /** + * Updates persistent configuration values + * + * @param {Object} state Current state + * @param {Object} payload Update payload + * @return {Object} Updated state + */ + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => + setPersistent( state, payload ), + + /** + * Resets state to defaults while maintaining initialization status + * + * @param {Object} state Current state + * @return {Object} Reset state + */ + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + cleanState.isReady = true; // Keep initialization flag + return cleanState; + }, + + /** + * Initializes persistent state with data from the server + * + * @param {Object} state Current state + * @param {Object} payload Hydration payload containing server data + * @param {Object} payload.data The settings data to hydrate + * @return {Object} Hydrated state + */ + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => + setPersistent( state, payload.data ), +} ); + +export default reducer; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/resolvers.js b/modules/ppcp-settings/resources/js/data/settings-tab/resolvers.js new file mode 100644 index 000000000..da28dd66d --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/resolvers.js @@ -0,0 +1,57 @@ +/** + * Resolvers: Handle asynchronous data fetching for the store. + * + * These functions update store state with data from external sources. + * Each resolver corresponds to a specific selector (selector with same name must exist). + * Resolvers are called automatically when selectors request unavailable data. + * + * @file + */ + +import { dispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { apiFetch } from '@wordpress/data-controls'; +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; + +export const resolvers = { + /** + * Retrieve PayPal settings from the site's REST API. + * Hydrates the store with the retrieved data and marks it as ready. + * + * @generator + * @yield {Object} API fetch and dispatch actions + */ + *persistentData() { + try { + // Fetch settings data from REST API + const result = yield apiFetch( { + path: REST_HYDRATE_PATH, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + } ); + + // Update store with retrieved data + yield dispatch( STORE_NAME ).hydrate( result ); + // Mark store as ready for use + yield dispatch( STORE_NAME ).setIsReady( true ); + } catch ( e ) { + // Log detailed error information for debugging + console.error( 'Full error details:', { + error: e, + path: REST_HYDRATE_PATH, + store: STORE_NAME, + } ); + + // Display user-friendly error notice + yield dispatch( 'core/notices' ).createErrorNotice( + __( + 'Error retrieving PayPal settings details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/selectors.js b/modules/ppcp-settings/resources/js/data/settings-tab/selectors.js new file mode 100644 index 000000000..c35f04306 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings-tab/selectors.js @@ -0,0 +1,46 @@ +/** + * Selectors: Extract specific pieces of state from the store. + * + * These functions provide a consistent interface for accessing store data. + * They allow components to retrieve data without knowing the store structure. + * + * @file + */ + +/** + * Empty frozen object used as fallback when state is undefined. + * + * @constant + * @type {Object} + */ +const EMPTY_OBJ = Object.freeze( {} ); + +/** + * Base selector that ensures a valid state object. + * + * @param {Object|undefined} state The current state + * @return {Object} The state or empty object if undefined + */ +export const getState = ( state ) => state || EMPTY_OBJ; + +/** + * Retrieves persistent (saved) data from the store. + * + * @param {Object} state The current state + * @return {Object} The persistent data or empty object if undefined + */ +export const persistentData = ( state ) => { + return getState( state ).data || EMPTY_OBJ; +}; + +/** + * Retrieves transient (temporary) data from the store. + * Excludes persistent data stored in the 'data' property. + * + * @param {Object} state The current state + * @return {Object} The transient state or empty object if undefined + */ +export const transientData = ( state ) => { + const { data, ...transientState } = getState( state ); + return transientState || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 6fc9d67e3..85d84dfa1 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -13,12 +13,14 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\SettingsRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; @@ -193,4 +195,13 @@ return array( $container->get( 'button.request-data' ), ); }, + 'settings.rest.settings' => static function( ContainerInterface $container ): SettingsRestEndpoint { + return new SettingsRestEndpoint( + $container->get( 'settings.data.settings' ), + $container->get( 'woocommerce.logger.woocommerce' ), + ); + }, + 'settings.data.settings' => static function() : SettingsModel { + return new SettingsModel(); + }, ); diff --git a/modules/ppcp-settings/src/Data/SettingsModel.php b/modules/ppcp-settings/src/Data/SettingsModel.php new file mode 100644 index 000000000..0d421913b --- /dev/null +++ b/modules/ppcp-settings/src/Data/SettingsModel.php @@ -0,0 +1,101 @@ + $settings['invoice_prefix'] ?? '', + 'authorizeOnly' => (bool) ( $settings['authorize_only'] ?? false ), + 'captureVirtualOnlyOrders' => (bool) ( $settings['capture_virtual_only_orders'] ?? false ), + 'savePaypalAndVenmo' => (bool) ( $settings['save_paypal_and_venmo'] ?? false ), + 'saveCreditCardAndDebitCard' => (bool) ( $settings['save_credit_card_and_debit_card'] ?? false ), + 'payNowExperience' => (bool) ( $settings['pay_now_experience'] ?? false ), + 'sandboxAccountCredentials' => (bool) ( $settings['sandbox_account_credentials'] ?? false ), + 'sandboxMode' => $settings['sandbox_mode'] ?? null, + 'sandboxEnabled' => (bool) ( $settings['sandbox_enabled'] ?? false ), + 'sandboxClientId' => $settings['sandbox_client_id'] ?? '', + 'sandboxSecretKey' => $settings['sandbox_secret_key'] ?? '', + 'sandboxConnected' => (bool) ( $settings['sandbox_connected'] ?? false ), + 'logging' => (bool) ( $settings['logging'] ?? false ), + 'subtotalMismatchFallback' => $settings['subtotal_mismatch_fallback'] ?? null, + 'brandName' => $settings['brand_name'] ?? '', + 'softDescriptor' => $settings['soft_descriptor'] ?? '', + 'paypalLandingPage' => $settings['paypal_landing_page'] ?? null, + 'buttonLanguage' => $settings['button_language'] ?? '', + ); + + return $formatted; + } + + /** + * Updates the settings in WordPress options. + * + * Converts the provided data array from camelCase to snake_case format + * and saves it to wp_options table. Throws an exception if update fails. + * + * @param array $data The settings data to update. + * @throws RuntimeException When the settings update fails. + * @return void + */ + public function update( array $data ): void { + $settings = array( + 'invoice_prefix' => $data['invoicePrefix'] ?? '', + 'authorize_only' => (bool) ( $data['authorizeOnly'] ?? false ), + 'capture_virtual_only_orders' => (bool) ( $data['captureVirtualOnlyOrders'] ?? false ), + 'save_paypal_and_venmo' => (bool) ( $data['savePaypalAndVenmo'] ?? false ), + 'save_credit_card_and_debit_card' => (bool) ( $data['saveCreditCardAndDebitCard'] ?? false ), + 'pay_now_experience' => (bool) ( $data['payNowExperience'] ?? false ), + 'sandbox_account_credentials' => (bool) ( $data['sandboxAccountCredentials'] ?? false ), + 'sandbox_mode' => $data['sandboxMode'] ?? null, + 'sandbox_enabled' => (bool) ( $data['sandboxEnabled'] ?? false ), + 'sandbox_client_id' => $data['sandboxClientId'] ?? '', + 'sandbox_secret_key' => $data['sandboxSecretKey'] ?? '', + 'sandbox_connected' => (bool) ( $data['sandboxConnected'] ?? false ), + 'logging' => (bool) ( $data['logging'] ?? false ), + 'subtotal_mismatch_fallback' => $data['subtotalMismatchFallback'] ?? null, + 'brand_name' => $data['brandName'] ?? '', + 'soft_descriptor' => $data['softDescriptor'] ?? '', + 'paypal_landing_page' => $data['paypalLandingPage'] ?? null, + 'button_language' => $data['buttonLanguage'] ?? '', + ); + + $result = update_option( self::OPTION_NAME, $settings ); + + if ( ! $result ) { + throw new RuntimeException( 'Failed to update settings' ); + } + } +} diff --git a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php new file mode 100644 index 000000000..0b154b3a8 --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php @@ -0,0 +1,163 @@ +settings = $settings; + $this->logger = $logger; + } + + /** + * Registers the REST API routes for settings management. + */ + public function register_routes(): void { + register_rest_route( + $this->namespace, + '/' . self::ENDPOINT, + array( + array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_settings' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + array( + 'methods' => 'POST', + 'callback' => array( $this, 'update_settings' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + } + + /** + * Retrieves the current settings. + * + * @param WP_REST_Request $request The request instance. + * @return WP_REST_Response The response containing settings data or error details. + */ + public function get_settings( WP_REST_Request $request ): WP_REST_Response { + try { + // Get settings data + $data = $this->settings->get(); + + // Ensure the data is JSON-encodable + $encoded = json_encode( $data ); + if ( $encoded === false ) { + throw new \Exception( 'Failed to encode settings data: ' . json_last_error_msg() ); + } + + // Create response with pre-verified JSON data + $response_data = array( + 'success' => true, + 'data' => json_decode( $encoded, true ), + ); + + return new WP_REST_Response( $response_data, 200 ); + + } catch ( \Exception $error ) { + return new WP_REST_Response( + array( + 'success' => false, + 'message' => $error->getMessage(), + ), + 500 + ); + } + } + + /** + * Updates the settings with provided data. + * + * @param WP_REST_Request $request The request instance containing new settings. + * @return WP_REST_Response The response containing updated settings or error details. + */ + public function update_settings( WP_REST_Request $request ): WP_REST_Response { + try { + $data = $request->get_json_params(); + + $this->settings->update( $data ); + $updated_data = $this->settings->get(); + + // Verify JSON encoding + $encoded = json_encode( $updated_data ); + if ( $encoded === false ) { + throw new \Exception( 'Failed to encode updated settings: ' . json_last_error_msg() ); + } + + return new WP_REST_Response( + array( + 'success' => true, + 'data' => json_decode( $encoded, true ), + ), + 200 + ); + + } catch ( \Exception $error ) { + return new WP_REST_Response( + array( + 'success' => false, + 'message' => $error->getMessage(), + ), + 500 + ); + } + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 2d9356ab7..2a9d8a718 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -205,12 +205,13 @@ class SettingsModule implements ServiceModule, ExecutableModule { 'rest_api_init', static function () use ( $container ) : void { $endpoints = array( - $container->get( 'settings.rest.onboarding' ), - $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' ), + 'onboarding' => $container->get( 'settings.rest.onboarding' ), + 'common' => $container->get( 'settings.rest.common' ), + 'connect_manual' => $container->get( 'settings.rest.connect_manual' ), + 'login_link' => $container->get( 'settings.rest.login_link' ), + 'webhooks' => $container->get( 'settings.rest.webhooks' ), + 'refresh_feature_status' => $container->get( 'settings.rest.refresh_feature_status' ), + 'settings' => $container->get( 'settings.rest.settings' ), ); foreach ( $endpoints as $endpoint ) { diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index e4e13a91d..7539f60ee 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -554,7 +554,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); - $reference_transactions_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); + $reference_transactions_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); $features['save_paypal_and_venmo'] = array( 'enabled' => $reference_transactions_enabled, ); @@ -562,7 +562,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul $dcc_product_status = $c->get( 'wcgateway.helper.dcc-product-status' ); assert( $dcc_product_status instanceof DCCProductStatus ); - $dcc_enabled = $dcc_product_status->dcc_is_active(); + $dcc_enabled = $dcc_product_status->dcc_is_active(); $features['advanced_credit_and_debit_cards'] = array( 'enabled' => $dcc_enabled, ); From a7be75b55e41691dbd545005fbd53f52f7d18939 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 15:35:21 +0100 Subject: [PATCH 264/298] =?UTF-8?q?=E2=9C=A8=20Add=20conditional=20logic?= =?UTF-8?q?=20to=20layout=20&=20tagline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/hooks.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 2767698d5..439b0ec2a 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -139,10 +139,11 @@ export const useLabelProps = ( location ) => { export const useLayoutProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); const { details } = useLocationProps( location ); + const isAvailable = false !== details.props.layout; return { choices: Object.values( STYLING_LAYOUTS ), - isAvailable: false !== details.props.layout, + isAvailable, layout: getLocationProp( location, 'layout' ), setLayout: ( layout ) => setLocationProp( location, 'layout', layout ), }; @@ -152,6 +153,12 @@ export const useTaglineProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); const { details } = useLocationProps( location ); + // Tagline is only available for horizontal layouts. + const isAvailable = + false !== details.props.tagline && + STYLING_LAYOUTS.horizontal.value === + getLocationProp( location, 'layout' ); + return { choices: [ { @@ -162,11 +169,8 @@ export const useTaglineProps = ( location ) => { ), }, ], - isAvailable: - false !== details.props.tagline && - STYLING_LAYOUTS.horizontal.value === - getLocationProp( location, 'layout' ), - tagline: getLocationProp( location, 'tagline' ), + isAvailable, + tagline: isAvailable ? getLocationProp( location, 'tagline' ) : false, setTagline: ( tagline ) => setLocationProp( location, 'tagline', tagline ), }; From 1152079df0c4659f86c110fdba7e08f65b259b5d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 15:35:50 +0100 Subject: [PATCH 265/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Small=20improvemen?= =?UTF-8?q?ts=20on=20reusable=20component=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SettingsBlocks/SettingsBlockElements.js | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js index de45a601b..46048e071 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js @@ -7,12 +7,16 @@ export const Title = ( { big = false, className = '', } ) => { - className = classNames( 'ppcp-r-settings-block__title', className, { - 'style-alt': altStyle, - 'style-big': big, - } ); + const elementClasses = classNames( + 'ppcp-r-settings-block__title', + className, + { + 'style-alt': altStyle, + 'style-big': big, + } + ); - return { children }; + return { children }; }; export const TitleWrapper = ( { children } ) => ( @@ -31,15 +35,18 @@ export const Description = ( { children, asHtml = false, className = '' } ) => { return null; } - className = classNames( 'ppcp-r-settings-block__description', className ); + const elementClasses = classNames( + 'ppcp-r-settings-block__description', + className + ); if ( ! asHtml ) { - return { children }; + return { children }; } return ( ); @@ -56,11 +63,18 @@ export const Header = ( { children, className = '' } ) => ( ); // Card Elements -export const Content = ( { children, id = '' } ) => ( -
- { children } -
-); +export const Content = ( { children, className = '', id = '' } ) => { + const elementClasses = classNames( + 'ppcp-r-settings-card__content', + className + ); + + return ( +
+ { children } +
+ ); +}; export const ContentWrapper = ( { children } ) => (
{ children }
From bfcb12c9ea44bebad88396ace70ff4dd31177d1b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 15:36:26 +0100 Subject: [PATCH 266/298] =?UTF-8?q?=E2=9C=A8=20Wrap=20Styling=20section=20?= =?UTF-8?q?contents=20in=20new=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Settings/Components/Styling/Layout/StylingSection.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js index e528361dc..b9fefc52e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js @@ -3,6 +3,7 @@ import { Description, Header, Title, + Content, } from '../../../../../ReusableComponents/SettingsBlocks'; const StylingSection = ( { @@ -25,7 +26,7 @@ const StylingSection = ( { { description } - { children } + { children } ); }; From 48c68c1855710babce94660e9f56d66587cc2072 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 15:36:59 +0100 Subject: [PATCH 267/298] =?UTF-8?q?=F0=9F=92=84=20Remove=20the=20excess=20?= =?UTF-8?q?margin=20below=20select=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Styling/Layout/StylingSectionWithSelect.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js index c4164d6b8..2b0ef8031 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js @@ -23,6 +23,7 @@ const StylingSectionWithSelect = ( { separatorAndGap={ separatorAndGap } > Date: Fri, 17 Jan 2025 15:38:08 +0100 Subject: [PATCH 268/298] =?UTF-8?q?=F0=9F=92=84=20New=20help=20icon=20inst?= =?UTF-8?q?ead=20of=20text=20for=20location=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/settings/_tab-styling.scss | 8 ++++ .../Styling/Content/LocationSelector.js | 13 +++++-- .../js/data/styling/configuration.js | 37 ++++--------------- .../resources/js/data/styling/hooks.js | 4 -- 4 files changed, 25 insertions(+), 37 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index 071855a64..852e21b51 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -25,6 +25,14 @@ padding-top: 16px; padding-bottom: var(--block-separator-gap); margin-bottom: -29px; + + .section-content { + display: flex; + + & > .components-base-control:first-of-type { + width: 100%; + } + } } // Select-fields have a smaller gap between the header and input field. diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js index f5d8231b8..8a6e6b4bc 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js @@ -1,11 +1,12 @@ import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { help } from '@wordpress/icons'; import { StylingHooks } from '../../../../../../data'; -import { Description } from '../../../../../ReusableComponents/SettingsBlocks'; import { SelectStylingSection, StylingSection } from '../Layout'; const LocationSelector = ( { location, setLocation } ) => { - const { choices, description } = StylingHooks.useLocationProps( location ); + const { choices, details } = StylingHooks.useLocationProps( location ); return ( <> @@ -26,7 +27,13 @@ const LocationSelector = ( { location, setLocation } ) => { value={ location } onChange={ setLocation } > - { description } + { details.link && ( + From b866ac79f65837349c844d86c1550fa2746c8e5d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 16:34:06 +0100 Subject: [PATCH 277/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20location?= =?UTF-8?q?=20naming=20in=20React?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/configuration.js | 12 ++++++------ .../resources/js/data/styling/reducer.js | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/configuration.js b/modules/ppcp-settings/resources/js/data/styling/configuration.js index beed2d832..130d5451b 100644 --- a/modules/ppcp-settings/resources/js/data/styling/configuration.js +++ b/modules/ppcp-settings/resources/js/data/styling/configuration.js @@ -13,20 +13,20 @@ export const STYLING_LOCATIONS = { link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-cart', props: { layout: false, tagline: false }, }, - 'classic-checkout': { - value: 'classic-checkout', + classicCheckout: { + value: 'classicCheckout', label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-checkout', props: { layout: true, tagline: true }, }, - 'express-checkout': { - value: 'express-checkout', + expressCheckout: { + value: 'expressCheckout', label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-block-express-checkout', props: { layout: false, tagline: false }, }, - 'mini-cart': { - value: 'mini-cart', + miniCart: { + value: 'miniCart', label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-mini-cart', props: { layout: true, tagline: true }, diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index e41cd1e40..5ce6062a9 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -34,7 +34,7 @@ const defaultPersistent = Object.freeze( { shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, } ), - [ STYLING_LOCATIONS[ 'classic-checkout' ].value ]: Object.freeze( { + [ STYLING_LOCATIONS.classicCheckout.value ]: Object.freeze( { enabled: true, methods: [], label: STYLING_LABELS.checkout.value, @@ -43,14 +43,14 @@ const defaultPersistent = Object.freeze( { layout: STYLING_LAYOUTS.vertical.value, tagline: false, } ), - [ STYLING_LOCATIONS[ 'express-checkout' ].value ]: Object.freeze( { + [ STYLING_LOCATIONS.expressCheckout.value ]: Object.freeze( { enabled: true, methods: [], label: STYLING_LABELS.checkout.value, shape: STYLING_SHAPES.rect.value, color: STYLING_COLORS.gold.value, } ), - [ STYLING_LOCATIONS[ 'mini-cart' ].value ]: Object.freeze( { + [ STYLING_LOCATIONS.miniCart.value ]: Object.freeze( { enabled: true, methods: [], label: STYLING_LABELS.pay.value, From 00519d9e2541b6f2d8c90553de976b0e8e66d81d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 16:34:17 +0100 Subject: [PATCH 278/298] =?UTF-8?q?=F0=9F=94=A5=20Clean=20up=20Styling=20h?= =?UTF-8?q?ooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/hooks.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 79314e18b..e50896ea3 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -7,8 +7,7 @@ * @file */ -import { __, sprintf } from '@wordpress/i18n'; -import { useCallback } from '@wordpress/element'; // Temporary +import { useCallback } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { createHooksForStore } from '../utils'; @@ -26,8 +25,6 @@ const useHooks = () => { const { useTransient } = createHooksForStore( STORE_NAME ); const { persist, setPersistent } = useDispatch( STORE_NAME ); - // Read-only flags and derived state. - // Transient accessors. const [ isReady ] = useTransient( 'isReady' ); const [ location, setLocation ] = useTransient( 'location' ); @@ -39,13 +36,13 @@ const useHooks = () => { ); const getLocationProp = useCallback( - ( locatonId, prop ) => { - if ( undefined === persistentData[ locatonId ]?.[ prop ] ) { + ( locationId, prop ) => { + if ( undefined === persistentData[ locationId ]?.[ prop ] ) { console.error( - `Trying to access non-existent style property: ${ locatonId }.${ prop }. Possibly wrong style name - review the reducer.` + `Trying to access non-existent style property: ${ locationId }.${ prop }. Possibly wrong style name - review the reducer.` ); } - return persistentData[ locatonId ]?.[ prop ]; + return persistentData[ locationId ]?.[ prop ]; }, [ persistentData ] ); From 5e9866cde9147702022ced0c3db90d8df8ac6448 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 18:05:50 +0100 Subject: [PATCH 279/298] =?UTF-8?q?=E2=9C=A8=20Add=20data=20sanitation=20t?= =?UTF-8?q?o=20the=20Redux=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/styling/hooks.js | 74 +++++++++++++++---- .../resources/js/data/styling/reducer.js | 29 +++++++- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index e50896ea3..ffcde584f 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -83,52 +83,86 @@ export const useLocationProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); const details = STYLING_LOCATIONS[ location ] ?? {}; + const sanitize = ( value ) => ( undefined === value ? true : !! value ); + return { choices: Object.values( STYLING_LOCATIONS ), details, - isActive: getLocationProp( location, 'enabled' ), - setActive: ( state ) => setLocationProp( location, 'enabled', state ), + isActive: sanitize( getLocationProp( location, 'enabled' ) ), + setActive: ( state ) => + setLocationProp( location, 'enabled', sanitize( state ) ), }; }; export const usePaymentMethodProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); + const sanitize = ( value ) => { + if ( Array.isArray( value ) ) { + return value; + } + return value ? [ value ] : []; + }; + return { choices: Object.values( STYLING_PAYMENT_METHODS ), - paymentMethods: getLocationProp( location, 'methods' ), + paymentMethods: sanitize( getLocationProp( location, 'methods' ) ), setPaymentMethods: ( methods ) => - setLocationProp( location, 'methods', methods ), + setLocationProp( location, 'methods', sanitize( methods ) ), }; }; export const useColorProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); + const sanitize = ( value ) => { + const isValidColor = Object.values( STYLING_COLORS ).some( + ( color ) => color.value === value + ); + return isValidColor ? value : STYLING_COLORS.gold.value; + }; + return { choices: Object.values( STYLING_COLORS ), - color: getLocationProp( location, 'color' ), - setColor: ( color ) => setLocationProp( location, 'color', color ), + color: sanitize( getLocationProp( location, 'color' ) ), + setColor: ( color ) => + setLocationProp( location, 'color', sanitize( color ) ), }; }; export const useShapeProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); + const sanitize = ( value ) => { + const isValidColor = Object.values( STYLING_SHAPES ).some( + ( color ) => color.value === value + ); + return isValidColor ? value : STYLING_SHAPES.rect.value; + }; + return { choices: Object.values( STYLING_SHAPES ), - shape: getLocationProp( location, 'shape' ), - setShape: ( shape ) => setLocationProp( location, 'shape', shape ), + shape: sanitize( getLocationProp( location, 'shape' ) ), + setShape: ( shape ) => + setLocationProp( location, 'shape', sanitize( shape ) ), }; }; export const useLabelProps = ( location ) => { const { getLocationProp, setLocationProp } = useHooks(); + const sanitize = ( value ) => { + const isValidColor = Object.values( STYLING_LABELS ).some( + ( color ) => color.value === value + ); + return isValidColor ? value : STYLING_LABELS.paypal.value; + }; + return { choices: Object.values( STYLING_LABELS ), - label: getLocationProp( location, 'label' ), - setLabel: ( label ) => setLocationProp( location, 'label', label ), + label: sanitize( getLocationProp( location, 'label' ) ), + setLabel: ( label ) => + setLocationProp( location, 'label', sanitize( label ) ), }; }; @@ -137,11 +171,19 @@ export const useLayoutProps = ( location ) => { const { details } = useLocationProps( location ); const isAvailable = false !== details.props.layout; + const sanitize = ( value ) => { + const isValidColor = Object.values( STYLING_LAYOUTS ).some( + ( color ) => color.value === value + ); + return isValidColor ? value : STYLING_LAYOUTS.vertical.value; + }; + return { choices: Object.values( STYLING_LAYOUTS ), isAvailable, - layout: getLocationProp( location, 'layout' ), - setLayout: ( layout ) => setLocationProp( location, 'layout', layout ), + layout: sanitize( getLocationProp( location, 'layout' ) ), + setLayout: ( layout ) => + setLocationProp( location, 'layout', sanitize( layout ) ), }; }; @@ -155,10 +197,14 @@ export const useTaglineProps = ( location ) => { STYLING_LAYOUTS.horizontal.value === getLocationProp( location, 'layout' ); + const sanitize = ( value ) => !! value; + return { isAvailable, - tagline: isAvailable ? getLocationProp( location, 'tagline' ) : false, + tagline: isAvailable + ? sanitize( getLocationProp( location, 'tagline' ) ) + : false, setTagline: ( tagline ) => - setLocationProp( location, 'tagline', tagline ), + setLocationProp( location, 'tagline', sanitize( tagline ) ), }; }; diff --git a/modules/ppcp-settings/resources/js/data/styling/reducer.js b/modules/ppcp-settings/resources/js/data/styling/reducer.js index 5ce6062a9..db92d646b 100644 --- a/modules/ppcp-settings/resources/js/data/styling/reducer.js +++ b/modules/ppcp-settings/resources/js/data/styling/reducer.js @@ -70,6 +70,19 @@ const defaultPersistent = Object.freeze( { } ), } ); +const sanitizeLocation = ( oldDetails, newDetails ) => { + // Skip if provided details are not a plain object. + if ( + ! newDetails || + 'object' !== typeof newDetails || + Array.isArray( newDetails ) + ) { + return oldDetails; + } + + return { ...oldDetails, ...newDetails }; +}; + // Reducer logic. const [ setTransient, setPersistent ] = createSetters( @@ -96,8 +109,20 @@ const reducer = createReducer( defaultTransient, defaultPersistent, { return cleanState; }, - [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => - setPersistent( state, payload.data ), + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { + const validData = Object.keys( defaultPersistent ).reduce( + ( data, location ) => { + data[ location ] = sanitizeLocation( + state.data[ location ], + payload.data[ location ] + ); + return data; + }, + {} + ); + + return setPersistent( state, validData ); + }, } ); export default reducer; From 451b45bc10996ca94bdf9822515ca99abc3ca809 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 18:06:45 +0100 Subject: [PATCH 280/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20the=20loc?= =?UTF-8?q?ation=20styling=20DTO=20with=20new=20props?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DTO/LocationStylingDTO.php | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/src/DTO/LocationStylingDTO.php b/modules/ppcp-settings/src/DTO/LocationStylingDTO.php index 14e4471ea..30b3d0cef 100644 --- a/modules/ppcp-settings/src/DTO/LocationStylingDTO.php +++ b/modules/ppcp-settings/src/DTO/LocationStylingDTO.php @@ -16,46 +16,60 @@ namespace WooCommerce\PayPalCommerce\Settings\DTO; */ class LocationStylingDTO { /** - * The location name, e.g., card, block-checkout, ... + * The location name. * - * @var string + * @var string [cart|classic_checkout|express_checkout|mini_cart|product] */ - public string $location = ''; + public string $location; /** * Whether PayPal payments are enabled on this location. * * @var bool */ - public bool $enabled = false; + public bool $enabled; /** * List of active payment methods, e.g., 'venmo', 'applepay', ... * * @var array */ - public array $methods = array(); + public array $methods; /** * Shape of buttons on this location. * * @var string [rect|pill] */ - public string $shape = 'rect'; + public string $shape; /** * Label of the button on this location. * * @var string */ - public string $label = ''; + public string $label; /** * Color of the button on this location. * * @var string [gold|blue|silver|black|white] */ - public string $color = 'gold'; + public string $color; + + /** + * The button layout + * + * @var string [horizontal|vertical] + */ + public string $layout; + + /** + * Whether to show a tagline below the buttons. + * + * @var bool + */ + public bool $tagline; /** * Constructor. @@ -66,14 +80,18 @@ class LocationStylingDTO { * @param string $shape Shape of buttons on this location. * @param string $label Label of the button on this location. * @param string $color Color of the button on this location. + * @param string $layout Horizontal or vertical button layout. + * @param bool $tagline Whether to show a tagline below the buttons. */ public function __construct( - string $location, - bool $enabled, - array $methods, - string $shape, - string $label, - string $color + string $location = '', + bool $enabled = true, + array $methods = array(), + string $shape = 'rect', + string $label = 'pay', + string $color = 'gold', + string $layout = 'vertical', + bool $tagline = false ) { $this->location = $location; $this->enabled = $enabled; @@ -81,5 +99,7 @@ class LocationStylingDTO { $this->shape = $shape; $this->label = $label; $this->color = $color; + $this->layout = $layout; + $this->tagline = $tagline; } } From 63eb577126e27a533ffab4465417d45bb082d028 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 18:08:24 +0100 Subject: [PATCH 281/298] =?UTF-8?q?=E2=9C=A8=20Pass=20prop-name=20to=20the?= =?UTF-8?q?=20REST-sanitizer=20callback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Endpoint/RestEndpoint.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 6f1eb0e4f..7fba1529c 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -112,9 +112,9 @@ abstract class RestEndpoint extends WC_REST_Controller { if ( null === $sanitation_cb ) { $sanitized[ $key ] = $value; } elseif ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) { - $sanitized[ $key ] = $this->{$sanitation_cb}( $value ); + $sanitized[ $key ] = $this->{$sanitation_cb}( $value, $key ); } elseif ( is_callable( $sanitation_cb ) ) { - $sanitized[ $key ] = $sanitation_cb( $value ); + $sanitized[ $key ] = $sanitation_cb( $value, $key ); } } From 0b90921dd43e8af960f012e3c96aa497332c1315 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 18:09:09 +0100 Subject: [PATCH 282/298] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20minor=20psalm=20wa?= =?UTF-8?q?rning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 54aced7ef..b055ab3e2 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -90,7 +90,7 @@ class OnboardingRestEndpoint extends RestEndpoint { public function __construct( OnboardingProfile $profile ) { $this->profile = $profile; - $this->field_map['products']['sanitize'] = fn( $list ) => array_map( 'sanitize_text_field', $list ); + $this->field_map['products']['sanitize'] = static fn( $list ) => array_map( 'sanitize_text_field', $list ); } /** From 72d84546d1cd5e37232495b0fd44b8091db6ac83 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 18:12:40 +0100 Subject: [PATCH 283/298] =?UTF-8?q?=E2=9C=A8=20Process=20location-styles?= =?UTF-8?q?=20in=20REST=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Data/StylingSettings.php | 53 +++++++++++++++++- .../src/Endpoint/StylingRestEndpoint.php | 56 ++++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/src/Data/StylingSettings.php b/modules/ppcp-settings/src/Data/StylingSettings.php index ee942693c..9c81aa408 100644 --- a/modules/ppcp-settings/src/Data/StylingSettings.php +++ b/modules/ppcp-settings/src/Data/StylingSettings.php @@ -9,6 +9,8 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Data; +use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; + /** * Class StylingSettings * @@ -30,7 +32,56 @@ class StylingSettings extends AbstractDataModel { */ protected function get_defaults() : array { return array( - 'shape' => 'rect', + 'cart' => new LocationStylingDTO( 'cart' ), + 'classic_checkout' => new LocationStylingDTO( 'classic_checkout' ), + 'express_checkout' => new LocationStylingDTO( 'express_checkout' ), + 'mini_cart' => new LocationStylingDTO( 'mini_cart' ), + 'product' => new LocationStylingDTO( 'product' ), ); } + + /** + * Get styling details for Cart and Block Cart. + * + * @return LocationStylingDTO + */ + public function get_cart() : LocationStylingDTO { + return $this->data['cart']; + } + + /** + * Get styling details for Classic Checkout. + * + * @return LocationStylingDTO + */ + public function get_classic_checkout() : LocationStylingDTO { + return $this->data['classic_checkout']; + } + + /** + * Get styling details for Express Checkout. + * + * @return LocationStylingDTO + */ + public function get_express_checkout() : LocationStylingDTO { + return $this->data['express_checkout']; + } + + /** + * Get styling details for Mini Cart + * + * @return LocationStylingDTO + */ + public function get_mini_cart() : LocationStylingDTO { + return $this->data['mini_cart']; + } + + /** + * Get styling details for Product Page. + * + * @return LocationStylingDTO + */ + public function get_product() : LocationStylingDTO { + return $this->data['product']; + } } diff --git a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php index 97b1158f2..8ad08f631 100644 --- a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php @@ -13,6 +13,7 @@ use WP_REST_Server; use WP_REST_Response; use WP_REST_Request; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; +use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; /** * REST controller for the "Styling" settings tab. @@ -41,8 +42,20 @@ class StylingRestEndpoint extends RestEndpoint { * @var array */ private array $field_map = array( - 'shape' => array( - 'js_name' => 'shape', + 'cart' => array( + 'js_name' => 'cart', + ), + 'classic_checkout' => array( + 'js_name' => 'classicCheckout', + ), + 'express_checkout' => array( + 'js_name' => 'expressCheckout', + ), + 'mini_cart' => array( + 'js_name' => 'miniCart', + ), + 'product' => array( + 'js_name' => 'product', ), ); @@ -53,6 +66,12 @@ class StylingRestEndpoint extends RestEndpoint { */ public function __construct( StylingSettings $settings ) { $this->settings = $settings; + + $this->field_map['cart']['sanitize'] = array( $this, 'to_location' ); + $this->field_map['classic_checkout']['sanitize'] = array( $this, 'to_location' ); + $this->field_map['express_checkout']['sanitize'] = array( $this, 'to_location' ); + $this->field_map['mini_cart']['sanitize'] = array( $this, 'to_location' ); + $this->field_map['product']['sanitize'] = array( $this, 'to_location' ); } /** @@ -123,4 +142,37 @@ class StylingRestEndpoint extends RestEndpoint { return $this->get_details(); } + + /** + * Converts the plain location-style input to a structured DTO. + * + * @param array $data Raw data received from the request. + * @param string $key The field name. + * + * @return LocationStylingDTO + */ + protected function to_location( array $data, string $key ) : LocationStylingDTO { + $is_enabled = ! isset( $data['enabled'] ) || $data['enabled']; + $methods = array(); + $shape = sanitize_text_field( $data['shape'] ?? 'rect' ); + $label = sanitize_text_field( $data['label'] ?? 'pay' ); + $color = sanitize_text_field( $data['color'] ?? 'gold' ); + $layout = sanitize_text_field( $data['layout'] ?? 'vertical' ); + $tagline = isset( $data['tagline'] ) && $data['tagline']; + + if ( isset( $data['methods'] ) && is_array( $data['methods'] ) ) { + $methods = array_map( 'sanitize_text_field', $data['methods'] ); + } + + return new LocationStylingDTO( + $key, + $is_enabled, + $methods, + $shape, + $label, + $color, + $layout, + $tagline, + ); + } } From 57afe4b95e29708233ac5d9f4504b3eac6587050 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 18:40:43 +0100 Subject: [PATCH 284/298] =?UTF-8?q?=E2=9C=A8=20Extract=20sanitizer=20logic?= =?UTF-8?q?=20to=20new=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 9 +- .../src/Endpoint/StylingRestEndpoint.php | 74 +++++++-------- .../src/Service/DataSanitizer.php | 89 +++++++++++++++++++ 3 files changed, 130 insertions(+), 42 deletions(-) create mode 100644 modules/ppcp-settings/src/Service/DataSanitizer.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 50afb3fdb..117833160 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -26,6 +26,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; +use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -76,7 +77,10 @@ return array( return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); }, 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint { - return new StylingRestEndpoint( $container->get( 'settings.data.styling' ) ); + return new StylingRestEndpoint( + $container->get( 'settings.data.styling' ), + $container->get( 'settings.service.sanitizer' ) + ); }, 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint { return new RefreshFeatureStatusEndpoint( @@ -195,6 +199,9 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ), ); }, + 'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer { + return new DataSanitizer(); + }, 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { return new SwitchSettingsUiEndpoint( $container->get( 'woocommerce.logger.woocommerce' ), diff --git a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php index 8ad08f631..450549e43 100644 --- a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php @@ -14,6 +14,7 @@ use WP_REST_Response; use WP_REST_Request; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; +use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer; /** * REST controller for the "Styling" settings tab. @@ -36,6 +37,13 @@ class StylingRestEndpoint extends RestEndpoint { */ protected StylingSettings $settings; + /** + * Data sanitizer service. + * + * @var DataSanitizer + */ + protected DataSanitizer $sanitizer; + /** * Field mapping for request to profile transformation. * @@ -62,16 +70,33 @@ class StylingRestEndpoint extends RestEndpoint { /** * Constructor. * - * @param StylingSettings $settings The settings instance. + * @param StylingSettings $settings The settings instance. + * @param DataSanitizer $sanitizer Data sanitizer service. */ - public function __construct( StylingSettings $settings ) { - $this->settings = $settings; + public function __construct( StylingSettings $settings, DataSanitizer $sanitizer ) { + $this->settings = $settings; + $this->sanitizer = $sanitizer; - $this->field_map['cart']['sanitize'] = array( $this, 'to_location' ); - $this->field_map['classic_checkout']['sanitize'] = array( $this, 'to_location' ); - $this->field_map['express_checkout']['sanitize'] = array( $this, 'to_location' ); - $this->field_map['mini_cart']['sanitize'] = array( $this, 'to_location' ); - $this->field_map['product']['sanitize'] = array( $this, 'to_location' ); + $this->field_map['cart']['sanitize'] = array( + $this->sanitizer, + 'sanitize_location_style', + ); + $this->field_map['classic_checkout']['sanitize'] = array( + $this->sanitizer, + 'sanitize_location_style', + ); + $this->field_map['express_checkout']['sanitize'] = array( + $this->sanitizer, + 'sanitize_location_style', + ); + $this->field_map['mini_cart']['sanitize'] = array( + $this->sanitizer, + 'sanitize_location_style', + ); + $this->field_map['product']['sanitize'] = array( + $this->sanitizer, + 'sanitize_location_style', + ); } /** @@ -142,37 +167,4 @@ class StylingRestEndpoint extends RestEndpoint { return $this->get_details(); } - - /** - * Converts the plain location-style input to a structured DTO. - * - * @param array $data Raw data received from the request. - * @param string $key The field name. - * - * @return LocationStylingDTO - */ - protected function to_location( array $data, string $key ) : LocationStylingDTO { - $is_enabled = ! isset( $data['enabled'] ) || $data['enabled']; - $methods = array(); - $shape = sanitize_text_field( $data['shape'] ?? 'rect' ); - $label = sanitize_text_field( $data['label'] ?? 'pay' ); - $color = sanitize_text_field( $data['color'] ?? 'gold' ); - $layout = sanitize_text_field( $data['layout'] ?? 'vertical' ); - $tagline = isset( $data['tagline'] ) && $data['tagline']; - - if ( isset( $data['methods'] ) && is_array( $data['methods'] ) ) { - $methods = array_map( 'sanitize_text_field', $data['methods'] ); - } - - return new LocationStylingDTO( - $key, - $is_enabled, - $methods, - $shape, - $label, - $color, - $layout, - $tagline, - ); - } } diff --git a/modules/ppcp-settings/src/Service/DataSanitizer.php b/modules/ppcp-settings/src/Service/DataSanitizer.php new file mode 100644 index 000000000..d7dccbbce --- /dev/null +++ b/modules/ppcp-settings/src/Service/DataSanitizer.php @@ -0,0 +1,89 @@ +sanitize_bool( $data['enabled'] ?? true ); + $shape = $this->sanitize_text( $data['shape'] ?? 'rect' ); + $label = $this->sanitize_text( $data['label'] ?? 'pay' ); + $color = $this->sanitize_text( $data['color'] ?? 'gold' ); + $layout = $this->sanitize_text( $data['layout'] ?? 'vertical' ); + $tagline = $this->sanitize_bool( $data['tagline'] ?? false ); + $methods = $this->sanitize_array( + $data['methods'] ?? array(), + array( $this, 'sanitize_text' ) + ); + + return new LocationStylingDTO( + $location, + $is_enabled, + $methods, + $shape, + $label, + $color, + $layout, + $tagline + ); + } + + /** + * Helper. Ensures the value is a string. + * + * @param mixed $value Value to sanitize. + * @param string $default Default value. + * @return string Sanitized string. + */ + protected function sanitize_text( $value, string $default = '' ) : string { + return sanitize_text_field( $value ?? $default ); + } + + /** + * Helper. Ensures the value is a boolean. + * + * @param mixed $value Value to sanitize. + * @return bool Sanitized boolean. + */ + protected function sanitize_bool( $value ) : bool { + return filter_var( $value, FILTER_VALIDATE_BOOLEAN ); + } + + /** + * Helper. Ensures the value is an array and all items are sanitized. + * + * @param null|array $array Value to sanitize. + * @param callable $sanitize_callback Callback to sanitize each item in the array. + * @return array Array with sanitized items. + */ + protected function sanitize_array( ?array $array, callable $sanitize_callback ) : array { + if ( ! is_array( $array ) ) { + return array(); + } + + return array_map( $sanitize_callback, $array ); + } +} From 605760f5fb67247b8d1cc0af99b28c2f0bbc38b6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:03:23 +0100 Subject: [PATCH 285/298] =?UTF-8?q?=E2=9C=A8=20Add=20data=20sanitizer=20to?= =?UTF-8?q?=20styling=20data=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 4 +- .../src/Data/StylingSettings.php | 71 +++++++++++++++++++ .../src/Service/DataSanitizer.php | 20 +++++- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 117833160..7bfd3fbff 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -68,7 +68,9 @@ return array( ); }, 'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings { - return new StylingSettings(); + return new StylingSettings( + $container->get( 'settings.service.sanitizer' ) + ); }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); diff --git a/modules/ppcp-settings/src/Data/StylingSettings.php b/modules/ppcp-settings/src/Data/StylingSettings.php index 9c81aa408..32fc3de2f 100644 --- a/modules/ppcp-settings/src/Data/StylingSettings.php +++ b/modules/ppcp-settings/src/Data/StylingSettings.php @@ -9,7 +9,9 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Data; +use RuntimeException; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; +use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer; /** * Class StylingSettings @@ -25,6 +27,25 @@ class StylingSettings extends AbstractDataModel { */ protected const OPTION_KEY = 'woocommerce-ppcp-data-styling'; + /** + * Data sanitizer service. + * + * @var DataSanitizer + */ + protected DataSanitizer $sanitizer; + + /** + * Constructor. + * + * @param DataSanitizer $sanitizer Data sanitizer service. + * @throws RuntimeException If the OPTION_KEY is not defined in the child class. + */ + public function __construct( DataSanitizer $sanitizer ) { + $this->sanitizer = $sanitizer; + + parent::__construct(); + } + /** * Get default values for the model. * @@ -49,6 +70,16 @@ class StylingSettings extends AbstractDataModel { return $this->data['cart']; } + /** + * Set styling details for Cart and Block Cart. + * + * @param mixed $styles The new styling details. + * @return void + */ + public function set_cart( $styles ) : void { + $this->data['cart'] = $this->sanitizer->sanitize_location_style( $styles ); + } + /** * Get styling details for Classic Checkout. * @@ -58,6 +89,16 @@ class StylingSettings extends AbstractDataModel { return $this->data['classic_checkout']; } + /** + * Set styling details for Classic Checkout. + * + * @param mixed $styles The new styling details. + * @return void + */ + public function set_classic_checkout( $styles ) : void { + $this->data['classic_checkout'] = $this->sanitizer->sanitize_location_style( $styles ); + } + /** * Get styling details for Express Checkout. * @@ -67,6 +108,16 @@ class StylingSettings extends AbstractDataModel { return $this->data['express_checkout']; } + /** + * Set styling details for Express Checkout. + * + * @param mixed $styles The new styling details. + * @return void + */ + public function set_express_checkout( $styles ) : void { + $this->data['express_checkout'] = $this->sanitizer->sanitize_location_style( $styles ); + } + /** * Get styling details for Mini Cart * @@ -76,6 +127,16 @@ class StylingSettings extends AbstractDataModel { return $this->data['mini_cart']; } + /** + * Set styling details for Mini Cart. + * + * @param mixed $styles The new styling details. + * @return void + */ + public function set_mini_cart( $styles ) : void { + $this->data['mini_cart'] = $this->sanitizer->sanitize_location_style( $styles ); + } + /** * Get styling details for Product Page. * @@ -84,4 +145,14 @@ class StylingSettings extends AbstractDataModel { public function get_product() : LocationStylingDTO { return $this->data['product']; } + + /** + * Set styling details for Product Page. + * + * @param mixed $styles The new styling details. + * @return void + */ + public function set_product( $styles ) : void { + $this->data['product'] = $this->sanitizer->sanitize_location_style( $styles ); + } } diff --git a/modules/ppcp-settings/src/Service/DataSanitizer.php b/modules/ppcp-settings/src/Service/DataSanitizer.php index d7dccbbce..3baab9034 100644 --- a/modules/ppcp-settings/src/Service/DataSanitizer.php +++ b/modules/ppcp-settings/src/Service/DataSanitizer.php @@ -19,11 +19,27 @@ class DataSanitizer { /** * Sanitizes the provided styling data. * - * @param array $data The styling data to sanitize. + * @param mixed $data The styling data to sanitize. * @param ?string $location Name of the location. * @return LocationStylingDTO Styling data. */ - public function sanitize_location_style( array $data, string $location = null ) : LocationStylingDTO { + public function sanitize_location_style( $data, string $location = null ) : LocationStylingDTO { + if ( $data instanceof LocationStylingDTO ) { + if ( $location ) { + $data->location = $location; + } + + return $data; + } + + if ( is_object( $data ) ) { + $data = (array) $data; + } + + if ( ! is_array( $data ) ) { + return new LocationStylingDTO( $location ?? '' ); + } + if ( null === $location ) { $location = $data['location'] ?? ''; } From 7365f75eba0ae896463b3f09be544de1374d3388 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:05:51 +0100 Subject: [PATCH 286/298] =?UTF-8?q?=F0=9F=90=9B=20Add=20missing=20action?= =?UTF-8?q?=20dispatcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/styling/actions.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/styling/actions.js b/modules/ppcp-settings/resources/js/data/styling/actions.js index 095a54c91..f7bdb4339 100644 --- a/modules/ppcp-settings/resources/js/data/styling/actions.js +++ b/modules/ppcp-settings/resources/js/data/styling/actions.js @@ -60,6 +60,14 @@ export const setPersistent = ( prop, value ) => ( { payload: { [ prop ]: value }, } ); +/** + * Transient. Changes the "ready-state" of the module. + * + * @param {boolean} state Whether the store is ready to be used. + * @return {Action} The action. + */ +export const setIsReady = ( state ) => setTransient( 'isReady', state ); + /** * Side effect. Triggers the persistence of store data to the server. * From 1726878f554ed5c3eaee43eb7cfc8734d17e3207 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:07:26 +0100 Subject: [PATCH 287/298] =?UTF-8?q?=E2=9C=A8=20Add=20busy-state=20to=20the?= =?UTF-8?q?=20save=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/Settings/Components/Navigation.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js index 5f9c19640..b15f1beea 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js @@ -2,27 +2,33 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import TopNavigation from '../../../ReusableComponents/TopNavigation'; -import { StylingHooks } from '../../../../data'; +import { CommonHooks, StylingHooks } from '../../../../data'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; const SettingsNavigation = () => { + const { withActivity, isBusy } = CommonHooks.useBusyState(); + + // Todo: Implement other stores here. const { persist: persistStyling } = StylingHooks.useStore(); - const isBusy = false; // TODO: Implement loading state. const handleSaveClick = () => { - persistStyling(); + // Todo: Add other stores here. + withActivity( + 'persist-styling', + 'Save styling details', + persistStyling + ); }; const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); return ( - + + + ); }; From cd63b186087d9dda114bb654d8d0ff81e49446a3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:22:01 +0100 Subject: [PATCH 288/298] =?UTF-8?q?=F0=9F=8E=A8=20Minor=20code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/Screens/Settings/Components/Navigation.js | 4 ++-- modules/ppcp-settings/resources/js/data/styling/hooks.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js index b15f1beea..9023a1f9b 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js @@ -1,12 +1,12 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import TopNavigation from '../../../ReusableComponents/TopNavigation'; import { CommonHooks, StylingHooks } from '../../../../data'; +import TopNavigation from '../../../ReusableComponents/TopNavigation'; import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; const SettingsNavigation = () => { - const { withActivity, isBusy } = CommonHooks.useBusyState(); + const { withActivity } = CommonHooks.useBusyState(); // Todo: Implement other stores here. const { persist: persistStyling } = StylingHooks.useStore(); diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index ffcde584f..8227d0124 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -41,8 +41,9 @@ const useHooks = () => { console.error( `Trying to access non-existent style property: ${ locationId }.${ prop }. Possibly wrong style name - review the reducer.` ); + return null; } - return persistentData[ locationId ]?.[ prop ]; + return persistentData[ locationId ][ prop ]; }, [ persistentData ] ); From 10129d49b55c9e142ee70aa41c9f9b661d54c7ca Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:25:10 +0100 Subject: [PATCH 289/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Apply=20new=20code?= =?UTF-8?q?=20style=20to=20example=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/_example/actions.js | 41 +++++++++++-------- .../resources/js/data/_example/hooks.js | 27 +++--------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/_example/actions.js b/modules/ppcp-settings/resources/js/data/_example/actions.js index 7360424f4..59d68d37c 100644 --- a/modules/ppcp-settings/resources/js/data/_example/actions.js +++ b/modules/ppcp-settings/resources/js/data/_example/actions.js @@ -36,28 +36,37 @@ export const hydrate = ( payload ) => ( { payload, } ); +/** + * Generic transient-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setTransient = ( prop, value ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { [ prop ]: value }, +} ); + +/** + * Generic persistent-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setPersistent = ( prop, value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { [ prop ]: value }, +} ); + /** * Transient. Marks the store as "ready", i.e., fully initialized. * * @param {boolean} isReady * @return {Action} The action. */ -export const setIsReady = ( isReady ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, -} ); - -/** - * Persistent. Sets a sample value. - * TODO: Replace with a real action/property. - * - * @param {string} value - * @return {Action} The action. - */ -export const setSampleValue = ( value ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { sampleValue: value }, -} ); +export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady ); /** * Side effect. Triggers the persistence of store data to the server. diff --git a/modules/ppcp-settings/resources/js/data/_example/hooks.js b/modules/ppcp-settings/resources/js/data/_example/hooks.js index 394fceb7e..b6878c2a9 100644 --- a/modules/ppcp-settings/resources/js/data/_example/hooks.js +++ b/modules/ppcp-settings/resources/js/data/_example/hooks.js @@ -7,39 +7,24 @@ * @file */ -import { useSelect, useDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; +import { createHooksForStore } from '../utils'; import { STORE_NAME } from './constants'; -const useTransient = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).transientData()?.[ key ], - [ key ] - ); - -const usePersistent = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).persistentData()?.[ key ], - [ key ] - ); - const useHooks = () => { - const { - persist, - - // TODO: Replace with real property. - setSampleValue, - } = useDispatch( STORE_NAME ); + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + const { persist } = useDispatch( STORE_NAME ); // Read-only flags and derived state. // Nothing here yet. // Transient accessors. - const isReady = useTransient( 'isReady' ); + const [ isReady ] = useTransient( 'isReady' ); // Persistent accessors. // TODO: Replace with real property. - const sampleValue = usePersistent( 'sampleValue' ); + const [ sampleValue, setSampleValue ] = usePersistent( 'sampleValue' ); return { persist, From 4d38c15b2975ee0b9fa53908d46095ec7a9f0db7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:25:44 +0100 Subject: [PATCH 290/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Apply=20new=20code?= =?UTF-8?q?=20style=20to=20onboarding=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/actions.js | 73 +++++++++++-------- .../resources/js/data/onboarding/hooks.js | 50 +++++-------- 2 files changed, 59 insertions(+), 64 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index e9bf8ed5f..8c6f2999f 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -36,16 +36,37 @@ export const hydrate = ( payload ) => ( { payload, } ); +/** + * Generic transient-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setTransient = ( prop, value ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { [ prop ]: value }, +} ); + +/** + * Generic persistent-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setPersistent = ( prop, value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { [ prop ]: value }, +} ); + /** * Transient. Marks the onboarding details as "ready", i.e., fully initialized. * * @param {boolean} isReady * @return {Action} The action. */ -export const setIsReady = ( isReady ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, -} ); +export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady ); /** * Transient. Sets the "manualClientId" value. @@ -53,10 +74,8 @@ export const setIsReady = ( isReady ) => ( { * @param {string} manualClientId * @return {Action} The action. */ -export const setManualClientId = ( manualClientId ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { manualClientId }, -} ); +export const setManualClientId = ( manualClientId ) => + setTransient( 'manualClientId', manualClientId ); /** * Transient. Sets the "manualClientSecret" value. @@ -64,10 +83,8 @@ export const setManualClientId = ( manualClientId ) => ( { * @param {string} manualClientSecret * @return {Action} The action. */ -export const setManualClientSecret = ( manualClientSecret ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { manualClientSecret }, -} ); +export const setManualClientSecret = ( manualClientSecret ) => + setTransient( 'manualClientSecret', manualClientSecret ); /** * Persistent.Set the "onboarding completed" flag which shows or hides the wizard. @@ -75,10 +92,8 @@ export const setManualClientSecret = ( manualClientSecret ) => ( { * @param {boolean} completed * @return {Action} The action. */ -export const setCompleted = ( completed ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { completed }, -} ); +export const setCompleted = ( completed ) => + setPersistent( 'completed', completed ); /** * Persistent. Sets the onboarding wizard to a new step. @@ -86,10 +101,7 @@ export const setCompleted = ( completed ) => ( { * @param {number} step * @return {Action} The action. */ -export const setStep = ( step ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { step }, -} ); +export const setStep = ( step ) => setPersistent( 'step', step ); /** * Persistent. Sets the "isCasualSeller" value. @@ -97,10 +109,8 @@ export const setStep = ( step ) => ( { * @param {boolean} isCasualSeller * @return {Action} The action. */ -export const setIsCasualSeller = ( isCasualSeller ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { isCasualSeller }, -} ); +export const setIsCasualSeller = ( isCasualSeller ) => + setPersistent( 'isCasualSeller', isCasualSeller ); /** * Persistent. Sets the "areOptionalPaymentMethodsEnabled" value. @@ -110,10 +120,11 @@ export const setIsCasualSeller = ( isCasualSeller ) => ( { */ export const setAreOptionalPaymentMethodsEnabled = ( areOptionalPaymentMethodsEnabled -) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { areOptionalPaymentMethodsEnabled }, -} ); +) => + setPersistent( + 'areOptionalPaymentMethodsEnabled', + areOptionalPaymentMethodsEnabled + ); /** * Persistent. Sets the "products" array. @@ -121,10 +132,8 @@ export const setAreOptionalPaymentMethodsEnabled = ( * @param {string[]} products * @return {Action} The action. */ -export const setProducts = ( products ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { products }, -} ); +export const setProducts = ( products ) => + setPersistent( 'products', products ); /** * Side effect. Triggers the persistence of onboarding data to the server. diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index 2d1542f68..70999960c 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -9,32 +9,14 @@ import { useSelect, useDispatch } from '@wordpress/data'; +import { createHooksForStore } from '../utils'; import { PRODUCT_TYPES } from './configuration'; import { STORE_NAME } from './constants'; -const useTransient = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).transientData()?.[ key ], - [ key ] - ); - -const usePersistent = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).persistentData()?.[ key ], - [ key ] - ); - const useHooks = () => { - const { - persist, - setStep, - setCompleted, - setIsCasualSeller, - setManualClientId, - setManualClientSecret, - setAreOptionalPaymentMethodsEnabled, - setProducts, - } = useDispatch( STORE_NAME ); + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + + const { persist } = useDispatch( STORE_NAME ); // Read-only flags and derived state. const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); @@ -44,18 +26,22 @@ const useHooks = () => { ); // Transient accessors. - const isReady = useTransient( 'isReady' ); - const manualClientId = useTransient( 'manualClientId' ); - const manualClientSecret = useTransient( 'manualClientSecret' ); + const [ isReady ] = useTransient( 'isReady' ); + const [ manualClientId, setManualClientId ] = + useTransient( 'manualClientId' ); + const [ manualClientSecret, setManualClientSecret ] = + useTransient( 'manualClientSecret' ); // Persistent accessors. - const step = usePersistent( 'step' ); - const completed = usePersistent( 'completed' ); - const isCasualSeller = usePersistent( 'isCasualSeller' ); - const areOptionalPaymentMethodsEnabled = usePersistent( - 'areOptionalPaymentMethodsEnabled' - ); - const products = usePersistent( 'products' ); + const [ step, setStep ] = usePersistent( 'step' ); + const [ completed, setCompleted ] = usePersistent( 'completed' ); + const [ isCasualSeller, setIsCasualSeller ] = + usePersistent( 'isCasualSeller' ); + const [ + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, + ] = usePersistent( 'areOptionalPaymentMethodsEnabled' ); + const [ products, setProducts ] = usePersistent( 'products' ); const savePersistent = async ( setter, value ) => { setter( value ); From 77ed657394ff35a6275faa0c4769c131e401279f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 17 Jan 2025 19:25:55 +0100 Subject: [PATCH 291/298] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Apple=20new=20code?= =?UTF-8?q?=20style=20to=20common=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/actions.js | 58 ++++++++++--------- .../resources/js/data/common/hooks.js | 28 +++------ 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 8f2b14812..cf7837d54 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -36,16 +36,37 @@ export const hydrate = ( payload ) => ( { payload, } ); +/** + * Generic transient-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setTransient = ( prop, value ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { [ prop ]: value }, +} ); + +/** + * Generic persistent-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setPersistent = ( prop, value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { [ prop ]: value }, +} ); + /** * Transient. Marks the onboarding details as "ready", i.e., fully initialized. * * @param {boolean} isReady * @return {Action} The action. */ -export const setIsReady = ( isReady ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, -} ); +export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady ); /** * Transient. Sets the active settings tab. @@ -53,21 +74,8 @@ export const setIsReady = ( isReady ) => ( { * @param {string} activeModal * @return {Action} The action. */ -export const setActiveModal = ( activeModal ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { activeModal }, -} ); - -/** - * Transient. Changes the "saving" flag. - * - * @param {boolean} isSaving - * @return {Action} The action. - */ -export const setIsSaving = ( isSaving ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isSaving }, -} ); +export const setActiveModal = ( activeModal ) => + setTransient( 'activeModal', activeModal ); /** * Transient (Activity): Marks the start of an async activity @@ -107,10 +115,8 @@ export const stopActivity = ( id ) => ( { * @param {boolean} useSandbox * @return {Action} The action. */ -export const setSandboxMode = ( useSandbox ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { useSandbox }, -} ); +export const setSandboxMode = ( useSandbox ) => + setPersistent( 'useSandbox', useSandbox ); /** * Persistent. Toggles the "Manual Connection" mode on or off. @@ -118,10 +124,8 @@ export const setSandboxMode = ( useSandbox ) => ( { * @param {boolean} useManualConnection * @return {Action} The action. */ -export const setManualConnectionMode = ( useManualConnection ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { useManualConnection }, -} ); +export const setManualConnectionMode = ( useManualConnection ) => + setPersistent( 'useManualConnection', useManualConnection ); /** * Side effect. Saves the persistent details to the WP database. diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 8fcf3f6a9..796d1c50c 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -9,41 +9,31 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { useCallback } from '@wordpress/element'; + +import { createHooksForStore } from '../utils'; import { STORE_NAME } from './constants'; -const useTransient = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).transientData()?.[ key ], - [ key ] - ); - -const usePersistent = ( key ) => - useSelect( - ( select ) => select( STORE_NAME ).persistentData()?.[ key ], - [ key ] - ); - const useHooks = () => { + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); const { persist, - setSandboxMode, - setManualConnectionMode, sandboxOnboardingUrl, productionOnboardingUrl, authenticateWithCredentials, authenticateWithOAuth, - setActiveModal, startWebhookSimulation, checkWebhookSimulationState, } = useDispatch( STORE_NAME ); // Transient accessors. - const isReady = useTransient( 'isReady' ); - const activeModal = useTransient( 'activeModal' ); + const [ isReady ] = useTransient( 'isReady' ); + const [ activeModal, setActiveModal ] = useTransient( 'activeModal' ); // Persistent accessors. - const isSandboxMode = usePersistent( 'useSandbox' ); - const isManualConnectionMode = usePersistent( 'useManualConnection' ); + const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' ); + const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent( + 'useManualConnection' + ); const merchant = useSelect( ( select ) => select( STORE_NAME ).merchant(), [] From bbd707c12c8e8260b879c9c265879d5310aaa0bc Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 20 Jan 2025 10:25:06 +0100 Subject: [PATCH 292/298] PHPCS fixes --- .../src/Endpoint/SettingsRestEndpoint.php | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php index 0b154b3a8..d18789e25 100644 --- a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php @@ -7,7 +7,6 @@ * * @package WooCommerce\PayPalCommerce\Settings\Endpoint */ - declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; @@ -23,6 +22,7 @@ use WP_REST_Response; * Handles REST API endpoints for managing PayPal settings (Settings Tab). */ class SettingsRestEndpoint extends RestEndpoint { + /** * The REST API endpoint base. * @@ -54,15 +54,15 @@ class SettingsRestEndpoint extends RestEndpoint { /** * SettingsRestEndpoint constructor. * - * @param SettingsModel $settings The settings model instance. - * @param LoggerInterface $logger The logger instance. + * @param SettingsModel $settings The settings model instance. + * @param LoggerInterface $logger The logger instance. */ public function __construct( SettingsModel $settings, LoggerInterface $logger ) { $this->settings = $settings; - $this->logger = $logger; + $this->logger = $logger; } /** @@ -92,26 +92,26 @@ class SettingsRestEndpoint extends RestEndpoint { * * @param WP_REST_Request $request The request instance. * @return WP_REST_Response The response containing settings data or error details. + * @throws \Exception When encoding settings data fails. */ public function get_settings( WP_REST_Request $request ): WP_REST_Response { try { - // Get settings data + // Get settings data. $data = $this->settings->get(); - // Ensure the data is JSON-encodable - $encoded = json_encode( $data ); + // Ensure the data is JSON-encodable. + $encoded = wp_json_encode( $data ); if ( $encoded === false ) { throw new \Exception( 'Failed to encode settings data: ' . json_last_error_msg() ); } - // Create response with pre-verified JSON data + // Create response with pre-verified JSON data. $response_data = array( 'success' => true, 'data' => json_decode( $encoded, true ), ); return new WP_REST_Response( $response_data, 200 ); - } catch ( \Exception $error ) { return new WP_REST_Response( array( @@ -128,16 +128,16 @@ class SettingsRestEndpoint extends RestEndpoint { * * @param WP_REST_Request $request The request instance containing new settings. * @return WP_REST_Response The response containing updated settings or error details. + * @throws \Exception When encoding updated settings fails. */ public function update_settings( WP_REST_Request $request ): WP_REST_Response { try { $data = $request->get_json_params(); - $this->settings->update( $data ); $updated_data = $this->settings->get(); - // Verify JSON encoding - $encoded = json_encode( $updated_data ); + // Verify JSON encoding. + $encoded = wp_json_encode( $updated_data ); if ( $encoded === false ) { throw new \Exception( 'Failed to encode updated settings: ' . json_last_error_msg() ); } @@ -149,7 +149,6 @@ class SettingsRestEndpoint extends RestEndpoint { ), 200 ); - } catch ( \Exception $error ) { return new WP_REST_Response( array( From 5fb19675d5616463a26f0d5355ef058733bcdd12 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 20 Jan 2025 11:14:27 +0100 Subject: [PATCH 293/298] Fix PHPCS issues --- .../ppcp-settings/src/Endpoint/SettingsRestEndpoint.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php index d18789e25..5b82c57d9 100644 --- a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php @@ -7,6 +7,7 @@ * * @package WooCommerce\PayPalCommerce\Settings\Endpoint */ + declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; @@ -54,15 +55,15 @@ class SettingsRestEndpoint extends RestEndpoint { /** * SettingsRestEndpoint constructor. * - * @param SettingsModel $settings The settings model instance. - * @param LoggerInterface $logger The logger instance. + * @param SettingsModel $settings The settings model instance. + * @param LoggerInterface $logger The logger instance. */ public function __construct( SettingsModel $settings, LoggerInterface $logger ) { $this->settings = $settings; - $this->logger = $logger; + $this->logger = $logger; } /** From 78e524f0f2276e6685e6448e8a91b6d564d90d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Mon, 20 Jan 2025 12:03:38 +0100 Subject: [PATCH 294/298] remove all tokens in continuation mode --- modules/ppcp-vaulting/src/VaultingModule.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 7fe87836b..427ff79c1 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -144,9 +144,7 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule if ( is_checkout() && ! $is_post && $this->is_paypal_continuation() ) { foreach ( $tokens as $index => $token ) { - if ( $token instanceof \WC_Payment_Token_CC && $token->get_gateway_id() === CreditCardGateway::ID ) { - unset( $tokens[ $index ] ); - } + unset( $tokens[ $index ] ); } } From 2bea225d9b831a07f4748298583526d25a7bd8f5 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 20 Jan 2025 16:04:35 +0100 Subject: [PATCH 295/298] Connect enable payment method to data store --- .../SettingsBlocks/PaymentMethodsBlock.js | 24 +- .../resources/js/data/payment/actions.js | 24 +- .../resources/js/data/payment/hooks.js | 21 +- modules/ppcp-settings/services.php | 5 +- .../src/Data/PaymentSettings.php | 210 ------------------ .../src/Endpoint/PaymentRestEndpoint.php | 2 +- 6 files changed, 30 insertions(+), 256 deletions(-) delete mode 100644 modules/ppcp-settings/src/Data/PaymentSettings.php diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js index 17f610660..1e1e9ee30 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js @@ -1,25 +1,25 @@ -import { useState, useCallback } from '@wordpress/element'; import SettingsBlock from './SettingsBlock'; import PaymentMethodItemBlock from './PaymentMethodItemBlock'; +import { usePaymentMethods } from '../../../data/payment/hooks'; const PaymentMethodsBlock = ( { paymentMethods, className = '', onTriggerModal, } ) => { - const [ selectedMethods, setSelectedMethods ] = useState( {} ); - - const handleSelect = useCallback( ( methodId, isSelected ) => { - setSelectedMethods( ( prev ) => ( { - ...prev, - [ methodId ]: isSelected, - } ) ); - }, [] ); + const { setPersistent } = usePaymentMethods(); if ( ! paymentMethods?.length ) { return null; } + const handleSelect = ( paymentMethod, isSelected ) => { + setPersistent( paymentMethod.id, { + ...paymentMethod, + enabled: isSelected, + } ); + }; + return ( - handleSelect( paymentMethod.id, checked ) + handleSelect( paymentMethod, checked ) } onTriggerModal={ () => onTriggerModal?.( paymentMethod.id ) diff --git a/modules/ppcp-settings/resources/js/data/payment/actions.js b/modules/ppcp-settings/resources/js/data/payment/actions.js index 7360424f4..95c0235b1 100644 --- a/modules/ppcp-settings/resources/js/data/payment/actions.js +++ b/modules/ppcp-settings/resources/js/data/payment/actions.js @@ -47,18 +47,6 @@ export const setIsReady = ( isReady ) => ( { payload: { isReady }, } ); -/** - * Persistent. Sets a sample value. - * TODO: Replace with a real action/property. - * - * @param {string} value - * @return {Action} The action. - */ -export const setSampleValue = ( value ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { sampleValue: value }, -} ); - /** * Side effect. Triggers the persistence of store data to the server. * @@ -69,3 +57,15 @@ export const persist = function* () { yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; }; + +/** + * Generic persistent-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Action} The action. + */ +export const setPersistent = ( prop, value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { [ prop ]: value }, +} ); diff --git a/modules/ppcp-settings/resources/js/data/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js index 54b6d9863..c10f0969e 100644 --- a/modules/ppcp-settings/resources/js/data/payment/hooks.js +++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js @@ -24,12 +24,7 @@ const usePersistent = ( key ) => ); const useHooks = () => { - const { - persist, - - // TODO: Replace with real property. - setSampleValue, - } = useDispatch( STORE_NAME ); + const { persist, setPersistent } = useDispatch( STORE_NAME ); // Read-only flags and derived state. // Nothing here yet. @@ -37,9 +32,6 @@ const useHooks = () => { // Transient accessors. const isReady = useTransient( 'isReady' ); - // Persistent accessors. - const sampleValue = usePersistent( 'sampleValue' ); - // PayPal checkout. const paypal = usePersistent( 'ppcp-gateway' ); const venmo = usePersistent( 'venmo' ); @@ -67,8 +59,7 @@ const useHooks = () => { return { persist, isReady, - sampleValue, - setSampleValue, + setPersistent, paypal, venmo, payLater, @@ -95,13 +86,11 @@ export const useState = () => { return { persist, isReady }; }; -// TODO: Replace with real hook. -export const useSampleValue = () => { - const { sampleValue, setSampleValue } = useHooks(); +export const usePaymentMethods = () => { + const { setPersistent } = useHooks(); return { - sampleValue, - setSampleValue, + setPersistent, }; }; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 838ebe3d5..e66a919d5 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -66,9 +66,6 @@ return array( $container->get( 'wcgateway.is-send-only-country' ) ); }, - 'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings { - return new PaymentSettings(); - }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); }, @@ -76,7 +73,7 @@ return array( return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); }, 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { - return new PaymentRestEndpoint( $container->get( 'settings.data.payment' ) ); + return new PaymentRestEndpoint(); }, 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint { return new RefreshFeatureStatusEndpoint( diff --git a/modules/ppcp-settings/src/Data/PaymentSettings.php b/modules/ppcp-settings/src/Data/PaymentSettings.php deleted file mode 100644 index d603a082f..000000000 --- a/modules/ppcp-settings/src/Data/PaymentSettings.php +++ /dev/null @@ -1,210 +0,0 @@ - array( - array( - 'id' => 'paypal', - 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ), - 'description' => __( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-paypal', - ), - array( - 'id' => 'venmo', - 'title' => __( 'Venmo', 'woocommerce-paypal-payments' ), - 'description' => __( - 'Offer Venmo at checkout to millions of active users.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-venmo', - ), - array( - 'id' => 'paypal_credit', - 'title' => __( 'Pay Later', 'woocommerce-paypal-payments' ), - 'description' => __( - 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-paypal', - ), - array( - 'id' => 'credit_and_debit_card_payments', - 'title' => __( - 'Credit and debit card payments', - 'woocommerce-paypal-payments' - ), - 'description' => __( - "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-cards', - ), - ), - 'paymentMethodsOnlineCardPayments' => array( - array( - 'id' => 'advanced_credit_and_debit_card_payments', - 'title' => __( - 'Advanced Credit and Debit Card Payments', - 'woocommerce-paypal-payments' - ), - 'description' => __( - "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-advanced-cards', - ), - array( - '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.", - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-fastlane', - ), - array( - 'id' => 'apple_pay', - 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ), - 'description' => __( - 'Allow customers to pay via their Apple Pay digital wallet.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-apple-pay', - ), - array( - 'id' => 'google_pay', - 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ), - 'description' => __( - 'Allow customers to pay via their Google Pay digital wallet.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-google-pay', - ), - ), - 'paymentMethodsAlternative' => array( - array( - 'id' => 'bancontact', - 'title' => __( 'Bancontact', 'woocommerce-paypal-payments' ), - 'description' => __( - 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-bancontact', - ), - array( - 'id' => 'ideal', - 'title' => __( 'iDEAL', 'woocommerce-paypal-payments' ), - 'description' => __( - 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', - 'woocommerce-paypal-payments' - ), - 'icon' => 'payment-method-ideal', - ), - array( - 'id' => 'eps', - 'title' => __( 'eps', 'woocommerce-paypal-payments' ), - 'description' => __( - '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', - ), - array( - 'id' => 'blik', - 'title' => __( 'BLIK', 'woocommerce-paypal-payments' ), - 'description' => __( - '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', - ), - array( - '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', - ), - array( - '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', - ), - array( - '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', - ), - array( - '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', - ), - array( - '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', - - ), - array( - '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', - ), - ), - ); - } -} diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index ea692242a..7e5ea161e 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -52,7 +52,7 @@ class PaymentRestEndpoint extends RestEndpoint { return array( // PayPal checkout. PayPalGateway::ID => array( - 'id' => 'paypal', + 'id' => 'ppcp-gateway', 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ), 'description' => __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', From 7af348772daec710c59a7c348ded154d541479be Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 21 Jan 2025 10:40:08 +0100 Subject: [PATCH 296/298] Remove settings cogwheels from venmo and pay later --- .../Overview/TabSettingsElements/Blocks/PaymentMethods.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js index 21ae3f028..658ec17d1 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js @@ -19,7 +19,7 @@ const createStandardFields = ( methodId, defaultTitle ) => ( { const paymentMethods = { // PayPal Checkout methods - paypal: { + 'ppcp-gateway': { fields: { ...createStandardFields( 'paypal', 'PayPal' ), showLogo: { @@ -29,12 +29,6 @@ const paymentMethods = { }, }, }, - venmo: { - fields: createStandardFields( 'venmo', 'Venmo' ), - }, - paypal_credit: { - fields: createStandardFields( 'paypal_credit', 'PayPal Credit' ), - }, credit_and_debit_card_payments: { fields: createStandardFields( 'credit_and_debit_card_payments', From 4f0137193e7cef7eec7d2666e96c9b60da347530 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 21 Jan 2025 13:58:29 +0400 Subject: [PATCH 297/298] Improve the styling --- .../settings/_tab-paylater-configurator.scss | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss index 9444f4e18..d4c10d6c3 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss @@ -3,6 +3,8 @@ border: 1px solid var(--color-separators); border-radius: 8px; overflow: hidden; + font-family: "PayPalPro", sans-serif; + -webkit-font-smoothing: antialiased; .css-1snxoyf.eolpigi0 { margin: 0; @@ -46,30 +48,25 @@ } &__subheader, #configurator-controlPanelSubHeader { - @include font(13, 20, 400); - color: $color-gray-800; + color: var(--color-text-description); margin: 0 0 18px 0; } - .css-1caaugt-links_base-text_body_strong, .css-dpyjrq-text_body { - @include font(13, 20, 400); - } - &__header, #configurator-controlPanelHeader, #configurator-previewSectionSubHeaderText.css-14ujlqd-text_body, .css-16jt5za-text_body { - @include font(14, 20, 600); - color: $color-gray-800; - margin: 0 0 8px 0; - display: block; + @include font(16, 20, 600); + color: var(--color-text-title); + margin-bottom: 6px; + font-family: "PayPalPro", sans-serif; + -webkit-font-smoothing: antialiased; } .css-1yo2lxy-text_body_strong { - @include font(13, 16, 600); - color: $color-black; + color: var(--color-text-description); margin: 0; text-transform: none; } - .css-rok10q { + .css-rok10q, .css-dfgbdq-text_body_strong { margin-top: 0; } @@ -77,13 +74,27 @@ display: none; } - .css-1oxdnb3-dropdown_menu_button-text_field_value_sm-active, .css-1wvwydd-dropdown_menu_button-text_field_value_sm-active-active, .css-16jt5za-text_body { - font-size: 13px; - line-height: 1.5384615385; - font-weight: 400; - } - .css-udzaps { padding: 0px; } + + .css-104jwuk, + .css-dpyjrq-text_body, + .css-1oxdnb3-dropdown_menu_button-text_field_value_sm-active, + .css-1wvwydd-dropdown_menu_button-text_field_value_sm-active-active, + .css-16jt5za-text_body, + .css-1caaugt-links_base-text_body_strong, + .css-dpyjrq-text_body, + &__subheader, + #configurator-controlPanelSubHeader, + .css-1yo2lxy-text_body_strong{ + @include font(13, 20, 400); + font-family: "PayPalPro", sans-serif; + -webkit-font-smoothing: antialiased; + } + + .css-1k9r7mv-text_body, .css-ra9ecy-text_body_strong { + font-family: "PayPalPro", sans-serif; + -webkit-font-smoothing: antialiased; + } } From ea6685d0d55daae5f2b46c9327acfcf6d9226bfa Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 21 Jan 2025 11:33:35 +0100 Subject: [PATCH 298/298] Fix phpcs --- .../src/Endpoint/PaymentRestEndpoint.php | 48 +++++++++---------- modules/ppcp-settings/src/SettingsModule.php | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index 7e5ea161e..29516107f 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -52,15 +52,15 @@ class PaymentRestEndpoint extends RestEndpoint { return array( // PayPal checkout. PayPalGateway::ID => array( - 'id' => 'ppcp-gateway', + 'id' => 'ppcp-gateway', 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ), 'description' => __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-paypal', + 'icon' => 'payment-method-paypal', ), - 'venmo' => array( + 'venmo' => array( 'id' => 'venmo', 'title' => __( 'Venmo', 'woocommerce-paypal-payments' ), 'description' => __( @@ -69,7 +69,7 @@ class PaymentRestEndpoint extends RestEndpoint { ), 'icon' => 'payment-method-venmo', ), - 'pay-later' => array( + 'pay-later' => array( 'id' => 'paypal_credit', 'title' => __( 'Pay Later', 'woocommerce-paypal-payments' ), 'description' => __( @@ -88,7 +88,7 @@ class PaymentRestEndpoint extends RestEndpoint { "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-cards', + 'icon' => 'payment-method-cards', ), // Online card Payments. @@ -102,7 +102,7 @@ class PaymentRestEndpoint extends RestEndpoint { "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-advanced-cards', + 'icon' => 'payment-method-advanced-cards', ), AxoGateway::ID => array( 'id' => 'fastlane', @@ -120,7 +120,7 @@ class PaymentRestEndpoint extends RestEndpoint { 'Allow customers to pay via their Apple Pay digital wallet.', 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-apple-pay', + 'icon' => 'payment-method-apple-pay', ), GooglePayGateway::ID => array( 'id' => 'google_pay', @@ -129,7 +129,7 @@ class PaymentRestEndpoint extends RestEndpoint { 'Allow customers to pay via their Google Pay digital wallet.', 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-google-pay', + 'icon' => 'payment-method-google-pay', ), // Alternative payment methods. @@ -140,7 +140,7 @@ class PaymentRestEndpoint extends RestEndpoint { 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-bancontact', + 'icon' => 'payment-method-bancontact', ), BlikGateway::ID => array( 'id' => 'blik', @@ -149,7 +149,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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', + 'icon' => 'payment-method-blik', ), EPSGateway::ID => array( 'id' => 'eps', @@ -158,7 +158,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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', + 'icon' => 'payment-method-eps', ), IDealGateway::ID => array( 'id' => 'ideal', @@ -167,7 +167,7 @@ class PaymentRestEndpoint extends RestEndpoint { 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', 'woocommerce-paypal-payments' ), - 'icon' => 'payment-method-ideal', + 'icon' => 'payment-method-ideal', ), MyBankGateway::ID => array( 'id' => 'mybank', @@ -176,7 +176,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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', + 'icon' => 'payment-method-mybank', ), P24Gateway::ID => array( 'id' => 'przelewy24', @@ -185,7 +185,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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', + 'icon' => 'payment-method-przelewy24', ), TrustlyGateway::ID => array( 'id' => 'trustly', @@ -194,7 +194,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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', + 'icon' => 'payment-method-trustly', ), MultibancoGateway::ID => array( 'id' => 'multibanco', @@ -203,7 +203,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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', + 'icon' => 'payment-method-multibanco', ), PayUponInvoiceGateway::ID => array( 'id' => 'pui', @@ -212,7 +212,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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' => '', + 'icon' => '', ), OXXO::ID => array( 'id' => 'oxxo', @@ -221,7 +221,7 @@ class PaymentRestEndpoint extends RestEndpoint { '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' => '', + 'icon' => '', ), ); } @@ -277,11 +277,11 @@ class PaymentRestEndpoint extends RestEndpoint { foreach ( $this->gateways() as $key => $value ) { if ( ! isset( $all_gateways[ $key ] ) ) { $gateway_settings[ $key ] = array( - 'id' => $this->gateways()[ $key ]['id'] ?? '', - 'title' => $this->gateways()[ $key ]['title'] ?? '', - 'description' => $this->gateways()[ $key ]['description'] ?? '', - 'enabled' => false, - 'icon' => $this->gateways()[ $key ]['icon'] ?? '', + 'id' => $this->gateways()[ $key ]['id'] ?? '', + 'title' => $this->gateways()[ $key ]['title'] ?? '', + 'description' => $this->gateways()[ $key ]['description'] ?? '', + 'enabled' => false, + 'icon' => $this->gateways()[ $key ]['icon'] ?? '', ); continue; @@ -314,7 +314,7 @@ class PaymentRestEndpoint extends RestEndpoint { $request_data = $request->get_params(); - foreach ( $this->gateways() as $key => $value) { + foreach ( $this->gateways() as $key => $value ) { // Check if the REST body contains details for this gateway. if ( ! isset( $request_data[ $key ] ) || ! isset( $all_gateways[ $key ] ) ) { continue; diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 3d91e8596..402379d8c 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -211,7 +211,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { 'login_link' => $container->get( 'settings.rest.login_link' ), 'webhooks' => $container->get( 'settings.rest.webhooks' ), 'refresh_feature_status' => $container->get( 'settings.rest.refresh_feature_status' ), - 'payment' => $container->get( 'settings.rest.payment' ), + 'payment' => $container->get( 'settings.rest.payment' ), 'settings' => $container->get( 'settings.rest.settings' ), 'styling' => $container->get( 'settings.rest.styling' ), );