fix(connections): show expired reconnect (#407)

## Summary
- show primary Reconnect action for expired EnableBanking connections
- keep dropdown reconnect available for expired connections
- add React test covering multiple expired connections

## Tests
- npm test -- resources/js/pages/settings/connections.test.tsx
- npx eslint resources/js/pages/settings/connections.tsx
resources/js/pages/settings/connections.test.tsx

## Notes
- npm run types -- --pretty false still fails on pre-existing unrelated
TypeScript errors
This commit is contained in:
Víctor Falcón 2026-05-20 13:13:13 +01:00 committed by GitHub
parent 11f989d03a
commit d2e00f14e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 144 additions and 1 deletions

View File

@ -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>{title}</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 }) => (
<span>{status}</span>
),
}));
function makeConnection(
overrides: Partial<BankingConnection> = {},
): 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(
<ConnectionsPage
connections={[
makeConnection({
id: 'connection-1',
aspsp_name: 'First Bank',
status: 'expired',
}),
makeConnection({
id: 'connection-2',
aspsp_name: 'Second Bank',
status: 'expired',
}),
]}
/>,
);
expect(
screen.getAllByRole('button', { name: /reconnect/i }),
).toHaveLength(2);
});
});

View File

@ -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) && (
<Button
size="sm"
disabled={
reconnectingId ===
connection.id
}
onClick={() =>
handleReconnect(
connection,
)
}
>
{reconnectingId ===
connection.id ? (
<Spinner className="mr-1.5 size-3" />
) : (
<RotateCcw className="mr-1.5 h-3 w-3" />
)}
{__('Reconnect')}
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@ -263,7 +294,7 @@ export default function ConnectionsPage({ connections }: Props) {
)}
</DropdownMenuItem>
)}
{isEnableBankingAuthError(
{canReconnect(
connection,
) && (
<DropdownMenuItem