allow payment methods to disable based on shipping (API change) (https://github.com/woocommerce/woocommerce-blocks/pull/2840)

* allow payment methods to disable based on shipping or other factors:
- renamed 'initialized' array 'available' to match primary purpose of
  `canMakePayment` api - whether payment method should be available
- trigger refresh of available payment methods when shopper chooses
  different shipping method
- rename resolveCanMakePayments => refreshCanMakePayments
- tweaked some variable names and scope for clarity
- added comments to clarify things

Note this should not affect behaviour yet - no existing payment methods
use this new feature. COD payment method will need this - woocommerce/woocommerce-blocks#2831

* optimise refreshCanMakePayments:
- useShallowEqual to avoid unnecessary call when shipping methods have
not actually changed (but object value has)

* replace ("set") payment methods in store, was appending:
- payment methods may come and go depending on cart/checkout state
- the previous SET action appended provided payment methods to the
  collection
- this prevents dynamic payment methods e.g. COD from being able to hide
  i.e. disable

* cache test payment request to avoid unnecessary stripe API calls:
- in the canMakePayment callback there's a test payment to determine if
  chrome pay/apple pay is set up and available
- canMakePayment is now called multiple times as checkout state changes
- now the results of the test payment are stored in variable, and
  returned on subsequent calls

* set init flag to avoid additional attempts to init stripe API:
+ tweak naming of init flag
This commit is contained in:
Rua Haszard 2020-07-14 10:52:13 +12:00 committed by GitHub
parent ea1435457a
commit 1df11f5461
3 changed files with 74 additions and 44 deletions

View File

@ -107,10 +107,7 @@ const reducer = (
case SET_REGISTERED_PAYMENT_METHODS:
return {
...state,
paymentMethods: {
...state.paymentMethods,
...paymentMethods,
},
paymentMethods,
};
case SET_REGISTERED_EXPRESS_PAYMENT_METHODS:
return {

View File

@ -11,7 +11,7 @@ import {
useEditorContext,
useShippingDataContext,
} from '@woocommerce/base-context';
import { useStoreCart } from '@woocommerce/base-hooks';
import { useStoreCart, useShallowEqual } from '@woocommerce/base-hooks';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
/**
@ -54,12 +54,14 @@ const usePaymentMethodRegistration = (
) => {
const [ isInitialized, setIsInitialized ] = useState( false );
const { isEditor } = useEditorContext();
const { shippingAddress } = useShippingDataContext();
const { selectedRates, shippingAddress } = useShippingDataContext();
const selectedShippingMethods = useShallowEqual( selectedRates );
const { cartTotals, cartNeedsShipping } = useStoreCart();
const canPayArgument = useRef( {
cartTotals,
cartNeedsShipping,
shippingAddress,
selectedShippingMethods,
} );
useEffect( () => {
@ -67,51 +69,64 @@ const usePaymentMethodRegistration = (
cartTotals,
cartNeedsShipping,
shippingAddress,
selectedShippingMethods,
};
}, [ cartTotals, cartNeedsShipping, shippingAddress ] );
}, [
cartTotals,
cartNeedsShipping,
shippingAddress,
selectedShippingMethods,
] );
const resolveCanMakePayments = useCallback( async () => {
let initializedPaymentMethods = {},
canPay;
const setInitializedPaymentMethods = ( paymentMethod ) => {
initializedPaymentMethods = {
...initializedPaymentMethods,
const refreshCanMakePayments = useCallback( async () => {
let availablePaymentMethods = {};
const addAvailablePaymentMethod = ( paymentMethod ) => {
availablePaymentMethods = {
...availablePaymentMethods,
[ paymentMethod.name ]: paymentMethod,
};
};
for ( const paymentMethodName in registeredPaymentMethods ) {
const current = registeredPaymentMethods[ paymentMethodName ];
const paymentMethod = registeredPaymentMethods[ paymentMethodName ];
// In editor, shortcut so all payment methods show as available.
if ( isEditor ) {
setInitializedPaymentMethods( current );
addAvailablePaymentMethod( paymentMethod );
continue;
}
// In front end, ask payment method if it should be available.
try {
canPay = await Promise.resolve(
current.canMakePayment( canPayArgument.current )
const canPay = await Promise.resolve(
paymentMethod.canMakePayment( canPayArgument.current )
);
if ( canPay ) {
if ( canPay.error ) {
throw new Error( canPay.error.message );
}
setInitializedPaymentMethods( current );
addAvailablePaymentMethod( paymentMethod );
}
} catch ( e ) {
// If user is admin, show payment `canMakePayment` errors as a notice.
handleRegistrationError( e );
}
}
// all payment methods have been initialized so dispatch and set
dispatcher( initializedPaymentMethods );
// Re-dispatch available payment methods to store.
dispatcher( availablePaymentMethods );
// Note: some payment methods use the `canMakePayment` callback to initialize / setup.
// Example: Stripe CC, Stripe Payment Request.
// That's why we track "is initialised" state here.
setIsInitialized( true );
}, [ dispatcher, isEditor, registeredPaymentMethods ] );
// if not initialized invoke the callback to kick off resolving the payments.
// Determine which payment methods are available initially and whenever
// shipping methods change.
// Some payment methods (e.g. COD) can be disabled for specific shipping methods.
useEffect( () => {
if ( ! isInitialized ) {
resolveCanMakePayments();
}
}, [ resolveCanMakePayments, isInitialized ] );
refreshCanMakePayments();
}, [ refreshCanMakePayments, selectedShippingMethods ] );
return isInitialized;
};

View File

@ -16,30 +16,48 @@ const ApplePayPreview = () => <img src={ applePayImage } alt="" />;
const canPayStripePromise = loadStripe();
const componentStripePromise = loadStripe();
let isStripeInitialized = false,
canPay = false;
// Initialise stripe API client and determine if payment method can be used
// in current environment (e.g. geo + shopper has payment settings configured).
function paymentRequestAvailable( currencyCode ) {
// If we've already initialised, return the cached results.
if ( isStripeInitialized ) {
return canPay;
}
return canPayStripePromise.then( ( stripe ) => {
if ( stripe === null ) {
isStripeInitialized = true;
return canPay;
}
// Do a test payment to confirm if payment method is available.
const paymentRequest = stripe.paymentRequest( {
total: {
label: 'Test total',
amount: 1000,
},
country: getSetting( 'baseLocation', {} )?.country,
currency: currencyCode,
} );
return paymentRequest.canMakePayment().then( ( result ) => {
canPay = !! result;
isStripeInitialized = true;
return canPay;
} );
} );
}
const PaymentRequestPaymentMethod = {
name: PAYMENT_METHOD_NAME,
content: <PaymentRequestExpress stripe={ componentStripePromise } />,
edit: <ApplePayPreview />,
canMakePayment: ( cartData ) =>
canPayStripePromise.then( ( stripe ) => {
if ( stripe === null ) {
return false;
}
// do a test payment request to check if payment request payment can be
// done.
const paymentRequest = stripe.paymentRequest( {
total: {
label: 'Test total',
amount: 1000,
},
country: getSetting( 'baseLocation', {} )?.country,
// eslint-disable-next-line camelcase
currency: cartData?.cartTotals?.currency_code?.toLowerCase(),
} );
return paymentRequest
.canMakePayment()
.then( ( result ) => !! result );
} ),
paymentRequestAvailable(
// eslint-disable-next-line camelcase
cartData?.cartTotals?.currency_code?.toLowerCase()
),
paymentMethodId: 'stripe',
};