🔀 Merge branch 'PCP-3981’

This commit is contained in:
Philipp Stracker 2024-12-04 16:06:34 +01:00
commit accf1b0619
No known key found for this signature in database
12 changed files with 307 additions and 196 deletions

View file

@ -24,6 +24,10 @@ $max-width-onboarding: 1024px;
$max-width-onboarding-content: 500px;
$max-width-settings: 938px;
:root {
--ppcp-color-app-bg: #{$color-white};
}
#ppcp-settings-container {
--max-width-settings: #{$max-width-settings};
--max-width-onboarding: #{$max-width-onboarding};

View file

@ -1,64 +1,111 @@
.ppcp-r-navigation-container {
padding: 24px 48px;
position: sticky;
top: var(--wp-admin--admin-bar--height);
z-index: 10;
padding: 10px 48px;
margin: 0 -20px 48px -20px;
border-bottom: 1px solid $color-gray-300;
position: relative;
box-shadow: 0 -1px 0 0 $color-gray-300 inset;
background: var(--ppcp-color-app-bg);
transition: box-shadow 0.3s;
--wp-components-color-accent: #{$color-blueberry};
--color-text: #{$color-gray-900};
--color-disabled: #CCC;
.ppcp-r-navigation {
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
button.is-primary {
padding: 10px 18px;
justify-content: center;
margin: 0 0 0 12px;
&:not(:disabled) {
background-color: $color-blueberry;
.components-button {
@include font(13, 20, 400);
&.is-primary {
background-color: var(--wp-components-color-accent);
padding: 10px 16px;
justify-content: center;
margin: 0 0 0 12px;
border-radius: 2px;
&:disabled {
background-color: var(--color-disabled);
}
}
}
button.is-tertiary {
@include font(16, 24, 600);
color: $color-gray-900;
&:hover{
background-color:none;
background:none;
&.is-link {
color: var(--wp-components-color-accent);
text-decoration: none;
&:disabled {
color: var(--color-disabled);
}
}
&.is-title {
@include font(16, 24, 600);
color: var(--color-text);
.title {
margin-left: 18px;
}
.big {
@include font(20, 28, 400);
}
}
}
&--left {
&__link {
@include font(20, 28, 400);
color: $color-gray-900;
text-decoration: none;
padding: 0 0 0 18px;
}
align-items: center;
display: inline-flex;
}
&--right a{
@include font(13, 20, 400);
color: $color-blueberry;
text-decoration: none;
&--right {
.is-link {
padding: 10px 16px;
}
}
&--progress-bar {
position: absolute;
bottom: 0px;
bottom: 0;
left: 0;
background-color: $color-blueberry;
background-color: var(--wp-components-color-accent);
height: 4px;
transition: width 0.3s;
}
}
@media screen and (max-width: 480px) {
padding: 24px 35px;
&.is-scrolled {
box-shadow: 0 -1px 0 0 $color-gray-300 inset, 0 8px 8px 0 rgba(85, 93, 102, .3);
}
@media screen and (max-width: 782px) {
padding: 10px 12px;
.ppcp-r-navigation {
flex-wrap: wrap;
row-gap: 8px;
white-space: nowrap;
&--right {
position: absolute;
right: 10px;
z-index: 10;
background: var(--ppcp-color-app-bg);
box-shadow: -5px 0 8px var(--ppcp-color-app-bg);
}
&--progress-bar {
display: none;
height: 2px;
}
.components-button.is-title {
.title {
margin-left: 4px;
}
}
}
}

View file

@ -0,0 +1,18 @@
body:has(.ppcp-r-container--settings),
body:has(.ppcp-r-container--onboarding) {
background-color: var(--ppcp-color-app-bg) !important;
.woocommerce-layout,
#woocommerce-layout__primary {
padding: 0 !important;
}
.notice,
.nav-tab-wrapper.woo-nav-tab-wrapper,
.woocommerce-layout__header,
.wrap.woocommerce form > h2,
#screen-meta-links {
display: none !important;
visibility: hidden;
}
}

View file

@ -1,8 +0,0 @@
body:has(.ppcp-r-container--onboarding) {
background-color: #fff !important;
.notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout__header, .wrap.woocommerce form > h2, #screen-meta-links {
display: none !important;
visibility: hidden;
}
}

View file

@ -1,7 +0,0 @@
body:has(.ppcp-r-container--settings) {
background-color: #fff !important;
.notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout, .wrap.woocommerce form > h2, #screen-meta-links {
display: none !important;
}
}

View file

@ -28,5 +28,4 @@
}
@import './components/reusable-components/payment-method-modal';
@import './components/screens/onboarding-global';
@import './components/screens/settings-global';
@import './components/screens/fullscreen';

View file

@ -1,130 +1,73 @@
import { Button } from '@wordpress/components';
import { Button, Icon } from '@wordpress/components';
import { chevronLeft } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { OnboardingHooks } from '../../../../data';
import data from '../../../../utils/data';
import useIsScrolled from '../../../../hooks/useIsScrolled';
const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => {
const isLastStep = () => currentStep + 1 === stepperOrder.length;
const isFistStep = () => currentStep === 0;
const navigateBy = ( stepDirection ) => {
let newStep = currentStep + stepDirection;
const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
const { isScrolled } = useIsScrolled();
if ( isNaN( newStep ) || newStep < 0 ) {
console.warn( 'Invalid next step:', newStep );
newStep = 0;
}
if ( newStep >= stepperOrder.length ) {
setCompleted( true );
} else {
setStep( newStep );
}
};
const { products } = OnboardingHooks.useProducts();
const { isCasualSeller } = OnboardingHooks.useBusiness();
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'
);
}
const state = OnboardingHooks.useNavigationState();
const isDisabled = ! canProceed( state );
const className = classNames( 'ppcp-r-navigation-container', {
'is-scrolled': isScrolled,
} );
return (
<div className="ppcp-r-navigation-container">
<div className={ className }>
<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>
) }
<Button
variant="link"
onClick={ isFirst ? onExit : onPrev }
className="is-title"
>
<Icon icon={ chevronLeft } />
<span className={ 'title ' + ( isFirst ? 'big' : '' ) }>
{ title }
</span>
</Button>
</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>
{ ! isFirst &&
NextButton( { showNext, isDisabled, onNext, onExit } ) }
<ProgressBar percent={ percentage } />
</div>
</div>
);
};
const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
return (
<div className="ppcp-r-navigation--right">
<Button variant="link" onClick={ onExit }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button>
{ showNext && (
<Button
variant="primary"
disabled={ isDisabled }
onClick={ onNext }
>
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
) }
</div>
);
};
const ProgressBar = ( { percent } ) => {
percent = Math.min( Math.max( percent, 0 ), 100 );
return (
<div
className="ppcp-r-navigation--progress-bar"
style={ { width: `${ percent * 0.9 }%` } }
/>
);
};
export default Navigation;

