Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
add defined payment method configs and implement initialization logic
Browse files Browse the repository at this point in the history
  • Loading branch information
nerrad committed Dec 19, 2019
1 parent e403ae2 commit d6c7e4f
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 77 deletions.
12 changes: 12 additions & 0 deletions assets/js/base/components/payment-methods/express-checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useExpressPaymentMethods } from '@woocommerce/base-hooks';

/**
* Internal dependencies
Expand All @@ -24,6 +25,17 @@ const ExpressCheckoutContainer = ( { children } ) => {
};

const ExpressCheckoutFormControl = () => {
const { paymentMethods, isInitialized } = useExpressPaymentMethods();

// determine whether we even show this
// @todo if in the editor we probably would want to show a placeholder maybe?
if (
! isInitialized ||
( isInitialized && Object.keys( paymentMethods ).length === 0 )
) {
return null;
}

return (
<ExpressCheckoutContainer>
<p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
/**
* External dependencies
*/
import { getExpressPaymentMethods } from '@woocommerce/blocks-registry';
import { useCheckoutData, usePaymentEvents } from '@woocommerce/base-hooks';
import {
useCheckoutData,
usePaymentEvents,
useExpressPaymentMethods,
} from '@woocommerce/base-hooks';

const ExpressPaymentMethods = () => {
const [ checkoutData ] = useCheckoutData();
const { dispatch, select } = usePaymentEvents();
const paymentMethods = getExpressPaymentMethods();
// not implementing isInitialized here because it's utilized further
// up in the tree for express payment methods. We won't even get here if
// there's no payment methods after initialization.
const { paymentMethods } = useExpressPaymentMethods();
const paymentMethodSlugs = Object.keys( paymentMethods );
const content =
paymentMethodSlugs.length > 0 ? (
paymentMethodSlugs.map( ( slug ) => {
const ExpressPaymentMethod = paymentMethods[ slug ];
const ExpressPaymentMethod =
paymentMethods[ slug ].activeContent;
const paymentEvents = { dispatch, select };
return (
<li
Expand Down
30 changes: 13 additions & 17 deletions assets/js/base/components/payment-methods/payment-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
useCheckoutData,
usePaymentEvents,
useActivePaymentMethod,
usePaymentMethods,
} from '@woocommerce/base-hooks';
import { getPaymentMethods } from '@woocommerce/blocks-registry';
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

Expand All @@ -28,21 +28,20 @@ const createTabs = ( paymentMethods ) => {
const paymentMethodsKeys = Object.keys( paymentMethods );
return paymentMethodsKeys.length > 0
? paymentMethodsKeys.map( ( key ) => {
const { tab, ariaLabel } = paymentMethods[ key ];
const { label, ariaLabel } = paymentMethods[ key ];
return {
name: key,
title: tab,
title: label,
ariaLabel,
};
} )
: [ noPaymentMethodTab() ];
};

const paymentMethods = getPaymentMethods();

const PaymentMethods = () => {
const [ checkoutData ] = useCheckoutData();
const { dispatch, select } = usePaymentEvents();
const { isInitialized, paymentMethods } = usePaymentMethods();
const {
activePaymentMethod,
setActivePaymentMethod,
Expand All @@ -51,19 +50,8 @@ const PaymentMethods = () => {
() => ( selectedTab ) => {
const PaymentMethod =
( paymentMethods[ selectedTab ] &&
paymentMethods[ selectedTab ].content ) ||
paymentMethods[ selectedTab ].activeContent ) ||
null;
// @todo if undefined return placeholder for no registered payment methods
if ( ! PaymentMethod ) {
return (
<p>
{ __(
'No payment methods setup',
'woo-gutenberg-products-block'
) }
</p>
);
}
const paymentEvents = { dispatch, select };
return (
<PaymentMethod
Expand All @@ -75,6 +63,14 @@ const PaymentMethods = () => {
},
[ checkoutData, dispatch, select ]
);
if (
! isInitialized ||
( Object.keys( paymentMethods ).length === 0 && isInitialized )
) {
// @todo this can be a placeholder informing the user there are no
// payment methods setup?
return <div>No Payment Methods Initialized</div>;
}
return (
<Tabs
className="wc-component__payment-method-options"
Expand Down
1 change: 1 addition & 0 deletions assets/js/base/hooks/payment-methods/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as useActivePaymentMethod } from './use-active-payment-method';
export { default as usePaymentEvents } from './use-payment-events';
export * from './use-payment-methods';
16 changes: 10 additions & 6 deletions assets/js/base/hooks/payment-methods/use-active-payment-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@
* External dependencies
*/
import { useCheckoutContext } from '@woocommerce/base-context/checkout-context';
import { usePaymentMethods } from '@woocommerce/base-hooks';
import { useEffect } from '@wordpress/element';
import { getPaymentMethods } from '@woocommerce/blocks-registry';

const useActivePaymentMethod = () => {
const {
activePaymentMethod,
setActivePaymentMethod,
} = useCheckoutContext();
const { paymentMethods, isInitialized } = usePaymentMethods();
// if payment method has not been set yet, let's set it.
useEffect( () => {
// if not initialized yet bail
if ( ! isInitialized ) {
return;
}
if ( ! activePaymentMethod && activePaymentMethod !== null ) {
const paymentMethods = getPaymentMethods();
const paymentMethodSlugs = Object.keys( paymentMethods );
const paymentMethodIds = Object.keys( paymentMethods );
setActivePaymentMethod(
paymentMethodSlugs.length > 0
? paymentMethods[ paymentMethodSlugs[ 0 ] ].name
paymentMethodIds.length > 0
? paymentMethods[ paymentMethodIds[ 0 ] ].name
: null
);
}
}, [ activePaymentMethod, setActivePaymentMethod ] );
}, [ activePaymentMethod, setActivePaymentMethod, isInitialized ] );
return { activePaymentMethod, setActivePaymentMethod };
};

Expand Down
59 changes: 59 additions & 0 deletions assets/js/base/hooks/payment-methods/use-payment-methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* External dependencies
*/
import {
getPaymentMethods,
getExpressPaymentMethods,
} from '@woocommerce/blocks-registry';
import { useState, useEffect, useRef } from '@wordpress/element';

const usePaymentMethodState = ( registeredPaymentMethods ) => {
const [ paymentMethods, setPaymentMethods ] = useState( [] );
const [ isInitialized, setIsInitialized ] = useState( false );
const countPaymentMethodsInitializing = useRef(
Object.keys( registeredPaymentMethods ).length
);

useEffect( () => {
// if all payment methods are initialized then bail.
if ( isInitialized ) {
return;
}
// loop through payment methods and see what the state is
for ( const paymentMethodId in registeredPaymentMethods ) {
const current = registeredPaymentMethods[ paymentMethodId ];
current.canMakePayment
.then( ( canPay ) => {
if ( canPay ) {
setPaymentMethods( ( previousPaymentMethods ) => {
return {
...previousPaymentMethods,
[ current.id ]: current,
};
} );
}
// update the initialized count
countPaymentMethodsInitializing.current--;
// if count remaining less than 1, then set initialized.
if ( countPaymentMethodsInitializing.current < 1 ) {
setIsInitialized( true );
}
} )
.catch( ( error ) => {
// @todo, would be a good place to use the checkout error
// hooks here? Or maybe throw and catch by error boundary?
throw new Error(
'Problem with payment method initialization' +
( error.message || '' )
);
} );
}
}, [ isInitialized ] );

return { paymentMethods, isInitialized };
};

export const usePaymentMethods = () =>
usePaymentMethodState( getPaymentMethods() );
export const useExpressPaymentMethods = () =>
usePaymentMethodState( getExpressPaymentMethods() );
55 changes: 25 additions & 30 deletions assets/js/blocks-registry/payment-methods/assertions.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
export const assertValidPaymentMethodComponent = ( paymentMethod ) => {
export const assertValidPaymentMethodComponent = (
component,
componentName
) => {
// @todo detect if functional component (not render prop)
if ( typeof paymentMethod !== 'function' ) {
if ( typeof component !== 'function' ) {
throw new Error(
'The registered payment method must be a functional component'
`The ${ componentName } for the payment method must be a functional component`
);
}
};

export const assertValidPaymentMethod = ( paymentMethod ) => {
// paymentMethods are expected to have 4 properties, tab, content, name, ariaLabel.
if ( ! paymentMethod.tab ) {
throw new Error(
'A payment method is expected to have a tab property'
);
export const assertConfigHasProperties = (
config,
expectedProperties = []
) => {
const missingProperties = expectedProperties.reduce( ( acc, property ) => {
if ( ! config[ property ] ) {
acc.push( property );
}
return acc;
}, [] );
if ( missingProperties.length > 0 ) {
const message =
'The payment method configuration object is missing the following properties:';
throw new Error( message + missingProperties.join( ', ' ) );
}
if ( ! paymentMethod.content ) {
throw new Error(
'A payment method is expected to have a content property'
);
}
if ( ! paymentMethod.ariaLabel ) {
throw new Error(
'A payment method is expected to have an ariaLabel property. This is used for tabs aria-label property.'
);
}
try {
assertValidPaymentMethodComponent( paymentMethod.content );
} catch ( e ) {
throw new Error(
'The paymentMethod.content value must be a functional components'
);
}
try {
assertValidPaymentMethodComponent( paymentMethod.tab );
} catch ( e ) {
};

export const assertValidPaymentMethodCreator = ( creator, configName ) => {
if ( typeof creator !== 'function' ) {
throw new Error(
'The paymentMethod.tab value must be a functional components'
`A payment method must be registered with a function that creates and returns a ${ configName } instance`
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Internal dependencies
*/
import {
assertConfigHasProperties,
assertValidPaymentMethodComponent,
} from './assertions';

export default class ExpressPaymentMethodConfig {
constructor( config ) {
// validate config
ExpressPaymentMethodConfig.assertValidConfig( config );
this.id = config.id;
this.activeContent = config.activeContent;
this.canMakePayment = config.canMakePayment;
Object.freeze( this );
}

static assertValidConfig = ( config ) => {
assertConfigHasProperties( config, [ 'id', 'activeContent' ] );
if ( typeof config.id !== 'string' ) {
throw new Error(
'The id for the express payment method must be a string'
);
}
assertValidPaymentMethodComponent(
config.activeContent,
'activeContent'
);
if ( ! ( config.canMakePayment instanceof Promise ) ) {
throw new Error(
'The canMakePayment property for the express payment method must be a promise.'
);
}
};
}
51 changes: 51 additions & 0 deletions assets/js/blocks-registry/payment-methods/payment-method-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Internal dependencies
*/
import {
assertConfigHasProperties,
assertValidPaymentMethodComponent,
} from './assertions';

export default class PaymentMethodConfig {
constructor( config ) {
// validate config
PaymentMethodConfig.assertValidConfig( config );
this.id = config.id;
this.label = config.label;
this.stepContent = config.stepContent;
this.ariaLabel = config.ariaLabel;
this.activeContent = config.activeContent;
this.canMakePayment = config.canMakePayment;
Object.freeze( this );
}

static assertValidConfig = ( config ) => {
assertConfigHasProperties( config, [
'id',
'label',
'stepContent',
'ariaLabel',
'activeContent',
'canMakePayment',
] );
if ( typeof config.id !== 'string' ) {
throw new Error( 'The id for the payment method must be a string' );
}
assertValidPaymentMethodComponent( config.label, 'label' );
assertValidPaymentMethodComponent( config.stepContent, 'stepContent' );
assertValidPaymentMethodComponent(
config.activeContent,
'activeContent'
);
if ( typeof config.ariaLabel !== 'string' ) {
throw new Error(
'The ariaLabel for the payment method must be a string'
);
}
if ( ! ( config.canMakePayment instanceof Promise ) ) {
throw new Error(
'The canMakePayment property for the payment method must be a promise.'
);
}
};
}
Loading

0 comments on commit d6c7e4f

Please sign in to comment.