feat(open-banking): allow re-connecting a bank behind a replace warning (#570)
## Why
Users with several accounts at the same bank (for example a partner's
account at the same bank) need to connect the same Enable Banking ASPSP
more than once. Previously an already-connected bank was hard-blocked:
disabled in the connect dialog list, and filtered out entirely from the
onboarding inline flow with a "you already have this connected, go
reconnect" message.
## What changed
- The already-connected bank stays **selectable** and is marked with an
**"Already connected"** badge instead of being blocked/hidden.
- On the confirm step, when the selected bank already has a **live**
connection, a destructive warning explains that authorizing with the
**same bank login replaces the existing session and stops it working** —
continue only when adding a *different* account.
- The **Connect** button is gated behind an acknowledgement checkbox ("I
understand the existing connection may stop working") so a working
connection can't be replaced by accident.
- New shared `ReplaceConnectionWarning` component keeps the dialog and
inline flows from drifting apart (the same divergence #569 unified).
## Notes
- **No backend changes.** `AuthorizationController@store` already
created a fresh connection for a repeated bank; the block was
frontend-only.
- Out of scope: the previous `BankingConnection` row stays `active` in
our DB after the user re-authorizes the same login, even though Enable
Banking has invalidated it. It self-corrects on the next sync failure.
Auto-marking/cleaning the superseded connection on callback is a
separate backend follow-up.
## Testing
- New `connect-account-dialog.test.tsx`: already-connected bank is
selectable + badged, Connect is gated behind the acknowledgement, and a
fresh bank shows no warning.
- Full JS suite (234 tests) and `LocalizationTest` pass; lint and format
clean.
This commit is contained in:
parent
7fb190683c
commit
64827fabae
|
|
@ -273,6 +273,7 @@
|
|||
"Allocated Amount": "Cantidad Asignado",
|
||||
"Allocated:": "Asignado:",
|
||||
"Almost there, just double-checking the math...": "Casi listo, revisando los números una vez más...",
|
||||
"Already connected": "Ya conectado",
|
||||
"Already have an account?": "¿Ya tienes una cuenta?",
|
||||
"Amount": "Valor",
|
||||
"Amount is required": "Se requiere un valor",
|
||||
|
|
@ -797,6 +798,7 @@
|
|||
"Health & Wellness (Medical, Pharmacy, Fitness)": "Salud y Bienestar (Médico, Farmacia, Gimnasio)",
|
||||
"Health and pharmaceuticals": "Salud y farmacia",
|
||||
"Healthy Pharmacy": "Healthy Pharmacy",
|
||||
"Having trouble with this connection?": "¿Tienes problemas con esta conexión?",
|
||||
"Help Me Improve": "Ayúdame a Mejorar",
|
||||
"Help Us Improve": "Ayúdanos a Mejorar",
|
||||
"Hey :name, is everything okay?": "Hey :name, ¿está todo bien?",
|
||||
|
|
@ -831,6 +833,7 @@
|
|||
"How's Your Experience So Far?": "¿Cómo Va Tu Experiencia Hasta Ahora?",
|
||||
"How's it going, :name?": "¿Cómo va todo, :name?",
|
||||
"I Understand, Continue": "Entiendo, Continuar",
|
||||
"I will use a different bank login": "Voy a usar un nuevo acceso del banco",
|
||||
"I built Whisper Money because I believe **your financial data should be yours alone**:": "Construí Whisper Money porque creo que **tus datos financieros deben ser solo tuyos**:",
|
||||
"I don't use AI to analyze your spending or sell insights about your habits. Your financial data stays between you and your spreadsheet-loving self.": "No uso IA para analizar tus gastos ni vender información sobre tus hábitos. Tus datos financieros quedan entre tú y tu afición por las hojas de cálculo.",
|
||||
"I know setting up a new app can be overwhelming, and I want to make sure you have everything you need to get started.": "Sé que configurar una nueva app puede ser abrumador, y quiero asegurarme de que tengas todo lo que necesitas para empezar.",
|
||||
|
|
@ -993,6 +996,7 @@
|
|||
"Maintain and promptly update your account information": "Mantén y actualiza prontamente tu información de cuenta",
|
||||
"Maintain and promptly update your account\\n information": "Mantén y actualiza puntualmente la información de tu cuenta",
|
||||
"Make child categories top level": "Convertir las categorías hijas en categorías principales",
|
||||
"mail support": "escribir a soporte",
|
||||
"Making your money talk...": "Haciendo que tu dinero hable...",
|
||||
"Manage Plan": "Gestionar Plan",
|
||||
"Manage Subscription": "Gestionar Suscripción",
|
||||
|
|
@ -1133,6 +1137,7 @@
|
|||
"Once your account is deleted, all of its resources\\n and data will also be permanently deleted. Please\\n enter your password to confirm you would like to\\n permanently delete your account.": "Una vez que tu cuenta sea eliminada, todos sus recursos y datos también serán eliminados permanentemente. Por favor, introduce tu contraseña para confirmar que deseas eliminar permanentemente tu cuenta.",
|
||||
"Once your account is deleted, you will be signed out\\n and your access will be disabled. Your data will\\n remain in the database. Please enter your password\\n to confirm you would like to mark your account as\\n deleted.": "Una vez que tu cuenta se marque como eliminada, se cerrará tu sesión y tu acceso quedará desactivado. Tus datos permanecerán en la base de datos. Por favor, introduce tu contraseña para confirmar que deseas marcar tu cuenta como eliminada.",
|
||||
"Only apply to uncategorized": "Aplicar solo a las no categorizadas",
|
||||
"Only continue if you are connecting a different account.": "Continúa solo si vas a conectar una cuenta diferente.",
|
||||
"Only you know this password. It never leaves your device.": "Solo tú conoces esta contraseña. Nunca sale de tu dispositivo.",
|
||||
"Open menu": "Abrir menú",
|
||||
"Open source": "Código abierto",
|
||||
|
|
@ -1561,6 +1566,7 @@
|
|||
"This password will encrypt all your financial data. Make it strong and memorable — we can't recover it for you.": "Esta contraseña encriptará todos tus datos financieros. Hazla fuerte y memorable: no podemos recuperarla por ti.",
|
||||
"This setting follows the selected category type and cannot be changed here.": "Esta configuración sigue el tipo de categoría seleccionado y no se puede cambiar aquí.",
|
||||
"This transaction was imported from a file. The description cannot be modified.": "Esta transacción fue importada de un archivo. La descripción no se puede modificar.",
|
||||
"This may replace your existing connection": "Esto podría reemplazar tu conexión activa",
|
||||
"This transaction was imported from a\\n file. The description cannot be\\n modified.": "Esta transacción fue importada desde un archivo. La descripción no se puede modificar.",
|
||||
"This transfer category stays out of the Sankey money flow chart.": "Esta categoría de transferencia queda fuera del gráfico Sankey de flujo de dinero.",
|
||||
"This transfer category stays out of the cashflow chart.": "Esta categoría de transferencia queda fuera del gráfico de flujo de caja.",
|
||||
|
|
@ -1858,7 +1864,7 @@
|
|||
"You agree to indemnify, defend, and hold\\n harmless Whisper Money and its officers,\\n directors, employees, and agents from any\\n claims, liabilities, damages, losses, and\\n expenses, including reasonable legal fees,\\n arising out of or related to your use of the\\n service, violation of these Terms, or violation\\n of any rights of another party.": "Aceptas indemnizar, defender y eximir de responsabilidad a Whisper Money y sus directivos, directores, empleados y agentes de cualquier reclamación, responsabilidad, daño, pérdida y gasto, incluidos los honorarios legales razonables, derivados de o relacionados con tu uso del servicio, la violación de estos Términos o la violación de los derechos de otra parte.",
|
||||
"You agree to pay all applicable fees as described at the time of purchase": "Aceptas pagar todas las tarifas aplicables según se describan en el momento de la compra",
|
||||
"You agree to pay all applicable fees as\\n described at the time of purchase": "Aceptas pagar todas las tarifas aplicables tal como se describen en el momento de la compra",
|
||||
"You already have a connection with this bank. Reconnect it.": "Ya tienes una conexión con este banco, reconéctala.",
|
||||
"You already have an active connection with this bank. If you authorize with the same bank login, the previous connection will stop working.": "Ya tienes una conexión activa con este banco. Si autorizas con el mismo acceso del banco, la conexión anterior dejará de funcionar.",
|
||||
"You already have accounts set up. Let's continue with the onboarding.": "Ya tienes cuentas configuradas. Continuemos con la configuración.",
|
||||
"You are always the owner of your data": "Siempre eres el dueño de tus datos",
|
||||
"You are being redirected in 3 seconds.": "Serás redirigido en 3 segundos.",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
import type { BankingConnection } from '@/types/banking';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ConnectAccountDialog } from './connect-account-dialog';
|
||||
|
||||
globalThis.ResizeObserver ??= class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
vi.mock('@/utils/i18n', () => ({
|
||||
__: (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/csrf', () => ({
|
||||
getCsrfToken: () => 'test-token',
|
||||
}));
|
||||
|
||||
// Radix Select relies on pointer APIs missing in jsdom; a native select keeps
|
||||
// the country step driveable without that brittleness.
|
||||
vi.mock('@/components/ui/select', () => ({
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onValueChange: (value: string) => void;
|
||||
}) => (
|
||||
<select
|
||||
data-testid="country-select"
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
),
|
||||
SelectTrigger: () => null,
|
||||
SelectValue: () => null,
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
),
|
||||
SelectItem: ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
}) => <option value={value}>{children}</option>,
|
||||
}));
|
||||
|
||||
function liveBbvaConnection(): BankingConnection {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
provider: 'enablebanking',
|
||||
aspsp_name: 'BBVA',
|
||||
aspsp_country: 'ES',
|
||||
status: 'active',
|
||||
valid_until: null,
|
||||
last_synced_at: null,
|
||||
error_message: null,
|
||||
accounts_count: 1,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
}
|
||||
|
||||
async function reachBankStep(connections: BankingConnection[]) {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [
|
||||
{
|
||||
name: 'BBVA',
|
||||
country: 'ES',
|
||||
logo: '',
|
||||
maximum_consent_validity: null,
|
||||
},
|
||||
{
|
||||
name: 'CaixaBank',
|
||||
country: 'ES',
|
||||
logo: '',
|
||||
maximum_consent_validity: null,
|
||||
},
|
||||
],
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
render(
|
||||
<ConnectAccountDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
connections={connections}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('country-select'), {
|
||||
target: { value: 'ES' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: /BBVA/ }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
}
|
||||
|
||||
describe('ConnectAccountDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('keeps an already-connected bank selectable and badges it', async () => {
|
||||
await reachBankStep([liveBbvaConnection()]);
|
||||
|
||||
const bbva = screen.getByRole('button', { name: /BBVA/ });
|
||||
expect(bbva).not.toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByText('Already connected')).toBeInTheDocument();
|
||||
|
||||
// A bank without a live connection shows no badge.
|
||||
const caixa = screen.getByRole('button', { name: /CaixaBank/ });
|
||||
expect(caixa).not.toHaveTextContent('Already connected');
|
||||
});
|
||||
|
||||
it('gates Connect behind the replace acknowledgement', async () => {
|
||||
await reachBankStep([liveBbvaConnection()]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /BBVA/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
expect(
|
||||
screen.getByText('This may replace your existing connection'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const connect = screen.getByRole('button', { name: 'Connect' });
|
||||
expect(connect).toBeDisabled();
|
||||
|
||||
fireEvent.click(screen.getByRole('checkbox'));
|
||||
expect(connect).toBeEnabled();
|
||||
});
|
||||
|
||||
it('does not warn when connecting a fresh bank', async () => {
|
||||
await reachBankStep([liveBbvaConnection()]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /CaixaBank/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
expect(
|
||||
screen.queryByText('This may replace your existing connection'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { BankLogo } from '@/components/bank-logo';
|
||||
import { IntegrationRequestsDrawer } from '@/components/integration-requests/integration-requests-drawer';
|
||||
import { ReplaceConnectionWarning } from '@/components/open-banking/replace-connection-warning';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -18,11 +20,6 @@ import {
|
|||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import {
|
||||
alreadyConnectedBankNames,
|
||||
hasLiveConnectionForProvider,
|
||||
|
|
@ -126,6 +123,7 @@ export function ConnectAccountDialog({
|
|||
const [coinbaseKeyName, setCoinbaseKeyName] = useState('');
|
||||
const [coinbasePrivateKey, setCoinbasePrivateKey] = useState('');
|
||||
const [wiseApiToken, setWiseApiToken] = useState('');
|
||||
const [acknowledgedReplace, setAcknowledgedReplace] = useState(false);
|
||||
|
||||
const isIndexaCapital = useMemo(
|
||||
() => selectedBank?.name === 'Indexa Capital',
|
||||
|
|
@ -154,6 +152,15 @@ export function ConnectAccountDialog({
|
|||
[connections],
|
||||
);
|
||||
|
||||
const isAlreadyConnected = useMemo(
|
||||
() => !!selectedBank && connectedBankNames.has(selectedBank.name),
|
||||
[selectedBank, connectedBankNames],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAcknowledgedReplace(false);
|
||||
}, [selectedBank]);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setStep('country');
|
||||
setCountry('');
|
||||
|
|
@ -171,6 +178,7 @@ export function ConnectAccountDialog({
|
|||
setCoinbaseKeyName('');
|
||||
setCoinbasePrivateKey('');
|
||||
setWiseApiToken('');
|
||||
setAcknowledgedReplace(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -433,52 +441,35 @@ export function ConnectAccountDialog({
|
|||
institution.name,
|
||||
);
|
||||
|
||||
const item = (
|
||||
return (
|
||||
<button
|
||||
key={institution.name}
|
||||
type="button"
|
||||
aria-disabled={isConnected}
|
||||
className={`flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors ${
|
||||
isConnected
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'hover:bg-accent'
|
||||
} ${
|
||||
className={`flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent ${
|
||||
selectedBank?.name ===
|
||||
institution.name
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (isConnected) {
|
||||
return;
|
||||
}
|
||||
setSelectedBank(institution);
|
||||
}}
|
||||
onClick={() =>
|
||||
setSelectedBank(institution)
|
||||
}
|
||||
>
|
||||
<BankLogo
|
||||
src={institution.logo}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
<span>{institution.name}</span>
|
||||
{isConnected && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
>
|
||||
{__('Already connected')}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!isConnected) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip key={institution.name}>
|
||||
<TooltipTrigger asChild>
|
||||
{item}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{__(
|
||||
'You already have a connection with this bank. Reconnect it.',
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{filteredInstitutions.length === 0 && (
|
||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||
|
|
@ -555,6 +546,15 @@ export function ConnectAccountDialog({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{isAlreadyConnected && (
|
||||
<ReplaceConnectionWarning
|
||||
acknowledged={acknowledgedReplace}
|
||||
onAcknowledgedChange={
|
||||
setAcknowledgedReplace
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isIndexaCapital && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api-token">
|
||||
|
|
@ -777,6 +777,8 @@ export function ConnectAccountDialog({
|
|||
onClick={handleAuthorize}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
(isAlreadyConnected &&
|
||||
!acknowledgedReplace) ||
|
||||
(isIndexaCapital && !apiToken) ||
|
||||
(isBinance &&
|
||||
(!apiKey || !apiSecret)) ||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { BankLogo } from '@/components/bank-logo';
|
||||
import { ReplaceConnectionWarning } from '@/components/open-banking/replace-connection-warning';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
|
@ -105,6 +107,7 @@ export function ConnectAccountInline({
|
|||
const [bitpandaApiKey, setBitpandaApiKey] = useState('');
|
||||
const [coinbaseKeyName, setCoinbaseKeyName] = useState('');
|
||||
const [coinbasePrivateKey, setCoinbasePrivateKey] = useState('');
|
||||
const [acknowledgedReplace, setAcknowledgedReplace] = useState(false);
|
||||
|
||||
const isIndexaCapital = useMemo(
|
||||
() => selectedBank?.name === 'Indexa Capital',
|
||||
|
|
@ -123,6 +126,20 @@ export function ConnectAccountInline({
|
|||
[selectedBank],
|
||||
);
|
||||
|
||||
const connectedBankNames = useMemo(
|
||||
() => alreadyConnectedBankNames(connections),
|
||||
[connections],
|
||||
);
|
||||
|
||||
const isAlreadyConnected = useMemo(
|
||||
() => !!selectedBank && connectedBankNames.has(selectedBank.name),
|
||||
[selectedBank, connectedBankNames],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAcknowledgedReplace(false);
|
||||
}, [selectedBank]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery) {
|
||||
setFilteredInstitutions(
|
||||
|
|
@ -170,8 +187,6 @@ export function ConnectAccountInline({
|
|||
|
||||
const data = await response.json();
|
||||
|
||||
const connectedEnableBankingNames =
|
||||
alreadyConnectedBankNames(connections);
|
||||
const hasProvider = (provider: string) =>
|
||||
hasLiveConnectionForProvider(connections, provider);
|
||||
|
||||
|
|
@ -198,7 +213,7 @@ export function ConnectAccountInline({
|
|||
if (institution.name === 'Indexa Capital') {
|
||||
return !hasProvider('indexacapital');
|
||||
}
|
||||
return !connectedEnableBankingNames.has(institution.name);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
|
|
@ -358,6 +373,14 @@ export function ConnectAccountInline({
|
|||
className="h-6 w-6"
|
||||
/>
|
||||
<span>{institution.name}</span>
|
||||
{connectedBankNames.has(institution.name) && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
>
|
||||
{__('Already connected')}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{filteredInstitutions.length === 0 && (
|
||||
|
|
@ -429,6 +452,13 @@ export function ConnectAccountInline({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{isAlreadyConnected && (
|
||||
<ReplaceConnectionWarning
|
||||
acknowledged={acknowledgedReplace}
|
||||
onAcknowledgedChange={setAcknowledgedReplace}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isIndexaCapital && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api-token">{__('API Token')}</Label>
|
||||
|
|
@ -593,6 +623,7 @@ export function ConnectAccountInline({
|
|||
onClick={handleAuthorize}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
(isAlreadyConnected && !acknowledgedReplace) ||
|
||||
(isIndexaCapital && !apiToken) ||
|
||||
(isBinance && (!apiKey || !apiSecret)) ||
|
||||
(isBitpanda && !bitpandaApiKey) ||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
|
||||
const DISCORD_URL = 'https://discord.gg/m8hUhx6D9D';
|
||||
const SUPPORT_MAILTO = 'mailto:support@whisper.money';
|
||||
|
||||
interface ReplaceConnectionWarningProps {
|
||||
acknowledged: boolean;
|
||||
onAcknowledgedChange: (acknowledged: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown when the user is about to authorize a bank they already have a live
|
||||
* connection to. Authorizing with the same bank login replaces the existing
|
||||
* session, so the previous connection stops working. The checkbox gates the
|
||||
* Connect button to avoid breaking a working connection by accident.
|
||||
*
|
||||
* Only the title and icon are red; the body stays neutral so the explanation
|
||||
* and support links read as guidance rather than an error.
|
||||
*/
|
||||
export function ReplaceConnectionWarning({
|
||||
acknowledged,
|
||||
onAcknowledgedChange,
|
||||
}: ReplaceConnectionWarningProps) {
|
||||
return (
|
||||
<Alert className="text-destructive">
|
||||
<TriangleAlert />
|
||||
<AlertTitle>
|
||||
{__('This may replace your existing connection')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
{__(
|
||||
'You already have an active connection with this bank. If you authorize with the same bank login, the previous connection will stop working.',
|
||||
)}{' '}
|
||||
<strong>
|
||||
{__(
|
||||
'Only continue if you are connecting a different account.',
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
<p className="my-1">
|
||||
{__('Having trouble with this connection?')}{' '}
|
||||
<a
|
||||
href={DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
{__('Join the community')}
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href={SUPPORT_MAILTO} className="text-primary underline">
|
||||
{__('mail support')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<label className="mt-2 flex items-start gap-2 text-foreground">
|
||||
<Checkbox
|
||||
checked={acknowledged}
|
||||
onCheckedChange={(checked) =>
|
||||
onAcknowledgedChange(checked === true)
|
||||
}
|
||||
/>
|
||||
<span>{__('I will use a different bank login')}</span>
|
||||
</label>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue