'use client' import { useEffect, useRef, useState } from 'react' import { Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' export interface Bank { name: string country: string logo?: string bic?: string } const POPULAR_SWEDISH_BANKS = [ 'Swedbank', 'SEB', 'Nordea', 'Handelsbanken', 'Danske Bank', ] interface BankSelectorProps { onConnect: (bank: Bank) => void onPsuTypeDetected?: (psuType: 'personal' | 'business') => void isConnecting?: boolean connectingBankName?: string | null className?: string } function BankCard({ bank, isConnecting, connectingBankName, onConnect }: { bank: Bank isConnecting: boolean connectingBankName: string | null onConnect: (bank: Bank) => void }) { const connecting = isConnecting && connectingBankName === bank.name return ( onConnect(bank)} className={cn( 'group flex items-center gap-3 p-4 border border-border rounded-lg bg-card text-left transition-all', 'hover:border-primary hover:bg-muted', connecting && 'border-primary bg-muted', isConnecting && !connecting && 'opacity-50 cursor-not-allowed', )} > {connecting ? ( ) : bank.logo ? ( ) : ( )} {bank.name} ) } export function BankSelector({ onConnect, onPsuTypeDetected, isConnecting = false, connectingBankName = null, className, }: BankSelectorProps) { const [banks, setBanks] = useState([]) const [isSandbox, setIsSandbox] = useState(true) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const searchRef = useRef(null) useEffect(() => { async function fetchBanks() { try { const res = await fetch('/api/extensions/ext/enable-banking/banks') if (!res.ok) { // A non-OK response (500, or the 403 capability gate) has no `banks` // array; without this guard it falls through to the "no banks // available" empty state, which misreads as a successful-but-empty // load rather than a failure. setError('Kunde inte ladda banker') return } const data = await res.json() if (data.banks) { setBanks(data.banks as Bank[]) } if (data.sandbox !== undefined) { setIsSandbox(data.sandbox) } if (data.psu_type && onPsuTypeDetected) { onPsuTypeDetected(data.psu_type) } } catch { setError('Kunde inte ladda banker') } finally { setIsLoading(false) } } fetchBanks() // eslint-disable-next-line react-hooks/exhaustive-deps -- onPsuTypeDetected is a stable setter, only run on mount }, [onPsuTypeDetected]) const filteredBanks = banks.filter((bank) => bank.name.toLowerCase().includes(searchQuery.toLowerCase()) ) const showPopular = !isSandbox && !searchQuery const popularBanks = showPopular ? filteredBanks.filter((bank) => POPULAR_SWEDISH_BANKS.includes(bank.name)) : [] const otherBanks = showPopular ? filteredBanks.filter((bank) => !POPULAR_SWEDISH_BANKS.includes(bank.name)) : filteredBanks return ( {/* Search input */} setSearchQuery(e.target.value)} className="w-full pl-10 pr-3 py-3 border border-border rounded-lg bg-background text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all" /> {/* Loading state */} {isLoading && ( )} {/* Error state */} {error && ( {error} )} {/* Bank grid */} {!isLoading && !error && ( <> {filteredBanks.length === 0 ? ( {searchQuery ? `Inga banker matchar "${searchQuery}"` : 'Inga banker tillgängliga'} {searchQuery && ( Försök med ett annat sökord )} ) : ( {popularBanks.length > 0 && ( Populära banker {popularBanks.map((bank) => ( ))} )} {popularBanks.length > 0 && otherBanks.length > 0 && ( Alla banker )} {otherBanks.map((bank) => ( ))} )} > )} {/* Connecting overlay */} {isConnecting && connectingBankName && ( Ansluter till {connectingBankName}... )} ) }
{error}
{searchQuery ? `Inga banker matchar "${searchQuery}"` : 'Inga banker tillgängliga'}
Försök med ett annat sökord