* Document payment_gateways() usage

* Type in code comment

* Return available methods in cart/checkout StoreAPI responses

* Filter available methods in checkout

* fix tests

* fix TS error

* fix TS warnings in tests

* Update src/StoreApi/Schemas/V1/CartSchema.php

Co-authored-by: Saad Tarhi <saad.trh@gmail.com>

* Only apply filtering on frontend

* Avoid filter on express methods

---------

Co-authored-by: Saad Tarhi <saad.trh@gmail.com>
This commit is contained in:
Mike Jolley 2023-03-13 10:29:17 +00:00 committed by GitHub
parent ad032a93e6
commit 95efc38d1f
17 changed files with 98 additions and 58 deletions

View File

@ -15,6 +15,7 @@ import {
EMPTY_CART_ERRORS,
EMPTY_SHIPPING_RATES,
EMPTY_TAX_LINES,
EMPTY_PAYMENT_METHODS,
EMPTY_PAYMENT_REQUIREMENTS,
EMPTY_EXTENSIONS,
} from '@woocommerce/block-data';
@ -112,6 +113,7 @@ export const defaultCartData: StoreCart = {
shippingRates: EMPTY_SHIPPING_RATES,
isLoadingRates: false,
cartHasCalculatedShipping: false,
paymentMethods: EMPTY_PAYMENT_METHODS,
paymentRequirements: EMPTY_PAYMENT_REQUIREMENTS,
receiveCart: () => undefined,
receiveCartContents: () => undefined,

View File

@ -7,7 +7,8 @@
"../providers/cart-checkout/checkout-events/index.tsx",
"../providers/cart-checkout/payment-events/index.tsx",
"../providers/cart-checkout/shipping/index.js",
"../../../editor-components/utils/*"
"../../../editor-components/utils/*",
"../../../data/index.ts"
],
"exclude": [ "**/test/**" ]
}

View File

