Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/components/landing/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const navItems = [
]

const Navbar: React.FC = () => {
const { publicKey, connected, connectionMethod, disconnect } = useWallet()
const { publicKey, connected, ready, connectionMethod, disconnect } = useWallet()
const navigate = useNavigate()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [mobileSearchOpen, setMobileSearchOpen] = useState(false)
Expand Down Expand Up @@ -147,7 +147,7 @@ const Navbar: React.FC = () => {
<Search size={15} />
</motion.button>

{connected && publicKey ? (
{connected && publicKey && ready ? (
<div className="flex items-center gap-0.5 bg-background-surface/40 border border-border-subtle/60 rounded-md pl-2 pr-1 py-0.5">
<span className="relative flex w-1.5 h-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent-green opacity-50" />
Expand Down
45 changes: 28 additions & 17 deletions frontend/src/components/layout/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const TopNav: React.FC<TopNavProps> = ({
isMobile,
isDrawerOpen = false,
}) => {
const { publicKey, connected, connectionMethod, disconnect } = useWallet()
const { publicKey, connected, ready, connectionMethod, disconnect } = useWallet()

const getTitle = () => {
const path = window.location.pathname
Expand Down Expand Up @@ -75,23 +75,34 @@ const TopNav: React.FC<TopNavProps> = ({

<div className="nav-right">
{connected && publicKey ? (
<>
<span className="wallet-chip connected" id="wallet-pubkey-display">
{truncateKey(publicKey)}
</span>
{connectionMethod && (
<span className="wallet-chip connected" style={{ fontSize: '10px', padding: '2px 6px' }}>
{connectionMethod === 'freighter' ? 'Freighter' : 'Secret Key'}
ready ? (
<>
<span className="wallet-chip connected" id="wallet-pubkey-display">
{truncateKey(publicKey)}
</span>
)}
<button
className="disconnect-btn"
onClick={disconnect}
id="btn-disconnect"
>
Disconnect
</button>
</>
{connectionMethod && (
<span className="wallet-chip connected" style={{ fontSize: '10px', padding: '2px 6px' }}>
{connectionMethod === 'freighter' ? 'Freighter' : 'Secret Key'}
</span>
)}
<button
className="disconnect-btn"
onClick={disconnect}
id="btn-disconnect"
>
Disconnect
</button>
</>
) : (
<>
<span className="wallet-chip connected" id="wallet-pubkey-display" style={{ opacity: 0.6 }}>
{truncateKey(publicKey)}
</span>
<span className="wallet-chip" style={{ fontSize: '10px', padding: '2px 6px', background: '#fef3c7', color: '#92400e' }}>
Reconnect Required
</span>
</>
)
) : (
<span className="wallet-chip disconnected" id="wallet-pubkey-display">
Not Connected
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/context/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface WalletContextType {
publicKey: string | null
keypair: Keypair | null
connected: boolean
ready: boolean
connectionMethod: ConnectionMethod | null
freighterAvailable: boolean
connect: (secretKey: string) => Promise<void>
Expand All @@ -40,6 +41,10 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const [freighterAvailable, setFreighterAvailable] = useState(false)

const connected = !!publicKey
// ready means the wallet can actually sign transactions:
// - Freighter: always ready when connected (Freighter handles signing)
// - Secret key: only ready when keypair is in memory (not serializable)
const ready = connected && (connectionMethod === 'freighter' || !!keypair)

useEffect(() => {
checkFreighterAvailable().then(setFreighterAvailable)
Expand Down Expand Up @@ -109,6 +114,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
publicKey,
keypair,
connected,
ready,
connectionMethod,
freighterAvailable,
connect,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/hooks/useWallet.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useWallet as useWalletContext } from '../context/WalletContext';

export const useWallet = () => {
const { publicKey, keypair, connected, connectionMethod, freighterAvailable, connect, connectFreighter, disconnect } = useWalletContext();
const { publicKey, keypair, connected, ready, connectionMethod, freighterAvailable, connect, connectFreighter, disconnect } = useWalletContext();
return {
address: publicKey,
publicKey,
keypair,
connected,
ready,
connectionMethod,
freighterAvailable,
connect,
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/pages/WalletPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,50 @@
align-items: flex-start;
}
}

/* Reconnect card (secret key lost after refresh) */
.reconnectCard {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 32px;
max-width: 440px;
}

.reconnectInfo {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.5;
margin: 0 0 16px;
}

.reconnectPubkey {
font-size: 0.8125rem;
color: var(--text-secondary);
margin: 0 0 16px;
word-break: break-all;
}

.reconnectPubkey code {
font-family: monospace;
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75rem;
}

.reconnectActions {
display: flex;
gap: 12px;
align-items: center;
margin-top: 16px;
}

.reconnectActions .connectButton {
flex: 1;
margin-top: 0;
}

.reconnectActions .disconnectButton {
margin-top: 0;
}
66 changes: 65 additions & 1 deletion frontend/src/pages/WalletPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import styles from './WalletPage.module.css'
const STELLAR_EXPLORER = 'https://stellar.expert/explorer/testnet'

function WalletPage() {
const { publicKey, connected, connectionMethod, freighterAvailable, connect, connectFreighter, disconnect } = useWallet()
const { publicKey, connected, ready, connectionMethod, freighterAvailable, connect, connectFreighter, disconnect } = useWallet()
const { balance, loading: balanceLoading, error: balanceError } = useWalletBalance(publicKey)
const { transactions, loading: txLoading, error: txError } = useTransactionHistory(publicKey)
const [copied, setCopied] = React.useState(false)
Expand Down Expand Up @@ -165,6 +165,70 @@ function WalletPage() {
)
}

// Reconnect prompt: wallet was previously connected via secret key but
// keypair is lost after page refresh (Keypair is not JSON-serializable).
if (!ready && connectionMethod === 'secret-key') {
return (
<div className={styles.page}>
<div className={styles.header}>
<h1 className={styles.title}>
<Wallet size={24} />
Wallet
</h1>
<p className={styles.subtitle}>Re-enter your secret key to resume.</p>
</div>

<div className={styles.reconnectCard}>
<p className={styles.reconnectInfo}>
Your wallet was previously connected via{' '}
<strong>Secret Key</strong>. Since the key cannot be stored in the
browser, please re-enter it to continue.
</p>
{publicKey && (
<p className={styles.reconnectPubkey}>
Public Key: <code>{publicKey}</code>
</p>
)}
<form onSubmit={handleConnect}>
<label className={styles.fieldLabel} htmlFor="reconnect-secret-key">
Stellar Secret Key
</label>
<input
id="reconnect-secret-key"
className={styles.secretInput}
type="password"
placeholder="SABCD...5678"
value={secretInput}
onChange={(e) => setSecretInput(e.target.value)}
aria-describedby="reconnect-error"
/>
{connectError && (
<p id="reconnect-error" className={styles.error} role="alert">
{connectError}
</p>
)}
<div className={styles.reconnectActions}>
<button
type="submit"
className={styles.connectButton}
disabled={connecting || !secretInput.trim()}
>
{connecting ? 'Connecting...' : 'Reconnect'}
</button>
<button
type="button"
className={styles.disconnectButton}
onClick={disconnect}
>
Disconnect
</button>
</div>
</form>
</div>
</div>
)
}

return (
<div className={styles.page}>
<div className={styles.header}>
Expand Down
Loading