Merge branch 'trunk' into PCP-3786-manual-input

This commit is contained in:
Philipp Stracker 2024-10-31 17:41:01 +01:00 committed by GitHub
commit 6d0a6c8653
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 412 additions and 162 deletions

View file

@ -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';

View file

@ -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 (
<Container page={ PAGE_ONBOARDING }>
<Container page="onboarding">
<div className="ppcp-r-card">
<OnboardingStep
currentStep={ step }
<CurrentStepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
/>
</div>
</Container>
);
};
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 (
<CurrentStepComponent
setStep={ setStep }
currentStep={ safeCurrentStep }
setCompleted={ setCompleted }
stepperOrder={ stepperOrder }
/>
);
};
export default Onboarding;

View file

@ -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 (
<div className="ppcp-r-page-business">
@ -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"
>
<PaymentMethodIcons
@ -69,12 +80,10 @@ const StepBusiness = ( {
) }
icon="icon-business-business.svg"
name={ BUSINESS_RADIO_GROUP_NAME }
value={ BUSINESS_CHECKBOX_VALUE }
currentValue={ businessCategory }
changeCallback={ setBusinessCategory }
checked={
businessCategory === { BUSINESS_CHECKBOX_VALUE }
}
value={ BUSINESS_TYPES.BUSINESS }
changeCallback={ handleSellerTypeChange }
currentValue={ getCurrentValue() }
checked={ isCasualSeller === false }
type="radio"
>
<PaymentMethodIcons
@ -97,7 +106,7 @@ const StepBusiness = ( {
currentStep={ currentStep }
stepperOrder={ stepperOrder }
setCompleted={ setCompleted }
canProceeedCallback={ () => businessCategory !== null }
canProceeedCallback={ () => isCasualSeller !== null }
/>
</div>
</div>

View file

@ -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 (
<div className="ppcp-r-page-products">
@ -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"
>

View file

@ -1,10 +1,11 @@
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 { useManualConnect, useOnboardingDetails } from '../../../data';
import { useOnboardingStepWelcome, useManualConnect } from '../../../data';
import DataStoreControl from '../../ReusableComponents/DataStoreControl';
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
@ -84,7 +85,8 @@ const WelcomeForm = ( { setCompleted } ) => {
setClientId,
clientSecret,
setClientSecret,
} = useOnboardingDetails();
} =
();
const { connectManual } = useManualConnect();

View file

@ -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;
};

View file

@ -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',
};

View file

@ -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',
};

View file

@ -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.
*

View file

@ -1,14 +1,19 @@
import { useSelect, useDispatch } from '@wordpress/data';
import { NAMESPACE, STORE_NAME } from '../constants';
import apiFetch from '@wordpress/api-fetch';
import { NAMESPACE, 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.
@ -16,7 +21,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;
}, [] );
@ -33,6 +55,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();
@ -40,50 +77,76 @@ 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 };
};
export const useManualConnect = () => {
const connectManual = async ( clientId, clientSecret, isSandboxMode ) => {
return await apiFetch( {

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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 */