'use client'
import { useEffect, useSyncExternalStore } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Loader2, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import { ToastAction } from '@/components/ui/toast'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { createClient } from '@/lib/supabase/client'
import { notifyBankSyncUpdated } from '@/lib/transactions/bank-sync-signal'
import {
claimConnectionsLoad,
clearBusyConnection,
getBankSyncSnapshot,
markConnectionStatus,
publishConnections,
releaseConnectionsLoad,
setBusyConnection,
setSyncingAll,
subscribeBankSync,
type BankConn,
} from '@/lib/transactions/bank-sync-store'
import { useCompany, useCapability } from '@/contexts/CompanyContext'
import { CAPABILITY } from '@/lib/entitlements/keys'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
export type { BankConn }
/**
* Shared on-demand bank sync state + actions. Powers the footer "Synka nu"
* button below and the "Synka bank nu" row in the Importera split-button menu
* (TransactionStatusBar). Reuses the per-connection sync endpoint that
* BankingSettingsPanel already calls.
*
* Also handles dead PSD2 sessions: a connection whose consent has closed or
* expired re-authorizes in place via `reconnect` (no disconnect needed), and a
* sync that fails with a session expiry surfaces the same reconnect action
* right in the error toast.
*/
export function useBankSync() {
const t = useTranslations('transactions')
const { toast } = useToast()
const router = useRouter()
const { company } = useCompany()
const hasBankSync = useCapability(CAPABILITY.bank_sync)
// Busy state and the connection list live in a module-level store so every
// useBankSync() instance (header split button, footer button) sees the same
// sync in flight and cannot start a concurrent one (#1162).
const store = useSyncExternalStore(subscribeBankSync, getBankSyncSnapshot, getBankSyncSnapshot)
// Never present another company's cached list while a switch is loading.
const connections = store.companyId === company?.id ? store.connections : null
useEffect(() => {
if (!company?.id) return
// First instance to mount claims the fetch; the rest read the store. The
// store outlives components, so the result publishes even if this
// instance unmounts mid-flight.
if (!claimConnectionsLoad(company.id)) return
const companyId = company.id
const supabase = createClient()
supabase
.from('bank_connections')
.select('id, bank_name, status, provider, last_synced_at')
// Include expired/error so the reconnect entry point survives a reload:
// not just active connections that can sync.
.in('status', ['active', 'expired', 'error'])
.eq('company_id', companyId)
.then(({ data, error }) => {
if (error) {
releaseConnectionsLoad(companyId)
return
}
publishConnections(companyId, (data as BankConn[]) ?? [])
})
}, [company?.id])
// Re-authorize an existing connection in place: posts the connection_id so
// the server reuses the same row, then hands off to the bank's consent screen.
async function reconnect(conn: BankConn) {
setBusyConnection(conn.id)
try {
const country = conn.provider?.split('-').pop()?.toUpperCase() || 'SE'
const res = await fetch('/api/extensions/ext/enable-banking/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
connection_id: conn.id,
aspsp_name: conn.bank_name,
aspsp_country: country,
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Reconnect failed')
window.location.href = data.authorization_url
} catch (error) {
toast({
title: t('bank_reconnect'),
description: error instanceof Error ? getUserErrorMessage(error) : 'Reconnect failed',
variant: 'destructive',
})
setBusyConnection(null)
}
}
async function syncConnection(conn: BankConn) {
setBusyConnection(conn.id)
try {
const res = await fetch('/api/extensions/ext/enable-banking/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connection_id: conn.id }),
})
const data = await res.json()
if (!res.ok) {
// A dead PSD2 session can't be fixed by retrying: surface a one-click
// reconnect in the toast instead of a dead-end error.
if (data?.reauth_required) {
toast({
title: t('bank_sync_session_expired'),
description: t('bank_sync_session_expired_desc'),
variant: 'destructive',
action: (