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
This commit is contained in:
Darren Ethier 2020-03-30 08:07:49 -04:00 committed by GitHub
parent 1648e651b4
commit fb3cae67e6
44 changed files with 2267 additions and 213 deletions

View File

@ -20,3 +20,7 @@ $block-container-side-padding: $block-side-ui-width + $block-padding + 2 * $bloc
// Cart block
$cart-image-width: 5rem;
// Card element widths
$card-element-small-width: 5rem;
$card-element-width: 7rem;

View File

@ -21,7 +21,7 @@ const ExpressPaymentMethods = () => {
paymentMethodIds.map( ( id ) => {
const expressPaymentMethod = isEditor
? paymentMethods[ id ].edit
: paymentMethods[ id ].activeContent;
: paymentMethods[ id ].content;
return isValidElement( expressPaymentMethod ) ? (
<li key={ id } id={ `express-payment-method-${ id }` }>
{ cloneElement( expressPaymentMethod, {

View File

@ -37,9 +37,7 @@ import SavedPaymentMethodOptions from './saved-payment-method-options';
const getPaymentMethod = ( id, paymentMethods, isEditor ) => {
let paymentMethod = paymentMethods[ id ] || null;
if ( paymentMethod ) {
paymentMethod = isEditor
? paymentMethod.edit
: paymentMethod.activeContent;
paymentMethod = isEditor ? paymentMethod.edit : paymentMethod.content;
}
return paymentMethod;
};
@ -77,7 +75,7 @@ const PaymentMethods = () => {
);
return paymentMethod
? cloneElement( paymentMethod, {
activePaymentMethod: paymentMethod.id,
activePaymentMethod,
...currentPaymentMethodInterface.current,
} )
: null;
@ -110,6 +108,7 @@ const PaymentMethods = () => {
'Payment Methods',
'woo-gutenberg-products-block'
) }
id="wc-block-payment-methods"
>
{ getRenderedTab() }
</Tabs>

View File

@ -36,6 +36,7 @@
width: 50%;
> img {
width: 100%;
height: 48px;
}
}
> li:nth-child(even) {
@ -46,3 +47,107 @@
}
}
}
.wc-block-card-elements {
display: flex;
width: 100%;
}
.wc-block-gateway-container {
position: relative;
margin-bottom: $gap-large;
white-space: nowrap;
&.wc-card-number-element {
flex: auto;
}
&.wc-card-expiry-element,
&.wc-card-cvc-element {
width: $card-element-width;
margin-left: $gap-small;
}
.wc-block-gateway-input {
background-color: #fff;
padding: $gap-small $gap;
border-radius: 4px;
border: 1px solid $input-border-gray;
width: 100%;
font-size: 16px;
line-height: 22px;
font-family: inherit;
margin: 0;
box-sizing: border-box;
height: 48px;
color: $input-text-active;
&:focus {
background-color: #fff;
}
}
&:focus {
background-color: #fff;
}
label {
position: absolute;
transform: translateY(#{$gap-small});
left: 0;
top: 0;
transform-origin: top left;
font-size: 16px;
line-height: 22px;
color: $gray-50;
transition: transform 200ms ease;
margin: 0 $gap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - #{2 * $gap});
@media screen and (prefers-reduced-motion: reduce) {
transition: none;
}
}
&.wc-inline-card-element {
label {
margin-left: $gap-largest;
}
.wc-block-gateway-input.focused.empty,
.wc-block-gateway-input:not(.empty) {
& + label {
margin-left: $gap;
transform: translateY(#{$gap-smallest}) scale(0.75);
}
}
& + .wc-block-form-input-validation-error {
position: static;
margin-top: -$gap-large;
}
}
.wc-block-gateway-input.focused.empty,
.wc-block-gateway-input:not(.empty) {
padding: $gap-large $gap $gap-smallest;
& + label {
transform: translateY(#{$gap-smallest}) scale(0.75);
}
}
.wc-block-gateway-input.has-error {
border-color: $error-red;
&:focus {
outline-color: $error-red;
}
}
.wc-block-gateway-input.has-error + label {
color: $error-red;
}
}
.wc-blocks-credit-card-images {
padding-top: $gap-small;
}

View File

@ -21,6 +21,7 @@ const Tabs = ( {
instanceId,
ariaLabel = __( 'Tabbed Content', 'woo-gutenberg-products-block' ),
children,
id,
} ) => {
const [ selected, setSelected ] = useState(
initialTabName || ( tabs.length > 0 ? tabs[ 0 ].name : '' )
@ -38,7 +39,10 @@ const Tabs = ( {
}
const selectedId = `${ instanceId }-${ selectedTab.name }`;
return (
<div className={ classnames( 'wc-block-components-tabs', className ) }>
<div
className={ classnames( 'wc-block-components-tabs', className ) }
id={ id }
>
<div
role="tablist"
aria-label={ ariaLabel }

View File

@ -98,12 +98,18 @@ export const CheckoutProvider = ( {
useEffect( () => {
currentObservers.current = observers;
}, [ observers ] );
const onCheckoutCompleteSuccess = emitterSubscribers( subscriber )
.onCheckoutCompleteSuccess;
const onCheckoutCompleteError = emitterSubscribers( subscriber )
.onCheckoutCompleteError;
const onCheckoutProcessing = emitterSubscribers( subscriber )
.onCheckoutProcessing;
const onCheckoutCompleteSuccess = useMemo(
() => emitterSubscribers( subscriber ).onCheckoutCompleteSuccess,
[]
);
const onCheckoutCompleteError = useMemo(
() => emitterSubscribers( subscriber ).onCheckoutCompleteError,
[]
);
const onCheckoutProcessing = useMemo(
() => emitterSubscribers( subscriber ).onCheckoutProcessing,
[]
);
/**
* @type {CheckoutDispatchActions}

View File

@ -173,6 +173,7 @@ export const PaymentMethodDataProvider = ( {
hasFailed: paymentStatus === FAILED,
isSuccessful: paymentStatus === SUCCESS,
};
/**
* @type {PaymentMethodDataContext}
*/

View File

@ -13,7 +13,7 @@ const ValidationContext = createContext( {
setValidationErrors: ( errors ) => void errors,
clearValidationError: ( property ) => void property,
clearAllValidationErrors: () => void null,
getValidationErrorId: ( inputId ) => void inputId,
getValidationErrorId: ( inputId ) => inputId,
} );
/**

View File

@ -11,6 +11,8 @@ import { __ } from '@wordpress/i18n';
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
import { useEffect, useRef } from '@wordpress/element';
import { DISPLAY_CART_PRICES_INCLUDING_TAX } from '@woocommerce/block-settings';
import { ValidationInputError } from '@woocommerce/base-components/validation';
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
/**
* Internal dependencies
@ -178,6 +180,10 @@ export const usePaymentMethodInterface = () => {
onShippingRateSelectSuccess,
onShippingRateSelectFail,
},
components: {
ValidationInputError,
CheckboxControl,
},
onSubmit,
activePaymentMethod,
setActivePaymentMethod,

View File

@ -8,19 +8,19 @@ export default class ExpressPaymentMethodConfig {
// validate config
ExpressPaymentMethodConfig.assertValidConfig( config );
this.id = config.id;
this.activeContent = config.activeContent;
this.content = config.content;
this.edit = config.edit;
this.canMakePayment = config.canMakePayment;
}
static assertValidConfig = ( config ) => {
assertConfigHasProperties( config, [ 'id', 'activeContent', 'edit' ] );
assertConfigHasProperties( config, [ 'id', 'content', 'edit' ] );
if ( typeof config.id !== 'string' ) {
throw new TypeError(
'The id for the express payment method must be a string'
);
}
assertValidElement( config.activeContent, 'activeContent' );
assertValidElement( config.content, 'content' );
assertValidElement( config.edit, 'edit' );
if ( ! ( config.canMakePayment instanceof Promise ) ) {
throw new TypeError(

View File

@ -10,7 +10,7 @@ export default class PaymentMethodConfig {
this.id = config.id;
this.label = config.label;
this.ariaLabel = config.ariaLabel;
this.activeContent = config.activeContent;
this.content = config.content;
this.edit = config.edit;
this.canMakePayment = config.canMakePayment;
}
@ -19,9 +19,8 @@ export default class PaymentMethodConfig {
assertConfigHasProperties( config, [
'id',
'label',
'stepContent',
'ariaLabel',
'activeContent',
'content',
'edit',
'canMakePayment',
] );
@ -29,8 +28,7 @@ export default class PaymentMethodConfig {
throw new Error( 'The id for the payment method must be a string' );
}
assertValidElement( config.label, 'label' );
assertValidElement( config.stepContent, 'stepContent' );
assertValidElement( config.activeContent, 'activeContent' );
assertValidElement( config.content, 'content' );
assertValidElement( config.edit, 'edit' );
if ( typeof config.ariaLabel !== 'string' ) {
throw new TypeError(

View File

@ -46,7 +46,6 @@ import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
*/
import CheckoutSidebar from './sidebar/index.js';
import './style.scss';
import '../../../payment-methods-demo';
const Block = ( { isEditor = false, ...props } ) => (
<CheckoutProvider isEditor={ isEditor }>
@ -75,7 +74,6 @@ const Checkout = ( {
} = useValidationContext();
const [ contactFields, setContactFields ] = useState( {} );
const [ shouldSavePayment, setShouldSavePayment ] = useState( true );
const [ shippingAsBilling, setShippingAsBilling ] = useState( true );
const validateSubmit = () => {
@ -335,18 +333,6 @@ const Checkout = ( {
) }
>
<PaymentMethods />
{ /*@todo this should be something the payment method controls*/ }
<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 )
}
/>
</FormStep>
<div className="wc-block-checkout__actions">
{ attributes.showReturnToCart && (

View File

@ -0,0 +1,347 @@
/**
* 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,
} ) => {
/**
* @type {[ StripePaymentRequest|null, function( StripePaymentRequest ):StripePaymentRequest|null]}
*/
// @ts-ignore
const [ paymentRequest, setPaymentRequest ] = useState( null );
const stripe = useStripe();
const [ canMakePayment, setCanMakePayment ] = 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.currentStatus.isPristine &&
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.currentStatus.isPristine,
stripe,
] );
// 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 );
currentPaymentStatus.current.setPaymentStatus().processing();
};
const abortPayment = ( paymentMethod, message ) => {
paymentMethod.complete( 'fail' );
currentPaymentStatus.current
.setPaymentStatus()
.failed(
message,
getBillingData( paymentMethod ),
getPaymentMethodData( paymentMethod, PAYMENT_METHOD_NAME )
);
};
const completePayment = ( paymentMethod ) => {
paymentMethod.complete( 'success' );
currentPaymentStatus.current.setPaymentStatus().completed();
};
const processPayment = ( paymentMethod ) => {
currentPaymentStatus.current
.setPaymentStatus()
.success(
getBillingData( paymentMethod ),
getPaymentMethodData( paymentMethod, PAYMENT_METHOD_NAME )
);
onSubmit();
};
// event callbacks.
const onShippingRatesEvent = ( forSuccess = true ) => () => {
const handlers = eventHandlers.current;
if (
typeof handlers.shippingAddressChange === 'function' &&
currentPaymentStatus.current.currentStatus.isProcessing
) {
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' &&
currentPaymentStatus.current.currentStatus.isProcessing
) {
const updateObject = forSuccess
? {
status: 'success',
total: getTotalPaymentItem(
currentBilling.current.cartTotal
),
displayItems: normalizeLineItems(
currentBilling.current.cartTotalItems
),
}
: {
status: 'fail',
};
handlers.shippingOptionsChange.updateWith( updateObject );
}
};
const onCheckoutComplete = ( forSuccess = true ) => () => {
const handlers = eventHandlers.current;
if (
typeof handlers.sourceEvent === 'function' &&
currentPaymentStatus.current.currentStatus.isSuccessful
) {
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
) {
// @todo this error message can be converted to use wp.i18n
// and be inline.
abortPayment(
paymentMethod,
// eslint-disable-next-line no-undef
__(
"Sorry, we're not accepting prepaid cards at this time.",
'woocommerce-gateway-stripe'
)
);
return;
}
eventHandlers.current.sourceEvent = paymentMethod;
processPayment( paymentMethod );
} );
}
}, [ 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 unsubscribeCheckoutCompleteSuccess = subscriber.onCheckoutCompleteSuccess(
onCheckoutComplete()
);
const unsubscribeCheckoutCompleteFail = subscriber.onCheckoutCompleteError(
onCheckoutComplete( false )
);
return () => {
unsubscribeCheckoutCompleteFail();
unsubscribeCheckoutCompleteSuccess();
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>
);
};

View File

@ -0,0 +1,7 @@
export const PAYMENT_METHOD_NAME = 'apple_pay';
export const DEFAULT_STRIPE_EVENT_HANDLERS = {
shippingAddressChange: null,
shippingOptionChange: null,
source: null,
};

View File

@ -0,0 +1,3 @@
export { PAYMENT_METHOD_NAME } from './constants';
export { ApplePayExpress } from './apple-pay-express';
export { applePayImage } from './apple-pay-preview';

View File

@ -0,0 +1,169 @@
/**
* @typedef {import('../../stripe-utils/type-defs').StripePaymentItem} StripePaymentItem
* @typedef {import('../../stripe-utils/type-defs').StripeShippingOption} StripeShippingOption
* @typedef {import('../../stripe-utils/type-defs').StripeShippingAddress} StripeShippingAddress
* @typedef {import('../../stripe-utils/type-defs').StripePaymentResponse} StripePaymentResponse
* @typedef {import('@woocommerce/type-defs/cart').CartTotalItem} CartTotalItem
* @typedef {import('@woocommerce/type-defs/cart').CartShippingOption} CartShippingOption
* @typedef {import('@woocommerce/type-defs/cart').CartShippingAddress} CartShippingAddress
* @typedef {import('@woocommerce/type-defs/cart').CartBillingAddress} CartBillingAddress
*/
/**
* Normalizes incoming cart total items for use as a displayItems with the
* Stripe api.
*
* @param {CartTotalItem[]} cartTotalItems CartTotalItems to normalize
* @param {boolean} pending Whether to mark items as pending or
* not
*
* @return {StripePaymentItem[]} An array of PaymentItems
*/
const normalizeLineItems = ( cartTotalItems, pending = false ) => {
return cartTotalItems.map( ( cartTotalItem ) => {
return {
amount: cartTotalItem.value,
label: cartTotalItem.label,
pending,
};
} );
};
/**
* Normalizes incoming cart shipping option items for use as shipping options
* with the Stripe api.
*
* @param {CartShippingOption[]} shippingOptions An array of CartShippingOption items.
*
* @return {StripeShippingOption[]} An array of Stripe shipping option items.
*/
const normalizeShippingOptions = ( shippingOptions ) => {
return shippingOptions.map( ( shippingOption ) => {
return {
id: shippingOption.rate_id,
label: shippingOption.name,
detail: shippingOption.description,
amount: parseInt( shippingOption.price, 10 ),
};
} );
};
/**
* Normalize shipping address information from stripe's address object to
* the cart shipping address object shape.
*
* @param {StripeShippingAddress} shippingAddress Stripe's shipping address item
*
* @return {CartShippingAddress} The shipping address in the shape expected by
* the cart.
*/
const normalizeShippingAddressForCheckout = ( shippingAddress ) => {
return {
first_name: shippingAddress.recipient
.split( ' ' )
.slice( 0, 1 )
.join( ' ' ),
last_name: shippingAddress.recipient
.split( ' ' )
.slice( 1 )
.join( ' ' ),
company: '',
address_1:
typeof shippingAddress.addressLine[ 0 ] === 'undefined'
? ''
: shippingAddress.addressLine[ 0 ],
address_2:
typeof shippingAddress.addressLine[ 1 ] === 'undefined'
? ''
: shippingAddress.addressLine[ 1 ],
city: shippingAddress.city,
state: shippingAddress.region,
country: shippingAddress.country,
postcode: shippingAddress.postalCode,
};
};
/**
* Normalizes shipping option shape selection from Stripe's shipping option
* object to the expected shape for cart shipping option selections.
*
* @param {StripeShippingOption} shippingOption The customer's selected shipping
* option.
*
* @return {string[]} An array of ids (in this case will just be one)
*/
const normalizeShippingOptionSelectionsForCheckout = ( shippingOption ) => {
return [ shippingOption.id ];
};
/**
* Returns the billing data extracted from the stripe payment response to the
* CartBillingData shape.
*
* @param {StripePaymentResponse} paymentResponse Stripe's payment response
* object.
*
* @return {CartBillingAddress} The cart billing data
*/
const getBillingData = ( paymentResponse ) => {
const source = paymentResponse.source;
const name = source && source.owner.name;
const billing = source && source.owner.address;
const payerEmail = paymentResponse.payerEmail || '';
const payerPhone = paymentResponse.payerPhone || '';
return {
first_name: name
? name
.split( ' ' )
.slice( 0, 1 )
.join( ' ' )
: '',
last_name: name
? name
.split( ' ' )
.slice( 1 )
.join( ' ' )
: '',
email: ( source && source.owner.email ) || payerEmail,
phone:
( source && source.owner.phone ) ||
payerPhone.replace( '/[() -]/g', '' ),
country: ( billing && billing.country ) || '',
address_1: ( billing && billing.line1 ) || '',
address_2: ( billing && billing.line2 ) || '',
city: ( billing && billing.city ) || '',
state: ( billing && billing.state ) || '',
postcode: ( billing && billing.postal_code ) || '',
company: '',
};
};
/**
* This returns extra payment method data to add to the payment method update
* request made by the checkout processor.
*
* @param {StripePaymentResponse} paymentResponse A stripe payment response
* object.
* @param {string} paymentRequestType The payment request type
* used for payment.
*
* @return {Object} An object with the extra payment data.
*/
const getPaymentMethodData = ( paymentResponse, paymentRequestType ) => {
return {
payment_method: 'stripe',
stripe_source: paymentResponse.source
? paymentResponse.source.id
: null,
payment_request_type: paymentRequestType,
};
};
export {
normalizeLineItems,
normalizeShippingOptions,
normalizeShippingAddressForCheckout,
normalizeShippingOptionSelectionsForCheckout,
getBillingData,
getPaymentMethodData,
};

View File

@ -0,0 +1,37 @@
/**
* Internal dependencies
*/
import {
PAYMENT_METHOD_NAME,
ApplePayExpress,
applePayImage,
} from './apple-pay';
import { stripePromise } from '../stripe-utils';
const ApplePayPreview = () => <img src={ applePayImage } alt="" />;
export const ApplePayConfig = {
id: PAYMENT_METHOD_NAME,
content: <ApplePayExpress />,
edit: <ApplePayPreview />,
canMakePayment: stripePromise.then( ( stripe ) => {
if ( stripe === null ) {
return false;
}
// do a test payment request to check if apple pay can be done.
const paymentRequest = stripe.paymentRequest( {
total: {
label: 'Test total',
amount: 1000,
},
country: 'US',
currency: 'usd',
} );
return paymentRequest.canMakePayment().then( ( result ) => {
if ( result && result.applePay ) {
return true;
}
return false;
} );
} ),
};

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
import {
registerExpressPaymentMethod,
registerPaymentMethod,
} from '@woocommerce/blocks-registry';
/**
* Internal dependencies
*/
import { ApplePayConfig } from './express-payment';
import { stripeCcPaymentMethod } from './payment-methods';
registerExpressPaymentMethod( ( Config ) => new Config( ApplePayConfig ) );
registerPaymentMethod( ( Config ) => new Config( stripeCcPaymentMethod ) );

View File

@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME, StripeCreditCard } from './stripe';
import { stripePromise } from '../stripe-utils';
const EditPlaceHolder = () => <div>TODO: Card edit preview soon...</div>;
export const stripeCcPaymentMethod = {
id: PAYMENT_METHOD_NAME,
label: (
<strong>
{ __( 'Credit/Debit Card', 'woo-gutenberg-products-block' ) }
</strong>
),
content: <StripeCreditCard />,
edit: <EditPlaceHolder />,
canMakePayment: stripePromise,
ariaLabel: __(
'Stripe Credit Card payment method',
'woo-gutenberg-products-block'
),
};

View File

@ -0,0 +1,30 @@
export const PAYMENT_METHOD_NAME = 'stripe';
export const errorTypes = {
INVALID_EMAIL: 'email_invalid',
INVALID_REQUEST: 'invalid_request_error',
API_CONNECTION: 'api_connection_error',
API_ERROR: 'api_error',
AUTHENTICATION_ERROR: 'authentication_error',
RATE_LIMIT_ERROR: 'rate_limit_error',
CARD_ERROR: 'card_error',
VALIDATION_ERROR: 'validation_error',
};
export const errorCodes = {
INVALID_NUMBER: 'invalid_number',
INVALID_EXPIRY_MONTH: 'invalid_expiry_month',
INVALID_EXPIRY_YEAR: 'invalid_expiry_year',
INVALID_CVC: 'invalid_cvc',
INCORRECT_NUMBER: 'incorrect_number',
INCOMPLETE_NUMBER: 'incomplete_number',
INCOMPLETE_CVC: 'incomplete_cvc',
INCOMPLETE_EXPIRY: 'incomplete_expiry',
EXPIRED_CARD: 'expired_card',
INCORRECT_CVC: 'incorrect_cvc',
INCORRECT_ZIP: 'incorrect_zip',
INVALID_EXPIRY_YEAR_PAST: 'invalid_expiry_year_past',
CARD_DECLINED: 'card_declined',
MISSING: 'missing',
PROCESSING_ERROR: 'processing_error',
};

View File

@ -0,0 +1,3 @@
export * from './constants';
export { StripeCreditCard } from './payment-method';
export { ccSvg } from './cc';

View File

@ -0,0 +1,413 @@
/**
* 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;
};

View File

@ -0,0 +1,2 @@
export * from './utils';
export { default as stripePromise } from './load-stripe';

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { loadStripe } from '@stripe/stripe-js';
/**
* Internal dependencies
*/
import { getApiKey } from './utils';
const stripePromise = new Promise( ( resolve ) => {
let stripe = null;
try {
stripe = loadStripe( getApiKey() );
} catch ( error ) {
// eslint-disable-next-line no-console
//console.error( error.message );
}
resolve( stripe );
} );
export default stripePromise;

View File

@ -0,0 +1,286 @@
/**
* Stripe PaymentItem object
*
* @typedef {Object} StripePaymentItem
*
* @property {string} label The label for the payment item.
* @property {number} amount The amount for the payment item (in subunits)
* @property {boolean} [pending] Whether or not the amount is pending update on
* recalculation.
*/
/**
* Stripe ShippingOption object
*
* @typedef {Object} StripeShippingOption
*
* @property {string} id A unique ID for the shipping option.
* @property {string} label A short label for the shipping option.
* @property {string} detail A longer description for the shipping option.
* @property {number} amount The amount to show for the shipping option
* (in subunits)
*/
/**
* @typedef {Object} StripeShippingAddress
*
* @property {string} country Two letter country code, capitalized
* (ISO3166 alpha-2).
* @property {Array} addressLine An array of address line items.
* @property {string} region The most coarse subdivision of a
* country. (state etc)
* @property {string} city The name of a city, town, village etc.
* @property {string} postalCode The postal or ZIP code.
* @property {string} recipient The name of the recipient.
* @property {string} phone The phone number of the recipient.
* @property {string} [sortingCode] The sorting code as used in France.
* Not present on Apple platforms.
* @property {string} [dependentLocality] A logical subdivision of a city.
* Not present on Apple platforms.
*/
/**
* @typedef {Object} StripeBillingDetails
*
* @property {Object} address The billing address
* @property {string} address.city The billing address city
* @property {string} address.country The billing address country
* @property {string} address.line1 The first line for the address
* @property {string} address.line2 The second line fro the address
* @property {string} address.postal_code The postal/zip code
* @property {string} address.state The state
* @property {string} email The billing email
* @property {string} name The billing name
* @property {string} phone The billing phone
* @property {Object} [verified_address] The verified address of the owner.
* @property {string} [verified_email] Provided by the payment provider.
* @property {string} [verified_phone] Provided by the payment provider.
* @property {string} [verified_name] Provided by the payment provider.
*/
/**
* @typedef {Object} StripeBillingCard
*
* @property {string} brand The card brand
* @property {Object} checks Various security checks
* @property {string} checks.address_line1_check If an address line1 was
* provided, results of the
* check.
* @property {string} checks.address_postal_code_check If a postal code was
* provided, results of the
* check.
* @property {string} checks.cvc_check If CVC provided, results
* of the check.
* @property {string} country Two-letter ISO code for
* the country on the card.
* @property {number} exp_month Two-digit number for
* card's expiry month.
* @property {number} exp_year Two-digit number for
* card's expiry year.
* @property {string} fingerprint Uniquely identifies this
* particular card number
* @property {string} funding The card funding type
* @property {Object} generated_from Details of the original
* PaymentMethod that
* created this object.
* @property {string} last4 The last 4 digits of the
* card
* @property {Object} three_d_secure_usage Contains details on how
* this card may be used for
* 3d secure
* @property {Object} wallet If this card is part of a
* card wallet, this
* contains the details of
* the card wallet.
*/
/**
* @typedef {Object} StripePaymentMethod
*
* @property {string} id Unique identifier for the
* object
* @property {StripeBillingDetails} billing_details The billing details for the
* payment method
* @property {StripeBillingCard} card Details on the card used to
* pay
* @property {string} customer The ID of the customer to
* which this payment method
* is saved.
* @property {Object} metadata Set of key-value pairs that
* can be attached to the
* object.
* @property {string} type Type of payment method
* @property {string} object The type of object. Always
* 'payment_method'. Can use
* to validate!
* @property {Object} card_present If this is a card present
* payment method, contains
* details about that card
* @property {number} created The timestamp for when the
* card was created.
* @property {Object} fpx If this is an fpx payment
* method, contains details
* about it.
* @property {Object} ideal If this is an ideal payment
* method, contains details
* about it.
* @property {boolean} livemode True if the object exists
* in live mode or if in test
* mode.
* @property {Object} sepa_debit If this is a sepa_debit
* payment method, contains
* details about it.
*/
/**
* @typedef {Object} StripeSource
*
* @property {string} id Unique identifier for
* object
* @property {number} amount A positive number in
* the smallest currency
* unit.
* @property {string} currency The three-letter ISO
* code for the currency
* @property {string} customer The ID of the customer
* to which this source
* is attached.
* @property {Object} metadata Arbitrary key-value
* pairs that can be
* attached.
* @property {StripeBillingDetails} owner Information about the
* owner of the payment
* made.
* @property {Object} [redirect] Information related to
* the redirect flow
* (present if the source
* is authenticated by
* redirect)
* @property {string} statement_descriptor Extra information
* about a source (will
* appear on customer's
* statement)
* @property {string} status The status of the
* source.
* @property {string} type The type of the source
* (it is a payment
* method type)
* @property {string} object Value is "source" can
* be used to validate.
* @property {string} client_secret The client secret of
* the source. Used for
* client-side retrieval
* using a publishable
* key.
* @property {Object} [code_verification] Information related to
* the code verification
* flow.
* @property {number} created When the source object
* was instantiated
* (timestamp).
* @property {string} flow The authentication
* flow of the source.
* @property {boolean} livemode If true then payment
* is made in live mode
* otherwise test mode.
* @property {Object} [receiver] Information related to
* the receiver flow.
* @property {Object} source_order Information about the
* items and shipping
* associated with the
* source.
* @property {string} usage Whether source should
* be reusable or not.
*/
/**
* @typedef {Object} StripePaymentResponse
*
* @property {Object} token A stripe token object
* @property {StripePaymentMethod} paymentMethod The stripe payment method
* object
* @property {?StripeSource} source Present if this was the
* result of a source event
* listener
* @property {Function} complete Call this when the token
* data has been processed.
* @property {string} [payerName] The customer's name.
* @property {string} [payerEmail] The customer's email.
* @property {string} [payerPhone] The customer's phone.
* @property {StripeShippingAddress} [shippingAddress] The final shipping
* address the customer
* indicated
* @property {StripeShippingOption} [shippingOption] The final shipping
* option the customer
* selected.
* @property {string} methodName The unique name of the
* payment handler the
* customer chose to
* authorize payment
*/
/**
* @typedef {Object} StripePaymentRequestOptions The configuration of stripe
* payment request options to
* pass in.
*
* @property {string} country Two-letter (ISO)
* country code.
* @property {string} currency Three letter currency
* code.
* @property {StripePaymentItem} total Shown to the customer.
* @property {StripePaymentItem[]} displayItems Line items shown to the
* customer.
* @property {boolean} requestPayerName Whether or not to
* collect the payer's
* name.
* @property {boolean} requestPayerEmail Whether or not to
* collect the payer's
* email.
* @property {boolean} requestPayerPhone Whether or not to
* collect the payer's
* phone.
* @property {boolean} requestShipping Whether to collect
* shipping address.
* @property {StripeShippingOption[]} shippingOptions Available shipping
* options.
*/
/**
* @typedef {Object} StripePaymentRequest Stripe payment request object.
*
* @property {Function<Promise>} canMakePayment Returns a promise that resolves
* with an object detailing if a
* browser payment API is
* available.
* @property {Function} show Shows the browser's payment
* interface (called automatically
* if payment request button in
* use)
* @property {Function} update Used to update a PaymentRequest
* object.
* @property {Function} on For registering callbacks on
* payment request events.
*/
/**
* @typedef {Object} Stripe Stripe api object.
*/
/**
* @typedef {Object} StripeServerData
*
* @property {string} stripeTotalLabel The string used for payment descriptor.
* @property {string} publicKey The public api key for stripe requests.
* @property {boolean} allowPrepaidCard True means that prepaid cards can be
* used for payment.
* @property {Object} button Contains button styles
* @property {string} button.type The type of button.
* @property {string} button.theme The theme for the button.
* @property {string} button.height The height (in pixels) for the button.
* @property {string} button.locale The locale to use for stripe elements.
* @property {boolean} inline_cc_form Whether stripe cc should use inline cc
* form or separate inputs.
*/
export {};

View File

@ -0,0 +1,258 @@
/**
* Internal dependencies
*/
import { normalizeLineItems } from '../express-payment/apple-pay/normalize';
import { errorTypes, errorCodes } from '../payment-methods/stripe';
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import { __ } from '@wordpress/i18n';
/**
* @typedef {import('./type-defs').StripeServerData} StripeServerData
* @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem
* @typedef {import('./type-defs').StripePaymentRequest} StripePaymentRequest
* @typedef {import('@woocommerce/type-defs/cart').CartTotalItem} CartTotalItem
*/
// @todo this can't be in the file because the block might not show up unless it's
// rendered! (which also means it won't get passed from the server until it is rendered).
// so we need to work out a loading process for when the cart or checkout hasn't been added to the
// content yet - definitely don't want to load stripe without the block in the page. So that means
// checkout will need to have some sort of editor preview for stripe.
/**
* Stripe data comes form the server passed on a global object.
*
* @return {StripeServerData}
*/
const getStripeServerData = () => {
const stripeServerData = getSetting( 'stripe_data', null );
if ( ! stripeServerData ) {
throw new Error( 'Stripe initialization data is not available' );
}
return stripeServerData;
};
/**
* Returns the public api key for the stripe payment method
*
* @throws Error
* @return {string} The public api key for the stripe payment method.
*/
const getApiKey = () => {
const apiKey = getStripeServerData().publicKey;
if ( ! apiKey ) {
throw new Error(
'There is no api key available for stripe. Make sure it is available on the wc.stripe_data.stripe.key property.'
);
}
return apiKey;
};
/**
* The total PaymentItem object used for the stripe PaymentRequest object.
*
* @param {CartTotalItem} total The total amount.
*
* @return {StripePaymentItem} The PaymentItem object used for stripe.
*/
const getTotalPaymentItem = ( total ) => {
return {
label: getStripeServerData().stripeTotalLabel,
amount: total.value,
};
};
/**
* Returns a stripe payment request object
*
* @param {Object} config A configuration object for
* getting the payment request.
* @param {Object} config.stripe The stripe api.
* @param {CartTotalItem} config.total The amount for the total
* (in subunits) provided by
* checkout/cart.
* @param {string} config.currencyCode The currency code provided
* by checkout/cart.
* @param {string} config.countryCode The country code provided by
* checkout/cart.
* @param {boolean} config.shippingRequired Whether or not shipping is
* required.
* @param {CartTotalItem[]} config.cartTotalItems Array of line items provided
* by checkout/cart.
*
* @return {StripePaymentRequest} A stripe payment request object
*/
const getPaymentRequest = ( {
stripe,
total,
currencyCode,
countryCode,
shippingRequired,
cartTotalItems,
} ) => {
const options = {
total: getTotalPaymentItem( total ),
currency: currencyCode,
country: countryCode || 'US',
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
requestShipping: shippingRequired,
displayItems: normalizeLineItems( cartTotalItems ),
};
return stripe.paymentRequest( options );
};
/**
* Utility function for updating the Stripe PaymentRequest object
*
* @param {Object} update An object containing the
* things needed for the
* update
* @param {StripePaymentRequest} update.paymentRequest A Stripe payment request
* object
* @param {CartTotalItem} update.total A total line item.
* @param {string} update.currencyCode The currency code for the
* amount provided.
* @param {CartTotalItem[]} update.cartTotalItems An array of line items
* provided by the
* cart/checkout.
*/
const updatePaymentRequest = ( {
paymentRequest,
total,
currencyCode,
cartTotalItems,
} ) => {
paymentRequest.update( {
total: getTotalPaymentItem( total ),
currency: currencyCode,
displayItems: normalizeLineItems( cartTotalItems ),
} );
};
/**
* Returns whether or not the current session can do apple pay.
*
* @param {StripePaymentRequest} paymentRequest A Stripe PaymentRequest instance.
*
* @return {Promise<boolean>} True means apple pay can be done.
*/
const canDoApplePay = ( paymentRequest ) => {
return new Promise( ( resolve ) => {
paymentRequest.canMakePayment().then( ( result ) => {
if ( result && result.applePay ) {
resolve( true );
return;
}
resolve( false );
} );
} );
};
const isNonFriendlyError = ( type ) =>
[
errorTypes.INVALID_REQUEST,
errorTypes.API_CONNECTION,
errorTypes.API_ERROR,
errorTypes.AUTHENTICATION_ERROR,
errorTypes.RATE_LIMIT_ERROR,
].includes( type );
const getErrorMessageForCode = ( code ) => {
const messages = {
[ errorCodes.INVALID_NUMBER ]: __(
'The card number is not a valid credit card number.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_EXPIRY_MONTH ]: __(
"The card's expiration month is invalid.",
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_EXPIRY_YEAR ]: __(
"The card's expiration year is invalid.",
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_CVC ]: __(
"The card's security code is invalid.",
'woocommerce-gateway-stripe'
),
[ errorCodes.INCORRECT_NUMBER ]: __(
'The card number is incorrect.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCOMPLETE_NUMBER ]: __(
'The card number is incomplete.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCOMPLETE_CVC ]: __(
"The card's security code is incomplete.",
'woocommerce-gateway-stripe'
),
[ errorCodes.INCOMPLETE_EXPIRY ]: __(
"The card's expiration date is incomplete.",
'woocommerce-gateway-stripe'
),
[ errorCodes.EXPIRED_CARD ]: __(
'The card has expired.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCORRECT_CVC ]: __(
"The card's security code is incorrect.",
'woocommerce-gateway-stripe'
),
[ errorCodes.INCORRECT_ZIP ]: __(
"The card's zip code failed validation.",
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_EXPIRY_YEAR_PAST ]: __(
"The card's expiration year is in the past",
'woocommerce-gateway-stripe'
),
[ errorCodes.CARD_DECLINED ]: __(
'The card was declined.',
'woocommerce-gateway-stripe'
),
[ errorCodes.MISSING ]: __(
'There is no card on a customer that is being charged.',
'woocommerce-gateway-stripe'
),
[ errorCodes.PROCESSING_ERROR ]: __(
'An error occurred while processing the card.',
'woocommerce-gateway-stripe'
),
};
return messages[ code ] || '';
};
const getErrorMessageForTypeAndCode = ( type, code = '' ) => {
switch ( type ) {
case errorTypes.INVALID_EMAIL:
return __(
'Invalid email address, please correct and try again.',
'woo-gutenberg-product-blocks'
);
case isNonFriendlyError( type ):
return __(
'Unable to process this payment, please try again or use alternative method.',
'woo-gutenberg-product-blocks'
);
case errorTypes.CARD_ERROR:
case errorTypes.VALIDATION_ERROR:
return getErrorMessageForCode( code );
}
return '';
};
export {
getStripeServerData,
getApiKey,
getTotalPaymentItem,
getPaymentRequest,
updatePaymentRequest,
canDoApplePay,
getErrorMessageForTypeAndCode,
};

View File

@ -1,9 +0,0 @@
/**
* Internal dependencies
*/
import { applePayImage } from './apple-pay';
import { paypalImage } from './paypal';
export const ExpressApplePay = () => <img src={ applePayImage } alt="" />;
export const ExpressPaypal = () => <img src={ paypalImage } alt="" />;

File diff suppressed because one or more lines are too long

View File

@ -1,34 +0,0 @@
/**
* External dependencies
*/
import {
registerExpressPaymentMethod,
registerPaymentMethod,
} from '@woocommerce/blocks-registry';
/**
* Internal dependencies
*/
import { ExpressApplePay, ExpressPaypal } from './express-payment';
import { paypalPaymentMethod, ccPaymentMethod } from './payment-methods';
registerExpressPaymentMethod(
( Config ) =>
new Config( {
id: 'applepay',
activeContent: <ExpressApplePay />,
edit: <ExpressApplePay />,
canMakePayment: Promise.resolve( true ),
} )
);
registerExpressPaymentMethod(
( Config ) =>
new Config( {
id: 'paypal',
activeContent: <ExpressPaypal />,
edit: <ExpressPaypal />,
canMakePayment: Promise.resolve( true ),
} )
);
registerPaymentMethod( ( Config ) => new Config( paypalPaymentMethod ) );
registerPaymentMethod( ( Config ) => new Config( ccPaymentMethod ) );

View File

@ -1,41 +0,0 @@
/**
* Internal dependencies
*/
import { paypalSvg } from './paypal';
import { ccSvg } from './cc';
const PaypalActivePaymentMethod = () => {
return (
<div>
<p>This is where paypal payment method stuff would be.</p>
</div>
);
};
const CreditCardActivePaymentMethod = () => {
return (
<div>
<p>This is where cc payment method stuff would be.</p>
</div>
);
};
export const paypalPaymentMethod = {
id: 'paypal',
label: <img src={ paypalSvg } alt="" />,
stepContent: <div>Billing steps</div>,
activeContent: <PaypalActivePaymentMethod />,
edit: <PaypalActivePaymentMethod />,
canMakePayment: Promise.resolve( true ),
ariaLabel: 'paypal payment method',
};
export const ccPaymentMethod = {
id: 'cc',
label: <img src={ ccSvg } alt="" />,
stepContent: null,
activeContent: <CreditCardActivePaymentMethod />,
edit: <CreditCardActivePaymentMethod />,
canMakePayment: Promise.resolve( true ),
ariaLabel: 'credit-card-payment-method',
};

File diff suppressed because one or more lines are too long

View File

@ -136,7 +136,7 @@
*/
/**
* @typedef {function():PaymentStatusDispatchers|undefined} PaymentStatusDispatch
* @typedef {function():PaymentStatusDispatchers} PaymentStatusDispatch
*/
/**
@ -163,6 +163,7 @@
* the active payment
* method.
* @property {SavedCustomerPaymentMethods} customerPaymentMethods Returns the customer
* payment for the customer
* if it exists.
* @property {Object} paymentMethods Registered payment
* methods.
@ -212,17 +213,17 @@
* @property {string} redirectUrl This is the url that
* checkout will redirect
* to when it's ready.
* @property {function()} onCheckoutCompleteSuccess Used to register a
* @property {function(function())} onCheckoutCompleteSuccess Used to register a
* callback that will
* fire when the checkout
* is marked complete
* successfully.
* @property {function()} onCheckoutCompleteError Used to register
* @property {function(function())} onCheckoutCompleteError Used to register
* a callback that will
* fire when the checkout
* is marked complete and
* has an error.
* @property {function()} onCheckoutProcessing Used to register a
* @property {function(function())} onCheckoutProcessing Used to register a
* callback that will
* fire when the checkout
* has been submitted

View File

@ -143,32 +143,38 @@
/**
* @typedef EventRegistrationProps
*
* @property {function()} onCheckoutCompleteSuccess Used to subscribe callbacks
* firing when checkout has
* completed processing
* successfully.
* @property {function()} onCheckoutCompleteError Used to subscribe callbacks
* firing when checkout has
* completed processing with an
* error.
* @property {function()} onCheckoutProcessing Used to subscribe callbacks
* that will fire when checkout
* begins processing (as a part
* of the processing process).
* @property {function()} onShippingRateSuccess Used to subscribe callbacks
* that will fire when shipping
* rates for a given address have
* been received successfully.
* @property {function()} onShippingRateFail Used to subscribe callbacks
* that will fire when retrieving
* shipping rates failed.
* @property {function()} onShippingRateSelectSuccess Used to subscribe
* callbacks that will fire after
* selecting a shipping rate
* successfully.
* @property {function()} onShippingRateSelectFail Used to subscribe callbacks
* that will fire after selecting
* a shipping rate unsuccessfully.
* @property {function(function())} onCheckoutCompleteSuccess Used to subscribe callbacks firing
* when checkout has completed
* processing successfully.
* @property {function(function())} onCheckoutCompleteError Used to subscribe callbacks firing
* when checkout has completed
* processing with an error.
* @property {function(function())} onCheckoutProcessing Used to subscribe callbacks that
* will fire when checkout begins
* processing (as a part of the
* processing process).
* @property {function()} onShippingRateSuccess Used to subscribe callbacks that
* will fire when shipping rates for a
* given address have been received
* successfully.
* @property {function()} onShippingRateFail Used to subscribe callbacks that
* will fire when retrieving shipping
* rates failed.
* @property {function()} onShippingRateSelectSuccess Used to subscribe callbacks that
* will fire after selecting a
* shipping rate successfully.
* @property {function()} onShippingRateSelectFail Used to subscribe callbacks that
* will fire after selecting a shipping
* rate unsuccessfully.
*/
/**
* @typedef ComponentProps
*
* @property {function(Object):Object} ValidationInputError A container for holding validation
* errors
* @property {function(Object):Object} CheckboxControl A checkbox control, usually used for
* saved payment method functionality
*/
/**
@ -176,36 +182,23 @@
*
* @typedef {Object} RegisteredPaymentMethodProps
*
* @property {CheckoutStatusProps} checkoutStatus The current
* checkout status
* exposed as
* various boolean
* state.
* @property {PaymentStatusProps} paymentStatus Various payment
* status helpers.
* @property {ShippingStatusProps} shippingStatus Various shipping
* status helpers.
* @property {ShippingDataProps} shippingData Various data
* related to
* shipping.
* @property {BillingDataProps} billing Various billing
* data items.
* @property {EventRegistrationProps} eventRegistration Various event
* registration
* helpers for
* subscribing
* callbacks for
* events.
* @property {Function} [onSubmit] Used to trigger
* checkout
* processing.
* @property {string} [activePaymentMethod] Indicates what
* the active
* payment method
* is.
* @property {function( string )} [setActivePaymentMethod] Used to set the
* active payment
* method.
* @property {CheckoutStatusProps} checkoutStatus The current checkout status exposed
* as various boolean state.
* @property {PaymentStatusProps} paymentStatus Various payment status helpers.
* @property {ShippingStatusProps} shippingStatus Various shipping status helpers.
* @property {ShippingDataProps} shippingData Various data related to shipping.
* @property {BillingDataProps} billing Various billing data items.
* @property {EventRegistrationProps} eventRegistration Various event registration helpers
* for subscribing callbacks for
* events.
* @property {Function} [onSubmit] Used to trigger checkout
* processing.
* @property {string} [activePaymentMethod] Indicates what the active payment
* method is.
* @property {function( string )} [setActivePaymentMethod] Used to set the active payment
* method.
* @property {ComponentProps} components Components exposed to payment
* methods for use.
*/
export {};

View File

@ -461,8 +461,144 @@ const getFrontConfig = ( options = {} ) => {
};
};
const getPaymentMethodsExtensionConfig = ( options = {} ) => {
const { alias, resolvePlugins = [] } = options;
const resolve = alias
? {
alias,
plugins: resolvePlugins,
}
: {
plugins: resolvePlugins,
};
return {
entry: {
'wc-payment-method-extensions':
'./assets/js/payment-method-extensions/index.js',
},
output: {
devtoolNamespace: 'wc',
path: path.resolve( __dirname, '../build/' ),
filename: `[name].js`,
// This fixes an issue with multiple webpack projects using chunking
// overwriting each other's chunk loader function.
// See https://webpack.js.org/configuration/output/#outputjsonpfunction
jsonpFunction: 'webpackWcBlocksPaymentMethodExtensionJsonp',
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader?cacheDirectory',
options: {
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: {
browsers: [
'extends @wordpress/browserslist-config',
],
},
},
],
],
plugins: [
require.resolve(
'@babel/plugin-proposal-object-rest-spread'
),
require.resolve(
'@babel/plugin-transform-react-jsx'
),
require.resolve(
'@babel/plugin-proposal-async-generator-functions'
),
require.resolve(
'@babel/plugin-transform-runtime'
),
require.resolve(
'@babel/plugin-proposal-class-properties'
),
NODE_ENV === 'production'
? require.resolve(
'babel-plugin-transform-react-remove-prop-types'
)
: false,
].filter( Boolean ),
},
},
},
{
test: /\.s?css$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
{ loader: 'css-loader', options: { importLoaders: 1 } },
'postcss-loader',
{
loader: 'sass-loader',
query: {
includePaths: [
'assets/css/abstracts',
'node_modules',
],
data: [
'_colors',
'_variables',
'_breakpoints',
'_mixins',
]
.map(
( imported ) =>
`@import "${ imported }";`
)
.join( ' ' ),
},
},
],
},
],
},
plugins: [
new WebpackRTLPlugin( {
filename: `[name]-rtl.css`,
minify: {
safe: true,
},
} ),
new MiniCssExtractPlugin( {
filename: `[name].css`,
} ),
new ProgressBarPlugin( {
format:
chalk.blue( 'Build payment method extension scripts' ) +
' [:bar] ' +
chalk.green( ':percent' ) +
' :msg (:elapsed seconds)',
} ),
new DependencyExtractionWebpackPlugin( {
injectPolyfill: true,
requestToExternal,
requestToHandle,
} ),
new DefinePlugin( {
// Inject the `WOOCOMMERCE_BLOCKS_PHASE` global, used for feature flagging.
'process.env.WOOCOMMERCE_BLOCKS_PHASE': JSON.stringify(
// eslint-disable-next-line woocommerce/feature-flag
process.env.WOOCOMMERCE_BLOCKS_PHASE || 'experimental'
),
} ),
],
resolve,
};
};
module.exports = {
getAlias,
getFrontConfig,
getMainConfig,
getPaymentMethodsExtensionConfig,
};

