mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-31 06:52:50 +08:00
Merge remote-tracking branch 'origin/trunk' into PCP-3814-Create-dashboard-placeholder-page-in-new-settings-module
# Conflicts: # modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
This commit is contained in:
commit
3a9f2a4a1f
14 changed files with 640 additions and 112 deletions
|
@ -0,0 +1,58 @@
|
|||
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
|
||||
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
|
||||
|
||||
/**
|
||||
* Approach 1: Component Injection
|
||||
*
|
||||
* A generic wrapper that adds debounced store updates to any controlled component.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {React.ComponentType} props.control The controlled component to render
|
||||
* @param {string|number} props.value The controlled value
|
||||
* @param {Function} props.onChange Change handler
|
||||
* @param {number} [props.delay=300] Debounce delay in milliseconds
|
||||
*/
|
||||
const DataStoreControl = ( {
|
||||
control: ControlComponent,
|
||||
value: externalValue,
|
||||
onChange,
|
||||
delay = 300,
|
||||
...props
|
||||
} ) => {
|
||||
const [ internalValue, setInternalValue ] = useState( externalValue );
|
||||
const onChangeRef = useRef( onChange );
|
||||
onChangeRef.current = onChange;
|
||||
|
||||
const debouncedUpdate = useRef(
|
||||
debounce( ( value ) => {
|
||||
onChangeRef.current( value );
|
||||
}, delay )
|
||||
).current;
|
||||
|
||||
useEffect( () => {
|
||||
setInternalValue( externalValue );
|
||||
debouncedUpdate?.cancel();
|
||||
}, [ externalValue ] );
|
||||
|
||||
useEffect( () => {
|
||||
return () => debouncedUpdate?.cancel();
|
||||
}, [ debouncedUpdate ] );
|
||||
|
||||
const handleChange = useCallback(
|
||||
( newValue ) => {
|
||||
setInternalValue( newValue );
|
||||
debouncedUpdate( newValue );
|
||||
},
|
||||
[ debouncedUpdate ]
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlComponent
|
||||
{ ...props }
|
||||
value={ internalValue }
|
||||
onChange={ handleChange }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataStoreControl;
|
|
@ -1,9 +1,6 @@
|
|||
import { useState } from '@wordpress/element';
|
||||
import { ToggleControl } from '@wordpress/components';
|
||||
|
||||
const SettingsToggleBlock = ( props ) => {
|
||||
const [ isToggled, setToggled ] = useState( false );
|
||||
|
||||
const SettingsToggleBlock = ( { isToggled, setToggled, ...props } ) => {
|
||||
return (
|
||||
<div className="ppcp-r-toggle-block">
|
||||
<div className="ppcp-r-toggle-block__wrapper">
|
||||
|
|
|
@ -4,6 +4,8 @@ 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 DataStoreControl from '../../ReusableComponents/DataStoreControl';
|
||||
|
||||
const StepWelcome = ( { setStep, currentStep } ) => {
|
||||
return (
|
||||
|
@ -73,6 +75,17 @@ const WelcomeFeatures = () => {
|
|||
};
|
||||
|
||||
const WelcomeForm = () => {
|
||||
const {
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
} = useOnboardingDetails();
|
||||
|
||||
const advancedUsersDescription = sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
|
@ -93,8 +106,10 @@ const WelcomeForm = () => {
|
|||
'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
isToggled={ !! isSandboxMode }
|
||||
setToggled={ setSandboxMode }
|
||||
>
|
||||
<Button variant="primary">
|
||||
<Button variant="secondary">
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</SettingsToggleBlock>
|
||||
|
@ -105,22 +120,29 @@ const WelcomeForm = () => {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ advancedUsersDescription }
|
||||
isToggled={ !! isManualConnectionMode }
|
||||
setToggled={ setManualConnectionMode }
|
||||
>
|
||||
<TextControl
|
||||
<DataStoreControl
|
||||
control={ TextControl }
|
||||
label={ __(
|
||||
'Sandbox Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
></TextControl>
|
||||
|
||||
<TextControl
|
||||
value={ clientId }
|
||||
onChange={ setClientId }
|
||||
/>
|
||||
<DataStoreControl
|
||||
control={ TextControl }
|
||||
label={ __(
|
||||
'Sandbox Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
value={ clientSecret }
|
||||
onChange={ setClientSecret }
|
||||
type="password"
|
||||
></TextControl>
|
||||
<Button variant="primary">
|
||||
/>
|
||||
<Button variant="secondary">
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</SettingsToggleBlock>
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
export default {
|
||||
SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS',
|
||||
// Transient data.
|
||||
SET_IS_SAVING_ONBOARDING_DETAILS: 'SET_IS_SAVING_ONBOARDING_DETAILS',
|
||||
|
||||
// Persistent data.
|
||||
SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS',
|
||||
SET_ONBOARDING_STEP: 'SET_ONBOARDING_STEP',
|
||||
SET_SANDBOX_MODE: 'SET_SANDBOX_MODE',
|
||||
SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE',
|
||||
SET_CLIENT_ID: 'SET_CLIENT_ID',
|
||||
SET_CLIENT_SECRET: 'SET_CLIENT_SECRET',
|
||||
};
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
import { dispatch, select } from '@wordpress/data';
|
||||
import { select } from '@wordpress/data';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import ACTION_TYPES from './action-types';
|
||||
import { NAMESPACE, STORE_NAME } from '../constants';
|
||||
|
||||
/**
|
||||
* Non-persistent. Changes the "saving" flag.
|
||||
*
|
||||
* @param {boolean} isSaving
|
||||
* @return {{type: string, isSaving}} The action.
|
||||
*/
|
||||
export const setIsSaving = ( isSaving ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING_DETAILS,
|
||||
isSaving,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Set the full onboarding details, usually during app initialization.
|
||||
*
|
||||
* @param {Object} payload
|
||||
* @return {{payload, type: string}} The action.
|
||||
* @return {{type: string, payload}} The action.
|
||||
*/
|
||||
export const updateOnboardingDetails = ( payload ) => {
|
||||
export const setOnboardingDetails = ( payload ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_ONBOARDING_DETAILS,
|
||||
payload,
|
||||
|
@ -31,48 +43,81 @@ export const setOnboardingStep = ( step ) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Non-persistent. Changes the "saving" flag.
|
||||
* Persistent. Sets the sandbox mode on or off.
|
||||
*
|
||||
* @param {boolean} isSaving
|
||||
* @return {{type: string, isSaving}} The action.
|
||||
* @param {boolean} sandboxMode
|
||||
* @return {{type: string, useSandbox}} An action.
|
||||
*/
|
||||
export const updateIsSaving = ( isSaving ) => {
|
||||
export const setSandboxMode = ( sandboxMode ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING_DETAILS,
|
||||
isSaving,
|
||||
type: ACTION_TYPES.SET_SANDBOX_MODE,
|
||||
useSandbox: sandboxMode,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Toggles the "Manual Connection" mode on or off.
|
||||
*
|
||||
* @param {boolean} manualConnectionMode
|
||||
* @return {{type: string, useManualConnection}} An action.
|
||||
*/
|
||||
export const setManualConnectionMode = ( manualConnectionMode ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_MANUAL_CONNECTION_MODE,
|
||||
useManualConnection: manualConnectionMode,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "client ID" value.
|
||||
*
|
||||
* @param {string} clientId
|
||||
* @return {{type: string, clientId}} The action.
|
||||
*/
|
||||
export const setClientId = ( clientId ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_CLIENT_ID,
|
||||
clientId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "client secret" value.
|
||||
*
|
||||
* @param {string} clientSecret
|
||||
* @return {{type: string, clientSecret}} The action.
|
||||
*/
|
||||
export const setClientSecret = ( clientSecret ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_CLIENT_SECRET,
|
||||
clientSecret,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the persistent details to the WP database.
|
||||
*
|
||||
* @return {Generator<any>} A generator function that handles the saving process.
|
||||
* @return {any} A generator function that handles the saving process.
|
||||
*/
|
||||
export function* persist() {
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const path = `${ NAMESPACE }/onboarding`;
|
||||
const data = select( STORE_NAME ).getOnboardingData();
|
||||
const data = select( STORE_NAME ).getPersistentData();
|
||||
|
||||
yield updateIsSaving( true );
|
||||
yield setIsSaving( true );
|
||||
|
||||
yield apiFetch( {
|
||||
path,
|
||||
method: 'post',
|
||||
data,
|
||||
} );
|
||||
|
||||
yield dispatch( 'core/notices' ).createSuccessNotice(
|
||||
__( 'Progress saved.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
} catch ( e ) {
|
||||
error = e;
|
||||
yield dispatch( 'core/notices' ).createErrorNotice(
|
||||
__( 'Error saving progress.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
console.error( 'Error saving progress.', e );
|
||||
} finally {
|
||||
yield updateIsSaving( false );
|
||||
yield setIsSaving( false );
|
||||
}
|
||||
|
||||
return error === null;
|
||||
|
|
|
@ -2,22 +2,61 @@ import { useSelect, useDispatch } from '@wordpress/data';
|
|||
import { STORE_NAME } from '../constants';
|
||||
|
||||
export const useOnboardingDetails = () => {
|
||||
const { setOnboardingStep, persist } = useDispatch( STORE_NAME );
|
||||
const {
|
||||
setOnboardingStep,
|
||||
setSandboxMode,
|
||||
setManualConnectionMode,
|
||||
persist,
|
||||
setClientId,
|
||||
setClientSecret,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Transient accessors.
|
||||
const isSaving = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getTransientData().isSaving;
|
||||
}, [] );
|
||||
|
||||
// Persistent accessors.
|
||||
const clientId = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().clientId;
|
||||
}, [] );
|
||||
|
||||
const clientSecret = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().clientSecret;
|
||||
}, [] );
|
||||
|
||||
const onboardingStep = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getOnboardingStep();
|
||||
return select( STORE_NAME ).getPersistentData().step || 0;
|
||||
}, [] );
|
||||
|
||||
const isSaving = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).isSaving();
|
||||
const isSandboxMode = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().useSandbox;
|
||||
}, [] );
|
||||
|
||||
const isManualConnectionMode = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().useManualConnection;
|
||||
}, [] );
|
||||
|
||||
const setDetailAndPersist = async ( setter, value ) => {
|
||||
setter( value );
|
||||
await persist();
|
||||
};
|
||||
|
||||
return {
|
||||
onboardingStep,
|
||||
isSaving,
|
||||
setOnboardingStep: async ( step ) => {
|
||||
setOnboardingStep( step );
|
||||
await persist();
|
||||
},
|
||||
isSandboxMode,
|
||||
isManualConnectionMode,
|
||||
clientId,
|
||||
setClientId: ( value ) => setDetailAndPersist( setClientId, value ),
|
||||
clientSecret,
|
||||
setClientSecret: ( value ) =>
|
||||
setDetailAndPersist( setClientSecret, value ),
|
||||
setOnboardingStep: ( step ) =>
|
||||
setDetailAndPersist( setOnboardingStep, step ),
|
||||
setSandboxMode: ( state ) =>
|
||||
setDetailAndPersist( setSandboxMode, state ),
|
||||
setManualConnectionMode: ( state ) =>
|
||||
setDetailAndPersist( setManualConnectionMode, state ),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,6 +4,10 @@ const defaultState = {
|
|||
isSaving: false,
|
||||
data: {
|
||||
step: 0,
|
||||
useSandbox: false,
|
||||
useManualConnection: false,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -11,32 +15,54 @@ export const onboardingReducer = (
|
|||
state = defaultState,
|
||||
{ type, ...action }
|
||||
) => {
|
||||
switch ( type ) {
|
||||
case ACTION_TYPES.SET_ONBOARDING_DETAILS:
|
||||
return {
|
||||
...state,
|
||||
data: action.payload,
|
||||
};
|
||||
const setTransient = ( changes ) => {
|
||||
const { data, ...transientChanges } = changes;
|
||||
return { ...state, ...transientChanges };
|
||||
};
|
||||
|
||||
const setPersistent = ( changes ) => {
|
||||
const validChanges = Object.keys( changes ).reduce( ( acc, key ) => {
|
||||
if ( key in defaultState.data ) {
|
||||
acc[ key ] = changes[ key ];
|
||||
}
|
||||
return acc;
|
||||
}, {} );
|
||||
|
||||
return {
|
||||
...state,
|
||||
data: { ...state.data, ...validChanges },
|
||||
};
|
||||
};
|
||||
|
||||
switch ( type ) {
|
||||
// Transient data.
|
||||
case ACTION_TYPES.SET_IS_SAVING_ONBOARDING_DETAILS:
|
||||
return {
|
||||
...state,
|
||||
isSaving: action.isSaving,
|
||||
};
|
||||
return setTransient( { isSaving: action.isSaving } );
|
||||
|
||||
// Persistent data.
|
||||
case ACTION_TYPES.SET_CLIENT_ID:
|
||||
return setPersistent( { clientId: action.clientId } );
|
||||
|
||||
case ACTION_TYPES.SET_CLIENT_SECRET:
|
||||
return setPersistent( { clientSecret: action.clientSecret } );
|
||||
|
||||
case ACTION_TYPES.SET_ONBOARDING_DETAILS:
|
||||
return setPersistent( action.payload );
|
||||
|
||||
case ACTION_TYPES.SET_ONBOARDING_STEP:
|
||||
return {
|
||||
...state,
|
||||
data: {
|
||||
...( state.data || {} ),
|
||||
step: action.step,
|
||||
},
|
||||
};
|
||||
return setPersistent( { step: action.step } );
|
||||
|
||||
case ACTION_TYPES.SET_SANDBOX_MODE:
|
||||
return setPersistent( { useSandbox: action.useSandbox } );
|
||||
|
||||
case ACTION_TYPES.SET_MANUAL_CONNECTION_MODE:
|
||||
return setPersistent( {
|
||||
useManualConnection: action.useManualConnection,
|
||||
} );
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default onboardingReducer;
|
||||
|
|
|
@ -2,17 +2,17 @@ import { dispatch } from '@wordpress/data';
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
import { NAMESPACE } from '../constants';
|
||||
import { updateOnboardingDetails } from './actions';
|
||||
import { setOnboardingDetails } from './actions';
|
||||
|
||||
/**
|
||||
* Retrieve settings from the site's REST API.
|
||||
*/
|
||||
export function* getOnboardingData() {
|
||||
export function* getPersistentData() {
|
||||
const path = `${ NAMESPACE }/onboarding`;
|
||||
|
||||
try {
|
||||
const result = yield apiFetch( { path } );
|
||||
yield updateOnboardingDetails( result );
|
||||
yield setOnboardingDetails( result );
|
||||
} catch ( e ) {
|
||||
yield dispatch( 'core/notices' ).createErrorNotice(
|
||||
__(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const EMPTY_OBJ = {};
|
||||
const EMPTY_OBJ = Object.freeze( {} );
|
||||
|
||||
const getOnboardingState = ( state ) => {
|
||||
if ( ! state ) {
|
||||
|
@ -8,14 +8,11 @@ const getOnboardingState = ( state ) => {
|
|||
return state.onboarding || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const getOnboardingData = ( state ) => {
|
||||
export const getPersistentData = ( state ) => {
|
||||
return getOnboardingState( state ).data || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const isSaving = ( state ) => {
|
||||
return getOnboardingState( state ).isSaving || false;
|
||||
};
|
||||
|
||||
export const getOnboardingStep = ( state ) => {
|
||||
return getOnboardingData( state ).step || 0;
|
||||
export const getTransientData = ( state ) => {
|
||||
const { data, ...transientState } = getOnboardingState( state );
|
||||
return transientState || EMPTY_OBJ;
|
||||
};
|
||||
|
|
99
modules/ppcp-settings/src/Data/AbstractDataModel.php
Normal file
99
modules/ppcp-settings/src/Data/AbstractDataModel.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
/**
|
||||
* Abstract Data Model Base Class
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Data
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Data;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Abstract class AbstractDataModel
|
||||
*
|
||||
* Provides a base implementation for data models that can be serialized to and from arrays,
|
||||
* and provide persistence capabilities.
|
||||
*/
|
||||
abstract class AbstractDataModel {
|
||||
/**
|
||||
* Stores the model data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $data = array();
|
||||
|
||||
/**
|
||||
* Option key for WordPress storage.
|
||||
* Must be overridden by the child class!
|
||||
*/
|
||||
protected const OPTION_KEY = '';
|
||||
|
||||
/**
|
||||
* Default values for the model.
|
||||
* Child classes should override this method to define their default structure.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function get_defaults() : array;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( empty( static::OPTION_KEY ) ) {
|
||||
throw new RuntimeException( 'OPTION_KEY must be defined in child class.' );
|
||||
}
|
||||
|
||||
$this->data = $this->get_defaults();
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the model data from WordPress options.
|
||||
*/
|
||||
public function load() : void {
|
||||
$saved_data = get_option( static::OPTION_KEY, array() );
|
||||
$this->data = array_merge( $this->data, $saved_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the model data to WordPress options.
|
||||
*/
|
||||
public function save() : void {
|
||||
update_option( static::OPTION_KEY, $this->data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all model data as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() : array {
|
||||
return array_merge( array(), $this->data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all model data from an array.
|
||||
*
|
||||
* @param array $data The model data.
|
||||
*/
|
||||
public function from_array( array $data ) : void {
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( ! array_key_exists( $key, $this->data ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$setter = "set_$key";
|
||||
if ( method_exists( $this, $setter ) ) {
|
||||
$this->$setter( $value );
|
||||
} else {
|
||||
$this->data[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* Settings container class
|
||||
* Onboarding Profile class
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Data
|
||||
*/
|
||||
|
@ -10,33 +10,125 @@ declare( strict_types = 1 );
|
|||
namespace WooCommerce\PayPalCommerce\Settings\Data;
|
||||
|
||||
/**
|
||||
* Class OnboardingProfile
|
||||
*
|
||||
* This class serves as a container for managing the onboarding profile details
|
||||
* within the WooCommerce PayPal Commerce plugin. It provides methods to retrieve
|
||||
* and save the onboarding profile data using WordPress options.
|
||||
*/
|
||||
class OnboardingProfile {
|
||||
class OnboardingProfile extends AbstractDataModel {
|
||||
|
||||
/**
|
||||
* Options key where profile details are stored.
|
||||
* Option key where profile details are stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const KEY = 'woocommerce-ppcp-data-onboarding';
|
||||
protected const OPTION_KEY = 'woocommerce-ppcp-data-onboarding';
|
||||
|
||||
/**
|
||||
* Returns the current onboarding profile details.
|
||||
* Get default values for the model.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_data() : array {
|
||||
return get_option( self::KEY, array() );
|
||||
protected function get_defaults() : array {
|
||||
return array(
|
||||
'step' => 0,
|
||||
'use_sandbox' => false,
|
||||
'use_manual_connection' => false,
|
||||
'client_id' => '',
|
||||
'client_secret' => '',
|
||||
);
|
||||
}
|
||||
|
||||
// -----
|
||||
|
||||
/**
|
||||
* Gets the 'step' setting.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_step() : int {
|
||||
return (int) $this->data['step'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the onboarding profile details.
|
||||
* Sets the 'step' setting.
|
||||
*
|
||||
* @param array $data The profile details to save.
|
||||
* @param int $step Whether to use sandbox mode.
|
||||
*/
|
||||
public function save_data( array $data ) : void {
|
||||
update_option( self::KEY, $data );
|
||||
public function set_step( int $step ) : void {
|
||||
$this->data['step'] = $step;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 'use sandbox' setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_use_sandbox() : bool {
|
||||
return (bool) $this->data['use_sandbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'use sandbox' setting.
|
||||
*
|
||||
* @param bool $use_sandbox Whether to use sandbox mode.
|
||||
*/
|
||||
public function set_use_sandbox( bool $use_sandbox ) : void {
|
||||
$this->data['use_sandbox'] = $use_sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 'use manual connection' setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_use_manual_connection() : bool {
|
||||
return (bool) $this->data['use_manual_connection'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'use manual connection' setting.
|
||||
*
|
||||
* @param bool $use_manual_connection Whether to use manual connection.
|
||||
*/
|
||||
public function set_use_manual_connection( bool $use_manual_connection ) : void {
|
||||
$this->data['use_manual_connection'] = $use_manual_connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_client_id() : string {
|
||||
return $this->data['client_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client ID.
|
||||
*
|
||||
* @param string $client_id The client ID.
|
||||
*/
|
||||
public function set_client_id( string $client_id ) : void {
|
||||
$this->data['client_id'] = sanitize_text_field( $client_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client secret.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_client_secret() : string {
|
||||
return $this->data['client_secret'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client secret.
|
||||
*
|
||||
* @param string $client_secret The client secret.
|
||||
*/
|
||||
public function set_client_secret( string $client_secret ) : void {
|
||||
$this->data['client_secret'] = sanitize_text_field( $client_secret );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
|
|||
/**
|
||||
* REST controller for the onboarding module.
|
||||
*
|
||||
* Responsible for persisting and loading the state of the onboarding wizard.
|
||||
* This API acts as the intermediary between the "external world" and our
|
||||
* internal data model.
|
||||
*/
|
||||
class OnboardingRestEndpoint extends RestEndpoint {
|
||||
/**
|
||||
|
@ -32,7 +33,35 @@ class OnboardingRestEndpoint extends RestEndpoint {
|
|||
*
|
||||
* @var OnboardingProfile
|
||||
*/
|
||||
protected $profile;
|
||||
protected OnboardingProfile $profile;
|
||||
|
||||
/**
|
||||
* Field mapping for request to profile transformation.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $field_map = array(
|
||||
'step' => array(
|
||||
'js_name' => 'step',
|
||||
'sanitize' => 'to_number',
|
||||
),
|
||||
'use_sandbox' => array(
|
||||
'js_name' => 'useSandbox',
|
||||
'sanitize' => 'to_boolean',
|
||||
),
|
||||
'use_manual_connection' => array(
|
||||
'js_name' => 'useManualConnection',
|
||||
'sanitize' => 'to_boolean',
|
||||
),
|
||||
'client_id' => array(
|
||||
'js_name' => 'clientId',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
'client_secret' => array(
|
||||
'js_name' => 'clientSecret',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
@ -73,42 +102,35 @@ class OnboardingRestEndpoint extends RestEndpoint {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an object with all details of the current onboarding wizard
|
||||
* progress.
|
||||
* Returns all details of the current onboarding wizard progress.
|
||||
*
|
||||
* @return WP_REST_Response The current state of the onboarding wizard.
|
||||
*/
|
||||
public function get_details() : WP_REST_Response {
|
||||
$details = $this->profile->get_data();
|
||||
$js_data = $this->sanitize_for_javascript(
|
||||
$this->profile->to_array(),
|
||||
$this->field_map
|
||||
);
|
||||
|
||||
return rest_ensure_response( $details );
|
||||
return rest_ensure_response( $js_data );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Receives an object with onboarding details and persists it in the DB.
|
||||
* Updates onboarding details based on the request.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response The current state of the onboarding wizard.
|
||||
* @return WP_REST_Response The updated state of the onboarding wizard.
|
||||
*/
|
||||
public function update_details( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$details = $this->profile->get_data();
|
||||
$wp_data = $this->sanitize_for_wordpress(
|
||||
$request->get_params(),
|
||||
$this->field_map
|
||||
);
|
||||
|
||||
$get_param = fn( $key ) => wc_clean( wp_unslash( $request->get_param( $key ) ) );
|
||||
$this->profile->from_array( $wp_data );
|
||||
$this->profile->save();
|
||||
|
||||
$raw_step = $get_param( 'step' );
|
||||
$raw_completed = $get_param( 'completed' );
|
||||
|
||||
if ( is_numeric( $raw_step ) ) {
|
||||
$details['step'] = intval( $raw_step );
|
||||
}
|
||||
if ( null !== $raw_completed ) {
|
||||
$details['completed'] = (bool) $raw_completed;
|
||||
}
|
||||
|
||||
$this->profile->save_data( $details );
|
||||
|
||||
return rest_ensure_response( $details );
|
||||
return $this->get_details();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,4 +33,93 @@ class RestEndpoint extends WC_REST_Controller {
|
|||
public function check_permission() : bool {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes parameters based on a field mapping.
|
||||
*
|
||||
* This method iterates through a field map, applying sanitization methods
|
||||
* to the corresponding values in the input parameters array.
|
||||
*
|
||||
* @param array $params The input parameters to sanitize.
|
||||
* @param array $field_map An associative array mapping profile keys to sanitization rules.
|
||||
* Each rule should have 'js_name' and 'sanitize' keys.
|
||||
*
|
||||
* @return array An array of sanitized parameters.
|
||||
*/
|
||||
protected function sanitize_for_wordpress( array $params, array $field_map ) : array {
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $field_map as $key => $details ) {
|
||||
$source_key = $details['js_name'] ?? '';
|
||||
$sanitation_cb = $details['sanitize'] ?? null;
|
||||
|
||||
if ( ! $source_key || ! isset( $params[ $source_key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $params[ $source_key ];
|
||||
|
||||
if ( null === $sanitation_cb ) {
|
||||
$sanitized[ $key ] = $value;
|
||||
} elseif ( method_exists( $this, $sanitation_cb ) ) {
|
||||
$sanitized[ $key ] = $this->{$sanitation_cb}( $value );
|
||||
} elseif ( is_callable( $sanitation_cb ) ) {
|
||||
$sanitized[ $key ] = $sanitation_cb( $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes data for JavaScript based on a field mapping.
|
||||
*
|
||||
* This method transforms the input data array according to the provided field map,
|
||||
* renaming keys to their JavaScript equivalents as specified in the mapping.
|
||||
*
|
||||
* @param array $data The input data array to be sanitized.
|
||||
* @param array $field_map An associative array mapping PHP keys to JavaScript key names.
|
||||
* Each element should have a 'js_name' key specifying the JavaScript
|
||||
* name.
|
||||
*
|
||||
* @return array An array of sanitized data with keys renamed for JavaScript use.
|
||||
*/
|
||||
protected function sanitize_for_javascript( array $data, array $field_map ) : array {
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $field_map as $key => $details ) {
|
||||
$output_key = $details['js_name'] ?? '';
|
||||
|
||||
if ( ! $output_key || ! isset( $data[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sanitized[ $output_key ] = $data[ $key ];
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to a boolean.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
*
|
||||
* @return bool|null The boolean value, or null if not set.
|
||||
*/
|
||||
protected function to_boolean( $value ) : ?bool {
|
||||
return $value !== null ? (bool) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to a number.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
*
|
||||
* @return int|float|null The numeric value, or null if not set.
|
||||
*/
|
||||
protected function to_number( $value ) {
|
||||
return $value !== null ? ( is_numeric( $value ) ? $value + 0 : null ) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue