Merge remote-tracking branch 'origin/temp/settings-ui' into PCP-3908-screen-1-localize-first-onboarding-page

# Conflicts:
#	modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
This commit is contained in:
Narek Zakarian 2024-11-12 18:06:49 +04:00
commit 70c524184c
No known key found for this signature in database
GPG key ID: 07AFD7E7A9C164A7
30 changed files with 967 additions and 291 deletions

View file

@ -0,0 +1,3 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.44497 12.0046L0.986328 6.00011L6.44497 -0.00439453L7.55488 1.00461L3.01352 6.00011L7.55488 10.9956L6.44497 12.0046Z" fill="#070707"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -40,8 +40,7 @@
font-style: italic;
}
* {
& {
font-family: "PayPalPro", sans-serif;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;

View file

@ -0,0 +1,27 @@
.ppcp-r-accordion {
margin-left: auto;
margin-right: auto;
&--title {
@include font(14, 32, 450);
color: $color-gray-900;
display: flex;
align-items: center;
gap: 16px;
margin: 24px auto;
border: 0;
background: transparent;
cursor: pointer;
}
&--content {
margin: 24px 0 0;
}
&.ppcp--is-open {
.ppcp-r-accordion--icon {
transform: rotate(180deg);
}
}
}

View file

@ -17,7 +17,7 @@ button.components-button, a.components-button {
}
&.is-primary {
@include font(13, 16, 500);
@include font(13, 20, 400);
color:$color-white;
}

View file

@ -1,19 +1,65 @@
.ppcp-r-navigation {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
.ppcp-r-navigation-container {
padding: 24px 48px;
margin: 0 -20px 48px -20px;
border-bottom: 1px solid $color-gray-300;
position: relative;
.is-primary {
min-width: 196px;
justify-content: center;
.ppcp-r-navigation {
display: flex;
justify-content: space-between;
align-items: center;
button.is-primary {
padding: 10px 18px;
justify-content: center;
margin: 0 0 0 12px;
&:not(:disabled) {
background-color: $color-blueberry;
}
}
button.is-tertiary {
@include font(16, 24, 600);
color: $color-gray-900;
&:hover{
background-color:none;
background:none;
}
}
&--left {
&__link {
@include font(20, 28, 400);
color: $color-gray-900;
text-decoration: none;
padding: 0 0 0 18px;
}
}
&--right a{
@include font(13, 20, 400);
color: $color-blueberry;
text-decoration: none;
}
&--progress-bar {
position: absolute;
bottom: 0px;
left: 0;
background-color: $color-blueberry;
height: 4px;
}
}
.is-tertiary {
padding: 10px 17px;
@media screen and (max-width: 480px) {
padding: 24px 35px;
.ppcp-r-navigation {
flex-wrap: wrap;
row-gap: 8px;
&:hover {
background-color: transparent;
&--progress-bar {
display: none;
}
}
}
}

View file

@ -1,8 +1,11 @@
.ppcp-r-toggle-block {
position: relative;
&__wrapper {
display: flex;
width: 100%;
gap: 12px;
justify-content: space-between;
}
&__switch {
@ -16,6 +19,7 @@
display: block;
margin: 0 0 4px 0;
color: $color-gray-900;
cursor: pointer;
}
&__content-description {
@ -27,4 +31,10 @@
&__toggled-content {
margin-top: 24px;
}
&.ppcp--is-loading {
pointer-events: none;
--spinner-overlay-color: #fff4;
}
}

View file

@ -0,0 +1,16 @@
.ppcp-r-spinner-overlay {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 10;
background: var(--spinner-overlay-color);
.components-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View file

@ -0,0 +1,7 @@
body:has(.ppcp-r-container--onboarding) {
background-color: #fff !important;
.notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout, .wrap.woocommerce h2:first-of-type {
display: none !important;
}
}

View file

@ -37,6 +37,10 @@
color: $color-white;
border: none;
}
.onboarding-advanced-options {
max-width: 800px;
}
}
.ppcp-r-welcome-features {
@ -109,6 +113,11 @@
justify-content: center;
padding: 8px;
margin: 0 0 48px 0;
@media screen and (max-width: 480px) {
flex-wrap: wrap;
row-gap: 8px;
}
}
&__col {
@ -128,26 +137,21 @@
border-right: 1px solid $color-gray-200;
margin-right: 48px;
}
@media screen and (max-width: 480px) {
width: 100%;
text-align: center;
border-right: 0 !important;
padding-right: 0 !important;
&:not(:last-child) {
padding-bottom: 8px;
}
}
}
.ppcp-r-page-welcome-mode-separator {
margin: 8px 0 16px 0;
}
@media screen and (max-width: 480px) {
flex-wrap: wrap;
row-gap: 8px;
&__col {
width: 100%;
text-align: center;
&:not(:last-child) {
border-bottom: 1px solid $color-gray-200;
border-right: 0;
padding-right: 0;
padding-bottom: 8px;
}
}
}
}

View file

@ -16,10 +16,13 @@
@import './components/reusable-components/navigation';
@import './components/reusable-components/fields';
@import './components/reusable-components/title-badge';
@import './components/reusable-components/_badge-box.scss';
@import './components/reusable-components/accordion-section';
@import './components/reusable-components/badge-box';
@import './components/reusable-components/spinner-overlay';
@import './components/screens/onboarding';
@import './components/screens/dashboard/tab-dashboard';
@import './components/screens/dashboard/tab-payment-methods';
}
@import './components/reusable-components/payment-method-modal';
@import './components/screens/onboarding-global';

View file

@ -0,0 +1,45 @@
import { Icon } from '@wordpress/components';
import { chevronDown, chevronUp } from '@wordpress/icons';
import { useState } from 'react';
const Accordion = ( {
title,
initiallyOpen = false,
className = '',
children,
} ) => {
const [ isOpen, setIsOpen ] = useState( initiallyOpen );
const toggleOpen = ( ev ) => {
setIsOpen( ! isOpen );
ev?.preventDefault();
return false;
};
const wrapperClasses = [ 'ppcp-r-accordion' ];
if ( className ) {
wrapperClasses.push( className );
}
if ( isOpen ) {
wrapperClasses.push( 'ppcp--is-open' );
}
return (
<div className={ wrapperClasses.join( ' ' ) }>
<button
onClick={ toggleOpen }
className="ppcp-r-accordion--title"
type="button"
>
<span>{ title }</span>
<Icon icon={ isOpen ? chevronUp : chevronDown } />
</button>
{ isOpen && (
<div className="ppcp-r-accordion--content">{ children }</div>
) }
</div>
);
};
export default Accordion;

View file

@ -12,47 +12,53 @@ import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounc
* @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 );
const DataStoreControl = React.forwardRef(
(
{
control: ControlComponent,
value: externalValue,
onChange,
delay = 300,
...props
},
[ debouncedUpdate ]
);
ref
) => {
const [ internalValue, setInternalValue ] = useState( externalValue );
const onChangeRef = useRef( onChange );
onChangeRef.current = onChange;
return (
<ControlComponent
{ ...props }
value={ internalValue }
onChange={ handleChange }
/>
);
};
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
ref={ ref }
{ ...props }
value={ internalValue }
onChange={ handleChange }
/>
);
}
);
export default DataStoreControl;

