Private
Public Access
1
0

Fix Plaid link token error handling and show user-visible errors

Validate Plaid credentials are configured before calling the API,
and validate the returned link token before passing it to Plaid
Link JS. Show error snackbars for all bank sync failures instead
of only logging to console.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 16:29:16 -05:00
parent 6e80bcf98b
commit 849a392d14
2 changed files with 31 additions and 9 deletions
+27 -9
View File
@@ -242,10 +242,13 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- Sync Results Snackbar --> <!-- Snackbars -->
<v-snackbar v-model="showSyncResult" :timeout="5000" color="success"> <v-snackbar v-model="showSyncResult" :timeout="5000" color="success">
{{ syncResultMessage }} {{ syncResultMessage }}
</v-snackbar> </v-snackbar>
<v-snackbar v-model="showError" :timeout="6000" color="error">
{{ errorMessage }}
</v-snackbar>
</v-tabs-window-item> </v-tabs-window-item>
<!-- Tab 3: Sync History --> <!-- Tab 3: Sync History -->
@@ -341,6 +344,10 @@ const deleteLoading = ref(false)
const showSyncResult = ref(false) const showSyncResult = ref(false)
const syncResultMessage = ref('') const syncResultMessage = ref('')
// Error display
const showError = ref(false)
const errorMessage = ref('')
// Sync logs // Sync logs
const syncLogs = ref<SyncLogEntry[]>([]) const syncLogs = ref<SyncLogEntry[]>([])
const logsLoading = ref(false) const logsLoading = ref(false)
@@ -420,6 +427,13 @@ async function startPlaidLink() {
try { try {
const { data: linkData } = await bankSyncApi.createLinkToken() const { data: linkData } = await bankSyncApi.createLinkToken()
if (!linkData.linkToken) {
errorMessage.value = 'Plaid returned an empty link token. Check your Plaid credentials in appsettings.json.'
showError.value = true
plaidLoading.value = false
return
}
// Dynamically load Plaid Link script // Dynamically load Plaid Link script
await loadPlaidScript() await loadPlaidScript()
@@ -431,8 +445,9 @@ async function startPlaidLink() {
connections.value.unshift(conn) connections.value.unshift(conn)
syncIntervals[conn.id] = conn.syncIntervalMinutes syncIntervals[conn.id] = conn.syncIntervalMinutes
openMappingDialog(conn) openMappingDialog(conn)
} catch (err) { } catch (err: any) {
console.error('Failed to complete Plaid link:', err) errorMessage.value = err.response?.data?.error || 'Failed to complete Plaid link'
showError.value = true
} }
}, },
onExit: () => { onExit: () => {
@@ -440,8 +455,9 @@ async function startPlaidLink() {
} }
}) })
plaid.open() plaid.open()
} catch (err) { } catch (err: any) {
console.error('Failed to create link token:', err) errorMessage.value = err.response?.data?.error || 'Failed to create Plaid link token. Check your Plaid credentials.'
showError.value = true
plaidLoading.value = false plaidLoading.value = false
} }
} }
@@ -469,8 +485,9 @@ async function connectSimpleFin() {
showSimpleFinDialog.value = false showSimpleFinDialog.value = false
simpleFinToken.value = '' simpleFinToken.value = ''
openMappingDialog(conn) openMappingDialog(conn)
} catch (err) { } catch (err: any) {
console.error('Failed to connect SimpleFIN:', err) errorMessage.value = err.response?.data?.error || 'Failed to connect via SimpleFIN. Check your setup token.'
showError.value = true
} finally { } finally {
simpleFinLoading.value = false simpleFinLoading.value = false
} }
@@ -540,8 +557,9 @@ async function syncNow(conn: SyncConnection) {
syncResultMessage.value = `Synced ${conn.institutionName}: ${data.transactionsImported} imported, ${data.transactionsSkipped} skipped` syncResultMessage.value = `Synced ${conn.institutionName}: ${data.transactionsImported} imported, ${data.transactionsSkipped} skipped`
showSyncResult.value = true showSyncResult.value = true
await loadConnections() await loadConnections()
} catch (err) { } catch (err: any) {
console.error('Sync failed:', err) errorMessage.value = err.response?.data?.error || 'Sync failed'
showError.value = true
} finally { } finally {
syncingConnections[conn.id] = false syncingConnections[conn.id] = false
} }
@@ -43,6 +43,10 @@ public class PlaidSyncProvider : IAccountSyncProvider
public async Task<PlaidLinkTokenResult> CreateLinkTokenAsync(string userId) public async Task<PlaidLinkTokenResult> CreateLinkTokenAsync(string userId)
{ {
if (string.IsNullOrEmpty(_config.ClientId) || string.IsNullOrEmpty(_config.Secret))
throw new InvalidOperationException(
"Plaid credentials are not configured. Set BankSync:Plaid:ClientId and BankSync:Plaid:Secret in appsettings.json.");
var response = await _client.LinkTokenCreateAsync(new LinkTokenCreateRequest var response = await _client.LinkTokenCreateAsync(new LinkTokenCreateRequest
{ {
ClientId = _config.ClientId, ClientId = _config.ClientId,