Skip to content

Commit

Permalink
Tidying up and improving the React logic for Stripe components (#9425)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmallory42 committed Sep 12, 2024
1 parent d4b6ffc commit 6631488
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 161 deletions.
4 changes: 4 additions & 0 deletions changelog/dev-session-fixes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: dev

Some refactors to embedded KYC logic.
254 changes: 109 additions & 145 deletions client/onboarding/steps/embedded-kyc.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,40 @@
/**
* External dependencies
*/
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
loadConnectAndInitialize,
StripeConnectInstance,
} from '@stripe/connect-js';
import { LoadError } from '@stripe/connect-js/types/config';
import {
ConnectAccountOnboarding,
ConnectComponentsProvider,
} from '@stripe/react-connect-js';
import apiFetch from '@wordpress/api-fetch';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';

/**
* Internal dependencies
*/
import { NAMESPACE } from 'data/constants';
import appearance from '../kyc/appearance';
import BannerNotice from 'wcpay/components/banner-notice';
import LoadBar from 'wcpay/components/load-bar';
import { useOnboardingContext } from 'wcpay/onboarding/context';
import {
AccountKycSession,
PoEligibleData,
PoEligibleResult,
} from 'wcpay/onboarding/types';
import { fromDotNotation } from 'wcpay/onboarding/utils';
createAccountSession,
finalizeOnboarding,
isPoEligible,
} from 'wcpay/onboarding/utils';
import { getConnectUrl, getOverviewUrl } from 'wcpay/utils';

type AccountKycSessionData = AccountKycSession;

interface FinalizeResponse {
success: boolean;
params: Record< string, string >;
}

interface Props {
continueKyc?: boolean;
}