View File

@ -3041,6 +3041,19 @@
}
}
},
"@stripe/react-stripe-js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.1.0.tgz",
"integrity": "sha512-9FzIv5R6J0ElRpGtrASxcgvmkwFyfQgwYKt6s3u1EaqfgOWCR0cgERmbEwEK2naI0xvr25BnSrd2DYiNbwNT+Q==",
"requires": {
"prop-types": "^15.7.2"
}
},
"@stripe/stripe-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.2.0.tgz",
"integrity": "sha512-z9N0ue1JDJSFL260uNqnEqyFzEVU9XMExEtTZNl02pH8/YBkbEu18cN9XTjAfs+qfPS/LQGOurGb7r5krIpWNQ=="
},
"@svgr/babel-plugin-add-jsx-attribute": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
@ -3497,6 +3510,15 @@
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "16.9.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz",
"integrity": "sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-syntax-highlighter": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz",
@ -3623,6 +3645,27 @@
}
}
},
"@types/wordpress__data": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/@types/wordpress__data/-/wordpress__data-4.6.6.tgz",
"integrity": "sha512-5qqW6kUISwzEyiCN3k8lxtY52wEhxCsrY6U2Af8H0JoICFTz8xv8vV63I1aLCi3K2aKgymeBqx+uzFgt0VL3QA==",
"dev": true,
"requires": {
"@types/wordpress__element": "*",
"redux": "^4.0.1"
}
},
"@types/wordpress__element": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/wordpress__element/-/wordpress__element-2.4.0.tgz",
"integrity": "sha512-pc/T2EVrVqeSDy9kCsLBcIGc/j3EQLkuUKNRERW3LpYIPFTcg7M9vQNw457GwnJP/c50zc43DEcROZCass+Egg==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-dom": "*",
"csstype": "^2.2.0"
}
},
"@types/yargs": {
"version": "13.0.8",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",

View File

@ -15,7 +15,6 @@
"*.scss",
"./assets/js/atomic/blocks/product/**",
"./assets/js/filters/**",
"./assets/js/payment-methods-demo/**",
"./assets/js/settings/blocks/**"
],
"repository": {
@ -77,6 +76,8 @@
"@storybook/react": "5.3.12",
"@types/jest": "25.1.4",
"@types/react": "16.9.23",
"@types/wordpress__data": "^4.6.6",
"@types/wordpress__element": "^2.4.0",
"@wordpress/babel-preset-default": "4.10.0",
"@wordpress/base-styles": "1.4.0",
"@wordpress/blocks": "6.12.0",
@ -145,6 +146,8 @@
"npm": "6.13.7"
},
"dependencies": {
"@stripe/react-stripe-js": "^1.1.0",
"@stripe/stripe-js": "^1.2.0",
"@woocommerce/components": "4.0.0",
"@wordpress/notices": "2.0.0",
"classnames": "2.2.6",

View File

@ -115,15 +115,17 @@ class Api {
* Registers a style according to `wp_register_style`.
*
* @since 2.5.0
* @since $VID:$ Change src to be relative source.
*
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $relative_src Relative source of the stylesheet to the plugin path.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
*/
public function register_style( $handle, $src, $deps = [], $media = 'all' ) {
$filename = str_replace( plugins_url( '/', __DIR__ ), '', $src );
public function register_style( $handle, $relative_src, $deps = [], $media = 'all' ) {
$filename = str_replace( plugins_url( '/', __DIR__ ), '', $relative_src );
$src = $this->get_asset_url( $relative_src );
$ver = $this->get_file_version( $filename );
wp_register_style( $handle, $src, $deps, $ver, $media );
}

View File

@ -74,10 +74,12 @@ class Cart extends AbstractBlock {
$max_quantity_limit = apply_filters( 'woocommerce_maximum_quantity_selected_cart', 99 );
$data_registry->add( 'quantitySelectLimit', $max_quantity_limit );
}
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' );
\Automattic\WooCommerce\Blocks\Assets::register_block_script(
$this->block_name . '-frontend',
$this->block_name . '-block-frontend'
);
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' );
return $content . $this->get_skeleton();
}

View File

@ -90,29 +90,19 @@ class Checkout extends AbstractBlock {
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
}
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend', $this->block_name . '-block-frontend' );
return $content . $this->get_skeleton();
}
/**
* Callback for woocommerce_payment_methods_list_item filter to add token id
* to the generated list.
*
* @param array $list_item The current list item for the saved payment method.
* @param \WC_Token $token The token for the current list item.
*
* @return array The list item with the token id added.
*/
public static function include_token_id_with_payment_methods( $list_item, $token ) {
$list_item['tokenId'] = $token->get_id();
$brand = ! empty( $list_item['method']['brand'] ) ?
strtolower( $list_item['method']['brand'] ) :
'';
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core.
if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) {
$list_item['method']['brand'] = wc_get_credit_card_type_label( $brand );
if ( is_user_logged_in() && ! $data_registry->exists( 'customerPaymentMethods' ) ) {
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
$data_registry->add(
'customerPaymentMethods',
wc_get_customer_saved_methods_list( get_current_user_id() )
);
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
}
return $list_item;
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' );
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend', $this->block_name . '-block-frontend' );
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' );
return $content . $this->get_skeleton();
}
@ -149,4 +139,25 @@ class Checkout extends AbstractBlock {
</div>
';
}
/**
* Callback for woocommerce_payment_methods_list_item filter to add token id
* to the generated list.
*
* @param array $list_item The current list item for the saved payment method.
* @param \WC_Token $token The token for the current list item.
*
* @return array The list item with the token id added.
*/
public static function include_token_id_with_payment_methods( $list_item, $token ) {
$list_item['tokenId'] = $token->get_id();
$brand = ! empty( $list_item['method']['brand'] ) ?
strtolower( $list_item['method']['brand'] ) :
'';
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core.
if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) {
$list_item['method']['brand'] = wc_get_credit_card_type_label( $brand );
}
return $list_item;
}
}

