diff --git a/plugins/woocommerce-blocks/assets/css/abstracts/_variables.scss b/plugins/woocommerce-blocks/assets/css/abstracts/_variables.scss index b93fb3b1faa..952216d1f59 100644 --- a/plugins/woocommerce-blocks/assets/css/abstracts/_variables.scss +++ b/plugins/woocommerce-blocks/assets/css/abstracts/_variables.scss @@ -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; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/express-payment-methods.js b/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/express-payment-methods.js index 5cb719f652e..e34ca48d06d 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/express-payment-methods.js +++ b/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/express-payment-methods.js @@ -21,7 +21,7 @@ const ExpressPaymentMethods = () => { paymentMethodIds.map( ( id ) => { const expressPaymentMethod = isEditor ? paymentMethods[ id ].edit - : paymentMethods[ id ].activeContent; + : paymentMethods[ id ].content; return isValidElement( expressPaymentMethod ) ? (
  • { cloneElement( expressPaymentMethod, { diff --git a/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/payment-methods.js b/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/payment-methods.js index 9bde8bb0afd..a0882eb8cbd 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/payment-methods.js +++ b/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/payment-methods.js @@ -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() } diff --git a/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/style.scss index 15800ceed6e..0501ceb6881 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/style.scss +++ b/plugins/woocommerce-blocks/assets/js/base/components/payment-methods/style.scss @@ -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; +} diff --git a/plugins/woocommerce-blocks/assets/js/base/components/tabs/index.js b/plugins/woocommerce-blocks/assets/js/base/components/tabs/index.js index 46cb96c3735..8109d209f30 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/tabs/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/components/tabs/index.js @@ -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 ( -
    +
    { 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} diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js index 46bba0bdc4d..6480b01b30d 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js @@ -173,6 +173,7 @@ export const PaymentMethodDataProvider = ( { hasFailed: paymentStatus === FAILED, isSuccessful: paymentStatus === SUCCESS, }; + /** * @type {PaymentMethodDataContext} */ diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/validation/index.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/validation/index.js index 37b6a737c34..a62ad147935 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/validation/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/validation/index.js @@ -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, } ); /** diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/payment-methods/use-payment-method-interface.js b/plugins/woocommerce-blocks/assets/js/base/hooks/payment-methods/use-payment-method-interface.js index 8dde586ba60..0d74eabfd25 100644 --- a/plugins/woocommerce-blocks/assets/js/base/hooks/payment-methods/use-payment-method-interface.js +++ b/plugins/woocommerce-blocks/assets/js/base/hooks/payment-methods/use-payment-method-interface.js @@ -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, diff --git a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.js b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.js index 18654a54285..2fcdc314179 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.js +++ b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.js @@ -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( diff --git a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/payment-method-config.js b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/payment-method-config.js index 2027e23cb48..e3f9d40e1e6 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/payment-method-config.js +++ b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/payment-method-config.js @@ -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( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/checkout/block.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/checkout/block.js index 94dd8d65689..ccbb7d39b67 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/checkout/block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/checkout/block.js @@ -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 } ) => ( @@ -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 = ( { ) } > - { /*@todo this should be something the payment method controls*/ } - - setShouldSavePayment( ! shouldSavePayment ) - } - />
    { attributes.showReturnToCart && ( diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/apple-pay-express.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/apple-pay-express.js new file mode 100644 index 00000000000..ed060093e95 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/apple-pay-express.js @@ -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 ? ( + + ) : null; +}; + +/** + * ApplePayExpress with stripe provider + * + * @param {RegisteredPaymentMethodProps|{}} props + */ +export const ApplePayExpress = ( props ) => { + const { locale } = getStripeServerData().button; + return ( + + + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/apple-pay.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/apple-pay-preview.js similarity index 100% rename from plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/apple-pay.js rename to plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/apple-pay-preview.js diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/constants.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/constants.js new file mode 100644 index 00000000000..d0098600bbe --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/constants.js @@ -0,0 +1,7 @@ +export const PAYMENT_METHOD_NAME = 'apple_pay'; + +export const DEFAULT_STRIPE_EVENT_HANDLERS = { + shippingAddressChange: null, + shippingOptionChange: null, + source: null, +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/index.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/index.js new file mode 100644 index 00000000000..fec7d7d154b --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/index.js @@ -0,0 +1,3 @@ +export { PAYMENT_METHOD_NAME } from './constants'; +export { ApplePayExpress } from './apple-pay-express'; +export { applePayImage } from './apple-pay-preview'; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/normalize.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/normalize.js new file mode 100644 index 00000000000..572880f79cd --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/apple-pay/normalize.js @@ -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, +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/index.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/index.js new file mode 100644 index 00000000000..b38cb162a79 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/express-payment/index.js @@ -0,0 +1,37 @@ +/** + * Internal dependencies + */ +import { + PAYMENT_METHOD_NAME, + ApplePayExpress, + applePayImage, +} from './apple-pay'; +import { stripePromise } from '../stripe-utils'; + +const ApplePayPreview = () => ; + +export const ApplePayConfig = { + id: PAYMENT_METHOD_NAME, + content: , + edit: , + 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; + } ); + } ), +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/index.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/index.js new file mode 100644 index 00000000000..ca53553467e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/index.js @@ -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 ) ); diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/index.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/index.js new file mode 100644 index 00000000000..57a07400c92 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/index.js @@ -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 = () =>
    TODO: Card edit preview soon...
    ; + +export const stripeCcPaymentMethod = { + id: PAYMENT_METHOD_NAME, + label: ( + + { __( 'Credit/Debit Card', 'woo-gutenberg-products-block' ) } + + ), + content: , + edit: , + canMakePayment: stripePromise, + ariaLabel: __( + 'Stripe Credit Card payment method', + 'woo-gutenberg-products-block' + ), +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/cc.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/cc.js similarity index 100% rename from plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/cc.js rename to plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/cc.js diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/constants.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/constants.js new file mode 100644 index 00000000000..33179af2a54 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/constants.js @@ -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', +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/index.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/index.js new file mode 100644 index 00000000000..e890070ee12 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/index.js @@ -0,0 +1,3 @@ +export * from './constants'; +export { StripeCreditCard } from './payment-method'; +export { ccSvg } from './cc'; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/payment-method.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/payment-method.js new file mode 100644 index 00000000000..9ad9fc2d072 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/payment-method.js @@ -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 ( + <> +
    + onActive( isEmpty ) } + onFocus={ () => onActive( isEmpty ) } + onChange={ errorCallback } + /> + +
    + + + ); +}; + +const CardElements = ( { + onChange, + inputErrorComponent: ValidationInputError, +} ) => { + const [ isEmpty, setIsEmpty ] = useState( true ); + const { + options: cardNumOptions, + onActive: cardNumOnActive, + error: cardNumError, + setError: cardNumSetError, + } = useElementOptions( { showIcon: false } ); + const { + options: cardExpiryOptions, + onActive: cardExpiryOnActive, + error: cardExpiryError, + setError: cardExpirySetError, + } = useElementOptions(); + const { + options: cardCvcOptions, + onActive: cardCvcOnActive, + error: cardCvcError, + setError: cardCvcSetError, + } = useElementOptions(); + const errorCallback = ( errorSetter ) => ( event ) => { + if ( event.error ) { + errorSetter( event.error.message ); + } else { + errorSetter( '' ); + } + setIsEmpty( event.empty ); + onChange( event ); + }; + return ( +
    +
    + cardNumOnActive( isEmpty ) } + onBlur={ () => cardNumOnActive( isEmpty ) } + /> + + +
    +
    + + + +
    +
    + + + +
    +
    + ); +}; + +const useStripeCheckoutSubscriptions = ( + eventRegistration, + paymentStatus, + billing, + sourceId, + setSourceId, + shouldSavePayment, + stripe +) => { + 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 ? ( + + ) : ( + + ); + + // we need to pass along source for customer from server if it's available + // and pre-populate for checkout (so it'd need to be returned with the + // order endpoint and available on billing details?) + // so this will need to be an option for selecting if there's a saved + // source attached with the order (see woocommerce/templates/myaccount/payment-methods.php) + // so that data will need to be included with the order endpoint (billing data) to choose from. + + //@todo do need to add save payment method checkbox here. + return ( + <> + { renderedCardElement } + setShouldSavePayment( ! shouldSavePayment ) } + /> + { + + ); +}; + +export const StripeCreditCard = ( props ) => { + const { locale } = getStripeServerData().button; + const { activePaymentMethod } = props; + + return activePaymentMethod === PAYMENT_METHOD_NAME ? ( + + + + ) : null; +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/index.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/index.js new file mode 100644 index 00000000000..b0300a1b157 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/index.js @@ -0,0 +1,2 @@ +export * from './utils'; +export { default as stripePromise } from './load-stripe'; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/load-stripe.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/load-stripe.js new file mode 100644 index 00000000000..7b31adead32 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/load-stripe.js @@ -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; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/type-defs.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/type-defs.js new file mode 100644 index 00000000000..c119b0034c3 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/type-defs.js @@ -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} 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 {}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/utils.js b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/utils.js new file mode 100644 index 00000000000..14dae54258b --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/payment-method-extensions/stripe-utils/utils.js @@ -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} 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, +}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/index.js b/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/index.js deleted file mode 100644 index a677003ee66..00000000000 --- a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Internal dependencies - */ -import { applePayImage } from './apple-pay'; -import { paypalImage } from './paypal'; - -export const ExpressApplePay = () => ; - -export const ExpressPaypal = () => ; diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/paypal.js b/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/paypal.js deleted file mode 100644 index 350db0a7801..00000000000 --- a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/express-payment/paypal.js +++ /dev/null @@ -1,2 +0,0 @@ -export const paypalImage = - "data:image/svg+xml,%3Csvg width='265' height='48' viewBox='0 0 265 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='265' height='48' rx='3' fill='%23FFC439'/%3E%3Cg clip-path='url(%23clip0)'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M111.579 17.3945H106.165C105.795 17.3945 105.48 17.6643 105.422 18.031L103.232 31.9439C103.189 32.2185 103.401 32.4659 103.679 32.4659H106.263C106.634 32.4659 106.949 32.1963 107.007 31.829L107.597 28.0763C107.654 27.709 107.97 27.4392 108.34 27.4392H110.053C113.619 27.4392 115.677 25.7096 116.215 22.2825C116.457 20.783 116.225 19.605 115.524 18.7799C114.755 17.8739 113.39 17.3945 111.579 17.3945ZM112.203 22.4761C111.907 24.423 110.423 24.423 108.988 24.423H108.171L108.744 20.787C108.778 20.5672 108.968 20.4054 109.19 20.4054H109.564C110.542 20.4054 111.464 20.4054 111.94 20.9639C112.225 21.297 112.311 21.7921 112.203 22.4761Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M127.76 22.4144H125.168C124.947 22.4144 124.756 22.5761 124.722 22.7959L124.607 23.5226L124.426 23.2593C123.865 22.4428 122.613 22.1699 121.365 22.1699C118.5 22.1699 116.053 24.3446 115.577 27.395C115.329 28.9166 115.681 30.3717 116.542 31.3864C117.332 32.3193 118.463 32.7081 119.807 32.7081C122.116 32.7081 123.395 31.2206 123.395 31.2206L123.28 31.9426C123.237 32.2186 123.449 32.4661 123.725 32.4661H126.06C126.431 32.4661 126.745 32.1964 126.803 31.829L128.204 22.9364C128.249 22.6626 128.037 22.4144 127.76 22.4144ZM124.147 27.4713C123.897 28.9555 122.721 29.9519 121.222 29.9519C120.469 29.9519 119.868 29.7099 119.481 29.2515C119.098 28.7961 118.953 28.1479 119.074 27.4259C119.308 25.9544 120.503 24.9253 121.98 24.9253C122.716 24.9253 123.314 25.1706 123.708 25.6328C124.103 26.1001 124.26 26.7524 124.147 27.4713Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M141.567 22.4141H138.962C138.714 22.4141 138.48 22.5378 138.339 22.745L134.747 28.0492L133.224 22.9518C133.128 22.633 132.834 22.4141 132.502 22.4141H129.942C129.631 22.4141 129.415 22.7187 129.513 23.0123L132.383 31.4518L129.685 35.2687C129.473 35.5694 129.687 35.9827 130.053 35.9827H132.655C132.902 35.9827 133.133 35.8621 133.273 35.6592L141.938 23.1241C142.145 22.8243 141.932 22.4141 141.567 22.4141Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M150.192 17.3945H144.778C144.408 17.3945 144.093 17.6643 144.035 18.031L141.846 31.9439C141.802 32.2185 142.014 32.4659 142.29 32.4659H145.069C145.327 32.4659 145.548 32.2772 145.588 32.0201L146.209 28.0763C146.266 27.709 146.582 27.4392 146.952 27.4392H148.665C152.232 27.4392 154.289 25.7096 154.827 22.2825C155.07 20.783 154.837 19.605 154.136 18.7799C153.367 17.8739 152.004 17.3945 150.192 17.3945ZM150.816 22.4761C150.521 24.423 149.037 24.423 147.601 24.423H146.785L147.359 20.787C147.393 20.5672 147.581 20.4054 147.804 20.4054H148.178C149.155 20.4054 150.078 20.4054 150.554 20.9639C150.838 21.297 150.925 21.7921 150.816 22.4761Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M166.373 22.4144H163.782C163.56 22.4144 163.371 22.5761 163.337 22.7959L163.223 23.5226L163.041 23.2593C162.479 22.4428 161.229 22.1699 159.979 22.1699C157.115 22.1699 154.669 24.3446 154.193 27.395C153.946 28.9166 154.296 30.3717 155.157 31.3864C155.949 32.3193 157.078 32.7081 158.423 32.7081C160.731 32.7081 162.011 31.2206 162.011 31.2206L161.895 31.9426C161.852 32.2186 162.064 32.4661 162.342 32.4661H164.676C165.046 32.4661 165.361 32.1964 165.418 31.829L166.82 22.9364C166.863 22.6626 166.651 22.4144 166.373 22.4144ZM162.76 27.4713C162.511 28.9555 161.334 29.9519 159.835 29.9519C159.083 29.9519 158.48 29.7099 158.094 29.2515C157.711 28.7961 157.567 28.1479 157.687 27.4259C157.922 25.9544 159.116 24.9253 160.592 24.9253C161.328 24.9253 161.927 25.1706 162.321 25.6328C162.717 26.1001 162.874 26.7524 162.76 27.4713Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M169.429 17.777L167.207 31.9445C167.163 32.2192 167.375 32.4665 167.652 32.4665H169.885C170.257 32.4665 170.572 32.197 170.629 31.8296L172.82 17.9174C172.863 17.6428 172.651 17.3945 172.375 17.3945H169.874C169.653 17.3952 169.463 17.5572 169.429 17.777Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M80.7514 35.1704L81.1653 32.5351L80.2432 32.5135H75.8398L78.9 13.0662C78.9095 13.0073 78.9404 12.9526 78.9854 12.9137C79.0306 12.8748 79.0883 12.8535 79.1486 12.8535H86.5733C89.0384 12.8535 90.7394 13.3675 91.6274 14.3822C92.0438 14.8582 92.3089 15.3557 92.4373 15.9031C92.5719 16.4775 92.5741 17.1637 92.4429 18.0008L92.4333 18.0617V18.5982L92.8497 18.8346C93.2003 19.0211 93.479 19.2344 93.6927 19.4786C94.0488 19.8857 94.2791 20.4031 94.3765 21.0162C94.4771 21.6468 94.4439 22.3975 94.2791 23.2473C94.0891 24.2246 93.782 25.076 93.3672 25.7724C92.9859 26.4142 92.4998 26.9466 91.9227 27.3591C91.3717 27.7511 90.7172 28.0486 89.9771 28.2391C89.2599 28.4262 88.4422 28.5206 87.5453 28.5206H86.9675C86.5544 28.5206 86.1531 28.6697 85.8381 28.9371C85.5221 29.21 85.3133 29.5828 85.2492 29.9906L85.2055 30.228L84.474 34.8731L84.441 35.0435C84.4321 35.0975 84.4171 35.1244 84.3949 35.1426C84.3752 35.1593 84.3468 35.1704 84.3191 35.1704H80.7514Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M93.2437 18.123C93.2218 18.265 93.1963 18.4102 93.1679 18.5593C92.1888 23.5979 88.8388 25.3386 84.5605 25.3386H82.3821C81.8589 25.3386 81.4179 25.7193 81.3365 26.2366L79.9053 35.3355C79.8523 35.6753 80.1135 35.9813 80.4554 35.9813H84.3191C84.7765 35.9813 85.1651 35.6482 85.2372 35.1959L85.2751 34.9993L86.0026 30.3724L86.0494 30.1186C86.1205 29.6648 86.5101 29.3315 86.9675 29.3315H87.5453C91.2886 29.3315 94.2191 27.8084 95.0756 23.4004C95.4332 21.559 95.2481 20.0215 94.3013 18.9402C94.0148 18.6142 93.6594 18.3435 93.2437 18.123Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M92.22 17.7136C92.0704 17.6698 91.916 17.6302 91.7577 17.5944C91.5985 17.5596 91.4356 17.5287 91.2677 17.5016C90.6804 17.4064 90.0367 17.3613 89.3474 17.3613H83.5279C83.3845 17.3613 83.2484 17.3938 83.1266 17.4524C82.8581 17.5818 82.6588 17.8364 82.6105 18.1482L81.3724 26.0073L81.3369 26.2364C81.4183 25.7191 81.8593 25.3384 82.3825 25.3384H84.5609C88.8392 25.3384 92.1892 23.5969 93.1683 18.5591C93.1976 18.41 93.2222 18.2649 93.2441 18.1229C92.9965 17.9911 92.7282 17.8784 92.4393 17.7824C92.3679 17.7587 92.2943 17.7358 92.22 17.7136Z' fill='%2322284F'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M82.61 18.149C82.6583 17.8372 82.8577 17.5826 83.1262 17.4541C83.2488 17.3952 83.384 17.3628 83.5275 17.3628H89.347C90.0363 17.3628 90.6799 17.4081 91.2673 17.5032C91.4351 17.5301 91.5981 17.5612 91.7573 17.5961C91.9156 17.6317 92.0699 17.6715 92.2196 17.715C92.2938 17.7372 92.3674 17.7603 92.4395 17.7832C92.7284 17.8792 92.9969 17.9928 93.2446 18.1237C93.5359 16.2617 93.2421 14.9939 92.2377 13.8459C91.1302 12.5819 89.1317 12.041 86.5741 12.041H79.1492C78.6268 12.041 78.1812 12.4217 78.1005 12.9399L75.0079 32.5872C74.9469 32.9759 75.246 33.3266 75.6372 33.3266H80.221L82.61 18.149Z' fill='%2328356A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0'%3E%3Crect x='75' y='12' width='98' height='24' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A"; diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/index.js b/plugins/woocommerce-blocks/assets/js/payment-methods-demo/index.js deleted file mode 100644 index 78db7b6d545..00000000000 --- a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/index.js +++ /dev/null @@ -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: , - edit: , - canMakePayment: Promise.resolve( true ), - } ) -); -registerExpressPaymentMethod( - ( Config ) => - new Config( { - id: 'paypal', - activeContent: , - edit: , - canMakePayment: Promise.resolve( true ), - } ) -); -registerPaymentMethod( ( Config ) => new Config( paypalPaymentMethod ) ); -registerPaymentMethod( ( Config ) => new Config( ccPaymentMethod ) ); diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/index.js b/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/index.js deleted file mode 100644 index f03a2cad40d..00000000000 --- a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Internal dependencies - */ -import { paypalSvg } from './paypal'; -import { ccSvg } from './cc'; - -const PaypalActivePaymentMethod = () => { - return ( -
    -

    This is where paypal payment method stuff would be.

    -
    - ); -}; - -const CreditCardActivePaymentMethod = () => { - return ( -
    -

    This is where cc payment method stuff would be.

    -
    - ); -}; - -export const paypalPaymentMethod = { - id: 'paypal', - label: , - stepContent:
    Billing steps
    , - activeContent: , - edit: , - canMakePayment: Promise.resolve( true ), - ariaLabel: 'paypal payment method', -}; - -export const ccPaymentMethod = { - id: 'cc', - label: , - stepContent: null, - activeContent: , - edit: , - canMakePayment: Promise.resolve( true ), - ariaLabel: 'credit-card-payment-method', -}; diff --git a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/paypal.js b/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/paypal.js deleted file mode 100644 index 34600efdaa2..00000000000 --- a/plugins/woocommerce-blocks/assets/js/payment-methods-demo/payment-methods/paypal.js +++ /dev/null @@ -1,2 +0,0 @@ -export const paypalSvg = - "data:image/svg+xml,%3Csvg width='88' height='22' viewBox='0 0 88 22' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M32.8441 4.76758H27.9833C27.6506 4.76758 27.3677 5.00779 27.3158 5.33428L25.3498 17.7226C25.3108 17.9672 25.5013 18.1874 25.7508 18.1874H28.0713C28.4039 18.1874 28.6868 17.9474 28.7388 17.6203L29.269 14.2789C29.3201 13.9518 29.6036 13.7116 29.9357 13.7116H31.4744C34.6762 13.7116 36.5241 12.1715 37.0069 9.11996C37.2243 7.78472 37.0161 6.73581 36.387 6.00111C35.6962 5.19439 34.4708 4.76758 32.8441 4.76758ZM33.4047 9.29231C33.1389 11.0259 31.8063 11.0259 30.5178 11.0259C30.1327 11.0259 29.8389 10.6816 29.8994 10.3013L30.2988 7.78828C30.3295 7.59259 30.4999 7.44854 30.699 7.44854H31.0352C31.913 7.44854 32.741 7.44854 33.1688 7.94579C33.424 8.2424 33.5021 8.68326 33.4047 9.29231Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M47.3737 9.23524H45.046C44.8477 9.23524 44.6763 9.37929 44.6458 9.57498L44.5876 9.94021C44.5714 10.0423 44.439 10.0725 44.3801 9.98754C43.8762 9.26056 42.7524 9.01758 41.6309 9.01758C39.0586 9.01758 36.8617 10.9539 36.4339 13.6701C36.2115 15.025 36.5276 16.3206 37.3011 17.2241C38.0104 18.0548 39.0253 18.401 40.2328 18.401C41.5163 18.401 42.4456 17.8931 42.9672 17.5063C43.1171 17.3952 43.3805 17.5351 43.3508 17.7194C43.3118 17.9651 43.5023 18.1856 43.7504 18.1856H45.8469C46.1804 18.1856 46.4619 17.9453 46.5144 17.6183L47.7724 9.70004C47.8123 9.45626 47.6223 9.23524 47.3737 9.23524ZM44.129 13.738C43.9045 15.0596 42.8491 15.9468 41.5029 15.9468C40.827 15.9468 40.2869 15.7314 39.9399 15.3232C39.5959 14.9177 39.4653 14.3405 39.5746 13.6976C39.7844 12.3873 40.8575 11.471 42.1832 11.471C42.8441 11.471 43.3815 11.6894 43.7354 12.101C44.09 12.5171 44.2307 13.0979 44.129 13.738Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M59.7719 9.23633H57.4326C57.2095 9.23633 56.9998 9.34654 56.8734 9.53096L53.6473 14.254L52.2798 9.71518C52.1938 9.43123 51.9302 9.23633 51.6316 9.23633H49.3331C49.0536 9.23633 48.8597 9.50761 48.9485 9.769L51.525 17.2838L49.1028 20.6824C48.9123 20.9501 49.1042 21.3182 49.4333 21.3182H51.7695C51.9913 21.3182 52.1987 21.2107 52.3246 21.0301L60.1043 9.86853C60.2907 9.6016 60.0993 9.23633 59.7719 9.23633Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M67.5158 4.76758H62.6544C62.3225 4.76758 62.0396 5.00779 61.9876 5.33428L60.0217 17.7226C59.9827 17.9672 60.1732 18.1874 60.4212 18.1874H62.916C63.1475 18.1874 63.3458 18.0194 63.382 17.7905L63.94 14.2789C63.9912 13.9518 64.2749 13.7116 64.6068 13.7116H66.1449C69.3473 13.7116 71.1946 12.1715 71.678 9.11996C71.8962 7.78472 71.6866 6.73581 71.0575 6.00111C70.3672 5.19439 69.1427 4.76758 67.5158 4.76758ZM68.0766 9.29231C67.8114 11.0259 66.4787 11.0259 65.1895 11.0259C64.805 11.0259 64.5116 10.6821 64.572 10.3023L64.9721 7.78828C65.0025 7.59259 65.1718 7.44854 65.3714 7.44854H65.7077C66.5847 7.44854 67.4134 7.44854 67.8413 7.94579C68.0965 8.2424 68.1739 8.68326 68.0766 9.29231Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M82.0443 9.23524H79.7181C79.5184 9.23524 79.3486 9.37929 79.3187 9.57498L79.2606 9.93976C79.2442 10.0422 79.1115 10.0727 79.0521 9.98754C78.5482 9.26056 77.4252 9.01758 76.3036 9.01758C73.7314 9.01758 71.5354 10.9539 71.1075 13.6701C70.8858 15.025 71.2005 16.3206 71.9738 17.2241C72.6847 18.0548 73.698 18.401 74.9056 18.401C76.189 18.401 77.1184 17.8931 77.6401 17.5063C77.79 17.3951 78.0534 17.5351 78.0237 17.7194C77.9846 17.9651 78.175 18.1856 78.4246 18.1856H80.5206C80.8524 18.1856 81.1353 17.9453 81.1873 17.6183L82.4459 9.70004C82.4843 9.45626 82.2938 9.23524 82.0443 9.23524ZM78.7999 13.738C78.5767 15.0596 77.5198 15.9468 76.1736 15.9468C75.4991 15.9468 74.9576 15.7314 74.6106 15.3232C74.2668 14.9177 74.1374 14.3405 74.2453 13.6976C74.4565 12.3873 75.5282 11.471 76.8539 11.471C77.5148 11.471 78.0521 11.6894 78.4061 12.101C78.7621 12.5171 78.9028 13.0979 78.7999 13.738Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M84.7892 5.10812L82.7942 17.7232C82.7551 17.9678 82.9455 18.188 83.1935 18.188H85.1993C85.5327 18.188 85.8154 17.948 85.8666 17.6209L87.8339 5.23317C87.8732 4.9886 87.6826 4.76758 87.4346 4.76758H85.1887C84.9904 4.76817 84.8198 4.91242 84.7892 5.10812Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.36581 20.5941C5.73488 20.5941 5.2533 20.0302 5.35201 19.4071L5.40611 19.0656C5.47468 18.6326 5.14591 18.2385 4.70769 18.2283C2.63224 18.2283 1.04828 16.3732 1.37354 14.3234L3.50165 0.912019C3.51021 0.859583 3.53788 0.810907 3.5783 0.776279C3.61891 0.741652 3.67067 0.722656 3.72482 0.722656H10.3915C12.6049 0.722656 14.1323 1.18033 14.9296 2.08381C15.3035 2.50766 15.5416 2.95069 15.6569 3.43805C15.7777 3.94954 15.7797 4.56057 15.6618 5.30595C15.6561 5.34204 15.6533 5.37871 15.6533 5.41525C15.6533 5.67541 15.7935 5.91682 16.0202 6.04447L16.0271 6.04837C16.3419 6.21438 16.5921 6.40434 16.7841 6.6218C17.1038 6.9843 17.3106 7.44495 17.398 7.99087C17.4884 8.55243 17.4586 9.22084 17.3106 9.9775C17.14 10.8477 16.8643 11.6058 16.4918 12.2259C16.1494 12.7974 15.713 13.2715 15.1948 13.6387C14.7001 13.9878 14.1124 14.2527 13.4478 14.4223C12.8038 14.5889 12.0696 14.673 11.2643 14.673H10.7455C10.3746 14.673 10.0142 14.8058 9.73135 15.0438C9.44766 15.2868 9.26012 15.6188 9.20259 15.9819L9.16337 16.1932L8.50659 20.3293L8.47693 20.4811C8.46897 20.5292 8.45543 20.5531 8.43552 20.5694C8.4178 20.5842 8.39232 20.5941 8.36743 20.5941H6.36581Z' fill='%2328356A'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16.7214 5.61684C16.5605 5.50966 16.3505 5.61455 16.3141 5.80444C15.435 10.291 12.427 11.8409 8.58551 11.8409H6.62952C6.15968 11.8409 5.76371 12.1799 5.69064 12.6405L4.40557 20.7424C4.35798 21.0449 4.5925 21.3174 4.89949 21.3174H8.36871C8.77942 21.3174 9.12841 21.0208 9.19311 20.6181L9.22715 20.443L9.88034 16.3231L9.92235 16.0971C9.98626 15.6931 10.336 15.3963 10.7468 15.3963H11.2656C14.6267 15.3963 17.258 14.04 18.027 10.1151C18.3481 8.4755 18.1819 7.10643 17.3318 6.14359C17.1563 5.94556 16.952 5.77045 16.7214 5.61684Z' fill='%23298FC2'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15.4624 5.05191C15.328 5.01293 15.1895 4.97771 15.0473 4.94585C14.9044 4.91478 14.7581 4.88728 14.6073 4.86314C14.08 4.77845 13.502 4.73828 12.8831 4.73828H7.65777C7.52896 4.73828 7.40672 4.76717 7.29743 4.81941C7.05634 4.93457 6.87736 5.16133 6.83396 5.43894L5.72228 12.4369L5.69043 12.6409C5.76349 12.1802 6.15947 11.8413 6.6293 11.8413H8.58529C12.4268 11.8413 15.4347 10.2906 16.3139 5.80481C16.3597 5.57336 16.2429 5.33958 16.0251 5.24893C15.9075 5.20002 15.7856 5.15487 15.6593 5.11325C15.5952 5.09208 15.5291 5.07169 15.4624 5.05191Z' fill='%2322284F'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.83306 5.4387C6.87646 5.16108 7.05544 4.93432 7.29653 4.81995C7.40662 4.76752 7.52806 4.73863 7.65687 4.73863H12.8822C13.5011 4.73863 14.0791 4.77899 14.6065 4.86368C14.7572 4.88762 14.9035 4.91533 15.0464 4.94639C15.1886 4.97805 15.3271 5.01347 15.4615 5.05225C15.5282 5.07204 15.5943 5.09262 15.659 5.113C16.0249 5.23359 16.4461 5.02084 16.468 4.63616C16.5389 3.38825 16.2308 2.46052 15.4778 1.60711C14.4834 0.481619 12.6889 0 10.3925 0H3.72558C3.25654 0 2.85638 0.338954 2.78391 0.80039L0.0071065 18.2948C-0.0476413 18.6409 0.220922 18.9531 0.572104 18.9531H1.27278C3.23984 18.9531 4.91496 17.5229 5.22332 15.5802L6.83306 5.4387Z' fill='%2328356A'/%3E%3C/svg%3E%0A"; diff --git a/plugins/woocommerce-blocks/assets/js/type-defs/contexts.js b/plugins/woocommerce-blocks/assets/js/type-defs/contexts.js index 5b7543039a4..f3076eab03b 100644 --- a/plugins/woocommerce-blocks/assets/js/type-defs/contexts.js +++ b/plugins/woocommerce-blocks/assets/js/type-defs/contexts.js @@ -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 diff --git a/plugins/woocommerce-blocks/assets/js/type-defs/registered-payment-method-props.js b/plugins/woocommerce-blocks/assets/js/type-defs/registered-payment-method-props.js index 8fc9ed764ed..d10d5be5ed4 100644 --- a/plugins/woocommerce-blocks/assets/js/type-defs/registered-payment-method-props.js +++ b/plugins/woocommerce-blocks/assets/js/type-defs/registered-payment-method-props.js @@ -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 {}; diff --git a/plugins/woocommerce-blocks/bin/webpack-helpers.js b/plugins/woocommerce-blocks/bin/webpack-helpers.js index 0fcf0321f6f..2ae9ad47cd3 100644 --- a/plugins/woocommerce-blocks/bin/webpack-helpers.js +++ b/plugins/woocommerce-blocks/bin/webpack-helpers.js @@ -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, }; diff --git a/plugins/woocommerce-blocks/package-lock.json b/plugins/woocommerce-blocks/package-lock.json index 5a6872f21cc..3ff07936b5c 100644 --- a/plugins/woocommerce-blocks/package-lock.json +++ b/plugins/woocommerce-blocks/package-lock.json @@ -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", diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json index 943f09fcf1d..1f1f69cb2d1 100644 --- a/plugins/woocommerce-blocks/package.json +++ b/plugins/woocommerce-blocks/package.json @@ -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", diff --git a/plugins/woocommerce-blocks/src/Assets/Api.php b/plugins/woocommerce-blocks/src/Assets/Api.php index 7bf2f6194e2..4679b029fcc 100644 --- a/plugins/woocommerce-blocks/src/Assets/Api.php +++ b/plugins/woocommerce-blocks/src/Assets/Api.php @@ -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 ); } diff --git a/plugins/woocommerce-blocks/src/BlockTypes/Cart.php b/plugins/woocommerce-blocks/src/BlockTypes/Cart.php index bbdc337940a..7defc196edb 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/Cart.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/Cart.php @@ -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(); } diff --git a/plugins/woocommerce-blocks/src/BlockTypes/Checkout.php b/plugins/woocommerce-blocks/src/BlockTypes/Checkout.php index 457401ff98c..258e02ac578 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/Checkout.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/Checkout.php @@ -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 {
    '; } + + /** + * 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; + } } diff --git a/plugins/woocommerce-blocks/src/Domain/Bootstrap.php b/plugins/woocommerce-blocks/src/Domain/Bootstrap.php index 1100e81039d..eb2843199ea 100644 --- a/plugins/woocommerce-blocks/src/Domain/Bootstrap.php +++ b/plugins/woocommerce-blocks/src/Domain/Bootstrap.php @@ -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 + ); + } } diff --git a/plugins/woocommerce-blocks/src/PaymentMethodIntegrations/Stripe.php b/plugins/woocommerce-blocks/src/PaymentMethodIntegrations/Stripe.php new file mode 100644 index 00000000000..8aca2fd4ea0 --- /dev/null +++ b/plugins/woocommerce-blocks/src/PaymentMethodIntegrations/Stripe.php @@ -0,0 +1,177 @@ +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 ) ); + } +} diff --git a/plugins/woocommerce-blocks/webpack.config.js b/plugins/woocommerce-blocks/webpack.config.js index 06de419b125..787f604ad52 100644 --- a/plugins/woocommerce-blocks/webpack.config.js +++ b/plugins/woocommerce-blocks/webpack.config.js @@ -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, ];