// TODO: extract this logic and move it to a generic component to be used for all embedded components, not just onboarding.
const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => {
const { data } = useOnboardingContext();
const [ publishableKey, setPublishableKey ] = useState( '' );
const [ locale, setLocale ] = useState( '' );
const [ publishableKey, setPublishableKey ] = useState( '' );
const [ clientSecret, setClientSecret ] = useState<
( () => Promise< string > ) | null
>( null );
Expand All @@ -55,101 +44,81 @@ const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => {
] = useState< StripeConnectInstance | null >( null );
const [ loading, setLoading ] = useState( true );
const [ loadErrorMessage, setLoadErrorMessage ] = useState( '' );
const onLoaderStart = () => {
setLoading( false );
};
const onLoadError = ( loadError: LoadError ) => {
setLoadErrorMessage( loadError.error.message || 'Unknown error' );
};

useEffect( () => {
const isEligibleForPo = async () => {
if (
! data.country ||
! data.business_type ||
! data.mcc ||
! data.annual_revenue ||
! data.go_live_timeframe
) {
return false;
const fetchAccountSession = useCallback( async () => {
try {
const isEligible = ! continueKyc && ( await isPoEligible( data ) );
const accountSession = await createAccountSession(
data,
isEligible
);
if ( accountSession && accountSession.clientSecret ) {
return accountSession; // Return the full account session object
}
const eligibilityDetails: PoEligibleData = {
business: {
country: data.country,
type: data.business_type,
mcc: data.mcc,
},
store: {
annual_revenue: data.annual_revenue,
go_live_timeframe: data.go_live_timeframe,
},
};

try {
const eligibleResult = await apiFetch< PoEligibleResult >( {
path: '/wc/v3/payments/onboarding/router/po_eligible',
method: 'POST',
data: eligibilityDetails,
} );

return 'eligible' === eligibleResult.result;
} catch ( error ) {
// Fall back to full KYC scenario.
return false;
}
};
setLoading( false );
setLoadErrorMessage(
__(
"Failed to create account session. Please check that you're using the latest version of WooPayments.",
'woocommerce-payments'
)
);
} catch ( error ) {
setLoading( false );
setLoadErrorMessage(
__(
'Failed to retrieve account session. Please try again later.',
'woocommerce-payments'
)
);
}

const fetchKeys = async () => {
// By default, we assume the merchant is not eligible for PO.
let isEligible = false;
// Return null if an error occurred.
return null;
}, [ continueKyc, data ] );

// If we are resuming an onboarding session, we don't need to check for PO eligibility again.
if ( ! continueKyc ) {
isEligible = await isEligibleForPo();
}
// Function to fetch clientSecret for use in Stripe auto-refresh or initialization
const fetchClientSecret = useCallback( async () => {
const accountSession = await fetchAccountSession();
if ( accountSession ) {
return accountSession.clientSecret; // Only return the clientSecret
}
throw new Error( 'Error fetching the client secret' );
}, [ fetchAccountSession ] );

const path = addQueryArgs(
`${ NAMESPACE }/onboarding/kyc/session`,
{
self_assessment: fromDotNotation( data ),
progressive: isEligible,
// Effect to fetch the publishable key and clientSecret on initial render
useEffect( () => {
const fetchKeys = async () => {
try {
const accountSession = await fetchAccountSession();
if ( accountSession ) {
setLocale( accountSession.locale );
setPublishableKey( accountSession.publishableKey );
setClientSecret( () => fetchClientSecret );
}
);
const accountSession = await apiFetch< AccountKycSessionData >( {
path: path,
method: 'GET',
} );
if (
accountSession.publishableKey &&
accountSession.clientSecret
) {
setPublishableKey( accountSession.publishableKey );
setLocale( accountSession.locale );
setClientSecret( () => () =>
Promise.resolve( accountSession.clientSecret )
); // Ensure clientSecret is wrapped as a function returning a Promise
} else {
} catch ( error ) {
setLoading( false );
setLoadErrorMessage(
__(
"Failed to create account session. Please check that you're using the latest version of WooPayments.",
'Failed to create account session. Please check that you are using the latest version of WooPayments.',
'woocommerce-payments'
)
);
} finally {
setLoading( false );
}
};

fetchKeys();
}, [ data, continueKyc ] );
}, [ data, continueKyc, fetchAccountSession, fetchClientSecret ] );

// Initialize the Stripe Connect instance only once when publishableKey and clientSecret are ready
// Effect to initialize the Stripe Connect instance once publishableKey and clientSecret are ready.
useEffect( () => {
if ( publishableKey && clientSecret && ! stripeConnectInstance ) {
const stripeInstance = loadConnectAndInitialize( {
publishableKey: publishableKey,
fetchClientSecret: clientSecret, // Pass the function returning the Promise<string>
publishableKey,
fetchClientSecret,
appearance: {
// See all possible variables below
overlays: 'drawer',
variables: appearance.variables,
},
Expand All @@ -158,7 +127,48 @@ const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => {

setStripeConnectInstance( stripeInstance );
}
}, [ publishableKey, clientSecret, stripeConnectInstance, locale ] );
}, [
publishableKey,
clientSecret,
stripeConnectInstance,
fetchClientSecret,
locale,
] );

const handleOnExit = async () => {
const urlParams = new URLSearchParams( window.location.search );
const urlSource =
urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) || 'unknown';

try {
const response = await finalizeOnboarding( urlSource );
if ( response.success ) {
window.location.href = getOverviewUrl(
{
...response.params,
'wcpay-connection-success': '1',
},
'WCPAY_ONBOARDING_WIZARD'
);
} else {
window.location.href = getConnectUrl(
{
...response.params,
'wcpay-connection-error': '1',
},
'WCPAY_ONBOARDING_WIZARD'
);
}
} catch ( error ) {
window.location.href = getConnectUrl(
{
'wcpay-connection-error': '1',
source: urlSource,
},
'WCPAY_ONBOARDING_WIZARD'
);
}
};

return (
<>
Expand All @@ -171,59 +181,13 @@ const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => {
connectInstance={ stripeConnectInstance }
>
<ConnectAccountOnboarding
onLoaderStart={ onLoaderStart }
onLoadError={ onLoadError }
onExit={ async () => {
const urlParams = new URLSearchParams(
window.location.search
);
const urlSource =
urlParams
.get( 'source' )
?.replace( /[^\w-]+/g, '' ) || 'unknown';
try {
const response = await apiFetch<
FinalizeResponse
>( {
path: `${ NAMESPACE }/onboarding/kyc/finalize`,
method: 'POST',
data: {
source: urlSource,
from: 'WCPAY_ONBOARDING_WIZARD',
clientSecret: clientSecret,
},
} );

if ( response.success ) {
window.location.href = getOverviewUrl(
{
...response.params,
'wcpay-connection-success': '1',
},
'WCPAY_ONBOARDING_WIZARD'
);
} else {
// If a non-success response is received we should redirect to the Connect page with an error flag:
window.location.href = getConnectUrl(
{
...response.params,
'wcpay-connection-error': '1',
},
'WCPAY_ONBOARDING_WIZARD'
);
}
} catch ( error ) {
// If an error response is received we should redirect to the Connect page with an error flag:
// Note that this should never happen, since we always expect a response from the server.
window.location.href = getConnectUrl(
{
'wcpay-connection-error': '1',
source: urlSource,
},
'WCPAY_ONBOARDING_WIZARD'
);
}
} }
onLoaderStart={ () => setLoading( false ) }
onLoadError={ ( loadError ) =>
setLoadErrorMessage(
loadError.error.message || 'Unknown error'
)
}
onExit={ handleOnExit }
/>
</ConnectComponentsProvider>
) }
Expand Down
4 changes: 2 additions & 2 deletions client/onboarding/steps/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies
*/
import { useOnboardingContext } from '../context';
import { PoEligibleData, PoEligibleResult } from '../types';
import { PoEligibleData, PoEligibleResponse } from '../types';
import { fromDotNotation } from '../utils';
import { trackRedirected, useTrackAbandoned } from '../tracking';
import LoadBar from 'components/load-bar';
Expand Down Expand Up @@ -45,7 +45,7 @@ const LoadingStep: React.FC< Props > = () => {
go_live_timeframe: data.go_live_timeframe,
},
};
const eligibleResult = await apiFetch< PoEligibleResult >( {
const eligibleResult = await apiFetch< PoEligibleResponse >( {
path: '/wc/v3/payments/onboarding/router/po_eligible',
method: 'POST',
data: eligibilityDetails,
Expand Down
16 changes: 6 additions & 10 deletions client/onboarding/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,7 @@ export type OnboardingFields = {
go_live_timeframe?: string;
};

export interface OnboardingProps {
country?: string;
type?: string;
structure?: string;
mcc?: string;
annual_revenue?: string;
go_live_timeframe?: string;
}

export interface PoEligibleResult {
export interface PoEligibleResponse {
result: 'eligible' | 'not_eligible';
}

Expand Down Expand Up @@ -74,3 +65,8 @@ export interface AccountKycSession {
publishableKey: string;
locale: string;
}

export interface FinalizeOnboardingResponse {
success: boolean;
params: Record< string, string >;
}
Loading

0 comments on commit 6631488

Please sign in to comment.