diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Container.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Container.js index e6b83e691..fd91017bf 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Container.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Container.js @@ -1,6 +1,3 @@ -export const PAGE_ONBOARDING = 'onboarding'; -export const PAGE_SETTINGS = 'settings'; - const Container = ( { isCard = true, page, children } ) => { let className = 'ppcp-r-container'; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index 195e3a089..20c18fc33 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -1,48 +1,36 @@ -import Container, { - PAGE_ONBOARDING, -} from '../../ReusableComponents/Container.js'; -import StepWelcome from './StepWelcome.js'; -import StepBusiness from './StepBusiness.js'; -import StepProducts from './StepProducts.js'; +import Container from '../../ReusableComponents/Container'; import { useOnboardingStep } from '../../../data'; +import { getSteps } from './availableSteps'; + +const getCurrentStep = ( requestedStep, steps ) => { + const isValidStep = ( step ) => + typeof step === 'number' && + Number.isInteger( step ) && + step >= 0 && + step < steps.length; + + const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0; + return steps[ safeCurrentStep ]; +}; const Onboarding = () => { - const { step, setStep, setCompleted } = useOnboardingStep(); + const { step, setStep, setCompleted, flags } = useOnboardingStep(); + const steps = getSteps( flags ); + + const CurrentStepComponent = getCurrentStep( step, steps ); return ( - +
-
); }; -const OnboardingStep = ( { currentStep, setStep, setCompleted } ) => { - const stepperOrder = [ StepWelcome, StepBusiness, StepProducts ]; - - const isValidStep = ( step ) => - typeof step === 'number' && - Number.isInteger( step ) && - step >= 0 && - step < stepperOrder.length; - - const safeCurrentStep = isValidStep( currentStep ) ? currentStep : 0; - - const CurrentStepComponent = stepperOrder[ safeCurrentStep ]; - - return ( - - ); -}; - export default Onboarding; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js index 9ac7de1c2..252045ba7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js @@ -1,10 +1,13 @@ -import OnboardingHeader from '../../ReusableComponents/OnboardingHeader.js'; -import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper.js'; -import SelectBox from '../../ReusableComponents/SelectBox.js'; +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; +import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; +import SelectBox from '../../ReusableComponents/SelectBox'; import { __ } from '@wordpress/i18n'; import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; -import { useState } from '@wordpress/element'; +import { useOnboardingStepBusiness } from '../../../data'; import Navigation from '../../ReusableComponents/Navigation'; +import { BUSINESS_TYPES } from '../../../data/constants'; + +const BUSINESS_RADIO_GROUP_NAME = 'business'; const StepBusiness = ( { setStep, @@ -12,10 +15,21 @@ const StepBusiness = ( { stepperOrder, setCompleted, } ) => { - const [ businessCategory, setBusinessCategory ] = useState( null ); - const BUSINESS_RADIO_GROUP_NAME = 'business'; - const CASUAL_SELLER_CHECKBOX_VALUE = 'casual_seller'; - const BUSINESS_CHECKBOX_VALUE = 'business'; + const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); + + const handleSellerTypeChange = ( value ) => { + setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value ); + }; + + const getCurrentValue = () => { + if ( isCasualSeller === null ) { + return ''; + } + + return isCasualSeller + ? BUSINESS_TYPES.CASUAL_SELLER + : BUSINESS_TYPES.BUSINESS; + }; return (
@@ -38,13 +52,10 @@ const StepBusiness = ( { ) } icon="icon-business-casual-seller.svg" name={ BUSINESS_RADIO_GROUP_NAME } - value={ CASUAL_SELLER_CHECKBOX_VALUE } - changeCallback={ setBusinessCategory } - currentValue={ businessCategory } - checked={ - businessCategory === - { CASUAL_SELLER_CHECKBOX_VALUE } - } + value={ BUSINESS_TYPES.CASUAL_SELLER } + changeCallback={ handleSellerTypeChange } + currentValue={ getCurrentValue() } + checked={ isCasualSeller === true } type="radio" > businessCategory !== null } + canProceeedCallback={ () => isCasualSeller !== null } />
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index efc477d2b..fecdc6029 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -3,7 +3,10 @@ import Navigation from '../../ReusableComponents/Navigation'; import { __ } from '@wordpress/i18n'; import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; -import { useState } from '@wordpress/element'; +import { useOnboardingStepProducts } from '../../../data'; +import { PRODUCT_TYPES } from '../../../data/constants'; + +const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; const StepProducts = ( { setStep, @@ -11,11 +14,7 @@ const StepProducts = ( { stepperOrder, setCompleted, } ) => { - const [ products, setProducts ] = useState( [] ); - const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; - const VIRTUAL_CHECKBOX_VALUE = 'virtual'; - const PHYSICAL_CHECKBOX_VALUE = 'physical'; - const SUBSCRIPTIONS_CHECKBOX_VALUE = 'subscriptions'; + const { products, toggleProduct } = useOnboardingStepProducts(); return (
@@ -35,8 +34,8 @@ const StepProducts = ( { ) } icon="icon-product-virtual.svg" name={ PRODUCTS_CHECKBOX_GROUP_NAME } - value={ VIRTUAL_CHECKBOX_VALUE } - changeCallback={ setProducts } + value={ PRODUCT_TYPES.VIRTUAL } + changeCallback={ toggleProduct } currentValue={ products } type="checkbox" > @@ -78,8 +77,8 @@ const StepProducts = ( { ) } icon="icon-product-physical.svg" name={ PRODUCTS_CHECKBOX_GROUP_NAME } - value={ PHYSICAL_CHECKBOX_VALUE } - changeCallback={ setProducts } + value={ PRODUCT_TYPES.PHYSICAL } + changeCallback={ toggleProduct } currentValue={ products } type="checkbox" > @@ -106,8 +105,8 @@ const StepProducts = ( { ) } icon="icon-product-subscription.svg" name={ PRODUCTS_CHECKBOX_GROUP_NAME } - value={ SUBSCRIPTIONS_CHECKBOX_VALUE } - changeCallback={ setProducts } + value={ PRODUCT_TYPES.SUBSCRIPTIONS } + changeCallback={ toggleProduct } currentValue={ products } type="checkbox" > diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index b6a5bb097..b5b05848a 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -1,10 +1,10 @@ -import OnboardingHeader from '../../ReusableComponents/OnboardingHeader.js'; +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import { __, sprintf } from '@wordpress/i18n'; import { Button, TextControl } from '@wordpress/components'; import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; import SettingsToggleBlock from '../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../ReusableComponents/Separator'; -import { useOnboardingDetails } from '../../../data'; +import { useOnboardingStepWelcome } from '../../../data'; import DataStoreControl from '../../ReusableComponents/DataStoreControl'; const StepWelcome = ( { setStep, currentStep } ) => { @@ -84,7 +84,7 @@ const WelcomeForm = () => { setClientId, clientSecret, setClientSecret, - } = useOnboardingDetails(); + } = useOnboardingStepWelcome(); const advancedUsersDescription = sprintf( // translators: %s: Link to PayPal REST application guide diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js new file mode 100644 index 000000000..32034ec57 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js @@ -0,0 +1,13 @@ +import StepWelcome from './StepWelcome'; +import StepBusiness from './StepBusiness'; +import StepProducts from './StepProducts'; + +export const getSteps = ( flags ) => { + const allSteps = [ StepWelcome, StepBusiness, StepProducts ]; + + if ( ! flags.canUseCasualSelling ) { + return allSteps.filter( ( step ) => step !== StepBusiness ); + } + + return allSteps; +}; diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js index 61ebf948e..e6f8f9de5 100644 --- a/modules/ppcp-settings/resources/js/data/constants.js +++ b/modules/ppcp-settings/resources/js/data/constants.js @@ -1,2 +1,13 @@ export const NAMESPACE = '/wc/v3/wc_paypal'; export const STORE_NAME = 'wc/paypal'; + +export const BUSINESS_TYPES = { + CASUAL_SELLER: 'casual_seller', + BUSINESS: 'business', +}; + +export const PRODUCT_TYPES = { + VIRTUAL: 'virtual', + PHYSICAL: 'physical', + SUBSCRIPTIONS: 'subscriptions', +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index ac5a6a0df..5d719f417 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -1,4 +1,6 @@ export default { + RESET_ONBOARDING: 'RESET_ONBOARDING', + // Transient data. SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY', SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING', @@ -11,4 +13,6 @@ export default { SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE', SET_CLIENT_ID: 'SET_CLIENT_ID', SET_CLIENT_SECRET: 'SET_CLIENT_SECRET', + SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER', + SET_PRODUCTS: 'SET_PRODUCTS', }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 6ac070d3a..1409c471a 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -3,6 +3,15 @@ import { apiFetch } from '@wordpress/data-controls'; import ACTION_TYPES from './action-types'; import { NAMESPACE, STORE_NAME } from '../constants'; +/** + * Special. Resets all values in the onboarding store to initial defaults. + * + * @return {{type: string}} The action. + */ +export const resetOnboarding = () => { + return { type: ACTION_TYPES.RESET_ONBOARDING }; +}; + /** * Non-persistent. Marks the onboarding details as "ready", i.e., fully initialized. * @@ -32,7 +41,7 @@ export const setIsSaving = ( isSaving ) => { /** * Persistent. Set the full onboarding details, usually during app initialization. * - * @param {Object} payload + * @param {{data: {}, flags?: {}}} payload * @return {{type: string, payload}} The action. */ export const setOnboardingDetails = ( payload ) => { @@ -120,6 +129,32 @@ export const setClientSecret = ( clientSecret ) => { }; }; +/** + * Persistent. Sets the "isCasualSeller" value. + * + * @param {boolean} isCasualSeller + * @return {{type: string, isCasualSeller}} The action. + */ +export const setIsCasualSeller = ( isCasualSeller ) => { + return { + type: ACTION_TYPES.SET_IS_CASUAL_SELLER, + isCasualSeller, + }; +}; + +/** + * Persistent. Sets the "products" array. + * + * @param {string[]} products + * @return {{type: string, products}} The action. + */ +export const setProducts = ( products ) => { + return { + type: ACTION_TYPES.SET_PRODUCTS, + products, + }; +}; + /** * Saves the persistent details to the WP database. * diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index 66bfec24a..e21cc5016 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -1,13 +1,18 @@ import { useSelect, useDispatch } from '@wordpress/data'; -import { STORE_NAME } from '../constants'; +import { PRODUCT_TYPES, STORE_NAME } from '../constants'; +import { getFlags } from './selectors'; -export const useOnboardingDetails = () => { +const useOnboardingDetails = () => { const { persist, + setOnboardingStep, + setCompleted, setSandboxMode, setManualConnectionMode, setClientId, setClientSecret, + setIsCasualSeller, + setProducts, } = useDispatch( STORE_NAME ); // Transient accessors. @@ -15,7 +20,24 @@ export const useOnboardingDetails = () => { return select( STORE_NAME ).getTransientData().isSaving; }, [] ); + const isReady = useSelect( ( select ) => { + return select( STORE_NAME ).getTransientData().isReady; + } ); + + // Read-only flags. + const flags = useSelect( ( select ) => { + return select( STORE_NAME ).getFlags(); + } ); + // Persistent accessors. + const step = useSelect( ( select ) => { + return select( STORE_NAME ).getPersistentData().step || 0; + } ); + + const completed = useSelect( ( select ) => { + return select( STORE_NAME ).getPersistentData().completed; + } ); + const clientId = useSelect( ( select ) => { return select( STORE_NAME ).getPersistentData().clientId; }, [] ); @@ -32,6 +54,21 @@ export const useOnboardingDetails = () => { return select( STORE_NAME ).getPersistentData().useManualConnection; }, [] ); + const isCasualSeller = useSelect( ( select ) => { + return select( STORE_NAME ).getPersistentData().isCasualSeller; + }, [] ); + + const products = useSelect( ( select ) => { + return select( STORE_NAME ).getPersistentData().products || []; + }, [] ); + + const toggleProduct = ( list ) => { + const validProducts = list.filter( ( item ) => + Object.values( PRODUCT_TYPES ).includes( item ) + ); + return setDetailAndPersist( setProducts, validProducts ); + }; + const setDetailAndPersist = async ( setter, value ) => { setter( value ); await persist(); @@ -39,46 +76,72 @@ export const useOnboardingDetails = () => { return { isSaving, - isSandboxMode, - isManualConnectionMode, - clientId, - setClientId: ( value ) => setDetailAndPersist( setClientId, value ), - clientSecret, - setClientSecret: ( value ) => - setDetailAndPersist( setClientSecret, value ), - setSandboxMode: ( state ) => - setDetailAndPersist( setSandboxMode, state ), - setManualConnectionMode: ( state ) => - setDetailAndPersist( setManualConnectionMode, state ), - }; -}; - -export const useOnboardingStep = () => { - const { persist, setOnboardingStep, setCompleted } = - useDispatch( STORE_NAME ); - - const isReady = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isReady; - } ); - - const step = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().step || 0; - } ); - - const completed = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().completed; - } ); - - const setDetailAndPersist = async ( setter, value ) => { - setter( value ); - await persist(); - }; - - return { isReady, step, setStep: ( value ) => setDetailAndPersist( setOnboardingStep, value ), completed, setCompleted: ( state ) => setDetailAndPersist( setCompleted, state ), + isSandboxMode, + setSandboxMode: ( state ) => + setDetailAndPersist( setSandboxMode, state ), + isManualConnectionMode, + setManualConnectionMode: ( state ) => + setDetailAndPersist( setManualConnectionMode, state ), + clientId, + setClientId: ( value ) => setDetailAndPersist( setClientId, value ), + clientSecret, + setClientSecret: ( value ) => + setDetailAndPersist( setClientSecret, value ), + isCasualSeller, + setIsCasualSeller: ( value ) => + setDetailAndPersist( setIsCasualSeller, value ), + products, + toggleProduct, + flags, }; }; + +export const useOnboardingStepWelcome = () => { + const { + isSaving, + isSandboxMode, + setSandboxMode, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + } = useOnboardingDetails(); + + return { + isSaving, + isSandboxMode, + setSandboxMode, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + }; +}; + +export const useOnboardingStepBusiness = () => { + const { isCasualSeller, setIsCasualSeller } = useOnboardingDetails(); + + return { isCasualSeller, setIsCasualSeller }; +}; + +export const useOnboardingStepProducts = () => { + const { products, toggleProduct } = useOnboardingDetails(); + + return { products, toggleProduct }; +}; + +export const useOnboardingStep = () => { + const { isReady, step, setStep, completed, setCompleted, flags } = + useOnboardingDetails(); + + return { isReady, step, setStep, completed, setCompleted, flags }; +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index e349d1f14..c0a47cb5e 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -3,6 +3,8 @@ import ACTION_TYPES from './action-types'; const defaultState = { isReady: false, isSaving: false, + + // Data persisted to the server. data: { completed: false, step: 0, @@ -10,6 +12,12 @@ const defaultState = { useManualConnection: false, clientId: '', clientSecret: '', + isCasualSeller: null, // null value will uncheck both options in the UI. + products: [], + }, + + // Read only values, provided by the server. + flags: { canUseCasualSelling: false, canUseVaulting: false, canUseCardPayments: false, @@ -40,6 +48,10 @@ export const onboardingReducer = ( }; switch ( type ) { + // Reset store to initial state. + case ACTION_TYPES.RESET_ONBOARDING: + return setPersistent( defaultState.data ); + // Transient data. case ACTION_TYPES.SET_ONBOARDING_IS_READY: return setTransient( { isReady: action.isReady } ); @@ -49,7 +61,13 @@ export const onboardingReducer = ( // Persistent data. case ACTION_TYPES.SET_ONBOARDING_DETAILS: - return setPersistent( action.payload ); + const newState = setPersistent( action.payload.data ); + + if ( action.payload.flags ) { + newState.flags = { ...newState.flags, ...action.payload.flags }; + } + + return newState; case ACTION_TYPES.SET_ONBOARDING_COMPLETED: return setPersistent( { completed: action.completed } ); @@ -71,6 +89,12 @@ export const onboardingReducer = ( useManualConnection: action.useManualConnection, } ); + case ACTION_TYPES.SET_IS_CASUAL_SELLER: + return setPersistent( { isCasualSeller: action.isCasualSeller } ); + + case ACTION_TYPES.SET_PRODUCTS: + return setPersistent( { products: action.products } ); + default: return state; } diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index 357ef963b..b7721b992 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -13,6 +13,10 @@ export const getPersistentData = ( state ) => { }; export const getTransientData = ( state ) => { - const { data, ...transientState } = getOnboardingState( state ); + const { data, flags, ...transientState } = getOnboardingState( state ); return transientState || EMPTY_OBJ; }; + +export const getFlags = ( state ) => { + return getOnboardingState( state ).flags || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/resources/js/data/store.js b/modules/ppcp-settings/resources/js/data/store.js index 32a05411f..a4acaf548 100644 --- a/modules/ppcp-settings/resources/js/data/store.js +++ b/modules/ppcp-settings/resources/js/data/store.js @@ -44,9 +44,14 @@ export const initStore = () => { console.groupEnd(); }; + window.ppcpSettings.resetStore = () => { + wp.data.dispatch( STORE_NAME ).resetOnboarding(); + wp.data.dispatch( STORE_NAME ).persist(); + }; window.ppcpSettings.startOnboarding = () => { wp.data.dispatch( STORE_NAME ).setCompleted( false ); wp.data.dispatch( STORE_NAME ).setOnboardingStep( 0 ); + wp.data.dispatch( STORE_NAME ).persist(); }; } /* eslint-enable no-console */ diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index f34257f79..3620957f9 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -14,7 +14,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; return array( - 'settings.url' => static function ( ContainerInterface $container ) : string { + 'settings.url' => static function ( ContainerInterface $container ) : string { /** * The path cannot be false. * @@ -25,12 +25,13 @@ return array( dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile { - $can_use_casual_selling = false; + 'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile { + $can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' ); $can_use_vaulting = $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' ); $can_use_card_payments = $container->has( 'card-fields.eligible' ) && $container->get( 'card-fields.eligible' ); // Card payments are disabled for this plugin when WooPayments is active. + // TODO: Move this condition to the card-fields.eligible service? if ( class_exists( '\WC_Payments' ) ) { $can_use_card_payments = false; } @@ -41,7 +42,26 @@ return array( $can_use_card_payments ); }, - 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { + 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); }, + 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array { + // TODO: This is a dummy list, while we wait for the official eligibility list. + + return array( + 'US', + 'CA', + 'DE', + 'ES', + 'AT', + 'CH', + 'NL', + ); + }, + 'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool { + $country = $container->get( 'api.shop.country' ); + $eligible_countries = $container->get( 'settings.casual-selling.supported-countries' ); + + return in_array( $country, $eligible_countries, true ); + }, ); diff --git a/modules/ppcp-settings/src/Data/AbstractDataModel.php b/modules/ppcp-settings/src/Data/AbstractDataModel.php index 1d711cbe5..780ad40bd 100644 --- a/modules/ppcp-settings/src/Data/AbstractDataModel.php +++ b/modules/ppcp-settings/src/Data/AbstractDataModel.php @@ -89,10 +89,38 @@ abstract class AbstractDataModel { continue; } - $setter = "set_$key"; - if ( method_exists( $this, $setter ) ) { + $setter = $this->get_setter_name( $key ); + + if ( $setter && method_exists( $this, $setter ) ) { $this->$setter( $value ); } } } + + /** + * Generates a setter method name for a given key, stripping the prefix from + * boolean fields (is_, use_, has_). + * + * @param int|string $field_key The key for which to generate a setter name. + * + * @return string The generated setter method name. + */ + private function get_setter_name( $field_key ) : string { + if ( ! is_string( $field_key ) ) { + return ''; + } + + $prefixes_to_strip = array( 'is_', 'use_', 'has_' ); + $stripped_key = $field_key; + + foreach ( $prefixes_to_strip as $prefix ) { + if ( str_starts_with( $field_key, $prefix ) ) { + $stripped_key = substr( $field_key, strlen( $prefix ) ); + break; + } + } + + return $stripped_key ? "set_$stripped_key" : ''; + } + } diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index 9f4f2ce2b..e1f9e16b4 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -29,6 +29,13 @@ class OnboardingProfile extends AbstractDataModel { */ protected const OPTION_KEY = 'woocommerce-ppcp-data-onboarding'; + /** + * List of customization flags, provided by the server (read-only). + * + * @var array + */ + protected array $flags = array(); + /** * Constructor. * @@ -45,9 +52,9 @@ class OnboardingProfile extends AbstractDataModel { ) { parent::__construct(); - $this->data['can_use_casual_selling'] = $can_use_casual_selling; - $this->data['can_use_vaulting'] = $can_use_vaulting; - $this->data['can_use_card_payments'] = $can_use_card_payments; + $this->flags['can_use_casual_selling'] = $can_use_casual_selling; + $this->flags['can_use_vaulting'] = $can_use_vaulting; + $this->flags['can_use_card_payments'] = $can_use_card_payments; } /** @@ -57,15 +64,14 @@ class OnboardingProfile extends AbstractDataModel { */ protected function get_defaults() : array { return array( - 'completed' => false, - 'step' => 0, - 'use_sandbox' => false, - 'use_manual_connection' => false, - 'client_id' => '', - 'client_secret' => '', - 'can_use_casual_selling' => null, - 'can_use_vaulting' => null, - 'can_use_card_payments' => null, + 'completed' => false, + 'step' => 0, + 'use_sandbox' => false, + 'use_manual_connection' => false, + 'client_id' => '', + 'client_secret' => '', + 'is_casual_seller' => null, + 'products' => array(), ); } @@ -112,7 +118,7 @@ class OnboardingProfile extends AbstractDataModel { * * @return bool */ - public function get_use_sandbox() : bool { + public function get_sandbox() : bool { return (bool) $this->data['use_sandbox']; } @@ -121,7 +127,7 @@ class OnboardingProfile extends AbstractDataModel { * * @param bool $use_sandbox Whether to use sandbox mode. */ - public function set_use_sandbox( bool $use_sandbox ) : void { + public function set_sandbox( bool $use_sandbox ) : void { $this->data['use_sandbox'] = $use_sandbox; } @@ -130,7 +136,7 @@ class OnboardingProfile extends AbstractDataModel { * * @return bool */ - public function get_use_manual_connection() : bool { + public function get_manual_connection() : bool { return (bool) $this->data['use_manual_connection']; } @@ -139,7 +145,7 @@ class OnboardingProfile extends AbstractDataModel { * * @param bool $use_manual_connection Whether to use manual connection. */ - public function set_use_manual_connection( bool $use_manual_connection ) : void { + public function set_manual_connection( bool $use_manual_connection ) : void { $this->data['use_manual_connection'] = $use_manual_connection; } @@ -180,29 +186,47 @@ class OnboardingProfile extends AbstractDataModel { } /** - * Gets whether casual selling can be used. + * Gets the casual seller flag. * - * @return bool + * @return bool|null */ - public function get_can_use_casual_selling() : bool { - return (bool) $this->data['can_use_casual_selling']; + public function get_casual_seller() : ?bool { + return $this->data['is_casual_seller']; } /** - * Gets whether vaulting can be used. + * Sets the casual-seller flag. * - * @return bool + * @param bool|null $casual_seller Whether the merchant uses a personal account for selling. */ - public function get_can_use_vaulting() : bool { - return (bool) $this->data['can_use_vaulting']; + public function set_casual_seller( ?bool $casual_seller ) : void { + $this->data['is_casual_seller'] = $casual_seller; } /** - * Gets whether Credit Card payments can be used. + * Gets the active product types for this store. * - * @return bool + * @return string[] */ - public function get_can_use_card_payments() : bool { - return (bool) $this->data['can_use_card_payments']; + public function get_products() : array { + return $this->data['products']; + } + + /** + * Sets the list of active product types. + * + * @param string[] $products Any of ['virtual'|'physical'|'subscriptions']. + */ + public function set_products( array $products ) : void { + $this->data['products'] = $products; + } + + /** + * Returns the list of read-only customization flags + * + * @return array + */ + public function get_flags() : array { + return $this->flags; } } diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 6e5e47bb5..6c59b1622 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -41,41 +41,53 @@ class OnboardingRestEndpoint extends RestEndpoint { * @var array */ private array $field_map = array( - 'completed' => array( + 'completed' => array( 'js_name' => 'completed', 'sanitize' => 'to_boolean', ), - 'step' => array( + 'step' => array( 'js_name' => 'step', 'sanitize' => 'to_number', ), - 'use_sandbox' => array( + 'use_sandbox' => array( 'js_name' => 'useSandbox', 'sanitize' => 'to_boolean', ), - 'use_manual_connection' => array( + 'use_manual_connection' => array( 'js_name' => 'useManualConnection', 'sanitize' => 'to_boolean', ), - 'client_id' => array( + 'client_id' => array( 'js_name' => 'clientId', 'sanitize' => 'sanitize_text_field', ), - 'client_secret' => array( + 'client_secret' => array( 'js_name' => 'clientSecret', 'sanitize' => 'sanitize_text_field', ), + 'is_casual_seller' => array( + 'js_name' => 'isCasualSeller', + 'sanitize' => 'to_boolean', + ), + 'products' => array( + 'js_name' => 'products', + ), + ); + + /** + * Map the internal flags to JS names. + * + * @var array + */ + private array $flag_map = array( 'can_use_casual_selling' => array( - 'js_name' => 'canUseCasualSelling', - 'sanitize' => 'read_only', + 'js_name' => 'canUseCasualSelling', ), 'can_use_vaulting' => array( - 'js_name' => 'canUseVaulting', - 'sanitize' => 'read_only', + 'js_name' => 'canUseVaulting', ), 'can_use_card_payments' => array( - 'js_name' => 'canUseCardPayments', - 'sanitize' => 'read_only', + 'js_name' => 'canUseCardPayments', ), ); @@ -86,6 +98,8 @@ class OnboardingRestEndpoint extends RestEndpoint { */ public function __construct( OnboardingProfile $profile ) { $this->profile = $profile; + + $this->field_map['products']['sanitize'] = fn( $list ) => array_map( 'sanitize_text_field', $list ); } /** @@ -128,7 +142,17 @@ class OnboardingRestEndpoint extends RestEndpoint { $this->field_map ); - return rest_ensure_response( $js_data ); + $js_flags = $this->sanitize_for_javascript( + $this->profile->get_flags(), + $this->flag_map + ); + + return rest_ensure_response( + array( + 'data' => $js_data, + 'flags' => $js_flags, + ) + ); } /** diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 9bb98dfac..08191276b 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -65,7 +65,7 @@ class RestEndpoint extends WC_REST_Controller { if ( null === $sanitation_cb ) { $sanitized[ $key ] = $value; - } elseif ( method_exists( $this, $sanitation_cb ) ) { + } elseif ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) { $sanitized[ $key ] = $this->{$sanitation_cb}( $value ); } elseif ( is_callable( $sanitation_cb ) ) { $sanitized[ $key ] = $sanitation_cb( $value );