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

Add payment method api and components to checkout steps #1349

Merged
merged 28 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cef7a16
add tabs component
nerrad Dec 15, 2019
63a736d
add checkout context
nerrad Dec 15, 2019
6d7ca7f
add checkout hooks
nerrad Dec 15, 2019
959e545
add payment method hooks
nerrad Dec 15, 2019
55cb116
add payment method registration api
nerrad Dec 15, 2019
97c0f2f
add payment method wrapper components
nerrad Dec 15, 2019
fa99dc4
implement payment method components
nerrad Dec 15, 2019
e96c670
implement demo with dummy payment method registration
nerrad Dec 15, 2019
a995c13
improve naming of variable
nerrad Dec 18, 2019
43a79a1
remove unnecessary finished status and drop ability to reset to prist…
nerrad Dec 18, 2019
a02a07f
add placeOrderLabel to checkout context and add hook for exposing
nerrad Dec 18, 2019
2b8da64
add defined payment method configs and implement initialization logic
nerrad Dec 19, 2019
c95059f
add clearAllNotices action to `useCheckoutNotices` hook.
nerrad Dec 19, 2019
d792316
use consistent type for defaults
nerrad Dec 20, 2019
f390e4f
use TypeError instead of Error and remove Object.freeze
nerrad Dec 20, 2019
864ad3a
refactor so payment method config expects react nodes
nerrad Dec 20, 2019
aeadf14
add missing dependency
nerrad Dec 20, 2019
c21db2a
Throw error if unable to retrieve information for the selected tab
nerrad Dec 20, 2019
6e9bd0d
remove lodash dependency
nerrad Dec 20, 2019
8149971
destructure status constant
nerrad Dec 20, 2019
f34dcc2
follow naming convention for boolean type logic
nerrad Jan 3, 2020
b8a3974
add missing dependency
nerrad Jan 3, 2020
2ede9a5
remove unnecessary calc usage
nerrad Jan 3, 2020
6853d21
remove unnecessary prefix
nerrad Jan 3, 2020
76ce412
remove unnecessary conditional check
nerrad Jan 3, 2020
c833a83
put less expensive conditional check first
nerrad Jan 3, 2020
ae58ab4
add package alias to jest config
nerrad Jan 3, 2020
c252929
simplify initialization of selected tab
nerrad Jan 6, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions assets/js/base/components/payment-methods/express-checkout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useExpressPaymentMethods } from '@woocommerce/base-hooks';

/**
* Internal dependencies
*/
import ExpressPaymentMethods from './express-payment-methods';

import './style.scss';

const ExpressCheckoutContainer = ( { children } ) => {
nerrad marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="wc-component__checkout-container wc-component__container-with-border">
<div className="wc-component__text-overlay-on-border">
<strong>
{ __( 'Express checkout', 'woo-gutenberg-products-block' ) }
</strong>
</div>
<div className="wc-component__container-content">{ children }</div>
</div>
);
};

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 )
nerrad marked this conversation as resolved.
Show resolved Hide resolved
) {
return null;
}

return (
nerrad marked this conversation as resolved.
Show resolved Hide resolved
<ExpressCheckoutContainer>
<p>
{ __(
'In a hurry? Use one of our express checkout options below:',
'woo-gutenberg-products-block'
) }
</p>
<ExpressPaymentMethods />
</ExpressCheckoutContainer>
);
};

export default ExpressCheckoutFormControl;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* External dependencies
*/
import {
useCheckoutData,
usePaymentEvents,
useExpressPaymentMethods,
} from '@woocommerce/base-hooks';
import { cloneElement } from '@wordpress/element';

