initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1 @@
export const PAYMENT_METHOD_NAME = 'stripe';

View File

@ -0,0 +1,161 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
CardElement,
CardNumberElement,
CardExpiryElement,
CardCvcElement,
} from '@stripe/react-stripe-js';
/**
* Internal dependencies
*/
import { useElementOptions } from './use-element-options';
/** @typedef {import('react')} React */
const baseTextInputStyles = 'wc-block-gateway-input';
/**
* InlineCard component
*
* @param {Object} props Incoming props for the component.
* @param {React.ReactElement} props.inputErrorComponent
* @param {function(any):any} props.onChange
*/
export 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',
'woocommerce'
) }
</label>
</div>
<ValidationInputError errorMessage={ error } />
</>
);
};
/**
* CardElements component.
*
* @param {Object} props
* @param {function(any):any} props.onChange
* @param {React.ReactElement} props.inputErrorComponent
*/
export const CardElements = ( {
onChange,
inputErrorComponent: ValidationInputError,
} ) => {
const [ isEmpty, setIsEmpty ] = useState( {
cardNumber: true,
cardExpiry: true,
cardCvc: 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, elementId ) => ( event ) => {
if ( event.error ) {
errorSetter( event.error.message );
} else {
errorSetter( '' );
}
setIsEmpty( { ...isEmpty, [ elementId ]: 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, 'cardNumber' ) }
options={ cardNumOptions }
className={ baseTextInputStyles }
id="wc-stripe-card-number-element"
onFocus={ () => cardNumOnActive( isEmpty.cardNumber ) }
onBlur={ () => cardNumOnActive( isEmpty.cardNumber ) }
/>
<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,
'cardExpiry'
) }
options={ cardExpiryOptions }
className={ baseTextInputStyles }
onFocus={ () => cardExpiryOnActive( isEmpty.cardExpiry ) }
onBlur={ () => cardExpiryOnActive( isEmpty.cardExpiry ) }
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, 'cardCvc' ) }
options={ cardCvcOptions }
className={ baseTextInputStyles }
onFocus={ () => cardCvcOnActive( isEmpty.cardCvc ) }
onBlur={ () => cardCvcOnActive( isEmpty.cardCvc ) }
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>
);
};

View File

@ -0,0 +1,65 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { getStripeServerData, loadStripe } from '../stripe-utils';
import { StripeCreditCard, getStripeCreditCardIcons } from './payment-method';
import { PAYMENT_METHOD_NAME } from './constants';
const stripePromise = loadStripe();
const StripeComponent = ( props ) => {
const [ errorMessage, setErrorMessage ] = useState( '' );
useEffect( () => {
Promise.resolve( stripePromise ).then( ( { error } ) => {
if ( error ) {
setErrorMessage( error.message );
}
} );
}, [ setErrorMessage ] );
useEffect( () => {
if ( errorMessage ) {
throw new Error( errorMessage );
}
}, [ errorMessage ] );
return <StripeCreditCard stripe={ stripePromise } { ...props } />;
};
const StripeLabel = ( props ) => {
const { PaymentMethodLabel } = props.components;
const labelText = getStripeServerData().title
? getStripeServerData().title
: __( 'Credit / Debit Card', 'woocommerce' );
return <PaymentMethodLabel text={ labelText } />;
};
const cardIcons = getStripeCreditCardIcons();
const stripeCcPaymentMethod = {
name: PAYMENT_METHOD_NAME,
label: <StripeLabel />,
content: <StripeComponent />,
edit: <StripeComponent />,
icons: cardIcons,
canMakePayment: () => stripePromise,
ariaLabel: __(
'Stripe Credit Card payment method',
'woocommerce'
),
supports: {
showSavedCards: getStripeServerData().showSavedCards,
showSaveOption: getStripeServerData().showSaveOption,
features: getStripeServerData()?.supports ?? [],
},
};
export default stripeCcPaymentMethod;

View File

