From 4d77216dda06e2d4b7e49270593599dfa633471e Mon Sep 17 00:00:00 2001
From: waterWang <672684719@qq.com>
Date: Sat, 22 Aug 2026 15:18:54 +0800
Subject: [PATCH] feat(frontend): add offline and reconnect state for transfer
mutations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Blocks unsafe transfer submissions while the browser is offline, shows an
honest 'unknown status' message when a mid-flight disconnect interrupts a
submission, and auto-reconciles transfers when connectivity returns:
- Mount the global ConnectionBanner in App so users always see connectivity
status.
- SendMoney: disable the submit button offline (labelled 'Offline — Reconnect
to send'), block programmatic submits with an inline notice, and distinguish
'connection lost while sending' (unknown outcome) from a generic submit
failure. On reconnect, clear stale errors and surface a transient
'form was not submitted' notice instead of silently resubmitting.
- Transfers: track offline transitions and automatically reload the list on
reconnect to reconcile the real status of transfers that may have settled
while the connection was down, with a visible sync notice.
- Add integration coverage: offline block, banner visibility, submit-handler
guard, re-enable on reconnect, no auto-resubmit, and unknown-status
messaging on mid-flight disconnect.
Closes #286
---
src/App.jsx | 2 +
src/pages/SendMoney.css | 27 +++
src/pages/SendMoney.jsx | 82 ++++++++-
src/pages/Transfers.css | 14 ++
src/pages/Transfers.jsx | 28 +++
test/integration/offline-reconnect.test.jsx | 189 ++++++++++++++++++++
6 files changed, 338 insertions(+), 4 deletions(-)
create mode 100644 test/integration/offline-reconnect.test.jsx
diff --git a/src/App.jsx b/src/App.jsx
index 3bfe35b..a988000 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,6 +5,7 @@ import ErrorBoundary from './components/ErrorBoundary.jsx';
import Navbar from './components/Navbar.jsx';
import Sidebar from './components/Sidebar.jsx';
import Footer from './components/Footer.jsx';
+import ConnectionBanner from './components/ConnectionBanner.jsx';
import Home from './pages/Home.jsx';
import SendMoney from './pages/SendMoney.jsx';
import Transfers from './pages/Transfers.jsx';
@@ -25,6 +26,7 @@ export default function App() {
{showSidebar && }
+
diff --git a/src/pages/SendMoney.css b/src/pages/SendMoney.css
index b6dc31f..20fea24 100644
--- a/src/pages/SendMoney.css
+++ b/src/pages/SendMoney.css
@@ -83,3 +83,30 @@
padding: 2rem;
text-align: center;
}
+
+/* Offline and reconnection notices */
+.send-offline-notice {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1rem;
+ margin-bottom: 1rem;
+ background: rgba(234, 179, 8, 0.12);
+ border: 1px solid rgba(234, 179, 8, 0.3);
+ border-radius: 8px;
+ color: #fde68a;
+ font-size: 0.875rem;
+}
+
+.send-reconnected-notice {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1rem;
+ margin-bottom: 1rem;
+ background: rgba(34, 197, 94, 0.12);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ border-radius: 8px;
+ color: #86efac;
+ font-size: 0.875rem;
+}
diff --git a/src/pages/SendMoney.jsx b/src/pages/SendMoney.jsx
index 344b26c..34859c8 100644
--- a/src/pages/SendMoney.jsx
+++ b/src/pages/SendMoney.jsx
@@ -1,4 +1,4 @@
-import { useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import TextField from '../components/TextField.jsx';
import CurrencySelect from '../components/CurrencySelect.jsx';
@@ -14,6 +14,7 @@ import {
} from '../utils/validate.js';
import { useWallet } from '../hooks/useWallet.js';
import { useTransfers } from '../hooks/useTransfers.js';
+import { useOnlineStatus } from '../hooks/useOnlineStatus.js';
import { useApp } from '../context/AppContext.jsx';
import { useDebouncedValue } from '../hooks/useDebouncedValue.js';
import { DEFAULT_SOURCE, DEFAULT_DEST } from '../constants/currencies.js';
@@ -27,6 +28,7 @@ export default function SendMoney() {
const { wallet, isConnected, connect } = useWallet();
const { addTransfer } = useTransfers();
const { locale } = useApp();
+ const isOnline = useOnlineStatus();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
@@ -36,6 +38,12 @@ export default function SendMoney() {
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState(null);
const submissionLock = useRef(false);
+ const wasOffline = useRef(false);
+
+ // True when the form just recovered from a disconnected state. Used to
+ // surface an honest, non-blocking "back online" notice after the browser
+ // regains connectivity (the transfer has NOT been submitted automatically).
+ const [justReconnected, setJustReconnected] = useState(false);
// Debounce the amount so the quote isn't rebuilt on every keystroke.
const debouncedAmount = useDebouncedValue(amount, 250);
@@ -84,11 +92,37 @@ export default function SendMoney() {
return isValid;
}
+ /**
+ * Track connectivity transitions. When the browser comes back online we do
+ * NOT blindly resubmit the form (that would duplicate the transfer) — we
+ * only clear the stale "offline" error state and inform the user.
+ */
+ useEffect(() => {
+ const recovered = wasOffline.current && isOnline;
+ wasOffline.current = !isOnline;
+ if (recovered) {
+ setSubmitError(null);
+ setJustReconnected(true);
+ setTimeout(() => setJustReconnected(false), 4000);
+ }
+ }, [isOnline]);
+
async function handleSubmit(e) {
e.preventDefault();
if (submissionLock.current) return;
setSubmitError(null);
+ setJustReconnected(false);
+
+ // Never start a transfer while offline: submitting blind would either
+ // fail confusingly or, worse, appear to succeed while nothing happened.
+ if (!isOnline) {
+ setSubmitError(
+ "You're offline. Connect to the internet before sending money.",
+ );
+ return;
+ }
+
if (!validate()) return;
submissionLock.current = true;
@@ -111,7 +145,18 @@ export default function SendMoney() {
});
navigate('/transfers');
} catch (err) {
- setSubmitError('Could not submit the transfer. Please try again.');
+ // A transfer can be interrupted mid-signature by a connection drop.
+ // The honest message here is "unknown", not "failed": the backend may
+ // have accepted the transfer even though the response never arrived.
+ // The transfers page reconciles real status on reconnect.
+ // Read the current connectivity directly (not from the render closure)
+ // so that a mid-flight disconnect produces the correct message.
+ const connectedNow = typeof navigator !== 'undefined' && navigator.onLine;
+ setSubmitError(
+ connectedNow
+ ? 'Could not submit the transfer. Please try again.'
+ : 'Connection lost while sending. Reconnect to check your transfer status.',
+ );
} finally {
submissionLock.current = false;
setSubmitting(false);
@@ -139,6 +184,28 @@ export default function SendMoney() {
)}
+ {!isOnline && (
+
+ ⚠️ No internet connection. Send Money is disabled until you
+ reconnect.
+
+ )}
+
+ {justReconnected && (
+
+ ✓ Back online. Your form was not submitted while you were
+ offline — review it and send when ready.
+