diff --git a/resources/js/pages/settings/connections.test.tsx b/resources/js/pages/settings/connections.test.tsx
new file mode 100644
index 00000000..ba1b7b0f
--- /dev/null
+++ b/resources/js/pages/settings/connections.test.tsx
@@ -0,0 +1,112 @@
+import type { BankingConnection } from '@/types/banking';
+import { render, screen } from '@testing-library/react';
+import type React from 'react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import ConnectionsPage from './connections';
+
+const mocks = vi.hoisted(() => ({
+ routerPost: vi.fn(),
+ routerVisit: vi.fn(),
+ pollStart: vi.fn(),
+ pollStop: vi.fn(),
+}));
+
+vi.mock('@inertiajs/react', () => ({
+ Head: ({ title }: { title: string }) =>
{title},
+ router: {
+ post: mocks.routerPost,
+ visit: mocks.routerVisit,
+ },
+ usePage: () => ({
+ props: {
+ auth: {
+ isDemoAccount: false,
+ hasProPlan: true,
+ },
+ flash: {},
+ subscriptionsEnabled: false,
+ },
+ }),
+ usePoll: () => ({
+ start: mocks.pollStart,
+ stop: mocks.pollStop,
+ }),
+}));
+
+vi.mock('@/layouts/app-layout', () => ({
+ default: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+vi.mock('@/layouts/settings/layout', () => ({
+ default: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+vi.mock('@/components/open-banking/connect-account-dialog', () => ({
+ ConnectAccountDialog: () => null,
+}));
+
+vi.mock('@/components/open-banking/disconnect-dialog', () => ({
+ DisconnectDialog: () => null,
+}));
+
+vi.mock('@/components/open-banking/update-credentials-dialog', () => ({
+ UpdateCredentialsDialog: () => null,
+}));
+
+vi.mock('@/components/open-banking/upgrade-connection-dialog', () => ({
+ UpgradeConnectionDialog: () => null,
+}));
+
+vi.mock('@/components/open-banking/connection-status-badge', () => ({
+ ConnectionStatusBadge: ({ status }: { status: string }) => (
+ {status}
+ ),
+}));
+
+function makeConnection(
+ overrides: Partial = {},
+): BankingConnection {
+ return {
+ id: 'connection-1',
+ provider: 'enablebanking',
+ aspsp_name: 'Test Bank',
+ aspsp_country: 'ES',
+ status: 'active',
+ valid_until: null,
+ last_synced_at: '2026-01-01T00:00:00.000000Z',
+ error_message: null,
+ accounts_count: 1,
+ created_at: '2026-01-01T00:00:00.000000Z',
+ updated_at: '2026-01-01T00:00:00.000000Z',
+ ...overrides,
+ };
+}
+
+describe('ConnectionsPage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('shows visible reconnect buttons for expired EnableBanking connections', () => {
+ render(
+ ,
+ );
+
+ expect(
+ screen.getAllByRole('button', { name: /reconnect/i }),
+ ).toHaveLength(2);
+ });
+});
diff --git a/resources/js/pages/settings/connections.tsx b/resources/js/pages/settings/connections.tsx
index d4b825c1..44af3e82 100644
--- a/resources/js/pages/settings/connections.tsx
+++ b/resources/js/pages/settings/connections.tsx
@@ -149,6 +149,14 @@ export default function ConnectionsPage({ connections }: Props) {
);
}
+ function canReconnect(connection: BankingConnection): boolean {
+ return (
+ connection.provider === 'enablebanking' &&
+ (connection.status === 'expired' ||
+ isEnableBankingAuthError(connection))
+ );
+ }
+
function formatDate(dateString: string | null): string {
if (!dateString) return __('Never');
return new Date(dateString).toLocaleDateString(undefined, {
@@ -224,6 +232,29 @@ export default function ConnectionsPage({ connections }: Props) {
connection.last_synced_at
}
/>
+ {connection.status === 'expired' &&
+ canReconnect(connection) && (
+
+ )}