feat(webapp): popup Plaid Link component

This commit is contained in:
Ahmed Bouhuolia
2024-01-30 23:09:29 +02:00
parent b9886cfac3
commit b43cd26ecc
7 changed files with 540 additions and 4 deletions

View File

@@ -0,0 +1,121 @@
import React, { useEffect } from 'react';
import {
usePlaidLink,
PlaidLinkOnSuccessMetadata,
PlaidLinkOnExitMetadata,
PlaidLinkError,
PlaidLinkOptionsWithLinkToken,
PlaidLinkOnEventMetadata,
PlaidLinkStableEvent,
} from 'react-plaid-link';
import { useHistory } from 'react-router-dom';
// import { exchangeToken, setItemState } from '../services/api';
// import { useItems, useLink, useErrors } from '../services';
interface LaunchLinkProps {
isOauth?: boolean;
token: string;
userId: number;
itemId?: number | null;
children?: React.ReactNode;
}
// Uses the usePlaidLink hook to manage the Plaid Link creation. See https://github.com/plaid/react-plaid-link for full usage instructions.
// The link token passed to usePlaidLink cannot be null. It must be generated outside of this component. In this sample app, the link token
// is generated in the link context in client/src/services/link.js.
export function LaunchLink(props: LaunchLinkProps) {
const history = useHistory();
// const { getItemsByUser, getItemById } = useItems();
// const { generateLinkToken, deleteLinkToken } = useLink();
// const { setError, resetError } = useErrors();
// define onSuccess, onExit and onEvent functions as configs for Plaid Link creation
const onSuccess = async (
publicToken: string,
metadata: PlaidLinkOnSuccessMetadata,
) => {
// log and save metatdata
// logSuccess(metadata, props.userId);
if (props.itemId != null) {
// update mode: no need to exchange public token
// await setItemState(props.itemId, 'good');
// deleteLinkToken(null, props.itemId);
// getItemById(props.itemId, true);
// regular link mode: exchange public token for access token
} else {
// call to Plaid api endpoint: /item/public_token/exchange in order to obtain access_token which is then stored with the created item
// await exchangeToken(
// publicToken,
// metadata.institution,
// metadata.accounts,
// props.userId,
// );
// getItemsByUser(props.userId, true);
}
// resetError();
// deleteLinkToken(props.userId, null);
history.push(`/user/${props.userId}`);
};
const onExit = async (
error: PlaidLinkError | null,
metadata: PlaidLinkOnExitMetadata,
) => {
// log and save error and metatdata
// logExit(error, metadata, props.userId);
if (error != null && error.error_code === 'INVALID_LINK_TOKEN') {
// await generateLinkToken(props.userId, props.itemId);
}
if (error != null) {
// setError(error.error_code, error.display_message || error.error_message);
}
// to handle other error codes, see https://plaid.com/docs/errors/
};
const onEvent = async (
eventName: PlaidLinkStableEvent | string,
metadata: PlaidLinkOnEventMetadata,
) => {
// handle errors in the event end-user does not exit with onExit function error enabled.
if (eventName === 'ERROR' && metadata.error_code != null) {
// setError(metadata.error_code, ' ');
}
// logEvent(eventName, metadata);
};
const config: PlaidLinkOptionsWithLinkToken = {
onSuccess,
onExit,
onEvent,
token: props.token,
};
if (props.isOauth) {
// add additional receivedRedirectUri config when handling an OAuth reidrect
config.receivedRedirectUri = window.location.href;
}
const { open, ready } = usePlaidLink(config);
useEffect(() => {
// initiallizes Link automatically
if (props.isOauth && ready) {
open();
} else if (ready) {
// regular, non-OAuth case:
// set link token, userId and itemId in local storage for use if needed later by OAuth
localStorage.setItem(
'oauthConfig',
JSON.stringify({
userId: props.userId,
itemId: props.itemId,
token: props.token,
}),
);
open();
}
}, [ready, open, props.isOauth, props.userId, props.itemId, props.token]);
return <></>;
}

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React from 'react';
import React, { useState } from 'react';
import {
Button,
NavbarGroup,
@@ -14,7 +14,10 @@ import {
Icon,
FormattedMessage as T,
} from '@/components';
import { useRefreshCashflowAccounts } from '@/hooks/query';
import {
useGetPlaidLinkToken,
useRefreshCashflowAccounts,
} from '@/hooks/query';
import { CashflowAction, AbilitySubject } from '@/constants/abilityOption';
import withDialogActions from '@/containers/Dialog/withDialogActions';
@@ -26,6 +29,7 @@ import { ACCOUNT_TYPE } from '@/constants';
import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
import { LaunchLink } from '@/containers/Banking/Plaid/PlaidLanchLink';
/**
* Cash Flow accounts actions bar.
@@ -63,8 +67,20 @@ function CashFlowAccountsActionsBar({
setCashflowAccountsTableState({ inactiveMode: checked });
};
const { mutateAsync: getPlaidLinkToken } = useGetPlaidLinkToken();
const [linkToken, setLinkToken] = useState<string>('');
const handleConnectToBank = () => {
getPlaidLinkToken()
.then((res) => {
setLinkToken(res.data.link_token);
})
.catch(() => {});
};
return (
<DashboardActionsBar>
<LaunchLink userId={3} token={linkToken} />
<NavbarGroup>
<Can I={CashflowAction.Create} a={AbilitySubject.Cashflow}>
<Button
@@ -104,6 +120,12 @@ function CashFlowAccountsActionsBar({
onChange={handleInactiveSwitchChange}
/>
</Can>
<Button
className={Classes.MINIMAL}
text={'Connect to Bank'}
onClick={handleConnectToBank}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>

View File

@@ -37,3 +37,4 @@ export * from './transactionsLocking';
export * from './warehouses';
export * from './branches';
export * from './warehousesTransfers';
export * from './plaid';

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import { useMutation } from 'react-query';
import useApiRequest from '../useRequest';
/**
* Retrieves the plaid link token.
*/
export function useGetPlaidLinkToken(props) {
const apiRequest = useApiRequest();
return useMutation(
() => apiRequest.post('banking/plaid/link-token', {}, {}),
{
...props,
},
);
}