1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

teller front end progress

This commit is contained in:
Tyler Myracle 2024-01-16 15:18:21 -06:00
parent b34563c60c
commit b61fececc7
16 changed files with 109 additions and 91 deletions

View file

@ -240,6 +240,7 @@ const userService = new UserService(
const institutionProviderFactory = new InstitutionProviderFactory({
PLAID: plaidService,
FINICITY: finicityService,
TELLER: tellerService,
})
const institutionService: IInstitutionService = new InstitutionService(

View file

@ -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(

View file

@ -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
*/

View file

@ -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<string>('')
const debouncedSearchQuery = useDebounce(searchQuery, SEARCH_DEBOUNCE_MS)
const [institutionId, setInstitutionId] = useState<string | undefined>(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<HTMLInputElement>(null)
@ -44,6 +55,15 @@ export default function AccountTypeSelector({
}
}, [])
const configRef = useRef<TellerConnectOptions | null>(null)
useEffect(() => {
if (institutionId) {
configRef.current = BrowserUtil.getTellerConfig(logger, institutionId)
openTeller()
}
}, [institutionId, logger, openTeller])
return (
<div>
{/* 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
}

View file

@ -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'

View file

@ -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<string | null>(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,
}
}

View file

@ -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<CreatePropertyFields> }
| { view: 'add-vehicle'; defaultValues: Partial<CreateVehicleFields> }

View file

@ -2,3 +2,4 @@ export * from './image-loaders'
export * from './browser-utils'
export * from './account-utils'
export * from './form-utils'
export * from './teller-utils'

View file

@ -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
}

View file

@ -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

View file

@ -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,
}
})

View file

@ -70,7 +70,7 @@ export type SyncSecurityQueue = IQueue<SyncSecurityQueueJobData, 'sync-all-secur
export type PurgeUserQueue = IQueue<{ userId: User['id'] }, 'purge-user'>
export type SyncInstitutionQueue = IQueue<
{},
'sync-finicity-institutions' | 'sync-plaid-institutions'
'sync-finicity-institutions' | 'sync-plaid-institutions' | 'sync-teller-institutions'
>
export type SendEmailQueue = IQueue<SendEmailQueueJobData, 'send-email'>

View file

@ -137,8 +137,8 @@ export class TellerApi {
}
private async getApi(accessToken: string): Promise<AxiosInstance> {
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,

View file

@ -9,6 +9,4 @@ export type Institution = {
type Capability = 'detail' | 'balance' | 'transaction' | 'identity'
export type GetInstitutionsResponse = {
institutions: Institution[]
}
export type GetInstitutionsResponse = Institution[]

View file

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Provider" ADD VALUE 'TELLER';

View file

@ -493,6 +493,7 @@ model Institution {
enum Provider {
PLAID
FINICITY
TELLER
}
model ProviderInstitution {