feat: enable PSD2 bank integration via settings

- Uncomment Enable Banking extension in loader (now registered at runtime)
- Add subscriptionNotice field to ExtensionDefinition type
- Show confirmation dialog when enabling extensions with subscription requirements
- Fix Settings banking tab: toggle-aware visibility, URL-addressable tabs,
  BankSelector widget, correct API paths (/api/extensions/ext/enable-banking/*)
- Replace inline bank connection cards with BankConnectionStatus component
- Add actionable link to Settings from EnableBankingWorkspace
- Update CLAUDE.md with latest architecture docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-24 17:01:31 +01:00
co-authored by Claude Opus 4.6
parent 4163b46eae
commit 9d684d7e9f
10 changed files with 234 additions and 144 deletions
@@ -56,7 +56,11 @@ export default async function ExtensionDetailPage({
</div>
</div>
</div>
<ExtensionToggleButton sectorSlug={sectorSlug} extensionSlug={extensionSlug} />
<ExtensionToggleButton
sectorSlug={sectorSlug}
extensionSlug={extensionSlug}
subscriptionNotice={definition.subscriptionNotice}
/>
</div>
{/* Details */}
+67 -89
View File
@@ -8,18 +8,13 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useToast } from '@/components/ui/use-toast'
import { formatDate } from '@/lib/utils'
import {
Loader2,
Building,
CreditCard,
User,
AlertTriangle,
RefreshCw,
Trash2,
LogOut,
Bell,
Calendar,
@@ -27,11 +22,14 @@ import {
Moon,
Monitor,
Palette,
Landmark,
} from 'lucide-react'
import { useTheme } from 'next-themes'
import type { CompanySettings, BankConnection } from '@/types'
import { NotificationSettings } from '@/extensions/general/push-notifications/NotificationSettings'
import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings'
import { BankSelector, type Bank } from '@/extensions/general/enable-banking/components/BankSelector'
import { BankConnectionStatus } from '@/extensions/general/enable-banking/components/BankConnectionStatus'
export default function SettingsPage() {
const router = useRouter()
@@ -46,9 +44,12 @@ export default function SettingsPage() {
const [isSyncing, setIsSyncing] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const [hasBankingExtension, setHasBankingExtension] = useState(false)
const [selectedBank, setSelectedBank] = useState<Bank | null>(null)
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const initialTab = searchParams.get('tab') || 'company'
useEffect(() => {
setMounted(true)
}, [])
@@ -106,18 +107,16 @@ export default function SettingsPage() {
setBankConnections(connections || [])
// Check if extension API is available (will return banks list if extension is loaded)
// Check if Enable Banking extension is enabled via toggle API
try {
const bankingCheck = await fetch('/api/extensions/enable-banking/callback', {
method: 'HEAD',
})
// The extension route existing means it's deployed - but we also need
// to check if there are existing connections to show the tab
setHasBankingExtension(
(connections && connections.length > 0) || bankingCheck.status !== 404
)
const toggleRes = await fetch('/api/extensions/toggles/general/enable-banking')
if (toggleRes.ok) {
const { data } = await toggleRes.json()
setHasBankingExtension(data?.enabled || (connections && connections.length > 0) || false)
} else {
setHasBankingExtension((connections && connections.length > 0) || false)
}
} catch {
// If the extension routes don't exist, only show tab if there are existing connections
setHasBankingExtension((connections && connections.length > 0) || false)
}
@@ -182,7 +181,7 @@ export default function SettingsPage() {
setIsConnecting(true)
try {
const response = await fetch('/api/extensions/enable-banking/callback', {
const response = await fetch('/api/extensions/ext/enable-banking/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aspsp_name: bankName, aspsp_country: bankCountry }),
@@ -210,7 +209,7 @@ export default function SettingsPage() {
setIsSyncing(true)
try {
const response = await fetch('/api/extensions/enable-banking/sync/cron', {
const response = await fetch('/api/extensions/ext/enable-banking/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connection_id: connectionId }),
@@ -275,24 +274,6 @@ export default function SettingsPage() {
const activeConnections = bankConnections.filter((c) => c.status === 'active')
// Helper to calculate days until consent expires
function getDaysUntilExpiry(expiresAt: string | null): number | null {
if (!expiresAt) return null
const expiryDate = new Date(expiresAt)
const now = new Date()
const diffTime = expiryDate.getTime() - now.getTime()
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
return Math.max(0, diffDays)
}
function isConsentExpiringSoon(expiresAt: string | null): boolean {
if (!expiresAt) return false
const expiryDate = new Date(expiresAt)
const warningDate = new Date()
warningDate.setDate(warningDate.getDate() + 7)
return expiryDate <= warningDate
}
return (
<div className="space-y-6">
<div>
@@ -302,7 +283,7 @@ export default function SettingsPage() {
</p>
</div>
<Tabs defaultValue="company" className="space-y-6">
<Tabs defaultValue={initialTab} className="space-y-6">
<TabsList className="flex-wrap h-auto gap-1">
<TabsTrigger value="company">
<Building className="mr-2 h-4 w-4" />
@@ -554,66 +535,63 @@ export default function SettingsPage() {
<CardTitle>Anslutna banker</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{activeConnections.map((connection) => {
const daysUntilExpiry = getDaysUntilExpiry(connection.consent_expires)
const isExpiring = isConsentExpiringSoon(connection.consent_expires)
return (
<div
key={connection.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<CreditCard className="h-5 w-5 text-primary" />
</div>
<div>
<p className="font-medium">{connection.bank_name}</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
Senast synkad: {formatDate(connection.last_synced_at || connection.created_at)}
</span>
{isExpiring && (
<Badge variant="warning" className="flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{daysUntilExpiry} dagar kvar
</Badge>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleSyncTransactions(connection.id)}
disabled={isSyncing}
>
{isSyncing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDisconnectBank(connection.id)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
)
})}
{activeConnections.map((connection) => (
<BankConnectionStatus
key={connection.id}
connection={connection}
onSync={handleSyncTransactions}
onDisconnect={handleDisconnectBank}
isSyncing={isSyncing}
/>
))}
</CardContent>
</Card>
)}
{/* Connect new bank */}
<Card>
<CardHeader>
<CardTitle>Anslut ny bank</CardTitle>
<CardDescription>
Välj din bank nedan för att koppla ditt konto via PSD2.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<BankSelector
onSelect={setSelectedBank}
isLoading={isConnecting}
/>
{selectedBank && (
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/50">
<div className="flex items-center gap-3">
<Landmark className="h-5 w-5 text-primary" />
<div>
<p className="text-sm font-medium">{selectedBank.name}</p>
<p className="text-xs text-muted-foreground">{selectedBank.country}</p>
</div>
</div>
<Button
onClick={() => handleConnectBank(selectedBank.name, selectedBank.country)}
disabled={isConnecting}
>
{isConnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Ansluter...
</>
) : (
'Anslut bank'
)}
</Button>
</div>
)}
</CardContent>
</Card>
{/* Info about PSD2 */}
<Card>
<CardHeader>
<CardTitle>Bankintegration (PSD2)</CardTitle>
<CardTitle>Om bankintegration (PSD2)</CardTitle>
<CardDescription>
Automatisk import av transaktioner via PSD2 open banking.
Samtycket gäller i 90 dagar och behöver sedan förnyas.