@ -0,0 +1,92 @@
/**
* External dependencies
*/
import { Elements, useStripe } from '@stripe/react-stripe-js';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { getStripeServerData } from '../stripe-utils';
import { useCheckoutSubscriptions } from './use-checkout-subscriptions';
import { InlineCard, CardElements } from './elements';
/**
* @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
*/
export const getStripeCreditCardIcons = () => {
return Object.entries( getStripeServerData().icons ).map(
( [ id, { src, alt } ] ) => {
return {
id,
src,
alt,
};
}
);
};
/**
* Stripe Credit Card component
*
* @param {RegisteredPaymentMethodProps} props Incoming props
*/
const CreditCardComponent = ( {
billing,
eventRegistration,
emitResponse,
components,
} ) => {
const { ValidationInputError, PaymentMethodIcons } = components;
const [ sourceId, setSourceId ] = useState( '' );
const stripe = useStripe();
const onStripeError = useCheckoutSubscriptions(
eventRegistration,
billing,
sourceId,
setSourceId,
emitResponse,
stripe
);
const onChange = ( paymentEvent ) => {
if ( paymentEvent.error ) {
onStripeError( paymentEvent );
}
setSourceId( '0' );
};
const cardIcons = getStripeCreditCardIcons();
const renderedCardElement = getStripeServerData().inline_cc_form ? (
<InlineCard
onChange={ onChange }
inputErrorComponent={ ValidationInputError }
/>
) : (
<CardElements
onChange={ onChange }
inputErrorComponent={ ValidationInputError }
/>
);
return (
<>
{ renderedCardElement }
{ PaymentMethodIcons && cardIcons.length && (
<PaymentMethodIcons icons={ cardIcons } align="left" />
) }
</>
);
};
export const StripeCreditCard = ( props ) => {
const { locale } = getStripeServerData().button;
const { stripe } = props;
return (
<Elements stripe={ stripe } locale={ locale }>
<CreditCardComponent { ...props } />
</Elements>
);
};

View File