View File

@ -14,6 +14,7 @@ use Automattic\WooCommerce\Blocks\Assets\BackCompatAssetDataRegistry;
use Automattic\WooCommerce\Blocks\Library;
use Automattic\WooCommerce\Blocks\Registry\Container;
use Automattic\WooCommerce\Blocks\RestApi;
use Automattic\WooCommerce\Blocks\PaymentMethodIntegrations\Stripe;
/**
* Takes care of bootstrapping the plugin.
@ -89,6 +90,10 @@ class Bootstrap {
// load AssetDataRegistry.
$this->container->get( AssetDataRegistry::class );
// @todo this will eventually get moved into the relevant payment
// extensions
$this->load_payment_method_integrations();
Library::init();
OldAssets::init();
RestApi::init();
@ -163,4 +168,32 @@ class Bootstrap {
}
define( 'WOOCOMMERCE_BLOCKS_PHASE', $flag );
}
/**
* This is a temporary method that is used for setting up payment method
* integrations with Cart and Checkout blocks. This logic should get moved
* to the payment gateway extensions.
*/
protected function load_payment_method_integrations() {
// stripe registration.
$this->container->register(
Stripe::class,
function( Container $container ) {
$asset_data_registry = $container->get( AssetDataRegistry::class );
$asset_api = $container->get( AssetApi::class );
return new Stripe( $asset_data_registry, $asset_api );
}
);
add_action(
'plugins_loaded',
function() {
if ( class_exists( 'WC_Stripe' ) ) {
// initialize hooking into blocks.
$stripe = $this->container->get( Stripe::class );
$stripe->register_assets();
}
},
15
);
}
}