View file

@ -1,13 +1,16 @@
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import {useOnboardingStepBusiness, useOnboardingStepProducts} from "../../data";
import data from "../../utils/data";
const Navigation = ( {
setStep,
setCompleted,
currentStep,
stepperOrder,
canProceeedCallback = () => true,
stepperOrder
} ) => {
const isLastStep = () => currentStep + 1 === stepperOrder.length;
const isFistStep = () => currentStep === 0;
const navigateBy = ( stepDirection ) => {
let newStep = currentStep + stepDirection;
@ -23,20 +26,77 @@ const Navigation = ( {
}
};
const { products, toggleProduct } = useOnboardingStepProducts();
const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness();
let navigationTitle = '';
let disabled = false;
switch ( currentStep ) {
case 1:
navigationTitle = __( 'Set up store type', 'woocommerce-paypal-payments' );
disabled = isCasualSeller === null
break;
case 2:
navigationTitle = __( 'Select product types', 'woocommerce-paypal-payments' );
disabled = products.length < 1
break;
case 3:
navigationTitle = __( 'Choose checkout options', 'woocommerce-paypal-payments' );
case 4:
navigationTitle = __( 'Connect your PayPal account', 'woocommerce-paypal-payments' );
break;
default:
navigationTitle = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
}
return (
<div className="ppcp-r-navigation">
<Button variant="tertiary" onClick={ () => navigateBy( -1 ) }>
{ __( 'Back', 'woocommerce-paypal-payments' ) }
</Button>
<Button
variant="primary"
disabled={ ! canProceeedCallback() }
onClick={ () => navigateBy( 1 ) }
>
{ __( 'Next', 'woocommerce-paypal-payments' ) }
</Button>
</div>
);
<div className="ppcp-r-navigation-container">
<div className="ppcp-r-navigation">
<div className="ppcp-r-navigation--left">
<span>{data().getImage('icon-arrow-left.svg')}</span>
{!isFistStep() ? (
<Button variant="tertiary" onClick={() => navigateBy(-1)}>
{navigationTitle}
</Button>
) : (
<a
className="ppcp-r-navigation--left__link"
href={global.ppcpSettings.wcPaymentsTabUrl}
aria-label={__('Return to payments', 'woocommerce-paypal-payments')}
>
{navigationTitle}
</a>
)}
</div>
{!isFistStep() && (
<div className="ppcp-r-navigation--right">
<a
href={ global.ppcpSettings.wcPaymentsTabUrl }
aria-label={ __( 'Return to payments', 'woocommerce-paypal-payments' ) }
>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</a>
{!isLastStep() && (
<Button
variant="primary"
disabled={ disabled }
onClick={ () => navigateBy( 1 ) }
>
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
)}
</div>
)}
<div
className="ppcp-r-navigation--progress-bar"
style={{
width: `${ ( currentStep / ( stepperOrder.length - 1 ) ) * 90 }%`
}}
></div>
</div>
</div>
);
};
export default Navigation;