@ -0,0 +1,97 @@
/**
* External dependencies
*/
import { useEffect, useCallback, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { getErrorMessageForTypeAndCode } from '../stripe-utils';
import { usePaymentIntents } from './use-payment-intents';
import { usePaymentProcessing } from './use-payment-processing';
/**
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
* @typedef {import('react').Dispatch<string>} SourceIdDispatch
*/
/**
* A custom hook for the Stripe processing and event observer logic.
*
* @param {EventRegistrationProps} eventRegistration Event registration functions.
* @param {BillingDataProps} billing Various billing data items.
* @param {string} sourceId Current set stripe source id.
* @param {SourceIdDispatch} setSourceId Setter for stripe source id.
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer
* response objects.
* @param {Stripe} stripe The stripe.js object.
*
* @return {function(Object):Object} Returns a function for handling stripe error.
*/
export const useCheckoutSubscriptions = (
eventRegistration,
billing,
sourceId,
setSourceId,
emitResponse,
stripe
) => {
const [ error, setError ] = useState( '' );
const onStripeError = useCallback( ( event ) => {
const type = event.error.type;
const code = event.error.code || '';
const message =
getErrorMessageForTypeAndCode( type, code ) ?? event.error.message;
setError( message );
return message;
}, [] );
const {
onCheckoutAfterProcessingWithSuccess,
onPaymentProcessing,
onCheckoutAfterProcessingWithError,
} = eventRegistration;
usePaymentIntents(
stripe,
onCheckoutAfterProcessingWithSuccess,
setSourceId,
emitResponse
);
usePaymentProcessing(
onStripeError,
error,
stripe,
billing,
emitResponse,
sourceId,
setSourceId,
onPaymentProcessing
);
// hook into and register callbacks for events.
useEffect( () => {
const onError = ( { processingResponse } ) => {
if ( processingResponse?.paymentDetails?.errorMessage ) {
return {
type: emitResponse.responseTypes.ERROR,
message: processingResponse.paymentDetails.errorMessage,
messageContext: emitResponse.noticeContexts.PAYMENTS,
};
}
// so we don't break the observers.
return true;
};
const unsubscribeAfterProcessing = onCheckoutAfterProcessingWithError(
onError
);
return () => {
unsubscribeAfterProcessing();
};
}, [
onCheckoutAfterProcessingWithError,
emitResponse.noticeContexts.PAYMENTS,
emitResponse.responseTypes.ERROR,
] );
return onStripeError;
};

View File

@ -0,0 +1,115 @@
/**
* External dependencies
*/
import { useState, useEffect, useCallback } from '@wordpress/element';
/**
* @typedef {import('../stripe-utils/type-defs').StripeElementOptions} StripeElementOptions
*/
/**
* Returns the value of a specific CSS property for the element matched by the provided selector.
*
* @param {string} selector CSS selector that matches the element to query.
* @param {string} property Name of the property to retrieve the style
* value from.
* @param {string} defaultValue Fallback value if the value for the property
* could not be retrieved.
*
* @return {string} The style value of that property in the document element.
*/
const getComputedStyle = ( selector, property, defaultValue ) => {
let elementStyle = {};
if (
typeof document === 'object' &&
typeof document.querySelector === 'function' &&
typeof window.getComputedStyle === 'function'
) {
const element = document.querySelector( selector );
if ( element ) {
elementStyle = window.getComputedStyle( element );
}
}
return elementStyle[ property ] || defaultValue;
};
/**
* Default options for the stripe elements.
*/
const elementOptions = {
style: {
base: {
iconColor: '#666EE8',
color: '#31325F',
fontSize: getComputedStyle(
'.wc-block-checkout',
'fontSize',
'16px'
),
lineHeight: 1.375, // With a font-size of 16px, line-height will be 22px.
'::placeholder': {
color: '#fff',
},
},
},
classes: {
focus: 'focused',
empty: 'empty',
invalid: 'has-error',
},
};
/**
* A custom hook handling options implemented on the stripe elements.
*
* @param {Object} [overloadedOptions] An array of extra options to merge with
* the options provided for the element.
*
* @return {StripeElementOptions} The stripe element options interface
*/
export 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 {
...prevOptions,
style: {
...prevOptions.style,
base: {
...prevOptions.style.base,
'::placeholder': {
color,
},
},
},
...showIcon,
};
} );
}, [ isActive ] );
const onActive = useCallback(
( isEmpty ) => {
if ( ! isEmpty ) {
setIsActive( true );
} else {
setIsActive( ( prevActive ) => ! prevActive );
}
},
[ setIsActive ]
);
return { options, onActive, error, setError };
};

View File

@ -0,0 +1,104 @@
/**
* External dependencies
*/
import { useEffect } from '@wordpress/element';
/**
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
*/
/**
* Opens the modal for PaymentIntent authorizations.
*
* @param {Object} params Params object.
* @param {Stripe} params.stripe The stripe object.
* @param {Object} params.paymentDetails The payment details from the
* server after checkout processing.
* @param {string} params.errorContext Context where errors will be added.
* @param {string} params.errorType Type of error responses.
* @param {string} params.successType Type of success responses.
*/
const openIntentModal = ( {
stripe,
paymentDetails,
errorContext,
errorType,
successType,
} ) => {
const checkoutResponse = { type: successType };
if (
! paymentDetails.setup_intent &&
! paymentDetails.payment_intent_secret
) {
return checkoutResponse;
}
const isSetupIntent = !! paymentDetails.setupIntent;
const verificationUrl = paymentDetails.verification_endpoint;
const intentSecret = isSetupIntent
? paymentDetails.setup_intent
: paymentDetails.payment_intent_secret;
return stripe[ isSetupIntent ? 'confirmCardSetup' : 'confirmCardPayment' ](
intentSecret
)
.then( function ( response ) {
if ( response.error ) {
throw response.error;
}
const intent =
response[ isSetupIntent ? 'setupIntent' : 'paymentIntent' ];
if (
intent.status !== 'requires_capture' &&
intent.status !== 'succeeded'
) {
return checkoutResponse;
}
checkoutResponse.redirectUrl = verificationUrl;
return checkoutResponse;
} )
.catch( function ( error ) {
checkoutResponse.type = errorType;
checkoutResponse.message = error.message;
checkoutResponse.retry = true;
checkoutResponse.messageContext = errorContext;
// Reports back to the server.
window.fetch( verificationUrl + '&is_ajax' );
return checkoutResponse;
} );
};
export const usePaymentIntents = (
stripe,
subscriber,
setSourceId,
emitResponse
) => {
useEffect( () => {
const unsubscribe = subscriber( async ( { processingResponse } ) => {
const paymentDetails = processingResponse.paymentDetails || {};
const response = await openIntentModal( {
stripe,
paymentDetails,
errorContext: emitResponse.noticeContexts.PAYMENTS,
errorType: emitResponse.responseTypes.ERROR,
successType: emitResponse.responseTypes.SUCCESS,
} );
if (
response.type === emitResponse.responseTypes.ERROR &&
response.retry
) {
setSourceId( '0' );
}
return response;
} );
return () => unsubscribe();
}, [
subscriber,
emitResponse.noticeContexts.PAYMENTS,
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
setSourceId,
stripe,
] );
};