View File

@ -0,0 +1,177 @@
<?php
/**
* Temporary integration of the stripe payment method for the new cart and
* checkout blocks. Once the api is demonstrated to be stable, this integration
* will be moved to the Stripe extension
*
* @package WooCommerce/Blocks
* @since $VID:$
*/
namespace Automattic\WooCommerce\Blocks\PaymentMethodIntegrations;
use Exception;
use WC_Stripe_Payment_Request;
use WC_Stripe_Helper;
use Automattic\WooCommerce\Blocks\Assets\Api;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
/**
* Stripe payment method integration
*
* @since $VID:$
*/
class Stripe {
/**
* An instance of the AssetDataRegistry
*
* @var AssetDataRegistry
*/
private $asset_registry;
/**
* An instance of the Asset Api
*
* @var Api
*/
private $asset_api;
/**
* Stripe settings from the WP options table
*
* @var array
*/
private $stripe_settings;
/**
* Constructor for the class
*
* @param AssetDataRegistry $asset_registry Used for registering data to pass along to the request.
* @param Api $asset_api Used for registering scripts and styles.
*/
public function __construct( AssetDataRegistry $asset_registry, Api $asset_api ) {
$this->asset_registry = $asset_registry;
$this->asset_api = $asset_api;
$this->stripe_settings = get_option( 'woocommerce_stripe_settings', [] );
}
/**
* When called registers the stripe handle for enqueueing with cart and
* checkout blocks.
* Note: this assumes the stripe extension has registered this script.
* This will also ensure stripe data is loaded with the blocks.
*/
public function register_assets() {
// only do when not in admin.
if ( ! is_admin() ) {
add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before', [ $this, 'enqueue_data' ] );
add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before', [ $this, 'enqueue_data' ] );
}
add_action( 'init', [ $this, 'register_scripts_and_styles' ] );
}
/**
* When called, registers a stripe data object (with 'stripe_data' as the
* key) for including everywhere stripe integration happens.
*/
public function enqueue_data() {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$stripe_gateway = $available_gateways['stripe'] ? $available_gateways['stripe'] : null;
$data = [
'stripeTotalLabel' => $this->get_total_label(),
'publicKey' => $this->get_publishable_key(),
'allowPrepaidCard' => $this->get_allow_prepaid_card(),
'button' => [
'type' => $this->get_button_type(),
'theme' => $this->get_button_theme(),
'height' => $this->get_button_height(),
'locale' => $this->get_button_locale(),
],
'inline_cc_form' => $stripe_gateway && $stripe_gateway->inline_cc_form,
];
if ( ! $this->asset_registry->exists( 'stripe_data' ) ) {
$this->asset_registry->add( 'stripe_data', $data );
}
wp_enqueue_script( 'wc-payment-method-extensions' );
}
/**
* Register scripts and styles for extension.
*/
public function register_scripts_and_styles() {
wp_register_script( 'stripe', 'https://js.stripe.com/v3/', '', '3.0', true );
$this->asset_api->register_script(
'wc-payment-method-extensions',
'build/wc-payment-method-extensions.js',
[ 'stripe' ]
);
}
/**
* Returns the label to use accompanying the total in the stripe statement.
*
* @return string Statement descriptor
*/
private function get_total_label() {
return ! empty( $this->stripe_settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->stripe_settings['statement_descriptor'] ) : '';
}
/**
* Returns the publishable api key for the Stripe service.
*
* @return string Public api key.
*/
private function get_publishable_key() {
$test_mode = ( ! empty( $this->stripe_settings['testmode'] ) && 'yes' === $this->stripe_settings['testmode'] );
if ( $test_mode ) {
return ! empty( $this->stripe_settings['test_publishable_key'] ) ? $this->stripe_settings['test_publishable_key'] : '';
}
return ! empty( $this->stripe_settings['publishable_key'] ) ? $this->stripe_settings['publishable_key'] : '';
}
/**
* Returns whether to allow prepaid cards for payments.
*
* @return bool True means to allow prepaid card (default)
*/
private function get_allow_prepaid_card() {
return apply_filters( 'wc_stripe_allow_prepaid_card', true );
}
/**
* Return the button type for the payment button.
*
* @return string Defaults to 'default'
*/
private function get_button_type() {
return isset( $this->stripe_settings['payment_request_button_type'] ) ? $this->stripe_settings['payment_request_button_type'] : 'default';
}
/**
* Return the theme to use for the payment button.
*
* @return string Defaults to 'dark'.
*/
private function get_button_theme() {
return isset( $this->stripe_settings['payment_request_button_theme'] ) ? $this->stripe_settings['payment_request_button_theme'] : 'dark';
}
/**
* Return the height for the payment button.
*
* @return string A pixel value for the hight (defaults to '64')
*/
private function get_button_height() {
return isset( $this->stripe_settings['payment_request_button_height'] ) ? str_replace( 'px', '', $this->stripe_settings['payment_request_button_height'] ) : '64';
}
/**
* Return the locale for the payment button.
*
* @return string Defaults to en_US.
*/
private function get_button_locale() {
return apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) );
}
}

View File

@ -15,6 +15,7 @@ const {
getAlias,
getMainConfig,
getFrontConfig,
getPaymentMethodsExtensionConfig,
} = require( './bin/webpack-helpers.js' );
const baseConfig = {
@ -165,10 +166,20 @@ const LegacyFrontendBlocksConfig = {
} ),
};
/**
* This is a temporary config for building the payment methods integration
* script until it can be moved into the payment extension(s).
*/
const PaymentMethodsConfig = {
...baseConfig,
...getPaymentMethodsExtensionConfig( { alias: getAlias() } ),
};
module.exports = [
CoreConfig,
GutenbergBlocksConfig,
BlocksFrontendConfig,
LegacyBlocksConfig,
LegacyFrontendBlocksConfig,
PaymentMethodsConfig,
];