/**
* 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;
};