364 lines
9.9 KiB
JavaScript
364 lines
9.9 KiB
JavaScript
/**
|
|
* Internal dependencies
|
|
*/
|
|
import {
|
|
DEFAULT_STRIPE_EVENT_HANDLERS,
|
|
PAYMENT_METHOD_NAME,
|
|
} from './constants';
|
|
import {
|
|
getBillingData,
|
|
getPaymentMethodData,
|
|
normalizeShippingAddressForCheckout,
|
|
normalizeShippingOptions,
|
|
normalizeLineItems,
|
|
} from './normalize';
|
|
import {
|
|
getStripeServerData,
|
|
getPaymentRequest,
|
|
updatePaymentRequest,
|
|
canDoApplePay,
|
|
getTotalPaymentItem,
|
|
stripePromise,
|
|
} 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
|
|
*/
|
|
|
|
// @todo, note it should be trivial to implement cross browser payment api
|
|
// handling here:
|
|
// - ApplePay in Safari
|
|
// - ChromePay in Chrome
|
|
// - not supported for other browsers yet (but if assuming stripe implements the
|
|
// official PaymentRequest API in their library this should enable support!).
|
|
|
|
/**
|
|
* AppleExpressComponent
|
|
*
|
|
* @param {StripeRegisteredPaymentMethodProps} props Incoming props
|
|
*/
|
|
const ApplePayExpressComponent = ( {
|
|
paymentStatus,
|
|
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 [ applePayProcessing, setApplePayProcessing ] = useState( false );
|
|
const eventHandlers = useRef( DEFAULT_STRIPE_EVENT_HANDLERS );
|
|
const currentBilling = useRef( billing );
|
|
const currentShipping = useRef( shippingData );
|
|
const currentPaymentRequest = useRef( paymentRequest );
|
|
const currentPaymentStatus = useRef( paymentStatus );
|
|
const currentEventRegistration = useRef( eventRegistration );
|
|
const isActive = useRef( activePaymentMethod === PAYMENT_METHOD_NAME );
|
|
|
|
// update refs when any change
|
|
useEffect( () => {
|
|
currentBilling.current = billing;
|
|
currentShipping.current = shippingData;
|
|
currentPaymentRequest.current = paymentRequest;
|
|
currentPaymentStatus.current = paymentStatus;
|
|
currentEventRegistration.current = eventRegistration;
|
|
isActive.current = activePaymentMethod === PAYMENT_METHOD_NAME;
|
|
}, [ billing, shippingData, paymentRequest, paymentStatus, isActive ] );
|
|
|
|
//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 ) {
|
|
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 (
|
|
! paymentStatus.isPristine &&
|
|
! applePayProcessing &&
|
|
currentPaymentRequest.current
|
|
) {
|
|
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,
|
|
paymentStatus.isPristine,
|
|
stripe,
|
|
applePayProcessing,
|
|
] );
|
|
|
|
// whenever paymentRequest changes, then we need to update whether
|
|
// payment can be made.
|
|
useEffect( () => {
|
|
if ( paymentRequest ) {
|
|
canDoApplePay( paymentRequest ).then( ( result ) => {
|
|
setCanMakePayment( result );
|
|
} );
|
|
}
|
|
}, [ paymentRequest ] );
|
|
|
|
// kick off payment processing.
|
|
const onButtonClick = () => {
|
|
setActivePaymentMethod( PAYMENT_METHOD_NAME );
|
|
setExpressPaymentError( '' );
|
|
setApplePayProcessing( true );
|
|
};
|
|
|
|
const abortPayment = ( paymentMethod, message ) => {
|
|
paymentMethod.complete( 'fail' );
|
|
setApplePayProcessing( false );
|
|
return {
|
|
fail: {
|
|
message,
|
|
billingData: getBillingData( paymentMethod ),
|
|
paymentMethodData: getPaymentMethodData(
|
|
paymentMethod,
|
|
PAYMENT_METHOD_NAME
|
|
),
|
|
},
|
|
};
|
|
};
|
|
|
|
const completePayment = ( paymentMethod ) => {
|
|
paymentMethod.complete( 'success' );
|
|
setApplePayProcessing( false );
|
|
};
|
|
|
|
// event callbacks.
|
|
const onShippingRatesEvent = ( forSuccess = true ) => () => {
|
|
const handlers = eventHandlers.current;
|
|
if (
|
|
typeof handlers.shippingAddressChange === 'function' &&
|
|
applePayProcessing
|
|
) {
|
|
handlers.shippingAddressChange.updateWith( {
|
|
status: forSuccess ? 'success' : 'fail',
|
|
shippingOptions: normalizeShippingOptions(
|
|
currentShipping.current.shippingRates
|
|
),
|
|
total: getTotalPaymentItem( currentBilling.current.cartTotal ),
|
|
displayItems: normalizeLineItems(
|
|
currentBilling.current.cartTotalItems
|
|
),
|
|
} );
|
|
}
|
|
};
|
|
|
|
const onShippingSelectedRate = ( forSuccess = true ) => () => {
|
|
const handlers = eventHandlers.current;
|
|
if (
|
|
typeof handlers.shippingOptionsChange === 'function' &&
|
|
applePayProcessing
|
|
) {
|
|
const updateObject = forSuccess
|
|
? {
|
|
status: 'success',
|
|
total: getTotalPaymentItem(
|
|
currentBilling.current.cartTotal
|
|
),
|
|
displayItems: normalizeLineItems(
|
|
currentBilling.current.cartTotalItems
|
|
),
|
|
}
|
|
: {
|
|
status: 'fail',
|
|
};
|
|
handlers.shippingOptionsChange.updateWith( updateObject );
|
|
}
|
|
};
|
|
|
|
const onPaymentProcessing = () => {
|
|
const handlers = eventHandlers.current;
|
|
if (
|
|
typeof handlers.sourceEvent === 'function' &&
|
|
applePayProcessing
|
|
) {
|
|
return {
|
|
billingData: getBillingData( handlers.sourceEvent ),
|
|
paymentMethodData: getPaymentMethodData(
|
|
handlers.sourceEvent,
|
|
PAYMENT_METHOD_NAME
|
|
),
|
|
};
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const onCheckoutComplete = ( forSuccess = true ) => () => {
|
|
const handlers = eventHandlers.current;
|
|
if (
|
|
typeof handlers.sourceEvent === 'function' &&
|
|
applePayProcessing
|
|
) {
|
|
if ( forSuccess ) {
|
|
completePayment( handlers.sourceEvent );
|
|
} else {
|
|
abortPayment( handlers.sourceEvent );
|
|
}
|
|
}
|
|
};
|
|
|
|
// when canMakePayment is true, then we set listeners on payment request for
|
|
// handling updates.
|
|
useEffect( () => {
|
|
if ( paymentRequest && canMakePayment && isActive.current ) {
|
|
paymentRequest.on( 'shippingaddresschange', ( event ) => {
|
|
currentShipping.current.setShippingAddress(
|
|
normalizeShippingAddressForCheckout( event.shippingAddress )
|
|
);
|
|
eventHandlers.current.shippingAddressChange = event;
|
|
} );
|
|
paymentRequest.on( 'shippingoptionchange', ( event ) => {
|
|
currentShipping.current.setSelectedRates(
|
|
normalizeShippingOptions( event.shipping )
|
|
);
|
|
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;
|
|
onSubmit();
|
|
} );
|
|
}
|
|
}, [ paymentRequest, canMakePayment ] );
|
|
|
|
// subscribe to events.
|
|
useEffect( () => {
|
|
if ( canMakePayment && isActive.current ) {
|
|
const subscriber = currentEventRegistration.current;
|
|
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();
|
|
};
|
|
}
|
|
}, [ canMakePayment ] );
|
|
|
|
// 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;
|
|
};
|
|
|
|
/**
|
|
* ApplePayExpress with stripe provider
|
|
*
|
|
* @param {RegisteredPaymentMethodProps|{}} props
|
|
*/
|
|
export const ApplePayExpress = ( props ) => {
|
|
const { locale } = getStripeServerData().button;
|
|
return (
|
|
<Elements stripe={ stripePromise } locale={ locale }>
|
|
<ApplePayExpressComponent { ...props } />
|
|
</Elements>
|
|
);
|
|
};
|