diff --git a/.storybook/decorators/withStellarWallet.tsx b/.storybook/decorators/withStellarWallet.tsx index 15109a2..48f1528 100644 --- a/.storybook/decorators/withStellarWallet.tsx +++ b/.storybook/decorators/withStellarWallet.tsx @@ -6,6 +6,7 @@ interface StellarWalletOverrides { isConnected?: boolean; isInstalled?: boolean | null; isNetworkMismatch?: boolean; + freighterNetwork?: string | null; connect?: () => Promise; disconnect?: () => void; signMessage?: (message: string) => Promise; @@ -25,6 +26,7 @@ export function withStellarWallet(overrides: StellarWalletOverrides = {}): Decor isConnected: overrides.isConnected ?? address !== null, isInstalled: overrides.isInstalled !== undefined ? overrides.isInstalled : true, isNetworkMismatch: overrides.isNetworkMismatch ?? false, + freighterNetwork: overrides.freighterNetwork ?? null, connect: overrides.connect ?? (async () => {}), disconnect: overrides.disconnect ?? (() => {}), signMessage: overrides.signMessage ?? (async () => new Uint8Array(64)), diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 7c4e8a5..551b16e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { ChainSwitcher } from './ChainSwitcher'; import { WalletConnect } from './WalletConnect'; import { LocaleSwitcher } from './LocaleSwitcher'; +import { NetworkChip } from './NetworkChip'; import { useTheme } from '@/context/ThemeContext'; export function Header() { @@ -83,6 +84,7 @@ export function Header() {
+
+ + + + ); +} diff --git a/src/components/StellarReceive.tsx b/src/components/StellarReceive.tsx index a7857ec..5ac342e 100644 --- a/src/components/StellarReceive.tsx +++ b/src/components/StellarReceive.tsx @@ -38,6 +38,7 @@ import type { ImportResult } from '@/lib/stealthLabels'; import { KeyVault } from '@/vault'; import { STELLAR_ASSETS, getAssetByKey, parseAssetBalances } from '@/lib/stellar/assets'; import { useStellarNotifications } from '@/hooks/useStellarNotifications'; +import { NetworkMismatchModal } from '@/components/NetworkMismatchModal'; import { useStealthLabels } from '@/hooks/useStealthLabels'; import { StellarBatchWithdrawModal } from '@/components/StellarBatchWithdrawModal'; import { createStellarQrUri } from '@/utils/qr'; @@ -178,7 +179,7 @@ function StellarMatchCardContainer({ onToggleSelect?: () => void; }) { const { t } = useTranslation(); - const { address, signTransaction } = useStellarWallet(); + const { address, signTransaction, isNetworkMismatch } = useStellarWallet(); const [balances, setBalances] = useState>({}); const [balanceState, setBalanceState] = useState<'loading' | 'loaded' | 'error'>('loading'); const [withdrawAssetKey, setWithdrawAssetKey] = useState('XLM'); @@ -192,6 +193,7 @@ function StellarMatchCardContainer({ const [retryStatus, setRetryStatus] = useState(''); const [showKey, setShowKey] = useState(false); const [showSponsorPrompt, setShowSponsorPrompt] = useState(false); + const [showNetworkModal, setShowNetworkModal] = useState(false); const { upsert } = useActivity(); const scalarHex = match.stealthPrivateScalar.toString(16).padStart(64, '0'); @@ -233,6 +235,12 @@ function StellarMatchCardContainer({ const handleWithdraw = async () => { if (!dest) return; + + if (isNetworkMismatch) { + setShowNetworkModal(true); + return; + } + setError(''); setRetryStatus(''); setWithdrawing(true); @@ -401,6 +409,12 @@ function StellarMatchCardContainer({ const handleSponsoredWithdraw = async () => { if (!dest || !address) return; + + if (isNetworkMismatch) { + setShowNetworkModal(true); + return; + } + setError(''); setRetryStatus(''); setWithdrawing(true); @@ -648,13 +662,15 @@ function StellarMatchCardContainer({ showPrivacyWarning={showPrivacyWarning} onDismissPrivacyWarning={onDismissPrivacyWarning} /> + {showNetworkModal && setShowNetworkModal(false)} />} ); } export function StellarReceive() { const { t } = useTranslation(); - const { address, isConnected, signMessage, signTransaction } = useStellarWallet(); + const { address, isConnected, signMessage, signTransaction, isNetworkMismatch } = + useStellarWallet(); const { stellarKeys, stellarMetaAddress, setStellarKeys, setStellarMetaAddress } = useStealthKeys(); const addActivity = useActivityStore((state) => state.addEntry); @@ -676,6 +692,7 @@ export function StellarReceive() { }, []); const [hasScanned, setHasScanned] = useState(false); const [error, setError] = useState(''); + const [showNetworkModal, setShowNetworkModal] = useState(false); const [retryStatus, setRetryStatus] = useState(''); const [isRegistering, setIsRegistering] = useState(false); const [isRegSuccess, setIsRegSuccess] = useState(false); @@ -1049,6 +1066,12 @@ export function StellarReceive() { const registerOnChain = useCallback(async () => { if (!stellarKeys || !address) return; + + if (isNetworkMismatch) { + setShowNetworkModal(true); + return; + } + setIsRegistering(true); setError(''); setRetryStatus(''); @@ -1450,6 +1473,7 @@ export function StellarReceive() { onClose={() => setShowQRModal(false)} /> )} + {showNetworkModal && setShowNetworkModal(false)} />} setIsBatchModalOpen(false)} diff --git a/src/components/StellarSend.tsx b/src/components/StellarSend.tsx index 8f8514a..ec85144 100644 --- a/src/components/StellarSend.tsx +++ b/src/components/StellarSend.tsx @@ -1,4 +1,5 @@ // @ts-nocheck (temporary: wave-6 merges left stale symbol names; unblocks CI) +import { NetworkMismatchModal } from '@/components/NetworkMismatchModal'; import { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { QrReader } from 'react-qr-reader'; @@ -103,7 +104,7 @@ export function StellarSend() { const paramMemo = searchParams.get('memo'); const paramExp = searchParams.get('exp'); - const { address, isConnected, signTransaction } = useStellarWallet(); + const { address, isConnected, signTransaction, isNetworkMismatch } = useStellarWallet(); const addActivity = useActivityStore((state) => state.addEntry); const updateActivity = useActivityStore((state) => state.updateStatus); const [recipient, setRecipient] = useState(paramTo || ''); @@ -114,6 +115,7 @@ export function StellarSend() { const { isKnownAddress, addContact } = useContacts(); const { isKnownRecipient, addToHistory } = useNameHistory(); const [error, setError] = useState(''); + const [showNetworkModal, setShowNetworkModal] = useState(false); const [, setTouched] = useState({ recipient: false, amount: false }); const [, setSubmitAttempted] = useState(false); @@ -491,6 +493,11 @@ export function StellarSend() { return; } + if (isNetworkMismatch) { + setShowNetworkModal(true); + return; + } + if (!canSubmit) { setError(validationError || 'Enter valid send details'); return; @@ -943,6 +950,7 @@ export function StellarSend() { )} )} + {showNetworkModal && setShowNetworkModal(false)} />} {isScanningQR && (
(MOCK_DEPOSITS); const [claimingId, setClaimingId] = useState(null); const [claimState, setClaimState] = useState('idle'); const [error, setError] = useState(''); const [txHash, setTxHash] = useState(null); + const [showNetworkModal, setShowNetworkModal] = useState(false); const handleClaim = useCallback( async (depositId: string) => { @@ -49,6 +51,11 @@ export function StellarVaultClaim() { return; } + if (isNetworkMismatch) { + setShowNetworkModal(true); + return; + } + setClaimingId(depositId); setClaimState('signing'); setError(''); @@ -80,7 +87,7 @@ export function StellarVaultClaim() { setClaimingId(null); } }, - [address, signMessage], + [address, signMessage, isNetworkMismatch], ); const reset = () => { @@ -221,6 +228,8 @@ export function StellarVaultClaim() {
))} + + {showNetworkModal && setShowNetworkModal(false)} />} ); } diff --git a/src/context/StellarWalletContext.tsx b/src/context/StellarWalletContext.tsx index 2be13e8..07ecd31 100644 --- a/src/context/StellarWalletContext.tsx +++ b/src/context/StellarWalletContext.tsx @@ -9,6 +9,7 @@ interface StellarWalletContextValue { isConnected: boolean; isInstalled: boolean | null; // null while the initial extension check is in-flight isNetworkMismatch: boolean; + freighterNetwork: string | null; connect: () => Promise; disconnect: () => void; signMessage: (message: string) => Promise; @@ -30,7 +31,7 @@ export function StellarWalletProvider({ children }: { children: React.ReactNode const [address, setAddress] = useState(null); const [isInstalled, setIsInstalled] = useState(null); const [freighterPassphrase, setFreighterPassphrase] = useState(null); - + const [freighterNetwork, setFreighterNetwork] = useState(null); const tabId = useRef(crypto.randomUUID()); const channelRef = useRef(null); const passPhraseRef = useRef(null); @@ -120,6 +121,10 @@ export function StellarWalletProvider({ children }: { children: React.ReactNode passPhraseRef.current = pass; } + if (details.network) { + setFreighterNetwork(details.network); + } + // Restore silently only if user has not explicitly disconnected this session const { isAllowed: allowed } = await freighter.isAllowed(); if (allowed && !manuallyDisconnected.current) { @@ -142,10 +147,12 @@ export function StellarWalletProvider({ children }: { children: React.ReactNode w.watch( ({ address: newAddr, + network: newNetwork, networkPassphrase: newPass, error, }: { address?: string; + network?: string; networkPassphrase?: string; error?: unknown; }) => { @@ -157,6 +164,9 @@ export function StellarWalletProvider({ children }: { children: React.ReactNode if (newPass && newPass !== passPhraseRef.current) { passPhraseRef.current = newPass; setFreighterPassphrase(newPass); + if (newNetwork) { + setFreighterNetwork(newNetwork); + } fireListeners(); channelRef.current?.postMessage({ type: 'NETWORK_CHANGED', @@ -318,6 +328,7 @@ export function StellarWalletProvider({ children }: { children: React.ReactNode isConnected, isInstalled, isNetworkMismatch, + freighterNetwork, connect, disconnect, signMessage,