@ -17,7 +17,6 @@ import { dispatch } from '@wordpress/data';
* Internal dependencies
*/
import PaymentMethods from '../payment-methods';
import { defaultCartState } from '../../../../data/cart/default-state';
jest.mock( '../saved-payment-method-options', () => ( { onChange } ) => {
return (
@ -102,9 +101,10 @@ describe( 'PaymentMethods', () => {
wpDataFunctions
.dispatch( CART_STORE_KEY )
.invalidateResolutionForStore();
wpDataFunctions
.dispatch( CART_STORE_KEY )
.receiveCart( defaultCartState.cartData );
wpDataFunctions.dispatch( CART_STORE_KEY ).receiveCart( {
...previewCart,
payment_methods: [ 'cod', 'credit-card' ],
} );
} );
afterEach( () => {

View File

@ -15,6 +15,7 @@ import {
EMPTY_CART_ERRORS,
EMPTY_SHIPPING_RATES,
EMPTY_TAX_LINES,
EMPTY_PAYMENT_METHODS,
EMPTY_PAYMENT_REQUIREMENTS,
EMPTY_EXTENSIONS,
} from '../constants';
@ -89,6 +90,7 @@ export const defaultCartState: CartState = {
tax_lines: EMPTY_TAX_LINES,
},
errors: EMPTY_CART_ITEM_ERRORS,
paymentMethods: EMPTY_PAYMENT_METHODS,
paymentRequirements: EMPTY_PAYMENT_REQUIREMENTS,
extensions: EMPTY_EXTENSIONS,
},

View File

@ -12,6 +12,7 @@ export const EMPTY_CART_FEES: [] = [];
export const EMPTY_CART_ITEM_ERRORS: [] = [];
export const EMPTY_CART_ERRORS: [] = [];
export const EMPTY_SHIPPING_RATES: [] = [];
export const EMPTY_PAYMENT_METHODS: [] = [];
export const EMPTY_PAYMENT_REQUIREMENTS: [] = [];
export const EMPTY_EXTENSIONS: Record< string, unknown > = {};
export const EMPTY_TAX_LINES: [] = [];

View File

@ -18,7 +18,7 @@ export interface PaymentState {
status: string;
activePaymentMethod: string;
activeSavedToken: string;
// Avilable payment methods are payment methods which have been validated and can make payment
// Available payment methods are payment methods which have been validated and can make payment.
availablePaymentMethods: PlainPaymentMethods;
availableExpressPaymentMethods: PlainExpressPaymentMethods;
savedPaymentMethods:

View File

@ -2,7 +2,8 @@
* External dependencies
*/
import * as wpDataFunctions from '@wordpress/data';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import { previewCart } from '@woocommerce/resource-previews';
import { PAYMENT_STORE_KEY, CART_STORE_KEY } from '@woocommerce/block-data';
import {
registerPaymentMethod,
registerExpressPaymentMethod,
@ -23,6 +24,7 @@ const requiredKeyCheck = ( args: CanMakePaymentArgument ) => {
'cart',
'cartNeedsShipping',
'cartTotals',
'paymentMethods',
'paymentRequirements',
'selectedShippingMethods',
'shippingAddress',
@ -133,6 +135,10 @@ const registerMockPaymentMethods = ( savedCards = true ) => {
wpDataFunctions
.dispatch( PAYMENT_STORE_KEY )
.__internalUpdateAvailablePaymentMethods();
wpDataFunctions.dispatch( CART_STORE_KEY ).receiveCart( {
...previewCart,
payment_methods: [ 'cheque', 'bacs', 'credit-card' ],
} );
};
const resetMockPaymentMethods = () => {

View File

@ -12,8 +12,6 @@ import {
emptyHiddenAddressFields,
} from '@woocommerce/base-utils';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import {
getExpressPaymentMethods,
getPaymentMethods,
@ -33,10 +31,36 @@ import {
} from '../../../data/constants';
import { defaultCartState } from '../../../data/cart/default-state';
const registrationErrorNotice = (
paymentMethod:
| ExpressPaymentMethodConfigInstance
| PaymentMethodConfigInstance,
errorMessage: string,
express = false
) => {
const { createErrorNotice } = dispatch( 'core/notices' );
const noticeContext = express
? noticeContexts.EXPRESS_PAYMENTS
: noticeContexts.PAYMENTS;
const errorText = sprintf(
/* translators: %s the id of the payment method being registered (bank transfer, cheque...) */
__(
`There was an error registering the payment method with id '%s': `,
'woo-gutenberg-products-block'
),
paymentMethod.paymentMethodId
);
createErrorNotice( `${ errorText } ${ errorMessage }`, {
context: noticeContext,
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
} );
};
export const checkPaymentMethodsCanPay = async ( express = false ) => {
const isEditor = !! select( 'core/editor' );
let availablePaymentMethods = {};
const paymentMethods = express
? getExpressPaymentMethods()
: getPaymentMethods();
@ -53,10 +77,6 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
};
};
const noticeContext = express
? noticeContexts.EXPRESS_PAYMENTS
: noticeContexts.PAYMENTS;
let cartForCanPayArgument: Record< string, unknown > = {};
let canPayArgument: Record< string, unknown > = {};
@ -94,7 +114,6 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
paymentRequirements: cart.paymentRequirements,
receiveCart: dispatch( CART_STORE_KEY ).receiveCart,
};
canPayArgument = {
cart: cartForCanPayArgument,
cartTotals: cart.totals,
@ -103,6 +122,7 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
billingAddress: cart.billingAddress,
shippingAddress: cart.shippingAddress,
selectedShippingMethods,
paymentMethods: cart.paymentMethods,
paymentRequirements: cart.paymentRequirements,
};
} else {
@ -139,68 +159,61 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
selectedShippingMethods: deriveSelectedShippingRates(
cartForCanPayArgument.shippingRates
),
paymentMethods: previewCart.payment_methods,
paymentRequirements: cartForCanPayArgument.paymentRequirements,
};
}
// Order payment methods
let paymentMethodsOrder;
if ( express ) {
paymentMethodsOrder = Object.keys( paymentMethods );
} else {
paymentMethodsOrder = Array.from(
new Set( [
...( getSetting( 'paymentGatewaySortOrder', [] ) as [] ),
...Object.keys( paymentMethods ),
] )
);
}
// Order payment methods.
const paymentMethodsOrder = express
? Object.keys( paymentMethods )
: Array.from(
new Set( [
...( getSetting( 'paymentGatewaySortOrder', [] ) as [] ),
...Object.keys( paymentMethods ),
] )
);
const cartPaymentMethods = canPayArgument.paymentMethods as string[];
for ( let i = 0; i < paymentMethodsOrder.length; i++ ) {
const paymentMethodName = paymentMethodsOrder[ i ];
const paymentMethod = paymentMethods[ paymentMethodName ];
if ( ! paymentMethod ) {
continue;
}
// See if payment method should be available. This always evaluates to true in the editor context.
try {
const validForCart =
isEditor || express
? true
: cartPaymentMethods.includes( paymentMethodName );
const canPay = isEditor
? true
: await Promise.resolve(
: validForCart &&
( await Promise.resolve(
paymentMethod.canMakePayment( canPayArgument )
);
) );
if ( canPay ) {
if ( typeof canPay === 'object' && canPay.error ) {
throw new Error( canPay.error.message );
}
addAvailablePaymentMethod( paymentMethod );
}
} catch ( e ) {
if ( CURRENT_USER_IS_ADMIN || isEditor ) {
const { createErrorNotice } = dispatch( noticesStore );
const errorText = sprintf(
/* translators: %s the id of the payment method being registered (bank transfer, cheque...) */
__(
`There was an error registering the payment method with id '%s': `,
'woo-gutenberg-products-block'
),
paymentMethod.paymentMethodId
);
createErrorNotice( `${ errorText } ${ e }`, {
context: noticeContext,
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
} );
registrationErrorNotice( paymentMethod, e as string, express );
}
}
}
const availablePaymentMethodNames = Object.keys( availablePaymentMethods );
const currentlyAvailablePaymentMethods = express
? select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods()
: select( PAYMENT_STORE_KEY ).getAvailablePaymentMethods();
const availablePaymentMethodNames = Object.keys( availablePaymentMethods );
if (
Object.keys( currentlyAvailablePaymentMethods ).length ===
availablePaymentMethodNames.length &&
@ -216,10 +229,11 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
__internalSetAvailablePaymentMethods,
__internalSetAvailableExpressPaymentMethods,
} = dispatch( PAYMENT_STORE_KEY );
if ( express ) {
__internalSetAvailableExpressPaymentMethods( availablePaymentMethods );
return true;
}
__internalSetAvailablePaymentMethods( availablePaymentMethods );
const setCallback = express
? __internalSetAvailableExpressPaymentMethods
: __internalSetAvailablePaymentMethods;
setCallback( availablePaymentMethods );
return true;
};

View File

@ -637,6 +637,7 @@ export const previewCart: CartResponse = {
],
},
errors: [],
payment_methods: [ 'cod', 'bacs', 'cheque' ],
payment_requirements: [ 'products' ],
extensions: {},
};

View File

@ -178,6 +178,7 @@ export interface CartResponse {
fees: Array< CartResponseFeeItem >;
totals: CartResponseTotals;
errors: Array< CartResponseErrorItem >;
payment_methods: string[];
payment_requirements: Array< unknown >;
extensions: ExtensionsData;
}

View File

@ -200,6 +200,7 @@ export interface Cart extends Record< string, unknown > {
fees: Array< CartFeeItem >;
totals: CartTotals;
errors: Array< CartErrorItem >;
paymentMethods: Array< string >;
paymentRequirements: Array< string >;
extensions: ExtensionsData;
}

View File

@ -56,6 +56,7 @@ export interface StoreCart {
extensions: Record< string, unknown >;
isLoadingRates: boolean;
cartHasCalculatedShipping: boolean;
paymentMethods: string[];
paymentRequirements: string[];
receiveCart: ( cart: CartResponse ) => void;
receiveCartContents: ( cart: CartResponse ) => void;

View File

@ -318,17 +318,17 @@ class Checkout extends AbstractBlock {
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
$payment_gateways = $this->get_enabled_payment_gateways();
// These are used to show options in the sidebar. We want to get the full list of enabled payment methods,
// not just the ones that are available for the current cart (which may not exist yet).
$payment_methods = $this->get_enabled_payment_gateways();
$formatted_payment_methods = array_reduce(
$payment_gateways,
$payment_methods,
function( $acc, $method ) {
if ( 'yes' === $method->enabled ) {
$acc[] = [
'id' => $method->id,
'title' => $method->method_title,
'description' => $method->method_description,
];
}
$acc[] = [
'id' => $method->id,
'title' => $method->method_title,
'description' => $method->method_description,
];
return $acc;
},
[]

View File

@ -82,6 +82,8 @@ class Api {
public function add_payment_method_script_data() {
// Enqueue the order of enabled gateways as `paymentGatewaySortOrder`.
if ( ! $this->asset_registry->exists( 'paymentGatewaySortOrder' ) ) {
// We use payment_gateways() here to get the sort order of all enabled gateways. Some may be
// programmatically disabled later on, but we still need to know where the enabled ones are in the list.
$payment_gateways = WC()->payment_gateways->payment_gateways();
$enabled_gateways = array_filter( $payment_gateways, array( $this, 'is_payment_gateway_enabled' ) );
$this->asset_registry->add( 'paymentGatewaySortOrder', array_keys( $enabled_gateways ) );

View File

@ -309,6 +309,12 @@ class CartSchema extends AbstractSchema {
'properties' => $this->force_schema_readonly( $this->error_schema->get_properties() ),
],
],
'payment_methods' => [
'description' => __( 'List of available payment method IDs that can be used to process the order.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'payment_requirements' => [
'description' => __( 'List of required payment gateway features to process the order.', 'woo-gutenberg-products-block' ),
'type' => 'array',
@ -373,6 +379,7 @@ class CartSchema extends AbstractSchema {
]
),
'errors' => $cart_errors,
'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ),
'payment_requirements' => $this->extend->get_payment_requirements(),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ),
];

View File

@ -112,7 +112,7 @@ class CheckoutSchema extends AbstractSchema {
'description' => __( 'The ID of the payment method being used to process the payment.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'enum' => wc()->payment_gateways->get_payment_gateway_ids(),
'enum' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ),
],
'create_account' => [
'description' => __( 'Whether to create a new user account as part of order processing.', 'woo-gutenberg-products-block' ),

View File

@ -262,6 +262,7 @@ All endpoints under `/cart` (listed in this doc) return responses in the same fo
"tax_lines": []
},
"errors": [],
"payment_methods": [ "cod", "bacs", "cheque" ],
"payment_requirements": [ "products" ],
"extensions": {}
}