Merge pull request #3097 from woocommerce/PCP-4183-manual-connection-credentials-are-not-working

Manual Connection credentials are not working (4183)
This commit is contained in:
Emili Castells 2025-02-12 16:47:28 +01:00 committed by GitHub
commit 8258c1c993
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 87 additions and 46 deletions

View file

@ -18,6 +18,7 @@ const DataStoreControl = React.forwardRef(
control: ControlComponent, control: ControlComponent,
value: externalValue, value: externalValue,
onChange, onChange,
onConfirm = null,
delay = 300, delay = 300,
...props ...props
}, },
@ -25,7 +26,9 @@ const DataStoreControl = React.forwardRef(
) => { ) => {
const [ internalValue, setInternalValue ] = useState( externalValue ); const [ internalValue, setInternalValue ] = useState( externalValue );
const onChangeRef = useRef( onChange ); const onChangeRef = useRef( onChange );
const onConfirmRef = useRef( onConfirm );
onChangeRef.current = onChange; onChangeRef.current = onChange;
onConfirmRef.current = onConfirm;
const debouncedUpdate = useRef( const debouncedUpdate = useRef(
debounce( ( value ) => { debounce( ( value ) => {
@ -36,7 +39,7 @@ const DataStoreControl = React.forwardRef(
useEffect( () => { useEffect( () => {
setInternalValue( externalValue ); setInternalValue( externalValue );
debouncedUpdate?.cancel(); debouncedUpdate?.cancel();
}, [ externalValue ] ); }, [ debouncedUpdate, externalValue ] );
useEffect( () => { useEffect( () => {
return () => debouncedUpdate?.cancel(); return () => debouncedUpdate?.cancel();
@ -50,12 +53,25 @@ const DataStoreControl = React.forwardRef(
[ debouncedUpdate ] [ debouncedUpdate ]
); );
const handleKeyDown = useCallback(
( event ) => {
if ( onConfirmRef.current && event.key === 'Enter' ) {
event.preventDefault();
debouncedUpdate.flush();
onConfirmRef.current();
return false;
}
},
[ debouncedUpdate ]
);
return ( return (
<ControlComponent <ControlComponent
ref={ ref } ref={ ref }
{ ...props } { ...props }
value={ internalValue } value={ internalValue }
onChange={ handleChange } onChange={ handleChange }
onKeyDown={ handleKeyDown }
/> />
); );
} }

View file

@ -157,6 +157,7 @@ const ManualConnectionForm = () => {
label={ clientIdLabel } label={ clientIdLabel }
value={ manualClientId } value={ manualClientId }
onChange={ setManualClientId } onChange={ setManualClientId }
onConfirm={ handleManualConnect }
className={ classNames( { className={ classNames( {
'ppcp--has-error': ! clientValid, 'ppcp--has-error': ! clientValid,
} ) } } ) }
@ -173,6 +174,7 @@ const ManualConnectionForm = () => {
label={ secretKeyLabel } label={ secretKeyLabel }
value={ manualClientSecret } value={ manualClientSecret }
onChange={ setManualClientSecret } onChange={ setManualClientSecret }
onConfirm={ handleManualConnect }
type="password" type="password"
/> />
<Button <Button

View file

@ -12,6 +12,7 @@ export default {
SET_PERSISTENT: 'ppcp/common/SET_PERSISTENT', SET_PERSISTENT: 'ppcp/common/SET_PERSISTENT',
RESET: 'ppcp/common/RESET', RESET: 'ppcp/common/RESET',
HYDRATE: 'ppcp/common/HYDRATE', HYDRATE: 'ppcp/common/HYDRATE',
SET_MERCHANT: 'ppcp/common/SET_MERCHANT',
RESET_MERCHANT: 'ppcp/common/RESET_MERCHANT', RESET_MERCHANT: 'ppcp/common/RESET_MERCHANT',
// Activity management (advanced solution that replaces the isBusy state). // Activity management (advanced solution that replaces the isBusy state).

View file

@ -110,6 +110,17 @@ export const setManualConnectionMode = ( useManualConnection ) =>
export const setWebhooks = ( webhooks ) => export const setWebhooks = ( webhooks ) =>
setPersistent( 'webhooks', webhooks ); setPersistent( 'webhooks', webhooks );
/**
* Replace merchant details in the store.
*
* @param {Object} merchant - The new merchant details.
* @return {Action} The action.
*/
export const setMerchant = ( merchant ) => ( {
type: ACTION_TYPES.SET_MERCHANT,
payload: { merchant },
} );
/** /**
* Reset merchant details in the store. * Reset merchant details in the store.
* *

View file

@ -141,18 +141,27 @@ export const useWebhooks = () => {
export const useMerchantInfo = () => { export const useMerchantInfo = () => {
const { isReady, features } = useHooks(); const { isReady, features } = useHooks();
const merchant = useMerchant(); const merchant = useMerchant();
const { refreshMerchantData } = useDispatch( STORE_NAME ); const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME );
const verifyLoginStatus = useCallback( async () => { const verifyLoginStatus = useCallback( async () => {
const result = await refreshMerchantData(); const result = await refreshMerchantData();
if ( ! result.success ) { if ( ! result.success || ! result.merchant ) {
throw new Error( result?.message || result?.error?.message ); throw new Error( result?.message || result?.error?.message );
} }
const newMerchant = result.merchant;
// Verify if the server state is "connected" and we have a merchant ID. // Verify if the server state is "connected" and we have a merchant ID.
return merchant?.isConnected && merchant?.id; if ( newMerchant?.isConnected && newMerchant?.id ) {
}, [ refreshMerchantData, merchant ] ); // Update the verified merchant details in Redux.
setMerchant( newMerchant );
return true;
}
return false;
}, [ refreshMerchantData, setMerchant ] );
return { return {
isReady, isReady,
@ -225,6 +234,8 @@ export const useBusyState = () => {
); );
return { return {
startActivity,
stopActivity,
withActivity, // HOC withActivity, // HOC
isBusy, // Boolean. isBusy, // Boolean.
}; };

View file

@ -114,6 +114,10 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
features: Object.freeze( { ...defaultTransient.features } ), features: Object.freeze( { ...defaultTransient.features } ),
} ), } ),
[ ACTION_TYPES.SET_MERCHANT ]: ( state, payload ) => {
return changePersistent( state, { merchant: payload.merchant } );
},
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
const newState = changePersistent( state, payload.data ); const newState = changePersistent( state, payload.data );

View file

@ -10,19 +10,7 @@ const PAYPAL_PARTNER_SDK_URL =
const MESSAGES = { const MESSAGES = {
CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ), CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ),
POPUP_BLOCKED: __( API_ERROR: __(
'Popup blocked. Please allow popups for this site to connect to PayPal.',
'woocommerce-paypal-payments'
),
SANDBOX_ERROR: __(
'Could not generate a Sandbox login link.',
'woocommerce-paypal-payments'
),
PRODUCTION_ERROR: __(
'Could not generate a login link.',
'woocommerce-paypal-payments'
),
MANUAL_ERROR: __(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
@ -33,17 +21,16 @@ const MESSAGES = {
}; };
const ACTIVITIES = { const ACTIVITIES = {
CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX', OAUTH_VERIFY: 'oauth/login',
CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION', API_LOGIN: 'auth/api-login',
CONNECT_ISU: 'ISU_LOGIN', API_VERIFY: 'auth/verify-login',
CONNECT_MANUAL: 'MANUAL_LOGIN',
}; };
export const useHandleOnboardingButton = ( isSandbox ) => { export const useHandleOnboardingButton = ( isSandbox ) => {
const { sandboxOnboardingUrl } = CommonHooks.useSandbox(); const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
const { productionOnboardingUrl } = CommonHooks.useProduction(); const { productionOnboardingUrl } = CommonHooks.useProduction();
const products = OnboardingHooks.useDetermineProducts(); const products = OnboardingHooks.useDetermineProducts();
const { withActivity } = CommonHooks.useBusyState(); const { withActivity, startActivity } = CommonHooks.useBusyState();
const { authenticateWithOAuth } = CommonHooks.useAuthentication(); const { authenticateWithOAuth } = CommonHooks.useAuthentication();
const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
const [ scriptLoaded, setScriptLoaded ] = useState( false ); const [ scriptLoaded, setScriptLoaded ] = useState( false );
@ -123,16 +110,15 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
* frame before the REST endpoint returns a value. Using "withActivity" is more of a * frame before the REST endpoint returns a value. Using "withActivity" is more of a
* visual cue to the user that something is still processing in the background. * visual cue to the user that something is still processing in the background.
*/ */
await withActivity( startActivity(
ACTIVITIES.CONNECT_ISU, ACTIVITIES.OAUTH_VERIFY,
'Validating the connection details', 'Validating the connection details'
async () => { );
await authenticateWithOAuth(
sharedId, await authenticateWithOAuth(
authCode, sharedId,
'sandbox' === environment authCode,
); 'sandbox' === environment
}
); );
}; };
@ -168,11 +154,13 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
}; };
}; };
// Base connection is only used for API login (manual connection).
const useConnectionBase = () => { const useConnectionBase = () => {
const { setCompleted } = OnboardingHooks.useSteps(); const { setCompleted } = OnboardingHooks.useSteps();
const { createSuccessNotice, createErrorNotice } = const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore ); useDispatch( noticesStore );
const { verifyLoginStatus } = CommonHooks.useMerchantInfo(); const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
const { withActivity } = CommonHooks.useBusyState();
return { return {
handleFailed: ( res, genericMessage ) => { handleFailed: ( res, genericMessage ) => {
@ -180,18 +168,26 @@ const useConnectionBase = () => {
createErrorNotice( res?.message ?? genericMessage ); createErrorNotice( res?.message ?? genericMessage );
}, },
handleCompleted: async () => { handleCompleted: async () => {
try { await withActivity(
const loginSuccessful = await verifyLoginStatus(); ACTIVITIES.API_VERIFY,
'Verifying Authentication',
async () => {
try {
const loginSuccessful = await verifyLoginStatus();
if ( loginSuccessful ) { if ( loginSuccessful ) {
createSuccessNotice( MESSAGES.CONNECTED ); createSuccessNotice( MESSAGES.CONNECTED );
await setCompleted( true ); await setCompleted( true );
} else { } else {
createErrorNotice( MESSAGES.LOGIN_FAILED ); createErrorNotice( MESSAGES.LOGIN_FAILED );
}
} catch ( error ) {
createErrorNotice(
error.message ?? MESSAGES.LOGIN_FAILED
);
}
} }
} catch ( error ) { );
createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED );
}
}, },
createErrorNotice, createErrorNotice,
}; };
@ -218,7 +214,7 @@ export const useDirectAuthentication = () => {
const handleDirectAuthentication = async ( connectionDetails ) => { const handleDirectAuthentication = async ( connectionDetails ) => {
return withActivity( return withActivity(
ACTIVITIES.CONNECT_MANUAL, ACTIVITIES.API_LOGIN,
'Connecting manually via Client ID and Secret', 'Connecting manually via Client ID and Secret',
async () => { async () => {
let data; let data;
@ -250,7 +246,7 @@ export const useDirectAuthentication = () => {
if ( res.success ) { if ( res.success ) {
await handleCompleted(); await handleCompleted();
} else { } else {
handleFailed( res, MESSAGES.MANUAL_ERROR ); handleFailed( res, MESSAGES.API_ERROR );
} }
return res.success; return res.success;

View file

@ -175,7 +175,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
} }
$account = $this->authentication_manager->get_account_details(); $account = $this->authentication_manager->get_account_details();
$response = $this->sanitize_for_javascript( $this->response_map, $account ); $response = $this->sanitize_for_javascript( $account, $this->response_map );
return $this->return_success( $response ); return $this->return_success( $response );
} }