From bd12e2aaf5e25f1523f65c27456dac688eec352d Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Wed, 4 Mar 2026 21:20:32 +0100 Subject: [PATCH] feat: redesign bank selector with popular banks, search, and one-click connect Replace the external Enable Banking widget with a custom component that fetches banks from our API, shows popular Swedish banks in a grid, provides search filtering, and connects on click without an intermediate selection step. Co-Authored-By: Claude Opus 4.6 --- .../components/BankSelector.tsx | 246 ++++++++++++------ .../components/BankingSettingsPanel.tsx | 42 +-- 2 files changed, 180 insertions(+), 108 deletions(-) diff --git a/extensions/general/enable-banking/components/BankSelector.tsx b/extensions/general/enable-banking/components/BankSelector.tsx index 2f288d84..5bbfba45 100644 --- a/extensions/general/enable-banking/components/BankSelector.tsx +++ b/extensions/general/enable-banking/components/BankSelector.tsx @@ -1,9 +1,10 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' -import { Loader2 } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Loader2, Search, RefreshCw } from 'lucide-react' import { cn } from '@/lib/utils' -import Script from 'next/script' export interface Bank { name: string @@ -13,104 +14,197 @@ export interface Bank { } interface BankSelectorProps { - onSelect: (bank: Bank) => void - selectedBank?: Bank | null - isLoading?: boolean + onConnect: (bank: Bank) => void + isConnecting?: boolean + connectingBankName?: string | null className?: string - country?: string - sandbox?: boolean } +const POPULAR_BANK_NAMES = [ + 'Nordea', + 'SEB', + 'Swedbank', + 'Handelsbanken', + 'Länsförsäkringar', + 'Skandia', + 'Danske Bank', +] + export function BankSelector({ - onSelect, - isLoading = false, + onConnect, + isConnecting = false, + connectingBankName = null, className, - country = 'SE', - sandbox = process.env.NEXT_PUBLIC_ENABLE_BANKING_SANDBOX === 'true', }: BankSelectorProps) { - const widgetRef = useRef(null) - const [scriptReady, setScriptReady] = useState(false) + const [banks, setBanks] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [searchQuery, setSearchQuery] = useState('') - // Stable ref for onSelect so the event listener never goes stale - const onSelectRef = useRef(onSelect) - useEffect(() => { - onSelectRef.current = onSelect - }, [onSelect]) - - const handleSelected = useCallback((e: Event) => { - const customEvent = e as CustomEvent - const detail = customEvent.detail as { - name: string - country: string - psuType: string - sandbox: boolean + const fetchBanks = useCallback(async () => { + setLoading(true) + setError(null) + try { + const res = await fetch('/api/extensions/ext/enable-banking/banks') + if (!res.ok) throw new Error('Kunde inte hämta banklistan') + const data = await res.json() + setBanks(data.banks || []) + } catch (e) { + setError(e instanceof Error ? e.message : 'Något gick fel') + } finally { + setLoading(false) } - - onSelectRef.current({ - name: detail.name, - country: detail.country, - }) }, []) - // Attach the event listener once the script is ready and the widget is in the DOM. - // Using onReady instead of onLoad ensures the script has fully executed and - // the custom element has been defined/upgraded before we interact with it. useEffect(() => { - if (!scriptReady) return + fetchBanks() + }, [fetchBanks]) - const widget = widgetRef.current - if (!widget) return + const popularBanks = useMemo( + () => + POPULAR_BANK_NAMES.map((name) => + banks.find((b) => b.name === name) + ).filter((b): b is Bank => b !== undefined), + [banks] + ) - widget.addEventListener('selected', handleSelected) - - return () => { - widget.removeEventListener('selected', handleSelected) + const filteredBanks = useMemo(() => { + if (!searchQuery.trim()) { + // Show all banks except popular ones + const popularSet = new Set(POPULAR_BANK_NAMES) + return banks + .filter((b) => !popularSet.has(b.name)) + .sort((a, b) => a.name.localeCompare(b.name, 'sv')) } - }, [scriptReady, handleSelected]) + const q = searchQuery.toLowerCase() + return banks + .filter((b) => b.name.toLowerCase().includes(q)) + .sort((a, b) => a.name.localeCompare(b.name, 'sv')) + }, [banks, searchQuery]) - if (isLoading) { + if (loading) { return ( -
- - Ansluter... +
+ {/* Popular banks skeleton */} +
+ {Array.from({ length: 7 }).map((_, i) => ( +
+ ))} +
+
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+
+ ) + } + + if (error) { + return ( +
+

{error}

+
) } return (
-