From f20113fa01c9a3817c67ca572ac036b5c9b45956 Mon Sep 17 00:00:00 2001 From: Fernando Date: Wed, 13 Oct 2021 13:15:47 -0300 Subject: [PATCH] Add country validation to subscription inclusion (https://github.com/woocommerce/woocommerce-admin/pull/7777) * Add country validation * Add OnboardingProductTypes * Add OnboardingProductTypes * Add country validation to product task * Add `productTypes` data handling * Add country validation and new productTypes handling * Fix to get `productTypes` from a SSOT * Add `invalidateResolution` for `getTaskLists` * Fixed testing instructions * Fix `isTaskListHidden` issue * Fixed product type * Added constant `EMPTY_ARRAY` to `selectors.ts` * Fixed constant `EMPTY_ARRAY` * Moved `invalidateResolutionForStoreSelector` into OBW * Updated testing instructions * Updated testing instructions * Fixed testing instructions Co-authored-by: Fernando Marichal --- .../woocommerce-admin/TESTING-INSTRUCTIONS.md | 40 +++++---- .../client/dashboard/components/cart-modal.js | 15 ++-- .../client/dashboard/utils.js | 35 +++++--- .../selective-extensions-bundle/index.js | 44 ++++++---- .../steps/product-types/index.js | 69 ++++++++++++--- .../steps/product-types/style.scss | 7 +- .../steps/product-types/test/index.js | 87 ++++++++++++------- .../steps/store-details/index.js | 17 +++- .../fills/products/product-template-modal.js | 35 ++++---- .../client/tasks/fills/products/products.js | 17 +++- .../client/tasks/fills/purchase.js | 22 +++-- .../data/src/onboarding/action-types.js | 2 + .../packages/data/src/onboarding/actions.js | 14 +++ .../packages/data/src/onboarding/reducer.js | 15 ++++ .../packages/data/src/onboarding/resolvers.js | 15 ++++ .../packages/data/src/onboarding/selectors.ts | 12 +++ plugins/woocommerce-admin/src/API/Init.php | 1 + .../src/API/OnboardingProductTypes.php | 78 +++++++++++++++++ .../src/Features/Onboarding.php | 6 +- 19 files changed, 396 insertions(+), 135 deletions(-) create mode 100644 plugins/woocommerce-admin/src/API/OnboardingProductTypes.php diff --git a/plugins/woocommerce-admin/TESTING-INSTRUCTIONS.md b/plugins/woocommerce-admin/TESTING-INSTRUCTIONS.md index 6efd687a7a8..54f8ce7afa8 100644 --- a/plugins/woocommerce-admin/TESTING-INSTRUCTIONS.md +++ b/plugins/woocommerce-admin/TESTING-INSTRUCTIONS.md @@ -1,27 +1,26 @@ # Testing instructions -## Unreleased - ## 2.8.0 ### Store Profiler and Product task - include Subscriptions #7734 -##### Feature: turned off +##### Non US stores 1. Deactivate and delete `WooCommerce Payments` if you have it installed. -2. Go to the 3rd step of the store profiler (`Product Types`). -3. Verify `Subscriptions` is shown as a paid extension (with a price chip). -4. Check `Subscriptions` and continue with the OBW. -5. Go back to the `Home` screen. Check that the task item `Add Subscriptions to my store` is visible in the setup task list. -6. Press `Add my products` in the setup task list. -7. Select `Start with a template`. Verify that the option `Subscription product` is not visible in the popup. +2. Go to step one of the store profiler and select `France` (or any country other than the US) as the store `Country / Region`. +3. Go to step three of the store profiler (`Product Types`). +4. Verify `Subscriptions` is shown as a paid extension (with a price chip). +5. Check `Subscriptions` and continue with the OBW. +6. Go back to the `Home` screen by pressing `Skip setup store details` in step one of the store profiler. Check that the task item `Add Subscriptions to my store` is visible in the setup task list. +7. Press `Add my products` in the setup task list. +8. Select `Start with a template`. Verify that the option `Subscription product` is not visible in the popup. -##### Feature: turned on +##### US stores -1. Install and activate this plugin to turn on the subscriptions inclusion -> https://gist.github.com/octaedro/455eb4c85887608c253249bad533ccb3 -2. Deactivate and delete `WooCommerce Payments` if you have it installed. -3. Go to the 3rd step of the store profiler (`Product Types`). -4. Verify `Subscriptions` is shown as free (without a price chip). Also, verify that the text +9. Deactivate and delete `WooCommerce Payments`. +10. Go to step one of the store profiler and select `US` as the store `Country / Region`. +11. Go to step three of the store profiler (`Product Types`). +12. Verify `Subscriptions` is shown as free (without a price chip). Also, verify that the text ``` The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature @@ -31,20 +30,23 @@ is visible at the bottom when `WooCommerce Payments` is not installed. ![screenshot-one wordpress test-2021 09 30-14_12_58](https://user-images.githubusercontent.com/1314156/135506696-b7812f7e-437f-4d89-956a-b73248f70f6b.png) -5. Press `Continue` and verify that the `WooCommerce Payments` plugin is installed and activated and it's not shown in the `Free features` list + +13. Check `Subscriptions` and press `Continue` and verify that the `WooCommerce Payments` plugin is installed and activated and it's not shown in the `Free features` list ![screenshot-one wordpress test-2021 09 30-14_32_20](https://user-images.githubusercontent.com/1314156/135506727-d8888f2b-3424-4cf5-a4bf-b67a14a198b6.png) -6. Go back to the `Home` screen. Check that the task item `Add Subscriptions to my store` is not visible in the setup task list. +14. Verify that the `WooCommerce Payments` plugin is being shown in the `Free features` list when the store country is other than the `US`. + +15. Go back to the `Home` screen by pressing `Skip setup store details` in step one of the store profiler. Check that the task item `Add Subscriptions to my store` is not visible in the setup task list. It should be visible if the store is from any country other than the `US`. ![screenshot-one wordpress test-2021 09 30-14_39_28](https://user-images.githubusercontent.com/1314156/135506770-91571f8f-2e2e-43a7-b092-b9e5fdf56df8.png) -7. Press `Add my products` in the setup task list. -8. Select `Start with a template`. Verify that the option `Subscription product` is visible in the popup +16. Press `Add my products` in the setup task list. +17. Select `Start with a template`. Verify that the option `Subscription product` is visible in the popup ![screenshot-one wordpress test-2021 09 30-14_35_22](https://user-images.githubusercontent.com/1314156/135506748-0b7bdce5-b006-47f9-9289-03ed26e4950c.png) -9. Select `Subscription product` and press `Ok`. You should have been redirected to `post-new.php?post_type=product&subscription_pointers=true` +18. Select `Subscription product` and press `Ok`. You should have been redirected to `post-new.php?post_type=product&subscription_pointers=true`. ## 2.7.1 diff --git a/plugins/woocommerce-admin/client/dashboard/components/cart-modal.js b/plugins/woocommerce-admin/client/dashboard/components/cart-modal.js index 847b8fe6a65..d0c52f0bf26 100644 --- a/plugins/woocommerce-admin/client/dashboard/components/cart-modal.js +++ b/plugins/woocommerce-admin/client/dashboard/components/cart-modal.js @@ -80,11 +80,8 @@ class CartModal extends Component { } renderProducts() { - const { productIds } = this.props; - const { productTypes = {}, themes = [] } = getSetting( - 'onboarding', - {} - ); + const { productIds, productTypes } = this.props; + const { themes = [] } = getSetting( 'onboarding', {} ); const listItems = []; productIds.forEach( ( productId ) => { @@ -169,15 +166,19 @@ class CartModal extends Component { export default compose( withSelect( ( select ) => { const { getInstalledPlugins } = select( PLUGINS_STORE_NAME ); - const { getProfileItems } = select( ONBOARDING_STORE_NAME ); + const { getProductTypes, getProfileItems } = select( + ONBOARDING_STORE_NAME + ); const profileItems = getProfileItems(); const installedPlugins = getInstalledPlugins(); + const productTypes = getProductTypes(); const productIds = getProductIdsForCart( + productTypes, profileItems, false, installedPlugins ); - return { profileItems, productIds }; + return { profileItems, productIds, productTypes }; } ) )( CartModal ); diff --git a/plugins/woocommerce-admin/client/dashboard/utils.js b/plugins/woocommerce-admin/client/dashboard/utils.js index 784696f0560..b4c5da56901 100644 --- a/plugins/woocommerce-admin/client/dashboard/utils.js +++ b/plugins/woocommerce-admin/client/dashboard/utils.js @@ -36,12 +36,14 @@ export function getCurrencyRegion( countryState ) { /** * Gets the product IDs for items based on the product types and theme selected in the onboarding profiler. * + * @param {Object} productTypes Product Types. * @param {Object} profileItems Onboarding profile. * @param {boolean} includeInstalledItems Include installed items in returned product IDs. * @param {Array} installedPlugins Installed plugins. * @return {Array} Product Ids. */ export function getProductIdsForCart( + productTypes, profileItems, includeInstalledItems = false, installedPlugins @@ -49,7 +51,8 @@ export function getProductIdsForCart( const productList = getProductList( profileItems, includeInstalledItems, - installedPlugins + installedPlugins, + productTypes ); const productIds = productList.map( ( product ) => product.id || product.product @@ -60,11 +63,13 @@ export function getProductIdsForCart( /** * Gets the labeled/categorized product names and types for items based on the product types and theme selected in the onboarding profiler. * + * @param {Object} productTypes Product Types. * @param {Object} profileItems Onboarding profile. * @param {Array} installedPlugins Installed plugins. * @return {Array} Objects with labeled/categorized product names and types. */ export function getCategorizedOnboardingProducts( + productTypes, profileItems, installedPlugins ) { @@ -72,12 +77,14 @@ export function getCategorizedOnboardingProducts( productList.products = getProductList( profileItems, true, - installedPlugins + installedPlugins, + productTypes ); productList.remainingProducts = getProductList( profileItems, false, - installedPlugins + installedPlugins, + productTypes ); const uniqueItemsList = [ @@ -106,37 +113,37 @@ export function getCategorizedOnboardingProducts( * @param {Object} profileItems Onboarding profile. * @param {boolean} includeInstalledItems Include installed items in returned product list. * @param {Array} installedPlugins Installed plugins. + * @param {Object} productTypes Product Types. * @return {Array} Products. */ export function getProductList( profileItems, includeInstalledItems = false, - installedPlugins + installedPlugins, + productTypes ) { - const onboarding = getSetting( 'onboarding', {} ); const productList = []; - // The population of onboarding.productTypes only happens if the task list should be shown - // so bail early if it isn't present. - if ( ! onboarding.productTypes ) { + if ( ! productTypes ) { return productList; } - const productTypes = profileItems.product_types || []; + const profileItemsProductTypes = profileItems.product_types || []; - productTypes.forEach( ( productType ) => { + profileItemsProductTypes.forEach( ( productType ) => { if ( - onboarding.productTypes[ productType ] && - onboarding.productTypes[ productType ].product && + productTypes[ productType ] && + productTypes[ productType ].product && ( includeInstalledItems || ! installedPlugins.includes( - onboarding.productTypes[ productType ].slug + productTypes[ productType ].slug ) ) ) { - productList.push( onboarding.productTypes[ productType ] ); + productList.push( productTypes[ productType ] ); } } ); + const onboarding = getSetting( 'onboarding', {} ); const theme = onboarding.themes.find( ( themeData ) => themeData.slug === profileItems.theme ); diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js index dd4060b09dd..8c9cbf2940d 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js @@ -8,7 +8,11 @@ import { Link } from '@woocommerce/components'; import { __, _n, sprintf } from '@wordpress/i18n'; import { Icon, chevronDown, chevronUp } from '@wordpress/icons'; import interpolateComponents from 'interpolate-components'; -import { pluginNames, ONBOARDING_STORE_NAME } from '@woocommerce/data'; +import { + pluginNames, + ONBOARDING_STORE_NAME, + SETTINGS_STORE_NAME, +} from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import { useSelect } from '@wordpress/data'; @@ -19,6 +23,7 @@ import { AppIllustration } from '../app-illustration'; import './style.scss'; import sanitizeHTML from '~/lib/sanitize-html'; import { setAllPropsToValue } from '~/lib/collections'; +import { getCountryCode } from '../../../../../../dashboard/utils'; const ALLOWED_PLUGIN_LISTS = [ 'basics' ]; @@ -178,28 +183,35 @@ export const SelectiveExtensionsBundle = ( { const [ showExtensions, setShowExtensions ] = useState( false ); const [ values, setValues ] = useState( baseValues ); - const { freeExtensions, isResolving, profileItems } = useSelect( - ( select ) => { - const { - getFreeExtensions, - getProfileItems, - hasFinishedResolution, - } = select( ONBOARDING_STORE_NAME ); + const { + countryCode, + freeExtensions, + isResolving, + profileItems, + } = useSelect( ( select ) => { + const { + getFreeExtensions, + getProfileItems, + hasFinishedResolution, + } = select( ONBOARDING_STORE_NAME ); + const { getSettings } = select( SETTINGS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); - return { - freeExtensions: getFreeExtensions(), - isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), - profileItems: getProfileItems(), - }; - } - ); + return { + countryCode: getCountryCode( settings.woocommerce_default_country ), + freeExtensions: getFreeExtensions(), + isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), + profileItems: getProfileItems(), + }; + } ); const installableExtensions = useMemo( () => { const { product_types: productTypes } = profileItems; return freeExtensions.filter( ( list ) => { if ( window.wcAdminFeatures && - window.wcAdminFeatures.subscriptions + window.wcAdminFeatures.subscriptions && + countryCode === 'US' ) { if ( productTypes.includes( 'subscriptions' ) ) { list.plugins = list.plugins.filter( diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js index ff2fb5a0c49..bf49ec58fdd 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js @@ -11,35 +11,34 @@ import { CardFooter, CheckboxControl, FormToggle, + Spinner, } from '@wordpress/components'; -import { includes, filter, get } from 'lodash'; -import { getSetting } from '@woocommerce/wc-admin-settings'; +import { includes, filter } from 'lodash'; import { recordEvent } from '@woocommerce/tracks'; import { withDispatch, withSelect } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data'; +import { + ONBOARDING_STORE_NAME, + PLUGINS_STORE_NAME, + SETTINGS_STORE_NAME, +} from '@woocommerce/data'; import { Text } from '@woocommerce/experimental'; /** * Internal dependencies */ import { createNoticesFromResponse } from '~/lib/notices'; +import { getCountryCode } from '../../../dashboard/utils'; import ProductTypeLabel from './label'; import './style.scss'; export class ProductTypes extends Component { - constructor( props ) { + constructor() { super(); - const profileItems = get( props, 'profileItems', {} ); - - const { productTypes = {} } = getSetting( 'onboarding', {} ); - const defaultProductTypes = Object.keys( productTypes ).filter( - ( key ) => !! productTypes[ key ].default - ); this.state = { error: null, isMonthlyPricing: true, - selected: profileItems.product_types || defaultProductTypes, + selected: [], isWCPayInstalled: null, }; @@ -48,9 +47,11 @@ export class ProductTypes extends Component { } componentDidMount() { - const { installedPlugins } = this.props; + const { installedPlugins, invalidateResolution } = this.props; const { isWCPayInstalled } = this.state; + invalidateResolution( 'getProductTypes', [] ); + if ( isWCPayInstalled === null && installedPlugins ) { this.setState( { isWCPayInstalled: installedPlugins.includes( @@ -60,6 +61,19 @@ export class ProductTypes extends Component { } } + componentDidUpdate( prevProps ) { + const { profileItems, productTypes } = this.props; + + if ( prevProps.productTypes !== productTypes ) { + const defaultProductTypes = Object.keys( productTypes ).filter( + ( key ) => !! productTypes[ key ].default + ); + this.setState( { + selected: profileItems.product_types || defaultProductTypes, + } ); + } + } + validateField() { const error = this.state.selected.length ? null @@ -80,6 +94,7 @@ export class ProductTypes extends Component { } const { + countryCode, createNotice, goToNextStep, installAndActivatePlugins, @@ -95,6 +110,7 @@ export class ProductTypes extends Component { if ( window.wcAdminFeatures && window.wcAdminFeatures.subscriptions && + countryCode === 'US' && ! installedPlugins.includes( 'woocommerce-payments' ) && selected.includes( 'subscriptions' ) ) { @@ -145,14 +161,27 @@ export class ProductTypes extends Component { } render() { - const { productTypes = {} } = getSetting( 'onboarding', {} ); + const { productTypes = [] } = this.props; const { error, isMonthlyPricing, isWCPayInstalled, selected, } = this.state; - const { isInstallingActivating, isProfileItemsRequesting } = this.props; + const { + countryCode, + isInstallingActivating, + isProductTypesRequesting, + isProfileItemsRequesting, + } = this.props; + + if ( isProductTypesRequesting ) { + return ( +
+ +
+ ); + } return (
@@ -256,6 +285,7 @@ export class ProductTypes extends Component { { window.wcAdminFeatures && window.wcAdminFeatures.subscriptions && + countryCode === 'US' && ! isWCPayInstalled && selected.includes( 'subscriptions' ) && ( { const { getProfileItems, + getProductTypes, getOnboardingError, + hasFinishedResolution, isOnboardingRequesting, } = select( ONBOARDING_STORE_NAME ); + const { getSettings } = select( SETTINGS_STORE_NAME ); const { getInstalledPlugins, isPluginsRequesting } = select( PLUGINS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); return { isError: Boolean( getOnboardingError( 'updateProfileItems' ) ), @@ -297,16 +331,23 @@ export default compose( isInstallingActivating: isPluginsRequesting( 'installPlugins' ) || isPluginsRequesting( 'activatePlugins' ), + countryCode: getCountryCode( settings.woocommerce_default_country ), + productTypes: getProductTypes(), + isProductTypesRequesting: ! hasFinishedResolution( + 'getProductTypes' + ), }; } ), withDispatch( ( dispatch ) => { const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME ); const { createNotice } = dispatch( 'core/notices' ); const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME ); + const { invalidateResolution } = dispatch( ONBOARDING_STORE_NAME ); return { createNotice, installAndActivatePlugins, + invalidateResolution, updateProfileItems, }; } ) diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/style.scss b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/style.scss index 0ed73b729ab..0a482909b3e 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/style.scss +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/style.scss @@ -60,10 +60,13 @@ margin-left: $gap; } } + &__spinner { + text-align: center; + } } } -@media screen and (max-width: 438px) { +@media screen and ( max-width: 438px ) { .woocommerce-profile-wizard__body { .woocommerce-profile-wizard__product-types { .woocommerce-product-wizard__product-types-label { @@ -73,7 +76,7 @@ } } -@media screen and (max-width: 375px) { +@media screen and ( max-width: 375px ) { .woocommerce-profile-wizard__body { .woocommerce-profile-wizard__product-types { .woocommerce-pill { diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/test/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/test/index.js index 7bee3a2e6c0..628fd24051b 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/test/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/test/index.js @@ -2,46 +2,44 @@ * External dependencies */ import { render, screen, waitFor } from '@testing-library/react'; -import { setSetting } from '@woocommerce/wc-admin-settings'; import userEvent from '@testing-library/user-event'; -import { createElement } from '@wordpress/element'; /** * Internal dependencies */ import { ProductTypes } from '../'; -describe( 'ProductTypes', () => { - beforeEach( () => { - setSetting( 'onboarding', { - productTypes: { - paidProduct: { - description: 'Paid product type', - label: 'Paid product', - more_url: 'https://woocommerce.com/paid-product', - product: 100, - slug: 'paid-product', - yearly_price: 120, - }, - freeProduct: { - label: 'Free product', - }, - }, - } ); - } ); +const testProps = { + countryCode: 'US', + invalidateResolution: jest.fn(), + isProductTypesRequesting: false, + productTypes: { + paidProduct: { + description: 'Paid product type', + label: 'Paid product', + more_url: 'https://woocommerce.com/paid-product', + product: 100, + slug: 'paid-product', + yearly_price: 120, + }, + freeProduct: { + label: 'Free product', + }, + }, +}; +describe( 'ProductTypes', () => { afterEach( () => { - setSetting( 'onboarding', {} ); window.wcAdminFeatures.subscriptions = false; } ); test( 'should render product types', () => { - const { container } = render( ); + const { container } = render( ); expect( container ).toMatchSnapshot(); } ); test( 'should show annual prices on toggle', () => { - const { container } = render( ); + const { container } = render( ); const toggle = screen.getByLabelText( 'Display monthly prices', { selector: 'input', @@ -62,6 +60,7 @@ describe( 'ProductTypes', () => { createNotice={ mockCreateNotice } goToNextStep={ mockGoToNextStep } updateProfileItems={ mockUpdateProfileItems } + { ...testProps } /> ); @@ -88,16 +87,16 @@ describe( 'ProductTypes', () => { } ); } ); test( 'should show a warning message at the bottom of the step', () => { - setSetting( 'onboarding', { - productTypes: { - subscriptions: { - label: 'Subscriptions', - }, + const productTypes = { + subscriptions: { + label: 'Subscriptions', }, - } ); + }; window.wcAdminFeatures.subscriptions = true; - render( ); + render( + + ); const subscription = screen.getByText( 'Subscriptions', { selector: 'label', @@ -110,4 +109,32 @@ describe( 'ProductTypes', () => { ) ).toBeInTheDocument(); } ); + test( 'should show the warning message only for US stores', () => { + const productTypes = { + subscriptions: { + label: 'Subscriptions', + }, + }; + const countryCode = 'FR'; + window.wcAdminFeatures.subscriptions = true; + + render( + + ); + + const subscription = screen.getByText( 'Subscriptions', { + selector: 'label', + } ); + userEvent.click( subscription ); + + expect( + screen.queryByText( + 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.' + ) + ).not.toBeInTheDocument(); + } ); } ); diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js index 224bae6d058..f259ef2939f 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js @@ -230,7 +230,13 @@ class StoreDetails extends Component { isStoreDetailsPopoverVisible, isSkipSetupPopoverVisible, } = this.state; - const { skipProfiler, isLoading, isBusy, initialValues } = this.props; + const { + skipProfiler, + isLoading, + isBusy, + initialValues, + invalidateResolutionForStoreSelector, + } = this.props; /* eslint-disable @wordpress/i18n-no-collapsible-whitespace */ const skipSetupText = __( @@ -390,6 +396,9 @@ class StoreDetails extends Component { isLink className="woocommerce-profile-wizard__footer-link" onClick={ () => { + invalidateResolutionForStoreSelector( + 'getTaskLists' + ); this.setState( { showUsageModal: true, skipping: true, @@ -498,13 +507,17 @@ export default compose( } ), withDispatch( ( dispatch ) => { const { createNotice } = dispatch( 'core/notices' ); - const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME ); + const { + invalidateResolutionForStoreSelector, + updateProfileItems, + } = dispatch( ONBOARDING_STORE_NAME ); const { updateAndPersistSettingsForGroup } = dispatch( SETTINGS_STORE_NAME ); return { createNotice, + invalidateResolutionForStoreSelector, updateProfileItems, updateAndPersistSettingsForGroup, }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js b/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js index cd47ae9dc29..29b08e08ef4 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js +++ b/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js @@ -5,11 +5,12 @@ import { __ } from '@wordpress/i18n'; import { Button, Modal, RadioControl } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { addFilter, applyFilters } from '@wordpress/hooks'; +import { applyFilters } from '@wordpress/hooks'; import { ITEMS_STORE_NAME, ONBOARDING_STORE_NAME, PLUGINS_STORE_NAME, + SETTINGS_STORE_NAME, } from '@woocommerce/data'; import { getAdminLink } from '@woocommerce/wc-admin-settings'; import { recordEvent } from '@woocommerce/tracks'; @@ -19,11 +20,12 @@ import { recordEvent } from '@woocommerce/tracks'; */ import './product-template-modal.scss'; import { createNoticesFromResponse } from '../../../lib/notices'; +import { getCountryCode } from '../../../dashboard/utils'; export const ONBOARDING_PRODUCT_TEMPLATES_FILTER = 'woocommerce_admin_onboarding_product_templates'; -const PRODUCT_TEMPLATES = [ +const getProductTemplates = () => [ { key: 'physical', title: __( 'Physical product', 'woocommerce-admin' ), @@ -62,10 +64,13 @@ export default function ProductTemplateModal( { onClose } ) { const [ selectedTemplate, setSelectedTemplate ] = useState( null ); const [ isRedirecting, setIsRedirecting ] = useState( false ); const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME ); - const { profileItems } = useSelect( ( select ) => { + const { countryCode, profileItems } = useSelect( ( select ) => { const { getProfileItems } = select( ONBOARDING_STORE_NAME ); + const { getSettings } = select( SETTINGS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); return { + countryCode: getCountryCode( settings.woocommerce_default_country ), profileItems: getProfileItems(), }; } ); @@ -116,25 +121,21 @@ export default function ProductTemplateModal( { onClose } ) { } }; - if ( + const removeSubscriptions = ( window.wcAdminFeatures && ! window.wcAdminFeatures.subscriptions ) || + countryCode !== 'US' || ! profileItems.product_types.includes( 'subscriptions' ) || - ! installedPlugins.includes( 'woocommerce-payments' ) - ) { - addFilter( - ONBOARDING_PRODUCT_TEMPLATES_FILTER, - 'woocommerce-admin', - ( productTemplates ) => { - return productTemplates.filter( - ( template ) => template.key !== 'subscription' - ); - } - ); - } + ! installedPlugins.includes( 'woocommerce-payments' ); + + const productTemplates = removeSubscriptions + ? getProductTemplates().filter( + ( template ) => template.key !== 'subscription' + ) + : getProductTemplates(); const templates = applyFilters( ONBOARDING_PRODUCT_TEMPLATES_FILTER, - PRODUCT_TEMPLATES + productTemplates ); return ( diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/products.js b/plugins/woocommerce-admin/client/tasks/fills/products/products.js index 926bed475bd..157471ada19 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/products/products.js +++ b/plugins/woocommerce-admin/client/tasks/fills/products/products.js @@ -12,7 +12,11 @@ import { archive, download, } from '@wordpress/icons'; -import { ONBOARDING_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data'; +import { + ONBOARDING_STORE_NAME, + PLUGINS_STORE_NAME, + SETTINGS_STORE_NAME, +} from '@woocommerce/data'; import { useSelect } from '@wordpress/data'; import { List, Pill } from '@woocommerce/components'; import { getAdminLink } from '@woocommerce/wc-admin-settings'; @@ -24,8 +28,9 @@ import { WooOnboardingTask } from '@woocommerce/onboarding'; * Internal dependencies */ import ProductTemplateModal from './product-template-modal'; +import { getCountryCode } from '../../../dashboard/utils'; -const subTasks = [ +const getSubTasks = () => [ { key: 'addProductTemplate', title: ( @@ -94,10 +99,13 @@ const subTasks = [ const Products = () => { const [ selectTemplate, setSelectTemplate ] = useState( null ); - const { profileItems } = useSelect( ( select ) => { + const { countryCode, profileItems } = useSelect( ( select ) => { const { getProfileItems } = select( ONBOARDING_STORE_NAME ); + const { getSettings } = select( SETTINGS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); return { + countryCode: getCountryCode( settings.woocommerce_default_country ), profileItems: getProfileItems(), }; } ); @@ -109,9 +117,12 @@ const Products = () => { }; } ); + const subTasks = getSubTasks(); + if ( window.wcAdminFeatures && window.wcAdminFeatures.subscriptions && + countryCode === 'US' && profileItems.product_types.includes( 'subscriptions' ) && installedPlugins.includes( 'woocommerce-payments' ) ) { diff --git a/plugins/woocommerce-admin/client/tasks/fills/purchase.js b/plugins/woocommerce-admin/client/tasks/fills/purchase.js index 5f1e436273f..31a894c9158 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/purchase.js +++ b/plugins/woocommerce-admin/client/tasks/fills/purchase.js @@ -18,15 +18,20 @@ import { getCategorizedOnboardingProducts } from '../../dashboard/utils'; const PurchaseTaskItem = () => { const [ cartModalOpen, setCartModalOpen ] = useState( false ); - const { installedPlugins, profileItems } = useSelect( ( select ) => { - const { getProfileItems } = select( ONBOARDING_STORE_NAME ); - const { getInstalledPlugins } = select( PLUGINS_STORE_NAME ); + const { installedPlugins, productTypes, profileItems } = useSelect( + ( select ) => { + const { getProductTypes, getProfileItems } = select( + ONBOARDING_STORE_NAME + ); + const { getInstalledPlugins } = select( PLUGINS_STORE_NAME ); - return { - installedPlugins: getInstalledPlugins(), - profileItems: getProfileItems(), - }; - } ); + return { + installedPlugins: getInstalledPlugins(), + productTypes: getProductTypes(), + profileItems: getProfileItems(), + }; + } + ); const toggleCartModal = useCallback( () => { if ( ! cartModalOpen ) { @@ -37,6 +42,7 @@ const PurchaseTaskItem = () => { }, [ cartModalOpen ] ); const groupedProducts = getCategorizedOnboardingProducts( + productTypes, profileItems, installedPlugins ); diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js b/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js index 36f621ab65d..7eae1465621 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js @@ -5,6 +5,8 @@ const TYPES = { SET_EMAIL_PREFILL: 'SET_EMAIL_PREFILL', SET_TASKS_STATUS: 'SET_TASKS_STATUS', GET_PAYMENT_METHODS_SUCCESS: 'GET_PAYMENT_METHODS_SUCCESS', + GET_PRODUCT_TYPES_SUCCESS: 'GET_PRODUCT_TYPES_SUCCESS', + GET_PRODUCT_TYPES_ERROR: 'GET_PRODUCT_TYPES_ERROR', GET_FREE_EXTENSIONS_ERROR: 'GET_FREE_EXTENSIONS_ERROR', GET_FREE_EXTENSIONS_SUCCESS: 'GET_FREE_EXTENSIONS_SUCCESS', GET_TASK_LISTS_ERROR: 'GET_TASK_LISTS_ERROR', diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js b/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js index 2efabe3ce45..e8b87d211aa 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js @@ -222,6 +222,20 @@ export function actionTaskSuccess( task ) { }; } +export function getProductTypesSuccess( productTypes ) { + return { + type: TYPES.GET_PRODUCT_TYPES_SUCCESS, + productTypes, + }; +} + +export function getProductTypesError( error ) { + return { + type: TYPES.GET_PRODUCT_TYPES_ERROR, + error, + }; +} + export function* updateProfileItems( items ) { yield setIsRequesting( 'updateProfileItems', true ); yield setError( 'updateProfileItems', null ); diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js b/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js index bfc07017651..4c2c28063ad 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js @@ -25,6 +25,7 @@ export const defaultState = { }, emailPrefill: '', paymentMethods: [], + productTypes: [], requesting: {}, taskLists: [], tasksStatus: {}, @@ -55,6 +56,7 @@ const onboarding = ( profileItems, emailPrefill, paymentMethods, + productTypes, replace, error, isRequesting, @@ -106,6 +108,19 @@ const onboarding = ( ...state, paymentMethods, }; + case TYPES.GET_PRODUCT_TYPES_SUCCESS: + return { + ...state, + productTypes, + }; + case TYPES.GET_PRODUCT_TYPES_ERROR: + return { + ...state, + errors: { + ...state.errors, + productTypes: error, + }, + }; case TYPES.GET_FREE_EXTENSIONS_ERROR: return { ...state, diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/resolvers.js b/plugins/woocommerce-admin/packages/data/src/onboarding/resolvers.js index 2f5b806827f..71ac758ad88 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/resolvers.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/resolvers.js @@ -17,6 +17,8 @@ import { setTasksStatus, setPaymentMethods, setEmailPrefill, + getProductTypesSuccess, + getProductTypesError, } from './actions'; import { DeprecatedTasks } from './deprecated-tasks'; @@ -103,3 +105,16 @@ export function* getFreeExtensions() { yield getFreeExtensionsError( error ); } } + +export function* getProductTypes() { + try { + const results = yield apiFetch( { + path: WC_ADMIN_NAMESPACE + '/onboarding/product-types', + method: 'GET', + } ); + + yield getProductTypesSuccess( results ); + } catch ( error ) { + yield getProductTypesError( error ); + } +} diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/selectors.ts b/plugins/woocommerce-admin/packages/data/src/onboarding/selectors.ts index a1497442cae..eb384d9b87c 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/selectors.ts +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/selectors.ts @@ -24,6 +24,7 @@ export const getTasksStatus = ( const initialTaskLists: TaskListType[] = []; +const EMPTY_ARRAY: Product[] = []; export const getTaskLists = ( state: OnboardingState ): TaskListType[] => { return state.taskLists || initialTaskLists; }; @@ -52,6 +53,10 @@ export const getEmailPrefill = ( state: OnboardingState ): string => { return state.emailPrefill || ''; }; +export const getProductTypes = ( state: OnboardingState ): Product[] => { + return state.productTypes || EMPTY_ARRAY; +}; + // Types export type OnboardingSelectors = { getProfileItems: () => ReturnType< typeof getProfileItems >; @@ -69,6 +74,7 @@ export type OnboardingState = { taskLists: TaskListType[]; tasksStatus: TasksStatusState; paymentMethods: PaymentMethodsState[]; + productTypes: Product[]; emailPrefill: string; // TODO clarify what the error record's type is errors: Record< string, unknown >; @@ -151,6 +157,12 @@ export type MethodFields = { value?: string; }; +export type Product = { + default?: boolean; + label: string; + product?: number; +}; + export type PaymentMethodsState = { locale: string; title: string; diff --git a/plugins/woocommerce-admin/src/API/Init.php b/plugins/woocommerce-admin/src/API/Init.php index 15111257df0..8028dec0ea3 100644 --- a/plugins/woocommerce-admin/src/API/Init.php +++ b/plugins/woocommerce-admin/src/API/Init.php @@ -76,6 +76,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\Plugins', 'Automattic\WooCommerce\Admin\API\OnboardingFreeExtensions', 'Automattic\WooCommerce\Admin\API\OnboardingPayments', + 'Automattic\WooCommerce\Admin\API\OnboardingProductTypes', 'Automattic\WooCommerce\Admin\API\OnboardingProfile', 'Automattic\WooCommerce\Admin\API\OnboardingTasks', 'Automattic\WooCommerce\Admin\API\OnboardingThemes', diff --git a/plugins/woocommerce-admin/src/API/OnboardingProductTypes.php b/plugins/woocommerce-admin/src/API/OnboardingProductTypes.php new file mode 100644 index 00000000000..46d128546a5 --- /dev/null +++ b/plugins/woocommerce-admin/src/API/OnboardingProductTypes.php @@ -0,0 +1,78 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_product_types' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read onboarding profile data. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Return available product types. + * + * @param \WP_REST_Request $request Request data. + * + * @return \WP_Error|\WP_REST_Response + */ + public function get_product_types( $request ) { + return Onboarding::get_allowed_product_types(); + } + +} diff --git a/plugins/woocommerce-admin/src/Features/Onboarding.php b/plugins/woocommerce-admin/src/Features/Onboarding.php index 176f617b9f3..dc6c4c9ea98 100644 --- a/plugins/woocommerce-admin/src/Features/Onboarding.php +++ b/plugins/woocommerce-admin/src/Features/Onboarding.php @@ -433,7 +433,7 @@ class Onboarding { * @return array */ public static function get_allowed_product_types() { - $products = array( + $products = array( 'physical' => array( 'label' => __( 'Physical products', 'woocommerce-admin' ), 'default' => true, @@ -461,7 +461,8 @@ class Onboarding { 'product' => 18618, ), ); - if ( ! Features::is_enabled( 'subscriptions' ) ) { + $base_location = wc_get_base_location(); + if ( ! Features::is_enabled( 'subscriptions' ) || 'US' !== $base_location['country'] ) { $products['subscriptions']['product'] = 27147; } $product_types = self::append_product_data( $products ); @@ -690,7 +691,6 @@ class Onboarding { $settings['onboarding']['euCountries'] = WC()->countries->get_european_union_countries(); $settings['onboarding']['industries'] = self::get_allowed_industries(); $settings['onboarding']['localeInfo'] = include WC()->plugin_path() . '/i18n/locale-info.php'; - $settings['onboarding']['productTypes'] = self::get_allowed_product_types(); $settings['onboarding']['profile'] = $profile; $settings['onboarding']['themes'] = self::get_themes();