mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 10:47:21 +08:00
Merge remote-tracking branch 'origin/trunk' into PCP-3896-dynamic-content-for-prices-in-onboarding-wizard
# Conflicts: # modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js # modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js # modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js
This commit is contained in:
commit
d0ba54e1c9
64 changed files with 2716 additions and 972 deletions
|
@ -1,3 +1,4 @@
|
|||
import { useEffect } from '@wordpress/element';
|
||||
import { Icon } from '@wordpress/components';
|
||||
import { chevronDown, chevronUp } from '@wordpress/icons';
|
||||
|
||||
|
@ -5,11 +6,33 @@ import { useState } from 'react';
|
|||
|
||||
const Accordion = ( {
|
||||
title,
|
||||
initiallyOpen = false,
|
||||
initiallyOpen = null,
|
||||
className = '',
|
||||
id = '',
|
||||
children,
|
||||
} ) => {
|
||||
const [ isOpen, setIsOpen ] = useState( initiallyOpen );
|
||||
const determineInitialState = () => {
|
||||
if ( id && initiallyOpen === null ) {
|
||||
return window.location.hash === `#${ id }`;
|
||||
}
|
||||
return !! initiallyOpen;
|
||||
};
|
||||
|
||||
const [ isOpen, setIsOpen ] = useState( determineInitialState );
|
||||
|
||||
useEffect( () => {
|
||||
const handleHashChange = () => {
|
||||
if ( id && window.location.hash === `#${ id }` ) {
|
||||
setIsOpen( true );
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener( 'hashchange', handleHashChange );
|
||||
|
||||
return () => {
|
||||
window.removeEventListener( 'hashchange', handleHashChange );
|
||||
};
|
||||
}, [ id ] );
|
||||
|
||||
const toggleOpen = ( ev ) => {
|
||||
setIsOpen( ! isOpen );
|
||||
|
@ -26,7 +49,7 @@ const Accordion = ( {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={ wrapperClasses.join( ' ' ) }>
|
||||
<div className={ wrapperClasses.join( ' ' ) } id={ id }>
|
||||
<button
|
||||
onClick={ toggleOpen }
|
||||
className="ppcp-r-accordion--title"
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import Separator from '../Separator';
|
||||
|
||||
const AcdcOptionalPaymentMethods = ( {
|
||||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
} ) => {
|
||||
if ( isFastlane && isPayLater && storeCountry === 'us' ) {
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods__wrapper">
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Custom Card Fields',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-visa.svg',
|
||||
'icon-button-mastercard.svg',
|
||||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 2.59% + $0.49 USD<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Digital Wallets',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-apple-pay.svg',
|
||||
'icon-button-google-pay.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 2.59% + $0.49 USD<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Accept Apple Pay on eligible devices and Google Pay through mobile and web. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Alternative Payment Methods',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-sepa.svg',
|
||||
'icon-button-ideal.svg',
|
||||
'icon-button-blik.svg',
|
||||
'icon-button-bancontact.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 3.49% + $0.49 USD<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Seamless payments for customers across the globe using their preferred payment methods. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __( '', 'woocommerce-paypal-payments' ) }
|
||||
imageBadge={ [ 'icon-payment-method-fastlane-small.svg' ] }
|
||||
textBadge={ __(
|
||||
'from 2.59% + $0.49 USD<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Speed up guest checkout with Fatslane. Link a customer\'s email address to their payment details. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( isPayLater && storeCountry === 'uk' ) {
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods__wrapper">
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Custom Card Fields',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-visa.svg',
|
||||
'icon-button-mastercard.svg',
|
||||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 1.20% + £0.30 GBP<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Digital Wallets',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-apple-pay.svg',
|
||||
'icon-button-google-pay.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 1.20% + £0.30 GBP<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Accept Apple Pay on eligible devices and Google Pay through mobile and web. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Alternative Payment Methods',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-sepa.svg',
|
||||
'icon-button-ideal.svg',
|
||||
'icon-button-blik.svg',
|
||||
'icon-button-bancontact.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 1.20% + £0.30 GBP<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Seamless payments for customers across the globe using their preferred payment methods. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods__wrapper">
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Optional payment methods',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
titleType={ BADGE_BOX_TITLE_BIG }
|
||||
description={ __(
|
||||
'with additional application',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Custom Card Fields',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-visa.svg',
|
||||
'icon-button-mastercard.svg',
|
||||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 3.40% + €0.35 EUR<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __( 'Digital Wallets', 'woocommerce-paypal-payments' ) }
|
||||
imageBadge={ [
|
||||
'icon-button-apple-pay.svg',
|
||||
'icon-button-google-pay.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 3.40% + €0.35 EUR<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Accept Apple Pay on eligible devices and Google Pay through mobile and web. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
<Separator className="ppcp-r-optional-payment-methods__separator" />
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Alternative Payment Methods',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-sepa.svg',
|
||||
'icon-button-ideal.svg',
|
||||
'icon-button-blik.svg',
|
||||
'icon-button-bancontact.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 3.40% + €0.35 EUR<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Seamless payments for customers across the globe using their preferred payment methods. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AcdcOptionalPaymentMethods;
|
|
@ -0,0 +1,66 @@
|
|||
import BadgeBox from '../BadgeBox';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => {
|
||||
if ( isPayLater && storeCountry === 'us' ) {
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods__wrapper">
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Credit and Debit Cards',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-visa.svg',
|
||||
'icon-button-mastercard.svg',
|
||||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 2.59% + $0.49 USD<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Process major credit and debit cards through PayPal’s card fields. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods__wrapper">
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Credit and Debit Cards',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-visa.svg',
|
||||
'icon-button-mastercard.svg',
|
||||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ __(
|
||||
'from 3.40% + €0.35 EUR<sup>1</sup>',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Process major credit and debit cards through PayPal’s card fields. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BcdcOptionalPaymentMethods;
|
|
@ -0,0 +1,28 @@
|
|||
import AcdcOptionalPaymentMethods from './AcdcOptionalPaymentMethods';
|
||||
import BcdcOptionalPaymentMethods from './BcdcOptionalPaymentMethods';
|
||||
|
||||
const OptionalPaymentMethods = ( {
|
||||
useAcdc,
|
||||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
} ) => {
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods">
|
||||
{ useAcdc ? (
|
||||
<AcdcOptionalPaymentMethods
|
||||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
/>
|
||||
) : (
|
||||
<BcdcOptionalPaymentMethods
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionalPaymentMethods;
|
|
@ -1,24 +1,32 @@
|
|||
const Separator = ( props ) => {
|
||||
let separatorClass = 'ppcp-r-separator';
|
||||
const Separator = ( { className = '', text = '', withLine = true } ) => {
|
||||
const separatorClass = [ 'ppcp-r-separator' ];
|
||||
const innerClass = withLine
|
||||
? 'ppcp-r-separator__line'
|
||||
: 'ppcp-r-separator__space';
|
||||
|
||||
if ( props?.className ) {
|
||||
separatorClass += ' ' + props.className;
|
||||
if ( className ) {
|
||||
separatorClass.push( className );
|
||||
}
|
||||
|
||||
if ( props.text ) {
|
||||
return (
|
||||
<div className={ separatorClass }>
|
||||
<span className="ppcp-r-separator__line ppcp-r-separator__line--before"></span>
|
||||
const getClass = ( type ) => `${ innerClass } ${ innerClass }--${ type }`;
|
||||
|
||||
<span className="ppcp-r-separator__text">{ props.text }</span>
|
||||
<span className="ppcp-r-separator__line ppcp-r-separator__line--after"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const renderSeparator = () => {
|
||||
if ( text ) {
|
||||
return (
|
||||
<>
|
||||
<span className={ getClass( 'before' ) }></span>
|
||||
<span className="ppcp-r-separator__text">{ text }</span>
|
||||
<span className={ getClass( 'after' ) }></span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={ getClass( 'full' ) }></span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ separatorClass }>
|
||||
<span className="ppcp-r-separator__line ppcp-r-separator__line--before"></span>
|
||||
<div className={ separatorClass.join( ' ' ) }>
|
||||
{ renderSeparator() }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,24 +7,25 @@ import { store as noticesStore } from '@wordpress/notices';
|
|||
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
|
||||
import Separator from '../../../ReusableComponents/Separator';
|
||||
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
|
||||
import { useManualConnect, useOnboardingStepWelcome } from '../../../../data';
|
||||
import { CommonHooks } from '../../../../data';
|
||||
import { openPopup } from '../../../../utils/window';
|
||||
|
||||
const AdvancedOptionsForm = ( { setCompleted } ) => {
|
||||
const { isBusy } = CommonHooks.useBusyState();
|
||||
const { isSandboxMode, setSandboxMode, connectViaSandbox } =
|
||||
CommonHooks.useSandbox();
|
||||
const {
|
||||
isManualConnectionBusy,
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
} = useOnboardingStepWelcome();
|
||||
connectViaIdAndSecret,
|
||||
} = CommonHooks.useManualConnection();
|
||||
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( noticesStore );
|
||||
const { connectManual } = useManualConnect();
|
||||
const refClientId = useRef( null );
|
||||
const refClientSecret = useRef( null );
|
||||
|
||||
|
@ -61,17 +62,9 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const handleServerError = ( res ) => {
|
||||
if ( res.message ) {
|
||||
createErrorNotice( res.message );
|
||||
} else {
|
||||
createErrorNotice(
|
||||
__(
|
||||
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
const handleServerError = ( res, genericMessage ) => {
|
||||
console.error( 'Connection error', res );
|
||||
createErrorNotice( res?.message ?? genericMessage );
|
||||
};
|
||||
|
||||
const handleServerSuccess = () => {
|
||||
|
@ -82,17 +75,50 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
|
|||
setCompleted( true );
|
||||
};
|
||||
|
||||
const handleConnect = async () => {
|
||||
const handleSandboxConnect = async () => {
|
||||
const res = await connectViaSandbox();
|
||||
|
||||
if ( ! res.success || ! res.data ) {
|
||||
handleServerError(
|
||||
res,
|
||||
__(
|
||||
'Could not generate a Sandbox login link.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionUrl = res.data;
|
||||
const popup = openPopup( connectionUrl );
|
||||
|
||||
if ( ! popup ) {
|
||||
createErrorNotice(
|
||||
__(
|
||||
'Popup blocked. Please allow popups for this site to connect to PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualConnect = async () => {
|
||||
if ( ! handleFormValidation() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await connectManual();
|
||||
const res = await connectViaIdAndSecret();
|
||||
|
||||
if ( res.success ) {
|
||||
handleServerSuccess();
|
||||
} else {
|
||||
handleServerError( res );
|
||||
handleServerError(
|
||||
res,
|
||||
__(
|
||||
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -118,21 +144,22 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
|
|||
) }
|
||||
isToggled={ !! isSandboxMode }
|
||||
setToggled={ setSandboxMode }
|
||||
isLoading={ isBusy }
|
||||
>
|
||||
<Button variant="secondary">
|
||||
<Button onClick={ handleSandboxConnect } variant="secondary">
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</SettingsToggleBlock>
|
||||
<Separator className="ppcp-r-page-welcome-mode-separator" />
|
||||
<Separator withLine={ false } />
|
||||
<SettingsToggleBlock
|
||||
label={ __(
|
||||
'Manually Connect',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
label={
|
||||
__( 'Manually Connect', 'woocommerce-paypal-payments' ) +
|
||||
( isBusy ? ' ...' : '' )
|
||||
}
|
||||
description={ advancedUsersDescription }
|
||||
isToggled={ !! isManualConnectionMode }
|
||||
setToggled={ setManualConnectionMode }
|
||||
isLoading={ isManualConnectionBusy }
|
||||
isLoading={ isBusy }
|
||||
>
|
||||
<DataStoreControl
|
||||
control={ TextControl }
|
||||
|
@ -169,7 +196,7 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
|
|||
onChange={ setClientSecret }
|
||||
type="password"
|
||||
/>
|
||||
<Button variant="secondary" onClick={ handleConnect }>
|
||||
<Button variant="secondary" onClick={ handleManualConnect }>
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</SettingsToggleBlock>
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useOnboardingStepBusiness,
|
||||
useOnboardingStepProducts,
|
||||
} from '../../data';
|
||||
import data from '../../utils/data';
|
||||
|
||||
import { OnboardingHooks } from '../../../../data';
|
||||
import data from '../../../../utils/data';
|
||||
|
||||
const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => {
|
||||
const isLastStep = () => currentStep + 1 === stepperOrder.length;
|
||||
|
@ -24,8 +22,8 @@ const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => {
|
|||
}
|
||||
};
|
||||
|
||||
const { products, toggleProduct } = useOnboardingStepProducts();
|
||||
const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness();
|
||||
const { products } = OnboardingHooks.useProducts();
|
||||
const { isCasualSeller } = OnboardingHooks.useBusiness();
|
||||
|
||||
let navigationTitle = '';
|
||||
let disabled = false;
|
|
@ -1,7 +1,7 @@
|
|||
import Container from '../../ReusableComponents/Container';
|
||||
import { useOnboardingStep } from '../../../data';
|
||||
import { OnboardingHooks } from '../../../data';
|
||||
import { getSteps } from './availableSteps';
|
||||
import Navigation from '../../ReusableComponents/Navigation';
|
||||
import Navigation from './Components/Navigation';
|
||||
|
||||
const getCurrentStep = ( requestedStep, steps ) => {
|
||||
const isValidStep = ( step ) =>
|
||||
|
@ -15,7 +15,7 @@ const getCurrentStep = ( requestedStep, steps ) => {
|
|||
};
|
||||
|
||||
const Onboarding = () => {
|
||||
const { step, setStep, setCompleted, flags } = useOnboardingStep();
|
||||
const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
|
||||
const steps = getSteps( flags );
|
||||
|
||||
const CurrentStepComponent = getCurrentStep( step, steps );
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
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 { useOnboardingStepBusiness } from '../../../data';
|
||||
import { BUSINESS_TYPES } from '../../../data/constants';
|
||||
import { OnboardingHooks, BUSINESS_TYPES } from '../../../data';
|
||||
|
||||
const BUSINESS_RADIO_GROUP_NAME = 'business';
|
||||
|
||||
const StepBusiness = ( {
|
||||
setStep,
|
||||
currentStep,
|
||||
stepperOrder,
|
||||
setCompleted,
|
||||
} ) => {
|
||||
const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness();
|
||||
const StepBusiness = ( {} ) => {
|
||||
const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness();
|
||||
|
||||
const handleSellerTypeChange = ( value ) => {
|
||||
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value );
|
||||
|
@ -40,23 +34,22 @@ const StepBusiness = ( {
|
|||
/>
|
||||
<div className="ppcp-r-inner-container">
|
||||
<SelectBoxWrapper>
|
||||
<SelectBox
|
||||
title={ __(
|
||||
'Business',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
name={ BUSINESS_RADIO_GROUP_NAME }
|
||||
value={ BUSINESS_TYPES.BUSINESS }
|
||||
changeCallback={ handleSellerTypeChange }
|
||||
currentValue={ getCurrentValue() }
|
||||
checked={ isCasualSeller === false }
|
||||
type="radio"
|
||||
>
|
||||
</SelectBox>
|
||||
<SelectBox
|
||||
title={ __(
|
||||
'Business',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
name={ BUSINESS_RADIO_GROUP_NAME }
|
||||
value={ BUSINESS_TYPES.BUSINESS }
|
||||
changeCallback={ handleSellerTypeChange }
|
||||
currentValue={ getCurrentValue() }
|
||||
checked={ isCasualSeller === false }
|
||||
type="radio"
|
||||
></SelectBox>
|
||||
<SelectBox
|
||||
title={ __(
|
||||
'Personal Account',
|
||||
|
@ -72,8 +65,7 @@ const StepBusiness = ( {
|
|||
currentValue={ getCurrentValue() }
|
||||
checked={ isCasualSeller === true }
|
||||
type="radio"
|
||||
>
|
||||
</SelectBox>
|
||||
></SelectBox>
|
||||
</SelectBoxWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,31 +1,23 @@
|
|||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Icon } from '@wordpress/components';
|
||||
|
||||
const StepCompleteSetup = ( {
|
||||
setStep,
|
||||
currentStep,
|
||||
stepperOrder,
|
||||
setCompleted,
|
||||
} ) => {
|
||||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
|
||||
const StepCompleteSetup = ( { setCompleted } ) => {
|
||||
const ButtonIcon = () => (
|
||||
<Icon
|
||||
icon={ () => (
|
||||
<svg
|
||||
width="24"
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.5001 4.5H12.5001V6H16.9394L10.9697 11.9697L12.0304 13.0303L18.0001 7.06066V11.5H19.5001V4.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 5.5C5.39543 5.5 4.5 6.39543 4.5 7.5V17.5C4.5 18.6046 5.39543 19.5 6.5 19.5H16.5C17.6046 19.5 18.5 18.6046 18.5 17.5V14.5H17V17.5C17 17.7761 16.7761 18 16.5 18H6.5C6.22386 18 6 17.7761 6 17.5V7.5C6 7.22386 6.22386 7 6.5 7H9.5V5.5H6.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
) }
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
|
||||
import SelectBox from '../../ReusableComponents/SelectBox';
|
||||
import { OnboardingHooks } from '../../../data';
|
||||
import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
|
||||
|
||||
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
|
||||
|
||||
const StepPaymentMethods = ( {} ) => {
|
||||
const {
|
||||
areOptionalPaymentMethodsEnabled,
|
||||
setAreOptionalPaymentMethodsEnabled,
|
||||
} = OnboardingHooks.useOptionalPaymentMethods();
|
||||
const pricesBasedDescription = sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-page-optional-payment-methods">
|
||||
<OnboardingHeader
|
||||
title={ __(
|
||||
'Add optional payment methods to your Checkout',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<div className="ppcp-r-inner-container">
|
||||
<SelectBoxWrapper>
|
||||
<SelectBox
|
||||
title={ __(
|
||||
'Available with additional application',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={
|
||||
<OptionalPaymentMethods
|
||||
useAcdc={ true }
|
||||
isFastlane={ true }
|
||||
isPayLater={ true }
|
||||
storeCountry={ 'us' }
|
||||
storeCurrency={ 'usd' }
|
||||
/>
|
||||
}
|
||||
name={ OPM_RADIO_GROUP_NAME }
|
||||
value={ true }
|
||||
changeCallback={ setAreOptionalPaymentMethodsEnabled }
|
||||
currentValue={ areOptionalPaymentMethodsEnabled }
|
||||
type="radio"
|
||||
></SelectBox>
|
||||
<SelectBox
|
||||
title={ __(
|
||||
'No thanks, I prefer to use a different provider for processing credit cards, digital wallets, and local payment methods',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
name={ OPM_RADIO_GROUP_NAME }
|
||||
value={ false }
|
||||
changeCallback={ setAreOptionalPaymentMethodsEnabled }
|
||||
currentValue={ areOptionalPaymentMethodsEnabled }
|
||||
type="radio"
|
||||
></SelectBox>
|
||||
</SelectBoxWrapper>
|
||||
<p
|
||||
className="ppcp-r-optional-payment-methods__description"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: pricesBasedDescription,
|
||||
} }
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepPaymentMethods;
|
|
@ -1,19 +1,14 @@
|
|||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import SelectBox from '../../ReusableComponents/SelectBox';
|
||||
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
|
||||
import { useOnboardingStepProducts } from '../../../data';
|
||||
import { PRODUCT_TYPES } from '../../../data/constants';
|
||||
import { OnboardingHooks, PRODUCT_TYPES } from '../../../data';
|
||||
|
||||
const PRODUCTS_CHECKBOX_GROUP_NAME = 'products';
|
||||
|
||||
const StepProducts = ( {
|
||||
setStep,
|
||||
currentStep,
|
||||
stepperOrder,
|
||||
setCompleted,
|
||||
} ) => {
|
||||
const { products, toggleProduct } = useOnboardingStepProducts();
|
||||
const StepProducts = () => {
|
||||
const { products, setProducts } = OnboardingHooks.useProducts();
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-page-products">
|
||||
|
@ -33,7 +28,7 @@ const StepProducts = ( {
|
|||
) }
|
||||
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
|
||||
value={ PRODUCT_TYPES.VIRTUAL }
|
||||
changeCallback={ toggleProduct }
|
||||
changeCallback={ setProducts }
|
||||
currentValue={ products }
|
||||
type="checkbox"
|
||||
>
|
||||
|
@ -75,7 +70,7 @@ const StepProducts = ( {
|
|||
) }
|
||||
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
|
||||
value={ PRODUCT_TYPES.PHYSICAL }
|
||||
changeCallback={ toggleProduct }
|
||||
changeCallback={ setProducts }
|
||||
currentValue={ products }
|
||||
type="checkbox"
|
||||
>
|
||||
|
@ -102,7 +97,7 @@ const StepProducts = ( {
|
|||
) }
|
||||
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
|
||||
value={ PRODUCT_TYPES.SUBSCRIPTIONS }
|
||||
changeCallback={ toggleProduct }
|
||||
changeCallback={ setProducts }
|
||||
currentValue={ products }
|
||||
type="checkbox"
|
||||
>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
|
||||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
|
||||
import Separator from '../../ReusableComponents/Separator';
|
||||
import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs';
|
||||
import AccordionSection from '../../ReusableComponents/AccordionSection';
|
||||
|
||||
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
|
||||
import AccordionSection from '../../ReusableComponents/AccordionSection';
|
||||
|
||||
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
||||
return (
|
||||
|
@ -57,7 +57,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
className="onboarding-advanced-options"
|
||||
initiallyOpen={ false }
|
||||
id="advanced-options"
|
||||
>
|
||||
<AdvancedOptionsForm setCompleted={ setCompleted } />
|
||||
</AccordionSection>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import StepWelcome from './StepWelcome';
|
||||
import StepBusiness from './StepBusiness';
|
||||
import StepProducts from './StepProducts';
|
||||
import StepPaymentMethods from './StepPaymentMethods';
|
||||
import StepCompleteSetup from './StepCompleteSetup';
|
||||
|
||||
export const getSteps = ( flags ) => {
|
||||
|
@ -8,6 +9,7 @@ export const getSteps = ( flags ) => {
|
|||
StepWelcome,
|
||||
StepBusiness,
|
||||
StepProducts,
|
||||
StepPaymentMethods,
|
||||
StepCompleteSetup,
|
||||
];
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useOnboardingStep } from '../../data';
|
||||
import { OnboardingHooks } from '../../data';
|
||||
import Onboarding from './Onboarding/Onboarding';
|
||||
import SettingsScreen from './SettingsScreen';
|
||||
|
||||
const Settings = () => {
|
||||
const onboardingProgress = useOnboardingStep();
|
||||
const onboardingProgress = OnboardingHooks.useSteps();
|
||||
|
||||
if ( ! onboardingProgress.isReady ) {
|
||||
// TODO: Use better loading state indicator.
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Action Types: Define unique identifiers for actions across all store modules.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
export default {
|
||||
// Transient data.
|
||||
SET_TRANSIENT: 'COMMON:SET_TRANSIENT',
|
||||
|
||||
// Persistent data.
|
||||
SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
|
||||
HYDRATE: 'COMMON:HYDRATE',
|
||||
|
||||
// Controls - always start with "DO_".
|
||||
DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
|
||||
DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION',
|
||||
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
|
||||
};
|
154
modules/ppcp-settings/resources/js/data/common/actions.js
Normal file
154
modules/ppcp-settings/resources/js/data/common/actions.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Action Creators: Define functions to create action objects.
|
||||
*
|
||||
* These functions update state or trigger side effects (e.g., async operations).
|
||||
* Actions are categorized as Transient, Persistent, or Side effect.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { select } from '@wordpress/data';
|
||||
|
||||
import ACTION_TYPES from './action-types';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Action An action object that is handled by a reducer or control.
|
||||
* @property {string} type - The action type.
|
||||
* @property {Object?} payload - Optional payload for the action.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Persistent. Set the full onboarding details, usually during app initialization.
|
||||
*
|
||||
* @param {{data: {}, flags?: {}}} payload
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const hydrate = ( payload ) => ( {
|
||||
type: ACTION_TYPES.HYDRATE,
|
||||
payload,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
|
||||
*
|
||||
* @param {boolean} isReady
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsReady = ( isReady ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { isReady },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Changes the "saving" flag.
|
||||
*
|
||||
* @param {boolean} isSaving
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsSaving = ( isSaving ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { isSaving },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Changes the "manual connection is busy" flag.
|
||||
*
|
||||
* @param {boolean} isBusy
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsBusy = ( isBusy ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { isBusy },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Sets the sandbox mode on or off.
|
||||
*
|
||||
* @param {boolean} useSandbox
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setSandboxMode = ( useSandbox ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { useSandbox },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Toggles the "Manual Connection" mode on or off.
|
||||
*
|
||||
* @param {boolean} useManualConnection
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setManualConnectionMode = ( useManualConnection ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { useManualConnection },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "client ID" value.
|
||||
*
|
||||
* @param {string} clientId
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setClientId = ( clientId ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { clientId },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "client secret" value.
|
||||
*
|
||||
* @param {string} clientSecret
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setClientSecret = ( clientSecret ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { clientSecret },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Side effect. Saves the persistent details to the WP database.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const persist = function* () {
|
||||
const data = yield select( STORE_NAME ).persistentData();
|
||||
|
||||
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Initiates the sandbox login ISU.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectViaSandbox = function* () {
|
||||
yield setIsBusy( true );
|
||||
|
||||
const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
|
||||
yield setIsBusy( false );
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Initiates a manual connection attempt using the provided client ID and secret.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectViaIdAndSecret = function* () {
|
||||
const { clientId, clientSecret, useSandbox } =
|
||||
yield select( STORE_NAME ).persistentData();
|
||||
|
||||
yield setIsBusy( true );
|
||||
|
||||
const result = yield {
|
||||
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
};
|
||||
yield setIsBusy( false );
|
||||
|
||||
return result;
|
||||
};
|
46
modules/ppcp-settings/resources/js/data/common/constants.js
Normal file
46
modules/ppcp-settings/resources/js/data/common/constants.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Name of the module-store in the main Redux store.
|
||||
*
|
||||
* Helps to isolate data, used by reducer and selectors.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const STORE_NAME = 'wc/paypal/common';
|
||||
|
||||
/**
|
||||
* REST path to hydrate data of this module by loading data from the WP DB..
|
||||
*
|
||||
* Used by resolvers.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common';
|
||||
|
||||
/**
|
||||
* REST path to persist data of this module to the WP DB.
|
||||
*
|
||||
* Used by controls.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
|
||||
|
||||
/**
|
||||
* REST path to perform the manual connection check, using client ID and secret,
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: ConnectManualRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
|
||||
|
||||
/**
|
||||
* REST path to generate an ISU URL for the sandbox-login.
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: LoginLinkRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link';
|
80
modules/ppcp-settings/resources/js/data/common/controls.js
vendored
Normal file
80
modules/ppcp-settings/resources/js/data/common/controls.js
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Controls: Implement side effects, typically asynchronous operations.
|
||||
*
|
||||
* Controls use ACTION_TYPES keys as identifiers.
|
||||
* They are triggered by corresponding actions and handle external interactions.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
import {
|
||||
REST_PERSIST_PATH,
|
||||
REST_MANUAL_CONNECTION_PATH,
|
||||
REST_SANDBOX_CONNECTION_PATH,
|
||||
} from './constants';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
export const controls = {
|
||||
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
|
||||
try {
|
||||
return await apiFetch( {
|
||||
path: REST_PERSIST_PATH,
|
||||
method: 'POST',
|
||||
data,
|
||||
} );
|
||||
} catch ( error ) {
|
||||
console.error( 'Error saving data.', error );
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await apiFetch( {
|
||||
path: REST_SANDBOX_CONNECTION_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
environment: 'sandbox',
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
result = {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
} ) {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await apiFetch( {
|
||||
path: REST_MANUAL_CONNECTION_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
result = {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
111
modules/ppcp-settings/resources/js/data/common/hooks.js
Normal file
111
modules/ppcp-settings/resources/js/data/common/hooks.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Hooks: Provide the main API for components to interact with the store.
|
||||
*
|
||||
* These encapsulate store interactions, offering a consistent interface.
|
||||
* Hooks simplify data access and manipulation for components.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
const useTransient = ( key ) =>
|
||||
useSelect(
|
||||
( select ) => select( STORE_NAME ).transientData()?.[ key ],
|
||||
[ key ]
|
||||
);
|
||||
|
||||
const usePersistent = ( key ) =>
|
||||
useSelect(
|
||||
( select ) => select( STORE_NAME ).persistentData()?.[ key ],
|
||||
[ key ]
|
||||
);
|
||||
|
||||
const useHooks = () => {
|
||||
const {
|
||||
persist,
|
||||
setSandboxMode,
|
||||
setManualConnectionMode,
|
||||
setClientId,
|
||||
setClientSecret,
|
||||
connectViaSandbox,
|
||||
connectViaIdAndSecret,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Transient accessors.
|
||||
const isReady = useTransient( 'isReady' );
|
||||
|
||||
// Persistent accessors.
|
||||
const clientId = usePersistent( 'clientId' );
|
||||
const clientSecret = usePersistent( 'clientSecret' );
|
||||
const isSandboxMode = usePersistent( 'useSandbox' );
|
||||
const isManualConnectionMode = usePersistent( 'useManualConnection' );
|
||||
|
||||
const savePersistent = async ( setter, value ) => {
|
||||
setter( value );
|
||||
await persist();
|
||||
};
|
||||
|
||||
return {
|
||||
isReady,
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
return savePersistent( setSandboxMode, state );
|
||||
},
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode: ( state ) => {
|
||||
return savePersistent( setManualConnectionMode, state );
|
||||
},
|
||||
clientId,
|
||||
setClientId: ( value ) => {
|
||||
return savePersistent( setClientId, value );
|
||||
},
|
||||
clientSecret,
|
||||
setClientSecret: ( value ) => {
|
||||
return savePersistent( setClientSecret, value );
|
||||
},
|
||||
connectViaSandbox,
|
||||
connectViaIdAndSecret,
|
||||
};
|
||||
};
|
||||
|
||||
export const useBusyState = () => {
|
||||
const { setIsBusy } = useDispatch( STORE_NAME );
|
||||
const isBusy = useTransient( 'isBusy' );
|
||||
|
||||
return {
|
||||
isBusy,
|
||||
setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ),
|
||||
};
|
||||
};
|
||||
|
||||
export const useSandbox = () => {
|
||||
const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks();
|
||||
|
||||
return { isSandboxMode, setSandboxMode, connectViaSandbox };
|
||||
};
|
||||
|
||||
export const useManualConnection = () => {
|
||||
const {
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
connectViaIdAndSecret,
|
||||
} = useHooks();
|
||||
|
||||
return {
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
connectViaIdAndSecret,
|
||||
};
|
||||
};
|
24
modules/ppcp-settings/resources/js/data/common/index.js
Normal file
24
modules/ppcp-settings/resources/js/data/common/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createReduxStore, register } from '@wordpress/data';
|
||||
import { controls as wpControls } from '@wordpress/data-controls';
|
||||
|
||||
import { STORE_NAME } from './constants';
|
||||
import reducer from './reducer';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
import * as hooks from './hooks';
|
||||
import { resolvers } from './resolvers';
|
||||
import { controls } from './controls';
|
||||
|
||||
export const initStore = () => {
|
||||
const store = createReduxStore( STORE_NAME, {
|
||||
reducer,
|
||||
controls: { ...wpControls, ...controls },
|
||||
actions,
|
||||
selectors,
|
||||
resolvers,
|
||||
} );
|
||||
|
||||
register( store );
|
||||
};
|
||||
|
||||
export { hooks, selectors, STORE_NAME };
|
45
modules/ppcp-settings/resources/js/data/common/reducer.js
Normal file
45
modules/ppcp-settings/resources/js/data/common/reducer.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Reducer: Defines store structure and state updates for this module.
|
||||
*
|
||||
* Manages both transient (temporary) and persistent (saved) state.
|
||||
* The initial state must define all properties, as dynamic additions are not supported.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { createReducer, createSetters } from '../utils';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
// Store structure.
|
||||
|
||||
const defaultTransient = {
|
||||
isReady: false,
|
||||
isBusy: false,
|
||||
};
|
||||
|
||||
const defaultPersistent = {
|
||||
useSandbox: false,
|
||||
useManualConnection: false,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
};
|
||||
|
||||
// Reducer logic.
|
||||
|
||||
const [ setTransient, setPersistent ] = createSetters(
|
||||
defaultTransient,
|
||||
defaultPersistent
|
||||
);
|
||||
|
||||
const commonReducer = createReducer( defaultTransient, defaultPersistent, {
|
||||
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, action ) =>
|
||||
setTransient( state, action ),
|
||||
|
||||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) =>
|
||||
setPersistent( state, action ),
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
|
||||
setPersistent( state, payload.data ),
|
||||
} );
|
||||
|
||||
export default commonReducer;
|
36
modules/ppcp-settings/resources/js/data/common/resolvers.js
Normal file
36
modules/ppcp-settings/resources/js/data/common/resolvers.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Resolvers: Handle asynchronous data fetching for the store.
|
||||
*
|
||||
* These functions update store state with data from external sources.
|
||||
* Each resolver corresponds to a specific selector (selector with same name must exist).
|
||||
* Resolvers are called automatically when selectors request unavailable data.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
|
||||
|
||||
export const resolvers = {
|
||||
/**
|
||||
* Retrieve settings from the site's REST API.
|
||||
*/
|
||||
*persistentData() {
|
||||
try {
|
||||
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
||||
|
||||
yield dispatch( STORE_NAME ).hydrate( result );
|
||||
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||
} catch ( e ) {
|
||||
yield dispatch( 'core/notices' ).createErrorNotice(
|
||||
__(
|
||||
'Error retrieving plugin details.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
21
modules/ppcp-settings/resources/js/data/common/selectors.js
Normal file
21
modules/ppcp-settings/resources/js/data/common/selectors.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Selectors: Extract specific pieces of state from the store.
|
||||
*
|
||||
* These functions provide a consistent interface for accessing store data.
|
||||
* They allow components to retrieve data without knowing the store structure.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
const EMPTY_OBJ = Object.freeze( {} );
|
||||
|
||||
const getState = ( state ) => state || EMPTY_OBJ;
|
||||
|
||||
export const persistentData = ( state ) => {
|
||||
return getState( state ).data || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const transientData = ( state ) => {
|
||||
const { data, ...transientState } = getState( state );
|
||||
return transientState || EMPTY_OBJ;
|
||||
};
|
|
@ -1,6 +1,3 @@
|
|||
export const NAMESPACE = '/wc/v3/wc_paypal';
|
||||
export const STORE_NAME = 'wc/paypal';
|
||||
|
||||
export const BUSINESS_TYPES = {
|
||||
CASUAL_SELLER: 'casual_seller',
|
||||
BUSINESS: 'business',
|
||||
|
|
47
modules/ppcp-settings/resources/js/data/debug.js
Normal file
47
modules/ppcp-settings/resources/js/data/debug.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { OnboardingStoreName } from './index';
|
||||
|
||||
export const addDebugTools = ( context, modules ) => {
|
||||
if ( ! context || ! context?.debug ) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.dumpStore = async () => {
|
||||
/* eslint-disable no-console */
|
||||
if ( ! console?.groupCollapsed ) {
|
||||
console.error( 'console.groupCollapsed is not supported.' );
|
||||
return;
|
||||
}
|
||||
|
||||
modules.forEach( ( module ) => {
|
||||
const storeName = module.STORE_NAME;
|
||||
const storeSelector = `wp.data.select( '${ storeName }' )`;
|
||||
console.group( `[STORE] ${ storeSelector }` );
|
||||
|
||||
const dumpStore = ( selector ) => {
|
||||
const contents = wp.data.select( storeName )[ selector ]();
|
||||
|
||||
console.groupCollapsed( `.${ selector }()` );
|
||||
console.table( contents );
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
Object.keys( module.selectors ).forEach( dumpStore );
|
||||
|
||||
console.groupEnd();
|
||||
} );
|
||||
/* eslint-enable no-console */
|
||||
};
|
||||
|
||||
context.resetStore = () => {
|
||||
const onboarding = wp.data.dispatch( OnboardingStoreName );
|
||||
onboarding.reset();
|
||||
onboarding.persist();
|
||||
};
|
||||
|
||||
context.startOnboarding = () => {
|
||||
const onboarding = wp.data.dispatch( OnboardingStoreName );
|
||||
onboarding.setCompleted( false );
|
||||
onboarding.setStep( 0 );
|
||||
onboarding.persist();
|
||||
};
|
||||
};
|
|
@ -1,7 +1,16 @@
|
|||
import { STORE_NAME } from './constants';
|
||||
import { initStore } from './store';
|
||||
import { addDebugTools } from './debug';
|
||||
import * as Onboarding from './onboarding';
|
||||
import * as Common from './common';
|
||||
|
||||
initStore();
|
||||
Onboarding.initStore();
|
||||
Common.initStore();
|
||||
|
||||
export const WC_PAYPAL_STORE_NAME = STORE_NAME;
|
||||
export * from './onboarding/hooks';
|
||||
export const OnboardingHooks = Onboarding.hooks;
|
||||
export const CommonHooks = Common.hooks;
|
||||
|
||||
export const OnboardingStoreName = Onboarding.STORE_NAME;
|
||||
export const CommonStoreName = Common.STORE_NAME;
|
||||
|
||||
export * from './constants';
|
||||
|
||||
addDebugTools( window.ppcpSettings, [ Onboarding, Common ] );
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
export default {
|
||||
RESET_ONBOARDING: 'RESET_ONBOARDING',
|
||||
/**
|
||||
* Action Types: Define unique identifiers for actions across all store modules.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
export default {
|
||||
// Transient data.
|
||||
SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY',
|
||||
SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING',
|
||||
SET_MANUAL_CONNECTION_BUSY: 'SET_MANUAL_CONNECTION_BUSY',
|
||||
SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT',
|
||||
|
||||
// Persistent data.
|
||||
SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED',
|
||||
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',
|
||||
SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER',
|
||||
SET_PRODUCTS: 'SET_PRODUCTS',
|
||||
SET_PERSISTENT: 'ONBOARDING:SET_PERSISTENT',
|
||||
RESET: 'ONBOARDING:RESET',
|
||||
HYDRATE: 'ONBOARDING:HYDRATE',
|
||||
|
||||
// Controls - always start with "DO_".
|
||||
DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA',
|
||||
};
|
||||
|
|
|
@ -1,235 +1,116 @@
|
|||
/**
|
||||
* Action Creators: Define functions to create action objects.
|
||||
*
|
||||
* These functions update state or trigger side effects (e.g., async operations).
|
||||
* Actions are categorized as Transient, Persistent, or Side effect.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { select } from '@wordpress/data';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
import ACTION_TYPES from './action-types';
|
||||
import { NAMESPACE, STORE_NAME } from '../constants';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Action An action object that is handled by a reducer or control.
|
||||
* @property {string} type - The action type.
|
||||
* @property {Object?} payload - Optional payload for the action.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Special. Resets all values in the onboarding store to initial defaults.
|
||||
*
|
||||
* @return {{type: string}} The action.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const resetOnboarding = () => {
|
||||
return { type: ACTION_TYPES.RESET_ONBOARDING };
|
||||
};
|
||||
|
||||
/**
|
||||
* Non-persistent. Marks the onboarding details as "ready", i.e., fully initialized.
|
||||
*
|
||||
* @param {boolean} isReady
|
||||
* @return {{type: string, isReady}} The action.
|
||||
*/
|
||||
export const setIsReady = ( isReady ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_ONBOARDING_IS_READY,
|
||||
isReady,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
isSaving,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Non-persistent. Changes the "manual connection is busy" flag.
|
||||
*
|
||||
* @param {boolean} isBusy
|
||||
* @return {{type: string, isBusy}} The action.
|
||||
*/
|
||||
export const setManualConnectionIsBusy = ( isBusy ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY,
|
||||
isBusy,
|
||||
};
|
||||
};
|
||||
export const reset = () => ( { type: ACTION_TYPES.RESET } );
|
||||
|
||||
/**
|
||||
* Persistent. Set the full onboarding details, usually during app initialization.
|
||||
*
|
||||
* @param {{data: {}, flags?: {}}} payload
|
||||
* @return {{type: string, payload}} The action.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setOnboardingDetails = ( payload ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_ONBOARDING_DETAILS,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
export const hydrate = ( payload ) => ( {
|
||||
type: ACTION_TYPES.HYDRATE,
|
||||
payload,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
|
||||
*
|
||||
* @param {boolean} isReady
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsReady = ( isReady ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { isReady },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
|
||||
*
|
||||
* @param {boolean} completed
|
||||
* @return {{type: string, payload}} The action.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setCompleted = ( completed ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_ONBOARDING_COMPLETED,
|
||||
completed,
|
||||
};
|
||||
};
|
||||
export const setCompleted = ( completed ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { completed },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Sets the onboarding wizard to a new step.
|
||||
*
|
||||
* @param {number} step
|
||||
* @return {{type: string, step}} An action.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setOnboardingStep = ( step ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_ONBOARDING_STEP,
|
||||
step,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Sets the sandbox mode on or off.
|
||||
*
|
||||
* @param {boolean} sandboxMode
|
||||
* @return {{type: string, useSandbox}} An action.
|
||||
*/
|
||||
export const setSandboxMode = ( sandboxMode ) => {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
};
|
||||
export const setStep = ( step ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { step },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Sets the "isCasualSeller" value.
|
||||
*
|
||||
* @param {boolean} isCasualSeller
|
||||
* @return {{type: string, isCasualSeller}} The action.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsCasualSeller = ( isCasualSeller ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_IS_CASUAL_SELLER,
|
||||
isCasualSeller,
|
||||
};
|
||||
};
|
||||
export const setIsCasualSeller = ( isCasualSeller ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { isCasualSeller },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Sets the "areOptionalPaymentMethodsEnabled" value.
|
||||
*
|
||||
* @param {boolean} areOptionalPaymentMethodsEnabled
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setAreOptionalPaymentMethodsEnabled = (
|
||||
areOptionalPaymentMethodsEnabled
|
||||
) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { areOptionalPaymentMethodsEnabled },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Sets the "products" array.
|
||||
*
|
||||
* @param {string[]} products
|
||||
* @return {{type: string, products}} The action.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setProducts = ( products ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_PRODUCTS,
|
||||
products,
|
||||
};
|
||||
export const setProducts = ( products ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { products },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Side effect. Triggers the persistence of onboarding data to the server.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const persist = function* () {
|
||||
const data = yield select( STORE_NAME ).persistentData();
|
||||
|
||||
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to establish a connection using client ID and secret via the server-side
|
||||
* connection endpoint.
|
||||
*
|
||||
* @return {Object} The server response object
|
||||
*/
|
||||
export function* connectViaIdAndSecret() {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
const path = `${ NAMESPACE }/connect_manual`;
|
||||
const { clientId, clientSecret, useSandbox } =
|
||||
yield select( STORE_NAME ).getPersistentData();
|
||||
|
||||
yield setManualConnectionIsBusy( true );
|
||||
|
||||
result = yield apiFetch( {
|
||||
path,
|
||||
method: 'POST',
|
||||
data: {
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
result = {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
} finally {
|
||||
yield setManualConnectionIsBusy( false );
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the persistent details to the WP database.
|
||||
*
|
||||
* @return {boolean} True, if the values were successfully saved.
|
||||
*/
|
||||
export function* persist() {
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const path = `${ NAMESPACE }/onboarding`;
|
||||
const data = select( STORE_NAME ).getPersistentData();
|
||||
|
||||
yield setIsSaving( true );
|
||||
|
||||
yield apiFetch( {
|
||||
path,
|
||||
method: 'post',
|
||||
data,
|
||||
} );
|
||||
} catch ( e ) {
|
||||
error = e;
|
||||
console.error( 'Error saving progress.', e );
|
||||
} finally {
|
||||
yield setIsSaving( false );
|
||||
}
|
||||
|
||||
return error === null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Name of the Redux store module.
|
||||
*
|
||||
* Used by: Reducer, Selector, Index
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const STORE_NAME = 'wc/paypal/onboarding';
|
||||
|
||||
/**
|
||||
* REST path to hydrate data of this module by loading data from the WP DB..
|
||||
*
|
||||
* Used by: Resolvers
|
||||
* See: OnboardingRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/onboarding';
|
||||
|
||||
/**
|
||||
* REST path to persist data of this module to the WP DB.
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: OnboardingRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/onboarding';
|
27
modules/ppcp-settings/resources/js/data/onboarding/controls.js
vendored
Normal file
27
modules/ppcp-settings/resources/js/data/onboarding/controls.js
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Controls: Implement side effects, typically asynchronous operations.
|
||||
*
|
||||
* Controls use ACTION_TYPES keys as identifiers.
|
||||
* They are triggered by corresponding actions and handle external interactions.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
import { REST_PERSIST_PATH } from './constants';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
export const controls = {
|
||||
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
|
||||
try {
|
||||
await apiFetch( {
|
||||
path: REST_PERSIST_PATH,
|
||||
method: 'POST',
|
||||
data,
|
||||
} );
|
||||
} catch ( e ) {
|
||||
console.error( 'Error saving progress.', e );
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,163 +1,115 @@
|
|||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { NAMESPACE, PRODUCT_TYPES, STORE_NAME } from '../constants';
|
||||
import { getFlags } from './selectors';
|
||||
/**
|
||||
* Hooks: Provide the main API for components to interact with the store.
|
||||
*
|
||||
* These encapsulate store interactions, offering a consistent interface.
|
||||
* Hooks simplify data access and manipulation for components.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
const useOnboardingDetails = () => {
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
|
||||
import { PRODUCT_TYPES } from '../constants';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
const useTransient = ( key ) =>
|
||||
useSelect(
|
||||
( select ) => select( STORE_NAME ).transientData()?.[ key ],
|
||||
[ key ]
|
||||
);
|
||||
|
||||
const usePersistent = ( key ) =>
|
||||
useSelect(
|
||||
( select ) => select( STORE_NAME ).persistentData()?.[ key ],
|
||||
[ key ]
|
||||
);
|
||||
|
||||
const useHooks = () => {
|
||||
const {
|
||||
persist,
|
||||
setOnboardingStep,
|
||||
setStep,
|
||||
setCompleted,
|
||||
setSandboxMode,
|
||||
setManualConnectionMode,
|
||||
setClientId,
|
||||
setClientSecret,
|
||||
setIsCasualSeller,
|
||||
setAreOptionalPaymentMethodsEnabled,
|
||||
setProducts,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Transient accessors.
|
||||
const isSaving = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getTransientData().isSaving;
|
||||
}, [] );
|
||||
|
||||
const isReady = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getTransientData().isReady;
|
||||
} );
|
||||
|
||||
const isManualConnectionBusy = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getTransientData().isManualConnectionBusy;
|
||||
}, [] );
|
||||
|
||||
// Read-only flags.
|
||||
const flags = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getFlags();
|
||||
} );
|
||||
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
|
||||
|
||||
// Transient accessors.
|
||||
const isReady = useTransient( 'isReady' );
|
||||
|
||||
// Persistent accessors.
|
||||
const step = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().step || 0;
|
||||
} );
|
||||
const step = usePersistent( 'step' );
|
||||
const completed = usePersistent( 'completed' );
|
||||
const isCasualSeller = usePersistent( 'isCasualSeller' );
|
||||
const areOptionalPaymentMethodsEnabled = usePersistent(
|
||||
'areOptionalPaymentMethodsEnabled'
|
||||
);
|
||||
const products = usePersistent( 'products' );
|
||||
|
||||
const completed = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().completed;
|
||||
} );
|
||||
|
||||
const clientId = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().clientId;
|
||||
}, [] );
|
||||
|
||||
const clientSecret = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().clientSecret;
|
||||
}, [] );
|
||||
|
||||
const isSandboxMode = useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).getPersistentData().useSandbox;
|
||||
}, [] );
|
||||
|
||||
const isManualConnectionMode = useSelect( ( select ) => {
|
||||
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 ) => {
|
||||
const savePersistent = async ( setter, value ) => {
|
||||
setter( value );
|
||||
await persist();
|
||||
};
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
isReady,
|
||||
isManualConnectionBusy,
|
||||
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,
|
||||
isReady,
|
||||
step,
|
||||
setStep: ( value ) => {
|
||||
return savePersistent( setStep, value );
|
||||
},
|
||||
completed,
|
||||
setCompleted: ( state ) => {
|
||||
return savePersistent( setCompleted, state );
|
||||
},
|
||||
isCasualSeller,
|
||||
setIsCasualSeller: ( value ) => {
|
||||
return savePersistent( setIsCasualSeller, value );
|
||||
},
|
||||
areOptionalPaymentMethodsEnabled,
|
||||
setAreOptionalPaymentMethodsEnabled: ( value ) => {
|
||||
return savePersistent( setAreOptionalPaymentMethodsEnabled, value );
|
||||
},
|
||||
products,
|
||||
setProducts: ( activeProducts ) => {
|
||||
const validProducts = activeProducts.filter( ( item ) =>
|
||||
Object.values( PRODUCT_TYPES ).includes( item )
|
||||
);
|
||||
return savePersistent( setProducts, validProducts );
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useOnboardingStepWelcome = () => {
|
||||
const {
|
||||
isSaving,
|
||||
isManualConnectionBusy,
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
} = useOnboardingDetails();
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
isManualConnectionBusy,
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
};
|
||||
};
|
||||
|
||||
export const useOnboardingStepBusiness = () => {
|
||||
const { isCasualSeller, setIsCasualSeller } = useOnboardingDetails();
|
||||
export const useBusiness = () => {
|
||||
const { isCasualSeller, setIsCasualSeller } = useHooks();
|
||||
|
||||
return { isCasualSeller, setIsCasualSeller };
|
||||
};
|
||||
|
||||
export const useOnboardingStepProducts = () => {
|
||||
const { products, toggleProduct } = useOnboardingDetails();
|
||||
export const useProducts = () => {
|
||||
const { products, setProducts } = useHooks();
|
||||
|
||||
return { products, toggleProduct };
|
||||
return { products, setProducts };
|
||||
};
|
||||
|
||||
export const useOnboardingStep = () => {
|
||||
const { isReady, step, setStep, completed, setCompleted, flags } =
|
||||
useOnboardingDetails();
|
||||
|
||||
return { isReady, step, setStep, completed, setCompleted, flags };
|
||||
};
|
||||
|
||||
export const useManualConnect = () => {
|
||||
const { connectViaIdAndSecret } = useDispatch( STORE_NAME );
|
||||
export const useOptionalPaymentMethods = () => {
|
||||
const {
|
||||
areOptionalPaymentMethodsEnabled,
|
||||
setAreOptionalPaymentMethodsEnabled,
|
||||
} = useHooks();
|
||||
|
||||
return {
|
||||
connectManual: connectViaIdAndSecret,
|
||||
areOptionalPaymentMethodsEnabled,
|
||||
setAreOptionalPaymentMethodsEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSteps = () => {
|
||||
const { flags, isReady, step, setStep, completed, setCompleted } =
|
||||
useHooks();
|
||||
|
||||
return { flags, isReady, step, setStep, completed, setCompleted };
|
||||
};
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
import { createReduxStore, register } from '@wordpress/data';
|
||||
import { controls as wpControls } from '@wordpress/data-controls';
|
||||
|
||||
import { STORE_NAME } from './constants';
|
||||
import reducer from './reducer';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
import * as resolvers from './resolvers';
|
||||
import * as hooks from './hooks';
|
||||
import { resolvers } from './resolvers';
|
||||
import { controls } from './controls';
|
||||
|
||||
export { reducer, selectors, actions, resolvers };
|
||||
export const initStore = () => {
|
||||
const store = createReduxStore( STORE_NAME, {
|
||||
reducer,
|
||||
controls: { ...wpControls, ...controls },
|
||||
actions,
|
||||
selectors,
|
||||
resolvers,
|
||||
} );
|
||||
|
||||
register( store );
|
||||
};
|
||||
|
||||
export { hooks, selectors, STORE_NAME };
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
/**
|
||||
* Reducer: Defines store structure and state updates for this module.
|
||||
*
|
||||
* Manages both transient (temporary) and persistent (saved) state.
|
||||
* The initial state must define all properties, as dynamic additions are not supported.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { createReducer, createSetters } from '../utils';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
const defaultState = {
|
||||
isReady: false,
|
||||
isSaving: false,
|
||||
isManualConnectionBusy: false,
|
||||
// Store structure.
|
||||
|
||||
// Data persisted to the server.
|
||||
data: {
|
||||
completed: false,
|
||||
step: 0,
|
||||
useSandbox: false,
|
||||
useManualConnection: false,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
isCasualSeller: null, // null value will uncheck both options in the UI.
|
||||
products: [],
|
||||
},
|
||||
const defaultTransient = {
|
||||
isReady: false,
|
||||
|
||||
// Read only values, provided by the server.
|
||||
flags: {
|
||||
|
@ -25,83 +23,41 @@ const defaultState = {
|
|||
},
|
||||
};
|
||||
|
||||
export const onboardingReducer = (
|
||||
state = defaultState,
|
||||
{ type, ...action }
|
||||
) => {
|
||||
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 ) {
|
||||
// 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 } );
|
||||
|
||||
case ACTION_TYPES.SET_IS_SAVING_ONBOARDING:
|
||||
return setTransient( { isSaving: action.isSaving } );
|
||||
|
||||
case ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY:
|
||||
return setTransient( { isManualConnectionBusy: action.isBusy } );
|
||||
|
||||
// Persistent data.
|
||||
case ACTION_TYPES.SET_ONBOARDING_DETAILS:
|
||||
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 } );
|
||||
|
||||
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_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,
|
||||
} );
|
||||
|
||||
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;
|
||||
}
|
||||
const defaultPersistent = {
|
||||
completed: false,
|
||||
step: 0,
|
||||
isCasualSeller: null, // null value will uncheck both options in the UI.
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [],
|
||||
};
|
||||
|
||||
// Reducer logic.
|
||||
|
||||
const [ setTransient, setPersistent ] = createSetters(
|
||||
defaultTransient,
|
||||
defaultPersistent
|
||||
);
|
||||
|
||||
const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
|
||||
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
|
||||
setTransient( state, payload ),
|
||||
|
||||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
|
||||
setPersistent( state, payload ),
|
||||
|
||||
[ ACTION_TYPES.RESET ]: ( state ) =>
|
||||
setPersistent( state, defaultPersistent ),
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||
const newState = setPersistent( state, payload.data );
|
||||
|
||||
// Flags are not updated by `setPersistent()`.
|
||||
if ( payload.flags ) {
|
||||
newState.flags = { ...newState.flags, ...payload.flags };
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
} );
|
||||
|
||||
export default onboardingReducer;
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
/**
|
||||
* Resolvers: Handle asynchronous data fetching for the store.
|
||||
*
|
||||
* These functions update store state with data from external sources.
|
||||
* Each resolver corresponds to a specific selector (selector with same name must exist).
|
||||
* Resolvers are called automatically when selectors request unavailable data.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
import { NAMESPACE } from '../constants';
|
||||
import { setIsReady, setOnboardingDetails } from './actions';
|
||||
|
||||
/**
|
||||
* Retrieve settings from the site's REST API.
|
||||
*/
|
||||
export function* getPersistentData() {
|
||||
const path = `${ NAMESPACE }/onboarding`;
|
||||
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
|
||||
|
||||
try {
|
||||
const result = yield apiFetch( { path } );
|
||||
yield setOnboardingDetails( result );
|
||||
yield setIsReady( true );
|
||||
} catch ( e ) {
|
||||
yield dispatch( 'core/notices' ).createErrorNotice(
|
||||
__(
|
||||
'Error retrieving onboarding details.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
export const resolvers = {
|
||||
/**
|
||||
* Retrieve settings from the site's REST API.
|
||||
*/
|
||||
*persistentData() {
|
||||
try {
|
||||
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
||||
|
||||
yield dispatch( STORE_NAME ).hydrate( result );
|
||||
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||
} catch ( e ) {
|
||||
yield dispatch( 'core/notices' ).createErrorNotice(
|
||||
__(
|
||||
'Error retrieving onboarding details.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
/**
|
||||
* Selectors: Extract specific pieces of state from the store.
|
||||
*
|
||||
* These functions provide a consistent interface for accessing store data.
|
||||
* They allow components to retrieve data without knowing the store structure.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
const EMPTY_OBJ = Object.freeze( {} );
|
||||
|
||||
const getOnboardingState = ( state ) => {
|
||||
if ( ! state ) {
|
||||
return EMPTY_OBJ;
|
||||
}
|
||||
const getState = ( state ) => state || EMPTY_OBJ;
|
||||
|
||||
return state.onboarding || EMPTY_OBJ;
|
||||
export const persistentData = ( state ) => {
|
||||
return getState( state ).data || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const getPersistentData = ( state ) => {
|
||||
return getOnboardingState( state ).data || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const getTransientData = ( state ) => {
|
||||
const { data, flags, ...transientState } = getOnboardingState( state );
|
||||
export const transientData = ( state ) => {
|
||||
const { data, flags, ...transientState } = getState( state );
|
||||
return transientState || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const getFlags = ( state ) => {
|
||||
return getOnboardingState( state ).flags || EMPTY_OBJ;
|
||||
export const flags = ( state ) => {
|
||||
return getState( state ).flags || EMPTY_OBJ;
|
||||
};
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import { createReduxStore, register, combineReducers } from '@wordpress/data';
|
||||
import { controls } from '@wordpress/data-controls';
|
||||
import { STORE_NAME } from './constants';
|
||||
import * as onboarding from './onboarding';
|
||||
|
||||
const actions = {};
|
||||
const selectors = {};
|
||||
const resolvers = {};
|
||||
|
||||
[ onboarding ].forEach( ( item ) => {
|
||||
Object.assign( actions, { ...item.actions } );
|
||||
Object.assign( selectors, { ...item.selectors } );
|
||||
Object.assign( resolvers, { ...item.resolvers } );
|
||||
} );
|
||||
|
||||
const reducer = combineReducers( {
|
||||
onboarding: onboarding.reducer,
|
||||
} );
|
||||
|
||||
export const initStore = () => {
|
||||
const store = createReduxStore( STORE_NAME, {
|
||||
reducer,
|
||||
controls,
|
||||
actions,
|
||||
selectors,
|
||||
resolvers,
|
||||
} );
|
||||
|
||||
register( store );
|
||||
|
||||
/* eslint-disable no-console */
|
||||
// Provide a debug tool to inspect the Redux store via the JS console.
|
||||
if ( window.ppcpSettings?.debug && console?.groupCollapsed ) {
|
||||
window.ppcpSettings.dumpStore = () => {
|
||||
const storeSelector = `wp.data.select('${ STORE_NAME }')`;
|
||||
console.group( `[STORE] ${ storeSelector }` );
|
||||
|
||||
const storeState = wp.data.select( STORE_NAME );
|
||||
Object.keys( selectors ).forEach( ( selector ) => {
|
||||
console.groupCollapsed( `[SELECTOR] .${ selector }()` );
|
||||
console.table( storeState[ selector ]() );
|
||||
console.groupEnd();
|
||||
} );
|
||||
|
||||
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 */
|
||||
};
|
75
modules/ppcp-settings/resources/js/data/utils.js
Normal file
75
modules/ppcp-settings/resources/js/data/utils.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Updates an object with new values, filtering based on allowed keys.
|
||||
*
|
||||
* Helper method used by createSetters.
|
||||
*
|
||||
* @param {Object} oldObject The original object to update.
|
||||
* @param {Object} newValues The new values to apply.
|
||||
* @param {Object} allowedKeys An object whose keys define the allowed keys to update.
|
||||
* @return {Object} A new object with the allowed updates applied.
|
||||
*/
|
||||
const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( {
|
||||
...oldObject,
|
||||
...Object.keys( newValues ).reduce( ( acc, key ) => {
|
||||
if ( key in allowedKeys ) {
|
||||
acc[ key ] = newValues[ key ];
|
||||
}
|
||||
return acc;
|
||||
}, {} ),
|
||||
} );
|
||||
|
||||
/**
|
||||
* Creates setter functions for updating state.
|
||||
*
|
||||
* Only properties that are present in the "defaultTransient" or "defaultPersistent"
|
||||
* arguments can be updated by the setters. Make sure that the default state defines
|
||||
* ALL possible properties.
|
||||
*
|
||||
* @param {Object} defaultTransient Object defining initial transient values.
|
||||
* @param {Object} defaultPersistent Object defining initial persistent values.
|
||||
* @return {[Function, Function]} An array containing setTransient and setPersistent functions.
|
||||
*/
|
||||
export const createSetters = ( defaultTransient, defaultPersistent ) => {
|
||||
const setTransient = ( oldState, newValues = {} ) =>
|
||||
updateObject( oldState, newValues, defaultTransient );
|
||||
|
||||
const setPersistent = ( oldState, newValues = {} ) => ( {
|
||||
...oldState,
|
||||
data: updateObject( oldState.data, newValues, defaultPersistent ),
|
||||
} );
|
||||
|
||||
return [ setTransient, setPersistent ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a reducer function with predefined action handlers.
|
||||
*
|
||||
* @param {Object} defaultTransient Object defining initial transient values.
|
||||
* @param {Object} defaultPersistent Object defining initial persistent values.
|
||||
* @param {Object} handlers An object mapping action types to handler functions.
|
||||
* @return {Function} A reducer function.
|
||||
*/
|
||||
export const createReducer = (
|
||||
defaultTransient,
|
||||
defaultPersistent,
|
||||
handlers
|
||||
) => {
|
||||
if ( Object.hasOwnProperty.call( defaultTransient, 'data' ) ) {
|
||||
throw new Error(
|
||||
'The transient state cannot contain a "data" property.'
|
||||
);
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
...defaultTransient,
|
||||
data: defaultPersistent,
|
||||
};
|
||||
|
||||
return function reducer( state = initialState, action ) {
|
||||
if ( Object.hasOwnProperty.call( handlers, action.type ) ) {
|
||||
return handlers[ action.type ]( state, action.payload ?? {} );
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
};
|
42
modules/ppcp-settings/resources/js/utils/window.js
Normal file
42
modules/ppcp-settings/resources/js/utils/window.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Opens the provided URL, preferably in a popup window.
|
||||
*
|
||||
* Popups are usually only supported on desktop devices, when the browser is not in fullscreen mode.
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {Object} options
|
||||
* @param {string} options.name
|
||||
* @param {number} options.width
|
||||
* @param {number} options.height
|
||||
* @param {boolean} options.resizeable
|
||||
* @return {null|Window} Popup window instance, or null.
|
||||
*/
|
||||
export const openPopup = (
|
||||
url,
|
||||
{ name = '_blank', width = 450, height = 720, resizeable = false } = {}
|
||||
) => {
|
||||
width = Math.max( 100, Math.min( window.screen.width - 40, width ) );
|
||||
height = Math.max( 100, Math.min( window.screen.height - 40, height ) );
|
||||
|
||||
const left = ( window.screen.width - width ) / 2;
|
||||
const top = ( window.screen.height - height ) / 2;
|
||||
|
||||
const features = [
|
||||
`width=${ width }`,
|
||||
`height=${ height }`,
|
||||
`left=${ left }`,
|
||||
`top=${ top }`,
|
||||
`resizable=${ resizeable ? 'yes' : 'no' }`,
|
||||
`scrollbars=yes`,
|
||||
`status=no`,
|
||||
];
|
||||
|
||||
const popup = window.open( url, name, features.join( ',' ) );
|
||||
|
||||
if ( popup && ! popup.closed ) {
|
||||
popup.focus();
|
||||
return popup;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue