From 9b8256cc3e2b60e9c81b18bfc0e1d859515d929c Mon Sep 17 00:00:00 2001 From: Alex Florisca Date: Wed, 18 Sep 2024 20:49:27 +0100 Subject: [PATCH] [Feature] Express Checkout Improvements (#50791) * Add new buttonAttributes API to style express checkout buttons coherently (#47899) * Expose buttonAttributes to the express payment methods * Add size and label attributes to the express checkout area * Remove defaultHeight * default button Label * Remove the button label attribute * Remove px from height * Change large button height to 55px * Load express checkout block with attributes * Add toggle and borderRadius controls and remove getting border radius from the theme * Remove extra border radius text * Only pass buttonAttributes if toggled on * Move express payment block attribute logic into a Provider * Tidy up editor grid and parse attributes into context on frontend * Add px to border-radius input * Express payment methods not selectable * Add a test * lint fixes * default button height is 48 not 4 * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Update docs * Add tests for express payment methods * Center images within the express payment area in the editor * Apply the buttonAttributes to the li container in the editor regardless of showButtonStyles * Fix style issue * fix linting * fix lint again * Update manifest * Update docs manifest * Resize images in editor * lint fix --------- Co-authored-by: github-actions * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * Synchronise the express payment settings between the Cart & Checkout blocks (#50688) * Add express payment methods to sidebar * Only add extra props for express payment methods * Update docs * Make title, description and gatewayId types optional * Update docs * Fix types again and editor side * Add changefile(s) from automation for the following project(s): woocommerce-blocks * handle situation when no methods are active * Update manifest * Add express payment methods to inspector controls for express checkout block (#50983) * Remove forced styles on the editor * Remove the darkMode setting from the buttonAttributes API (#51109) * Remove darkMode from the buttonAttributes API * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce --------- Co-authored-by: github-actions * Accept supports declarations for express payment style controls + merchant ux improvements in the editor (#51296) * Fix images in editor displaying weird * Fix long express payment names breaking layout * Default to uniform styles off * Use heightControl for border radius and fix height for cart buttons * Move formatting title and description to the config validation * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * Fix linting * Fix failing test * Add back the 48px height for images in editor * Fix linting again * Update docs * Update docs manifest * Update docs to fix linting * Add comment to test to better explain why we are expecting a console warning * make strings translatable * Sync cart & checkout directly without option * Remove current styles * Change the beta label * Replace < and > with symbol references in docs * Update docs manifest * Increase padding of beta label * fix linter issues * change to using looger helper * fix CSS --------- Co-authored-by: github-actions Co-authored-by: Nadir Seghir --- .../payment-method-integration.md | 78 +++++-- docs/docs-manifest.json | 8 +- .../express-payment-method-config.ts | 18 ++ .../express-payment-settings.tsx | 202 ++++++++++++++++++ .../block-settings/index.tsx | 2 + ...methods.js => express-payment-methods.tsx} | 13 ++ ...ayment.js => checkout-express-payment.tsx} | 0 .../express-payment-context.ts | 21 ++ .../payment-methods/express-payment/index.js | 2 +- .../express-payment/style.scss | 3 +- .../test/__mocks__/editor-context.ts | 3 + .../test/__mocks__/express-payment-props.ts | 187 ++++++++++++++++ .../test/express-payment-methods.tsx | 139 ++++++++++++ .../js/blocks/cart-checkout-shared/types.ts | 23 ++ .../cart-express-payment-block/block.json | 12 ++ .../cart-express-payment-block/edit.tsx | 22 +- .../cart-express-payment-block/editor.scss | 35 +++ .../cart-express-payment-block/frontend.tsx | 28 ++- .../checkout-express-payment-block/block.json | 12 ++ .../checkout-express-payment-block/block.tsx | 1 - .../checkout-express-payment-block/edit.tsx | 26 ++- .../editor.scss | 35 +++ .../frontend.tsx | 32 +++ .../checkout-express-payment-block/types.ts | 10 + .../inner-blocks/register-components.ts | 2 +- .../payment/test/check-payment-methods.tsx | 3 + .../assets/js/data/payment/test/selectors.js | 3 + .../payment/utils/check-payment-methods.ts | 29 ++- .../assets/js/types/type-defs/payments.ts | 41 +++- ...899-try-poc-express-checkout-button-styles | 4 + .../changelog/50688-add-sync-cart-checkout | 4 + ...50791-feature-express-payment-improvements | 4 + ...983-add-express-payment-methods-to-sidebar | 4 + .../changelog/51109-remove-dark-mode | 4 + ...1296-try-supports-express-payment-controls | 4 + .../BlockTypes/CartExpressPaymentBlock.php | 14 ++ .../CheckoutExpressPaymentBlock.php | 127 +++++++++++ .../src/Blocks/Utils/BlockTemplateUtils.php | 4 +- .../src/Blocks/Utils/CartCheckoutUtils.php | 47 +++- 39 files changed, 1155 insertions(+), 51 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx rename plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/{express-payment-methods.js => express-payment-methods.tsx} (92%) rename plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/{checkout-express-payment.js => checkout-express-payment.tsx} (100%) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts create mode 100644 plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles create mode 100644 plugins/woocommerce/changelog/50688-add-sync-cart-checkout create mode 100644 plugins/woocommerce/changelog/50791-feature-express-payment-improvements create mode 100644 plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar create mode 100644 plugins/woocommerce/changelog/51109-remove-dark-mode create mode 100644 plugins/woocommerce/changelog/51296-try-supports-express-payment-controls diff --git a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md index d3457214654..664e64370ab 100644 --- a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md +++ b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md @@ -43,12 +43,16 @@ The options you feed the configuration instance should be an object in this shap ```js const options = { name: 'my_payment_method', - content: <div>A React node</div>, - edit: <div>A React node</div>, + title: 'My Mayment Method', + description: 'A setence or two about your payment method', + gatewayId: 'gateway-id', + content: <ReactNode />, + edit: <ReactNode />, canMakePayment: () => true, paymentMethodId: 'new_payment_method', supports: { features: [], + style: [], }, }; ``` @@ -59,6 +63,18 @@ Here's some more details on the configuration options: This should be a unique string (wise to try to pick something unique for your gateway that wouldn't be used by another implementation) that is used as the identifier for the gateway client side. If `paymentMethodId` is not provided, `name` is used for `paymentMethodId` as well. +#### `title` (optional) + +This should be a human readable string with the name of your payment method. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. If it is not provided, the `name` will be used as the title. + +#### `description` (optional) + +This is one or two sentences maximum describing your payment gateway. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. + +#### `gatewayId` (optional) + +This is the ID of the Payment Gateway that your plugin registers server side, and which registers the express payment method. It is used to link your express payment method on the clinet, to a payment gateway defined on the server. It is used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page. + #### `content` (required) This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](#props-fed-to-payment-method-nodes)). @@ -97,7 +113,11 @@ This is the only optional configuration object. The value of this property is wh This is an array of payment features supported by the gateway. It is used to crosscheck if the payment method can be used for the content of the cart. By default payment methods should support at least `products` feature. If no value is provided then this assumes that `['products']` are supported. ---- +#### `supports:style` + +This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`. + +![Express Checkout Uniform Styles](https://github.com/user-attachments/assets/f0f99f3f-dca7-42b0-8685-3b098a825020) ### Payment Methods - `registerPaymentMethod( options )` @@ -139,23 +159,24 @@ The options you feed the configuration instance are the same as those for expres A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts). -| Property | Type | Description | Values | -| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - | -| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` | -| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` | -| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` | +| Property | Type | Description | Values | +| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - | +| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` | +| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` | +| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` | | `components` | Object | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. |
  • `ValidationInputError`: a container for holding validation errors which typically you'll include after any inputs.
  • [`PaymentMethodLabel`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e089ae17043fa525e8397d605f0f470959f2ae95/assets/js/payment-method-extensions/payment-methods/paypal/index.js#L37-L40): use this component for the payment method label, including an optional icon.
  • `PaymentMethodIcons`: a React component used for displaying payment method icons.
  • - `LoadingMask`: a wrapper component that handles displaying a loading state when the isLoading prop is true. Exposes the [LoadingMask component](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c9074a4941919987dbad16a80f358b960336a09d/assets/js/base/components/loading-mask/index.js)
| | `emitResponse` | Object | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. |
  • `noticeContexts`: This is an object containing properties referencing areas where notices can be targeted in the checkout. The object has the following properties:
    • `PAYMENTS`: This is a reference to the notice area in the payment methods step.
    • `EXPRESS_PAYMENTS`: This is a reference to the notice area in the express payment methods step.
  • `responseTypes`: This is an object containing properties referencing the various response types that can be returned by observers for some event emitters. It makes it easier for autocompleting the types and avoiding typos due to human error. The types are `SUCCESS`, `FAIL`, `ERROR`. The values for these types also correspond to the [payment status types](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/Payments/PaymentResult.php#L21) from the [checkout endpoint response from the server](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L113).
| -| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` | -| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - | -| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - | -| `onSubmit` | Function | Submits the checkout and begins processing | - | -| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful` (see below for explanation) | -| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - | -| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` | +| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` | +| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - | +| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - | +| `onSubmit` | Function | Submits the checkout and begins processing | - | +| `buttonAttributes` | Object | Styles set by the merchant that should be respected by all express payment buttons | `height, borderRadius` | +| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful`(see below for explanation) | +| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - | +| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` | | `shippingStatus` | Object | Various shipping status helpers. |
  • `shippingErrorStatus`: an object with various error statuses that might exist for shipping
  • `shippingErrorTypes`: an object containing all the possible types for shipping error status
| -| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | +| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | - `isPristine`: This is true when the current payment status is `PRISTINE`. - `isStarted`: This is true when the current payment status is `EXPRESS_STARTED`. @@ -167,6 +188,29 @@ A big part of the payment method integration is the interface that is exposed fo Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security). +### Button Attributes for Express Payment Methods + +This API provides a way to synchronise the look and feel of the express payment buttons for a coherent shopper experience. Express Payment Methods must prefer the values provided in the `buttonAttributes`, and use it's own configuration settings as backup when the buttons are rendered somewhere other than the Cart or Checkout block. + +For example, in your button component, you would do something like this: + +```js +// Get your extension specific settings and set defaults if not available +let { + borderRadius = '4', + height = '48', +} = getButtonSettingsFromConfig(); + +// In a cart & checkout block context, we receive `buttonAttributes` as a prop which overwrite the extension specific settings +if ( typeof buttonAttributes !== 'undefined' ) { + height = buttonAttributes.height; + borderRadius = buttonAttributes.borderRadius; +} +... + +return <button style={height: `${height}px`, borderRadius: `${borderRadius}px`} /> +``` + ## Server Side Integration ### Processing Payment diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index d18a4367869..0b71e764ab1 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -83,7 +83,7 @@ "menu_title": "Add Custom Fields to Products", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md", - "hash": "fe8cf43940f5166bf69f102aa4643cbe32415b1167d6b6d8968d434a4d113879", + "hash": "df61c93febc234fe0dbb4826a20ae120b153ab6f6c92d8778177fcac8d6696fe", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md", "id": "64b686dcd5fdd4842be2fc570108231d5a8bfc1b" } @@ -223,7 +223,7 @@ "menu_title": "Payment Method Integration", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", - "hash": "138ffbf27e79ec8b35d2c46e87e3663c203d91fc9ba3f76c43f3cbe76258e5bf", + "hash": "015aae25bb331364c224fe8eb2b7675e4cbed0a9e6bee0dde5f5311388161b0a", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", "id": "c9a763b6976ecf03aeb961577c17c31f1ac7c420", "links": { @@ -1229,7 +1229,7 @@ "menu_title": "Core critical flows", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md", - "hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e", + "hash": "c7122979df14f46646b3f1472ba071bc560b99e6462c5790a9aeaa3b4238ce15", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md", "id": "e561b46694dba223c38b87613ce4907e4e14333a" }, @@ -1804,5 +1804,5 @@ "categories": [] } ], - "hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3" + "hash": "a88d9ea54465c8bbd820042a92df79cbd48943e785b418fcaa04d0c0e66116c0" } \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts index 4e039c9d6ac..38ab4568d2b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts @@ -19,6 +19,9 @@ export default class ExpressPaymentMethodConfig implements ExpressPaymentMethodConfigInstance { public name: string; + public title: string; + public description: string; + public gatewayId: string; public content: ReactNode; public edit: ReactNode; public paymentMethodId?: string; @@ -27,13 +30,28 @@ export default class ExpressPaymentMethodConfig constructor( config: ExpressPaymentMethodConfiguration ) { // validate config + + const readableName = + typeof config.name === 'string' + ? config.name.replace( /[_-]/g, ' ' ) + : config.name; + const trimedDescription = + typeof config?.description === 'string' && + config.description.length > 130 + ? config.description.slice( 0, 130 ) + '...' + : config.description; + ExpressPaymentMethodConfig.assertValidConfig( config ); this.name = config.name; + this.title = config.title || readableName; + this.description = trimedDescription || ''; + this.gatewayId = config.gatewayId || ''; this.content = config.content; this.edit = config.edit; this.paymentMethodId = config.paymentMethodId || this.name; this.supports = { features: config?.supports?.features || [ 'products' ], + style: config?.supports?.style || [], }; this.canMakePaymentFromConfig = config.canMakePayment; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx new file mode 100644 index 00000000000..cc352b10f4e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx @@ -0,0 +1,202 @@ +/** + * External dependencies + */ +import { InspectorControls, HeightControl } from '@wordpress/block-editor'; +import { + PanelBody, + ToggleControl, + RadioControl, + Notice, +} from '@wordpress/components'; +import ExternalLinkCard from '@woocommerce/editor-components/external-link-card'; +import { __ } from '@wordpress/i18n'; +import type { BlockAttributes } from '@wordpress/blocks'; +import { select } from '@wordpress/data'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; +import { ADMIN_URL } from '@woocommerce/settings'; + +const allStyleControls = [ 'height', 'borderRadius' ]; + +const atLeastOnePaymentMethodSupportsOneOf = ( styleControl: string[] ) => { + const availableExpressMethods = + select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods(); + + return Object.values( availableExpressMethods ).reduce( + ( acc, currentValue ) => { + return ( + acc || + currentValue?.supportsStyle.some( ( el ) => + styleControl.includes( el ) + ) + ); + }, + false + ); +}; + +const ExpressPaymentButtonStyleControls = ( { + attributes, + setAttributes, +}: { + attributes: BlockAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; +} ) => { + const { buttonHeight, buttonBorderRadius } = attributes; + + return ( + <> + { atLeastOnePaymentMethodSupportsOneOf( [ 'height' ] ) && ( + + setAttributes( { buttonHeight: newValue } ) + } + /> + ) } + { atLeastOnePaymentMethodSupportsOneOf( [ 'borderRadius' ] ) && ( +
+ { + const valueOnly = newValue.replace( 'px', '' ); + setAttributes( { + buttonBorderRadius: valueOnly, + } ); + } } + /> +
+ ) } + + ); +}; + +const ExpressPaymentToggle = ( { + attributes, + setAttributes, +}: { + attributes: BlockAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; +} ) => { + if ( attributes.showButtonStyles ) { + return ( + + ); + } + return null; +}; + +const ExpressPaymentMethods = () => { + const availableExpressMethods = + select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods(); + + if ( Object.entries( availableExpressMethods ).length < 1 ) { + return ( +

+ { __( + 'You currently have no express payment integrations active.', + 'woocommerce' + ) } +

+ ); + } + + return ( + <> +

+ { __( + 'You currently have the following express payment integrations active.', + 'woocommerce' + ) } +

+ { Object.values( availableExpressMethods ).map( ( values ) => { + return ( + + ); + } ) } + + ); +}; + +const toggleLabel = ( + <> + { __( 'Apply uniform styles', 'woocommerce' ) }{ ' ' } + Beta + +); + +export const ExpressPaymentControls = ( { + attributes, + setAttributes, +}: { + attributes: BlockAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; +} ) => { + return ( + + { atLeastOnePaymentMethodSupportsOneOf( allStyleControls ) && ( + + + setAttributes( { + showButtonStyles: ! attributes.showButtonStyles, + } ) + } + help={ __( + 'Sets a consistent style for express payment buttons.', + 'woocommerce' + ) } + /> + + { __( 'Note', 'woocommerce' ) }:{ ' ' } + { __( + 'Some payment methods might not yet support all style controls', + 'woocommerce' + ) } + + + + ) } + + + + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx index c0794a295b0..3bc73292d8f 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx @@ -34,3 +34,5 @@ export const BlockSettings = ( { ); }; + +export { ExpressPaymentControls } from './express-payment-settings'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx similarity index 92% rename from plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js rename to plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx index af627dacac1..466c6c863d4 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx @@ -21,10 +21,22 @@ import { useDispatch, useSelect } from '@wordpress/data'; */ import PaymentMethodErrorBoundary from './payment-method-error-boundary'; import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants'; +import { useExpressPaymentContext } from '../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; const ExpressPaymentMethods = () => { const { isEditor } = useEditorContext(); + const { showButtonStyles, buttonHeight, buttonBorderRadius } = + useExpressPaymentContext(); + + // API for passing styles to express payment buttons + const buttonAttributes = showButtonStyles + ? { + height: buttonHeight, + borderRadius: buttonBorderRadius, + } + : undefined; + const { activePaymentMethod, paymentMethodData } = useSelect( ( select ) => { const store = select( PAYMENT_STORE_KEY ); @@ -150,6 +162,7 @@ const ExpressPaymentMethods = () => { onError: onExpressPaymentError, setExpressPaymentError: deprecatedSetExpressPaymentError, + buttonAttributes, } ) } ) : null; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.tsx similarity index 100% rename from plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js rename to plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.tsx diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts new file mode 100644 index 00000000000..4526987c1a8 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import { useContext, createContext } from '@wordpress/element'; + +type ExpressPaymentContextProps = { + showButtonStyles: boolean; + buttonHeight: string; + buttonBorderRadius: string; +}; + +export const ExpressPaymentContext: React.Context< ExpressPaymentContextProps > = + createContext< ExpressPaymentContextProps >( { + showButtonStyles: false, + buttonHeight: '48', + buttonBorderRadius: '4', + } ); + +export const useExpressPaymentContext = () => { + return useContext( ExpressPaymentContext ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js index 62836b04504..2bca0b47405 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js @@ -1,2 +1,2 @@ export { default as CartExpressPayment } from './cart-express-payment.js'; -export { default as CheckoutExpressPayment } from './checkout-express-payment.js'; +export { default as CheckoutExpressPayment } from './checkout-express-payment'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss index 100962f2501..56720d7d55b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss @@ -14,6 +14,7 @@ $border-width: 1px; > li { margin: 0; width: 100%; + overflow: hidden; > img { width: 100%; @@ -95,7 +96,7 @@ $border-width: 1px; .wc-block-components-express-payment--cart { .wc-block-components-express-payment__event-buttons { > li { - padding-bottom: $gap; + padding-bottom: $gap-small; text-align: center; width: 100%; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts new file mode 100644 index 00000000000..441521f5750 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts @@ -0,0 +1,3 @@ +// This needs to be defined in a separate file because we are mocking an import. +// The only way to do this is to define the mock and import it BEFORE the module being mocked. +export default jest.fn( () => ( { isEditor: false } ) ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts new file mode 100644 index 00000000000..cc3873ad1e9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts @@ -0,0 +1,187 @@ +// This is the shape of the API exposed to the express payment methods via props +// Note that this is a public API! +export const getExpectedExpressPaymentProps = ( name: string ) => ( { + activePaymentMethod: undefined, + billing: { + appliedCoupons: [], + billingAddress: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + billingData: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + cartTotal: { + label: 'Total', + value: 0, + }, + cartTotalItems: [ + { + key: 'total_items', + label: 'Subtotal:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_fees', + label: 'Fees:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_discount', + label: 'Discount:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_tax', + label: 'Taxes:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_shipping', + label: 'Shipping:', + value: 0, + valueWithTax: 0, + }, + ], + currency: { + code: 'USD', + decimalSeparator: '.', + minorUnit: 2, + prefix: '$', + suffix: '', + symbol: '$', + thousandSeparator: ',', + }, + customerId: 1, + displayPricesIncludingTax: false, + }, + buttonAttributes: { + borderRadius: '4', + height: '48', + }, + cartData: { + cartFees: [], + cartItems: [], + extensions: {}, + }, + checkoutStatus: { + isCalculating: false, + isComplete: false, + isIdle: true, + isProcessing: false, + }, + components: { + LoadingMask: expect.any( Function ), + PaymentMethodIcons: expect.any( Function ), + PaymentMethodLabel: expect.any( Function ), + ValidationInputError: expect.any( Function ), + }, + emitResponse: { + noticeContexts: { + BILLING_ADDRESS: 'wc/checkout/billing-address', + CART: 'wc/cart', + CHECKOUT: 'wc/checkout', + CHECKOUT_ACTIONS: 'wc/checkout/checkout-actions', + CONTACT_INFORMATION: 'wc/checkout/contact-information', + EXPRESS_PAYMENTS: 'wc/checkout/express-payments', + ORDER_INFORMATION: 'wc/checkout/additional-information', + PAYMENTS: 'wc/checkout/payments', + SHIPPING_ADDRESS: 'wc/checkout/shipping-address', + SHIPPING_METHODS: 'wc/checkout/shipping-methods', + }, + responseTypes: { + ERROR: 'error', + FAIL: 'failure', + SUCCESS: 'success', + }, + }, + eventRegistration: { + onCheckoutAfterProcessingWithError: expect.any( Function ), + onCheckoutAfterProcessingWithSuccess: expect.any( Function ), + onCheckoutBeforeProcessing: expect.any( Function ), + onCheckoutFail: expect.any( Function ), + onCheckoutSuccess: expect.any( Function ), + onCheckoutValidation: expect.any( Function ), + onCheckoutValidationBeforeProcessing: expect.any( Function ), + onPaymentProcessing: expect.any( Function ), + onPaymentSetup: expect.any( Function ), + onShippingRateFail: expect.any( Function ), + onShippingRateSelectFail: expect.any( Function ), + onShippingRateSelectSuccess: expect.any( Function ), + onShippingRateSuccess: expect.any( Function ), + }, + name, + onClick: expect.any( Function ), + onClose: expect.any( Function ), + onError: expect.any( Function ), + onSubmit: expect.any( Function ), + paymentStatus: { + hasError: false, + hasFailed: false, + isDoingExpressPayment: false, + isFinished: false, + isIdle: true, + isPristine: true, + isProcessing: false, + isReady: false, + isStarted: false, + isSuccessful: false, + }, + setExpressPaymentError: expect.any( Function ), + shippingData: { + isSelectingRate: false, + needsShipping: true, + selectedRates: {}, + setSelectedRates: expect.any( Function ), + setShippingAddress: expect.any( Function ), + shippingAddress: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + shippingRates: [], + shippingRatesLoading: false, + }, + shippingStatus: { + shippingErrorStatus: { + hasError: false, + hasInvalidAddress: false, + isPristine: true, + isValid: false, + }, + shippingErrorTypes: { + INVALID_ADDRESS: 'invalid_address', + NONE: 'none', + UNKNOWN: 'unknown_error', + }, + }, + shouldSavePayment: false, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx new file mode 100644 index 00000000000..94b2e8e43de --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx @@ -0,0 +1,139 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; +import { + registerExpressPaymentMethod, + __experimentalDeRegisterExpressPaymentMethod, +} from '@woocommerce/blocks-registry'; +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import mockEditorContext from './__mocks__/editor-context'; +import { getExpectedExpressPaymentProps } from './__mocks__/express-payment-props'; +import ExpressPaymentMethods from '../express-payment-methods'; +jest.mock( '@woocommerce/base-context', () => ( { + useEditorContext: mockEditorContext, +} ) ); + +// Button styles are disabled by default. We need to mock the express payment context +// to enable them. +jest.mock( '../express-payment/express-payment-context', () => { + return { + useExpressPaymentContext: jest.fn().mockReturnValue( { + showButtonStyles: true, + buttonHeight: '48', + buttonBorderRadius: '4', + } ), + }; +} ); + +const mockExpressPaymentMethodNames = [ 'paypal', 'google pay', 'apple pay' ]; + +const MockExpressButton = jest.fn( ( { name } ) => ( +
{ `${ name } button` }
+) ); + +const MockEditorExpressButton = jest.fn( ( { name } ) => ( +
{ `${ name } preview` }
+) ); + +const registerMockExpressPaymentMethods = () => { + mockExpressPaymentMethodNames.forEach( ( name ) => { + registerExpressPaymentMethod( { + name, + title: `${ name } payment method`, + description: `A test ${ name } payment method`, + gatewayId: 'test-express-payment-method', + paymentMethodId: name, + content: , + edit: , + canMakePayment: () => true, + supports: { + features: [ 'products' ], + }, + } ); + } ); + dispatch( PAYMENT_STORE_KEY ).__internalUpdateAvailablePaymentMethods(); +}; + +const deregisterMockExpressPaymentMethods = () => { + mockExpressPaymentMethodNames.forEach( ( name ) => { + __experimentalDeRegisterExpressPaymentMethod( name ); + } ); +}; + +describe( 'Express payment methods', () => { + afterAll( () => { + jest.restoreAllMocks(); + } ); + describe( 'No payment methods available', () => { + it( 'should display no registered payment methods', () => { + render( ); + + const noPaymentMethods = screen.queryAllByText( + /No registered Payment Methods/ + ); + expect( noPaymentMethods.length ).toEqual( 1 ); + } ); + } ); + + describe( 'Payment methods available', () => { + beforeAll( () => { + registerMockExpressPaymentMethods(); + } ); + afterAll( () => { + deregisterMockExpressPaymentMethods(); + } ); + describe( 'In a frontend context', () => { + it( 'should display the element provided by paymentMethods.content', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + const btn = screen.getByText( `${ name } button` ); + expect( btn ).toBeVisible(); + } ); + } ); + it( 'should pass the correct properties to the rendered element', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + expect( MockExpressButton ).toHaveBeenCalledWith( + getExpectedExpressPaymentProps( name ), + {} + ); + } ); + // This is a bit out of place, but the console warning is triggered when the + // usePaymentMethodInterface hook is called so we need to expect it here otherwise + // the test fails on unexpected console warnings. + expect( console ).toHaveWarnedWith( + 'isPristine is deprecated since version 9.6.0. Please use isIdle instead. See: https://github.com/woocommerce/woocommerce-blocks/pull/8110' + ); + } ); + } ); + describe( 'In an editor context', () => { + beforeEach( () => { + mockEditorContext.mockImplementation( () => ( { + isEditor: true, + } ) ); + } ); + it( 'should display the element provided by paymentMethods.edit', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + const btn = screen.getByText( `${ name } preview` ); + expect( btn ).toBeVisible(); + } ); + } ); + it( 'should pass the correct properties to the rendered element', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + expect( MockEditorExpressButton ).toHaveBeenCalledWith( + getExpectedExpressPaymentProps( name ), + {} + ); + } ); + } ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts new file mode 100644 index 00000000000..af521dc3f67 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts @@ -0,0 +1,23 @@ +export type ExpressCheckoutAttributes = { + className?: string; + buttonHeight: string; + showButtonStyles: boolean; + buttonBorderRadius: string; + lock: { + move: boolean; + remove: boolean; + }; +}; + +export type ExpressCartAttributes = { + className: string; + buttonHeight: string; + showButtonStyles: boolean; + buttonBorderRadius: string; +}; + +export type ExpressPaymentSettings = { + showButtonStyles: boolean; + buttonHeight: string; + buttonBorderRadius: string; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json index ab2c221618a..cba68cc4f13 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json @@ -13,6 +13,18 @@ "lock": false }, "attributes": { + "showButtonStyles": { + "type": "boolean", + "default": false + }, + "buttonHeight": { + "type": "string", + "default": "48" + }, + "buttonBorderRadius": { + "type": "string", + "default": "4" + }, "lock": { "type": "object", "default": { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx index 8bf88316bb3..db7834518c0 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx @@ -4,17 +4,23 @@ import { useBlockProps } from '@wordpress/block-editor'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; import clsx from 'clsx'; +import { ExpressPaymentControls } from '@woocommerce/blocks/cart-checkout-shared'; +import type { BlockAttributes } from '@wordpress/blocks'; /** * Internal dependencies */ import Block from './block'; import './editor.scss'; +import type { ExpressCartAttributes } from '../../../cart-checkout-shared/types'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; export const Edit = ( { attributes, + setAttributes, }: { - attributes: { className: string }; + attributes: ExpressCartAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; } ): JSX.Element | null => { const { paymentMethods, isInitialized } = useExpressPaymentMethods(); const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0; @@ -24,7 +30,9 @@ export const Edit = ( { hasExpressPaymentMethods, } ), } ); - const { className } = attributes; + + const { className, showButtonStyles, buttonHeight, buttonBorderRadius } = + attributes; if ( ! isInitialized || ! hasExpressPaymentMethods ) { return null; @@ -32,7 +40,15 @@ export const Edit = ( { return (
- + + + +
); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss index 24a209975d4..47ddf8126fc 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss @@ -32,3 +32,38 @@ display: block; } } + +.express-payment-styles-notice { + margin-bottom: $gap; +} + +.express-payment-styles-beta-badge { + margin-left: $grid-unit-10; + padding: 3px $grid-unit-10; + height: $grid-unit-30; + border-radius: $radius-block-ui; + background-color: $gray-900; + color: $white; + align-items: center; + font-size: $helptext-font-size; + line-height: 1; +} + +// Disabled changing units from px for border radius control +.border-radius-control-container select { + pointer-events: none; +} + +.button-height-control + .border-radius-control-container { + margin-top: $grid-unit-30; +} + +// Center images rendered in place of buttons in the editor +.wc-block-components-express-payment { + .wc-block-components-express-payment__event-buttons { + > li { + pointer-events: none; + user-select: none; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx index 4fc9ad2897a..c4a6edd46d0 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx @@ -1,6 +1,32 @@ +/** + * External dependencies + */ +import { getValidBlockAttributes } from '@woocommerce/base-utils'; + /** * Internal dependencies */ import Block from './block'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; +import metadata from './block.json'; +import { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types'; -export default Block; +const FrontendBlock = ( attributes: ExpressCheckoutAttributes ) => { + const validAttributes = getValidBlockAttributes( + metadata.attributes, + attributes + ); + + const { showButtonStyles, buttonHeight, buttonBorderRadius, className } = + validAttributes; + + return ( + + + + ); +}; + +export default FrontendBlock; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json index cb5edb97731..7ba93490519 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json @@ -13,6 +13,18 @@ "lock": false }, "attributes": { + "showButtonStyles": { + "type": "boolean", + "default": false + }, + "buttonHeight": { + "type": "string", + "default": "48" + }, + "buttonBorderRadius": { + "type": "string", + "default": "4" + }, "className": { "type": "string", "default": "" diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx index cbaa8434163..f6b104d22f9 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx @@ -10,7 +10,6 @@ import { CheckoutExpressPayment } from '../../../cart-checkout-shared/payment-me const Block = ( { className }: { className?: string } ): JSX.Element | null => { const { cartNeedsPayment } = useStoreCart(); - if ( ! cartNeedsPayment ) { return null; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx index f076388889d..399f873bac1 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx @@ -4,23 +4,23 @@ import { useBlockProps } from '@wordpress/block-editor'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; import clsx from 'clsx'; +import { ExpressPaymentControls } from '@woocommerce/blocks/cart-checkout-shared'; +import type { BlockAttributes } from '@wordpress/blocks'; /** * Internal dependencies */ import Block from './block'; import './editor.scss'; +import type { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; export const Edit = ( { attributes, + setAttributes, }: { - attributes: { - className?: string; - lock: { - move: boolean; - remove: boolean; - }; - }; + attributes: ExpressCheckoutAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; } ): JSX.Element | null => { const { paymentMethods, isInitialized } = useExpressPaymentMethods(); const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0; @@ -39,9 +39,19 @@ export const Edit = ( { return null; } + const { buttonHeight, buttonBorderRadius, showButtonStyles } = attributes; + return (
- + + + +
); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss index bc5ba4b76d2..ea77acc5400 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss @@ -27,3 +27,38 @@ margin: 0 0 1em; } } + +.express-payment-styles-notice { + margin-bottom: $gap; +} + +.express-payment-styles-beta-badge { + margin-left: $grid-unit-10; + padding: 3px $grid-unit-10; + height: $grid-unit-30; + border-radius: $radius-block-ui; + background-color: $gray-900; + color: $white; + align-items: center; + font-size: $helptext-font-size; + line-height: 1; +} + +// Disabled changing units from px for border radius control +.border-radius-control-container select { + pointer-events: none; +} + +.button-height-control + .border-radius-control-container { + margin-top: $grid-unit-30; +} + +// Center images rendered in place of buttons in the editor +.wc-block-components-express-payment { + .wc-block-components-express-payment__event-buttons { + > li { + pointer-events: none; + user-select: none; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx new file mode 100644 index 00000000000..a458cde4131 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { getValidBlockAttributes } from '@woocommerce/base-utils'; + +/** + * Internal dependencies + */ +import Block from './block'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; +import metadata from './block.json'; +import { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types'; + +const FrontendBlock = ( attributes: ExpressCheckoutAttributes ) => { + const validAttributes = getValidBlockAttributes( + metadata.attributes, + attributes + ); + + const { showButtonStyles, buttonHeight, buttonBorderRadius } = + validAttributes; + + return ( + + + + ); +}; + +export default FrontendBlock; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts new file mode 100644 index 00000000000..93e998b7dd4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts @@ -0,0 +1,10 @@ +export type ExpressCheckoutAttributes = { + className?: string; + buttonHeight: string; + showButtonStyles: boolean; + buttonBorderRadius: string; + lock: { + move: boolean; + remove: boolean; + }; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts index 803344294f6..b7ed53c3d79 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts @@ -33,7 +33,7 @@ registerCheckoutBlock( { component: lazy( () => import( - /* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/block' + /* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/frontend' ) ), } ); diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx b/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx index 4669d09e0a1..fda47363036 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx +++ b/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx @@ -123,6 +123,9 @@ const registerMockPaymentMethods = ( savedCards = true ) => { }; registerExpressPaymentMethod( { name, + title: 'Express Payment Method', + description: 'A test express payment method', + gatewayId: 'test-express-payment-method', content: , edit:
An express payment method
, canMakePayment: mockedExpressCanMakePayment, diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js b/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js index cf0739e6a95..b7d30f83a1c 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js +++ b/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js @@ -123,6 +123,9 @@ const registerMockPaymentMethods = ( savedCards = true ) => { }; registerExpressPaymentMethod( { name, + title: `${ name } express payment method`, + description: `${ name } express payment method description`, + gatewayId: 'woo', content: , edit:
An express payment method
, canMakePayment: () => true, diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts b/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts index 594c04fd8c2..1391b00a074 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts +++ b/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts @@ -163,11 +163,30 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => { | PaymentMethodConfigInstance | ExpressPaymentMethodConfigInstance ) => { - const { name } = paymentMethod; - availablePaymentMethods = { - ...availablePaymentMethods, - [ paymentMethod.name ]: { name }, - }; + if ( express ) { + const { name, title, description, gatewayId, supports } = + paymentMethod as ExpressPaymentMethodConfigInstance; + + availablePaymentMethods = { + ...availablePaymentMethods, + [ paymentMethod.name ]: { + name, + title, + description, + gatewayId, + supportsStyle: supports?.style, + }, + }; + } else { + const { name } = paymentMethod as PaymentMethodConfigInstance; + + availablePaymentMethods = { + ...availablePaymentMethods, + [ paymentMethod.name ]: { + name, + }, + }; + } }; // Order payment methods. diff --git a/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts b/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts index c0c82626ecb..2a11bf7e7fb 100644 --- a/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts +++ b/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts @@ -31,6 +31,7 @@ export interface SupportsConfiguration { features?: string[]; // Deprecated, in favour of showSavedCards and showSaveOption savePaymentInfo?: boolean; + style?: string[]; } // we assign a value in the class for supports.features @@ -119,10 +120,28 @@ export interface PaymentMethodConfiguration { savedTokenComponent?: ReactNode | null; } -export type ExpressPaymentMethodConfiguration = Omit< - PaymentMethodConfiguration, - 'icons' | 'label' | 'ariaLabel' | 'placeOrderButtonLabel' ->; +export interface ExpressPaymentMethodConfiguration { + // A unique string to identify the payment method client side. + name: string; + // A human readable title for the payment method. + title?: string; + // A human readable description for the payment method. + description?: string; + // The gateway ID for the payment method. + gatewayId?: string; + // A react node for your payment method UI. + content: ReactNode; + // A react node to display a preview of your payment method in the editor. + edit: ReactNode; + // A callback to determine whether the payment method should be shown in the checkout. + canMakePayment: CanMakePaymentCallback; + // A unique string to represent the payment method server side. If not provided, defaults to name. + paymentMethodId?: string; + // Object that describes various features provided by the payment method. + supports: SupportsConfiguration; + // A React node that contains logic handling any processing your payment method has to do with saved payment methods if your payment method supports them + savedTokenComponent?: ReactNode | null; +} export type PaymentMethods = | Record< string, PaymentMethodConfigInstance > @@ -131,7 +150,16 @@ export type PaymentMethods = /** * Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores. */ -export type PlainPaymentMethods = Record< string, { name: string } >; +export type PlainPaymentMethods = Record< + string, + { + name: string; + title: string; + description: string; + gatewayId: string; + supportsStyle: string[]; + } +>; /** * Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores. @@ -159,6 +187,9 @@ export interface PaymentMethodConfigInstance { export interface ExpressPaymentMethodConfigInstance { name: string; + title: string; + description: string; + gatewayId: string; content: ReactNode; edit: ReactNode; paymentMethodId?: string; diff --git a/plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles b/plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles new file mode 100644 index 00000000000..857d00e2bb8 --- /dev/null +++ b/plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adds unified styles for the express checkout block \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50688-add-sync-cart-checkout b/plugins/woocommerce/changelog/50688-add-sync-cart-checkout new file mode 100644 index 00000000000..2358e0fc1ce --- /dev/null +++ b/plugins/woocommerce/changelog/50688-add-sync-cart-checkout @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Synchronise the express payment controls between the Cart & Checkout blocks \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50791-feature-express-payment-improvements b/plugins/woocommerce/changelog/50791-feature-express-payment-improvements new file mode 100644 index 00000000000..84c3df51562 --- /dev/null +++ b/plugins/woocommerce/changelog/50791-feature-express-payment-improvements @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Improve the express checkout experience with several design tweak, uniform button styles and editor improvements \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar b/plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar new file mode 100644 index 00000000000..640c491e1cc --- /dev/null +++ b/plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Available express payment methods are visible in the editor when selecting the express payment block \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51109-remove-dark-mode b/plugins/woocommerce/changelog/51109-remove-dark-mode new file mode 100644 index 00000000000..b0cb81d045a --- /dev/null +++ b/plugins/woocommerce/changelog/51109-remove-dark-mode @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: This is part of a bigger feature that will have its own changelog entry + diff --git a/plugins/woocommerce/changelog/51296-try-supports-express-payment-controls b/plugins/woocommerce/changelog/51296-try-supports-express-payment-controls new file mode 100644 index 00000000000..ef5d03c75bb --- /dev/null +++ b/plugins/woocommerce/changelog/51296-try-supports-express-payment-controls @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +UX improvements to the express payment block in the editor \ No newline at end of file diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php index d491967c844..b8fd43c7b5b 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php @@ -11,4 +11,18 @@ class CartExpressPaymentBlock extends AbstractInnerBlock { * @var string */ protected $block_name = 'cart-express-payment-block'; + + /** + * Uniform default_styles for the express payment buttons + * + * @var boolean + */ + protected $default_styles = null; + + /** + * Current styles for the express payment buttons + * + * @var boolean + */ + protected $current_styles = null; } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php index 3efe4261e6a..a5864d7502c 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php @@ -1,6 +1,9 @@ default_styles = array( + 'showButtonStyles' => false, + 'buttonHeight' => '48', + 'buttonBorderRadius' => '4', + ); + + add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 ); + } + + /** + * Synchorize the express payment attributes between the Cart and Checkout pages. + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ + public function sync_express_payment_attrs( $post_id, $post ) { + if ( wc_get_page_id( 'cart' ) === $post_id ) { + $cart_or_checkout = 'cart'; + } elseif ( wc_get_page_id( 'checkout' ) === $post_id ) { + $cart_or_checkout = 'checkout'; + } else { + return; + } + + // This is not a proper save action, maybe an autosave, so don't continue. + if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) { + return; + } + + $block_name = 'woocommerce/' . $cart_or_checkout; + $page_id = 'woocommerce_' . $cart_or_checkout . '_page_id'; + $template_name = 'page-' . $cart_or_checkout; + + // Check if we are editing the cart/checkout page and that it contains a Cart/Checkout block. + // Cast to string for Cart/Checkout page ID comparison because get_option can return it as a string, so better to compare both values as strings. + if ( ! empty( $post->post_type ) && 'wp_template' !== $post->post_type && ( false === has_block( $block_name, $post ) || (string) get_option( $page_id ) !== (string) $post_id ) ) { + return; + } + + // Check if we are editing the Cart/Checkout template and that it contains a Cart/Checkout block. + if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && $template_name !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( $block_name, $post ) ) { + return; + } + + if ( empty( $post->post_content ) ) { + return; + } + + try { + // Parse the post content to get the express payment attributes of the current page. + $blocks = parse_blocks( $post->post_content ); + $attrs = CartCheckoutUtils::find_express_checkout_attributes( $blocks, $cart_or_checkout ); + + if ( ! is_array( $attrs ) ) { + return; + } + $updated_attrs = array_merge( $this->default_styles, $attrs ); + + // We need to sync the attributes between the Cart and Checkout pages. + $other_page = 'cart' === $cart_or_checkout ? 'checkout' : 'cart'; + + $this->update_other_page_with_express_payment_attrs( $other_page, $updated_attrs ); + } catch ( Exception $e ) { + wc_get_logger()->log( 'error', 'Error updating express payment attributes: ' . $e->getMessage() ); + } + } + + /** + * Update the express payment attributes in the other page (Cart or Checkout). + * + * @param string $cart_or_checkout The page to update. + * @param array $updated_attrs The updated attributes. + */ + private function update_other_page_with_express_payment_attrs( $cart_or_checkout, $updated_attrs ) { + $page_id = 'cart' === $cart_or_checkout ? wc_get_page_id( 'cart' ) : wc_get_page_id( 'checkout' ); + + if ( -1 === $page_id ) { + return; + } + + $post = get_post( $page_id ); + + if ( empty( $post->post_content ) ) { + return; + } + + $blocks = parse_blocks( $post->post_content ); + CartCheckoutUtils::update_blocks_with_new_attrs( $blocks, $cart_or_checkout, $updated_attrs ); + + $updated_content = serialize_blocks( $blocks ); + remove_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 ); + + wp_update_post( + array( + 'ID' => $page_id, + 'post_content' => $updated_content, + ), + false, + false + ); + + add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 ); + } } diff --git a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php index 595ceca323e..8e6133155fa 100644 --- a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php @@ -481,7 +481,7 @@ class BlockTemplateUtils { * @return boolean */ public static function theme_has_template( $template_name ) { - return ! ! self::get_theme_template_path( $template_name, 'wp_template' ); + return (bool) self::get_theme_template_path( $template_name, 'wp_template' ); } /** @@ -491,7 +491,7 @@ class BlockTemplateUtils { * @return boolean */ public static function theme_has_template_part( $template_name ) { - return ! ! self::get_theme_template_path( $template_name, 'wp_template_part' ); + return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' ); } /** diff --git a/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php b/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php index 53b4cde7f56..8b9e0e9098a 100644 --- a/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php @@ -108,7 +108,7 @@ class CartCheckoutUtils { } $array_without_accents = array_map( - function( $value ) { + function ( $value ) { return is_array( $value ) ? self::deep_sort_with_accents( $value ) : remove_accents( wc_strtolower( html_entity_decode( $value ) ) ); @@ -129,7 +129,7 @@ class CartCheckoutUtils { $shipping_zones = \WC_Shipping_Zones::get_zones(); $formatted_shipping_zones = array_reduce( $shipping_zones, - function( $acc, $zone ) { + function ( $acc, $zone ) { $acc[] = [ 'id' => $zone['id'], 'title' => $zone['zone_name'], @@ -146,4 +146,47 @@ class CartCheckoutUtils { ]; return $formatted_shipping_zones; } + + /** + * Recursively search the checkout block to find the express checkout block and + * get the button style attributes + * + * @param array $blocks Blocks to search. + * @param string $cart_or_checkout The block type to check. + */ + public static function find_express_checkout_attributes( $blocks, $cart_or_checkout ) { + $express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block'; + foreach ( $blocks as $block ) { + if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] && ! empty( $block['attrs'] ) ) { + return $block['attrs']; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $answer = self::find_express_checkout_attributes( $block['innerBlocks'], $cart_or_checkout ); + if ( $answer ) { + return $answer; + } + } + } + } + + /** + * Given an array of blocks, find the express payment block and update its attributes. + * + * @param array $blocks Blocks to search. + * @param string $cart_or_checkout The block type to check. + * @param array $updated_attrs The new attributes to set. + */ + public static function update_blocks_with_new_attrs( &$blocks, $cart_or_checkout, $updated_attrs ) { + $express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block'; + foreach ( $blocks as $key => &$block ) { + if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] ) { + $blocks[ $key ]['attrs'] = $updated_attrs; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + self::update_blocks_with_new_attrs( $block['innerBlocks'], $cart_or_checkout, $updated_attrs ); + } + } + } }