feat(web): handle adding accounts to institution
This commit is contained in:
@@ -21,12 +21,16 @@ export interface IInstitutionService {
|
||||
createLinkToken: () => Promise<Result<LinkTokenResponse, Error[]>>;
|
||||
connect: (publicToken: string, plaidInstitutionId: string) => Promise<Result<boolean, Error[]>>;
|
||||
getInstitutions: () => Promise<Result<Institution[], Error[]>>;
|
||||
getAvailableAccounts: (institutionId: string) => Promise<Result<AvailableAccount[], Error[]>>;
|
||||
}
|
||||
|
||||
export class InstitutionService implements IInstitutionService {
|
||||
private readonly client: IClient;
|
||||
private readonly endpoints = {
|
||||
link: '/api/institutions/link',
|
||||
available(id: string) {
|
||||
return `/api/institutions/${id}/available`;
|
||||
},
|
||||
connect: '/api/institutions/connect',
|
||||
institutions: '/api/institutions',
|
||||
};
|
||||
@@ -105,13 +109,47 @@ export class InstitutionService implements IInstitutionService {
|
||||
return Err([new Error('Failed to get institutions')]);
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailableAccounts(institutionId: string) {
|
||||
const request = new ClientRequest(this.endpoints.available(institutionId));
|
||||
|
||||
try {
|
||||
const res = await this.client.get(request);
|
||||
|
||||
if (res.ok === false) {
|
||||
return Err([new Error('Failed to get available accounts')]);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return Ok(data.accounts as AvailableAccount[]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return Err([new Error('Failed to get available accounts')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type LinkTokenResponse = {
|
||||
linkToken: string;
|
||||
};
|
||||
|
||||
type Account = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type Institution = {
|
||||
id: string;
|
||||
name: string;
|
||||
accounts: Account[];
|
||||
};
|
||||
|
||||
export type AvailableAccount = {
|
||||
providerInstitutionId: string;
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
currentBalance: number;
|
||||
availableBalance: number;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useInstitutionService } from '@/composables/useInstitutionService';
|
||||
import type { Institution } from '@/services/institutionService';
|
||||
import type { AvailableAccount, Institution } from '@/services/institutionService';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { usePlaidLink, type PlaidLinkOptions } from '@jcss/vue-plaid-link';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
@@ -40,6 +40,7 @@
|
||||
onLoad: () => console.log('loaded'),
|
||||
onExit: () => console.log('exit'),
|
||||
});
|
||||
|
||||
const { open } = usePlaidLink(plaidOptions);
|
||||
|
||||
watch(
|
||||
@@ -85,39 +86,92 @@
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
const targetInstitutionOfAdd = ref<Institution | null>(null);
|
||||
const availableAccounts = ref<AvailableAccount[]>([]);
|
||||
const targetAccountOfAdd = ref<AvailableAccount | null>(null);
|
||||
|
||||
async function handleAddAccountClick(institution: Institution) {
|
||||
const accountsResult = await institutionService.getAvailableAccounts(institution.id);
|
||||
|
||||
if (accountsResult.err) {
|
||||
alert('Failed to retrieve accounts to add');
|
||||
return;
|
||||
}
|
||||
|
||||
availableAccounts.value = accountsResult.val;
|
||||
targetInstitutionOfAdd.value = institution;
|
||||
}
|
||||
|
||||
async function handleAccountAddClick() {
|
||||
if (targetAccountOfAdd.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = {
|
||||
plaidInstitutionId: targetAccountOfAdd.value.providerInstitutionId,
|
||||
plaidAccountId: targetAccountOfAdd.value.providerId,
|
||||
plaidAccountName: targetAccountOfAdd.value.providerName,
|
||||
accountAvailableBalance: targetAccountOfAdd.value.availableBalance,
|
||||
accountCurrencyCode: targetAccountOfAdd.value.currencyCode,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/accounts', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${userStore.user?.token}`,
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (res.ok === false) {
|
||||
alert('Add failed');
|
||||
return;
|
||||
}
|
||||
|
||||
targetInstitutionOfAdd.value = null;
|
||||
institutionsData.value = { status: 'loading' };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
class="add-institution-button"
|
||||
type="button"
|
||||
@click="handleAddInstitutionClick"
|
||||
>
|
||||
<button class="add-institution-button" type="button" @click="handleAddInstitutionClick">
|
||||
Add Institution
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="institutionsData.status === 'loaded'"
|
||||
class="institutions-container"
|
||||
>
|
||||
<div
|
||||
class="institution-card"
|
||||
v-for="institution in institutionsData.data"
|
||||
v-bind:key="institution.id"
|
||||
>
|
||||
<div v-if="institutionsData.status === 'loaded'" class="institutions-container">
|
||||
<div v-for="institution in institutionsData.data" v-bind:key="institution.id">
|
||||
<div class="institution-card">
|
||||
<div>
|
||||
<div>{{ institution.name }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="add-account-button"
|
||||
type="button"
|
||||
>
|
||||
<button class="add-account-button" type="button" @click="handleAddAccountClick(institution)">
|
||||
Add Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="targetInstitutionOfAdd != null && targetInstitutionOfAdd.id === institution.id">
|
||||
<select v-model="targetAccountOfAdd">
|
||||
<option v-for="account in availableAccounts" v-bind:key="account.providerId" :value="account">
|
||||
{{ account.providerName }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" @click="handleAccountAddClick">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="accounts in institution.accounts" v-bind:key="accounts.id">
|
||||
<div>{{ accounts.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="institutionsData.status === 'errored'">Failed to load institutions</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user