View File

@ -0,0 +1,166 @@
/**
* External dependencies
*/
import { useEffect } from '@wordpress/element';
import {
CardElement,
CardNumberElement,
useElements,
} from '@stripe/react-stripe-js';
/**
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME } from './constants';
import {
getStripeServerData,
getErrorMessageForTypeAndCode,
} from '../stripe-utils';
import { errorTypes } from '../stripe-utils/constants';
/**
* @typedef {import('@stripe/stripe-js').Stripe} Stripe
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
* @typedef {import('react').Dispatch<string>} SourceIdDispatch
*/
/**
* @typedef {function(function():any):function():void} EventRegistration
*/
/**
* A custom hook that registers stripe payment processing with the
* onPaymentProcessing event from checkout.
*
* @param {function(any):string} onStripeError Sets an error for stripe.
* @param {string} error Any set error message (an empty string if no
* error).
* @param {Stripe} stripe The stripe utility
* @param {BillingDataProps} billing Various billing data items.
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer
* response objects.
* @param {string} sourceId Current set stripe source id.
* @param {SourceIdDispatch} setSourceId Setter for stripe source id.
* @param {EventRegistration} onPaymentProcessing The event emitter for processing payment.
*/
export const usePaymentProcessing = (
onStripeError,
error,
stripe,
billing,
emitResponse,
sourceId,
setSourceId,
onPaymentProcessing
) => {
const elements = useElements();
// hook into and register callbacks for events
useEffect( () => {
const createSource = async ( ownerInfo ) => {
const elementToGet = getStripeServerData().inline_cc_form
? CardElement
: CardNumberElement;
return await stripe.createSource(
// @ts-ignore
elements?.getElement( elementToGet ),
{
type: 'card',
owner: ownerInfo,
}
);
};
const onSubmit = async () => {
try {
const billingData = billing.billingData;
// if there's an error return that.
if ( error ) {
return {
type: emitResponse.responseTypes.ERROR,
message: error,
};
}
// use token if it's set.
if ( sourceId !== '' && sourceId !== '0' ) {
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paymentMethod: PAYMENT_METHOD_NAME,
paymentRequestType: 'cc',
stripe_source: sourceId,
},
billingData,
},
};
}
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 {
type: emitResponse.responseTypes.ERROR,
message: onStripeError( response ),
};
}
if ( ! response.source || ! response.source.id ) {
throw new Error(
getErrorMessageForTypeAndCode( errorTypes.API_ERROR )
);
}
setSourceId( response.source.id );
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
stripe_source: response.source.id,
paymentMethod: PAYMENT_METHOD_NAME,
paymentRequestType: 'cc',
},
billingData,
},
};
} catch ( e ) {
return {
type: emitResponse.responseTypes.ERROR,
message: e,
};
}
};
const unsubscribeProcessing = onPaymentProcessing( onSubmit );
return () => {
unsubscribeProcessing();
};
}, [
onPaymentProcessing,
billing.billingData,
stripe,
sourceId,
setSourceId,
onStripeError,
error,
emitResponse.noticeContexts.PAYMENTS,
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
elements,
] );
};