View file

@ -1,14 +1,42 @@
import { ToggleControl } from '@wordpress/components';
import { useRef } from '@wordpress/element';
import SpinnerOverlay from './SpinnerOverlay';
const SettingsToggleBlock = ( {
isToggled,
setToggled,
isLoading = false,
...props
} ) => {
const toggleRef = useRef( null );
const blockClasses = [ 'ppcp-r-toggle-block' ];
if ( isLoading ) {
blockClasses.push( 'ppcp--is-loading' );
}
const handleLabelClick = () => {
if ( ! toggleRef.current || isLoading ) {
return;
}
toggleRef.current.click();
toggleRef.current.focus();
};
const SettingsToggleBlock = ( { isToggled, setToggled, ...props } ) => {
return (
<div className="ppcp-r-toggle-block">
<div className={ blockClasses.join( ' ' ) }>
<div className="ppcp-r-toggle-block__wrapper">
<div className="ppcp-r-toggle-block__content">
{ props?.label && (
<span className="ppcp-r-toggle-block__content-label">
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -- keyboard element is ToggleControl
<div
className="ppcp-r-toggle-block__content-label"
onClick={ handleLabelClick }
>
{ props.label }
</span>
</div>
) }
{ props?.description && (
<p
@ -21,15 +49,16 @@ const SettingsToggleBlock = ( { isToggled, setToggled, ...props } ) => {
</div>
<div className="ppcp-r-toggle-block__switch">
<ToggleControl
ref={ toggleRef }
checked={ isToggled }
onChange={ ( newValue ) => {
setToggled( newValue );
} }
onChange={ ( newState ) => setToggled( newState ) }
disabled={ isLoading }
/>
</div>
</div>
{ props.children && isToggled && (
<div className="ppcp-r-toggle-block__toggled-content">
{ isLoading && <SpinnerOverlay /> }
{ props.children }
</div>
) }

View file

@ -0,0 +1,11 @@
import { Spinner } from '@wordpress/components';
const SpinnerOverlay = () => {
return (
<div className="ppcp-r-spinner-overlay">
<Spinner />
</div>
);
};
export default SpinnerOverlay;

View file

@ -0,0 +1,180 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
import { useRef } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
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';
const AdvancedOptionsForm = ( { setCompleted } ) => {
const {
isManualConnectionBusy,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = useOnboardingStepWelcome();
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { connectManual } = useManualConnect();
const refClientId = useRef( null );
const refClientSecret = useRef( null );
const handleFormValidation = () => {
const fields = [
{
ref: refClientId,
value: clientId,
errorMessage: __(
'Please enter your Client ID',
'woocommerce-paypal-payments'
),
},
{
ref: refClientSecret,
value: clientSecret,
errorMessage: __(
'Please enter your Secret Key',
'woocommerce-paypal-payments'
),
},
];
for ( const { ref, value, errorMessage } of fields ) {
if ( value ) {
continue;
}
ref?.current?.focus();
createErrorNotice( errorMessage );
return false;
}
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 handleServerSuccess = () => {
createSuccessNotice(
__( 'Connected to PayPal', 'woocommerce-paypal-payments' )
);
setCompleted( true );
};
const handleConnect = async () => {
if ( ! handleFormValidation() ) {
return;
}
const res = await connectManual();
if ( res.success ) {
handleServerSuccess();
} else {
handleServerError( res );
}
};
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input'
);
return (
<>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
'woocommerce-paypal-payments'
) }
description={ __(
'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="secondary">
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
<Separator className="ppcp-r-page-welcome-mode-separator" />
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
isLoading={ isManualConnectionBusy }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={
isSandboxMode
? __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
)
: __(
'Live Client ID',
'woocommerce-paypal-payments'
)
}
value={ clientId }
onChange={ setClientId }
/>
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={
isSandboxMode
? __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
)
: __(
'Live Secret Key',
'woocommerce-paypal-payments'
)
}
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button variant="secondary" onClick={ handleConnect }>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
</>
);
};
export default AdvancedOptionsForm;

View file

@ -1,6 +1,8 @@
import Container from '../../ReusableComponents/Container';
import { useOnboardingStep } from '../../../data';
import { getSteps } from './availableSteps';
import {__} from "@wordpress/i18n";
import Navigation from "../../ReusableComponents/Navigation";
const getCurrentStep = ( requestedStep, steps ) => {
const isValidStep = ( step ) =>
@ -20,16 +22,24 @@ const Onboarding = () => {
const CurrentStepComponent = getCurrentStep( step, steps );
return (
<Container page="onboarding">
<div className="ppcp-r-card">
<CurrentStepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
/>
</div>
</Container>
<>
<Navigation
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
/>
<Container page="onboarding">
<div className="ppcp-r-card">
<CurrentStepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
/>
</div>
</Container>
</>
);
};

View file

@ -4,7 +4,6 @@ import SelectBox from '../../ReusableComponents/SelectBox';
import { __ } from '@wordpress/i18n';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import { useOnboardingStepBusiness } from '../../../data';
import Navigation from '../../ReusableComponents/Navigation';
import { BUSINESS_TYPES } from '../../../data/constants';
const BUSINESS_RADIO_GROUP_NAME = 'business';
@ -101,13 +100,6 @@ const StepBusiness = ( {
/>
</SelectBox>
</SelectBoxWrapper>
<Navigation
setStep={ setStep }
currentStep={ currentStep }
stepperOrder={ stepperOrder }
setCompleted={ setCompleted }
canProceeedCallback={ () => isCasualSeller !== null }
/>
</div>
</div>
);

View file

@ -1,5 +1,4 @@
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import Navigation from '../../ReusableComponents/Navigation';
import { __ } from '@wordpress/i18n';
import SelectBox from '../../ReusableComponents/SelectBox';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
@ -121,13 +120,6 @@ const StepProducts = ( {
</a>
</SelectBox>
</SelectBoxWrapper>
<Navigation
setStep={ setStep }
currentStep={ currentStep }
stepperOrder={ stepperOrder }
setCompleted={ setCompleted }
canProceeedCallback={ () => products.length > 0 }
/>
</div>
</div>
);

View file

@ -1,50 +1,62 @@
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
import { Button } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import SettingsToggleBlock from '../../ReusableComponents/SettingsToggleBlock';
import Separator from '../../ReusableComponents/Separator';
import { useOnboardingStepWelcome, useManualConnect } from '../../../data';
import WelcomeDocs from '../../ReusableComponents/WelcomeDocs';
import DataStoreControl from '../../ReusableComponents/DataStoreControl';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
import AccordionSection from '../../ReusableComponents/AccordionSection';
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
return (
<div className="ppcp-r-page-welcome">
<OnboardingHeader
title={__(
'Welcome to PayPal Payments',
'woocommerce-paypal-payments'
)}
description={__(
'Your all-in-one integration for PayPal checkout solutions that enable buyers<br/> to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
'woocommerce-paypal-payments'
)}
/>
<div className="ppcp-r-inner-container">
<WelcomeFeatures/>
<PaymentMethodIcons icons="all"/>
<p className="ppcp-r-button__description">{__(
`Click the button below to be guided through connecting your existing PayPal account or creating a new one.You will be able to choose the payment options that are right for your store.`,
'woocommerce-paypal-payments'
)}
</p>
<Button
className="ppcp-r-button-activate-paypal"
variant="primary"
onClick={() => setStep(currentStep + 1)}
>
{__(
'Activate PayPal Payments',
'woocommerce-paypal-payments'
)}
</Button>
</div>
<Separator className="ppcp-r-page-welcome-mode-separator"/>
<WelcomeDocs/>
<WelcomeForm setCompleted={setCompleted}/>
</div>
);
<div className="ppcp-r-page-welcome">
<OnboardingHeader
title={ __(
'Welcome to PayPal Payments',
'woocommerce-paypal-payments'
) }
description={ __(
'Your all-in-one integration for PayPal checkout solutions that enable buyers<br/> to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
'woocommerce-paypal-payments'
) }
/>
<div className="ppcp-r-inner-container">
<WelcomeFeatures />
<PaymentMethodIcons icons="all" />
<p className="ppcp-r-button__description">
{ __(
`Click the button below to be guided through connecting your existing PayPal account or creating a new one.You will be able to choose the payment options that are right for your store.`,
'woocommerce-paypal-payments'
) }
</p>
<Button
className="ppcp-r-button-activate-paypal"
variant="primary"
onClick={ () => setStep( currentStep + 1 ) }
>
{ __(
'Activate PayPal Payments',
'woocommerce-paypal-payments'
) }
</Button>
</div>
<Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs />
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />
<AccordionSection
title={ __(
'See advanced options',
'woocommerce-paypal-payments'
) }
className="onboarding-advanced-options"
initiallyOpen={ false }
>
<AdvancedOptionsForm setCompleted={ setCompleted } />
</AccordionSection>
</div>
);
};
const WelcomeFeatures = () => {
@ -72,121 +84,9 @@ const WelcomeFeatures = () => {
'woocommerce-paypal-payments'
) }
</span>
<p>{ __( 'Supported', 'woocommerce-paypal-payments' ) }</p>
</div>
</div>
);
};
const WelcomeForm = ( { setCompleted } ) => {
const {
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = useOnboardingStepWelcome();
const { connectManual } = useManualConnect();
const handleConnect = async () => {
try {
const res = await connectManual(
clientId,
clientSecret,
isSandboxMode
);
if ( ! res.success ) {
throw new Error( 'Request failed.' );
}
console.log(`Merchant ID: ${res.merchantId}, email: ${res.email}`);
setCompleted( true );
} catch ( exc ) {
console.error( exc );
alert( 'Connection failed.' );
}
};
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
return (
<>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
'woocommerce-paypal-payments'
) }
description={ __(
'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="secondary">
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
<Separator className="ppcp-r-page-welcome-mode-separator" />
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
>
<DataStoreControl
control={ TextControl }
label={
isSandboxMode
? __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
)
: __(
'Live Client ID',
'woocommerce-paypal-payments'
)
}
value={ clientId }
onChange={ setClientId }
/>
<DataStoreControl
control={ TextControl }
label={
isSandboxMode
? __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
)
: __(
'Live Secret Key',
'woocommerce-paypal-payments'
)
}
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button variant="secondary" onClick={ handleConnect }>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
</>
<p>{ __( 'Supported', 'woocommerce-paypal-payments' ) }</p>
</div>
</div>
);
};

View file

@ -4,6 +4,7 @@ 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',
// Persistent data.
SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED',

View file

@ -38,6 +38,19 @@ export const setIsSaving = ( 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,
};
};
/**
* Persistent. Set the full onboarding details, usually during app initialization.
*
@ -155,10 +168,47 @@ export const setProducts = ( products ) => {
};
};
/**
* 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 {any} A generator function that handles the saving process.
* @return {boolean} True, if the values were successfully saved.
*/
export function* persist() {
let error = null;

View file

@ -25,6 +25,10 @@ const useOnboardingDetails = () => {
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();
@ -78,6 +82,7 @@ const useOnboardingDetails = () => {
return {
isSaving,
isReady,
isManualConnectionBusy,
step,
setStep: ( value ) => setDetailAndPersist( setOnboardingStep, value ),
completed,
@ -105,6 +110,7 @@ const useOnboardingDetails = () => {
export const useOnboardingStepWelcome = () => {
const {
isSaving,
isManualConnectionBusy,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
@ -117,6 +123,7 @@ export const useOnboardingStepWelcome = () => {
return {
isSaving,
isManualConnectionBusy,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
@ -148,19 +155,9 @@ export const useOnboardingStep = () => {
};
export const useManualConnect = () => {
const connectManual = async ( clientId, clientSecret, isSandboxMode ) => {
return await apiFetch( {
path: `${ NAMESPACE }/connect_manual`,
method: 'POST',
data: {
clientId,
clientSecret,
useSandbox: isSandboxMode,
},
} );
};
const { connectViaIdAndSecret } = useDispatch( STORE_NAME );
return {
connectManual,
connectManual: connectViaIdAndSecret,
};
};

View file

@ -3,6 +3,7 @@ import ACTION_TYPES from './action-types';
const defaultState = {
isReady: false,
isSaving: false,
isManualConnectionBusy: false,
// Data persisted to the server.
data: {
@ -59,6 +60,9 @@ export const onboardingReducer = (
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 );

View file

@ -0,0 +1,191 @@
<?php
/**
* GeneralSettings class
*
* @package WooCommerce\PayPalCommerce\Settings\Data
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use RuntimeException;
/**
* This class serves as a container for managing the general plugin settings,
* such as the connection credentials.
*/
class GeneralSettings extends AbstractDataModel {
/**
* Option key where profile details are stored.
*
* @var string
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-general-settings';
/**
* Get default values for the model.
*
* @return array
*/
protected function get_defaults() : array {
return array(
'is_sandbox' => false,
'live_client_id' => '',
'live_client_secret' => '',
'live_merchant_id' => '',
'live_merchant_email' => '',
'sandbox_client_id' => '',
'sandbox_client_secret' => '',
'sandbox_merchant_id' => '',
'sandbox_merchant_email' => '',
);
}
// -----
/**
* Gets the 'is_sandbox' flag.
*/
public function is_sandbox() : bool {
return (bool) $this->data['is_sandbox'];
}
/**
* Sets the 'is_sandbox' flag.
*
* @param bool $value The value to set.
*/
public function set_is_sandbox( bool $value ) : void {
$this->data['is_sandbox'] = $value;
}
/**
* Gets the live client ID.
*/
public function live_client_id() : string {
return $this->data['live_client_id'];
}
/**
* Sets the live client ID.
*
* @param string $value The value to set.
*/
public function set_live_client_id( string $value ) : void {
$this->data['live_client_id'] = sanitize_text_field( $value );
}
/**
* Gets the live client secret.
*/
public function live_client_secret() : string {
return $this->data['live_client_secret'];
}
/**
* Sets the live client secret.
*
* @param string $value The value to set.
*/
public function set_live_client_secret( string $value ) : void {
$this->data['live_client_secret'] = sanitize_text_field( $value );
}
/**
* Gets the live merchant ID.
*/
public function live_merchant_id() : string {
return $this->data['live_merchant_id'];
}
/**
* Sets the live merchant ID.
*
* @param string $value The value to set.
*/
public function set_live_merchant_id( string $value ) : void {
$this->data['live_merchant_id'] = sanitize_text_field( $value );
}
/**
* Gets the live merchant email.
*/
public function live_merchant_email() : string {
return $this->data['live_merchant_email'];
}
/**
* Sets the live merchant email.
*
* @param string $value The value to set.
*/
public function set_live_merchant_email( string $value ) : void {
$this->data['live_merchant_email'] = sanitize_email( $value );
}
/**
* Gets the sandbox client ID.
*/
public function sandbox_client_id() : string {
return $this->data['sandbox_client_id'];
}
/**
* Sets the sandbox client ID.
*
* @param string $value The value to set.
*/
public function set_sandbox_client_id( string $value ) : void {
$this->data['sandbox_client_id'] = sanitize_text_field( $value );
}
/**
* Gets the sandbox client secret.
*/
public function sandbox_client_secret() : string {
return $this->data['sandbox_client_secret'];
}
/**
* Sets the sandbox client secret.
*
* @param string $value The value to set.
*/
public function set_sandbox_client_secret( string $value ) : void {
$this->data['sandbox_client_secret'] = sanitize_text_field( $value );
}
/**
* Gets the sandbox merchant ID.
*/
public function sandbox_merchant_id() : string {
return $this->data['sandbox_merchant_id'];
}
/**
* Sets the sandbox merchant ID.
*
* @param string $value The value to set.
*/
public function set_sandbox_merchant_id( string $value ) : void {
$this->data['sandbox_merchant_id'] = sanitize_text_field( $value );
}
/**
* Gets the sandbox merchant email.
*/
public function sandbox_merchant_email() : string {
return $this->data['sandbox_merchant_email'];
}
/**
* Sets the sandbox merchant email.
*
* @param string $value The value to set.
*/
public function set_sandbox_merchant_email( string $value ) : void {
$this->data['sandbox_merchant_email'] = sanitize_email( $value );
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* PaymentSettings class
*
* @package WooCommerce\PayPalCommerce\Settings\Data
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use RuntimeException;
/**
* This class serves as a container for managing the payment settings.
*/
class PaymentSettings extends AbstractDataModel {
/**
* Option key where profile details are stored.
*
* @var string
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-payment-settings';
/**
* Get default values for the model.
*
* @return array
*/
protected function get_defaults() : array {
return array();
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* StylingSettings class
*
* @package WooCommerce\PayPalCommerce\Settings\Data
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use RuntimeException;
/**
* This class serves as a container for managing the styling settings.
*/
class StylingSettings extends AbstractDataModel {
/**
* Option key where profile details are stored.
*
* @var string
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-styling-settings';
/**
* Get default values for the model.
*
* @return array
*/
protected function get_defaults() : array {
return array();
}
}

View file

@ -85,10 +85,11 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'ppcp-admin-settings',
'ppcpSettings',
array(
'assets' => array(
'assets' => array(
'imagesUrl' => $module_url . '/images/',
),
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'wcPaymentsTabUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout' ),
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
)
);
}

View file

@ -108,6 +108,7 @@
"wp_org_slug": "woocommerce-paypal-payments"
},
"dependencies": {
"@wordpress/icons": "^10.11.0",
"dotenv": "^16.0.3",
"npm-run-all": "^4.1.5",
"playwright": "^1.43.0",

View file

@ -2671,7 +2671,7 @@
mime "^3.0.0"
web-vitals "^4.2.1"
"@wordpress/element@^6.1.0":
"@wordpress/element@*", "@wordpress/element@^6.1.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-6.11.0.tgz#7bc3e453a95bb806a707b4dc617373afa108af19"
integrity sha512-UvHFYkT+EEaXEyEfw+iqLHRO9OwBjjsUydEMHcqntzkNcsYeAbmaL9V8R9ikXHLe6ftdbkwoXgF85xVPhVsL+Q==
@ -2715,6 +2715,15 @@
globals "^13.12.0"
requireindex "^1.2.0"
"@wordpress/icons@^10.11.0":
version "10.11.0"
resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-10.11.0.tgz#0beedef8ee49c135412fb81fc59440dd48d652aa"
integrity sha512-RMetpFwUIeh3sVj2+p6+QX5AW8pF7DvQzxH9jUr8YjaF2iLE64vy6m0cZz/X8xkSktHrXMuPJIr7YIVF20TEyw==
dependencies:
"@babel/runtime" "7.25.7"
"@wordpress/element" "*"
"@wordpress/primitives" "*"
"@wordpress/jest-console@*":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@wordpress/jest-console/-/jest-console-8.11.0.tgz#a825f4d9ee4eb007c9b6329687f8507dc30b49d2"
@ -2749,6 +2758,15 @@
resolved "https://registry.yarnpkg.com/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz#6b3f9aa7e2698c0d78e644037c6778b5c1da12ce"
integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw==
"@wordpress/primitives@*":
version "4.11.0"
resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-4.11.0.tgz#7bc24c07ed11057340832791c1c21e75a5181194"
integrity sha512-CoBXbh0mOSxcZtuzL7gK3RVumFx71DXQBfd3IkbRHuuVxa+2hI4KDuFyomSsbjQDshHsfuVrKUvuT3UGt6pdpQ==
dependencies:
"@babel/runtime" "7.25.7"
"@wordpress/element" "*"
clsx "^2.1.1"
"@wordpress/scripts@~30.0.0":
version "30.0.6"
resolved "https://registry.yarnpkg.com/@wordpress/scripts/-/scripts-30.0.6.tgz#37f5e45068829ed8da24e3727f9946f5351e1904"
@ -3727,6 +3745,11 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"