Convert apple pay integration to payment request integration and finish implementation (https://github.com/woocommerce/woocommerce-blocks/pull/2127)

* add logic allowing payment method to be overridden via payment data in request

* hook in to trigger server side processing of stripe payment request

* improvements to shipping data context

- memoize event emitters
- split up emitted events (reduces how often events trigger)
- Include whether rate is being selected in exported data.

* expose `isSelectingRate` value to payment method interface

* fix typo in shipping emitters for emitter type

* include setting of shipping data in payment method success status call

- this also requires changing the nested order of providers in checkout provider

* fix priority logic for event emitters.

- lower priority is supposed to fire before higher priority.

* normalize postal code for comparisons

* move normalize functions into stripe-utils folder

* refactor stripePromise so that it provides a specific instance to each payment method.

This also provides it as a prop to the pm components.

* renadme apple pay express to payment request express

This adds full support for the stripe payment request api instead of just applePay (so GooglePay, MicrosoftPay and ApplePay are now supported).

Also adds numerous fixes to internal logic.

* add handling to skip core checkout validation logic if express payment method is handling payment

Express payment methods have their own internal validation so this removes the need for checkout validating fields. This is also necessary because checkout validation breaks the flow when making a payment using express payment methods because of the order of the flow for these methods.

* splitting out emmitter effects for checkout and improving logic

Splitting up effects limits the potential for firing off emitters more than needed.

* remove unnecessary ref definitions

* fix on cancel action erroring for payment request modal

* ensure unique stripe object for component and canPay

* set default total label if one isn’t configured on the server

* fix order of state changes

* simplify condition

* remove unnecessary dependency

* normalize to uppercase too

* simplify can make payment conditional

* update comment blocks
This commit is contained in:
Darren Ethier 2020-04-08 12:36:04 -04:00 committed by GitHub
parent bfdfa2f603
commit d79f5ab271
26 changed files with 651 additions and 114 deletions

View File

@ -139,7 +139,7 @@ export const CheckoutStateProvider = ( {
// emit events.
useEffect( () => {
const { hasError, status } = checkoutState;
const { status } = checkoutState;
if ( status === STATUS.PROCESSING ) {
removeNotices( 'error' );
emitEvent(
@ -147,7 +147,7 @@ export const CheckoutStateProvider = ( {
EMIT_TYPES.CHECKOUT_PROCESSING,
{}
).then( ( response ) => {
if ( response !== true || hasError ) {
if ( response !== true ) {
if ( Array.isArray( response ) ) {
response.forEach(
( { errorMessage, validationErrors } ) => {
@ -162,8 +162,11 @@ export const CheckoutStateProvider = ( {
}
} );
}
if ( status === STATUS.COMPLETE ) {
if ( hasError ) {
}, [ checkoutState.status, setValidationErrors ] );
useEffect( () => {
if ( checkoutState.status === STATUS.COMPLETE ) {
if ( checkoutState.hasError ) {
emitEvent(
currentObservers.current,
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_ERROR,
@ -177,7 +180,7 @@ export const CheckoutStateProvider = ( {
);
}
}
}, [ checkoutState.status, checkoutState.hasError, setValidationErrors ] );
}, [ checkoutState.status, checkoutState.hasError ] );
const onSubmit = () => {
dispatch( actions.setProcessing() );

View File

@ -41,14 +41,14 @@ export const CheckoutProvider = ( {
submitLabel={ submitLabel }
>
<BillingDataProvider>
<ShippingDataProvider>
<PaymentMethodDataProvider
activePaymentMethod={ initialActivePaymentMethod }
>
<ShippingDataProvider>
{ children }
<CheckoutProcessor />
</ShippingDataProvider>
</PaymentMethodDataProvider>
</ShippingDataProvider>
</BillingDataProvider>
</CheckoutStateProvider>
);

View File

@ -56,21 +56,26 @@ const CheckoutProcessor = () => {
currentStatus: currentPaymentStatus,
errorMessage,
paymentMethodData,
expressPaymentMethods,
} = usePaymentMethodDataContext();
const { addErrorNotice, removeNotice } = useStoreNotices();
const currentBillingData = useRef( billingData );
const currentShippingAddress = useRef( shippingAddress );
const [ isProcessingOrder, setIsProcessingOrder ] = useState( false );
const expressPaymentMethodActive = Object.keys(
expressPaymentMethods
).includes( activePaymentMethod );
const checkoutWillHaveError =
hasValidationErrors ||
( hasValidationErrors && ! expressPaymentMethodActive ) ||
currentPaymentStatus.hasError ||
shippingErrorStatus.hasError;
useEffect( () => {
if (
checkoutWillHaveError !== checkoutHasError &&
( checkoutIsProcessing || checkoutIsProcessingComplete )
( checkoutIsProcessing || checkoutIsProcessingComplete ) &&
! expressPaymentMethodActive
) {
dispatchActions.setHasError( checkoutWillHaveError );
}
@ -79,6 +84,7 @@ const CheckoutProcessor = () => {
checkoutHasError,
checkoutIsProcessing,
checkoutIsProcessingComplete,
expressPaymentMethodActive,
] );
const paidAndWithoutErrors =
@ -136,14 +142,16 @@ const CheckoutProcessor = () => {
] );
useEffect( () => {
const unsubscribeProcessing = onCheckoutProcessing(
checkValidation,
0
);
let unsubscribeProcessing;
if ( ! expressPaymentMethodActive ) {
unsubscribeProcessing = onCheckoutProcessing( checkValidation, 0 );
}
return () => {
if ( ! expressPaymentMethodActive ) {
unsubscribeProcessing();
}
};
}, [ onCheckoutProcessing, checkValidation ] );
}, [ onCheckoutProcessing, checkValidation, expressPaymentMethodActive ] );
const processOrder = useCallback( () => {
setIsProcessingOrder( true );
@ -209,14 +217,7 @@ const CheckoutProcessor = () => {
dispatchActions.setComplete();
setIsProcessingOrder( false );
} );
}, [
addErrorNotice,
removeNotice,
activePaymentMethod,
currentBillingData,
currentShippingAddress,
paymentMethodData,
] );
}, [ addErrorNotice, removeNotice, activePaymentMethod, paymentMethodData ] );
// setup checkout processing event observers.
useEffect( () => {
const unsubscribeRedirect = onCheckoutCompleteSuccess( () => {
@ -225,7 +226,7 @@ const CheckoutProcessor = () => {
return () => {
unsubscribeRedirect();
};
}, [ onCheckoutProcessing, onCheckoutCompleteSuccess, redirectUrl ] );
}, [ onCheckoutCompleteSuccess, redirectUrl ] );
// process order if conditions are good.
useEffect( () => {

View File

@ -5,7 +5,7 @@ import { actions } from './reducer';
export const emitterCallback = ( type, dispatcher ) => (
callback,
priority
priority = 10
) => {
const action = actions.addEventCallback( type, callback, priority );
dispatcher( action );

View File

@ -1,7 +1,7 @@
const getObserversByPriority = ( observers, eventType ) => {
return observers[ eventType ]
? Array.from( observers[ eventType ].values() ).sort( ( a, b ) => {
return b.priority - a.priority;
return a.priority - b.priority;
} )
: [];
};

View File

@ -74,8 +74,8 @@ describe( 'Testing emitters', () => {
};
await emitEventWithAbort( observers, 'test', 'foo' );
expect( console ).not.toHaveErrored();
expect( a ).toHaveBeenCalledTimes( 1 );
expect( b ).not.toHaveBeenCalled();
expect( b ).toHaveBeenCalledTimes( 1 );
expect( a ).not.toHaveBeenCalled();
} );
} );
} );

View File

@ -21,6 +21,7 @@ import {
} from './use-payment-method-registration';
import { useBillingDataContext } from '../billing';
import { useCheckoutContext } from '../checkout-state';
import { useShippingDataContext } from '../shipping';
import {
EMIT_TYPES,
emitterSubscribers,
@ -52,6 +53,7 @@ import { useStoreNotices } from '@woocommerce/base-hooks';
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatchers} PaymentStatusDispatchers
* @typedef {import('@woocommerce/type-defs/billing').BillingData} BillingData
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataResponse} ShippingDataResponse
*/
const {
@ -125,10 +127,13 @@ export const PaymentMethodDataProvider = ( {
reducer,
DEFAULT_PAYMENT_DATA
);
const setActivePaymentMethod = ( paymentMethodSlug ) => {
const setActivePaymentMethod = useCallback(
( paymentMethodSlug ) => {
setActive( paymentMethodSlug );
dispatch( statusOnly( PRISTINE ) );
};
},
[ setActive, dispatch ]
);
const paymentMethodsInitialized = usePaymentMethods( ( paymentMethod ) =>
dispatch( setRegisteredPaymentMethod( paymentMethod ) )
);
@ -139,13 +144,15 @@ export const PaymentMethodDataProvider = ( {
);
const { setValidationErrors } = useValidationContext();
const { addErrorNotice, removeNotice } = useStoreNotices();
const { setShippingAddress } = useShippingDataContext();
const setExpressPaymentError = ( message ) => {
if ( message ) {
addErrorNotice( message, {
context: 'wc/express-payment-area',
id: 'wc-express-payment-error',
} );
if ( ! message ) {
} else {
removeNotice( 'wc-express-payment-error' );
}
};
@ -185,8 +192,8 @@ export const PaymentMethodDataProvider = ( {
[ paymentStatus.currentStatus ]
);
// flip payment to processing if checkout processing is complete and there
// are no errors.
// flip payment to processing if checkout processing is complete, there are
// no errors, and payment status is started.
useEffect( () => {
if (
checkoutIsProcessingComplete &&
@ -256,8 +263,17 @@ export const PaymentMethodDataProvider = ( {
* accompany the checkout.
* @param {BillingData|null} [billingData] The billing data accompanying the
* payment method.
* @param {ShippingDataResponse|null} [shippingData] The shipping data accompanying the
* payment method.
*/
success: ( paymentMethodData = {}, billingData = null ) => {
success: (
paymentMethodData = {},
billingData = null,
shippingData = null
) => {
if ( shippingData !== null && shippingData?.address ) {
setShippingAddress( shippingData.address );
}
if ( billingData ) {
setBillingData( billingData );
}
@ -287,7 +303,8 @@ export const PaymentMethodDataProvider = ( {
if ( isSuccessResponse( response ) ) {
setPaymentStatus().success(
response.paymentMethodData,
response.billingData
response.billingData,
response.shippingData
);
} else if ( isFailResponse( response ) ) {
setPaymentStatus().failed(

View File

@ -25,11 +25,11 @@ const emitterSubscribers = ( dispatcher ) => ( {
onSuccess: emitterCallback( EMIT_TYPES.SHIPPING_RATES_SUCCESS, dispatcher ),
onFail: emitterCallback( EMIT_TYPES.SHIPPING_RATES_FAIL, dispatcher ),
onSelectSuccess: emitterCallback(
EMIT_TYPES.SHIPPING_RATES_SELECT_SUCCESS,
EMIT_TYPES.SHIPPING_RATE_SELECT_SUCCESS,
dispatcher
),
onSelectFail: emitterCallback(
EMIT_TYPES.SHIPPING_RATES_SELECT_FAIL,
EMIT_TYPES.SHIPPING_RATE_SELECT_FAIL,
dispatcher
),
} );

View File

@ -81,12 +81,17 @@ export const ShippingDataProvider = ( { children } ) => {
selectedShippingRates: selectedRates,
isSelectingRate,
} = useSelectShippingRate( shippingRates );
const onShippingRateSuccess = emitterSubscribers( subscriber ).onSuccess;
const onShippingRateFail = emitterSubscribers( subscriber ).onFail;
const onShippingRateSelectSuccess = emitterSubscribers( subscriber )
.onSelectSuccess;
const onShippingRateSelectFail = emitterSubscribers( subscriber )
.onShippingRateSelectFail;
const eventSubscribers = useMemo(
() => ( {
onShippingRateSuccess: emitterSubscribers( subscriber ).onSuccess,
onShippingRateFail: emitterSubscribers( subscriber ).onFail,
onShippingRateSelectSuccess: emitterSubscribers( subscriber )
.onSelectSuccess,
onShippingRateSelectFail: emitterSubscribers( subscriber )
.onSelectFail,
} ),
[ subscriber ]
);
// set observers on ref so it's always current.
useEffect( () => {
@ -132,14 +137,22 @@ export const ShippingDataProvider = ( { children } ) => {
EMIT_TYPES.SHIPPING_RATES_FAIL,
currentErrorStatus
);
} else if ( ! shippingRatesLoading && shippingRates ) {
}
}, [ shippingRates, shippingRatesLoading, currentErrorStatus.hasError ] );
useEffect( () => {
if (
! shippingRatesLoading &&
shippingRates &&
! currentErrorStatus.hasError
) {
emitEvent(
currentObservers.current,
EMIT_TYPES.SHIPPING_RATES_SUCCESS,
shippingRates
);
}
}, [ shippingRates, shippingRatesLoading, currentErrorStatus ] );
}, [ shippingRates, shippingRatesLoading, currentErrorStatus.hasError ] );
// emit shipping rate selection events.
useEffect( () => {
@ -149,14 +162,22 @@ export const ShippingDataProvider = ( { children } ) => {
EMIT_TYPES.SHIPPING_RATE_SELECT_FAIL,
currentErrorStatus
);
} else if ( ! isSelectingRate && selectedRates ) {
}
}, [ selectedRates, isSelectingRate, currentErrorStatus.hasError ] );
useEffect( () => {
if (
! isSelectingRate &&
selectedRates &&
! currentErrorStatus.hasError
) {
emitEvent(
currentObservers.current,
EMIT_TYPES.SHIPPING_RATE_SELECT_SUCCESS,
selectedRates
);
}
}, [ selectedRates, isSelectingRate, currentErrorStatus ] );
}, [ selectedRates, isSelectingRate, currentErrorStatus.hasError ] );
/**
* @type {ShippingDataContext}
@ -170,12 +191,14 @@ export const ShippingDataProvider = ( { children } ) => {
shippingRatesLoading,
selectedRates,
setSelectedRates,
isSelectingRate,
shippingAddress,
setShippingAddress,
onShippingRateSuccess,
onShippingRateFail,
onShippingRateSelectSuccess,
onShippingRateSelectFail,
onShippingRateSuccess: eventSubscribers.onShippingRateSuccess,
onShippingRateFail: eventSubscribers.onShippingRateFail,
onShippingRateSelectSuccess:
eventSubscribers.onShippingRateSelectSuccess,
onShippingRateSelectFail: eventSubscribers.onShippingRateSelectFail,
needsShipping,
};
return (

View File

@ -110,6 +110,7 @@ export const usePaymentMethodInterface = () => {
shippingRatesLoading,
selectedRates,
setSelectedRates,
isSelectingRate,
shippingAddress,
setShippingAddress,
onShippingRateSuccess,
@ -158,6 +159,7 @@ export const usePaymentMethodInterface = () => {
shippingRatesLoading,
selectedRates,
setSelectedRates,
isSelectingRate,
shippingAddress,
setShippingAddress,
needsShipping,

View File

@ -10,5 +10,5 @@ export const pluckAddress = ( { country, state, city, postcode } ) => ( {
country,
state,
city,
postcode,
postcode: postcode.replace( ' ', '' ).toUpperCase(),
} );

View File

@ -6,12 +6,14 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { stripePromise } from '../stripe-utils';
import { loadStripe } from '../stripe-utils';
import { StripeCreditCard } from './payment-method';
import { PAYMENT_METHOD_NAME } from './constants';
const EditPlaceHolder = () => <div>TODO: Card edit preview soon...</div>;
const stripePromise = loadStripe();
const stripeCcPaymentMethod = {
id: PAYMENT_METHOD_NAME,
label: (
@ -19,7 +21,7 @@ const stripeCcPaymentMethod = {
{ __( 'Credit/Debit Card', 'woo-gutenberg-products-block' ) }
</strong>
),
content: <StripeCreditCard />,
content: <StripeCreditCard stripe={ stripePromise } />,
edit: <EditPlaceHolder />,
canMakePayment: stripePromise,
ariaLabel: __(

View File

@ -2,7 +2,7 @@
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME } from './constants';
import { getStripeServerData, stripePromise } from '../stripe-utils';
import { getStripeServerData } from '../stripe-utils';
import { ccSvg } from './cc';
import { useCheckoutSubscriptions } from './use-checkout-subscriptions';
import { InlineCard, CardElements } from './elements';
@ -90,10 +90,10 @@ const CreditCardComponent = ( { billing, eventRegistration, components } ) => {
export const StripeCreditCard = ( props ) => {
const { locale } = getStripeServerData().button;
const { activePaymentMethod } = props;
const { activePaymentMethod, stripe } = props;
return activePaymentMethod === PAYMENT_METHOD_NAME ? (
<Elements stripe={ stripePromise } locale={ locale }>
<Elements stripe={ stripe } locale={ locale }>
<CreditCardComponent { ...props } />
</Elements>
) : null;

View File

@ -10,9 +10,9 @@ import {
* Internal dependencies
*/
import stripeCcPaymentMethod from './credit-card';
import ApplePayPaymentMethod from './apple-pay';
import PaymentRequestPaymentMethod from './payment-request';
registerPaymentMethod( ( Config ) => new Config( stripeCcPaymentMethod ) );
registerExpressPaymentMethod(
( Config ) => new Config( ApplePayPaymentMethod )
( Config ) => new Config( PaymentRequestPaymentMethod )
);

View File

@ -0,0 +1,2 @@
export const applePayImage =
"data:image/svg+xml,%3Csvg width='264' height='48' viewBox='0 0 264 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='264' height='48' rx='3' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.114 16.6407C125.682 15.93 126.067 14.9756 125.966 14C125.135 14.0415 124.121 14.549 123.533 15.2602C123.006 15.8693 122.539 16.8641 122.661 17.7983C123.594 17.8797 124.526 17.3317 125.114 16.6407Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.955 17.982C124.601 17.9011 123.448 18.7518 122.801 18.7518C122.154 18.7518 121.163 18.0224 120.092 18.0421C118.696 18.0629 117.402 18.8524 116.694 20.1079C115.238 22.6196 116.31 26.3453 117.726 28.3909C118.414 29.4028 119.242 30.5174 120.334 30.4769C121.366 30.4365 121.77 29.8087 123.024 29.8087C124.277 29.8087 124.641 30.4769 125.733 30.4567C126.865 30.4365 127.573 29.4443 128.261 28.4313C129.049 27.2779 129.373 26.1639 129.393 26.1027C129.373 26.0825 127.209 25.2515 127.189 22.7606C127.169 20.6751 128.888 19.6834 128.969 19.6217C127.998 18.1847 126.481 18.0224 125.955 17.982Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M136.131 23.1804H138.834C140.886 23.1804 142.053 22.0752 142.053 20.1592C142.053 18.2432 140.886 17.1478 138.845 17.1478H136.131V23.1804ZM139.466 15.1582C142.411 15.1582 144.461 17.1903 144.461 20.1483C144.461 23.1172 142.369 25.1596 139.392 25.1596H136.131V30.3498H133.775V15.1582H139.466Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M152.198 26.224V25.3712L149.579 25.5397C148.106 25.6341 147.339 26.182 147.339 27.14C147.339 28.0664 148.138 28.6667 149.39 28.6667C150.988 28.6667 152.198 27.6449 152.198 26.224ZM145.046 27.2032C145.046 25.2551 146.529 24.1395 149.263 23.971L152.198 23.7922V22.9498C152.198 21.7181 151.388 21.0442 149.947 21.0442C148.758 21.0442 147.896 21.6548 147.717 22.5916H145.592C145.656 20.6232 147.507 19.1914 150.01 19.1914C152.703 19.1914 154.459 20.602 154.459 22.7917V30.351H152.282V28.5298H152.229C151.609 29.719 150.241 30.4666 148.758 30.4666C146.571 30.4666 145.046 29.1612 145.046 27.2032Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.461 34.4145V32.5934C156.608 32.6141 156.965 32.6354 157.155 32.6354C158.196 32.6354 158.785 32.1932 159.142 31.0564L159.353 30.3824L155.366 19.3281H157.827L160.604 28.298H160.657L163.434 19.3281H165.832L161.698 30.9402C160.752 33.6038 159.668 34.4778 157.376 34.4778C157.197 34.4778 156.618 34.4565 156.461 34.4145Z' fill='white'/%3E%3C/svg%3E%0A";

View File

@ -0,0 +1,7 @@
export const PAYMENT_METHOD_NAME = 'payment_request';
export const DEFAULT_STRIPE_EVENT_HANDLERS = {
shippingAddressChange: null,
shippingOptionChange: null,
source: null,
};

View File

@ -0,0 +1,37 @@
/**
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME } from './constants';
import { PaymentRequestExpress } from './payment-request-express';
import { applePayImage } from './apple-pay-preview';
import { loadStripe } from '../stripe-utils';
const ApplePayPreview = () => <img src={ applePayImage } alt="" />;
const canPayStripePromise = loadStripe();
const componentStripePromise = loadStripe();
const PaymentRequestPaymentMethod = {
id: PAYMENT_METHOD_NAME,
content: <PaymentRequestExpress stripe={ componentStripePromise } />,
edit: <ApplePayPreview />,
canMakePayment: canPayStripePromise.then( ( stripe ) => {
if ( stripe === null ) {
return false;
}
// do a test payment request to check if payment request payment can be
// done.
// @todo, initial country and currency needs to server.
const paymentRequest = stripe.paymentRequest( {
total: {
label: 'Test total',
amount: 1000,
},
country: 'CA',
currency: 'cad',
} );
return paymentRequest.canMakePayment().then( ( result ) => !! result );
} ),
};
export default PaymentRequestPaymentMethod;

View File

@ -0,0 +1,380 @@
/**
* Internal dependencies
*/
import {
DEFAULT_STRIPE_EVENT_HANDLERS,
PAYMENT_METHOD_NAME,
} from './constants';
import {
getStripeServerData,
getPaymentRequest,
updatePaymentRequest,
canDoPaymentRequest,
getTotalPaymentItem,
getBillingData,
getPaymentMethodData,
getShippingData,
normalizeShippingAddressForCheckout,
normalizeShippingOptions,
normalizeLineItems,
normalizeShippingOptionSelectionsForCheckout,
} from '../stripe-utils';
/**
* External dependencies
*/
import { useRef, useState, useEffect } from '@wordpress/element';
import {
Elements,
PaymentRequestButtonElement,
useStripe,
} from '@stripe/react-stripe-js';
import { __ } from '@wordpress/i18n';
/**
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
* @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').RegisteredPaymentMethodProps} RegisteredPaymentMethodProps
*/
/**
* @typedef {Object} WithStripe
*
* @property {Stripe} [stripe] Stripe api (might not be present)
*/
/**
* @typedef {RegisteredPaymentMethodProps & WithStripe} StripeRegisteredPaymentMethodProps
*/
/**
* PaymentRequestExpressComponent
*
* @param {StripeRegisteredPaymentMethodProps} props Incoming props
*/
const PaymentRequestExpressComponent = ( {
shippingData,
billing,
eventRegistration,
onSubmit,
activePaymentMethod,
setActivePaymentMethod,
setExpressPaymentError,
} ) => {
/**
* @type {[ StripePaymentRequest|null, function( StripePaymentRequest ):StripePaymentRequest|null]}
*/
// @ts-ignore
const [ paymentRequest, setPaymentRequest ] = useState( null );
const stripe = useStripe();
const [ canMakePayment, setCanMakePayment ] = useState( false );
const [ paymentRequestType, setPaymentRequestType ] = useState( '' );
const [ isProcessing, setIsProcessing ] = useState( false );
const [ isFinished, setIsFinished ] = useState( false );
const eventHandlers = useRef( DEFAULT_STRIPE_EVENT_HANDLERS );
const currentBilling = useRef( billing );
const currentShipping = useRef( shippingData );
const currentPaymentRequest = useRef( paymentRequest );
// for keeping track of what the active payment method was before this
// payment button was clicked.
const originalActivePaymentMethod = useRef( activePaymentMethod );
// update refs when any change.
useEffect( () => {
currentBilling.current = billing;
currentShipping.current = shippingData;
currentPaymentRequest.current = paymentRequest;
}, [ billing, shippingData, paymentRequest ] );
// set paymentRequest.
useEffect( () => {
// can't do anything if stripe isn't available yet or we have zero total.
if ( ! stripe || ! billing.cartTotal.value ) {
return;
}
// if payment request hasn't been set yet then set it.
if ( ! currentPaymentRequest.current && ! isFinished ) {
setPaymentRequest(
getPaymentRequest( {
total: billing.cartTotal,
currencyCode: billing.currency.code.toLowerCase(),
countryCode: shippingData.shippingAddress.country,
shippingRequired: shippingData.needsShipping,
cartTotalItems: billing.cartTotalItems,
stripe,
} )
);
}
// otherwise we just update it (but only if payment processing hasn't
// already started).
if ( ! isProcessing && currentPaymentRequest.current && ! isFinished ) {
updatePaymentRequest( {
// @ts-ignore
paymentRequest: currentPaymentRequest.current,
total: billing.cartTotal,
currencyCode: billing.currency.code.toLowerCase(),
cartTotalItems: billing.cartTotalItems,
} );
}
}, [
billing.cartTotal,
billing.currency.code,
shippingData.shippingAddress.country,
shippingData.needsShipping,
billing.cartTotalItems,
stripe,
isProcessing,
isFinished,
] );
// whenever paymentRequest changes, then we need to update whether
// payment can be made.
useEffect( () => {
if ( paymentRequest ) {
canDoPaymentRequest( paymentRequest ).then( ( result ) => {
if ( result.requestType ) {
setPaymentRequestType( result.requestType );
}
setCanMakePayment( result.canPay );
} );
}
}, [ paymentRequest ] );
// kick off payment processing.
const onButtonClick = () => {
originalActivePaymentMethod.current = activePaymentMethod;
setActivePaymentMethod( PAYMENT_METHOD_NAME );
setIsProcessing( true );
setIsFinished( false );
setExpressPaymentError( '' );
};
const abortPayment = ( paymentMethod, message ) => {
const response = {
fail: {
message,
billingData: getBillingData( paymentMethod ),
paymentMethodData: getPaymentMethodData(
paymentMethod,
paymentRequestType
),
},
};
paymentMethod.complete( 'fail' );
setIsProcessing( false );
setIsFinished( true );
return response;
};
const completePayment = ( paymentMethod ) => {
paymentMethod.complete( 'success' );
setIsFinished( true );
setIsProcessing( false );
};
// event callbacks.
const onShippingRatesEvent = ( forSuccess = true ) => ( shippingRates ) => {
const handlers = eventHandlers.current;
const billingData = currentBilling.current;
if ( handlers.shippingAddressChange && isProcessing ) {
handlers.shippingAddressChange.updateWith( {
status: forSuccess ? 'success' : 'fail',
shippingOptions: normalizeShippingOptions( shippingRates ),
total: getTotalPaymentItem( billingData.cartTotal ),
displayItems: normalizeLineItems( billingData.cartTotalItems ),
} );
handlers.shippingAddressChange = null;
}
};
const onShippingSelectedRate = ( forSuccess = true ) => () => {
const handlers = eventHandlers.current;
const shipping = currentShipping.current;
const billingData = currentBilling.current;
if (
handlers.shippingOptionChange &&
! shipping.isSelectingRate &&
isProcessing
) {
const updateObject = forSuccess
? {
status: 'success',
total: getTotalPaymentItem( billingData.cartTotal ),
displayItems: normalizeLineItems(
billingData.cartTotalItems
),
}
: {
status: 'fail',
};
handlers.shippingOptionChange.updateWith( updateObject );
handlers.shippingOptionChange = null;
}
};
const onPaymentProcessing = () => {
const handlers = eventHandlers.current;
if ( handlers.sourceEvent && isProcessing ) {
const response = {
billingData: getBillingData( handlers.sourceEvent ),
paymentMethodData: getPaymentMethodData(
handlers.sourceEvent,
paymentRequestType
),
shippingData: getShippingData( handlers.sourceEvent ),
};
return response;
}
return true;
};
const onCheckoutComplete = ( forSuccess = true ) => () => {
const handlers = eventHandlers.current;
if ( handlers.sourceEvent && isProcessing ) {
if ( forSuccess ) {
completePayment( handlers.sourceEvent );
} else {
abortPayment( handlers.sourceEvent );
}
handlers.sourceEvent = null;
}
return true;
};
// when canMakePayment is true, then we set listeners on payment request for
// handling updates.
useEffect( () => {
if ( paymentRequest && canMakePayment && isProcessing ) {
paymentRequest.on( 'shippingaddresschange', ( event ) => {
// @todo check if there is an address change, and if not, then
// just call updateWith and don't call setShippingAddress here
// because the state won't change upstream.
currentShipping.current.setShippingAddress(
normalizeShippingAddressForCheckout( event.shippingAddress )
);
eventHandlers.current.shippingAddressChange = event;
} );
paymentRequest.on( 'shippingoptionchange', ( event ) => {
currentShipping.current.setSelectedRates(
normalizeShippingOptionSelectionsForCheckout(
event.shippingOption
)
);
eventHandlers.current.shippingOptionChange = event;
} );
paymentRequest.on( 'source', ( paymentMethod ) => {
if (
// eslint-disable-next-line no-undef
! getStripeServerData().allowPrepaidCard &&
paymentMethod.source.card.funding
) {
setExpressPaymentError(
__(
"Sorry, we're not accepting prepaid cards at this time.",
'woocommerce-gateway-stripe'
)
);
return;
}
eventHandlers.current.sourceEvent = paymentMethod;
// kick off checkout processing step.
onSubmit();
} );
paymentRequest.on( 'cancel', () => {
setIsFinished( true );
setIsProcessing( false );
setActivePaymentMethod( originalActivePaymentMethod.current );
} );
}
}, [
paymentRequest,
canMakePayment,
isProcessing,
setActivePaymentMethod,
] );
// subscribe to events.
useEffect( () => {
if ( canMakePayment && isProcessing ) {
const subscriber = eventRegistration;
const unsubscribeShippingRateSuccess = subscriber.onShippingRateSuccess(
onShippingRatesEvent()
);
const unsubscribeShippingRateFail = subscriber.onShippingRateFail(
onShippingRatesEvent( false )
);
const unsubscribeShippingRateSelectSuccess = subscriber.onShippingRateSelectSuccess(
onShippingSelectedRate()
);
const unsubscribeShippingRateSelectFail = subscriber.onShippingRateSelectFail(
onShippingRatesEvent( false )
);
const unsubscribePaymentProcessing = subscriber.onPaymentProcessing(
onPaymentProcessing
);
const unsubscribeCheckoutCompleteSuccess = subscriber.onCheckoutCompleteSuccess(
onCheckoutComplete()
);
const unsubscribeCheckoutCompleteFail = subscriber.onCheckoutCompleteError(
onCheckoutComplete( false )
);
return () => {
unsubscribeCheckoutCompleteFail();
unsubscribeCheckoutCompleteSuccess();
unsubscribePaymentProcessing();
unsubscribeShippingRateFail();
unsubscribeShippingRateSuccess();
unsubscribeShippingRateSelectSuccess();
unsubscribeShippingRateSelectFail();
};
}
return undefined;
}, [
canMakePayment,
isProcessing,
eventRegistration.onShippingRateSuccess,
eventRegistration.onShippingRateFail,
eventRegistration.onShippingRateSelectSuccess,
eventRegistration.onShippingRateSelectFail,
eventRegistration.onPaymentProcessing,
eventRegistration.onCheckoutCompleteSuccess,
eventRegistration.onCheckoutCompleteError,
] );
// locale is not a valid value for the paymentRequestButton style.
const { theme } = getStripeServerData().button;
const paymentRequestButtonStyle = {
paymentRequestButton: {
type: 'default',
theme,
height: '48px',
},
};
return canMakePayment && paymentRequest ? (
<PaymentRequestButtonElement
onClick={ onButtonClick }
options={ {
style: paymentRequestButtonStyle,
paymentRequest,
} }
/>
) : null;
};
/**
* PaymentRequestExpress with stripe provider
*
* @param {StripeRegisteredPaymentMethodProps} props
*/
export const PaymentRequestExpress = ( props ) => {
const { locale } = getStripeServerData().button;
const { stripe } = props;
return (
<Elements stripe={ stripe } locale={ locale }>
<PaymentRequestExpressComponent { ...props } />
</Elements>
);
};

View File

@ -1,2 +1,3 @@
export * from './normalize';
export * from './utils';
export { default as stripePromise } from './load-stripe';
export * from './load-stripe';

View File

@ -8,7 +8,8 @@ import { loadStripe } from '@stripe/stripe-js';
*/
import { getApiKey } from './utils';
const stripePromise = new Promise( ( resolve ) => {
const stripePromise = () =>
new Promise( ( resolve ) => {
let stripe = null;
try {
stripe = loadStripe( getApiKey() );
@ -17,6 +18,6 @@ const stripePromise = new Promise( ( resolve ) => {
//console.error( error.message );
}
resolve( stripe );
} );
} );
export default stripePromise;
export { stripePromise as loadStripe };

View File

@ -1,12 +1,12 @@
/**
* @typedef {import('../stripe-utils/type-defs').StripePaymentItem} StripePaymentItem
* @typedef {import('../stripe-utils/type-defs').StripeShippingOption} StripeShippingOption
* @typedef {import('../stripe-utils/type-defs').StripeShippingAddress} StripeShippingAddress
* @typedef {import('../stripe-utils/type-defs').StripePaymentResponse} StripePaymentResponse
* @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem
* @typedef {import('./type-defs').StripeShippingOption} StripeShippingOption
* @typedef {import('./type-defs').StripeShippingAddress} StripeShippingAddress
* @typedef {import('./type-defs').StripePaymentResponse} StripePaymentResponse
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').PreparedCartTotalItem} CartTotalItem
* @typedef {import('@woocommerce/type-defs/cart').CartShippingOption} CartShippingOption
* @typedef {import('@woocommerce/type-defs/cart').CartShippingAddress} CartShippingAddress
* @typedef {import('@woocommerce/type-defs/cart').CartBillingAddress} CartBillingAddress
* @typedef {import('@woocommerce/type-defs/billing').BillingData} CartBillingAddress
*/
/**
@ -38,12 +38,15 @@ const normalizeLineItems = ( cartTotalItems, pending = false ) => {
* @return {StripeShippingOption[]} An array of Stripe shipping option items.
*/
const normalizeShippingOptions = ( shippingOptions ) => {
return shippingOptions.map( ( shippingOption ) => {
// @todo, note this currently does not handle multiple packages and this
// will need to be dealt with in a followup.
const rates = shippingOptions[ 0 ].shipping_rates;
return rates.map( ( rate ) => {
return {
id: shippingOption.rate_id,
label: shippingOption.name,
detail: shippingOption.description,
amount: parseInt( shippingOption.price, 10 ),
id: rate.rate_id,
label: rate.name,
detail: rate.description,
amount: parseInt( rate.price, 10 ),
};
} );
};
@ -58,7 +61,7 @@ const normalizeShippingOptions = ( shippingOptions ) => {
* the cart.
*/
const normalizeShippingAddressForCheckout = ( shippingAddress ) => {
return {
const address = {
first_name: shippingAddress.recipient
.split( ' ' )
.slice( 0, 1 )
@ -79,8 +82,9 @@ const normalizeShippingAddressForCheckout = ( shippingAddress ) => {
city: shippingAddress.city,
state: shippingAddress.region,
country: shippingAddress.country,
postcode: shippingAddress.postalCode,
postcode: shippingAddress.postalCode.replace( ' ', '' ),
};
return address;
};
/**
@ -93,7 +97,7 @@ const normalizeShippingAddressForCheckout = ( shippingAddress ) => {
* @return {string[]} An array of ids (in this case will just be one)
*/
const normalizeShippingOptionSelectionsForCheckout = ( shippingOption ) => {
return [ shippingOption.id ];
return shippingOption.id;
};
/**
@ -159,6 +163,16 @@ const getPaymentMethodData = ( paymentResponse, paymentRequestType ) => {
};
};
const getShippingData = ( paymentResponse ) => {
return paymentResponse.shippingAddress
? {
address: normalizeShippingAddressForCheckout(
paymentResponse.shippingAddress
),
}
: null;
};
export {
normalizeLineItems,
normalizeShippingOptions,
@ -166,4 +180,5 @@ export {
normalizeShippingOptionSelectionsForCheckout,
getBillingData,
getPaymentMethodData,
getShippingData,
};

View File

@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { normalizeLineItems } from '../apple-pay/normalize';
import { normalizeLineItems } from './normalize';
import { errorTypes, errorCodes } from './constants';
/**
@ -60,7 +60,9 @@ const getApiKey = () => {
*/
const getTotalPaymentItem = ( total ) => {
return {
label: getStripeServerData().stripeTotalLabel,
label:
getStripeServerData().stripeTotalLabel ||
__( 'Total', 'woo-gutenberg-products-block' ),
amount: total.value,
};
};
@ -139,16 +141,19 @@ const updatePaymentRequest = ( {
*
* @param {StripePaymentRequest} paymentRequest A Stripe PaymentRequest instance.
*
* @return {Promise<boolean>} True means apple pay can be done.
* @return {Promise<Object>} True means apple pay can be done.
*/
const canDoApplePay = ( paymentRequest ) => {
const canDoPaymentRequest = ( paymentRequest ) => {
return new Promise( ( resolve ) => {
paymentRequest.canMakePayment().then( ( result ) => {
if ( result && result.applePay ) {
resolve( true );
if ( result ) {
const paymentRequestType = result.applePay
? 'apple_pay'
: 'payment_request_api';
resolve( { canPay: true, requestType: paymentRequestType } );
return;
}
resolve( false );
resolve( { canPay: false } );
} );
} );
};
@ -253,6 +258,6 @@ export {
getTotalPaymentItem,
getPaymentRequest,
updatePaymentRequest,
canDoApplePay,
canDoPaymentRequest,
getErrorMessageForTypeAndCode,
};

View File

@ -39,6 +39,8 @@
* that are selected.
* @property {Function} setSelectedRates A function for setting
* the selected rates.
* @property {boolean} isSelectingRate True when rate is being
* selected.
* @property {CartShippingAddress} shippingAddress The current set
* address for shipping.
* @property {function()} setShippingAddress A function for setting
@ -125,6 +127,13 @@
* with the payment method.
*/
/**
* @typedef {Object} ShippingDataResponse
*
* @property {CartShippingAddress} address The address selected for
* shipping.
*/
/**
* A Saved Customer Payment methods object
*
@ -142,7 +151,7 @@
* @property {function()} completed
* @property {function(string)} error
* @property {function(string, Object, Object=)} failed
* @property {function(Object=,Object=)} success
* @property {function(Object=,Object=,Object=)} success
*/
/**

View File

@ -96,6 +96,8 @@
* @property {Function} setSelectedRates A function for setting
* selected rates
* (recieves id)
* @property {boolean} isSelectingRate True when rates are
* being selected.
* @property {CartShippingAddress} shippingAddress The current set
* shipping address.
* @property {Function} setShippingAddress A function for setting

View File

@ -14,6 +14,8 @@ use Exception;
use WC_Stripe_Payment_Request;
use WC_Stripe_Helper;
use Automattic\WooCommerce\Blocks\Assets\Api;
use Automattic\WooCommerce\Blocks\Payments\PaymentContext;
use Automattic\WooCommerce\Blocks\Payments\PaymentResult;
/**
* Stripe payment method integration
@ -49,6 +51,7 @@ final class Stripe extends AbstractPaymentMethodType {
*/
public function __construct( Api $asset_api ) {
$this->asset_api = $asset_api;
add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_payment_request_order_meta' ], 8, 2 );
}
/**
@ -177,4 +180,20 @@ final class Stripe extends AbstractPaymentMethodType {
private function get_button_locale() {
return apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) );
}
/**
* Add payment request data to the order meta as hooked on the
* woocommerce_rest_checkout_process_payment_with_context action.
*
* @param PaymentContext $context Holds context for the payment.
* @param PaymentResult $result Result object for the payment.
*/
public function add_payment_request_order_meta( PaymentContext $context, PaymentResult $result ) {
// phpcs:ignore WordPress.Security.NonceVerification
$post_data = $_POST;
$_POST = $context->payment_data;
WC_Stripe_Payment_Request::add_order_meta( $context->order->id, $context->payment_data );
$_POST = $post_data;
}
}

View File

@ -357,7 +357,16 @@ class Checkout extends AbstractRoute {
* @return string
*/
protected function get_request_payment_method_id( \WP_REST_Request $request ) {
$payment_method = wc_clean( wp_unslash( $request['payment_method'] ) );
$payment_data = $this->get_request_payment_data( $request );
// allows for payment methods to override the payment method
// automatically set by checkout if necessary (typically when a gateway)
// may have multiple payment methods for the same gateway.
$payment_method = isset( $payment_data['payment_method'] )
? $payment_data['payment_method']
: null;
$payment_method = null === $payment_method
? wc_clean( wp_unslash( $request['payment_method'] ) )
: $payment_method;
$valid_methods = WC()->payment_gateways->get_payment_gateway_ids();
if ( empty( $payment_method ) ) {
@ -414,8 +423,10 @@ class Checkout extends AbstractRoute {
* @return array
*/
protected function get_request_payment_data( \WP_REST_Request $request ) {
$payment_data = [];
static $payment_data = [];
if ( ! empty( $payment_data ) ) {
return $payment_data;
}
if ( ! empty( $request['payment_data'] ) ) {
foreach ( $request['payment_data'] as $data ) {
$payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );