Merge pull request #2945 from woocommerce/PCP-4020-review-and-fix-the-sandbox-login

Review and fix the login logic - Frontend changes (4020)
This commit is contained in:
Philipp Stracker 2025-01-09 15:26:05 +01:00 committed by GitHub
commit 689463e243
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1750 additions and 1023 deletions

View file

@ -19,11 +19,11 @@ export default {
// 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',
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
DO_DIRECT_API_AUTHENTICATION: 'COMMON:DO_DIRECT_API_AUTHENTICATION',
DO_OAUTH_AUTHENTICATION: 'COMMON:DO_OAUTH_AUTHENTICATION',
DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES',
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
DO_CHECK_WEBHOOK_SIMULATION_STATE:

View file

@ -123,28 +123,6 @@ export const setManualConnectionMode = ( useManualConnection ) => ( {
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.
*
@ -161,8 +139,12 @@ export const persist = function* () {
*
* @return {Action} The action.
*/
export const connectToSandbox = function* () {
return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
export const sandboxOnboardingUrl = function* () {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: true,
products: [ 'EXPRESS_CHECKOUT' ],
};
};
/**
@ -171,27 +153,65 @@ export const connectToSandbox = function* () {
* @param {string[]} products Which products/features to display in the ISU popup.
* @return {Action} The action.
*/
export const connectToProduction = function* ( products = [] ) {
return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products };
export const productionOnboardingUrl = function* ( products = [] ) {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: false,
products,
};
};
/**
* Side effect. Initiates a manual connection attempt using the provided client ID and secret.
* Side effect. Initiates a direct connection attempt using the provided client ID and secret.
*
* This action accepts parameters instead of fetching data from the Redux state because the
* values (ID and secret) are not managed by a central redux store, but might come from private
* component state.
*
* @param {string} clientId - AP client ID (always 80-characters, starting with "A").
* @param {string} clientSecret - API client secret.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action.
*/
export const connectViaIdAndSecret = function* () {
const { clientId, clientSecret, useSandbox } =
yield select( STORE_NAME ).persistentData();
export const authenticateWithCredentials = function* (
clientId,
clientSecret,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION,
clientId,
clientSecret,
useSandbox,
};
};
/**
* Side effect. Completes the ISU login by authenticating the user via the one time sharedId and
* authCode provided by PayPal.
*
* This action accepts parameters instead of fetching data from the Redux state because all
* parameters are dynamically generated during the authentication process, and not managed by our
* Redux store.
*
* @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID.
* @param {string} authCode - OAuth authorization code provided during onboarding.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action.
*/
export const authenticateWithOAuth = function* (
sharedId,
authCode,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION,
sharedId,
authCode,
useSandbox,
};
};
/**
* Side effect. Clears and refreshes the merchant data via a REST request.
*

View file

@ -35,14 +35,25 @@ export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant';
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
/**
* REST path to perform the manual connection check, using client ID and secret,
* REST path to perform the manual connection authentication, using client ID and secret.
*
* Used by: Controls
* See: ConnectManualRestEndpoint.php
* See: AuthenticateRestEndpoint.php
*
* @type {string}
*/
export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
export const REST_DIRECT_AUTHENTICATION_PATH =
'/wc/v3/wc_paypal/authenticate/direct';
/**
* REST path to perform the ISU authentication check, using shared ID and authCode.
*
* Used by: Controls
* See: AuthenticateRestEndpoint.php
*
* @type {string}
*/
export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu';
/**
* REST path to generate an ISU URL for the PayPal-login.

View file

@ -10,11 +10,12 @@
import apiFetch from '@wordpress/api-fetch';
import {
REST_PERSIST_PATH,
REST_DIRECT_AUTHENTICATION_PATH,
REST_CONNECTION_URL_PATH,
REST_HYDRATE_MERCHANT_PATH,
REST_MANUAL_CONNECTION_PATH,
REST_PERSIST_PATH,
REST_REFRESH_FEATURES_PATH,
REST_ISU_AUTHENTICATION_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} from './constants';
@ -33,15 +34,15 @@ export const controls = {
}
},
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
async [ ACTION_TYPES.DO_GENERATE_ONBOARDING_URL ]( {
products,
useSandbox,
} ) {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
environment: 'sandbox',
products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT.
},
data: { useSandbox, products },
} );
} catch ( e ) {
return {
@ -51,32 +52,14 @@ export const controls = {
}
},
async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
environment: 'production',
products,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
async [ ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION ]( {
clientId,
clientSecret,
useSandbox,
} ) {
try {
return await apiFetch( {
path: REST_MANUAL_CONNECTION_PATH,
path: REST_DIRECT_AUTHENTICATION_PATH,
method: 'POST',
data: {
clientId,
@ -92,6 +75,29 @@ export const controls = {
}
},
async [ ACTION_TYPES.DO_OAUTH_AUTHENTICATION ]( {
sharedId,
authCode,
useSandbox,
} ) {
try {
return await apiFetch( {
path: REST_ISU_AUTHENTICATION_PATH,
method: 'POST',
data: {
sharedId,
authCode,
useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
try {
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );

View file

@ -28,11 +28,10 @@ const useHooks = () => {
persist,
setSandboxMode,
setManualConnectionMode,
setClientId,
setClientSecret,
connectToSandbox,
connectToProduction,
connectViaIdAndSecret,
sandboxOnboardingUrl,
productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
setActiveModal,
startWebhookSimulation,
checkWebhookSimulationState,
@ -43,19 +42,26 @@ const useHooks = () => {
const activeModal = useTransient( 'activeModal' );
// Persistent accessors.
const clientId = usePersistent( 'clientId' );
const clientSecret = usePersistent( 'clientSecret' );
const isSandboxMode = usePersistent( 'useSandbox' );
const isManualConnectionMode = usePersistent( 'useManualConnection' );
const webhooks = usePersistent( 'webhooks' );
const merchant = useSelect(
( select ) => select( STORE_NAME ).merchant(),
[]
);
// Read-only properties.
const wooSettings = useSelect(
( select ) => select( STORE_NAME ).wooSettings(),
[]
);
const features = useSelect(
( select ) => select( STORE_NAME ).features(),
[]
);
const webhooks = useSelect(
( select ) => select( STORE_NAME ).webhooks(),
[]
);
const savePersistent = async ( setter, value ) => {
setter( value );
@ -74,19 +80,13 @@ const useHooks = () => {
setManualConnectionMode: ( state ) => {
return savePersistent( setManualConnectionMode, state );
},
clientId,
setClientId: ( value ) => {
return savePersistent( setClientId, value );
},
clientSecret,
setClientSecret: ( value ) => {
return savePersistent( setClientSecret, value );
},
connectToSandbox,
connectToProduction,
connectViaIdAndSecret,
sandboxOnboardingUrl,
productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
merchant,
wooSettings,
features,
webhooks,
startWebhookSimulation,
checkWebhookSimulationState,
@ -94,36 +94,30 @@ const useHooks = () => {
};
export const useSandbox = () => {
const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks();
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
return { isSandboxMode, setSandboxMode, connectToSandbox };
return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl };
};
export const useProduction = () => {
const { connectToProduction } = useHooks();
const { productionOnboardingUrl } = useHooks();
return { connectToProduction };
return { productionOnboardingUrl };
};
export const useManualConnection = () => {
export const useAuthentication = () => {
const {
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
authenticateWithCredentials,
authenticateWithOAuth,
} = useHooks();
return {
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
authenticateWithCredentials,
authenticateWithOAuth,
};
};
@ -150,7 +144,7 @@ export const useWebhooks = () => {
};
};
export const useMerchantInfo = () => {
const { merchant } = useHooks();
const { merchant, features } = useHooks();
const { refreshMerchantData } = useDispatch( STORE_NAME );
const verifyLoginStatus = useCallback( async () => {
@ -166,6 +160,7 @@ export const useMerchantInfo = () => {
return {
merchant, // Merchant details
features, // Eligible merchant features
verifyLoginStatus, // Callback
};
};

View file

@ -23,20 +23,36 @@ const defaultTransient = Object.freeze( {
isSandbox: false,
id: '',
email: '',
clientId: '',
clientSecret: '',
} ),
wooSettings: Object.freeze( {
storeCountry: '',
storeCurrency: '',
} ),
features: Object.freeze( {
save_paypal_and_venmo: {
enabled: false,
},
advanced_credit_and_debit_cards: {
enabled: false,
},
apple_pay: {
enabled: false,
},
google_pay: {
enabled: false,
},
} ),
webhooks: Object.freeze( [] ),
} );
const defaultPersistent = Object.freeze( {
useSandbox: false,
useManualConnection: false,
clientId: '',
clientSecret: '',
webhooks: [],
} );
// Reducer logic.
@ -84,22 +100,25 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( {
...state,
merchant: Object.freeze( { ...defaultTransient.merchant } ),
features: Object.freeze( { ...defaultTransient.features } ),
} ),
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
const newState = setPersistent( state, payload.data );
// Populate read-only properties.
[ 'wooSettings', 'merchant' ].forEach( ( key ) => {
if ( ! payload[ key ] ) {
return;
}
[ 'wooSettings', 'merchant', 'features', 'webhooks' ].forEach(
( key ) => {
if ( ! payload[ key ] ) {
return;
}
newState[ key ] = Object.freeze( {
...newState[ key ],
...payload[ key ],
} );
} );
newState[ key ] = Object.freeze( {
...newState[ key ],
...payload[ key ],
} );
}
);
return newState;
},

View file

@ -16,8 +16,14 @@ export const persistentData = ( state ) => {
};
export const transientData = ( state ) => {
const { data, merchant, wooSettings, ...transientState } =
getState( state );
const {
data,
merchant,
features,
wooSettings,
webhooks,
...transientState
} = getState( state );
return transientState || EMPTY_OBJ;
};
@ -30,6 +36,10 @@ export const merchant = ( state ) => {
return getState( state ).merchant || EMPTY_OBJ;
};
export const features = ( state ) => {
return getState( state ).features || EMPTY_OBJ;
};
export const wooSettings = ( state ) => {
return getState( state ).wooSettings || EMPTY_OBJ;
};

View file

@ -47,6 +47,28 @@ export const setIsReady = ( isReady ) => ( {
payload: { isReady },
} );
/**
* Transient. Sets the "manualClientId" value.
*
* @param {string} manualClientId
* @return {Action} The action.
*/
export const setManualClientId = ( manualClientId ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { manualClientId },
} );
/**
* Transient. Sets the "manualClientSecret" value.
*
* @param {string} manualClientSecret
* @return {Action} The action.
*/
export const setManualClientSecret = ( manualClientSecret ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { manualClientSecret },
} );
/**
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
*

View file

@ -30,6 +30,8 @@ const useHooks = () => {
setStep,
setCompleted,
setIsCasualSeller,
setManualClientId,
setManualClientSecret,
setAreOptionalPaymentMethodsEnabled,
setProducts,
} = useDispatch( STORE_NAME );
@ -43,6 +45,8 @@ const useHooks = () => {
// Transient accessors.
const isReady = useTransient( 'isReady' );
const manualClientId = useTransient( 'manualClientId' );
const manualClientSecret = useTransient( 'manualClientSecret' );
// Persistent accessors.
const step = usePersistent( 'step' );
@ -73,6 +77,14 @@ const useHooks = () => {
setIsCasualSeller: ( value ) => {
return savePersistent( setIsCasualSeller, value );
},
manualClientId,
setManualClientId: ( value ) => {
return savePersistent( setManualClientId, value );
},
manualClientSecret,
setManualClientSecret: ( value ) => {
return savePersistent( setManualClientSecret, value );
},
areOptionalPaymentMethodsEnabled,
setAreOptionalPaymentMethodsEnabled: ( value ) => {
return savePersistent( setAreOptionalPaymentMethodsEnabled, value );
@ -88,6 +100,22 @@ const useHooks = () => {
};
};
export const useManualConnectionForm = () => {
const {
manualClientId,
setManualClientId,
manualClientSecret,
setManualClientSecret,
} = useHooks();
return {
manualClientId,
setManualClientId,
manualClientSecret,
setManualClientSecret,
};
};
export const useBusiness = () => {
const { isCasualSeller, setIsCasualSeller } = useHooks();

View file

@ -14,6 +14,8 @@ import ACTION_TYPES from './action-types';
const defaultTransient = Object.freeze( {
isReady: false,
manualClientId: '',
manualClientSecret: '',
// Read only values, provided by the server.
flags: Object.freeze( {

View file

@ -52,9 +52,6 @@ export const determineProducts = ( state ) => {
* The store uses the Express-checkout product.
*/
derivedProducts.push( 'EXPRESS_CHECKOUT' );
// TODO: Add the "BCDC" product/feature
// Requirement: "EXPRESS_CHECKOUT with BCDC"
} else {
/**
* Branch 3: Merchant is business, and can use CC payments.
@ -64,8 +61,7 @@ export const determineProducts = ( state ) => {
}
if ( canUseVaulting ) {
// TODO: Add the "Vaulting" product/feature
// Requirement: "... with Vault"
derivedProducts.push( 'ADVANCED_VAULTING' );
}
return derivedProducts;