/** * Internal dependencies */ import { PAYMENT_METHOD_NAME } from './constants'; import { getStripeServerData, stripePromise, getErrorMessageForTypeAndCode, } from '../../stripe-utils'; import { ccSvg } from './cc'; /** * External dependencies */ import { Elements, CardElement, CardNumberElement, CardExpiryElement, CardCvcElement, useElements, useStripe, } from '@stripe/react-stripe-js'; import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; 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 */ const elementOptions = { style: { base: { iconColor: '#666EE8', color: '#31325F', fontSize: '15px', '::placeholder': { color: '#fff', }, }, }, classes: { focus: 'focused', empty: 'empty', invalid: 'has-error', }, }; const useElementOptions = ( overloadedOptions ) => { const [ isActive, setIsActive ] = useState( false ); const [ options, setOptions ] = useState( { ...elementOptions, ...overloadedOptions, } ); const [ error, setError ] = useState( '' ); useEffect( () => { const color = isActive ? '#CFD7E0' : '#fff'; setOptions( ( prevOptions ) => { const showIcon = typeof prevOptions.showIcon !== 'undefined' ? { showIcon: isActive } : {}; return { ...options, style: { ...options.style, base: { ...options.style.base, '::placeholder': { color, }, }, }, ...showIcon, }; } ); }, [ isActive ] ); const onActive = useCallback( ( isEmpty ) => { if ( ! isEmpty ) { setIsActive( true ); } else { setIsActive( ( prevActive ) => ! prevActive ); } }, [ setIsActive ] ); return { options, onActive, error, setError }; }; const baseTextInputStyles = 'wc-block-gateway-input'; const InlineCard = ( { inputErrorComponent: ValidationInputError, onChange, } ) => { const [ isEmpty, setIsEmpty ] = useState( true ); const { options, onActive, error, setError } = useElementOptions( { hidePostalCode: true, } ); const errorCallback = ( event ) => { if ( event.error ) { setError( event.error.message ); } else { setError( '' ); } setIsEmpty( event.empty ); onChange( event ); }; return ( <>
onActive( isEmpty ) } onFocus={ () => onActive( isEmpty ) } onChange={ errorCallback } />
); }; const CardElements = ( { onChange, inputErrorComponent: ValidationInputError, } ) => { const [ isEmpty, setIsEmpty ] = useState( true ); const { options: cardNumOptions, onActive: cardNumOnActive, error: cardNumError, setError: cardNumSetError, } = useElementOptions( { showIcon: false } ); const { options: cardExpiryOptions, onActive: cardExpiryOnActive, error: cardExpiryError, setError: cardExpirySetError, } = useElementOptions(); const { options: cardCvcOptions, onActive: cardCvcOnActive, error: cardCvcError, setError: cardCvcSetError, } = useElementOptions(); const errorCallback = ( errorSetter ) => ( event ) => { if ( event.error ) { errorSetter( event.error.message ); } else { errorSetter( '' ); } setIsEmpty( event.empty ); onChange( event ); }; return (
cardNumOnActive( isEmpty ) } onBlur={ () => cardNumOnActive( isEmpty ) } />
); }; const useStripeCheckoutSubscriptions = ( eventRegistration, paymentStatus, billing, sourceId, setSourceId, shouldSavePayment, stripe, elements ) => { const onStripeError = useRef( ( event ) => { return event; } ); // hook into and register callbacks for events. useEffect( () => { onStripeError.current = ( event ) => { const type = event.error.type; const code = event.error.code || ''; let message = getErrorMessageForTypeAndCode( type, code ); message = message || event.error.message; paymentStatus.setPaymentStatus().error( message ); // @todo we'll want to do inline invalidation errors for any element // inputs return {}; }; const createSource = async ( ownerInfo ) => { const elementToGet = getStripeServerData().inline_cc_form ? CardElement : CardNumberElement; return await stripe.createSource( elements.getElement( elementToGet ), { type: 'card', owner: ownerInfo, } ); }; const onSubmit = async () => { try { paymentStatus.setPaymentStatus().processing(); const { billingData } = billing; // use token if it's set. if ( sourceId !== 0 ) { paymentStatus.setPaymentStatus().success( billingData, { paymentMethod: PAYMENT_METHOD_NAME, paymentRequestType: 'cc', sourceId, shouldSavePayment, } ); return true; } const ownerInfo = { address: { line1: billingData.address_1, line2: billingData.address_2, city: billingData.city, state: billingData.state, postal_code: billingData.postcode, country: billingData.country, }, }; if ( billingData.phone ) { ownerInfo.phone = billingData.phone; } if ( billingData.email ) { ownerInfo.email = billingData.email; } if ( billingData.first_name || billingData.last_name ) { ownerInfo.name = `${ billingData.first_name } ${ billingData.last_name }`; } const response = await createSource( ownerInfo ); if ( response.error ) { return onStripeError.current( response ); } paymentStatus.setPaymentStatus().success( billingData, { sourceId: response.source.id, paymentMethod: PAYMENT_METHOD_NAME, paymentRequestType: 'cc', shouldSavePayment, } ); setSourceId( response.source.id ); return true; } catch ( e ) { return e; } }; const onComplete = () => { paymentStatus.setPaymentStatus().completed(); }; const onError = () => { paymentStatus.setPaymentStatus().started(); }; // @todo Right now all the registered callbacks will go stale, so we need // either implement useRef or make sure functions being used from these // callbacks don't change so we can add them as dependencies. // validation and stripe processing (get source etc). const unsubscribeProcessing = eventRegistration.onCheckoutProcessing( onSubmit ); const unsubscribeCheckoutComplete = eventRegistration.onCheckoutCompleteSuccess( onComplete ); const unsubscribeCheckoutCompleteError = eventRegistration.onCheckoutCompleteError( onError ); return () => { unsubscribeProcessing(); unsubscribeCheckoutComplete(); unsubscribeCheckoutCompleteError(); }; }, [ eventRegistration.onCheckoutProcessing, eventRegistration.onCheckoutCompleteSuccess, eventRegistration.onCheckoutCompleteError, paymentStatus.setPaymentStatus, stripe, sourceId, billing.billingData, setSourceId, shouldSavePayment, ] ); return onStripeError.current; }; // @todo add intents? /** * Stripe Credit Card component * * @param {RegisteredPaymentMethodProps} props Incoming props */ const CreditCardComponent = ( { paymentStatus, billing, eventRegistration, components, } ) => { const { ValidationInputError, CheckboxControl } = components; const [ sourceId, setSourceId ] = useState( 0 ); const stripe = useStripe(); const [ shouldSavePayment, setShouldSavePayment ] = useState( true ); const elements = useElements(); const onStripeError = useStripeCheckoutSubscriptions( eventRegistration, paymentStatus, billing, sourceId, setSourceId, shouldSavePayment, stripe, elements ); const onChange = ( paymentEvent ) => { if ( paymentEvent.error ) { onStripeError( paymentEvent ); } setSourceId( 0 ); }; const renderedCardElement = getStripeServerData().inline_cc_form ? ( ) : ( ); // we need to pass along source for customer from server if it's available // and pre-populate for checkout (so it'd need to be returned with the // order endpoint and available on billing details?) // so this will need to be an option for selecting if there's a saved // source attached with the order (see woocommerce/templates/myaccount/payment-methods.php) // so that data will need to be included with the order endpoint (billing data) to choose from. //@todo do need to add save payment method checkbox here. return ( <> { renderedCardElement } setShouldSavePayment( ! shouldSavePayment ) } /> { ); }; export const StripeCreditCard = ( props ) => { const { locale } = getStripeServerData().button; const { activePaymentMethod } = props; return activePaymentMethod === PAYMENT_METHOD_NAME ? ( ) : null; };