diff --git a/.github/automate-team-review-assignment-config.yml b/.github/automate-team-review-assignment-config.yml index 21f21d82aa8..c6568b8df6b 100644 --- a/.github/automate-team-review-assignment-config.yml +++ b/.github/automate-team-review-assignment-config.yml @@ -9,9 +9,9 @@ when: - rubik-fp-squad - author: teamIs: - - rubik-fse-squad + - kirigami ignore: nameIs: assign: teams: - - rubik-fse-squad + - kirigami diff --git a/.github/patch-initial-checklist.md b/.github/patch-initial-checklist.md index 764ac328149..e3714d7eae1 100644 --- a/.github/patch-initial-checklist.md +++ b/.github/patch-initial-checklist.md @@ -40,6 +40,7 @@ Additionally, make sure to differentiate between things in the testing notes tha * [ ] Execute `npm run deploy` * Note: the script automatically updates version numbers (commits on your behalf). * **ALERT**: This script will ask you if this release will be deployed to WordPress.org. You should only answer yes for this release **if it's the latest release and you want to deploy to WordPress.org**. Otherwise, answer no. If you answer yes, you will get asked additional verification by the `npm run deploy` script about deploying a patch release to WordPress.org. + * An email confirmation is required before the new version will be released, so check your email in order to confirm the release. ## If this release is deployed to WordPress.org... diff --git a/.github/release-initial-checklist.md b/.github/release-initial-checklist.md index 6aea7d34794..f12b776641a 100644 --- a/.github/release-initial-checklist.md +++ b/.github/release-initial-checklist.md @@ -56,6 +56,7 @@ Additionally, make sure to differentiate between things in the testing notes tha * Note: the script automatically updates version numbers on Github (commits on your behalf). * **ALERT**: This script will ask you if this release will be deployed to WordPress.org. You should answer yes for this release even if it is a pre-release. * A GitHub release will automatically be created and this will trigger a workflow that automatically deploys the plugin to WordPress.org. + * An email confirmation is required before the new version will be released, so check your email in order to confirm the release. * [ ] Edit the [GitHub release](https://github.com/woocommerce/woocommerce-gutenberg-products-block/releases) and copy changelog into the release notes. Ensure there is a release with the correct version, the one you entered above. * [ ] The `#team-rubik` slack instance will be notified about the progress with the WordPress.org deploy. Watch for that. If anything goes wrong, an error will be reported and you can followup via the GitHub actions tab and the log for that workflow. @@ -98,9 +99,9 @@ Additionally, make sure to differentiate between things in the testing notes tha This only needs to be done if this release is the last release of the feature plugin before code freeze in the WooCommerce core cycle. If this condition doesn't exist you can skip this section. * [ ] Remind whoever is porter this week to audit our codebase to ensure this [experimental interface document](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/blocks/feature-flags-and-experimental-interfaces.md) is up to date. See Pca54o-rM-p2 for more details. -* [ ] Create a pull request for updating the package in the [WooCommerce Core Repository](https://github.com/woocommerce/woocommerce/) that [bumps the package version](https://github.com/woocommerce/woocommerce/blob/master/composer.json) for the blocks package to the version being pulled in. - * The content for the pull release can follow [this example](https://github.com/woocommerce/woocommerce/pull/27676). Essentially you link to all the important things that have already been prepared. Note, you need to make sure you link to all the related documents for the feature plugin releases since the last package version bump in Woo Core. - * Please add a changelog to the content which is aggregated from all the releases included in the package bump. The changelog should only list things surfaced to users of the package in WooCommerce core (i.e. excluding things only available in the feature plugin or development builds). This changelog will be used in the release notes for the WooCommerce release. **Note: This currently is not shown in the linked example.** +* [ ] Create a pull request for updating the package in the [WooCommerce Core Repository](https://github.com/woocommerce/woocommerce/) that [bumps the package version](https://github.com/woocommerce/woocommerce/blob/747cb6b7184ba9fdc875ab104da5839cfda8b4be/plugins/woocommerce/composer.json) for the Woo Blocks package to the version being pulled in. + * The content for the pull release can follow [this example](https://github.com/woocommerce/woocommerce/pull/31556). Update the `plugins/woocommerce/composer.json` file and then run `composer update`. In the PR description you will link to all the important things that have already been prepared since the version you replaced. Note, you need to make sure you link to all the related documents for the plugin releases since the last package version bump in Woo Core. + * Please add a changelog to the content which is aggregated from all the releases included in the package bump. The changelog should only list things surfaced to users of the package in WooCommerce core (i.e. excluding things only available in the feature plugin or development builds). This changelog will be used in the release notes for the WooCommerce release. * Run through the testing checklist to ensure everything works in that branch for that package bump. **Note:** Testing should include ensuring any features/new blocks that are supposed to be behind feature gating for the core merge of this package update are working as expected. * Testing should include completing the [Smoke testing checklist](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/testing/smoke-testing.md). It's up to you to verify that those tests have been done. * Verify and make any additional edits to the pull request description for things like: Changelog to be included with WooCommerce core, additional communication that might be needed elsewhere, additional marketing communication notes that may be needed etc. diff --git a/.nvmrc b/.nvmrc index 07c142ffe20..23d9c36a118 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.13.1 +16.13.2 diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js b/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js index a08fe69ee38..ce3670bcd36 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js +++ b/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js @@ -18,6 +18,7 @@ const Simple = () => { quantity, minQuantity, maxQuantity, + multipleOf, dispatchActions, isDisabled, } = useAddToCartFormContext(); @@ -43,6 +44,7 @@ const Simple = () => { value={ quantity } min={ minQuantity } max={ maxQuantity } + step={ multipleOf } disabled={ isDisabled } onChange={ dispatchActions.setQuantity } /> diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js b/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js index fb7d707e306..4cc5e9e16fd 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js +++ b/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js @@ -23,6 +23,7 @@ const Variable = () => { quantity, minQuantity, maxQuantity, + multipleOf, dispatchActions, isDisabled, } = useAddToCartFormContext(); @@ -52,6 +53,7 @@ const Variable = () => { value={ quantity } min={ minQuantity } max={ maxQuantity } + step={ multipleOf } disabled={ isDisabled } onChange={ dispatchActions.setQuantity } /> diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart/shared/quantity-input.js b/assets/js/atomic/blocks/product-elements/add-to-cart/shared/quantity-input.js index 5f275ab98ac..a58a7250ada 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart/shared/quantity-input.js +++ b/assets/js/atomic/blocks/product-elements/add-to-cart/shared/quantity-input.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { useDebouncedCallback } from 'use-debounce'; + /** * Quantity Input Component. * @@ -5,10 +10,49 @@ * @param {boolean} props.disabled Whether input is disabled or not. * @param {number} props.min Minimum value for input. * @param {number} props.max Maximum value for input. + * @param {number} props.step Step attribute for input. * @param {number} props.value Value for input. * @param {function():any} props.onChange Function to call on input change event. */ -const QuantityInput = ( { disabled, min, max, value, onChange } ) => { +const QuantityInput = ( { disabled, min, max, step = 1, value, onChange } ) => { + const hasMaximum = typeof max !== 'undefined'; + + /** + * The goal of this function is to normalize what was inserted, + * but after the customer has stopped typing. + * + * It's important to wait before normalizing or we end up with + * a frustrating experience, for example, if the minimum is 2 and + * the customer is trying to type "10", premature normalizing would + * always kick in at "1" and turn that into 2. + * + * Copied from + */ + const normalizeQuantity = useDebouncedCallback( ( initialValue ) => { + // We copy the starting value. + let newValue = initialValue; + + // We check if we have a maximum value, and select the lowest between what was inserted and the maximum. + if ( hasMaximum ) { + newValue = Math.min( + newValue, + // the maximum possible value in step increments. + Math.floor( max / step ) * step + ); + } + + // Select the biggest between what's inserted, the the minimum value in steps. + newValue = Math.max( newValue, Math.ceil( min / step ) * step ); + + // We round off the value to our steps. + newValue = Math.floor( newValue / step ) * step; + + // Only commit if the value has changed + if ( newValue !== initialValue ) { + onChange( newValue ); + } + }, 300 ); + return ( { value={ value } min={ min } max={ max } + step={ step } hidden={ max === 1 } disabled={ disabled } onChange={ ( e ) => { onChange( e.target.value ); + normalizeQuantity( e.target.value ); } } /> ); diff --git a/assets/js/atomic/blocks/product-elements/summary/block.js b/assets/js/atomic/blocks/product-elements/summary/block.js index 967e63bd302..716bb7fc127 100644 --- a/assets/js/atomic/blocks/product-elements/summary/block.js +++ b/assets/js/atomic/blocks/product-elements/summary/block.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import Summary from '@woocommerce/base-components/summary'; import { blocksConfig } from '@woocommerce/block-settings'; + import { useInnerBlockLayoutContext, useProductDataContext, @@ -15,6 +16,10 @@ import { withProductDataContext } from '@woocommerce/shared-hocs'; * Internal dependencies */ import './style.scss'; +import { + useColorProps, + useTypographyProps, +} from '../../../../hooks/style-attributes'; /** * Product Summary Block Component. @@ -23,9 +28,13 @@ import './style.scss'; * @param {string} [props.className] CSS Class name for the component. * @return {*} The component. */ -const Block = ( { className } ) => { +const Block = ( props ) => { + const { className } = props; + const { parentClassName } = useInnerBlockLayoutContext(); const { product } = useProductDataContext(); + const colorProps = useColorProps( props ); + const typographyProps = useTypographyProps( props ); if ( ! product ) { return ( @@ -53,6 +62,7 @@ const Block = ( { className } ) => { { source={ source } maxLength={ 150 } countType={ blocksConfig.wordCountType || 'words' } + style={ { + ...colorProps.style, + ...typographyProps.style, + } } /> ); }; diff --git a/assets/js/atomic/blocks/product-elements/summary/edit.js b/assets/js/atomic/blocks/product-elements/summary/edit.js index 3a828dd2c84..05169265755 100644 --- a/assets/js/atomic/blocks/product-elements/summary/edit.js +++ b/assets/js/atomic/blocks/product-elements/summary/edit.js @@ -2,6 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,7 +12,12 @@ import withProductSelector from '../shared/with-product-selector'; import { BLOCK_TITLE, BLOCK_ICON } from './constants'; const Edit = ( { attributes } ) => { - return ; + const blockProps = useBlockProps(); + return ( +
+ +
+ ); }; export default withProductSelector( { diff --git a/assets/js/atomic/blocks/product-elements/summary/index.js b/assets/js/atomic/blocks/product-elements/summary/index.js index af193f9c882..32a31f09717 100644 --- a/assets/js/atomic/blocks/product-elements/summary/index.js +++ b/assets/js/atomic/blocks/product-elements/summary/index.js @@ -9,18 +9,23 @@ import { registerBlockType } from '@wordpress/blocks'; import sharedConfig from '../shared/config'; import attributes from './attributes'; import edit from './edit'; +import { supports } from './supports'; import { BLOCK_TITLE as title, BLOCK_ICON as icon, BLOCK_DESCRIPTION as description, } from './constants'; +import { Save } from './save'; const blockConfig = { + apiVersion: 2, title, description, icon: { src: icon }, attributes, + supports, edit, + save: Save, }; registerBlockType( 'woocommerce/product-summary', { diff --git a/assets/js/atomic/blocks/product-elements/summary/save.tsx b/assets/js/atomic/blocks/product-elements/summary/save.tsx new file mode 100644 index 00000000000..03a720e27df --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/summary/save.tsx @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; + +type Props = { + attributes: Record< string, unknown > & { + className?: string; + }; +}; + +export const Save = ( { attributes }: Props ): JSX.Element => { + return ( +
+ ); +}; diff --git a/assets/js/atomic/blocks/product-elements/summary/supports.js b/assets/js/atomic/blocks/product-elements/summary/supports.js new file mode 100644 index 00000000000..8d47e2bf6c1 --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/summary/supports.js @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { isFeaturePluginBuild } from '@woocommerce/block-settings'; + +export const supports = { + ...( isFeaturePluginBuild() && { + color: { + text: true, + background: false, + link: false, + }, + } ), + typography: { + fontSize: true, + }, +}; diff --git a/assets/js/atomic/blocks/product-elements/title/edit.tsx b/assets/js/atomic/blocks/product-elements/title/edit.tsx index 513b8e45f6c..edadb1ffcb2 100644 --- a/assets/js/atomic/blocks/product-elements/title/edit.tsx +++ b/assets/js/atomic/blocks/product-elements/title/edit.tsx @@ -20,6 +20,7 @@ import Block from './block'; import withProductSelector from '../shared/with-product-selector'; import { BLOCK_TITLE, BLOCK_ICON } from './constants'; import { Attributes } from './types'; +import './editor.scss'; interface Props { attributes: Attributes; diff --git a/assets/js/atomic/blocks/product-elements/title/editor.scss b/assets/js/atomic/blocks/product-elements/title/editor.scss new file mode 100644 index 00000000000..faeb1645153 --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/title/editor.scss @@ -0,0 +1,3 @@ +.editor-styles-wrapper a.wc-block-components-product-name { + color: inherit; +} diff --git a/assets/js/atomic/blocks/product-elements/title/index.ts b/assets/js/atomic/blocks/product-elements/title/index.ts index 6ac95ebc102..8b3ed0f43ab 100644 --- a/assets/js/atomic/blocks/product-elements/title/index.ts +++ b/assets/js/atomic/blocks/product-elements/title/index.ts @@ -28,6 +28,7 @@ const blockConfig: BlockConfiguration = { edit, save: Save, supports: { + ...sharedConfig.supports, ...( isFeaturePluginBuild() && { typography: { fontSize: true, @@ -36,8 +37,6 @@ const blockConfig: BlockConfiguration = { __experimentalTextTransform: true, __experimentalFontFamily: true, }, - } ), - ...( isFeaturePluginBuild() && { color: { text: true, background: true, @@ -45,15 +44,12 @@ const blockConfig: BlockConfiguration = { gradients: true, __experimentalSkipSerialization: true, }, - } ), - ...( isFeaturePluginBuild() && - hasSpacingStyleSupport() && { + ...( hasSpacingStyleSupport() && { spacing: { margin: true, __experimentalSkipSerialization: true, }, } ), + } ), }, }; - -registerBlockType( 'woocommerce/product-title', blockConfig ); diff --git a/assets/js/base/components/cart-checkout/shipping-rates-control-package/style.scss b/assets/js/base/components/cart-checkout/shipping-rates-control-package/style.scss index d8e590c2912..d3495745880 100644 --- a/assets/js/base/components/cart-checkout/shipping-rates-control-package/style.scss +++ b/assets/js/base/components/cart-checkout/shipping-rates-control-package/style.scss @@ -1,4 +1,5 @@ .wc-block-components-shipping-rates-control__package { + .wc-block-components-panel__button { margin-bottom: 0; margin-top: 0; @@ -41,3 +42,13 @@ content: ", "; white-space: pre; } + +// Target the shipping selection in checkout only, the Cart block has enough spacing because of the buttons on the panel. +.wc-block-checkout .wc-block-components-shipping-rates-control__package { + margin-bottom: em($gap-large); + + &:last-of-type { + margin-bottom: 0; + } + +} diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 516740236c4..1076c3c6dd4 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -4,8 +4,9 @@ import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import classNames from 'classnames'; -import { useCallback } from '@wordpress/element'; +import { useCallback, useLayoutEffect } from '@wordpress/element'; import { DOWN, UP } from '@wordpress/keycodes'; +import { useDebouncedCallback } from 'use-debounce'; /** * Internal dependencies @@ -31,6 +32,10 @@ export interface QuantitySelectorProps { * Maximum quantity */ maximum: number; + /** + * Input step attribute. + */ + step?: number; /** * Event handler triggered when the quantity is changed */ @@ -53,6 +58,7 @@ const QuantitySelector = ( { minimum = 1, maximum, onChange = () => void 0, + step = 1, itemName = '', disabled, }: QuantitySelectorProps ): JSX.Element => { @@ -62,8 +68,59 @@ const QuantitySelector = ( { ); const hasMaximum = typeof maximum !== 'undefined'; - const canDecrease = quantity > minimum; - const canIncrease = ! hasMaximum || quantity < maximum; + const canDecrease = quantity - step >= minimum; + const canIncrease = ! hasMaximum || quantity + step <= maximum; + + /** + * The goal of this function is to normalize what was inserted, + * but after the customer has stopped typing. + */ + const normalizeQuantity = useCallback( + ( initialValue: number ) => { + // We copy the starting value. + let value = initialValue; + + // We check if we have a maximum value, and select the lowest between what was inserted and the maximum. + if ( hasMaximum ) { + value = Math.min( + value, + // the maximum possible value in step increments. + Math.floor( maximum / step ) * step + ); + } + + // Select the biggest between what's inserted, the the minimum value in steps. + value = Math.max( value, Math.ceil( minimum / step ) * step ); + + // We round off the value to our steps. + value = Math.floor( value / step ) * step; + + // Only commit if the value has changed + if ( value !== initialValue ) { + onChange( value ); + } + }, + [ hasMaximum, maximum, minimum, onChange, step ] + ); + + /* + * It's important to wait before normalizing or we end up with + * a frustrating experience, for example, if the minimum is 2 and + * the customer is trying to type "10", premature normalizing would + * always kick in at "1" and turn that into 2. + */ + const debouncedNormalizeQuantity = useDebouncedCallback( + normalizeQuantity, + // This value is deliberately smaller than what's in useStoreCartItemQuantity so we don't end up with two requests. + 300 + ); + + /** + * Normalize qty on mount before render. + */ + useLayoutEffect( () => { + normalizeQuantity( quantity ); + }, [ quantity, normalizeQuantity ] ); /** * Handles keyboard up and down keys to change quantity value. @@ -83,15 +140,15 @@ const QuantitySelector = ( { if ( isArrowDown && canDecrease ) { event.preventDefault(); - onChange( quantity - 1 ); + onChange( quantity - step ); } if ( isArrowUp && canIncrease ) { event.preventDefault(); - onChange( quantity + 1 ); + onChange( quantity + step ); } }, - [ quantity, onChange, canIncrease, canDecrease ] + [ quantity, onChange, canIncrease, canDecrease, step ] ); return ( @@ -100,22 +157,23 @@ const QuantitySelector = ( { className="wc-block-components-quantity-selector__input" disabled={ disabled } type="number" - step="1" - min="0" + step={ step } + min={ minimum } + max={ maximum } value={ quantity } onKeyDown={ quantityInputOnKeyDown } onChange={ ( event ) => { - let value = - Number.isNaN( event.target.value ) || - ! event.target.value - ? 0 - : parseInt( event.target.value, 10 ); - if ( hasMaximum ) { - value = Math.min( value, maximum ); - } - value = Math.max( value, minimum ); + // Inputs values are strings, we parse them here. + let value = parseInt( event.target.value, 10 ); + // parseInt would throw NaN for anything not a number, + // so we revert value to the quantity value. + value = isNaN( value ) ? quantity : value; + if ( value !== quantity ) { + // we commit this value immediately. onChange( value ); + // but once the customer has stopped typing, we make sure his value is respecting the bounds (maximum value, minimum value, step value), and commit the normalized value. + debouncedNormalizeQuantity( value ); } } } aria-label={ sprintf( @@ -135,7 +193,7 @@ const QuantitySelector = ( { className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus" disabled={ disabled || ! canDecrease } onClick={ () => { - const newQuantity = quantity - 1; + const newQuantity = quantity - step; onChange( newQuantity ); speak( sprintf( @@ -147,6 +205,7 @@ const QuantitySelector = ( { newQuantity ) ); + normalizeQuantity( newQuantity ); } } > - @@ -159,7 +218,7 @@ const QuantitySelector = ( { disabled={ disabled || ! canIncrease } className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus" onClick={ () => { - const newQuantity = quantity + 1; + const newQuantity = quantity + step; onChange( newQuantity ); speak( sprintf( @@ -171,6 +230,7 @@ const QuantitySelector = ( { newQuantity ) ); + normalizeQuantity( newQuantity ); } } > + diff --git a/assets/js/base/components/quantity-selector/style.scss b/assets/js/base/components/quantity-selector/style.scss index 3a7095e9eea..a2ab45f775f 100644 --- a/assets/js/base/components/quantity-selector/style.scss +++ b/assets/js/base/components/quantity-selector/style.scss @@ -41,6 +41,7 @@ line-height: 1; vertical-align: middle; -moz-appearance: textfield; + font-weight: 600; &:focus { background: $gray-100; @@ -70,11 +71,12 @@ .wc-block-components-quantity-selector__button { @include reset-button; - @include font-size(regular); + @include font-size(regular, 0.9em); min-width: 30px; cursor: pointer; - color: $gray-900; + color: $gray-600; font-style: normal; + font-weight: normal; text-align: center; text-decoration: none; diff --git a/assets/js/base/components/summary/index.tsx b/assets/js/base/components/summary/index.tsx index 5dee7e5bf81..7d833a7300e 100644 --- a/assets/js/base/components/summary/index.tsx +++ b/assets/js/base/components/summary/index.tsx @@ -3,6 +3,7 @@ */ import { RawHTML, useMemo } from '@wordpress/element'; import { WordCountType } from '@woocommerce/block-settings'; +import { CSSProperties } from 'react'; /** * Internal dependencies @@ -14,6 +15,7 @@ interface SummaryProps { source: string; maxLength?: number; countType?: WordCountType; + style?: CSSProperties; } /** * Summary component. @@ -23,18 +25,25 @@ interface SummaryProps { * @param {number} props.maxLength Max length of the summary, using countType. * @param {string} props.countType One of words, characters_excluding_spaces, or characters_including_spaces. * @param {string} props.className Class name for rendered component. + * @param {CSSProperties} props.style Style Object for rendered component. + * */ export const Summary = ( { source, maxLength = 15, countType = 'words', className = '', + style = {}, }: SummaryProps ): JSX.Element => { const summaryText = useMemo( () => { return generateSummary( source, maxLength, countType ); }, [ source, maxLength, countType ] ); - return { summaryText }; + return ( + + { summaryText } + + ); }; export default Summary; diff --git a/assets/js/base/context/providers/add-to-cart-form/form-state/constants.js b/assets/js/base/context/providers/add-to-cart-form/form-state/constants.js index 9f3633ced93..c9ebea0b330 100644 --- a/assets/js/base/context/providers/add-to-cart-form/form-state/constants.js +++ b/assets/js/base/context/providers/add-to-cart-form/form-state/constants.js @@ -13,7 +13,7 @@ export const STATUS = { export const DEFAULT_STATE = { status: STATUS.PRISTINE, hasError: false, - quantity: 1, + quantity: 0, processingResponse: null, requestParams: {}, }; diff --git a/assets/js/base/context/providers/add-to-cart-form/form-state/index.js b/assets/js/base/context/providers/add-to-cart-form/form-state/index.js index 4a409eb5f9b..b8438f0b46b 100644 --- a/assets/js/base/context/providers/add-to-cart-form/form-state/index.js +++ b/assets/js/base/context/providers/add-to-cart-form/form-state/index.js @@ -296,9 +296,11 @@ export const AddToCartFormStateContextProvider = ( { productHasOptions: product.has_options || false, supportsFormElements, showFormElements: showFormElements && supportsFormElements, - quantity: addToCartFormState.quantity, - minQuantity: 1, - maxQuantity: product.quantity_limit || 99, + quantity: + addToCartFormState.quantity || product?.add_to_cart?.minimum || 1, + minQuantity: product?.add_to_cart?.minimum || 1, + maxQuantity: product?.add_to_cart?.maximum || 99, + multipleOf: product?.add_to_cart?.multiple_of || 1, requestParams: addToCartFormState.requestParams, isIdle: addToCartFormState.status === STATUS.IDLE, isDisabled: addToCartFormState.status === STATUS.DISABLED, diff --git a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx index 3fe19ab4f60..d760dbd9f3a 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx +++ b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx @@ -195,14 +195,14 @@ export const PaymentMethodDataProvider = ( { )[ 0 ] || undefined; if ( customerPaymentMethod ) { - const token = customerPaymentMethod.tokenId; + const token = customerPaymentMethod.tokenId.toString(); const paymentMethodSlug = customerPaymentMethod.method.gateway; const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`; dispatchActions.setActivePaymentMethod( paymentMethodSlug, { token, payment_method: paymentMethodSlug, - [ savedTokenKey ]: token.toString(), + [ savedTokenKey ]: token, isSavedToken: true, } ); return; diff --git a/assets/js/base/context/providers/cart-checkout/payment-methods/test/payment-method-data-context.js b/assets/js/base/context/providers/cart-checkout/payment-methods/test/payment-method-data-context.js index f2e145fb1bb..740459ab9f6 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-methods/test/payment-method-data-context.js +++ b/assets/js/base/context/providers/cart-checkout/payment-methods/test/payment-method-data-context.js @@ -44,7 +44,7 @@ jest.mock( '@woocommerce/settings', () => { cc: [ { method: { - gateway: 'stripe', + gateway: 'credit-card', last4: '4242', brand: 'Visa', }, @@ -75,7 +75,7 @@ const registerMockPaymentMethods = ( savedCards = true ) => { ariaLabel: name, } ); } ); - [ 'stripe' ].forEach( ( name ) => { + [ 'credit-card' ].forEach( ( name ) => { registerPaymentMethod( { name, label: name, @@ -121,7 +121,7 @@ const registerMockPaymentMethods = ( savedCards = true ) => { }; const resetMockPaymentMethods = () => { - [ 'cheque', 'bacs', 'stripe' ].forEach( ( name ) => { + [ 'cheque', 'bacs', 'credit-card' ].forEach( ( name ) => { __experimentalDeRegisterPaymentMethod( name ); } ); [ 'express-payment' ].forEach( ( name ) => { @@ -255,8 +255,8 @@ describe( 'Testing Payment Method Data Context Provider with saved cards turned void null } /> { 'Active Payment Method: ' + activePaymentMethod } - { paymentMethodData[ 'wc-stripe-payment-token' ] && ( - Stripe token + { paymentMethodData[ 'wc-credit-card-payment-token' ] && ( + credit-card token ) } ); @@ -276,11 +276,11 @@ describe( 'Testing Payment Method Data Context Provider with saved cards turned // Should initialize by default the default saved payment method. await waitFor( () => { const activePaymentMethod = screen.queryByText( - /Active Payment Method: stripe/ + /Active Payment Method: credit-card/ ); - const stripeToken = screen.queryByText( /Stripe token/ ); + const creditCardToken = screen.queryByText( /credit-card token/ ); expect( activePaymentMethod ).not.toBeNull(); - expect( stripeToken ).not.toBeNull(); + expect( creditCardToken ).not.toBeNull(); } ); act( () => { @@ -294,9 +294,9 @@ describe( 'Testing Payment Method Data Context Provider with saved cards turned const activePaymentMethod = screen.queryByText( /Active Payment Method: express-payment/ ); - const stripeToken = screen.queryByText( /Stripe token/ ); + const creditCardToken = screen.queryByText( /credit-card token/ ); expect( activePaymentMethod ).not.toBeNull(); - expect( stripeToken ).toBeNull(); + expect( creditCardToken ).toBeNull(); } ); act( () => { @@ -310,11 +310,11 @@ describe( 'Testing Payment Method Data Context Provider with saved cards turned await waitFor( () => { const activePaymentMethod = screen.queryByText( - /Active Payment Method: stripe/ + /Active Payment Method: credit-card/ ); - const stripeToken = screen.queryByText( /Stripe token/ ); + const creditCardToken = screen.queryByText( /credit-card token/ ); expect( activePaymentMethod ).not.toBeNull(); - expect( stripeToken ).not.toBeNull(); + expect( creditCardToken ).not.toBeNull(); } ); } ); } ); diff --git a/assets/js/base/context/providers/cart-checkout/payment-methods/use-payment-method-registration.ts b/assets/js/base/context/providers/cart-checkout/payment-methods/use-payment-method-registration.ts index 58c2de1d44e..9a1ac7274ed 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-methods/use-payment-method-registration.ts +++ b/assets/js/base/context/providers/cart-checkout/payment-methods/use-payment-method-registration.ts @@ -135,7 +135,7 @@ const usePaymentMethodRegistration = ( } catch ( e ) { if ( CURRENT_USER_IS_ADMIN || isEditor ) { const errorText = sprintf( - /* translators: %s the id of the payment method being registered (bank transfer, Stripe...) */ + /* translators: %s the id of the payment method being registered (bank transfer, cheque...) */ __( `There was an error registering the payment method with id '%s': `, 'woo-gutenberg-products-block' @@ -153,8 +153,7 @@ const usePaymentMethodRegistration = ( // Re-dispatch available payment methods to store. dispatcher( availablePaymentMethods ); - // Note: some payment methods use the `canMakePayment` callback to initialize / setup. - // Example: Stripe CC, Stripe Payment Request. + // Note: Some 4rd party payment methods use the `canMakePayment` callback to initialize / setup. // That's why we track "is initialized" state here. setIsInitialized( true ); }, [ diff --git a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx index 94bc2b0e05d..26894b8250a 100644 --- a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx +++ b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx @@ -70,7 +70,13 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( description: fullDescription = '', low_stock_remaining: lowStockRemaining = null, show_backorder_badge: showBackorderBadge = false, - quantity_limit: quantityLimit = 99, + quantity_limits: quantityLimits = { + minimum: 1, + maximum: 99, + multiple_of: 1, + editable: true, + }, + sold_individually: soldIndividually = false, permalink = '', images = [], variation = [], @@ -278,19 +284,26 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( />
- { - setItemQuantity( newQuantity ); - dispatchStoreEvent( 'cart-set-item-quantity', { - product: lineItem, - quantity: newQuantity, - } ); - } } - itemName={ name } - /> + { ! soldIndividually && !! quantityLimits.editable && ( + { + setItemQuantity( newQuantity ); + dispatchStoreEvent( + 'cart-set-item-quantity', + { + product: lineItem, + quantity: newQuantity, + } + ); + } } + itemName={ name } + /> + ) } @@ -43,7 +43,7 @@ jest.mock( ); const registerMockPaymentMethods = () => { - [ 'stripe' ].forEach( ( name ) => { + [ 'credit-card' ].forEach( ( name ) => { registerPaymentMethod( { name, label: name, @@ -62,7 +62,7 @@ const registerMockPaymentMethods = () => { }; const resetMockPaymentMethods = () => { - [ 'stripe' ].forEach( ( name ) => { + [ 'credit-card' ].forEach( ( name ) => { __experimentalDeRegisterPaymentMethod( name ); } ); }; @@ -137,7 +137,7 @@ describe( 'PaymentMethods', () => { expect( savedPaymentMethodOptions ).not.toBeNull(); expect( paymentMethodOptions ).not.toBeNull(); const savedToken = screen.queryByText( - /Active Payment Method: stripe/ + /Active Payment Method: credit-card/ ); expect( savedToken ).toBeNull(); } ); @@ -146,7 +146,7 @@ describe( 'PaymentMethods', () => { await waitFor( () => { const activePaymentMethod = screen.queryByText( - /Active Payment Method: stripe/ + /Active Payment Method: credit-card/ ); expect( activePaymentMethod ).not.toBeNull(); } ); diff --git a/assets/js/blocks/cart-checkout/shared/use-view-switcher.tsx b/assets/js/blocks/cart-checkout/shared/use-view-switcher.tsx index d9a92290c96..dc25faf363e 100644 --- a/assets/js/blocks/cart-checkout/shared/use-view-switcher.tsx +++ b/assets/js/blocks/cart-checkout/shared/use-view-switcher.tsx @@ -14,6 +14,10 @@ interface View { icon: string | JSX.Element; } +function getView( viewName: string, views: View[] ) { + return views.find( ( view ) => view.view === viewName ); +} + export const useViewSwitcher = ( clientId: string, views: View[] @@ -32,7 +36,25 @@ export const useViewSwitcher = ( const selectedBlockClientId = getSelectedBlockClientId(); useEffect( () => { + const selectedBlock = getBlock( selectedBlockClientId ); + + if ( ! selectedBlock ) { + return; + } + + if ( currentView.view === selectedBlock.name ) { + return; + } + const viewNames = views.map( ( { view } ) => view ); + + if ( viewNames.includes( selectedBlock.name ) ) { + const newView = getView( selectedBlock.name, views ); + if ( newView ) { + return setCurrentView( newView ); + } + } + const parentBlockIds = getBlockParentsByBlockName( selectedBlockClientId, viewNames @@ -47,15 +69,11 @@ export const useViewSwitcher = ( return; } - const filteredViews = views.filter( - ( { view } ) => view === parentBlock.name - ); + const newView = getView( parentBlock.name, views ); - if ( filteredViews.length !== 1 ) { - return; + if ( newView ) { + setCurrentView( newView ); } - - setCurrentView( filteredViews[ 0 ] ); }, [ getBlockParentsByBlockName, selectedBlockClientId, diff --git a/assets/js/blocks/legacy-template/index.tsx b/assets/js/blocks/legacy-template/index.tsx index fe973e65f10..c37165d5be3 100644 --- a/assets/js/blocks/legacy-template/index.tsx +++ b/assets/js/blocks/legacy-template/index.tsx @@ -82,7 +82,7 @@ registerBlockType( 'woocommerce/legacy-template', { 'woo-gutenberg-products-block' ), supports: { - align: false, + align: [ 'wide', 'full' ], html: false, multiple: false, reusable: false, diff --git a/assets/js/blocks/product-category/block.js b/assets/js/blocks/product-category/block.js index 66f7fef63f3..8e96617ff47 100644 --- a/assets/js/blocks/product-category/block.js +++ b/assets/js/blocks/product-category/block.js @@ -18,6 +18,7 @@ import GridContentControl from '@woocommerce/editor-components/grid-content-cont import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { Icon, folder } from '@woocommerce/icons'; import { getSetting } from '@woocommerce/settings'; @@ -119,6 +120,7 @@ class ProductByCategoryBlock extends Component { orderby, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -186,6 +188,18 @@ class ProductByCategoryBlock extends Component { value={ orderby } /> + + + ); } diff --git a/assets/js/blocks/product-new/block.js b/assets/js/blocks/product-new/block.js index 6cb4b7dc8f9..1c26d0ac944 100644 --- a/assets/js/blocks/product-new/block.js +++ b/assets/js/blocks/product-new/block.js @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import GridContentControl from '@woocommerce/editor-components/grid-content-control'; import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -26,6 +27,7 @@ class ProductNewestBlock extends Component { contentVisibility, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -56,6 +58,18 @@ class ProductNewestBlock extends Component { } /> + + + + + + ); } diff --git a/assets/js/blocks/product-tag/block.js b/assets/js/blocks/product-tag/block.js index 5d82c4879d9..90d858e9b8d 100644 --- a/assets/js/blocks/product-tag/block.js +++ b/assets/js/blocks/product-tag/block.js @@ -18,6 +18,7 @@ import GridContentControl from '@woocommerce/editor-components/grid-content-cont import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductTagControl from '@woocommerce/editor-components/product-tag-control'; import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { Icon, more } from '@woocommerce/icons'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -91,6 +92,7 @@ class ProductsByTagBlock extends Component { orderby, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -150,6 +152,18 @@ class ProductsByTagBlock extends Component { value={ orderby } /> + + + ); } diff --git a/assets/js/blocks/product-tag/index.js b/assets/js/blocks/product-tag/index.js index 87056fbc579..ba1f606b14a 100644 --- a/assets/js/blocks/product-tag/index.js +++ b/assets/js/blocks/product-tag/index.js @@ -109,6 +109,14 @@ registerBlockType( 'woocommerce/product-tag', { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'array', + default: getSetting( 'stockStatusOptions', [] ), + }, }, /** diff --git a/assets/js/blocks/product-top-rated/block.js b/assets/js/blocks/product-top-rated/block.js index 795ea3e384f..98968ab9e70 100644 --- a/assets/js/blocks/product-top-rated/block.js +++ b/assets/js/blocks/product-top-rated/block.js @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import GridContentControl from '@woocommerce/editor-components/grid-content-control'; import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -26,6 +27,7 @@ class ProductTopRatedBlock extends Component { contentVisibility, rows, alignButtons, + stockStatus, } = attributes; return ( @@ -75,6 +77,18 @@ class ProductTopRatedBlock extends Component { } /> + + + ); } diff --git a/assets/js/blocks/products-by-attribute/block.js b/assets/js/blocks/products-by-attribute/block.js index 62a7b472e4e..e7cd6d82f91 100644 --- a/assets/js/blocks/products-by-attribute/block.js +++ b/assets/js/blocks/products-by-attribute/block.js @@ -19,6 +19,7 @@ import GridContentControl from '@woocommerce/editor-components/grid-content-cont import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control'; import ProductAttributeTermControl from '@woocommerce/editor-components/product-attribute-term-control'; import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control'; +import ProductStockControl from '@woocommerce/editor-components/product-stock-control'; import { gridBlockPreview } from '@woocommerce/resource-previews'; import { getSetting } from '@woocommerce/settings'; @@ -36,6 +37,7 @@ class ProductsByAttributeBlock extends Component { orderby, rows, alignButtons, + stockStatus, } = this.props.attributes; return ( @@ -100,6 +102,18 @@ class ProductsByAttributeBlock extends Component { value={ orderby } /> + + + ); } diff --git a/assets/js/blocks/products-by-attribute/index.js b/assets/js/blocks/products-by-attribute/index.js index 6f9b7c798aa..abaed9c437c 100644 --- a/assets/js/blocks/products-by-attribute/index.js +++ b/assets/js/blocks/products-by-attribute/index.js @@ -116,6 +116,14 @@ registerBlockType( blockTypeName, { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'string', + default: getSetting( 'stockStatusOptions', [] ), + }, }, /** diff --git a/assets/js/editor-components/product-stock-control/index.tsx b/assets/js/editor-components/product-stock-control/index.tsx new file mode 100644 index 00000000000..553f27c2b70 --- /dev/null +++ b/assets/js/editor-components/product-stock-control/index.tsx @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import CheckboxList from '@woocommerce/base-components/checkbox-list'; +import { getSetting } from '@woocommerce/settings'; +import { useCallback, useState } from '@wordpress/element'; + +export interface ProductStockControlProps { + value: Array< string >; + setAttributes: ( attributes: Record< string, unknown > ) => void; +} + +/** + * A pre-configured SelectControl for product stock settings. + */ +const ProductStockControl = ( { + value, + setAttributes, +}: ProductStockControlProps ): JSX.Element => { + // Should out of stock items be hidden? + const [ hideOutOfStockItems ] = useState( + getSetting( 'hideOutOfStockItems', false ) + ); + + // Get the stock status options. + const [ { outofstock, ...otherStockStatusOptions } ] = useState( + getSetting( 'stockStatusOptions', {} ) + ); + + // Determine whether or not to use the out of stock status. + const [ STOCK_STATUS_OPTIONS ] = useState( + hideOutOfStockItems + ? otherStockStatusOptions + : { outofstock, ...otherStockStatusOptions } + ); + + // Set the initial state to the default or saved value. + const [ checkedOptions, setChecked ] = useState( value ); + + /** + * Valid options must be in an array of [ 'value' : 'mystatus', 'label' : 'My label' ] format. + * stockStatusOptions are returned as [ 'mystatus' : 'My label' ]. + * Formatting is corrected here. + */ + const [ displayOptions ] = useState( + Object.entries( STOCK_STATUS_OPTIONS ) + .map( ( [ slug, name ] ) => ( { value: slug, label: name } ) ) + .filter( ( status ) => !! status.label ) + .sort( ( a, b ) => a.value.localeCompare( b.value ) ) + ); + + /** + * When a checkbox in the list changes, update state. + */ + const onChange = useCallback( + ( checkedValue: string ) => { + const previouslyChecked = checkedOptions.includes( checkedValue ); + + const newChecked = checkedOptions.filter( + ( filteredValue ) => filteredValue !== checkedValue + ); + + if ( ! previouslyChecked ) { + newChecked.push( checkedValue ); + newChecked.sort(); + } + + setChecked( newChecked ); + setAttributes( { + stockStatus: newChecked, + } ); + }, + [ checkedOptions, setAttributes ] + ); + + return ( + + ); +}; + +export default ProductStockControl; diff --git a/assets/js/hooks/style-attributes.ts b/assets/js/hooks/style-attributes.ts index 509a82dc6c2..6a38aa3e86f 100644 --- a/assets/js/hooks/style-attributes.ts +++ b/assets/js/hooks/style-attributes.ts @@ -5,7 +5,10 @@ import { __experimentalUseColorProps, __experimentalGetSpacingClassesAndStyles, +<<<<<<< HEAD __experimentalUseBorderProps, +======= +>>>>>>> 22b2cebe1c8fd44bace5c0f34f3641246b8be9bd } from '@wordpress/block-editor'; /** diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/constants.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/constants.js deleted file mode 100644 index 24e1e1d470a..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const PAYMENT_METHOD_NAME = 'stripe'; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/elements.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/elements.js deleted file mode 100644 index 434a1459b95..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/elements.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * External dependencies - */ -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { - CardElement, - CardNumberElement, - CardExpiryElement, - CardCvcElement, -} from '@stripe/react-stripe-js'; - -/** - * Internal dependencies - */ -import { useElementOptions } from './use-element-options'; - -/** @typedef {import('react')} React */ - -const baseTextInputStyles = 'wc-block-gateway-input'; - -/** - * InlineCard component - * - * @param {Object} props Incoming props for the component. - * @param {React.ReactElement} props.inputErrorComponent - * @param {function(any):any} props.onChange - */ -export const InlineCard = ( { - inputErrorComponent: ValidationInputError, - onChange, -} ) => { - const [ isEmpty, setIsEmpty ] = useState( true ); - const { options, onActive, error, setError } = useElementOptions( { - hidePostalCode: true, - } ); - const errorCallback = ( event ) => { - if ( event.error ) { - setError( event.error.message ); - } else { - setError( '' ); - } - setIsEmpty( event.empty ); - onChange( event ); - }; - return ( - <> -
- onActive( isEmpty ) } - onFocus={ () => onActive( isEmpty ) } - onChange={ errorCallback } - /> - -
- - - ); -}; - -/** - * CardElements component. - * - * @param {Object} props - * @param {function(any):any} props.onChange - * @param {React.ReactElement} props.inputErrorComponent - */ -export const CardElements = ( { - onChange, - inputErrorComponent: ValidationInputError, -} ) => { - const [ isEmpty, setIsEmpty ] = useState( { - cardNumber: true, - cardExpiry: true, - cardCvc: true, - } ); - const { - options: cardNumOptions, - onActive: cardNumOnActive, - error: cardNumError, - setError: cardNumSetError, - } = useElementOptions( { showIcon: false } ); - const { - options: cardExpiryOptions, - onActive: cardExpiryOnActive, - error: cardExpiryError, - setError: cardExpirySetError, - } = useElementOptions(); - const { - options: cardCvcOptions, - onActive: cardCvcOnActive, - error: cardCvcError, - setError: cardCvcSetError, - } = useElementOptions(); - const errorCallback = ( errorSetter, elementId ) => ( event ) => { - if ( event.error ) { - errorSetter( event.error.message ); - } else { - errorSetter( '' ); - } - setIsEmpty( { ...isEmpty, [ elementId ]: event.empty } ); - onChange( event ); - }; - return ( -
-
- cardNumOnActive( isEmpty.cardNumber ) } - onBlur={ () => cardNumOnActive( isEmpty.cardNumber ) } - /> - - -
-
- cardExpiryOnActive( isEmpty.cardExpiry ) } - onBlur={ () => cardExpiryOnActive( isEmpty.cardExpiry ) } - id="wc-stripe-card-expiry-element" - /> - - -
-
- cardCvcOnActive( isEmpty.cardCvc ) } - onBlur={ () => cardCvcOnActive( isEmpty.cardCvc ) } - id="wc-stripe-card-code-element" - /> - - -
-
- ); -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/index.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/index.js deleted file mode 100644 index 5ca9889fd42..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/index.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useEffect, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { getStripeServerData, loadStripe } from '../stripe-utils'; -import { StripeCreditCard, getStripeCreditCardIcons } from './payment-method'; -import { PAYMENT_METHOD_NAME } from './constants'; - -const stripePromise = loadStripe(); - -const StripeComponent = ( props ) => { - const [ errorMessage, setErrorMessage ] = useState( '' ); - - useEffect( () => { - Promise.resolve( stripePromise ).then( ( { error } ) => { - if ( error ) { - setErrorMessage( error.message ); - } - } ); - }, [ setErrorMessage ] ); - - useEffect( () => { - if ( errorMessage ) { - throw new Error( errorMessage ); - } - }, [ errorMessage ] ); - - return ; -}; - -const StripeLabel = ( props ) => { - const { PaymentMethodLabel } = props.components; - - const labelText = getStripeServerData().title - ? getStripeServerData().title - : __( 'Credit / Debit Card', 'woo-gutenberg-products-block' ); - - return ; -}; - -const cardIcons = getStripeCreditCardIcons(); -const stripeCcPaymentMethod = { - name: PAYMENT_METHOD_NAME, - label: , - content: , - edit: , - icons: cardIcons, - canMakePayment: () => stripePromise, - ariaLabel: __( - 'Stripe Credit Card payment method', - 'woo-gutenberg-products-block' - ), - supports: { - showSavedCards: getStripeServerData().showSavedCards, - showSaveOption: getStripeServerData().showSaveOption, - features: getStripeServerData()?.supports ?? [], - }, -}; - -export default stripeCcPaymentMethod; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/payment-method.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/payment-method.js deleted file mode 100644 index f0b80c5a7f9..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/payment-method.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * External dependencies - */ -import { Elements, useStripe } from '@stripe/react-stripe-js'; -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { getStripeServerData } from '../stripe-utils'; -import { useCheckoutSubscriptions } from './use-checkout-subscriptions'; -import { InlineCard, CardElements } from './elements'; - -/** - * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe - * @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest - * @typedef {import('@woocommerce/type-defs/payment-method-interface').PaymentMethodInterface} RegisteredPaymentMethodProps - */ - -export const getStripeCreditCardIcons = () => { - return Object.entries( getStripeServerData().icons ).map( - ( [ id, { src, alt } ] ) => { - return { - id, - src, - alt, - }; - } - ); -}; - -/** - * Stripe Credit Card component - * - * @param {RegisteredPaymentMethodProps} props Incoming props - */ -const CreditCardComponent = ( { - billing, - eventRegistration, - emitResponse, - components, -} ) => { - const { ValidationInputError, PaymentMethodIcons } = components; - const [ sourceId, setSourceId ] = useState( '' ); - const stripe = useStripe(); - const onStripeError = useCheckoutSubscriptions( - eventRegistration, - billing, - sourceId, - setSourceId, - emitResponse, - stripe - ); - const onChange = ( paymentEvent ) => { - if ( paymentEvent.error ) { - onStripeError( paymentEvent ); - } - setSourceId( '0' ); - }; - const cardIcons = getStripeCreditCardIcons(); - - const renderedCardElement = getStripeServerData().inline_cc_form ? ( - - ) : ( - - ); - return ( - <> - { renderedCardElement } - { PaymentMethodIcons && cardIcons.length && ( - - ) } - - ); -}; - -export const StripeCreditCard = ( props ) => { - const { locale } = getStripeServerData().button; - const { stripe } = props; - - return ( - - - - ); -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-checkout-subscriptions.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-checkout-subscriptions.js deleted file mode 100644 index 6b5555a2551..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-checkout-subscriptions.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useCallback, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { getErrorMessageForTypeAndCode } from '../stripe-utils'; -import { usePaymentIntents } from './use-payment-intents'; -import { usePaymentProcessing } from './use-payment-processing'; - -/** - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EventRegistrationProps} EventRegistrationProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').BillingDataProps} BillingDataProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EmitResponseProps} EmitResponseProps - * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe - * @typedef {import('react').Dispatch} SourceIdDispatch - */ - -/** - * A custom hook for the Stripe processing and event observer logic. - * - * @param {EventRegistrationProps} eventRegistration Event registration functions. - * @param {BillingDataProps} billing Various billing data items. - * @param {string} sourceId Current set stripe source id. - * @param {SourceIdDispatch} setSourceId Setter for stripe source id. - * @param {EmitResponseProps} emitResponse Various helpers for usage with observer - * response objects. - * @param {Stripe} stripe The stripe.js object. - * - * @return {function(Object):Object} Returns a function for handling stripe error. - */ -export const useCheckoutSubscriptions = ( - eventRegistration, - billing, - sourceId, - setSourceId, - emitResponse, - stripe -) => { - const [ error, setError ] = useState( '' ); - const onStripeError = useCallback( ( event ) => { - const type = event.error.type; - const code = event.error.code || ''; - const message = - getErrorMessageForTypeAndCode( type, code ) ?? event.error.message; - setError( message ); - return message; - }, [] ); - const { - onCheckoutAfterProcessingWithSuccess, - onPaymentProcessing, - onCheckoutAfterProcessingWithError, - } = eventRegistration; - usePaymentIntents( - stripe, - onCheckoutAfterProcessingWithSuccess, - setSourceId, - emitResponse - ); - usePaymentProcessing( - onStripeError, - error, - stripe, - billing, - emitResponse, - sourceId, - setSourceId, - onPaymentProcessing - ); - // hook into and register callbacks for events. - useEffect( () => { - const onError = ( { processingResponse } ) => { - if ( processingResponse?.paymentDetails?.errorMessage ) { - return { - type: emitResponse.responseTypes.ERROR, - message: processingResponse.paymentDetails.errorMessage, - messageContext: emitResponse.noticeContexts.PAYMENTS, - }; - } - // so we don't break the observers. - return true; - }; - const unsubscribeAfterProcessing = onCheckoutAfterProcessingWithError( - onError - ); - return () => { - unsubscribeAfterProcessing(); - }; - }, [ - onCheckoutAfterProcessingWithError, - emitResponse.noticeContexts.PAYMENTS, - emitResponse.responseTypes.ERROR, - ] ); - return onStripeError; -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-element-options.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-element-options.js deleted file mode 100644 index 17243c9a806..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-element-options.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * External dependencies - */ -import { useState, useEffect, useCallback } from '@wordpress/element'; - -/** - * @typedef {import('../stripe-utils/type-defs').StripeElementOptions} StripeElementOptions - */ - -/** - * Returns the value of a specific CSS property for the element matched by the provided selector. - * - * @param {string} selector CSS selector that matches the element to query. - * @param {string} property Name of the property to retrieve the style - * value from. - * @param {string} defaultValue Fallback value if the value for the property - * could not be retrieved. - * - * @return {string} The style value of that property in the document element. - */ -const getComputedStyle = ( selector, property, defaultValue ) => { - let elementStyle = {}; - - if ( - typeof document === 'object' && - typeof document.querySelector === 'function' && - typeof window.getComputedStyle === 'function' - ) { - const element = document.querySelector( selector ); - if ( element ) { - elementStyle = window.getComputedStyle( element ); - } - } - - return elementStyle[ property ] || defaultValue; -}; - -/** - * Default options for the stripe elements. - */ -const elementOptions = { - style: { - base: { - iconColor: '#666EE8', - color: '#31325F', - fontSize: getComputedStyle( - '.wc-block-checkout', - 'fontSize', - '16px' - ), - lineHeight: 1.375, // With a font-size of 16px, line-height will be 22px. - '::placeholder': { - color: '#fff', - }, - }, - }, - classes: { - focus: 'focused', - empty: 'empty', - invalid: 'has-error', - }, -}; - -/** - * A custom hook handling options implemented on the stripe elements. - * - * @param {Object} [overloadedOptions] An array of extra options to merge with - * the options provided for the element. - * - * @return {StripeElementOptions} The stripe element options interface - */ -export const useElementOptions = ( overloadedOptions ) => { - const [ isActive, setIsActive ] = useState( false ); - const [ options, setOptions ] = useState( { - ...elementOptions, - ...overloadedOptions, - } ); - const [ error, setError ] = useState( '' ); - - useEffect( () => { - const color = isActive ? '#CFD7E0' : '#fff'; - - setOptions( ( prevOptions ) => { - const showIcon = - typeof prevOptions.showIcon !== 'undefined' - ? { showIcon: isActive } - : {}; - return { - ...prevOptions, - style: { - ...prevOptions.style, - base: { - ...prevOptions.style.base, - '::placeholder': { - color, - }, - }, - }, - ...showIcon, - }; - } ); - }, [ isActive ] ); - - const onActive = useCallback( - ( isEmpty ) => { - if ( ! isEmpty ) { - setIsActive( true ); - } else { - setIsActive( ( prevActive ) => ! prevActive ); - } - }, - [ setIsActive ] - ); - return { options, onActive, error, setError }; -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-intents.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-intents.js deleted file mode 100644 index 86a386e72fd..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-intents.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * External dependencies - */ -import { useEffect } from '@wordpress/element'; - -/** - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EmitResponseProps} EmitResponseProps - * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe - */ - -/** - * Opens the modal for PaymentIntent authorizations. - * - * @param {Object} params Params object. - * @param {Stripe} params.stripe The stripe object. - * @param {Object} params.paymentDetails The payment details from the - * server after checkout processing. - * @param {string} params.errorContext Context where errors will be added. - * @param {string} params.errorType Type of error responses. - * @param {string} params.successType Type of success responses. - */ -const openIntentModal = ( { - stripe, - paymentDetails, - errorContext, - errorType, - successType, -} ) => { - const checkoutResponse = { type: successType }; - if ( - ! paymentDetails.setup_intent && - ! paymentDetails.payment_intent_secret - ) { - return checkoutResponse; - } - const isSetupIntent = !! paymentDetails.setupIntent; - const verificationUrl = paymentDetails.verification_endpoint; - const intentSecret = isSetupIntent - ? paymentDetails.setup_intent - : paymentDetails.payment_intent_secret; - return stripe[ isSetupIntent ? 'confirmCardSetup' : 'confirmCardPayment' ]( - intentSecret - ) - .then( function ( response ) { - if ( response.error ) { - throw response.error; - } - const intent = - response[ isSetupIntent ? 'setupIntent' : 'paymentIntent' ]; - if ( - intent.status !== 'requires_capture' && - intent.status !== 'succeeded' - ) { - return checkoutResponse; - } - checkoutResponse.redirectUrl = verificationUrl; - return checkoutResponse; - } ) - .catch( function ( error ) { - checkoutResponse.type = errorType; - checkoutResponse.message = error.message; - checkoutResponse.retry = true; - checkoutResponse.messageContext = errorContext; - // Reports back to the server. - window.fetch( verificationUrl + '&is_ajax' ); - return checkoutResponse; - } ); -}; - -export const usePaymentIntents = ( - stripe, - subscriber, - setSourceId, - emitResponse -) => { - useEffect( () => { - const unsubscribe = subscriber( async ( { processingResponse } ) => { - const paymentDetails = processingResponse.paymentDetails || {}; - const response = await openIntentModal( { - stripe, - paymentDetails, - errorContext: emitResponse.noticeContexts.PAYMENTS, - errorType: emitResponse.responseTypes.ERROR, - successType: emitResponse.responseTypes.SUCCESS, - } ); - if ( - response.type === emitResponse.responseTypes.ERROR && - response.retry - ) { - setSourceId( '0' ); - } - - return response; - } ); - return () => unsubscribe(); - }, [ - subscriber, - emitResponse.noticeContexts.PAYMENTS, - emitResponse.responseTypes.ERROR, - emitResponse.responseTypes.SUCCESS, - setSourceId, - stripe, - ] ); -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-processing.js b/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-processing.js deleted file mode 100644 index 8054d5cec54..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-processing.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * External dependencies - */ -import { useEffect } from '@wordpress/element'; -import { - CardElement, - CardNumberElement, - useElements, -} from '@stripe/react-stripe-js'; - -/** - * Internal dependencies - */ -import { PAYMENT_METHOD_NAME } from './constants'; -import { - getStripeServerData, - getErrorMessageForTypeAndCode, -} from '../stripe-utils'; -import { errorTypes } from '../stripe-utils/constants'; - -/** - * @typedef {import('@stripe/stripe-js').Stripe} Stripe - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EventRegistrationProps} EventRegistrationProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').BillingDataProps} BillingDataProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EmitResponseProps} EmitResponseProps - * @typedef {import('react').Dispatch} SourceIdDispatch - */ - -/** - * @typedef {function(function():any):function():void} EventRegistration - */ - -/** - * A custom hook that registers stripe payment processing with the - * onPaymentProcessing event from checkout. - * - * @param {function(any):string} onStripeError Sets an error for stripe. - * @param {string} error Any set error message (an empty string if no - * error). - * @param {Stripe} stripe The stripe utility - * @param {BillingDataProps} billing Various billing data items. - * @param {EmitResponseProps} emitResponse Various helpers for usage with observer - * response objects. - * @param {string} sourceId Current set stripe source id. - * @param {SourceIdDispatch} setSourceId Setter for stripe source id. - * @param {EventRegistration} onPaymentProcessing The event emitter for processing payment. - */ -export const usePaymentProcessing = ( - onStripeError, - error, - stripe, - billing, - emitResponse, - sourceId, - setSourceId, - onPaymentProcessing -) => { - const elements = useElements(); - // hook into and register callbacks for events - useEffect( () => { - const createSource = async ( ownerInfo ) => { - const elementToGet = getStripeServerData().inline_cc_form - ? CardElement - : CardNumberElement; - return await stripe.createSource( - // @ts-ignore - elements?.getElement( elementToGet ), - { - type: 'card', - owner: ownerInfo, - } - ); - }; - const onSubmit = async () => { - try { - const billingData = billing.billingData; - // if there's an error return that. - if ( error ) { - return { - type: emitResponse.responseTypes.ERROR, - message: error, - }; - } - // use token if it's set. - if ( sourceId !== '' && sourceId !== '0' ) { - return { - type: emitResponse.responseTypes.SUCCESS, - meta: { - paymentMethodData: { - paymentMethod: PAYMENT_METHOD_NAME, - paymentRequestType: 'cc', - stripe_source: sourceId, - }, - billingData, - }, - }; - } - const ownerInfo = { - address: { - line1: billingData.address_1, - line2: billingData.address_2, - city: billingData.city, - state: billingData.state, - postal_code: billingData.postcode, - country: billingData.country, - }, - }; - if ( billingData.phone ) { - ownerInfo.phone = billingData.phone; - } - if ( billingData.email ) { - ownerInfo.email = billingData.email; - } - if ( billingData.first_name || billingData.last_name ) { - ownerInfo.name = `${ billingData.first_name } ${ billingData.last_name }`; - } - - const response = await createSource( ownerInfo ); - if ( response.error ) { - return { - type: emitResponse.responseTypes.ERROR, - message: onStripeError( response ), - }; - } - if ( ! response.source || ! response.source.id ) { - throw new Error( - getErrorMessageForTypeAndCode( errorTypes.API_ERROR ) - ); - } - setSourceId( response.source.id ); - return { - type: emitResponse.responseTypes.SUCCESS, - meta: { - paymentMethodData: { - stripe_source: response.source.id, - paymentMethod: PAYMENT_METHOD_NAME, - paymentRequestType: 'cc', - }, - billingData, - }, - }; - } catch ( e ) { - return { - type: emitResponse.responseTypes.ERROR, - message: e, - }; - } - }; - const unsubscribeProcessing = onPaymentProcessing( onSubmit ); - return () => { - unsubscribeProcessing(); - }; - }, [ - onPaymentProcessing, - billing.billingData, - stripe, - sourceId, - setSourceId, - onStripeError, - error, - emitResponse.noticeContexts.PAYMENTS, - emitResponse.responseTypes.ERROR, - emitResponse.responseTypes.SUCCESS, - elements, - ] ); -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/index.js b/assets/js/payment-method-extensions/payment-methods/stripe/index.js deleted file mode 100644 index 49dd7e4fe41..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { - registerPaymentMethod, - registerExpressPaymentMethod, -} from '@woocommerce/blocks-registry'; - -/** - * Internal dependencies - */ -import stripeCcPaymentMethod from './credit-card'; -import paymentRequestPaymentMethod from './payment-request'; -import { getStripeServerData } from './stripe-utils'; - -// Register Stripe Credit Card. -registerPaymentMethod( stripeCcPaymentMethod ); - -// Register Stripe Payment Request (Apple/Chrome Pay) if enabled. -if ( getStripeServerData().allowPaymentRequest ) { - registerExpressPaymentMethod( paymentRequestPaymentMethod ); -} diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/apple-pay-preview.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/apple-pay-preview.js deleted file mode 100644 index cc740802ca0..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/apple-pay-preview.js +++ /dev/null @@ -1,2 +0,0 @@ -export const applePayImage = - "data:image/svg+xml,%3Csvg width='264' height='48' viewBox='0 0 264 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='264' height='48' rx='3' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.114 16.6407C125.682 15.93 126.067 14.9756 125.966 14C125.135 14.0415 124.121 14.549 123.533 15.2602C123.006 15.8693 122.539 16.8641 122.661 17.7983C123.594 17.8797 124.526 17.3317 125.114 16.6407Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.955 17.982C124.601 17.9011 123.448 18.7518 122.801 18.7518C122.154 18.7518 121.163 18.0224 120.092 18.0421C118.696 18.0629 117.402 18.8524 116.694 20.1079C115.238 22.6196 116.31 26.3453 117.726 28.3909C118.414 29.4028 119.242 30.5174 120.334 30.4769C121.366 30.4365 121.77 29.8087 123.024 29.8087C124.277 29.8087 124.641 30.4769 125.733 30.4567C126.865 30.4365 127.573 29.4443 128.261 28.4313C129.049 27.2779 129.373 26.1639 129.393 26.1027C129.373 26.0825 127.209 25.2515 127.189 22.7606C127.169 20.6751 128.888 19.6834 128.969 19.6217C127.998 18.1847 126.481 18.0224 125.955 17.982Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M136.131 23.1804H138.834C140.886 23.1804 142.053 22.0752 142.053 20.1592C142.053 18.2432 140.886 17.1478 138.845 17.1478H136.131V23.1804ZM139.466 15.1582C142.411 15.1582 144.461 17.1903 144.461 20.1483C144.461 23.1172 142.369 25.1596 139.392 25.1596H136.131V30.3498H133.775V15.1582H139.466Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M152.198 26.224V25.3712L149.579 25.5397C148.106 25.6341 147.339 26.182 147.339 27.14C147.339 28.0664 148.138 28.6667 149.39 28.6667C150.988 28.6667 152.198 27.6449 152.198 26.224ZM145.046 27.2032C145.046 25.2551 146.529 24.1395 149.263 23.971L152.198 23.7922V22.9498C152.198 21.7181 151.388 21.0442 149.947 21.0442C148.758 21.0442 147.896 21.6548 147.717 22.5916H145.592C145.656 20.6232 147.507 19.1914 150.01 19.1914C152.703 19.1914 154.459 20.602 154.459 22.7917V30.351H152.282V28.5298H152.229C151.609 29.719 150.241 30.4666 148.758 30.4666C146.571 30.4666 145.046 29.1612 145.046 27.2032Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.461 34.4145V32.5934C156.608 32.6141 156.965 32.6354 157.155 32.6354C158.196 32.6354 158.785 32.1932 159.142 31.0564L159.353 30.3824L155.366 19.3281H157.827L160.604 28.298H160.657L163.434 19.3281H165.832L161.698 30.9402C160.752 33.6038 159.668 34.4778 157.376 34.4778C157.197 34.4778 156.618 34.4565 156.461 34.4145Z' fill='white'/%3E%3C/svg%3E%0A"; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/constants.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/constants.js deleted file mode 100644 index 25ff6bf6dd4..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/constants.js +++ /dev/null @@ -1,7 +0,0 @@ -export const PAYMENT_METHOD_NAME = 'payment_request'; - -export const DEFAULT_STRIPE_EVENT_HANDLERS = { - shippingAddressChange: null, - shippingOptionChange: null, - source: null, -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/index.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/index.js deleted file mode 100644 index b4174ac55d8..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/index.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * External dependencies - */ -import { getSetting } from '@woocommerce/settings'; - -/** - * Internal dependencies - */ -import { PAYMENT_METHOD_NAME } from './constants'; -import { PaymentRequestExpress } from './payment-request-express'; -import { applePayImage } from './apple-pay-preview'; -import { getStripeServerData, loadStripe } from '../stripe-utils'; - -const ApplePayPreview = () => ; - -const canPayStripePromise = loadStripe(); -const componentStripePromise = loadStripe(); - -let isStripeInitialized = false, - canPay = false; - -// Initialise stripe API client and determine if payment method can be used -// in current environment (e.g. geo + shopper has payment settings configured). -function paymentRequestAvailable( { currencyCode, totalPrice } ) { - // Stripe only supports carts of greater value than 30 cents. - if ( totalPrice < 30 ) { - return false; - } - - // If we've already initialised, return the cached results. - if ( isStripeInitialized ) { - return canPay; - } - - return canPayStripePromise.then( ( stripe ) => { - if ( stripe === null ) { - isStripeInitialized = true; - return canPay; - } - if ( stripe.error && stripe.error instanceof Error ) { - throw stripe.error; - } - // Do a test payment to confirm if payment method is available. - const paymentRequest = stripe.paymentRequest( { - total: { - label: 'Total', - amount: totalPrice, - pending: true, - }, - country: getSetting( 'baseLocation', {} )?.country, - currency: currencyCode, - } ); - return paymentRequest.canMakePayment().then( ( result ) => { - canPay = !! result; - isStripeInitialized = true; - return canPay; - } ); - } ); -} - -const paymentRequestPaymentMethod = { - name: PAYMENT_METHOD_NAME, - content: , - edit: , - canMakePayment: ( cartData ) => - paymentRequestAvailable( { - currencyCode: cartData?.cartTotals?.currency_code?.toLowerCase(), - totalPrice: parseInt( cartData?.cartTotals?.total_price || 0, 10 ), - } ), - paymentMethodId: 'stripe', - supports: { - features: getStripeServerData()?.supports ?? [], - }, -}; - -export default paymentRequestPaymentMethod; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/payment-request-express.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/payment-request-express.js deleted file mode 100644 index 6b98f453bf4..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/payment-request-express.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * External dependencies - */ -import { Elements, PaymentRequestButtonElement } from '@stripe/react-stripe-js'; - -/** - * Internal dependencies - */ -import { getStripeServerData } from '../stripe-utils'; -import { useInitialization } from './use-initialization'; -import { useCheckoutSubscriptions } from './use-checkout-subscriptions'; - -/** - * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe - * @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest - * @typedef {import('@woocommerce/type-defs/payment-method-interface').PaymentMethodInterface} RegisteredPaymentMethodProps - */ - -/** - * @typedef {Object} WithStripe - * - * @property {Stripe} [stripe] Stripe api (might not be present) - */ - -/** - * @typedef {RegisteredPaymentMethodProps & WithStripe} StripeRegisteredPaymentMethodProps - */ - -/** - * PaymentRequestExpressComponent - * - * @param {StripeRegisteredPaymentMethodProps} props Incoming props - */ -const PaymentRequestExpressComponent = ( { - shippingData, - billing, - eventRegistration, - onSubmit, - setExpressPaymentError, - emitResponse, - onClick, - onClose, -} ) => { - const { - paymentRequest, - paymentRequestEventHandlers, - clearPaymentRequestEventHandler, - isProcessing, - canMakePayment, - onButtonClick, - abortPayment, - completePayment, - paymentRequestType, - } = useInitialization( { - billing, - shippingData, - setExpressPaymentError, - onClick, - onClose, - onSubmit, - } ); - useCheckoutSubscriptions( { - canMakePayment, - isProcessing, - eventRegistration, - paymentRequestEventHandlers, - clearPaymentRequestEventHandler, - billing, - shippingData, - emitResponse, - paymentRequestType, - completePayment, - abortPayment, - } ); - - // 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; -}; - -/** - * PaymentRequestExpress with stripe provider - * - * @param {StripeRegisteredPaymentMethodProps} props - */ -export const PaymentRequestExpress = ( props ) => { - const { locale } = getStripeServerData().button; - const { stripe } = props; - return ( - - - - ); -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-checkout-subscriptions.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-checkout-subscriptions.js deleted file mode 100644 index b74ea2fbbe7..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-checkout-subscriptions.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { - normalizeShippingOptions, - getTotalPaymentItem, - normalizeLineItems, - getBillingData, - getPaymentMethodData, - getShippingData, -} from '../stripe-utils'; - -/** - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EventRegistrationProps} EventRegistrationProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').BillingDataProps} BillingDataProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').ShippingDataProps} ShippingDataProps - * @typedef {import('@woocommerce/type-defs/payment-method-interface').EmitResponseProps} EmitResponseProps - */ - -/** - * @param {Object} props - * - * @param {boolean} props.canMakePayment Whether the payment request - * can make payment or not. - * @param {boolean} props.isProcessing Whether the express payment - * method is processing or not. - * @param {EventRegistrationProps} props.eventRegistration Various functions for - * registering observers to - * events. - * @param {Object} props.paymentRequestEventHandlers Cached handlers registered - * for paymentRequest events. - * @param {function(string):void} props.clearPaymentRequestEventHandler Clears the cached payment - * request event handler. - * @param {BillingDataProps} props.billing - * @param {ShippingDataProps} props.shippingData - * @param {EmitResponseProps} props.emitResponse - * @param {string} props.paymentRequestType The derived payment request - * type for the express - * payment being processed. - * @param {function(any):void} props.completePayment This is a callback - * receiving the source event - * and setting it to - * successful payment. - * @param {function(any,string):any} props.abortPayment This is a callback - * receiving the source - * event and setting it to - * failed payment. - */ -export const useCheckoutSubscriptions = ( { - canMakePayment, - isProcessing, - eventRegistration, - paymentRequestEventHandlers, - clearPaymentRequestEventHandler, - billing, - shippingData, - emitResponse, - paymentRequestType, - completePayment, - abortPayment, -} ) => { - const { - onShippingRateSuccess, - onShippingRateFail, - onShippingRateSelectSuccess, - onShippingRateSelectFail, - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - onCheckoutAfterProcessingWithError, - } = eventRegistration; - const { noticeContexts, responseTypes } = emitResponse; - const eventHandlers = useRef( paymentRequestEventHandlers ); - const currentBilling = useRef( billing ); - const currentShipping = useRef( shippingData ); - const currentPaymentRequestType = useRef( paymentRequestType ); - - useEffect( () => { - eventHandlers.current = paymentRequestEventHandlers; - currentBilling.current = billing; - currentShipping.current = shippingData; - currentPaymentRequestType.current = paymentRequestType; - }, [ - paymentRequestEventHandlers, - billing, - shippingData, - paymentRequestType, - ] ); - - // subscribe to events. - useEffect( () => { - const onShippingRatesEvent = ( shippingRates ) => { - const handlers = eventHandlers.current; - const billingData = currentBilling.current; - if ( handlers.shippingAddressChange && isProcessing ) { - handlers.shippingAddressChange.updateWith( { - status: 'success', - shippingOptions: normalizeShippingOptions( shippingRates ), - total: getTotalPaymentItem( billingData.cartTotal ), - displayItems: normalizeLineItems( - billingData.cartTotalItems - ), - } ); - clearPaymentRequestEventHandler( 'shippingAddressChange' ); - } - }; - const onShippingRatesEventFail = ( currentErrorStatus ) => { - const handlers = eventHandlers.current; - if ( handlers.shippingAddressChange && isProcessing ) { - handlers.shippingAddressChange.updateWith( { - status: currentErrorStatus.hasInvalidAddress - ? 'invalid_shipping_address' - : 'fail', - shippingOptions: [], - } ); - } - clearPaymentRequestEventHandler( 'shippingAddressChange' ); - }; - const onShippingSelectedRate = ( forSuccess = true ) => () => { - const handlers = eventHandlers.current; - const shipping = currentShipping.current; - const billingData = currentBilling.current; - if ( - handlers.shippingOptionChange && - ! shipping.isSelectingRate && - isProcessing - ) { - const updateObject = forSuccess - ? { - status: 'success', - total: getTotalPaymentItem( billingData.cartTotal ), - displayItems: normalizeLineItems( - billingData.cartTotalItems - ), - } - : { - status: 'fail', - }; - handlers.shippingOptionChange.updateWith( updateObject ); - clearPaymentRequestEventHandler( 'shippingOptionChange' ); - } - }; - const onProcessingPayment = () => { - const handlers = eventHandlers.current; - if ( handlers.sourceEvent && isProcessing ) { - const response = { - type: responseTypes.SUCCESS, - meta: { - billingData: getBillingData( handlers.sourceEvent ), - paymentMethodData: getPaymentMethodData( - handlers.sourceEvent, - currentPaymentRequestType.current - ), - shippingData: getShippingData( handlers.sourceEvent ), - }, - }; - return response; - } - return { type: responseTypes.SUCCESS }; - }; - const onCheckoutComplete = ( checkoutResponse ) => { - const handlers = eventHandlers.current; - let response = { type: responseTypes.SUCCESS }; - if ( handlers.sourceEvent && isProcessing ) { - const { - paymentStatus, - paymentDetails, - } = checkoutResponse.processingResponse; - if ( paymentStatus === responseTypes.SUCCESS ) { - completePayment( handlers.sourceEvent ); - } - if ( - paymentStatus === responseTypes.ERROR || - paymentStatus === responseTypes.FAIL - ) { - abortPayment( handlers.sourceEvent ); - response = { - type: responseTypes.ERROR, - message: paymentDetails?.errorMessage, - messageContext: noticeContexts.EXPRESS_PAYMENTS, - retry: true, - }; - } - clearPaymentRequestEventHandler( 'sourceEvent' ); - } - return response; - }; - if ( canMakePayment && isProcessing ) { - const unsubscribeShippingRateSuccess = onShippingRateSuccess( - onShippingRatesEvent - ); - const unsubscribeShippingRateFail = onShippingRateFail( - onShippingRatesEventFail - ); - const unsubscribeShippingRateSelectSuccess = onShippingRateSelectSuccess( - onShippingSelectedRate() - ); - const unsubscribeShippingRateSelectFail = onShippingRateSelectFail( - onShippingRatesEventFail - ); - const unsubscribePaymentProcessing = onPaymentProcessing( - onProcessingPayment - ); - const unsubscribeCheckoutCompleteSuccess = onCheckoutAfterProcessingWithSuccess( - onCheckoutComplete - ); - const unsubscribeCheckoutCompleteFail = onCheckoutAfterProcessingWithError( - onCheckoutComplete - ); - return () => { - unsubscribeCheckoutCompleteFail(); - unsubscribeCheckoutCompleteSuccess(); - unsubscribePaymentProcessing(); - unsubscribeShippingRateFail(); - unsubscribeShippingRateSuccess(); - unsubscribeShippingRateSelectSuccess(); - unsubscribeShippingRateSelectFail(); - }; - } - return undefined; - }, [ - canMakePayment, - isProcessing, - onShippingRateSuccess, - onShippingRateFail, - onShippingRateSelectSuccess, - onShippingRateSelectFail, - onPaymentProcessing, - onCheckoutAfterProcessingWithSuccess, - onCheckoutAfterProcessingWithError, - responseTypes, - noticeContexts, - completePayment, - abortPayment, - clearPaymentRequestEventHandler, - ] ); -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-event-handlers.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-event-handlers.js deleted file mode 100644 index 8789e5f15db..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-event-handlers.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * External dependencies - */ -import { useState, useCallback } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { DEFAULT_STRIPE_EVENT_HANDLERS } from './constants'; - -/** - * A utility hook for maintaining an event handler cache. - */ -export const useEventHandlers = () => { - const [ paymentRequestEventHandlers, setEventHandlers ] = useState( - DEFAULT_STRIPE_EVENT_HANDLERS - ); - - const setPaymentRequestEventHandler = useCallback( - ( eventName, handler ) => { - setEventHandlers( ( prevEventHandlers ) => { - return { - ...prevEventHandlers, - [ eventName ]: handler, - }; - } ); - }, - [ setEventHandlers ] - ); - - const clearPaymentRequestEventHandler = useCallback( - ( eventName ) => { - // @ts-ignore - setEventHandlers( ( prevEventHandlers ) => { - // @ts-ignore - // eslint-disable-next-line no-unused-vars - const { [ eventName ]: __, ...newHandlers } = prevEventHandlers; - return newHandlers; - } ); - }, - [ setEventHandlers ] - ); - return { - paymentRequestEventHandlers, - setPaymentRequestEventHandler, - clearPaymentRequestEventHandler, - }; -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-initialization.js b/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-initialization.js deleted file mode 100644 index 117e9cab182..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/use-initialization.js +++ /dev/null @@ -1,251 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useState, useRef, useCallback } from '@wordpress/element'; -import { useStripe } from '@stripe/react-stripe-js'; -import { getSetting } from '@woocommerce/settings'; -import { __ } from '@wordpress/i18n'; -import isShallowEqual from '@wordpress/is-shallow-equal'; - -/** - * Internal dependencies - */ -import { - getPaymentRequest, - updatePaymentRequest, - canDoPaymentRequest, - normalizeShippingAddressForCheckout, - normalizeShippingOptionSelectionsForCheckout, - getStripeServerData, - pluckAddress, - normalizeShippingOptions, -} from '../stripe-utils'; -import { useEventHandlers } from './use-event-handlers'; - -/** - * @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest - */ - -export const useInitialization = ( { - billing, - shippingData, - setExpressPaymentError, - onClick, - onClose, - onSubmit, -} ) => { - const stripe = useStripe(); - /** - * @type {[ StripePaymentRequest|null, function( StripePaymentRequest ):void]} - */ - // @ts-ignore - const [ paymentRequest, setPaymentRequest ] = useState( null ); - const [ isFinished, setIsFinished ] = useState( false ); - const [ isProcessing, setIsProcessing ] = useState( false ); - const [ canMakePayment, setCanMakePayment ] = useState( false ); - const [ paymentRequestType, setPaymentRequestType ] = useState( '' ); - const currentShipping = useRef( shippingData ); - const { - paymentRequestEventHandlers, - clearPaymentRequestEventHandler, - setPaymentRequestEventHandler, - } = useEventHandlers(); - - // Update refs when any change. - useEffect( () => { - currentShipping.current = shippingData; - }, [ shippingData ] ); - - // Create the initial paymentRequest object. Note, we can't do anything if stripe isn't available yet or we have zero total. - useEffect( () => { - if ( - ! stripe || - ! billing.cartTotal.value || - isFinished || - isProcessing || - paymentRequest - ) { - return; - } - const pr = getPaymentRequest( { - total: billing.cartTotal, - currencyCode: billing.currency.code.toLowerCase(), - countryCode: getSetting( 'baseLocation', {} )?.country, - shippingRequired: shippingData.needsShipping, - cartTotalItems: billing.cartTotalItems, - stripe, - } ); - canDoPaymentRequest( pr ).then( ( result ) => { - setPaymentRequest( pr ); - setPaymentRequestType( result.requestType || '' ); - setCanMakePayment( result.canPay ); - } ); - }, [ - billing.cartTotal, - billing.currency.code, - shippingData.needsShipping, - billing.cartTotalItems, - stripe, - isProcessing, - isFinished, - paymentRequest, - ] ); - - // When the payment button is clicked, update the request and show it. - const onButtonClick = useCallback( () => { - setIsProcessing( true ); - setIsFinished( false ); - setExpressPaymentError( '' ); - updatePaymentRequest( { - // @ts-ignore - paymentRequest, - total: billing.cartTotal, - currencyCode: billing.currency.code.toLowerCase(), - cartTotalItems: billing.cartTotalItems, - } ); - onClick(); - }, [ - onClick, - paymentRequest, - setExpressPaymentError, - billing.cartTotal, - billing.currency.code, - billing.cartTotalItems, - ] ); - - const abortPayment = useCallback( ( paymentMethod ) => { - paymentMethod.complete( 'fail' ); - setIsProcessing( false ); - setIsFinished( true ); - }, [] ); - - const completePayment = useCallback( ( paymentMethod ) => { - paymentMethod.complete( 'success' ); - setIsFinished( true ); - setIsProcessing( false ); - }, [] ); - - // whenever paymentRequest changes, hook in event listeners. - useEffect( () => { - const noop = { removeAllListeners: () => void null }; - let shippingAddressChangeEvent = noop, - shippingOptionChangeEvent = noop, - sourceChangeEvent = noop, - cancelChangeEvent = noop; - - if ( paymentRequest ) { - const cancelHandler = () => { - setIsFinished( false ); - setIsProcessing( false ); - setPaymentRequest( null ); - onClose(); - }; - - const shippingAddressChangeHandler = ( event ) => { - const newShippingAddress = normalizeShippingAddressForCheckout( - event.shippingAddress - ); - if ( - isShallowEqual( - pluckAddress( newShippingAddress ), - pluckAddress( currentShipping.current.shippingAddress ) - ) - ) { - // the address is the same so no change needed. - event.updateWith( { - status: 'success', - shippingOptions: normalizeShippingOptions( - currentShipping.current.shippingRates - ), - } ); - } else { - // the address is different so let's set the new address and - // register the handler to be picked up by the shipping rate - // change event. - currentShipping.current.setShippingAddress( - normalizeShippingAddressForCheckout( - event.shippingAddress - ) - ); - setPaymentRequestEventHandler( - 'shippingAddressChange', - event - ); - } - }; - - const shippingOptionChangeHandler = ( event ) => { - currentShipping.current.setSelectedRates( - normalizeShippingOptionSelectionsForCheckout( - event.shippingOption - ) - ); - setPaymentRequestEventHandler( 'shippingOptionChange', event ); - }; - - const sourceHandler = ( paymentMethod ) => { - if ( - // eslint-disable-next-line no-undef - ! getStripeServerData().allowPrepaidCard && - paymentMethod.source.card.funding - ) { - setExpressPaymentError( - /* eslint-disable-next-line @wordpress/i18n-text-domain */ - __( - "Sorry, we're not accepting prepaid cards at this time.", - 'woocommerce-gateway-stripe' - ) - ); - return; - } - setPaymentRequestEventHandler( 'sourceEvent', paymentMethod ); - // kick off checkout processing step. - onSubmit(); - }; - - // @ts-ignore - shippingAddressChangeEvent = paymentRequest.on( - 'shippingaddresschange', - shippingAddressChangeHandler - ); - // @ts-ignore - shippingOptionChangeEvent = paymentRequest.on( - 'shippingoptionchange', - shippingOptionChangeHandler - ); - // @ts-ignore - sourceChangeEvent = paymentRequest.on( 'source', sourceHandler ); - // @ts-ignore - cancelChangeEvent = paymentRequest.on( 'cancel', cancelHandler ); - } - - return () => { - if ( paymentRequest ) { - shippingAddressChangeEvent.removeAllListeners(); - shippingOptionChangeEvent.removeAllListeners(); - sourceChangeEvent.removeAllListeners(); - cancelChangeEvent.removeAllListeners(); - } - }; - }, [ - paymentRequest, - canMakePayment, - isProcessing, - setPaymentRequestEventHandler, - setExpressPaymentError, - onSubmit, - onClose, - ] ); - - return { - paymentRequest, - paymentRequestEventHandlers, - clearPaymentRequestEventHandler, - isProcessing, - canMakePayment, - onButtonClick, - abortPayment, - completePayment, - paymentRequestType, - }; -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/constants.js b/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/constants.js deleted file mode 100644 index 7257f91cca3..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/constants.js +++ /dev/null @@ -1,28 +0,0 @@ -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/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/index.js b/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/index.js deleted file mode 100644 index 6bbd44578f6..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './normalize'; -export * from './utils'; -export * from './load-stripe'; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/load-stripe.js b/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/load-stripe.js deleted file mode 100644 index da46b583390..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/load-stripe.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { loadStripe } from '@stripe/stripe-js'; - -/** - * Internal dependencies - */ -import { getApiKey } from './utils'; - -const stripePromise = () => - new Promise( ( resolve ) => { - try { - resolve( loadStripe( getApiKey() ) ); - } catch ( error ) { - // In order to avoid showing console error publicly to users, - // we resolve instead of rejecting when there is an error. - resolve( { error } ); - } - } ); - -export { stripePromise as loadStripe }; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/normalize.js b/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/normalize.js deleted file mode 100644 index ef66e7b648a..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/normalize.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem - * @typedef {import('./type-defs').StripeShippingOption} StripeShippingOption - * @typedef {import('./type-defs').StripeShippingAddress} StripeShippingAddress - * @typedef {import('./type-defs').StripePaymentResponse} StripePaymentResponse - * @typedef {import('@woocommerce/type-defs/payment-method-interface').PreparedCartTotalItem} CartTotalItem - * @typedef {import('@woocommerce/type-defs/cart').CartShippingOption} CartShippingOption - * @typedef {import('@woocommerce/type-defs/shipping').ShippingAddress} CartShippingAddress - * @typedef {import('@woocommerce/type-defs/billing').BillingData} 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 cartTotalItem.value - ? { - amount: cartTotalItem.value, - label: cartTotalItem.label, - pending, - } - : false; - } ) - .filter( Boolean ); -}; - -/** - * 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 ) => { - const rates = shippingOptions[ 0 ].shipping_rates; - return rates.map( ( rate ) => { - return { - id: rate.rate_id, - label: rate.name, - detail: rate.description, - amount: parseInt( rate.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 ) => { - const address = { - 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.replace( ' ', '' ), - }; - return address; -}; - -/** - * 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, - }; -}; - -const getShippingData = ( paymentResponse ) => { - return paymentResponse.shippingAddress - ? { - address: normalizeShippingAddressForCheckout( - paymentResponse.shippingAddress - ), - } - : null; -}; - -export { - normalizeLineItems, - normalizeShippingOptions, - normalizeShippingAddressForCheckout, - normalizeShippingOptionSelectionsForCheckout, - getBillingData, - getPaymentMethodData, - getShippingData, -}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/type-defs.js b/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/type-defs.js deleted file mode 100644 index 22c10d128dc..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/type-defs.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * 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 expiry month. - * @property {number} exp_year Two-digit number for - * card expiry year. - * @property {string} fingerprint Uniquely identifies this - * particular card number - * @property {string} funding The card funding type - * @property {Object} generated_from Details of the original - * PaymentMethod that - * created this object. - * @property {string} last4 The last 4 digits of the - * card - * @property {Object} three_d_secure_usage Contains details on how - * this card may be used for - * 3d secure - * @property {Object} wallet If this card is part of a - * card wallet, this - * contains the details of - * the card wallet. - */ - -/** - * @typedef {Object} StripePaymentMethod - * - * @property {string} id Unique identifier for the - * object - * @property {StripeBillingDetails} billing_details The billing details for the - * payment method - * @property {StripeBillingCard} card Details on the card used to - * pay - * @property {string} customer The ID of the customer to - * which this payment method - * is saved. - * @property {Object} metadata Set of key-value pairs that - * can be attached to the - * object. - * @property {string} type Type of payment method - * @property {string} object The type of object. Always - * 'payment_method'. Can use - * to validate! - * @property {Object} card_present If this is a card present - * payment method, contains - * details about that card - * @property {number} created The timestamp for when the - * card was created. - * @property {Object} fpx If this is an fpx payment - * method, contains details - * about it. - * @property {Object} ideal If this is an ideal payment - * method, contains details - * about it. - * @property {boolean} livemode True if the object exists - * in live mode or if in test - * mode. - * @property {Object} sepa_debit If this is a sepa_debit - * payment method, contains - * details about it. - */ - -/** - * @typedef {Object} StripeSource - * - * @property {string} id Unique identifier for - * object - * @property {number} amount A positive number in - * the smallest currency - * unit. - * @property {string} currency The three-letter ISO - * code for the currency - * @property {string} customer The ID of the customer - * to which this source - * is attached. - * @property {Object} metadata Arbitrary key-value - * pairs that can be - * attached. - * @property {StripeBillingDetails} owner Information about the - * owner of the payment - * made. - * @property {Object} [redirect] Information related to - * the redirect flow - * (present if the source - * is authenticated by - * redirect) - * @property {string} statement_descriptor Extra information - * about a source (will - * appear on customer's - * statement) - * @property {string} status The status of the - * source. - * @property {string} type The type of the source - * (it is a payment - * method type) - * @property {string} object Value is "source" can - * be used to validate. - * @property {string} client_secret The client secret of - * the source. Used for - * client-side retrieval - * using a publishable - * key. - * @property {Object} [code_verification] Information related to - * the code verification - * flow. - * @property {number} created When the source object - * was instantiated - * (timestamp). - * @property {string} flow The authentication - * flow of the source. - * @property {boolean} livemode If true then payment - * is made in live mode - * otherwise test mode. - * @property {Object} [receiver] Information related to - * the receiver flow. - * @property {Object} source_order Information about the - * items and shipping - * associated with the - * source. - * @property {string} usage Whether source should - * be reusable or not. - */ - -/** - * @typedef {Object} StripePaymentResponse - * - * @property {Object} token A stripe token object - * @property {StripePaymentMethod} paymentMethod The stripe payment method - * object - * @property {?StripeSource} source Present if this was the - * result of a source event - * listener - * @property {Function} complete Call this when the token - * data has been processed. - * @property {string} [payerName] The customer's name. - * @property {string} [payerEmail] The customer's email. - * @property {string} [payerPhone] The customer's phone. - * @property {StripeShippingAddress} [shippingAddress] The final shipping - * address the customer - * indicated - * @property {StripeShippingOption} [shippingOption] The final shipping - * option the customer - * selected. - * @property {string} methodName The unique name of the - * payment handler the - * customer chose to - * authorize payment - */ - -/** - * @typedef {Object} StripePaymentRequestOptions The configuration of stripe - * payment request options to - * pass in. - * - * @property {string} country Two-letter (ISO) - * country code. - * @property {string} currency Three letter currency - * code. - * @property {StripePaymentItem} total Shown to the customer. - * @property {StripePaymentItem[]} displayItems Line items shown to the - * customer. - * @property {boolean} requestPayerName Whether or not to - * collect the payer's - * name. - * @property {boolean} requestPayerEmail Whether or not to - * collect the payer's - * email. - * @property {boolean} requestPayerPhone Whether or not to - * collect the payer's - * phone. - * @property {boolean} requestShipping Whether to collect - * shipping address. - * @property {StripeShippingOption[]} shippingOptions Available shipping - * options. - */ - -/** - * @typedef {Object} StripePaymentRequest Stripe payment request object. - * - * @property {function():Promise} canMakePayment Returns a promise that resolves - * with an object detailing if a - * browser payment API is - * available. - * @property {function()} show Shows the browser's payment - * interface (called automatically - * if payment request button in - * use) - * @property {function()} update Used to update a PaymentRequest - * object. - * @property {function()} on For registering callbacks on - * payment request events. - */ - -/** - * @typedef {Object} Stripe Stripe api object. - * @property {any} api Various api properties - */ - -/** - * @typedef {Object} CreditCardIcon - * - * @property {string} url Url to icon. - * @property {string} alt Alt text for icon. - */ - -/* eslint-disable jsdoc/valid-types */ -// [k:string]:CreditCardIcon triggers the above rule even though VSCode interprets it fine. -/** - * @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. - * @property {{[k:string]:CreditCardIcon}} icons Contains supported cc icons. - * @property {boolean} showSavedCards Used to indicate whether saved cards - * can be used. - * @property {boolean} showSaveOption Used to indicate whether the option to - * save card can be displayed. - * @property {boolean} allowPaymentRequest True if merchant has enabled payment - * request (Chrome/Apple Pay). - * @property {Object} supports List of features supported by the payment gateway - */ -/* eslint-enable jsdoc/valid-types */ - -/** - * @typedef {Object} StripeElementOptions - * - * @property {Object} options The configuration object for stripe - * elements. - * @property {function(boolean)} onActive A callback for setting whether an - * element is active or not. "Active" - * means it's not empty. - * @property {string} error Any error message from the stripe - * element. - * @property {function(string)} setError A callback for setting an error - * message. - */ - -export {}; diff --git a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/utils.js b/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/utils.js deleted file mode 100644 index 74f071107fc..00000000000 --- a/assets/js/payment-method-extensions/payment-methods/stripe/stripe-utils/utils.js +++ /dev/null @@ -1,281 +0,0 @@ -/** - * External dependencies - */ -import { getSetting } from '@woocommerce/settings'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { normalizeLineItems } from './normalize'; -import { errorTypes, errorCodes } from './constants'; - -/** - * @typedef {import('./type-defs').StripeServerData} StripeServerData - * @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem - * @typedef {import('./type-defs').StripePaymentRequest} StripePaymentRequest - * @typedef {import('@woocommerce/type-defs/payment-method-interface').PreparedCartTotalItem} CartTotalItem - */ - -/** - * Stripe data comes form the server passed on a global object. - * - * @return {StripeServerData} Stripe server data. - */ -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 || - __( 'Total', 'woo-gutenberg-products-block' ), - 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 canDoPaymentRequest = ( paymentRequest ) => { - return new Promise( ( resolve ) => { - paymentRequest.canMakePayment().then( ( result ) => { - if ( result ) { - const paymentRequestType = result.applePay - ? 'apple_pay' - : 'payment_request_api'; - resolve( { canPay: true, requestType: paymentRequestType } ); - return; - } - resolve( { canPay: 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 = { - /* eslint-disable @wordpress/i18n-text-domain */ - [ errorCodes.INVALID_NUMBER ]: __( - 'The card number is not a valid credit card number.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INVALID_EXPIRY_MONTH ]: __( - 'The card expiration month is invalid.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INVALID_EXPIRY_YEAR ]: __( - 'The card expiration year is invalid.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INVALID_CVC ]: __( - 'The card 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 security code is incomplete.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INCOMPLETE_EXPIRY ]: __( - 'The card expiration date is incomplete.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.EXPIRED_CARD ]: __( - 'The card has expired.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INCORRECT_CVC ]: __( - 'The card security code is incorrect.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INCORRECT_ZIP ]: __( - 'The card zip code failed validation.', - 'woocommerce-gateway-stripe' - ), - [ errorCodes.INVALID_EXPIRY_YEAR_PAST ]: __( - 'The card 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' - ), - /* eslint-enable @wordpress/i18n-text-domain */ - }; - return messages[ code ] || null; -}; - -const getErrorMessageForTypeAndCode = ( type, code = '' ) => { - switch ( type ) { - case errorTypes.INVALID_EMAIL: - return __( - 'Invalid email address, please correct and try again.', - 'woo-gutenberg-products-block' - ); - case isNonFriendlyError( type ): - return __( - 'Unable to process this payment, please try again or use alternative method.', - 'woo-gutenberg-products-block' - ); - case errorTypes.CARD_ERROR: - return getErrorMessageForCode( code ); - case errorTypes.VALIDATION_ERROR: - return ''; // These are shown inline. - } - return null; -}; - -/** - * pluckAddress takes a full address object and returns relevant fields for calculating - * shipping, so we can track when one of them change to update rates. - * - * @param {Object} address An object containing all address information - * @param {string} address.country - * @param {string} address.state - * @param {string} address.city - * @param {string} address.postcode - * - * @return {Object} pluckedAddress An object containing shipping address that are needed to fetch an address. - */ -const pluckAddress = ( { country, state, city, postcode } ) => ( { - country, - state, - city, - postcode: postcode.replace( ' ', '' ).toUpperCase(), -} ); - -export { - getStripeServerData, - getApiKey, - getTotalPaymentItem, - getPaymentRequest, - updatePaymentRequest, - canDoPaymentRequest, - getErrorMessageForTypeAndCode, - pluckAddress, -}; diff --git a/assets/js/previews/saved-payment-methods.js b/assets/js/previews/saved-payment-methods.js index de6d047b0cb..2a848d6ace5 100644 --- a/assets/js/previews/saved-payment-methods.js +++ b/assets/js/previews/saved-payment-methods.js @@ -2,7 +2,7 @@ export const previewSavedPaymentMethods = { cc: [ { method: { - gateway: 'stripe', + gateway: 'credit-card', last4: '5678', brand: 'Visa', }, diff --git a/assets/js/shared/context/product-data-context.js b/assets/js/shared/context/product-data-context.js index 1726d497f6e..ef94a8eea79 100644 --- a/assets/js/shared/context/product-data-context.js +++ b/assets/js/shared/context/product-data-context.js @@ -45,11 +45,13 @@ const defaultProductData = { is_on_backorder: false, low_stock_remaining: null, sold_individually: false, - quantity_limit: 99, add_to_cart: { text: 'Add to cart', description: 'Add to cart', url: '', + minimum: 1, + maximum: 99, + multiple_of: 1, }, }; diff --git a/assets/js/types/type-defs/cart-response.ts b/assets/js/types/type-defs/cart-response.ts index ee968c9dd86..925c093d990 100644 --- a/assets/js/types/type-defs/cart-response.ts +++ b/assets/js/types/type-defs/cart-response.ts @@ -2,13 +2,7 @@ * Internal dependencies */ import { CurrencyResponse } from './currency'; -import { - CartImageItem, - CartItemPrices, - CartItemTotals, - CartVariationItem, - CatalogVisibility, -} from './cart'; +import { CartItem } from './cart'; export interface CartResponseTotalsItem extends CurrencyResponse { total_discount: string; @@ -127,30 +121,7 @@ export interface CartResponseItemTotals extends CurrencyResponse { line_total_tax: string; } -export interface CartResponseItem { - key: string; - id: number; - quantity: number; - catalog_visibility: CatalogVisibility; - quantity_limit: number; - name: string; - summary: string; - short_description: string; - description: string; - sku: string; - low_stock_remaining: null | number; - backorders_allowed: boolean; - show_backorder_badge: boolean; - sold_individually: boolean; - permalink: string; - images: Array< CartImageItem >; - variation: Array< CartVariationItem >; - prices: CartItemPrices; - totals: CartItemTotals; - extensions: ExtensionsData; - item_data: Record< string, unknown >[]; -} - +export type CartResponseItem = CartItem; export interface CartResponseTotalsTaxLineItem { name: string; price: string; diff --git a/assets/js/types/type-defs/cart.ts b/assets/js/types/type-defs/cart.ts index 31d17c53fea..e00d0a19d7a 100644 --- a/assets/js/types/type-defs/cart.ts +++ b/assets/js/types/type-defs/cart.ts @@ -117,7 +117,12 @@ export interface CartItem { id: number; quantity: number; catalog_visibility: CatalogVisibility; - quantity_limit: number; + quantity_limits: { + minimum: number; + maximum: number; + multiple_of: number; + editable: boolean; + }; name: string; summary: string; short_description: string; diff --git a/assets/js/types/type-defs/payments.ts b/assets/js/types/type-defs/payments.ts index 8dba7172373..af0478e6efc 100644 --- a/assets/js/types/type-defs/payments.ts +++ b/assets/js/types/type-defs/payments.ts @@ -69,7 +69,7 @@ export interface PaymentMethodConfiguration { paymentMethodId?: string; // Object that describes various features provided by the payment method. supports: SupportsConfiguration; - // Array of card types (brands) supported by the payment method. (See stripe/credit-card for example.) + // Array of card types (brands) supported by the payment method. icons?: null | PaymentMethodIcons; // A react node that will be used as a label for the payment method in the checkout. label: ReactNode; diff --git a/assets/js/types/type-defs/product-response.ts b/assets/js/types/type-defs/product-response.ts index 56baaaa1d54..8c1d037bde5 100644 --- a/assets/js/types/type-defs/product-response.ts +++ b/assets/js/types/type-defs/product-response.ts @@ -86,10 +86,12 @@ export interface ProductResponseItem { is_on_backorder: boolean; low_stock_remaining: null | number; sold_individually: boolean; - quantity_limit: number; add_to_cart: { text: string; description: string; url: string; + minimum: number; + maximum: number; + multiple_of: number; }; } diff --git a/assets/js/utils/shared-attributes.js b/assets/js/utils/shared-attributes.js index 5533b7022ea..ef4a77f9d93 100644 --- a/assets/js/utils/shared-attributes.js +++ b/assets/js/utils/shared-attributes.js @@ -72,4 +72,12 @@ export default { type: 'boolean', default: false, }, + + /** + * Whether to display in stock, out of stock or backorder products. + */ + stockStatus: { + type: 'array', + default: Object.keys( getSetting( 'stockStatusOptions', [] ) ), + }, }; diff --git a/bin/hook-docs/data/actions.json b/bin/hook-docs/data/actions.json index 2b5553e8697..1556fb4c4e7 100644 --- a/bin/hook-docs/data/actions.json +++ b/bin/hook-docs/data/actions.json @@ -31,7 +31,7 @@ "types": [ "integer" ], - "variable": "$quantity" + "variable": "$request_quantity" }, { "name": "param", diff --git a/bin/hook-docs/data/filters.json b/bin/hook-docs/data/filters.json index 7ed46163cbb..178329e58a3 100644 --- a/bin/hook-docs/data/filters.json +++ b/bin/hook-docs/data/filters.json @@ -83,100 +83,6 @@ }, "args": 2 }, - { - "name": "wc_stripe_allow_prepaid_card", - "file": "Payments/Integrations/Stripe.php", - "type": "filter", - "doc": { - "description": "Filters if prepaid cards are supported by Stripe.", - "long_description": "", - "tags": [ - { - "name": "param", - "content": "True if prepaid cards are allowed.", - "types": [ - "boolean" - ], - "variable": "$allow_prepaid_card" - }, - { - "name": "return", - "content": "", - "types": [ - "boolean" - ] - } - ], - "long_description_html": "" - }, - "args": 1 - }, - { - "name": "wc_stripe_display_save_payment_method_checkbox", - "file": "Payments/Integrations/Stripe.php", - "type": "filter", - "doc": { - "description": "Filters if the save payment method checkbox is shown for Stripe.", - "long_description": "This assumes that Stripe supports `tokenization` - currently this is true, based on https://github.com/woocommerce/woocommerce-gateway-stripe/blob/master/includes/class-wc-gateway-stripe.php#L95", - "tags": [ - { - "name": "see", - "content": "", - "refers": "https://github.com/woocommerce/woocommerce-gateway-stripe/blob/ad19168b63df86176cbe35c3e95203a245687640/includes/class-wc-gateway-stripe.php#L271" - }, - { - "name": "see", - "content": "", - "refers": "https://github.com/woocommerce/woocommerce/wiki/Payment-Token-API" - }, - { - "name": "param", - "content": "True if saved cards functionality is enabled.", - "types": [ - "boolean" - ], - "variable": "$saved_cards" - }, - { - "name": "return", - "content": "", - "types": [ - "boolean" - ] - } - ], - "long_description_html": "

This assumes that Stripe supports tokenization - currently this is true, based on https://github.com/woocommerce/woocommerce-gateway-stripe/blob/master/includes/class-wc-gateway-stripe.php#L95

" - }, - "args": 1 - }, - { - "name": "wc_stripe_payment_request_button_locale", - "file": "Payments/Integrations/Stripe.php", - "type": "filter", - "doc": { - "description": "Filters the payment request button locale.", - "long_description": "", - "tags": [ - { - "name": "param", - "content": "Current locale. Defaults to en_US.", - "types": [ - "string" - ], - "variable": "$locale" - }, - { - "name": "return", - "content": "", - "types": [ - "string" - ] - } - ], - "long_description_html": "" - }, - "args": 1 - }, { "name": "woocommerce_add_cart_item", "file": "StoreApi/Utilities/CartController.php", @@ -910,7 +816,7 @@ }, { "name": "woocommerce_store_api_product_quantity_limit", - "file": "StoreApi/Schemas/ProductSchema.php", + "file": "StoreApi/Utilities/QuantityLimits.php", "type": "filter", "doc": { "description": "Filters the quantity limit for a product being added to the cart via the Store API.", @@ -944,6 +850,51 @@ }, "args": 2 }, + { + "name": "woocommerce_store_api_product_quantity_{$value_type}", + "file": "StoreApi/Utilities/QuantityLimits.php", + "type": "filter", + "doc": { + "description": "Filters the quantity minimum for a cart item in Store API. This allows extensions to control the minimum qty of items already within the cart.", + "long_description": "The suffix of the hook will vary depending on the value being filtered. For example, minimum, maximum, multiple_of, editable.", + "tags": [ + { + "name": "param", + "content": "The value being filtered.", + "types": [ + "mixed" + ], + "variable": "$value" + }, + { + "name": "param", + "content": "The product object.", + "types": [ + "\\WC_Product" + ], + "variable": "$product" + }, + { + "name": "param", + "content": "The cart item if the product exists in the cart, or null.", + "types": [ + "array", + "null" + ], + "variable": "$cart_item" + }, + { + "name": "return", + "content": "", + "types": [ + "mixed" + ] + } + ], + "long_description_html": "

The suffix of the hook will vary depending on the value being filtered. For example, minimum, maximum, multiple_of, editable.

" + }, + "args": 3 + }, { "name": "woocommerce_variation_option_name", "file": "StoreApi/Schemas/CartItemSchema.php", diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 6f4ef3506a8..3c4d256809e 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -134,8 +134,6 @@ const entries = { './assets/js/blocks/cart-checkout/mini-cart/component-frontend.tsx', }, payments: { - 'wc-payment-method-stripe': - './assets/js/payment-method-extensions/payment-methods/stripe/index.js', 'wc-payment-method-cheque': './assets/js/payment-method-extensions/payment-methods/cheque/index.js', 'wc-payment-method-paypal': diff --git a/docs/contributors/folder-structure.md b/docs/contributors/folder-structure.md index 8eff8780641..567bd1a224c 100644 --- a/docs/contributors/folder-structure.md +++ b/docs/contributors/folder-structure.md @@ -128,7 +128,7 @@ The following snippet explains how the WooCommerce Blocks repository is structur │ The middleware code to handle Store API calls. │ ├── assets/js/payment-method-extensions - │ Functionality for the payment options such as PayPal and Stripe. + │ Functionality for the payment options such as PayPal. │ ├── assets/js/previews │ The previews of various components such the All Products Block. diff --git a/docs/extensibility/actions.md b/docs/extensibility/actions.md index 670f959602a..e514a3648d2 100644 --- a/docs/extensibility/actions.md +++ b/docs/extensibility/actions.md @@ -44,7 +44,7 @@ Fires when an item is added to the cart. ```php -do_action( 'woocommerce_add_to_cart', string $cart_id, integer $product_id, integer $quantity, integer $variation_id, array $variation, array $cart_item_data ) +do_action( 'woocommerce_add_to_cart', string $cart_id, integer $product_id, integer $request_quantity, integer $variation_id, array $variation, array $cart_item_data ) ``` ### Description @@ -57,7 +57,7 @@ do_action( 'woocommerce_add_to_cart', string $cart_id, integer $product_id, inte | -------- | ---- | ----------- | | $cart_id | string | ID of the item in the cart. | | $product_id | integer | ID of the product added to the cart. | -| $quantity | integer | Quantity of the item added to the cart. | +| $request_quantity | integer | Quantity of the item added to the cart. | | $variation_id | integer | Variation ID of the product added to the cart. | | $variation | array | Array of variation data. | | $cart_item_data | array | Array of other cart item data. | diff --git a/docs/extensibility/filters.md b/docs/extensibility/filters.md index e269bc8a0d0..db690f3e034 100644 --- a/docs/extensibility/filters.md +++ b/docs/extensibility/filters.md @@ -10,9 +10,6 @@ - [__experimental_woocommerce_blocks_add_data_attributes_to_block](#__experimental_woocommerce_blocks_add_data_attributes_to_block) - [__experimental_woocommerce_blocks_add_data_attributes_to_namespace](#__experimental_woocommerce_blocks_add_data_attributes_to_namespace) - [__experimental_woocommerce_blocks_payment_gateway_features_list](#__experimental_woocommerce_blocks_payment_gateway_features_list) - - [wc_stripe_allow_prepaid_card](#wc_stripe_allow_prepaid_card) - - [wc_stripe_display_save_payment_method_checkbox](#wc_stripe_display_save_payment_method_checkbox) - - [wc_stripe_payment_request_button_locale](#wc_stripe_payment_request_button_locale) - [woocommerce_add_cart_item](#woocommerce_add_cart_item) - [woocommerce_add_cart_item_data](#woocommerce_add_cart_item_data) - [woocommerce_add_to_cart_sold_individually_quantity](#woocommerce_add_to_cart_sold_individually_quantity) @@ -33,6 +30,7 @@ - [woocommerce_show_page_title](#woocommerce_show_page_title) - [woocommerce_store_api_disable_nonce_check](#woocommerce_store_api_disable_nonce_check) - [woocommerce_store_api_product_quantity_limit](#woocommerce_store_api_product_quantity_limit) + - [woocommerce_store_api_product_quantity_{$value_type}](#woocommerce_store_api_product_quantity_-value_type) - [woocommerce_variation_option_name](#woocommerce_variation_option_name) --- @@ -133,97 +131,6 @@ add_filter( '__experimental_woocommerce_blocks_payment_gateway_features_list', ' --- -## wc_stripe_allow_prepaid_card - - -Filters if prepaid cards are supported by Stripe. - -```php -apply_filters( 'wc_stripe_allow_prepaid_card', boolean $allow_prepaid_card ) -``` - -### Parameters - -| Argument | Type | Description | -| -------- | ---- | ----------- | -| $allow_prepaid_card | boolean | True if prepaid cards are allowed. | - -### Returns - - -`boolean` - -### Source - - - - [Payments/Integrations/Stripe.php](../src/Payments/Integrations/Stripe.php) - ---- - -## wc_stripe_display_save_payment_method_checkbox - - -Filters if the save payment method checkbox is shown for Stripe. - -```php -apply_filters( 'wc_stripe_display_save_payment_method_checkbox', boolean $saved_cards ) -``` - -### Description - -

This assumes that Stripe supports tokenization - currently this is true, based on https://github.com/woocommerce/woocommerce-gateway-stripe/blob/master/includes/class-wc-gateway-stripe.php#L95

- -### Parameters - -| Argument | Type | Description | -| -------- | ---- | ----------- | -| $saved_cards | boolean | True if saved cards functionality is enabled. | - -### Returns - - -`boolean` - -### See - - - - https://github.com/woocommerce/woocommerce-gateway-stripe/blob/ad19168b63df86176cbe35c3e95203a245687640/includes/class-wc-gateway-stripe.php#L271 - - https://github.com/woocommerce/woocommerce/wiki/Payment-Token-API - -### Source - - - - [Payments/Integrations/Stripe.php](../src/Payments/Integrations/Stripe.php) - ---- - -## wc_stripe_payment_request_button_locale - - -Filters the payment request button locale. - -```php -apply_filters( 'wc_stripe_payment_request_button_locale', string $locale ) -``` - -### Parameters - -| Argument | Type | Description | -| -------- | ---- | ----------- | -| $locale | string | Current locale. Defaults to en_US. | - -### Returns - - -`string` - -### Source - - - - [Payments/Integrations/Stripe.php](../src/Payments/Integrations/Stripe.php) - ---- - ## woocommerce_add_cart_item @@ -814,7 +721,40 @@ apply_filters( 'woocommerce_store_api_product_quantity_limit', integer $quantity ### Source - - [StoreApi/Schemas/ProductSchema.php](../src/StoreApi/Schemas/ProductSchema.php) + - [StoreApi/Utilities/QuantityLimits.php](../src/StoreApi/Utilities/QuantityLimits.php) + +--- + +## woocommerce_store_api_product_quantity_{$value_type} + + +Filters the quantity minimum for a cart item in Store API. This allows extensions to control the minimum qty of items already within the cart. + +```php +apply_filters( 'woocommerce_store_api_product_quantity_{$value_type}', mixed $value, \WC_Product $product, array|null $cart_item ) +``` + +### Description + +

The suffix of the hook will vary depending on the value being filtered. For example, minimum, maximum, multiple_of, editable.

+ +### Parameters + +| Argument | Type | Description | +| -------- | ---- | ----------- | +| $value | mixed | The value being filtered. | +| $product | \WC_Product | The product object. | +| $cart_item | array, null | The cart item if the product exists in the cart, or null. | + +### Returns + + +`mixed` + +### Source + + + - [StoreApi/Utilities/QuantityLimits.php](../src/StoreApi/Utilities/QuantityLimits.php) --- diff --git a/docs/testing/releases/671.md b/docs/testing/releases/671.md new file mode 100644 index 00000000000..1c7b773d9d4 --- /dev/null +++ b/docs/testing/releases/671.md @@ -0,0 +1,15 @@ +## Testing notes and ZIP for release 6.7.1 + +Zip file for testing: [woocommerce-gutenberg-products-block.zip](https://github.com/woocommerce/woocommerce-gutenberg-products-block/files/7829419/woocommerce-gutenberg-products-block.zip) + +## Feature Plugin + +### Convert token to string when setting the active payment method. ([5535](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5535)) + +1. On your site make sure you have Stripe set up +2. Create a new customer user for your website +3. Login with that user and add a product to the Cart block +4. In the Checkout block pay with a Stripe card and select Save payment information to my account for future purchases. +5. Successfully place the order +6. Do another purchase and make sure to select the saved card as a payment method +7. Notice that you can successfully place the order and no `payment_data[0][value] is not of type string.boolean` error is showed. diff --git a/docs/testing/releases/README.md b/docs/testing/releases/README.md index 85aa3c63e70..64754e18a89 100644 --- a/docs/testing/releases/README.md +++ b/docs/testing/releases/README.md @@ -55,3 +55,4 @@ Every release includes specific testing instructions for new features and bug fi - [6.5.0](./650.md) - [6.6.0](./660.md) - [6.7.0](./670.md) +- [6.7.1](./671.md) diff --git a/docs/testing/smoke-testing.md b/docs/testing/smoke-testing.md index 06e7c44bdd8..392f2d492f5 100644 --- a/docs/testing/smoke-testing.md +++ b/docs/testing/smoke-testing.md @@ -131,6 +131,6 @@ In the `wp:woocommerce/product-search` substitute the URL used for the `action` * [ ] Do critical flows for the Cart and Checkout blocks work? * [ ] Address and shipping calculations * [ ] Payment with core payment methods - * [ ] Payment with Stripe and saved payment methods + * [ ] Payment with Stripe (extension) and saved payment methods * [ ] Payment with Express payment methods (Chrome Pay or Apple Pay) * [ ] Make sure you test with logged in user and in browser incognito mode. diff --git a/package-lock.json b/package-lock.json index 77428903972..5b5938f2f5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,6 @@ "hasInstallScript": true, "license": "GPL-3.0+", "dependencies": { - "@stripe/react-stripe-js": "1.6.0", - "@stripe/stripe-js": "1.16.0", "@wordpress/autop": "3.2.3", "@wordpress/deprecated": "3.2.3", "@wordpress/icons": "6.1.1", @@ -36,7 +34,7 @@ }, "devDependencies": { "@automattic/color-studio": "2.5.0", - "@babel/cli": "7.16.7", + "@babel/cli": "7.16.8", "@babel/core": "7.16.7", "@babel/plugin-proposal-class-properties": "7.16.7", "@babel/plugin-syntax-jsx": "7.16.7", @@ -128,7 +126,7 @@ "merge-config": "2.0.0", "mini-css-extract-plugin": "1.3.6", "patch-package": "6.4.7", - "postcss": "8.2.10", + "postcss": "8.2.13", "postcss-loader": "4.2.0", "prettier": "npm:wp-prettier@2.0.5", "progress-bar-webpack-plugin": "2.1.0", @@ -432,9 +430,9 @@ "dev": true }, "node_modules/@babel/cli": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.7.tgz", - "integrity": "sha512-0iBF+G2Qml0y3mY5dirolyToLSR88a/KB6F2Gm8J/lOnyL8wbEOHak0DHF8gjc9XZGgTDGv/jYXNiapvsYyHTA==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.8.tgz", + "integrity": "sha512-FTKBbxyk5TclXOGmwYyqelqP5IF6hMxaeJskd85jbR5jBfYlwqgwAbJwnixi1ZBbTqKfFuAA95mdmUFeSRwyJA==", "dev": true, "dependencies": { "commander": "^4.0.1", @@ -7340,24 +7338,6 @@ "node": ">=8" } }, - "node_modules/@stripe/react-stripe-js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.6.0.tgz", - "integrity": "sha512-tMmsPD+wkpiiVJZgQ1E06tklG5MZHG462s6OWja9abpxq76kerAxMFN+KdhUg0LIEY79THbzvH3s/WGHasnV3w==", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "@stripe/stripe-js": "^1.19.1", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/@stripe/stripe-js": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.16.0.tgz", - "integrity": "sha512-ZSHbiwTrISoaTbpercmYGuY7QTg7HxfFyNgbJBaYbwHWbzMhpEdGTsmMpaBXIU6iiqwEEDaIyD8O6yJ+H5DWCg==" - }, "node_modules/@stylelint/postcss-css-in-js": { "version": "0.37.2", "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", @@ -30868,9 +30848,9 @@ } }, "node_modules/postcss": { - "version": "8.2.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz", - "integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==", + "version": "8.2.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.13.tgz", + "integrity": "sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ==", "dev": true, "dependencies": { "colorette": "^1.2.2", @@ -41775,9 +41755,9 @@ "dev": true }, "@babel/cli": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.7.tgz", - "integrity": "sha512-0iBF+G2Qml0y3mY5dirolyToLSR88a/KB6F2Gm8J/lOnyL8wbEOHak0DHF8gjc9XZGgTDGv/jYXNiapvsYyHTA==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.8.tgz", + "integrity": "sha512-FTKBbxyk5TclXOGmwYyqelqP5IF6hMxaeJskd85jbR5jBfYlwqgwAbJwnixi1ZBbTqKfFuAA95mdmUFeSRwyJA==", "dev": true, "requires": { "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", @@ -46739,19 +46719,6 @@ } } }, - "@stripe/react-stripe-js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.6.0.tgz", - "integrity": "sha512-tMmsPD+wkpiiVJZgQ1E06tklG5MZHG462s6OWja9abpxq76kerAxMFN+KdhUg0LIEY79THbzvH3s/WGHasnV3w==", - "requires": { - "prop-types": "^15.7.2" - } - }, - "@stripe/stripe-js": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.16.0.tgz", - "integrity": "sha512-ZSHbiwTrISoaTbpercmYGuY7QTg7HxfFyNgbJBaYbwHWbzMhpEdGTsmMpaBXIU6iiqwEEDaIyD8O6yJ+H5DWCg==" - }, "@stylelint/postcss-css-in-js": { "version": "0.37.2", "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", @@ -65557,9 +65524,9 @@ "dev": true }, "postcss": { - "version": "8.2.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz", - "integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==", + "version": "8.2.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.13.tgz", + "integrity": "sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ==", "dev": true, "requires": { "colorette": "^1.2.2", diff --git a/package.json b/package.json index cee251a61fb..1884a3dd09a 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ }, "devDependencies": { "@automattic/color-studio": "2.5.0", - "@babel/cli": "7.16.7", + "@babel/cli": "7.16.8", "@babel/core": "7.16.7", "@babel/plugin-proposal-class-properties": "7.16.7", "@babel/plugin-syntax-jsx": "7.16.7", @@ -168,7 +168,7 @@ "merge-config": "2.0.0", "mini-css-extract-plugin": "1.3.6", "patch-package": "6.4.7", - "postcss": "8.2.10", + "postcss": "8.2.13", "postcss-loader": "4.2.0", "prettier": "npm:wp-prettier@2.0.5", "progress-bar-webpack-plugin": "2.1.0", @@ -193,8 +193,6 @@ "npm": "^8.0.0" }, "dependencies": { - "@stripe/react-stripe-js": "1.6.0", - "@stripe/stripe-js": "1.16.0", "@wordpress/autop": "3.2.3", "@wordpress/deprecated": "3.2.3", "@wordpress/icons": "6.1.1", diff --git a/packages/checkout/blocks-registry/types.ts b/packages/checkout/blocks-registry/types.ts index b172f032ea4..7dbcea192f7 100644 --- a/packages/checkout/blocks-registry/types.ts +++ b/packages/checkout/blocks-registry/types.ts @@ -21,6 +21,7 @@ export enum innerBlockAreas { MINI_CART = 'woocommerce/mini-cart-contents', EMPTY_MINI_CART = 'woocommerce/empty-mini-cart-contents-block', FILLED_MINI_CART = 'woocommerce/filled-mini-cart-contents-block', + MINI_CART_ITEMS = 'woocommerce/mini-cart-items-block', } interface CheckoutBlockOptionsMetadata extends Partial< BlockConfiguration > { diff --git a/readme.txt b/readme.txt index 7b06844158d..7c6621967f0 100644 --- a/readme.txt +++ b/readme.txt @@ -85,6 +85,12 @@ Release and roadmap notes available on the [WooCommerce Developers Blog](https:/ == Changelog == += 6.7.1 - 2022-01-07 = + +#### Bug Fixes + +- Convert token to string when setting the active payment method. ([5535](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5535)) + = 6.7.0 - 2022-01-03 = #### Enhancements diff --git a/src/BlockTemplatesController.php b/src/BlockTemplatesController.php index 7ea5ca9ce1a..11ed5eace06 100644 --- a/src/BlockTemplatesController.php +++ b/src/BlockTemplatesController.php @@ -100,8 +100,8 @@ public function maybe_return_blocks_template( $template, $id, $template_type ) { // been unhooked so won't run again. add_filter( 'get_block_file_template', array( $this, 'get_single_block_template' ), 10, 3 ); $maybe_template = function_exists( 'gutenberg_get_block_template' ) ? - gutenberg_get_block_template( 'woocommerce//' . $slug, $template_type ) : - get_block_template( 'woocommerce//' . $slug, $template_type ); + gutenberg_get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//' . $slug, $template_type ) : + get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//' . $slug, $template_type ); // Re-hook this function, it was only unhooked to stop recursion. add_filter( 'pre_get_block_file_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 ); @@ -265,6 +265,11 @@ function( $template ) use ( $customised_template_slugs ) { * @return int[]|\WP_Post[] An array of found templates. */ public function get_block_templates_from_db( $slugs = array(), $template_type = 'wp_template' ) { + // This was the previously incorrect slug used to save DB templates against. + // To maintain compatibility with users sites who have already customised WooCommerce block templates using this slug we have to still use it to query those. + // More context found here: https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5423. + $invalid_plugin_slug = 'woocommerce'; + $check_query_args = array( 'post_type' => $template_type, 'posts_per_page' => -1, @@ -273,13 +278,15 @@ public function get_block_templates_from_db( $slugs = array(), $template_type = array( 'taxonomy' => 'wp_theme', 'field' => 'name', - 'terms' => array( 'woocommerce', get_stylesheet() ), + 'terms' => array( $invalid_plugin_slug, BlockTemplateUtils::PLUGIN_SLUG, get_stylesheet() ), ), ), ); + if ( is_array( $slugs ) && count( $slugs ) > 0 ) { $check_query_args['post_name__in'] = $slugs; } + $check_query = new \WP_Query( $check_query_args ); $saved_woo_templates = $check_query->posts; diff --git a/src/BlockTypes/AbstractProductGrid.php b/src/BlockTypes/AbstractProductGrid.php index 25bb57c4b02..b16f97702fc 100644 --- a/src/BlockTypes/AbstractProductGrid.php +++ b/src/BlockTypes/AbstractProductGrid.php @@ -31,6 +31,13 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock { */ protected $query_args = array(); + /** + * Meta query args. + * + * @var array + */ + protected $meta_query = array(); + /** * Get a set of attributes shared across most of the grid blocks. * @@ -50,6 +57,7 @@ protected function get_block_type_attributes() { 'align' => $this->get_schema_align(), 'alignButtons' => $this->get_schema_boolean( false ), 'isPreview' => $this->get_schema_boolean( false ), + 'stockStatus' => array_keys( wc_get_product_stock_status_options() ), ); } @@ -161,6 +169,7 @@ protected function parse_attributes( $attributes ) { 'rating' => true, 'button' => true, ), + 'stockStatus' => array_keys( wc_get_product_stock_status_options() ), ); return wp_parse_args( $attributes, $defaults ); @@ -172,6 +181,9 @@ protected function parse_attributes( $attributes ) { * @return array */ protected function parse_query_args() { + // Store the original meta query. + $this->meta_query = WC()->query->get_meta_query(); + $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', @@ -180,7 +192,7 @@ protected function parse_query_args() { 'no_found_rows' => false, 'orderby' => '', 'order' => '', - 'meta_query' => WC()->query->get_meta_query(), // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_query' => $this->meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery 'tax_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery 'posts_per_page' => $this->get_products_limit(), ); @@ -189,6 +201,7 @@ protected function parse_query_args() { $this->set_ordering_query_args( $query_args ); $this->set_categories_query_args( $query_args ); $this->set_visibility_query_args( $query_args ); + $this->set_stock_status_query_args( $query_args ); return $query_args; } @@ -272,6 +285,29 @@ protected function set_visibility_query_args( &$query_args ) { ); } + /** + * Set which stock status to use when displaying products. + * + * @param array $query_args Query args. + * @return void + */ + protected function set_stock_status_query_args( &$query_args ) { + // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query + if ( isset( $this->attributes['stockStatus'] ) && + ( array_keys( wc_get_product_stock_status_options() ) !== $this->attributes['stockStatus'] || [] !== $this->attributes['stockStatus'] ) + ) { + // Reset meta_query then update with our stock status. + $query_args['meta_query'] = $this->meta_query; + $query_args['meta_query'][] = array( + 'key' => '_stock_status', + 'value' => $this->attributes['stockStatus'], + ); + } else { + $query_args['meta_query'] = $this->meta_query; + } + // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + /** * Works out the item limit based on rows and columns, or returns default. * @@ -502,6 +538,7 @@ protected function get_title_html( $product ) { if ( empty( $this->attributes['contentVisibility']['title'] ) ) { return ''; } + return '
' . wp_kses_post( $product->get_title() ) . '
'; } @@ -625,5 +662,6 @@ protected function enqueue_data( array $attributes = [] ) { $this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true ); $this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true ); $this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true ); + $this->asset_data_registry->add( 'stock_status_options', wc_get_product_stock_status_options(), true ); } } diff --git a/src/BlockTypes/LegacyTemplate.php b/src/BlockTypes/LegacyTemplate.php index 38980836fdb..a9c0ce6a9b6 100644 --- a/src/BlockTypes/LegacyTemplate.php +++ b/src/BlockTypes/LegacyTemplate.php @@ -1,6 +1,8 @@ block_name ) !== $block['blockName'] ) { + return $content; + } + + $attributes = (array) $block['attrs']; + $align_class_and_style = StyleAttributesUtils::get_align_class_and_style( $attributes ); + + if ( ! isset( $align_class_and_style['class'] ) ) { + return $content; + } + + // Find the first tag. + $first_tag = '<[^<>]+>'; + $matches = array(); + preg_match( $first_tag, $content, $matches ); + + // If there is a tag, but it doesn't have a class attribute, add the class attribute. + if ( isset( $matches[0] ) && strpos( $matches[0], ' class=' ) === false ) { + $pattern_before_tag_closing = '/.+?(?=>)/'; + return preg_replace( $pattern_before_tag_closing, '$0 class="' . $align_class_and_style['class'] . '"', $content, 1 ); + } + + // If there is a tag, and it has a class already, add the class attribute. + $pattern_get_class = '/(?<=class=\"|\')[^"|\']+(?=\"|\')/'; + return preg_replace( $pattern_get_class, '$0 ' . $align_class_and_style['class'], $content, 1 ); + } + + } diff --git a/src/BlockTypes/ProductTag.php b/src/BlockTypes/ProductTag.php index f4be1af9545..9bb64ac43f5 100644 --- a/src/BlockTypes/ProductTag.php +++ b/src/BlockTypes/ProductTag.php @@ -48,6 +48,7 @@ protected function get_block_type_attributes() { 'default' => 'any', ), 'isPreview' => $this->get_schema_boolean( false ), + 'stockStatus' => array_keys( wc_get_product_stock_status_options() ), ); } diff --git a/src/BlockTypes/ProductsByAttribute.php b/src/BlockTypes/ProductsByAttribute.php index 9d559b89cc2..8bbf6363f50 100644 --- a/src/BlockTypes/ProductsByAttribute.php +++ b/src/BlockTypes/ProductsByAttribute.php @@ -67,6 +67,7 @@ protected function get_block_type_attributes() { 'orderby' => $this->get_schema_orderby(), 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), 'isPreview' => $this->get_schema_boolean( false ), + 'stockStatus' => array_keys( wc_get_product_stock_status_options() ), ); } } diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index d25b2e3334c..9617df9b902 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -12,7 +12,6 @@ use Automattic\WooCommerce\Blocks\RestApi; use Automattic\WooCommerce\Blocks\Payments\Api as PaymentsApi; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; -use Automattic\WooCommerce\Blocks\Payments\Integrations\Stripe; use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque; use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal; use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer; @@ -300,18 +299,8 @@ function ( Container $container ) { /** * Register payment method integrations with the container. - * - * @internal Stripe 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 register_payment_methods() { - $this->container->register( - Stripe::class, - function( Container $container ) { - $asset_api = $container->get( AssetApi::class ); - return new Stripe( $asset_api ); - } - ); $this->container->register( Cheque::class, function( Container $container ) { diff --git a/src/Payments/Api.php b/src/Payments/Api.php index 72a2bbe0e70..d6154fdcd7f 100644 --- a/src/Payments/Api.php +++ b/src/Payments/Api.php @@ -4,7 +4,6 @@ use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\NoticeHandler; -use Automattic\WooCommerce\Blocks\Payments\Integrations\Stripe; use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque; use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal; use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer; @@ -105,12 +104,6 @@ public function add_payment_method_script_data() { * @param PaymentMethodRegistry $payment_method_registry Payment method registry instance. */ public function register_payment_method_integrations( PaymentMethodRegistry $payment_method_registry ) { - // This is temporarily registering Stripe until it's moved to the extension. - if ( class_exists( '\WC_Stripe', false ) && ! $payment_method_registry->is_registered( 'stripe' ) ) { - $payment_method_registry->register( - Package::container()->get( Stripe::class ) - ); - } $payment_method_registry->register( Package::container()->get( Cheque::class ) ); diff --git a/src/Payments/Integrations/Stripe.php b/src/Payments/Integrations/Stripe.php deleted file mode 100644 index b6594a90372..00000000000 --- a/src/Payments/Integrations/Stripe.php +++ /dev/null @@ -1,367 +0,0 @@ -asset_api = $asset_api; - add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_payment_request_order_meta' ], 8, 2 ); - add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_stripe_intents' ], 9999, 2 ); - } - - /** - * Initializes the payment method type. - */ - public function initialize() { - $this->settings = get_option( 'woocommerce_stripe_settings', [] ); - } - - /** - * Returns if this payment method should be active. If false, the scripts will not be enqueued. - * - * @return boolean - */ - public function is_active() { - return ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled']; - } - - /** - * Returns an array of scripts/handles to be registered for this payment method. - * - * @return array - */ - public function get_payment_method_script_handles() { - $this->asset_api->register_script( - 'wc-payment-method-stripe', - 'build/wc-payment-method-stripe.js', - [] - ); - - return [ 'wc-payment-method-stripe' ]; - } - - /** - * Returns an array of key=>value pairs of data made available to the payment methods script. - * - * @return array - */ - public function get_payment_method_data() { - return [ - 'stripeTotalLabel' => $this->get_total_label(), - 'publicKey' => $this->get_publishable_key(), - 'allowPrepaidCard' => $this->get_allow_prepaid_card(), - 'title' => $this->get_title(), - 'button' => [ - 'type' => $this->get_button_type(), - 'theme' => $this->get_button_theme(), - 'height' => $this->get_button_height(), - 'locale' => $this->get_button_locale(), - ], - 'inline_cc_form' => $this->get_inline_cc_form(), - 'icons' => $this->get_icons(), - 'showSavedCards' => $this->get_show_saved_cards(), - 'allowPaymentRequest' => $this->get_allow_payment_request(), - 'showSaveOption' => $this->get_show_save_option(), - 'supports' => $this->get_supported_features(), - ]; - } - - /** - * Determine if store allows cards to be saved during checkout. - * - * @return bool True if merchant allows shopper to save card (payment method) during checkout). - */ - private function get_show_saved_cards() { - return isset( $this->settings['saved_cards'] ) ? 'yes' === $this->settings['saved_cards'] : false; - } - - /** - * Determine if the checkbox to enable the user to save their payment method should be shown. - * - * @return bool True if the save payment checkbox should be displayed to the user. - */ - private function get_show_save_option() { - $saved_cards = $this->get_show_saved_cards(); - /** - * Filters if the save payment method checkbox is shown for Stripe. - * - * This assumes that Stripe supports `tokenization` - currently this is true, based on https://github.com/woocommerce/woocommerce-gateway-stripe/blob/master/includes/class-wc-gateway-stripe.php#L95 - * - * @see https://github.com/woocommerce/woocommerce-gateway-stripe/blob/ad19168b63df86176cbe35c3e95203a245687640/includes/class-wc-gateway-stripe.php#L271 - * @see https://github.com/woocommerce/woocommerce/wiki/Payment-Token-API - * - * @param boolean $saved_cards True if saved cards functionality is enabled. - * @return boolean - */ - return apply_filters( 'wc_stripe_display_save_payment_method_checkbox', filter_var( $saved_cards, FILTER_VALIDATE_BOOLEAN ) ); - } - - /** - * Returns the label to use accompanying the total in the stripe statement. - * - * @return string Statement descriptor. - */ - private function get_total_label() { - return ! empty( $this->settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->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->settings['testmode'] ) && 'yes' === $this->settings['testmode'] ); - $setting_key = $test_mode ? 'test_publishable_key' : 'publishable_key'; - return ! empty( $this->settings[ $setting_key ] ) ? $this->settings[ $setting_key ] : ''; - } - - /** - * Returns whether to allow prepaid cards for payments. - * - * @return bool True means to allow prepaid card (default). - */ - private function get_allow_prepaid_card() { - /** - * Filters if prepaid cards are supported by Stripe. - * - * @param boolean $allow_prepaid_card True if prepaid cards are allowed. - * @return boolean - */ - return apply_filters( 'wc_stripe_allow_prepaid_card', true ); - } - - /** - * Returns the title string to use in the UI (customisable via admin settings screen). - * - * @return string Title / label string - */ - private function get_title() { - return isset( $this->settings['title'] ) ? $this->settings['title'] : __( 'Credit / Debit Card', 'woo-gutenberg-products-block' ); - } - - /** - * Determine if store allows Payment Request buttons - e.g. Apple Pay / Chrome Pay. - * - * @return bool True if merchant has opted into payment request. - */ - private function get_allow_payment_request() { - $option = isset( $this->settings['payment_request'] ) ? $this->settings['payment_request'] : false; - return filter_var( $option, FILTER_VALIDATE_BOOLEAN ); - } - - /** - * Return the button type for the payment button. - * - * @return string Defaults to 'default'. - */ - private function get_button_type() { - return isset( $this->settings['payment_request_button_type'] ) ? $this->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->settings['payment_request_button_theme'] ) ? $this->settings['payment_request_button_theme'] : 'dark'; - } - - /** - * Return the height for the payment button. - * - * @return string A pixel value for the height (defaults to '64'). - */ - private function get_button_height() { - return isset( $this->settings['payment_request_button_height'] ) ? str_replace( 'px', '', $this->settings['payment_request_button_height'] ) : '64'; - } - - /** - * Return the inline cc option. - * - * @return boolean True if the inline CC form option is enabled. - */ - private function get_inline_cc_form() { - return isset( $this->settings['inline_cc_form'] ) && 'yes' === $this->settings['inline_cc_form']; - } - - /** - * Return the locale for the payment button. - * - * @return string Defaults to en_US. - */ - private function get_button_locale() { - /** - * Filters the payment request button locale. - * - * @param string $locale Current locale. Defaults to en_US. - * @return string - */ - return apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ); - } - - /** - * Return the icons urls. - * - * @return array Arrays of icons metadata. - */ - private function get_icons() { - $icons_src = [ - 'visa' => [ - 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg', - 'alt' => __( 'Visa', 'woo-gutenberg-products-block' ), - ], - 'amex' => [ - 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg', - 'alt' => __( 'American Express', 'woo-gutenberg-products-block' ), - ], - 'mastercard' => [ - 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg', - 'alt' => __( 'Mastercard', 'woo-gutenberg-products-block' ), - ], - ]; - - if ( 'USD' === get_woocommerce_currency() ) { - $icons_src['discover'] = [ - 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg', - 'alt' => __( 'Discover', 'woo-gutenberg-products-block' ), - ]; - $icons_src['jcb'] = [ - 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg', - 'alt' => __( 'JCB', 'woo-gutenberg-products-block' ), - ]; - $icons_src['diners'] = [ - 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg', - 'alt' => __( 'Diners', 'woo-gutenberg-products-block' ), - ]; - } - return $icons_src; - } - - /** - * Add payment request data to the order meta as hooked on the - * woocommerce_rest_checkout_process_payment_with_context action. - * - * @param PaymentContext $context Holds context for the payment. - * @param PaymentResult $result Result object for the payment. - */ - public function add_payment_request_order_meta( PaymentContext $context, PaymentResult &$result ) { - $data = $context->payment_data; - if ( ! empty( $data['payment_request_type'] ) && 'stripe' === $context->payment_method ) { - // phpcs:ignore WordPress.Security.NonceVerification - $post_data = $_POST; - $_POST = $context->payment_data; - $this->add_order_meta( $context->order, $data['payment_request_type'] ); - $_POST = $post_data; - } - - // hook into stripe error processing so that we can capture the error to - // payment details (which is added to notices and thus not helpful for - // this context). - if ( 'stripe' === $context->payment_method ) { - add_action( - 'wc_gateway_stripe_process_payment_error', - function( $error ) use ( &$result ) { - $payment_details = $result->payment_details; - $payment_details['errorMessage'] = wp_strip_all_tags( $error->getLocalizedMessage() ); - $result->set_payment_details( $payment_details ); - } - ); - } - } - - /** - * Handles any potential stripe intents on the order that need handled. - * - * This is configured to execute after legacy payment processing has - * happened on the woocommerce_rest_checkout_process_payment_with_context - * action hook. - * - * @param PaymentContext $context Holds context for the payment. - * @param PaymentResult $result Result object for the payment. - */ - public function add_stripe_intents( PaymentContext $context, PaymentResult &$result ) { - if ( 'stripe' === $context->payment_method - && ( - ! empty( $result->payment_details['payment_intent_secret'] ) - || ! empty( $result->payment_details['setup_intent_secret'] ) - ) - ) { - $payment_details = $result->payment_details; - $payment_details['verification_endpoint'] = add_query_arg( - [ - 'order' => $context->order->get_id(), - 'nonce' => wp_create_nonce( 'wc_stripe_confirm_pi' ), - 'redirect_to' => rawurlencode( $result->redirect_url ), - ], - home_url() . \WC_Ajax::get_endpoint( 'wc_stripe_verify_intent' ) - ); - $result->set_payment_details( $payment_details ); - $result->set_status( 'success' ); - } - } - - /** - * Handles adding information about the payment request type used to the order meta. - * - * @param \WC_Order $order The order being processed. - * @param string $payment_request_type The payment request type used for payment. - */ - private function add_order_meta( \WC_Order $order, string $payment_request_type ) { - if ( 'apple_pay' === $payment_request_type ) { - $order->set_payment_method_title( 'Apple Pay (Stripe)' ); - $order->save(); - } - - if ( 'payment_request_api' === $payment_request_type ) { - $order->set_payment_method_title( 'Chrome Payment Request (Stripe)' ); - $order->save(); - } - } - - /** - * Returns an array of supported features. - * - * @return string[] - */ - public function get_supported_features() { - $gateway = new WC_Gateway_Stripe(); - return array_filter( $gateway->supports, array( $gateway, 'supports' ) ); - } -} diff --git a/src/StoreApi/Routes/CartAddItem.php b/src/StoreApi/Routes/CartAddItem.php index 113a9b7e536..09aa901d733 100644 --- a/src/StoreApi/Routes/CartAddItem.php +++ b/src/StoreApi/Routes/CartAddItem.php @@ -29,15 +29,13 @@ public function get_args() { 'permission_callback' => '__return_true', 'args' => [ 'id' => [ - 'description' => __( 'The cart item product or variation ID.', 'woo-gutenberg-products-block' ), - 'type' => 'integer', - 'context' => [ 'view', 'edit' ], - 'arg_options' => [ - 'sanitize_callback' => 'absint', - ], + 'description' => __( 'The cart item product or variation ID.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'sanitize_callback' => 'absint', ], 'quantity' => [ - 'description' => __( 'Quantity of this item in the cart.', 'woo-gutenberg-products-block' ), + 'description' => __( 'Quantity of this item to add to the cart.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'arg_options' => [ diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index f384d970dff..ba6ab3fe002 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -2,7 +2,7 @@ namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\DraftOrderTrait; -use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; +use Automattic\WooCommerce\Blocks\StoreApi\Utilities\QuantityLimits; /** * CartItemSchema class. * @@ -47,15 +47,43 @@ public function get_properties() { ], 'quantity' => [ 'description' => __( 'Quantity of this item in the cart.', 'woo-gutenberg-products-block' ), - 'type' => 'integer', + 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'quantity_limit' => [ - 'description' => __( 'The maximum quantity than can be added to the cart at once.', 'woo-gutenberg-products-block' ), - 'type' => 'integer', + 'quantity_limits' => [ + 'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woo-gutenberg-products-block' ), + 'type' => 'object', 'context' => [ 'view', 'edit' ], 'readonly' => true, + 'properties' => [ + 'minimum' => [ + 'description' => __( 'The minimum quantity allowed in the cart for this line item.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'maximum' => [ + 'description' => __( 'The maximum quantity allowed in the cart for this line item.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'multiple_of' => [ + 'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + 'default' => 1, + ], + 'editable' => [ + 'description' => __( 'If the quantity in the cart is editable or fixed.', 'woo-gutenberg-products-block' ), + 'type' => 'boolean', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + 'default' => true, + ], + ], ], 'name' => [ 'description' => __( 'Product name.', 'woo-gutenberg-products-block' ), @@ -313,7 +341,7 @@ public function get_item_response( $cart_item ) { 'key' => $cart_item['key'], 'id' => $product->get_id(), 'quantity' => wc_stock_amount( $cart_item['quantity'] ), - 'quantity_limit' => $this->get_product_quantity_limit( $product ), + 'quantity_limits' => (object) ( new QuantityLimits() )->get_cart_item_quantity_limits( $cart_item ), 'name' => $this->prepare_html_response( $product->get_title() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), @@ -363,25 +391,6 @@ protected function prepare_product_price_response( \WC_Product $product, $tax_di return $prices; } - /** - * Returns the remaining stock for a product if it has stock. - * - * This also factors in draft orders. - * - * @param \WC_Product $product Product instance. - * @return integer|null - */ - protected function get_remaining_stock( \WC_Product $product ) { - if ( is_null( $product->get_stock_quantity() ) ) { - return null; - } - - $reserve_stock = new ReserveStock(); - $reserved_stock = $reserve_stock->get_reserved_stock( $product, $this->get_draft_order_id() ); - - return $product->get_stock_quantity() - $reserved_stock; - } - /** * Format variation data, for example convert slugs such as attribute_pa_size to Size. * @@ -452,8 +461,7 @@ protected function get_item_data( $cart_item ) { } /** - * Remove HTML tags from cart item data and set the `hidden` property to - * `__experimental_woocommerce_blocks_hidden`. + * Remove HTML tags from cart item data and set the `hidden` property to `__experimental_woocommerce_blocks_hidden`. * * @param array $item_data_element Individual element of a cart item data. * @return array diff --git a/src/StoreApi/Schemas/ProductCategorySchema.php b/src/StoreApi/Schemas/ProductCategorySchema.php index 92e2165fa3b..8e52ebc3ffe 100644 --- a/src/StoreApi/Schemas/ProductCategorySchema.php +++ b/src/StoreApi/Schemas/ProductCategorySchema.php @@ -111,14 +111,12 @@ protected function get_category_review_count( $term ) { $terms_to_count_str = implode( ',', $terms_to_count ); } - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $products_of_category_sql = $wpdb->prepare( - "SELECT SUM(comment_count) as review_count + $products_of_category_sql = " + SELECT SUM(comment_count) as review_count FROM {$wpdb->posts} AS posts INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id - WHERE term_relationships.term_taxonomy_id IN ({$terms_to_count_str})" - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + WHERE term_relationships.term_taxonomy_id IN (" . esc_sql( $terms_to_count_str ) . ') + '; $review_count = $wpdb->get_var( $products_of_category_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared diff --git a/src/StoreApi/Schemas/ProductSchema.php b/src/StoreApi/Schemas/ProductSchema.php index 278e6f666ba..fbcae6fde03 100644 --- a/src/StoreApi/Schemas/ProductSchema.php +++ b/src/StoreApi/Schemas/ProductSchema.php @@ -2,7 +2,7 @@ namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas; use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi; - +use Automattic\WooCommerce\Blocks\StoreApi\Utilities\QuantityLimits; /** * ProductSchema class. @@ -392,12 +392,6 @@ public function get_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'quantity_limit' => [ - 'description' => __( 'The maximum quantity than can be added to the cart at once.', 'woo-gutenberg-products-block' ), - 'type' => 'integer', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - ], 'add_to_cart' => [ 'description' => __( 'Add to cart button parameters.', 'woo-gutenberg-products-block' ), 'type' => 'object', @@ -422,6 +416,25 @@ public function get_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], + 'minimum' => [ + 'description' => __( 'The minimum quantity that can be added to the cart.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'maximum' => [ + 'description' => __( 'The maximum quantity that can be added to the cart.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'multiple_of' => [ + 'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + 'default' => 1, + ], ], ], ]; @@ -460,13 +473,13 @@ public function get_item_response( $product ) { 'is_on_backorder' => 'onbackorder' === $product->get_stock_status(), 'low_stock_remaining' => $this->get_low_stock_remaining( $product ), 'sold_individually' => $product->is_sold_individually(), - 'quantity_limit' => $this->get_product_quantity_limit( $product ), - 'add_to_cart' => (object) $this->prepare_html_response( + 'add_to_cart' => (object) array_merge( [ - 'text' => $product->add_to_cart_text(), - 'description' => $product->add_to_cart_description(), - 'url' => $product->add_to_cart_url(), - ] + 'text' => $this->prepare_html_response( $product->add_to_cart_text() ), + 'description' => $this->prepare_html_response( $product->add_to_cart_description() ), + 'url' => $this->prepare_html_response( $product->add_to_cart_url() ), + ], + ( new QuantityLimits() )->get_add_to_cart_limits( $product ) ), ]; } @@ -512,33 +525,6 @@ protected function get_low_stock_remaining( \WC_Product $product ) { return null; } - /** - * Get the quantity limit for an item in the cart. - * - * @param \WC_Product $product Product instance. - * @return int - */ - protected function get_product_quantity_limit( \WC_Product $product ) { - $limits = [ 99 ]; - - if ( $product->is_sold_individually() ) { - $limits[] = 1; - } elseif ( ! $product->backorders_allowed() ) { - $limits[] = $this->get_remaining_stock( $product ); - } - - /** - * Filters the quantity limit for a product being added to the cart via the Store API. - * - * Filters the variation option name for custom option slugs. - * - * @param integer $quantity_limit Quantity limit which defaults to 99 unless sold individually. - * @param \WC_Product $product Product instance. - * @return integer - */ - return apply_filters( 'woocommerce_store_api_product_quantity_limit', max( min( array_filter( $limits ) ), 1 ), $product ); - } - /** * Returns true if the given attribute is valid. * diff --git a/src/StoreApi/Utilities/CartController.php b/src/StoreApi/Utilities/CartController.php index 5316d2c3fef..5689a30a080 100644 --- a/src/StoreApi/Utilities/CartController.php +++ b/src/StoreApi/Utilities/CartController.php @@ -4,6 +4,7 @@ use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\DraftOrderTrait; use Automattic\WooCommerce\Blocks\StoreApi\Utilities\NoticeHandler; +use Automattic\WooCommerce\Blocks\StoreApi\Utilities\QuantityLimits; use Automattic\WooCommerce\Blocks\Utils\ArrayUtils; use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; use WP_Error; @@ -62,25 +63,33 @@ public function add_to_cart( $request ) { $this->validate_add_to_cart( $product, $request ); + $quantity_limits = new QuantityLimits(); $existing_cart_id = $cart->find_product_in_cart( $cart_id ); if ( $existing_cart_id ) { - if ( $product->is_sold_individually() ) { - throw new RouteException( - 'woocommerce_rest_cart_product_sold_individually', - sprintf( - /* translators: %s: product name */ - __( 'You cannot add another "%s" to your cart.', 'woo-gutenberg-products-block' ), - $product->get_name() - ), - 400 - ); + $cart_item = $cart->cart_contents[ $existing_cart_id ]; + $quantity_validation = $quantity_limits->validate_cart_item_quantity( $request['quantity'] + $cart_item['quantity'], $cart_item ); + + if ( is_wp_error( $quantity_validation ) ) { + throw new RouteException( $quantity_validation->get_error_code(), $quantity_validation->get_error_message(), 400 ); } + $cart->set_quantity( $existing_cart_id, $request['quantity'] + $cart->cart_contents[ $existing_cart_id ]['quantity'], true ); return $existing_cart_id; } + // Normalize quantity. + $add_to_cart_limits = $quantity_limits->get_add_to_cart_limits( $product ); + $request_quantity = (int) $request['quantity']; + + if ( $add_to_cart_limits['maximum'] ) { + $request_quantity = min( $request_quantity, $add_to_cart_limits['maximum'] ); + } + + $request_quantity = max( $request_quantity, $add_to_cart_limits['minimum'] ); + $request_quantity = $quantity_limits->limit_to_multiple( $request_quantity, $add_to_cart_limits['multiple_of'] ); + /** * Filters the item being added to the cart. * @@ -97,7 +106,7 @@ public function add_to_cart( $request ) { 'product_id' => $this->get_product_id( $product ), 'variation_id' => $this->get_variation_id( $product ), 'variation' => $request['variation'], - 'quantity' => $request['quantity'], + 'quantity' => $request_quantity, 'data' => $product, 'data_hash' => wc_get_cart_item_data_hash( $product ), ) @@ -121,7 +130,7 @@ public function add_to_cart( $request ) { * * @param string $cart_id ID of the item in the cart. * @param integer $product_id ID of the product added to the cart. - * @param integer $quantity Quantity of the item added to the cart. + * @param integer $request_quantity Quantity of the item added to the cart. * @param integer $variation_id Variation ID of the product added to the cart. * @param array $variation Array of variation data. * @param array $cart_item_data Array of other cart item data. @@ -130,7 +139,7 @@ public function add_to_cart( $request ) { 'woocommerce_add_to_cart', $cart_id, $this->get_product_id( $product ), - $request['quantity'], + $request_quantity, $this->get_variation_id( $product ), $request['variation'], $request['cart_item_data'] @@ -140,7 +149,8 @@ public function add_to_cart( $request ) { } /** - * Based on core `set_quantity` method, but validates if an item is sold individually first. + * Based on core `set_quantity` method, but validates if an item is sold individually first and enforces any limits in + * place. * * @throws RouteException Exception if invalid data is detected. * @@ -160,17 +170,12 @@ public function set_cart_item_quantity( $item_id, $quantity = 1 ) { throw new RouteException( 'woocommerce_rest_cart_invalid_product', __( 'Cart item is invalid.', 'woo-gutenberg-products-block' ), 404 ); } - if ( $product->is_sold_individually() && $quantity > 1 ) { - throw new RouteException( - 'woocommerce_rest_cart_product_sold_individually', - sprintf( - /* translators: %s: product name */ - __( 'You cannot add another "%s" to your cart.', 'woo-gutenberg-products-block' ), - $product->get_name() - ), - 400 - ); + $quantity_validation = ( new QuantityLimits() )->validate_cart_item_quantity( $quantity, $cart_item ); + + if ( is_wp_error( $quantity_validation ) ) { + throw new RouteException( $quantity_validation->get_error_code(), $quantity_validation->get_error_message(), 400 ); } + $cart = $this->get_cart_instance(); $cart->set_quantity( $item_id, $quantity ); } diff --git a/src/StoreApi/Utilities/QuantityLimits.php b/src/StoreApi/Utilities/QuantityLimits.php new file mode 100644 index 00000000000..f6a8f8bea41 --- /dev/null +++ b/src/StoreApi/Utilities/QuantityLimits.php @@ -0,0 +1,211 @@ + 1, + 'maximum' => null, + 'multiple_of' => 1, + 'editable' => true, + ]; + } + + $multiple_of = (int) $this->filter_value( 1, 'multiple_of', $cart_item ); + $minimum = (int) $this->filter_value( 1, 'minimum', $cart_item ); + $maximum = (int) $this->filter_value( $this->get_product_quantity_limit( $product ), 'maximum', $cart_item ); + $editable = (bool) $this->filter_value( ! $product->is_sold_individually(), 'editable', $cart_item ); + + return [ + 'minimum' => $this->limit_to_multiple( $minimum, $multiple_of, 'ceil' ), + 'maximum' => $this->limit_to_multiple( $maximum, $multiple_of, 'floor' ), + 'multiple_of' => $multiple_of, + 'editable' => $editable, + ]; + } + + /** + * Get limits for product add to cart forms. + * + * @param \WC_Product $product Product instance. + * @return array + */ + public function get_add_to_cart_limits( \WC_Product $product ) { + $multiple_of = $this->filter_value( 1, 'multiple_of', $product ); + $minimum = $this->filter_value( 1, 'minimum', $product ); + $maximum = $this->filter_value( $this->get_product_quantity_limit( $product ), 'maximum', $product ); + + return [ + 'minimum' => $this->limit_to_multiple( $minimum, $multiple_of, 'ceil' ), + 'maximum' => $this->limit_to_multiple( $maximum, $multiple_of, 'floor' ), + 'multiple_of' => $multiple_of, + ]; + } + + /** + * Return a number using the closest multiple of another number. Used to enforce step/multiple values. + * + * @param int $number Number to round. + * @param int $multiple_of The multiple. + * @param string $rounding_function ceil, floor, or round. + * @return int + */ + public function limit_to_multiple( int $number, int $multiple_of, string $rounding_function = 'round' ) { + if ( $multiple_of <= 1 ) { + return $number; + } + $rounding_function = in_array( $rounding_function, [ 'ceil', 'floor', 'round' ], true ) ? $rounding_function : 'round'; + return $rounding_function( $number / $multiple_of ) * $multiple_of; + } + + /** + * Check that a given quantity is valid according to any limits in place. + * + * @param integer $quantity Quantity to validate. + * @param \WC_Product|array $cart_item Cart item. + * @return \WP_Error|true + */ + public function validate_cart_item_quantity( $quantity, $cart_item ) { + $limits = $this->get_cart_item_quantity_limits( $cart_item ); + + if ( ! $limits['editable'] ) { + return new \WP_Error( + 'readonly_quantity', + __( 'This item is already in the cart and it\'s quantity cannot be edited', 'woo-gutenberg-products-block' ) + ); + } + + if ( $quantity < $limits['minimum'] ) { + return new \WP_Error( + 'invalid_quantity', + sprintf( + // Translators: %s amount. + __( 'The minimum quantity that can be added to the cart is %s', 'woo-gutenberg-products-block' ), + $limits['minimum'] + ) + ); + } + + if ( $quantity > $limits['maximum'] ) { + return new \WP_Error( + 'invalid_quantity', + sprintf( + // Translators: %s amount. + __( 'The maximum quantity that can be added to the cart is %s', 'woo-gutenberg-products-block' ), + $limits['maximum'] + ) + ); + } + + if ( $quantity % $limits['multiple_of'] ) { + return new \WP_Error( + 'invalid_quantity', + sprintf( + // Translators: %s amount. + __( 'The quantity added to the cart must be a multiple of %s', 'woo-gutenberg-products-block' ), + $limits['multiple_of'] + ) + ); + } + + return true; + } + + /** + * Get the limit for the total number of a product allowed in the cart. + * + * This is based on product properties, including remaining stock, and defaults to a maximum of 99 of any product + * in the cart at once. + * + * @param \WC_Product $product Product instance. + * @return int + */ + protected function get_product_quantity_limit( \WC_Product $product ) { + $limits = [ 99 ]; + + if ( $product->is_sold_individually() ) { + $limits[] = 1; + } elseif ( ! $product->backorders_allowed() ) { + $limits[] = $this->get_remaining_stock( $product ); + } + + /** + * Filters the quantity limit for a product being added to the cart via the Store API. + * + * Filters the variation option name for custom option slugs. + * + * @param integer $quantity_limit Quantity limit which defaults to 99 unless sold individually. + * @param \WC_Product $product Product instance. + * @return integer + */ + return apply_filters( 'woocommerce_store_api_product_quantity_limit', max( min( array_filter( $limits ) ), 1 ), $product ); + } + + /** + * Returns the remaining stock for a product if it has stock. + * + * This also factors in draft orders. + * + * @param \WC_Product $product Product instance. + * @return integer|null + */ + protected function get_remaining_stock( \WC_Product $product ) { + if ( is_null( $product->get_stock_quantity() ) ) { + return null; + } + + $reserve_stock = new ReserveStock(); + $reserved_stock = $reserve_stock->get_reserved_stock( $product, $this->get_draft_order_id() ); + + return $product->get_stock_quantity() - $reserved_stock; + } + + /** + * Get a quantity for a product or cart item by running it through a filter hook. + * + * @param int|null $value Value to filter. + * @param string $value_type Type of value. Used for filter suffix. + * @param \WC_Product|array $cart_item_or_product Either a cart item or a product instance. + * @return mixed + */ + protected function filter_value( $value, string $value_type, $cart_item_or_product ) { + $is_product = $cart_item_or_product instanceof \WC_Product; + $product = $is_product ? $cart_item_or_product : $cart_item_or_product['data']; + $cart_item = $is_product ? null : $cart_item_or_product; + /** + * Filters the quantity minimum for a cart item in Store API. This allows extensions to control the minimum qty + * of items already within the cart. + * + * The suffix of the hook will vary depending on the value being filtered. + * For example, minimum, maximum, multiple_of, editable. + * + * @param mixed $value The value being filtered. + * @param \WC_Product $product The product object. + * @param array|null $cart_item The cart item if the product exists in the cart, or null. + * @return mixed + */ + return apply_filters( "woocommerce_store_api_product_quantity_{$value_type}", $value, $product, $cart_item ); + } +} diff --git a/src/StoreApi/docs/cart-items.md b/src/StoreApi/docs/cart-items.md index 01f17a83c1a..8c9ce3bbc6b 100644 --- a/src/StoreApi/docs/cart-items.md +++ b/src/StoreApi/docs/cart-items.md @@ -23,152 +23,162 @@ curl "https://example-store.com/wp-json/wc/store/cart/items" ```json [ - { - "key": "9bf31c7ff062936a96d3c8bd1f8f2ff3", - "id": 15, - "quantity": 1, - "quantity_limit": 99, - "name": "Beanie", - "summary": "

This is a simple product.<\/p>", - "short_description": "

This is a simple product.<\/p>", - "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.<\/p>", - "sku": "woo-beanie", - "low_stock_remaining": null, - "backorders_allowed": false, - "show_backorder_badge": false, - "sold_individually": false, - "permalink": "https:\/\/local.wordpress.test\/product\/beanie\/", - "images": [ - { - "id": 44, - "src": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2.jpg", - "thumbnail": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-324x324.jpg", - "srcset": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2.jpg 801w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-324x324.jpg 324w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-100x100.jpg 100w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-416x416.jpg 416w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-300x300.jpg 300w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-150x150.jpg 150w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/beanie-2-768x768.jpg 768w", - "sizes": "(max-width: 801px) 100vw, 801px", - "name": "beanie-2.jpg", - "alt": "" - } - ], - "variation": [], - "item_data": [], - "prices": { - "currency_code": "GBP", - "currency_symbol": "£", - "currency_minor_unit": 2, - "currency_decimal_separator": ".", - "currency_thousand_separator": ",", - "currency_prefix": "£", - "currency_suffix": "", - "price": "1000", - "regular_price": "2000", - "sale_price": "1000", - "price_range": null, - "raw_prices": { - "precision": 6, - "price": "10000000", - "regular_price": "20000000", - "sale_price": "10000000" - } - }, - "totals": { - "currency_code": "GBP", - "currency_symbol": "£", - "currency_minor_unit": 2, - "currency_decimal_separator": ".", - "currency_thousand_separator": ",", - "currency_prefix": "£", - "currency_suffix": "", - "line_subtotal": "1000", - "line_subtotal_tax": "0", - "line_total": "800", - "line_total_tax": "0" - }, - "_links": { - "self": [ - { - "href": "https:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/items\/9bf31c7ff062936a96d3c8bd1f8f2ff3" - } - ], - "collection": [ - { - "href": "https:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/items" - } - ] - } - }, - { - "key": "e369853df766fa44e1ed0ff613f563bd", - "id": 34, - "quantity": 1, - "quantity_limit": 99, - "name": "WordPress Pennant", - "summary": "

This is an external product.<\/p>", - "short_description": "

This is an external product.<\/p>", - "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.<\/p>", - "sku": "wp-pennant", - "low_stock_remaining": null, - "backorders_allowed": false, - "show_backorder_badge": false, - "sold_individually": false, - "permalink": "https:\/\/local.wordpress.test\/product\/wordpress-pennant\/", - "images": [ - { - "id": 57, - "src": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1.jpg", - "thumbnail": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-324x324.jpg", - "srcset": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1.jpg 800w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-324x324.jpg 324w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-100x100.jpg 100w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-416x416.jpg 416w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-300x300.jpg 300w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-150x150.jpg 150w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-768x768.jpg 768w", - "sizes": "(max-width: 800px) 100vw, 800px", - "name": "pennant-1.jpg", - "alt": "" - } - ], - "variation": [], - "item_data": [], - "prices": { - "currency_code": "GBP", - "currency_symbol": "£", - "currency_minor_unit": 2, - "currency_decimal_separator": ".", - "currency_thousand_separator": ",", - "currency_prefix": "£", - "currency_suffix": "", - "price": "1105", - "regular_price": "1105", - "sale_price": "1105", - "price_range": null, - "raw_prices": { - "precision": 6, - "price": "11050000", - "regular_price": "11050000", - "sale_price": "11050000" - } - }, - "totals": { - "currency_code": "GBP", - "currency_symbol": "£", - "currency_minor_unit": 2, - "currency_decimal_separator": ".", - "currency_thousand_separator": ",", - "currency_prefix": "£", - "currency_suffix": "", - "line_subtotal": "1105", - "line_subtotal_tax": "0", - "line_total": "884", - "line_total_tax": "0" - }, - "_links": { - "self": [ - { - "href": "https:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/items\/e369853df766fa44e1ed0ff613f563bd" - } - ], - "collection": [ - { - "href": "https:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/items" - } - ] - } - } + { + "key": "9bf31c7ff062936a96d3c8bd1f8f2ff3", + "id": 15, + "quantity": 1, + "quantity_limits": { + "minimum": 1, + "maximum": 99, + "multiple_of": 1, + "editable": true + }, + "name": "Beanie", + "summary": "

This is a simple product.

", + "short_description": "

This is a simple product.

", + "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", + "sku": "woo-beanie", + "low_stock_remaining": null, + "backorders_allowed": false, + "show_backorder_badge": false, + "sold_individually": false, + "permalink": "https://local.wordpress.test/product/beanie/", + "images": [ + { + "id": 44, + "src": "https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2.jpg", + "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-324x324.jpg", + "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2.jpg 801w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-768x768.jpg 768w", + "sizes": "(max-width: 801px) 100vw, 801px", + "name": "beanie-2.jpg", + "alt": "" + } + ], + "variation": [], + "item_data": [], + "prices": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "price": "1000", + "regular_price": "2000", + "sale_price": "1000", + "price_range": null, + "raw_prices": { + "precision": 6, + "price": "10000000", + "regular_price": "20000000", + "sale_price": "10000000" + } + }, + "totals": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "line_subtotal": "1000", + "line_subtotal_tax": "0", + "line_total": "800", + "line_total_tax": "0" + }, + "_links": { + "self": [ + { + "href": "https://local.wordpress.test/wp-json/wc/store/cart/items/9bf31c7ff062936a96d3c8bd1f8f2ff3" + } + ], + "collection": [ + { + "href": "https://local.wordpress.test/wp-json/wc/store/cart/items" + } + ] + } + }, + { + "key": "e369853df766fa44e1ed0ff613f563bd", + "id": 34, + "quantity": 1, + "quantity_limits": { + "minimum": 1, + "maximum": 99, + "multiple_of": 1, + "editable": true + }, + "name": "WordPress Pennant", + "summary": "

This is an external product.

", + "short_description": "

This is an external product.

", + "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", + "sku": "wp-pennant", + "low_stock_remaining": null, + "backorders_allowed": false, + "show_backorder_badge": false, + "sold_individually": false, + "permalink": "https://local.wordpress.test/product/wordpress-pennant/", + "images": [ + { + "id": 57, + "src": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg", + "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg", + "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-768x768.jpg 768w", + "sizes": "(max-width: 800px) 100vw, 800px", + "name": "pennant-1.jpg", + "alt": "" + } + ], + "variation": [], + "item_data": [], + "prices": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "price": "1105", + "regular_price": "1105", + "sale_price": "1105", + "price_range": null, + "raw_prices": { + "precision": 6, + "price": "11050000", + "regular_price": "11050000", + "sale_price": "11050000" + } + }, + "totals": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "line_subtotal": "1105", + "line_subtotal_tax": "0", + "line_total": "884", + "line_total_tax": "0" + }, + "_links": { + "self": [ + { + "href": "https://local.wordpress.test/wp-json/wc/store/cart/items/e369853df766fa44e1ed0ff613f563bd" + } + ], + "collection": [ + { + "href": "https://local.wordpress.test/wp-json/wc/store/cart/items" + } + ] + } + } ] ``` @@ -192,77 +202,82 @@ curl "https://example-store.com/wp-json/wc/store/cart/items/e369853df766fa44e1ed ```json { - "key": "e369853df766fa44e1ed0ff613f563bd", - "id": 34, - "quantity": 1, - "quantity_limit": 99, - "name": "WordPress Pennant", - "summary": "

This is an external product.<\/p>", - "short_description": "

This is an external product.<\/p>", - "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.<\/p>", - "sku": "wp-pennant", - "low_stock_remaining": null, - "backorders_allowed": false, - "show_backorder_badge": false, - "sold_individually": false, - "permalink": "https:\/\/local.wordpress.test\/product\/wordpress-pennant\/", - "images": [ - { - "id": 57, - "src": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1.jpg", - "thumbnail": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-324x324.jpg", - "srcset": "https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1.jpg 800w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-324x324.jpg 324w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-100x100.jpg 100w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-416x416.jpg 416w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-300x300.jpg 300w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-150x150.jpg 150w, https:\/\/local.wordpress.test\/wp-content\/uploads\/2020\/03\/pennant-1-768x768.jpg 768w", - "sizes": "(max-width: 800px) 100vw, 800px", - "name": "pennant-1.jpg", - "alt": "" - } - ], - "variation": [], - "item_data": [], - "prices": { - "currency_code": "GBP", - "currency_symbol": "£", - "currency_minor_unit": 2, - "currency_decimal_separator": ".", - "currency_thousand_separator": ",", - "currency_prefix": "£", - "currency_suffix": "", - "price": "1105", - "regular_price": "1105", - "sale_price": "1105", - "price_range": null, - "raw_prices": { - "precision": 6, - "price": "11050000", - "regular_price": "11050000", - "sale_price": "11050000" - } - }, - "totals": { - "currency_code": "GBP", - "currency_symbol": "£", - "currency_minor_unit": 2, - "currency_decimal_separator": ".", - "currency_thousand_separator": ",", - "currency_prefix": "£", - "currency_suffix": "", - "line_subtotal": "1105", - "line_subtotal_tax": "0", - "line_total": "884", - "line_total_tax": "0" - }, - "_links": { - "self": [ - { - "href": "https:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/items\/(?P[\\w-]{32})\/e369853df766fa44e1ed0ff613f563bd" - } - ], - "collection": [ - { - "href": "https:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/items\/(?P[\\w-]{32})" - } - ] - } + "key": "e369853df766fa44e1ed0ff613f563bd", + "id": 34, + "quantity": 1, + "quantity_limits": { + "minimum": 1, + "maximum": 99, + "multiple_of": 1, + "editable": true + }, + "name": "WordPress Pennant", + "summary": "

This is an external product.

", + "short_description": "

This is an external product.

", + "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", + "sku": "wp-pennant", + "low_stock_remaining": null, + "backorders_allowed": false, + "show_backorder_badge": false, + "sold_individually": false, + "permalink": "https://local.wordpress.test/product/wordpress-pennant/", + "images": [ + { + "id": 57, + "src": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg", + "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg", + "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-768x768.jpg 768w", + "sizes": "(max-width: 800px) 100vw, 800px", + "name": "pennant-1.jpg", + "alt": "" + } + ], + "variation": [], + "item_data": [], + "prices": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "price": "1105", + "regular_price": "1105", + "sale_price": "1105", + "price_range": null, + "raw_prices": { + "precision": 6, + "price": "11050000", + "regular_price": "11050000", + "sale_price": "11050000" + } + }, + "totals": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "line_subtotal": "1105", + "line_subtotal_tax": "0", + "line_total": "884", + "line_total_tax": "0" + }, + "_links": { + "self": [ + { + "href": "https://local.wordpress.test/wp-json/wc/store/cart/items/(?P[\\w-]{32})/e369853df766fa44e1ed0ff613f563bd" + } + ], + "collection": [ + { + "href": "https://local.wordpress.test/wp-json/wc/store/cart/items/(?P[\\w-]{32})" + } + ] + } } ``` diff --git a/src/StoreApi/docs/cart.md b/src/StoreApi/docs/cart.md index 4cd6c0885a9..a9828deda19 100644 --- a/src/StoreApi/docs/cart.md +++ b/src/StoreApi/docs/cart.md @@ -107,7 +107,12 @@ All endpoints under `/cart` (listed in this doc) return responses in the same fo "key": "9bf31c7ff062936a96d3c8bd1f8f2ff3", "id": 15, "quantity": 1, - "quantity_limit": 99, + "quantity_limits": { + "minimum": 1, + "maximum": 99, + "multiple_of": 1, + "editable": true + }, "name": "Beanie", "summary": "

This is a simple product.

", "short_description": "

This is a simple product.

", @@ -167,7 +172,12 @@ All endpoints under `/cart` (listed in this doc) return responses in the same fo "key": "e369853df766fa44e1ed0ff613f563bd", "id": 34, "quantity": 1, - "quantity_limit": 99, + "quantity_limits": { + "minimum": 1, + "maximum": 99, + "multiple_of": 1, + "editable": true + }, "name": "WordPress Pennant", "summary": "

This is an external product.

", "short_description": "

This is an external product.

", diff --git a/src/Utils/BlockTemplateUtils.php b/src/Utils/BlockTemplateUtils.php index fc50cd059b9..4e8414f489a 100644 --- a/src/Utils/BlockTemplateUtils.php +++ b/src/Utils/BlockTemplateUtils.php @@ -27,6 +27,15 @@ class BlockTemplateUtils { 'TEMPLATE_PARTS' => 'parts', ); + /** + * WooCommerce plugin slug + * + * This is used to save templates to the DB which are stored against this value in the wp_terms table. + * + * @var string + */ + const PLUGIN_SLUG = 'woocommerce/woocommerce'; + /** * Returns an array containing the references of * the passed blocks and their inner blocks. @@ -119,7 +128,7 @@ public static function gutenberg_build_template_result_from_post( $post ) { $template = new \WP_Block_Template(); $template->wp_id = $post->ID; $template->id = $theme . '//' . $post->post_name; - $template->theme = 'woocommerce' === $theme ? 'WooCommerce' : $theme; + $template->theme = $theme; $template->content = $post->post_content; $template->slug = $post->post_name; $template->source = 'custom'; @@ -138,7 +147,10 @@ public static function gutenberg_build_template_result_from_post( $post ) { } } - if ( 'woocommerce' === $theme ) { + // We are checking 'woocommerce' to maintain legacy templates which are saved to the DB, + // prior to updating to use the correct slug. + // More information found here: https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5423. + if ( self::PLUGIN_SLUG === $theme || 'woocommerce' === strtolower( $theme ) ) { $template->origin = 'plugin'; } @@ -164,8 +176,8 @@ public static function gutenberg_build_template_result_from_file( $template_file // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $template_content = file_get_contents( $template_file->path ); $template = new \WP_Block_Template(); - $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : 'woocommerce//' . $template_file->slug; - $template->theme = $template_is_from_theme ? $theme_name : 'WooCommerce'; + $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug; + $template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG; $template->content = self::gutenberg_inject_theme_attribute_in_content( $template_content ); // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909. $template->source = $template_file->source ? $template_file->source : 'plugin'; @@ -196,10 +208,10 @@ public static function create_new_block_template_object( $template_file, $templa $new_template_item = array( 'slug' => $template_slug, - 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : 'woocommerce//' . $template_slug, + 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug, 'path' => $template_file, 'type' => $template_type, - 'theme' => $template_is_from_theme ? $theme_name : 'woocommerce', + 'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG, // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909. 'source' => $template_is_from_theme ? 'theme' : 'plugin', 'title' => self::convert_slug_to_title( $template_slug ), diff --git a/src/Utils/StyleAttributesUtils.php b/src/Utils/StyleAttributesUtils.php index 2a43f55bcc3..b173c536717 100644 --- a/src/Utils/StyleAttributesUtils.php +++ b/src/Utils/StyleAttributesUtils.php @@ -206,13 +206,13 @@ public static function get_border_radius_class_and_style( $attributes ) { ); } - /** - * Get class and style for background-color from attributes. - * - * @param array $attributes Block attributes. - * - * @return (array | null) - */ + /** + * Get class and style for background-color from attributes. + * + * @param array $attributes Block attributes. + * + * @return (array | null) + */ public static function get_border_width_class_and_style( $attributes ) { $custom_border_width = $attributes['style']['border']['width'] ?? ''; @@ -227,6 +227,60 @@ public static function get_border_width_class_and_style( $attributes ) { ); } + /** + * Get class and style for border-color from attributes. + * Get class and style for align from attributes. + * + * @param array $attributes Block attributes. + * + * @return (array | null) + */ + public static function get_align_class_and_style( $attributes ) { + + $align_attribute = isset( $attributes['align'] ) ? $attributes['align'] : null; + + if ( ! $align_attribute ) { + return null; + }; + + if ( 'wide' === $align_attribute ) { + return array( + 'class' => 'alignwide', + 'style' => null, + ); + } + + if ( 'full' === $align_attribute ) { + return array( + 'class' => 'alignfull', + 'style' => null, + ); + } + + if ( 'left' === $align_attribute ) { + return array( + 'class' => 'alignleft', + 'style' => null, + ); + } + + if ( 'right' === $align_attribute ) { + return array( + 'class' => 'alignright', + 'style' => null, + ); + } + + if ( 'center' === $align_attribute ) { + return array( + 'class' => 'aligncenter', + 'style' => null, + ); + } + + return null; + } + /** * Get classes and styles from attributes. * diff --git a/templates/parts/mini-cart.html b/templates/parts/mini-cart.html index c2400ba76ed..bd2838e4459 100644 --- a/templates/parts/mini-cart.html +++ b/templates/parts/mini-cart.html @@ -2,9 +2,22 @@
- - - + +
+
+ + +
+ +
+
+ +
+ + + +