diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx
index 5ecb1b8..462f561 100644
--- a/frontend/src/components/NavBar.jsx
+++ b/frontend/src/components/NavBar.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import DarkModeToggle from './DarkModeToggle';
import Tooltip from './Tooltip';
+import WalletConnectionProgress from './WalletConnectionProgress';
import { useAuth } from '../hooks/useFreighter';
const NAV_LINKS = [
@@ -19,7 +20,7 @@ function truncate(addr) {
}
function WalletIndicator() {
- const { publicKey, connect, disconnect } = useAuth();
+ const { publicKey, connect, disconnect, loading, connectionStep, error } = useAuth();
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const ref = useRef(null);
@@ -34,22 +35,29 @@ function WalletIndicator() {
if (!publicKey) {
return (
-
+
+
+ {loading && }
+ {!loading && error && }
+
);
}
diff --git a/frontend/src/components/WalletConnectionProgress.jsx b/frontend/src/components/WalletConnectionProgress.jsx
new file mode 100644
index 0000000..136bcaa
--- /dev/null
+++ b/frontend/src/components/WalletConnectionProgress.jsx
@@ -0,0 +1,66 @@
+import { CONNECTION_STEPS } from '../hooks/useFreighter';
+
+const STEP_ORDER = [
+ CONNECTION_STEPS.CHECKING,
+ CONNECTION_STEPS.REQUESTING_KEY,
+ CONNECTION_STEPS.REQUESTING_CHALLENGE,
+ CONNECTION_STEPS.SIGNING,
+ CONNECTION_STEPS.VERIFYING,
+];
+
+const spinnerStyle = {
+ width: 16,
+ height: 16,
+ border: '2px solid #334155',
+ borderTopColor: '#38bdf8',
+ borderRadius: '50%',
+ display: 'inline-block',
+ animation: 'spin 0.7s linear infinite',
+ flexShrink: 0,
+};
+
+/**
+ * Shown while wallet connection is in progress.
+ * Renders a spinner + current step message.
+ */
+export default function WalletConnectionProgress({ step, error }) {
+ if (!step && !error) return null;
+
+ return (
+
+ {error ? (
+ ⚠️
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ {error || step}
+
+ {!error && (
+
+ {STEP_ORDER.indexOf(step) + 1}/{STEP_ORDER.length}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/hooks/useFreighter.jsx b/frontend/src/hooks/useFreighter.jsx
index dc65857..19bf8b9 100644
--- a/frontend/src/hooks/useFreighter.jsx
+++ b/frontend/src/hooks/useFreighter.jsx
@@ -10,24 +10,37 @@ const AuthContext = createContext(null);
// Only persist publicKey and role — token is kept in memory only
const STORAGE_KEY = 'vaccichain_wallet';
+export const CONNECTION_STEPS = {
+ IDLE: null,
+ CHECKING: 'Checking Freighter wallet…',
+ REQUESTING_KEY: 'Requesting public key…',
+ REQUESTING_CHALLENGE: 'Requesting SEP-10 challenge…',
+ SIGNING: 'Waiting for wallet signature…',
+ VERIFYING: 'Verifying signature…',
+};
+
export function AuthProvider({ children }) {
const toast = useToast();
const [publicKey, setPublicKey] = useState(null);
const [role, setRole] = useState(null);
const [loading, setLoading] = useState(false);
+ const [connectionStep, setConnectionStep] = useState(CONNECTION_STEPS.IDLE);
const [error, setError] = useState(null);
const [freighterInstalled, setFreighterInstalled] = useState(() => typeof window !== 'undefined' && !!window.freighter);
// Token lives only in memory — never written to localStorage
const tokenRef = useRef(null);
- const runSep10 = useCallback(async (pk) => {
+ const runSep10 = useCallback(async (pk, onStep) => {
+ onStep?.(CONNECTION_STEPS.REQUESTING_CHALLENGE);
const challengeRes = await fetch('/v1/auth/sep10', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ public_key: pk }),
});
const { transaction, nonce } = await challengeRes.json();
+ onStep?.(CONNECTION_STEPS.SIGNING);
const signedXDR = await signTransaction(transaction, { network: 'TESTNET' });
+ onStep?.(CONNECTION_STEPS.VERIFYING);
const verifyRes = await fetch('/v1/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -39,16 +52,19 @@ export function AuthProvider({ children }) {
}, []);
const connect = useCallback(async () => {
+ if (loading) return; // prevent duplicate attempts
setLoading(true);
setError(null);
+ setConnectionStep(CONNECTION_STEPS.CHECKING);
try {
const connected = await isConnected();
if (!connected) {
setFreighterInstalled(false);
throw new Error('Freighter wallet not found. Please install it.');
}
+ setConnectionStep(CONNECTION_STEPS.REQUESTING_KEY);
const pk = await getPublicKey();
- const data = await runSep10(pk);
+ const data = await runSep10(pk, setConnectionStep);
setPublicKey(pk);
tokenRef.current = data.token;
setRole(data.role);
@@ -63,8 +79,9 @@ export function AuthProvider({ children }) {
throw e;
} finally {
setLoading(false);
+ setConnectionStep(CONNECTION_STEPS.IDLE);
}
- }, [runSep10, toast]);
+ }, [loading, runSep10, toast]);
const disconnect = useCallback(() => {
setPublicKey(null);
@@ -130,6 +147,7 @@ export function AuthProvider({ children }) {
apiFetch,
isConnected: isConnectedState,
loading,
+ connectionStep,
error,
}}>
{children}