View file

@ -1,40 +1,37 @@
import Container from '../../ReusableComponents/Container';
import { OnboardingHooks } from '../../../data';
import { getSteps } from './availableSteps';
import { getSteps, getCurrentStep } from './availableSteps';
import Navigation from './Components/Navigation';
const getCurrentStep = ( requestedStep, steps ) => {
const isValidStep = ( step ) =>
typeof step === 'number' &&
Number.isInteger( step ) &&
step >= 0 &&
step < steps.length;
const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0;
return steps[ safeCurrentStep ];
};
const Onboarding = () => {
const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
const steps = getSteps( flags );
const CurrentStepComponent = getCurrentStep( step, steps );
const Steps = getSteps( flags );
const currentStep = getCurrentStep( step, Steps );
const handleNext = () => setStep( currentStep.nextStep );
const handlePrev = () => setStep( currentStep.prevStep );
const handleExit = () => {
window.location.href = window.ppcpSettings.wcPaymentsTabUrl;
};
return (
<>
<Navigation
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
stepDetails={ currentStep }
onNext={ handleNext }
onPrev={ handlePrev }
onExit={ handleExit }
/>
<Container page="onboarding">
<div className="ppcp-r-card">
<CurrentStepComponent
<currentStep.StepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ steps }
stepperOrder={ Steps }
/>
</div>
</Container>

View file

@ -10,9 +10,8 @@ const BUSINESS_RADIO_GROUP_NAME = 'business';
const StepBusiness = ( {} ) => {
const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness();
const handleSellerTypeChange = ( value ) => {
const handleSellerTypeChange = ( value ) =>
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value );
};
const getCurrentValue = () => {
if ( isCasualSeller === null ) {

View file

@ -1,21 +1,86 @@
import { __ } from '@wordpress/i18n';
import StepWelcome from './StepWelcome';
import StepBusiness from './StepBusiness';
import StepProducts from './StepProducts';
import StepPaymentMethods from './StepPaymentMethods';
import StepCompleteSetup from './StepCompleteSetup';
/**
* List of all onboarding screens that are available.
*
* The screens are displayed in the order in which they appear in this array
*
* @type {[{id, StepComponent, title}]}
*/
const ALL_STEPS = [
{
id: 'welcome',
title: __( 'PayPal Payments', 'woocommerce-paypal-payments' ),
StepComponent: StepWelcome,
canProceed: () => true,
},
{
id: 'business',
title: __( 'Set up store type', 'woocommerce-paypal-payments' ),
StepComponent: StepBusiness,
canProceed: ( { business } ) => business.isCasualSeller !== null,
},
{
id: 'products',
title: __( 'Select product types', 'woocommerce-paypal-payments' ),
StepComponent: StepProducts,
canProceed: ( { products } ) => products.products.length > 0,
},
{
id: 'methods',
title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ),
StepComponent: StepPaymentMethods,
canProceed: () => true,
},
{
id: 'complete',
title: __(
'Connect your PayPal account',
'woocommerce-paypal-payments'
),
StepComponent: StepCompleteSetup,
canProceed: () => true,
},
];
export const getSteps = ( flags ) => {
const allSteps = [
StepWelcome,
StepBusiness,
StepProducts,
StepPaymentMethods,
StepCompleteSetup,
];
const steps = flags.canUseCasualSelling
? ALL_STEPS
: ALL_STEPS.filter( ( step ) => step.id !== 'business' );
if ( ! flags.canUseCasualSelling ) {
return allSteps.filter( ( step ) => step !== StepBusiness );
}
const totalStepsCount = steps.length;
return allSteps;
return steps.map( ( step, index ) => ( {
...step,
isFirst: index === 0,
isLast: index === totalStepsCount - 1,
showNext: index < totalStepsCount - 1,
percentage: 100 * ( index / ( totalStepsCount - 1 ) ),
nextStep: index < totalStepsCount - 1 ? index + 1 : index,
prevStep: index > 0 ? index - 1 : 0,
} ) );
};
/**
* Returns the screen-details of the current step, based on the numeric step-index.
*
* @param {number} requestedStep Index of the screen to display.
* @param {[]} steps List of all available steps (see `getSteps()`)
* @return {{id, StepComponent, title}} The requested screen details, or the first welcome screen.
*/
export const getCurrentStep = ( requestedStep, steps ) => {
const isValidStep = ( step ) =>
typeof step === 'number' &&
Number.isInteger( step ) &&
step >= 0 &&
step < steps.length;
const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0;
return steps[ safeCurrentStep ];
};

View file

@ -113,3 +113,13 @@ export const useSteps = () => {
return { flags, isReady, step, setStep, completed, setCompleted };
};
export const useNavigationState = () => {
const products = useProducts();
const business = useBusiness();
return {
products,
business,
};
};

View file

@ -0,0 +1,44 @@
/**
* Taken from WooCommerce core:
* https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/client/hooks/useIsScrolled.js
*/
import { useEffect, useRef, useState } from '@wordpress/element';
const isAtBottom = () =>
window.innerHeight + window.scrollY >= document.body.scrollHeight;
const useIsScrolled = () => {
const [ isScrolled, setIsScrolled ] = useState( false );
const [ atBottom, setAtBottom ] = useState( isAtBottom() );
const rafHandle = useRef( null );
useEffect( () => {
const updateIsScrolled = () => {
setIsScrolled( window.pageYOffset > 20 );
setAtBottom( isAtBottom() );
};
const scrollListener = () => {
rafHandle.current =
window.requestAnimationFrame( updateIsScrolled );
};
window.addEventListener( 'scroll', scrollListener );
window.addEventListener( 'resize', scrollListener );
return () => {
window.removeEventListener( 'scroll', scrollListener );
window.removeEventListener( 'resize', scrollListener );
window.cancelAnimationFrame( rafHandle.current );
};
}, [] );
return {
isScrolled,
atBottom,
atTop: ! isScrolled,
};
};
export default useIsScrolled;