mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-31 06:52:50 +08:00
♻️ Fix the PayPal onboarding button
This commit is contained in:
parent
827e08d91f
commit
1c44a8105b
2 changed files with 127 additions and 100 deletions
|
@ -1,15 +1,59 @@
|
|||
import { Button } from '@wordpress/components';
|
||||
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CommonHooks } from '../../../../data';
|
||||
import { openSignup } from '../../../ReusableComponents/Icons';
|
||||
import {
|
||||
useProductionConnection,
|
||||
useSandboxConnection,
|
||||
} from '../../../../hooks/useHandleConnections';
|
||||
import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections';
|
||||
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
||||
|
||||
/**
|
||||
* Button component that outputs a placeholder button when no onboardingUrl is present yet - the
|
||||
* placeholder button looks identical to the working button, but has no href, target, or
|
||||
* custom connection attributes.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} props.className
|
||||
* @param {string} props.variant
|
||||
* @param {boolean} props.showIcon
|
||||
* @param {?string} props.href
|
||||
* @param {Element} props.children
|
||||
*/
|
||||
const ButtonOrPlaceholder = ( {
|
||||
className,
|
||||
variant,
|
||||
showIcon,
|
||||
href,
|
||||
children,
|
||||
} ) => {
|
||||
if ( ! href ) {
|
||||
return (
|
||||
<Button
|
||||
className={ className }
|
||||
variant={ variant }
|
||||
icon={ showIcon ? openSignup : null }
|
||||
>
|
||||
{ children }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={ className }
|
||||
variant={ variant }
|
||||
icon={ showIcon ? openSignup : null }
|
||||
href={ href }
|
||||
target={ 'PPFrame' }
|
||||
{ ...{
|
||||
'data-paypal-button': 'true',
|
||||
'data-paypal-onboard-complete': 'onOnboardComplete',
|
||||
'data-paypal-onboard-button': 'true',
|
||||
} }
|
||||
>
|
||||
{ children }
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectionButton = ( {
|
||||
title,
|
||||
isSandbox = false,
|
||||
|
@ -17,31 +61,29 @@ const ConnectionButton = ( {
|
|||
showIcon = true,
|
||||
className = '',
|
||||
} ) => {
|
||||
const { handleSandboxConnect } = useSandboxConnection();
|
||||
const { handleProductionConnect } = useProductionConnection();
|
||||
const { onboardingUrl, scriptLoaded } =
|
||||
useHandleOnboardingButton( isSandbox );
|
||||
const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
|
||||
'sandbox-mode': isSandbox,
|
||||
'live-mode': ! isSandbox,
|
||||
} );
|
||||
|
||||
const handleConnectClick = async () => {
|
||||
if ( isSandbox ) {
|
||||
await handleSandboxConnect();
|
||||
} else {
|
||||
await handleProductionConnect();
|
||||
useEffect( () => {
|
||||
if ( scriptLoaded && onboardingUrl ) {
|
||||
window.PAYPAL.apps.Signup.render();
|
||||
}
|
||||
};
|
||||
}, [ scriptLoaded, onboardingUrl ] );
|
||||
|
||||
return (
|
||||
<BusyStateWrapper>
|
||||
<Button
|
||||
<BusyStateWrapper isBusy={ ! onboardingUrl }>
|
||||
<ButtonOrPlaceholder
|
||||
className={ buttonClassName }
|
||||
variant={ variant }
|
||||
icon={ showIcon ? openSignup : null }
|
||||
onClick={ handleConnectClick }
|
||||
showIcon={ showIcon }
|
||||
href={ onboardingUrl }
|
||||
>
|
||||
<span className="button-title">{ title }</span>
|
||||
</Button>
|
||||
</ButtonOrPlaceholder>
|
||||
</BusyStateWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
import { CommonHooks, OnboardingHooks } from '../data';
|
||||
import { openPopup } from '../utils/window';
|
||||
|
||||
const MESSAGES = {
|
||||
CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ),
|
||||
|
@ -35,32 +35,77 @@ const ACTIVITIES = {
|
|||
CONNECT_MANUAL: 'MANUAL_LOGIN',
|
||||
};
|
||||
|
||||
const handlePopupWithCompletion = ( url, onError ) => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const popup = openPopup( url );
|
||||
export const useHandleOnboardingButton = ( isSandbox ) => {
|
||||
const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
|
||||
const { productionOnboardingUrl } = CommonHooks.useProduction();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
|
||||
const [ scriptLoaded, setScriptLoaded ] = useState( false );
|
||||
|
||||
if ( ! popup ) {
|
||||
onError( MESSAGES.POPUP_BLOCKED );
|
||||
resolve( false );
|
||||
useEffect( () => {
|
||||
const fetchOnboardingUrl = async () => {
|
||||
let res;
|
||||
if ( isSandbox ) {
|
||||
res = await sandboxOnboardingUrl();
|
||||
} else {
|
||||
res = await productionOnboardingUrl( products );
|
||||
}
|
||||
|
||||
if ( res.success && res.data ) {
|
||||
setOnboardingUrl( res.data );
|
||||
} else {
|
||||
console.error( 'Failed to fetch onboarding URL' );
|
||||
}
|
||||
};
|
||||
|
||||
fetchOnboardingUrl();
|
||||
}, [ isSandbox, productionOnboardingUrl, products, sandboxOnboardingUrl ] );
|
||||
|
||||
useEffect( () => {
|
||||
/**
|
||||
* The partner.js script initializes all onboarding buttons in the onload event.
|
||||
* When no buttons are present, a JS error is displayed; i.e. we should load this script
|
||||
* only when the button is ready (with a valid href and data-attributes).
|
||||
*/
|
||||
if ( ! onboardingUrl ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check popup state every 500ms
|
||||
const checkPopup = setInterval( () => {
|
||||
if ( popup.closed ) {
|
||||
clearInterval( checkPopup );
|
||||
resolve( true );
|
||||
}
|
||||
}, 500 );
|
||||
const script = document.createElement( 'script' );
|
||||
script.id = 'partner-js';
|
||||
script.src =
|
||||
'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js';
|
||||
script.onload = () => {
|
||||
setScriptLoaded( true );
|
||||
};
|
||||
document.body.appendChild( script );
|
||||
|
||||
return () => {
|
||||
clearInterval( checkPopup );
|
||||
/**
|
||||
* When the component is unmounted, remove the partner.js script, as well as the
|
||||
* dynamic scripts it loaded (signup-js and rampConfig-js)
|
||||
*
|
||||
* This is important, as the onboarding button is only initialized during the onload
|
||||
* event of those scripts; i.e. we need to load the scripts again, when the button is
|
||||
* rendered again.
|
||||
*/
|
||||
const onboardingScripts = [
|
||||
'partner-js',
|
||||
'signup-js',
|
||||
'rampConfig-js',
|
||||
];
|
||||
|
||||
if ( popup && ! popup.closed ) {
|
||||
popup.close();
|
||||
}
|
||||
onboardingScripts.forEach( ( id ) => {
|
||||
const el = document.querySelector( `script[id="${ id }"]` );
|
||||
|
||||
if ( el?.parentNode ) {
|
||||
el.parentNode.removeChild( el );
|
||||
}
|
||||
} );
|
||||
};
|
||||
} );
|
||||
}, [ onboardingUrl ] );
|
||||
|
||||
return { onboardingUrl, scriptLoaded };
|
||||
};
|
||||
|
||||
const useConnectionBase = () => {
|
||||
|
@ -92,75 +137,15 @@ const useConnectionBase = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const useConnectionAttempt = ( connectFn, errorMessage ) => {
|
||||
const { handleFailed, createErrorNotice, handleCompleted } =
|
||||
useConnectionBase();
|
||||
|
||||
return async ( ...args ) => {
|
||||
const res = await connectFn( ...args );
|
||||
|
||||
if ( ! res.success || ! res.data ) {
|
||||
handleFailed( res, errorMessage );
|
||||
return false;
|
||||
}
|
||||
|
||||
const popupClosed = await handlePopupWithCompletion(
|
||||
res.data,
|
||||
createErrorNotice
|
||||
);
|
||||
|
||||
if ( popupClosed ) {
|
||||
await handleCompleted();
|
||||
}
|
||||
|
||||
return popupClosed;
|
||||
};
|
||||
};
|
||||
|
||||
export const useSandboxConnection = () => {
|
||||
const { sandboxOnboardingUrl, isSandboxMode, setSandboxMode } =
|
||||
CommonHooks.useSandbox();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const connectionAttempt = useConnectionAttempt(
|
||||
sandboxOnboardingUrl,
|
||||
MESSAGES.SANDBOX_ERROR
|
||||
);
|
||||
|
||||
const handleSandboxConnect = async () => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_SANDBOX,
|
||||
'Connecting to sandbox account',
|
||||
connectionAttempt
|
||||
);
|
||||
};
|
||||
const { isSandboxMode, setSandboxMode } = CommonHooks.useSandbox();
|
||||
|
||||
return {
|
||||
handleSandboxConnect,
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProductionConnection = () => {
|
||||
const { productionOnboardingUrl } = CommonHooks.useProduction();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const connectionAttempt = useConnectionAttempt(
|
||||
() => productionOnboardingUrl( products ),
|
||||
MESSAGES.PRODUCTION_ERROR
|
||||
);
|
||||
|
||||
const handleProductionConnect = async () => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_PRODUCTION,
|
||||
'Connecting to production account',
|
||||
connectionAttempt
|
||||
);
|
||||
};
|
||||
|
||||
return { handleProductionConnect };
|
||||
};
|
||||
|
||||
export const useManualConnection = () => {
|
||||
const { handleFailed, handleCompleted, createErrorNotice } =
|
||||
useConnectionBase();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue