feat(connections): filter already-connected institutions from connect bank dialog (#217)
## Summary - When a user already has a connection to a bank/provider, that institution is now hidden from the list when they open the \"Connect Bank\" dialog - Applies to all providers: EnableBanking institutions (matched by `aspsp_name`), Binance, Bitpanda, and Indexa Capital (matched by `provider`) - `ConnectAccountDialog` accepts a new `connections` prop (passed from the connections settings page where it's already available); `ConnectAccountInline` (used in onboarding) accepts an optional `connections` prop defaulting to `[]` ## Tests - Added 2 backend tests to `ConnectionControllerTest` verifying the data contract: the connections page sends `provider` and `aspsp_name` fields for all provider types, which the frontend filtering logic depends on
This commit is contained in:
parent
28c8df34d5
commit
1058904b14
|
|
@ -16,7 +16,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { EnableBankingInstitution } from '@/types/banking';
|
||||
import type {
|
||||
BankingConnection,
|
||||
EnableBankingInstitution,
|
||||
} from '@/types/banking';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
|
|
@ -65,6 +68,7 @@ const BITPANDA_INSTITUTION: EnableBankingInstitution = {
|
|||
interface ConnectAccountDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
connections?: BankingConnection[];
|
||||
}
|
||||
|
||||
type Step = 'country' | 'bank' | 'confirm';
|
||||
|
|
@ -81,6 +85,7 @@ function getCsrfToken(): string {
|
|||
export function ConnectAccountDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
connections = [],
|
||||
}: ConnectAccountDialogProps) {
|
||||
const [step, setStep] = useState<Step>('country');
|
||||
const [country, setCountry] = useState<string>('');
|
||||
|
|
@ -171,6 +176,14 @@ export function ConnectAccountDialog({
|
|||
|
||||
const data = await response.json();
|
||||
|
||||
const connectedEnableBankingNames = new Set(
|
||||
connections
|
||||
.filter((c) => c.provider === 'enablebanking')
|
||||
.map((c) => c.aspsp_name),
|
||||
);
|
||||
const hasProvider = (provider: string) =>
|
||||
connections.some((c) => c.provider === provider);
|
||||
|
||||
const extraInstitutions = [
|
||||
BINANCE_INSTITUTION,
|
||||
BITPANDA_INSTITUTION,
|
||||
|
|
@ -179,9 +192,20 @@ export function ConnectAccountDialog({
|
|||
extraInstitutions.push(INDEXA_CAPITAL_INSTITUTION);
|
||||
}
|
||||
|
||||
const allInstitutions = [...extraInstitutions, ...data].sort(
|
||||
(a, b) => a.name.localeCompare(b.name),
|
||||
);
|
||||
const allInstitutions = [...extraInstitutions, ...data]
|
||||
.filter((institution) => {
|
||||
if (institution.name === 'Binance') {
|
||||
return !hasProvider('binance');
|
||||
}
|
||||
if (institution.name === 'Bitpanda') {
|
||||
return !hasProvider('bitpanda');
|
||||
}
|
||||
if (institution.name === 'Indexa Capital') {
|
||||
return !hasProvider('indexacapital');
|
||||
}
|
||||
return !connectedEnableBankingNames.has(institution.name);
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
setInstitutions(allInstitutions);
|
||||
setFilteredInstitutions(allInstitutions);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { EnableBankingInstitution } from '@/types/banking';
|
||||
import type {
|
||||
BankingConnection,
|
||||
EnableBankingInstitution,
|
||||
} from '@/types/banking';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
|
@ -70,9 +73,13 @@ function getCsrfToken(): string {
|
|||
|
||||
interface ConnectAccountInlineProps {
|
||||
onBack: () => void;
|
||||
connections?: BankingConnection[];
|
||||
}
|
||||
|
||||
export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) {
|
||||
export function ConnectAccountInline({
|
||||
onBack,
|
||||
connections = [],
|
||||
}: ConnectAccountInlineProps) {
|
||||
const [step, setStep] = useState<Step>('country');
|
||||
const { trigger } = useWebHaptics();
|
||||
const [country, setCountry] = useState<string>('');
|
||||
|
|
@ -153,6 +160,14 @@ export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) {
|
|||
|
||||
const data = await response.json();
|
||||
|
||||
const connectedEnableBankingNames = new Set(
|
||||
connections
|
||||
.filter((c) => c.provider === 'enablebanking')
|
||||
.map((c) => c.aspsp_name),
|
||||
);
|
||||
const hasProvider = (provider: string) =>
|
||||
connections.some((c) => c.provider === provider);
|
||||
|
||||
const extraInstitutions = [
|
||||
BINANCE_INSTITUTION,
|
||||
BITPANDA_INSTITUTION,
|
||||
|
|
@ -161,9 +176,20 @@ export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) {
|
|||
extraInstitutions.push(INDEXA_CAPITAL_INSTITUTION);
|
||||
}
|
||||
|
||||
const allInstitutions = [...extraInstitutions, ...data].sort(
|
||||
(a, b) => a.name.localeCompare(b.name),
|
||||
);
|
||||
const allInstitutions = [...extraInstitutions, ...data]
|
||||
.filter((institution) => {
|
||||
if (institution.name === 'Binance') {
|
||||
return !hasProvider('binance');
|
||||
}
|
||||
if (institution.name === 'Bitpanda') {
|
||||
return !hasProvider('bitpanda');
|
||||
}
|
||||
if (institution.name === 'Indexa Capital') {
|
||||
return !hasProvider('indexacapital');
|
||||
}
|
||||
return !connectedEnableBankingNames.has(institution.name);
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
setInstitutions(allInstitutions);
|
||||
setFilteredInstitutions(allInstitutions);
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ export default function ConnectionsPage({ connections }: Props) {
|
|||
<ConnectAccountDialog
|
||||
open={connectDialogOpen}
|
||||
onOpenChange={setConnectDialogOpen}
|
||||
connections={connections}
|
||||
/>
|
||||
|
||||
<UpgradeConnectionDialog
|
||||
|
|
|
|||
|
|
@ -397,3 +397,42 @@ test('credential update validates required fields for binance', function () {
|
|||
|
||||
$response->assertSessionHasErrors('api_secret');
|
||||
});
|
||||
|
||||
test('connections page includes provider and aspsp_name fields needed for frontend duplicate filtering', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
|
||||
BankingConnection::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'enablebanking',
|
||||
'aspsp_name' => 'CaixaBank',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/settings/connections');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->has('connections', 1)
|
||||
->where('connections.0.provider', 'enablebanking')
|
||||
->where('connections.0.aspsp_name', 'CaixaBank')
|
||||
);
|
||||
});
|
||||
|
||||
test('connections page includes connections from all provider types for frontend filtering', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
|
||||
BankingConnection::factory()->create(['user_id' => $user->id, 'provider' => 'enablebanking', 'aspsp_name' => 'CaixaBank']);
|
||||
BankingConnection::factory()->binance()->create(['user_id' => $user->id]);
|
||||
BankingConnection::factory()->bitpanda()->create(['user_id' => $user->id]);
|
||||
BankingConnection::factory()->indexaCapital()->create(['user_id' => $user->id]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/settings/connections');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->has('connections', 4)
|
||||
->has('connections.0.provider')
|
||||
->has('connections.0.aspsp_name')
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue