From b61fececc7b698776bc65a115e241b913528c5a0 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 15:18:21 -0600 Subject: [PATCH] teller front end progress --- apps/server/src/app/lib/endpoint.ts | 1 + apps/workers/src/app/lib/di.ts | 1 + apps/workers/src/main.ts | 13 ++++ .../accounts-manager/AccountTypeSelector.tsx | 38 +++++++++- libs/client/shared/src/hooks/index.ts | 1 - libs/client/shared/src/hooks/useTeller.ts | 71 ------------------- .../src/providers/AccountContextProvider.tsx | 1 + libs/client/shared/src/utils/index.ts | 1 + libs/client/shared/src/utils/teller-utils.ts | 39 ++++++++++ .../src/institution/institution.service.ts | 2 +- .../src/providers/teller/teller.service.ts | 19 ++--- .../shared/src/services/queue.service.ts | 2 +- libs/teller-api/src/teller-api.ts | 4 +- libs/teller-api/src/types/institutions.ts | 4 +- .../migration.sql | 2 + prisma/schema.prisma | 1 + 16 files changed, 109 insertions(+), 91 deletions(-) delete mode 100644 libs/client/shared/src/hooks/useTeller.ts create mode 100644 libs/client/shared/src/utils/teller-utils.ts create mode 100644 prisma/migrations/20240116185600_add_teller_provider/migration.sql diff --git a/apps/server/src/app/lib/endpoint.ts b/apps/server/src/app/lib/endpoint.ts index 8e01b00d..f6340672 100644 --- a/apps/server/src/app/lib/endpoint.ts +++ b/apps/server/src/app/lib/endpoint.ts @@ -240,6 +240,7 @@ const userService = new UserService( const institutionProviderFactory = new InstitutionProviderFactory({ PLAID: plaidService, FINICITY: finicityService, + TELLER: tellerService, }) const institutionService: IInstitutionService = new InstitutionService( diff --git a/apps/workers/src/app/lib/di.ts b/apps/workers/src/app/lib/di.ts index 987ebedd..9e6d68c5 100644 --- a/apps/workers/src/app/lib/di.ts +++ b/apps/workers/src/app/lib/di.ts @@ -259,6 +259,7 @@ export const securityPricingProcessor: ISecurityPricingProcessor = new SecurityP const institutionProviderFactory = new InstitutionProviderFactory({ PLAID: plaidService, FINICITY: finicityService, + TELLER: tellerService, }) export const institutionService: IInstitutionService = new InstitutionService( diff --git a/apps/workers/src/main.ts b/apps/workers/src/main.ts index 35c16cdd..7db31bb0 100644 --- a/apps/workers/src/main.ts +++ b/apps/workers/src/main.ts @@ -115,6 +115,11 @@ syncInstitutionQueue.process( async () => await institutionService.sync('FINICITY') ) +syncInstitutionQueue.process( + 'sync-teller-institutions', + async () => await institutionService.sync('TELLER') +) + syncInstitutionQueue.add( 'sync-plaid-institutions', {}, @@ -131,6 +136,14 @@ syncInstitutionQueue.add( } ) +syncInstitutionQueue.add( + 'sync-teller-institutions', + {}, + { + repeat: { cron: '* */24 * * *' }, // Run every 24 hours + } +) + /** * send-email queue */ diff --git a/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx b/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx index 2b7c81f8..d3637783 100644 --- a/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx +++ b/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useMemo } from 'react' import { RiFolderLine, RiHandCoinLine, RiLockLine, RiSearchLine } from 'react-icons/ri' import maxBy from 'lodash/maxBy' import { @@ -8,11 +8,13 @@ import { usePlaid, useFinicity, } from '@maybe-finance/client/shared' - import { Input } from '@maybe-finance/design-system' import InstitutionGrid from './InstitutionGrid' import { AccountTypeGrid } from './AccountTypeGrid' import InstitutionList, { MIN_QUERY_LENGTH } from './InstitutionList' +import { useLogger } from '@maybe-finance/client/shared' +import { BrowserUtil } from '@maybe-finance/client/shared' +import { useTellerConnect, type TellerConnectOptions } from 'teller-connect-react' const SEARCH_DEBOUNCE_MS = 300 @@ -23,18 +25,27 @@ export default function AccountTypeSelector({ view: string onViewChange: (view: string) => void }) { + const logger = useLogger() const { setAccountManager } = useAccountContext() const [searchQuery, setSearchQuery] = useState('') const debouncedSearchQuery = useDebounce(searchQuery, SEARCH_DEBOUNCE_MS) + const [institutionId, setInstitutionId] = useState(undefined) + const showInstitutionList = searchQuery.length >= MIN_QUERY_LENGTH && debouncedSearchQuery.length >= MIN_QUERY_LENGTH && view !== 'manual' + const config = useMemo( + () => BrowserUtil.getTellerConfig(logger, institutionId), + [logger, institutionId] + ) as TellerConnectOptions + const { openPlaid } = usePlaid() const { openFinicity } = useFinicity() + const { open: openTeller } = useTellerConnect(config) const inputRef = useRef(null) @@ -44,6 +55,15 @@ export default function AccountTypeSelector({ } }, []) + const configRef = useRef(null) + + useEffect(() => { + if (institutionId) { + configRef.current = BrowserUtil.getTellerConfig(logger, institutionId) + openTeller() + } + }, [institutionId, logger, openTeller]) + return (
{/* Search */} @@ -68,6 +88,8 @@ export default function AccountTypeSelector({ if (!providerInstitution) { alert('No provider found for institution') return + } else { + setInstitutionId(providerInstitution.providerId) } switch (providerInstitution.provider) { @@ -77,6 +99,9 @@ export default function AccountTypeSelector({ case 'FINICITY': openFinicity(providerInstitution.providerId) break + case 'TELLER': + openTeller() + break default: break } @@ -142,7 +167,11 @@ export default function AccountTypeSelector({ return } - if (!data) return + if (!data) { + return + } else { + setInstitutionId(data.providerId) + } switch (data.provider) { case 'PLAID': @@ -151,6 +180,9 @@ export default function AccountTypeSelector({ case 'FINICITY': openFinicity(data.providerId) break + case 'TELLER': + openTeller() + break default: break } diff --git a/libs/client/shared/src/hooks/index.ts b/libs/client/shared/src/hooks/index.ts index 9410b7f4..6481beb2 100644 --- a/libs/client/shared/src/hooks/index.ts +++ b/libs/client/shared/src/hooks/index.ts @@ -9,6 +9,5 @@ export * from './useQueryParam' export * from './useScreenSize' export * from './useAccountNotifications' export * from './usePlaid' -export * from './useTeller' export * from './useProviderStatus' export * from './useModalManager' diff --git a/libs/client/shared/src/hooks/useTeller.ts b/libs/client/shared/src/hooks/useTeller.ts deleted file mode 100644 index e44838f2..00000000 --- a/libs/client/shared/src/hooks/useTeller.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useState } from 'react' -import toast from 'react-hot-toast' -import * as Sentry from '@sentry/react' -import { useTellerConnect } from 'teller-connect-react' -import { useAccountContext, useUserAccountContext } from '../providers' -import { useLogger } from './useLogger' - -type TellerFailure = { - type: 'payee' | 'payment' - code: 'timeout' | 'error' - message: string -} - -export function useTeller() { - const logger = useLogger() - - const [institutionId, setInstitutionId] = useState(null) - - const { setExpectingAccounts } = useUserAccountContext() - const { setAccountManager } = useAccountContext() - - const tellerConfig = { - applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID, - institution: institutionId, - environment: process.env.NEXT_PUBLIC_TELLER_ENV, - selectAccount: 'disabled', - onInit: () => { - toast.dismiss(toastId) - logger.debug(`Teller Connect has initialized`) - }, - onSuccess: (enrollment) => { - logger.debug(`User enrolled successfully`, enrollment) - console.log(enrollment) - setExpectingAccounts(true) - }, - onExit: () => { - logger.debug(`Teller Connect exited`) - }, - onFailure: (failure: TellerFailure) => { - logger.error(`Teller Connect exited with error`, failure) - Sentry.captureEvent({ - level: 'error', - message: 'TELLER_CONNECT_ERROR', - tags: { - 'teller.error.code': failure.code, - 'teller.error.message': failure.message, - }, - }) - }, - } - - const { open, ready } = useTellerConnect(tellerConfig) - - useEffect(() => { - if (ready) { - open() - - if (selectAccount === 'disabled') { - setAccountManager({ view: 'idle' }) - } - } - }, [ready, open, setAccountManager]) - - return { - openTeller: async (institutionId: string) => { - toast('Initializing Teller...', { duration: 2_000 }) - setInstitutionId(institutionId) - }, - ready, - } -} diff --git a/libs/client/shared/src/providers/AccountContextProvider.tsx b/libs/client/shared/src/providers/AccountContextProvider.tsx index 510a4690..454533ca 100644 --- a/libs/client/shared/src/providers/AccountContextProvider.tsx +++ b/libs/client/shared/src/providers/AccountContextProvider.tsx @@ -48,6 +48,7 @@ type AccountManager = | { view: 'idle' } | { view: 'add-plaid'; linkToken: string } | { view: 'add-finicity' } + | { view: 'add-teller' } | { view: 'add-account' } | { view: 'add-property'; defaultValues: Partial } | { view: 'add-vehicle'; defaultValues: Partial } diff --git a/libs/client/shared/src/utils/index.ts b/libs/client/shared/src/utils/index.ts index a1ac6e57..90694d3a 100644 --- a/libs/client/shared/src/utils/index.ts +++ b/libs/client/shared/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './image-loaders' export * from './browser-utils' export * from './account-utils' export * from './form-utils' +export * from './teller-utils' diff --git a/libs/client/shared/src/utils/teller-utils.ts b/libs/client/shared/src/utils/teller-utils.ts new file mode 100644 index 00000000..4265ae84 --- /dev/null +++ b/libs/client/shared/src/utils/teller-utils.ts @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/react' +import type { Logger } from '../providers/LogProvider' +import type { + TellerConnectEnrollment, + TellerConnectFailure, + TellerConnectOptions, +} from 'teller-connect-react' + +type TellerEnvironment = 'sandbox' | 'development' | 'production' | undefined +type TellerAccountSelection = 'disabled' | 'single' | 'multiple' | undefined + +export const getTellerConfig = (logger: Logger, institutionId: string | undefined) => { + return { + applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID ?? 'ADD_TELLER_APP_ID', + environment: (process.env.NEXT_PUBLIC_TELLER_ENV as TellerEnvironment) ?? 'sandbox', + selectAccount: 'disabled' as TellerAccountSelection, + ...(institutionId !== undefined ? { institution: institutionId } : {}), + onInit: () => { + logger.debug(`Teller Connect has initialized`) + }, + onSuccess: (enrollment: TellerConnectEnrollment) => { + logger.debug(`User enrolled successfully`, enrollment) + }, + onExit: () => { + logger.debug(`Teller Connect exited`) + }, + onFailure: (failure: TellerConnectFailure) => { + logger.error(`Teller Connect exited with error`, failure) + Sentry.captureEvent({ + level: 'error', + message: 'TELLER_CONNECT_ERROR', + tags: { + 'teller.error.code': failure.code, + 'teller.error.message': failure.message, + }, + }) + }, + } as TellerConnectOptions +} diff --git a/libs/server/features/src/institution/institution.service.ts b/libs/server/features/src/institution/institution.service.ts index 32cfeebd..ed76de28 100644 --- a/libs/server/features/src/institution/institution.service.ts +++ b/libs/server/features/src/institution/institution.service.ts @@ -271,7 +271,7 @@ export class InstitutionService implements IInstitutionService { provider_institution pi SET institution_id = i.id, - rank = (CASE WHEN pi.provider = 'PLAID' THEN 1 ELSE 0 END) + rank = (CASE WHEN pi.provider = 'TELLER' THEN 1 ELSE 0 END) FROM duplicates d INNER JOIN institutions i ON i.name = d.name AND i.url = d.url diff --git a/libs/server/features/src/providers/teller/teller.service.ts b/libs/server/features/src/providers/teller/teller.service.ts index e884639f..a5847041 100644 --- a/libs/server/features/src/providers/teller/teller.service.ts +++ b/libs/server/features/src/providers/teller/teller.service.ts @@ -79,7 +79,7 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr async getInstitutions() { const tellerInstitutions = await SharedUtil.paginate({ - pageSize: 500, + pageSize: 10000, delay: process.env.NODE_ENV !== 'production' ? { @@ -87,20 +87,20 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr milliseconds: 7_000, // Sandbox rate limited at 10 calls / minute } : undefined, - fetchData: (offset, count) => + fetchData: () => SharedUtil.withRetry( () => this.teller.getInstitutions().then((data) => { this.logger.debug( - `paginated teller fetch inst=${data.institutions.length} (total=${data.institutions.length} offset=${offset} count=${count})` + `teller fetch inst=${data.length} (total=${data.length})` ) - return data.institutions + return data }), { maxRetries: 3, onError: (error, attempt) => { this.logger.error( - `Teller fetch institutions request failed attempt=${attempt} offset=${offset} count=${count}`, + `Teller fetch institutions request failed attempt=${attempt}`, { error: ErrorUtil.parseError(error) } ) @@ -115,10 +115,11 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr return { providerId: id, name, - url: undefined, - logo: `https://teller.io/images/banks/${id}.jpg}`, - primaryColor: undefined, - oauth: undefined, + url: null, + logo: null, + logoUrl: `https://teller.io/images/banks/${id}.jpg`, + primaryColor: null, + oauth: false, data: tellerInstitution, } }) diff --git a/libs/server/shared/src/services/queue.service.ts b/libs/server/shared/src/services/queue.service.ts index 319fef92..c0c1d8c7 100644 --- a/libs/server/shared/src/services/queue.service.ts +++ b/libs/server/shared/src/services/queue.service.ts @@ -70,7 +70,7 @@ export type SyncSecurityQueue = IQueue export type SyncInstitutionQueue = IQueue< {}, - 'sync-finicity-institutions' | 'sync-plaid-institutions' + 'sync-finicity-institutions' | 'sync-plaid-institutions' | 'sync-teller-institutions' > export type SendEmailQueue = IQueue diff --git a/libs/teller-api/src/teller-api.ts b/libs/teller-api/src/teller-api.ts index ca3a180a..16b982f7 100644 --- a/libs/teller-api/src/teller-api.ts +++ b/libs/teller-api/src/teller-api.ts @@ -137,8 +137,8 @@ export class TellerApi { } private async getApi(accessToken: string): Promise { - const cert = fs.readFileSync('../../../certs/teller-certificate.pem', 'utf8') - const key = fs.readFileSync('../../../certs/teller-private-key.pem', 'utf8') + const cert = fs.readFileSync('./certs/certificate.pem', 'utf8') + const key = fs.readFileSync('./certs/private_key.pem', 'utf8') const agent = new https.Agent({ cert, diff --git a/libs/teller-api/src/types/institutions.ts b/libs/teller-api/src/types/institutions.ts index 6f243375..3a593e14 100644 --- a/libs/teller-api/src/types/institutions.ts +++ b/libs/teller-api/src/types/institutions.ts @@ -9,6 +9,4 @@ export type Institution = { type Capability = 'detail' | 'balance' | 'transaction' | 'identity' -export type GetInstitutionsResponse = { - institutions: Institution[] -} +export type GetInstitutionsResponse = Institution[] diff --git a/prisma/migrations/20240116185600_add_teller_provider/migration.sql b/prisma/migrations/20240116185600_add_teller_provider/migration.sql new file mode 100644 index 00000000..20c526f2 --- /dev/null +++ b/prisma/migrations/20240116185600_add_teller_provider/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Provider" ADD VALUE 'TELLER'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 007cad61..918f1349 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -493,6 +493,7 @@ model Institution { enum Provider { PLAID FINICITY + TELLER } model ProviderInstitution {