woocommerce/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/payment-method.js

414 lines
11 KiB
JavaScript
Raw Normal View History

Implement Stripe CC and Stripe ApplePay payment methods (https://github.com/woocommerce/woocommerce-blocks/pull/1983) * Server side changes for payment method integrations Including adding a stripe class temporarily * update needed npm packages (and add some types) * updates to contexts * remove stepContent from payment config for payment methods * update payment method interface and typedefs Exposing a components property to pass along components that payment methods can use (so we keep styles consistent for them) * add apple pay and stripe cc integration and remove paypal * remove save payment checkbox from checkout block It is handled by payment methods. * Include an id prop for tabs * fix activePaymentMethod pass through on rendered payment method element also adds an id for the rendered tab * add styles for payment method fields If payment methods use these classes for their fields then the styles will get applied. It _could_ allow for consistent styling, we may have to provide design documentation for this? These are styles in cases where payment methods have to use elements provided by the gateway (eg. Stripe elements). In future iterations we could look at providing components to payment methods to use (if they aren’t restricted by the gateway). * fix rebase conflict * do a test payment request for applePay to determine if the current browser supports it * don’t console.error for stripe loading. * Fix placeholder errors in the editor * improve styling and add missing validation for inline card element * update pacakge-lock * rename payment-methods-demo folder to payment-methods-extension * expose checkbox control on payment method interface * export payment-methods-extension to it’s own asset build This allows us to more accurately demonstrate how payment extensions would hook in to the blocks. * don’t enqueue a style that doesn’t exist * add full stop to comments and remove obsolete comment blcok * fix spacing * switch `activeContent` to `content` for payment method registration config
2020-03-30 12:07:49 +00:00
/**
* 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,
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 (
<>
<div className="wc-block-gateway-container wc-inline-card-element">
<CardElement
id="wc-stripe-inline-card-element"
className={ baseTextInputStyles }
options={ options }
onBlur={ () => onActive( isEmpty ) }
onFocus={ () => onActive( isEmpty ) }
onChange={ errorCallback }
/>
<label htmlFor="wc-stripe-inline-card-element">
{ __(
'Credit Card Information',
'woo-gutenberg-products-block'
) }
</label>
</div>
<ValidationInputError errorMessage={ error } />
</>
);
};
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 (
<div className="wc-block-card-elements">
<div className="wc-block-gateway-container wc-card-number-element">
<CardNumberElement
onChange={ errorCallback( cardNumSetError ) }
options={ cardNumOptions }
className={ baseTextInputStyles }
id="wc-stripe-card-number-element"
onFocus={ () => cardNumOnActive( isEmpty ) }
onBlur={ () => cardNumOnActive( isEmpty ) }
/>
<label htmlFor="wc-stripe-card-number-element">
{ __( 'Card Number', 'woo-gutenberg-product-blocks' ) }
</label>
<ValidationInputError errorMessage={ cardNumError } />
</div>
<div className="wc-block-gateway-container wc-card-expiry-element">
<CardExpiryElement
onChange={ errorCallback( cardExpirySetError ) }
options={ cardExpiryOptions }
className={ baseTextInputStyles }
onFocus={ cardExpiryOnActive }
onBlur={ cardExpiryOnActive }
id="wc-stripe-card-expiry-element"
/>
<label htmlFor="wc-stripe-card-expiry-element">
{ __( 'Expiry Date', 'woo-gutenberg-product-blocks' ) }
</label>
<ValidationInputError errorMessage={ cardExpiryError } />
</div>
<div className="wc-block-gateway-container wc-card-cvc-element">
<CardCvcElement
onChange={ errorCallback( cardCvcSetError ) }
options={ cardCvcOptions }
className={ baseTextInputStyles }
onFocus={ cardCvcOnActive }
onBlur={ cardCvcOnActive }
id="wc-stripe-card-code-element"
/>
<label htmlFor="wc-stripe-card-code-element">
{ __( 'CVV/CVC', 'woo-gutenberg-product-blocks' ) }
</label>
<ValidationInputError errorMessage={ cardCvcError } />
</div>
</div>
);
};
const useStripeCheckoutSubscriptions = (
eventRegistration,
paymentStatus,
billing,
sourceId,
setSourceId,
shouldSavePayment,
stripe
) => {
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 ( stripeBilling ) => {
return await stripe.createSource( stripeBilling );
};
const onSubmit = async () => {
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 stripeBilling = {
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 ) {
stripeBilling.phone = billingData.phone;
}
if ( billingData.email ) {
stripeBilling.email = billingData.email;
}
if ( billingData.first_name || billingData.last_name ) {
stripeBilling.name = `${ billingData.first_name } ${ billingData.last_name }`;
}
const response = await createSource( stripeBilling );
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;
};
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 onStripeError = useStripeCheckoutSubscriptions(
eventRegistration,
paymentStatus,
billing,
sourceId,
shouldSavePayment,
stripe
);
const onChange = ( paymentEvent ) => {
if ( paymentEvent.error ) {
onStripeError( paymentEvent );
}
setSourceId( 0 );
};
const renderedCardElement = getStripeServerData().inline_cc_form ? (
<InlineCard
onChange={ onChange }
inputErrorComponent={ ValidationInputError }
/>
) : (
<CardElements
onChange={ onChange }
inputErrorComponent={ ValidationInputError }
/>
);
// 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 }
<CheckboxControl
className="wc-block-checkout__save-card-info"
label={ __(
'Save payment information to my account for future purchases.',
'woo-gutenberg-products-block'
) }
checked={ shouldSavePayment }
onChange={ () => setShouldSavePayment( ! shouldSavePayment ) }
/>
<img
src={ ccSvg }
alt={ __(
'Accepted cards for processing',
'woo-gutenberg-products-block'
) }
className="wc-blocks-credit-card-images"
/>
</>
);
};
export const StripeCreditCard = ( props ) => {
const { locale } = getStripeServerData().button;
const { activePaymentMethod } = props;
return activePaymentMethod === PAYMENT_METHOD_NAME ? (
<Elements stripe={ stripePromise } locale={ locale }>
<CreditCardComponent { ...props } />
</Elements>
) : null;
};