const ExpressPaymentMethods = () => {
const [ checkoutData ] = useCheckoutData();
const { dispatch, select } = usePaymentEvents();
// 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 ? (
Aljullu marked this conversation as resolved.
Show resolved Hide resolved
paymentMethodSlugs.map( ( slug ) => {
const expressPaymentMethod =
paymentMethods[ slug ].activeContent;
const paymentEvents = { dispatch, select };
return (
<li key={ slug } id={ `express-payment-method-${ slug }` }>
{ cloneElement( expressPaymentMethod, {
checkoutData,
paymentEvents,
} ) }
</li>
);
} )
) : (
<li key="noneRegistered">No registered Payment Methods</li>
nerrad marked this conversation as resolved.
Show resolved Hide resolved
);
return (
<ul className="wc-component__express-payment-event-buttons">
{ content }
</ul>
);
};

export default ExpressPaymentMethods;
3 changes: 3 additions & 0 deletions assets/js/base/components/payment-methods/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as PaymentMethods } from './payment-methods';
export { default as ExpressPaymentMethods } from './express-payment-methods';
export { default as ExpressCheckoutFormControl } from './express-checkout';
90 changes: 90 additions & 0 deletions assets/js/base/components/payment-methods/payment-methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import {
useCheckoutData,
usePaymentEvents,
useActivePaymentMethod,
usePaymentMethods,
} from '@woocommerce/base-hooks';
import { useCallback, cloneElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import Tabs from '../tabs';

const noPaymentMethodTab = () => {
const label = __( 'Not Existing', 'woo-gutenberg-products-block' );
return {
name: label,
label,
title: () => label,
};
};

const createTabs = ( paymentMethods ) => {
const paymentMethodsKeys = Object.keys( paymentMethods );
return paymentMethodsKeys.length > 0
? paymentMethodsKeys.map( ( key ) => {
Aljullu marked this conversation as resolved.
Show resolved Hide resolved
const { label, ariaLabel } = paymentMethods[ key ];
return {
name: key,
title: () => label,
ariaLabel,
};
} )
: [ noPaymentMethodTab() ];
};

const PaymentMethods = () => {
const [ checkoutData ] = useCheckoutData();
const { dispatch, select } = usePaymentEvents();
const { isInitialized, paymentMethods } = usePaymentMethods();
const {
activePaymentMethod,
setActivePaymentMethod,
} = useActivePaymentMethod();
const getRenderedTab = useCallback(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of memoizing this function? Is it to preserve something in the payment method component?

If it's just to inject the selectedTab as a render prop, could we use a custom hook (eg: getSelectedTab) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably no need to memoize actually.

If it's just to inject the selectedTab as a render prop, could we use a custom hook (eg: getSelectedTab) instead?

Hmm... I'm not sure I follow what you are suggesting here. Do you mean useSelectedTab?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I'm not sure I follow what you are suggesting here. Do you mean useSelectedTab?

Oh, um, yes. I think that is probably what I meant. 😅 Sorry.

() => ( selectedTab ) => {
const paymentMethod =
( paymentMethods[ selectedTab ] &&
paymentMethods[ selectedTab ].activeContent ) ||
null;
const paymentEvents = { dispatch, select };
return paymentMethod
? cloneElement( paymentMethod, {
isActive: true,
checkoutData,
paymentEvents,
} )
: null;
},
[ checkoutData, dispatch, select ]
);
if (
! isInitialized ||
( Object.keys( paymentMethods ).length === 0 && isInitialized )
nerrad marked this conversation as resolved.
Show resolved Hide resolved
) {
// @todo this can be a placeholder informing the user there are no
// payment methods setup?
Copy link
Contributor Author

@nerrad nerrad Dec 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...but only in the editor. Might want to consider something for the frontend to communicate to customers that payments can't be completed?

return <div>No Payment Methods Initialized</div>;
}
return (
<Tabs
className="wc-component__payment-method-options"
onSelect={ ( tabName ) => setActivePaymentMethod( tabName ) }
tabs={ createTabs( paymentMethods ) }
initialTabName={ activePaymentMethod }
ariaLabel={ __(
'Payment Methods',
'woo-gutenberg-products-block'
) }
>
{ getRenderedTab() }
</Tabs>
);
};

export default PaymentMethods;
38 changes: 38 additions & 0 deletions assets/js/base/components/payment-methods/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.wc-component__container-with-border {
margin: auto;
border: 2px solid $black;
border-radius: 5px;
padding: 10px;
margin-bottom: $gap-large;

.wc-component__text-overlay-on-border {
position: relative;
top: -24px;
background-color: $white;
padding-left: $gap-small;
padding-right: $gap-small;
width: fit-content;
color: $black;
}

.wc-component__container-content {
margin-top: -15px;
padding-left: $gap-large;
padding-right: $gap-large;
padding-bottom: $gap;
}

.wc-component__express-payment-event-buttons {
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
padding: 0;
margin: 0;
> li {
display: inline-block;
width: 50%;
}
}
}
105 changes: 105 additions & 0 deletions assets/js/base/components/tabs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import './style.scss';

const TabButton = ( {
tabId,
onClick,
children,
selected,
ariaLabel,
...rest
nerrad marked this conversation as resolved.
Show resolved Hide resolved
} ) => {
return (
<button
role="tab"
type="button"
tabIndex={ selected ? null : -1 }
nerrad marked this conversation as resolved.
Show resolved Hide resolved
aria-selected={ selected }
aria-label={ ariaLabel }
id={ tabId }
onClick={ onClick }
{ ...rest }
>
<span className="wc-component__tab-item-content">{ children }</span>
</button>
);
};

const Tabs = ( {
className,
onSelect = () => null,
tabs,
activeClass = 'is-active',
initialTabName,
instanceId,
ariaLabel = __( 'Tabbed Content', 'woo-gutenberg-products-block' ),
children,
} ) => {
const [ selected, setSelected ] = useState(
initialTabName || ( tabs.length > 0 ? tabs[ 0 ].name : '' )
);
if ( ! selected ) {
return null;
}
const handleClick = ( tabKey ) => {
setSelected( tabKey );
onSelect( tabKey );
};
const selectedTab = tabs.find( ( tab ) => tab.name === selected );
if ( ! selectedTab ) {
throw new Error( 'There is no available tab for the selected item' );
}
const selectedId = `${ instanceId }-${ selectedTab.name }`;
nerrad marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className={ classnames( 'wc-component__tabs', className ) }>
<div
role="tablist"
aria-label={ ariaLabel }
className="wc-component__tab-list"
>
{ tabs.map( ( tab ) => (
<TabButton
className={ classnames(
'wc-component__tab-item',
tab.className,
{
[ activeClass ]: tab.name === selected,
}
) }
tabId={ `${ instanceId }-${ tab.name }` }
aria-controls={ `${ instanceId }-${ tab.name }-view` }
selected={ tab.name === selected }
key={ tab.name }
ariaLabel={ tab.ariaLabel || null }
onClick={ () => handleClick( tab.name ) }
>
{ tab.title() }
</TabButton>
) ) }
</div>
{ selectedTab && (
<div
aria-labelledby={ selectedId }
role="tabpanel"
id={ `${ selectedId }-view` }
className="wc-component__tab-content"
tabIndex="0"
>
{ children( selected ) }
nerrad marked this conversation as resolved.
Show resolved Hide resolved
</div>
) }
</div>
);
};

export default withInstanceId( Tabs );
nerrad marked this conversation as resolved.
Show resolved Hide resolved
35 changes: 35 additions & 0 deletions assets/js/base/components/tabs/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.wc-component__tabs {
.wc-component__tab-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
> .wc-component__tab-item {
border: none;
flex: auto;
background: transparent;
padding: $gap-small $gap;
color: $black;
outline-offset: -1px;
transition: box-shadow 0.1s linear;
&.is-active {
box-shadow: inset 0 -3px $black;
font-weight: 600;
position: relative;
}
&:focus {
color: $black;
outline-offset: -1px;
outline: 1px dotted $gray-60;
}
.wc-component__tab-item-content {
width: fit-content;
display: block;
margin: auto;
}
}
}
.wc-component__tab-content {
padding: $gap;
}
}
Loading