diff --git a/mobile_app/components/nodes/BeaconRegistry.tsx b/mobile_app/components/nodes/BeaconRegistry.tsx index 2dd8d90a..b702bdc8 100644 --- a/mobile_app/components/nodes/BeaconRegistry.tsx +++ b/mobile_app/components/nodes/BeaconRegistry.tsx @@ -1,4 +1,4 @@ -import React, { memo, useState, useRef, useCallback, useEffect } from 'react'; +import React, { memo, useState, useRef, useCallback } from 'react'; import { Animated, KeyboardAvoidingView, Modal, Platform, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Feather } from '@expo/vector-icons'; import { fontFamily, useTheme } from '@/theme'; @@ -38,15 +38,14 @@ export const BeaconRegistry = memo(function BeaconRegistry({ initialActive: _ini const [stakeAmt, setStakeAmt] = useState(0.5); const [rawAmt, setRawAmt] = useState('0.5'); const amtInputRef = useRef(null); - const autoActivatedRef = useRef(false); const sheetAnim = useRef(new Animated.Value(0)).current; const stakeAnim = useRef(new Animated.Value(0)).current; - useEffect(() => { - if (!hasInternet || active || autoActivatedRef.current) return; - autoActivatedRef.current = true; - setBeaconMode(true); - }, [hasInternet, active, setBeaconMode]); + // Auto-activate on first internet was removed per AUDIT T10 / ROADMAP § 0.B.3: + // beacon mode carries trust implications (relaying others' traffic) so the + // user has to opt in via the Register-as-Beacon control. hasInternet is still + // consumed for UI affordances. + void hasInternet; const openModal = useCallback(() => { setModal(true); @@ -58,6 +57,11 @@ export const BeaconRegistry = memo(function BeaconRegistry({ initialActive: _ini .start(() => setModal(false)); }, [sheetAnim]); + const handleRegister = useCallback(() => { + setBeaconMode(true); + dismiss(); + }, [setBeaconMode, dismiss]); + const commitAmt = useCallback((v: number) => { const n = Math.max(0.5, Number.parseFloat(Math.max(0.5, v).toFixed(1))); setStakeAmt(n); @@ -196,14 +200,23 @@ export const BeaconRegistry = memo(function BeaconRegistry({ initialActive: _ini ))} - {/* CTA disabled in preview — biometric signing flow isn't wired yet. - Beacon mode still auto-activates via the hasInternet effect above. */} - [ + S.actionBtn, + { + backgroundColor: colors.primary, + opacity: pressed ? 0.8 : 1, + }, + ]} > - Preview — not yet active - + Enable Beacon Mode + diff --git a/mobile_app/components/nodes/constants.ts b/mobile_app/components/nodes/constants.ts index 899fe1a6..f315ede8 100644 --- a/mobile_app/components/nodes/constants.ts +++ b/mobile_app/components/nodes/constants.ts @@ -1,14 +1,7 @@ -type Iface = 'TCP' | 'BLE' | 'RNode'; - -export const NODES: { handle: string; hops: number; iface: Iface; signal: number; latency: string; beacon?: boolean; online?: boolean; weak?: boolean }[] = [ - { handle: '@beacon_prime', hops: 0, iface: 'TCP', signal: 4, beacon: true, latency: '12ms' }, - { handle: '@node_a1b2', hops: 1, iface: 'TCP', signal: 4, latency: '48ms' }, - { handle: '@node_7f3a', hops: 3, iface: 'RNode', signal: 3, online: true, latency: '112ms' }, - { handle: '@node_c91d', hops: 2, iface: 'BLE', signal: 3, latency: '89ms' }, - { handle: '@relay_e2f0', hops: 4, iface: 'RNode', signal: 2, latency: '340ms' }, - { handle: '@node_44ab', hops: 2, iface: 'BLE', signal: 3, latency: '76ms' }, - { handle: '@sensor_9812', hops: 5, iface: 'RNode', signal: 1, weak: true, latency: '612ms' }, -]; +// NODES fixture removed per AUDIT T7 / ROADMAP § 0.4. The radar populates from +// real peers; when the mesh isn't running NodesScreen renders an honest empty +// state instead of a 7-item fake-peer list. MAP_NODES / MAP_EDGES below are +// unrelated topology layout fixtures (currently unused). export const MAP_W = 320; export const MAP_H = 270; diff --git a/mobile_app/screens/MessagesScreen.tsx b/mobile_app/screens/MessagesScreen.tsx index 85c60752..4e1f05ba 100644 --- a/mobile_app/screens/MessagesScreen.tsx +++ b/mobile_app/screens/MessagesScreen.tsx @@ -311,6 +311,28 @@ export default function MessagesScreen() { clearTimeout(immediateTimers.current.get(seq)); immediateTimers.current.delete(seq); setSeqStates(m => new Map(m).set(seq, state)); + + // Flip enc to true only on confirmed delivery — never before the native + // module reports messageDelivered. AUDIT T9. + if (state === 'delivered') { + let msgIdForSeq: number | null = null; + for (const [mid, s] of idToSeqRef.current) { + if (s === seq) { msgIdForSeq = mid; break; } + } + if (msgIdForSeq !== null) { + const mid = msgIdForSeq; + const flip = (m: AnyMsg): AnyMsg => + m.id === mid && 'enc' in m ? { ...m, enc: true } : m; + // Update currently-rendered thread. + setMsgs(prev => prev.map(flip)); + // Also update any cached non-active thread that holds the sent bubble, + // so reopening the conversation still shows the lock after delivery. + threadsRef.current.forEach((thread, peerHash) => { + if (!thread.some(m => m.id === mid)) return; + threadsRef.current.set(peerHash, thread.map(flip)); + }); + } + } }, []); // Incoming messages + queue state events @@ -442,7 +464,10 @@ export default function MessagesScreen() { const sendMsg = useCallback(async (text: string) => { const now = new Date().toTimeString().slice(0, 8); const msgId = nextId(); - setMsgs(m => [...m, { id: msgId, from: 'me', me: true, time: now, text, enc: true }]); + // enc:false until the native module emits messageDelivered for this seq — + // a lock icon on a still-queued (or eventually failed) send is a false + // present-tense claim per AUDIT T9 / ROADMAP § 0.3. + setMsgs(m => [...m, { id: msgId, from: 'me', me: true, time: now, text, enc: false }]); if (!activePeerHex) { setMsgs(m => [...m, { id: nextId(), kind: 'sys' as const, text: 'no peer selected — open drawer and pick one' }]); return; diff --git a/mobile_app/screens/NodesScreen.tsx b/mobile_app/screens/NodesScreen.tsx index 92537b07..4f5a786d 100644 --- a/mobile_app/screens/NodesScreen.tsx +++ b/mobile_app/screens/NodesScreen.tsx @@ -11,7 +11,7 @@ import { MeshMap } from '@/components/nodes/MeshMap'; import { formatAgo } from '@/utils/time'; import { BeaconRegistry } from '@/components/nodes/BeaconRegistry'; import { PulseDot } from '@/components/ui/PulseDot'; -import { NODES, FILTERS } from '@/components/nodes/constants'; +import { FILTERS } from '@/components/nodes/constants'; import type { NodeData, Filter } from '@/components/nodes/types'; import { requestBLEPermissions } from '@/src/utils/blePermissions'; @@ -75,11 +75,19 @@ export default function NodesScreen() { // Stable node identity: only re-creates when peers change, not on timer ticks. // MeshMap receives this — topology layout only runs when peer set actually changes. + // + // Pre-AUDIT T7: when !isRunning we returned a 7-item fixture so the radar + // wouldn't look empty. That was a present-tense lie about the live mesh. + // Now we return [] and let the empty-state below speak for itself. const meshNodes = useMemo( - () => isRunning ? peers.map(peerToMapNode) : NODES, + () => isRunning ? peers.map(peerToMapNode) : [], [peers, isRunning], ); + const emptyStateCopy = isNativeAvailable + ? 'Starting mesh…' + : 'Mesh unavailable on this device'; + // Fast lookup by destHash for latency enrichment const peerMap = useMemo( () => new Map(peers.map(p => [p.destHash, p])), @@ -121,6 +129,13 @@ export default function NodesScreen() { {/* Map with filter chips overlaid at bottom */} + {meshNodes.length === 0 && ( + + + {emptyStateCopy} + + + )} {mapExpanded && {FILTERS.map(f => { @@ -173,4 +188,6 @@ const S = StyleSheet.create({ chipText: { fontFamily: fontFamily.sansMd, fontSize: 10, letterSpacing: 2, textTransform: 'uppercase' }, chipCount: { fontFamily: fontFamily.sansMd, fontSize: 9, letterSpacing: 0.5, opacity: 0.8 }, bottomScroll: { paddingBottom: 24 }, + emptyState: { position: 'absolute', left: 0, right: 0, top: 60, alignItems: 'center' }, + emptyStateText:{ fontFamily: fontFamily.sansMd, fontSize: 11, letterSpacing: 1.5, textTransform: 'uppercase' }, }); diff --git a/mobile_app/screens/SettingsScreen.tsx b/mobile_app/screens/SettingsScreen.tsx index 8ee77516..08e5bcce 100644 --- a/mobile_app/screens/SettingsScreen.tsx +++ b/mobile_app/screens/SettingsScreen.tsx @@ -197,7 +197,8 @@ export default function SettingsScreen() { - + {/* 78% BATT pill removed per AUDIT T11 / ROADMAP § 0.B.5 + — no battery telemetry from the pairing API today. */} diff --git a/mobile_app/screens/WalletScreen.tsx b/mobile_app/screens/WalletScreen.tsx index 7aed8399..cbfbedc3 100644 --- a/mobile_app/screens/WalletScreen.tsx +++ b/mobile_app/screens/WalletScreen.tsx @@ -131,40 +131,54 @@ const ACTIONS = [ function ActionTiles() { const { colors } = useTheme(); const router = useRouter(); + // Gate the Send tile at render-time so isolated-mode users can't walk three + // screens deep before the confirm-step error tells them no RPC route exists. + // ROADMAP § 2.4 / 02-UX P0 #6. + const { mode } = useNetworkMode(); + const isolated = mode === 'isolated'; return ( - {ACTIONS.map(a => ( - { - if (!('route' in a)) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {}); - router.push(a.route as never); - }} - style={({ pressed }) => { - const soon = 'soon' in a && a.soon; - const pressedOpacity = pressed ? 0.7 : 1; - const opacity = soon ? 0.4 : pressedOpacity; - return [S.tile, S.actionTile, { - backgroundColor: a.primary ? colors.primary : colors.surface2, - borderColor: a.primary ? colors.primary : colors.border, - opacity, - }]; - }} - > - - - - - {a.label} - - {'soon' in a && a.soon && ( - SOON - )} - - ))} + {ACTIONS.map(a => { + const soon = 'soon' in a && a.soon; + const sendDisabled = a.id === 'send' && isolated; + const disabled = soon || sendDisabled; + return ( + { + if (!('route' in a)) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {}); + router.push(a.route as never); + }} + style={({ pressed }) => { + const pressedOpacity = pressed ? 0.7 : 1; + const opacity = disabled ? 0.4 : pressedOpacity; + return [S.tile, S.actionTile, { + backgroundColor: a.primary ? colors.primary : colors.surface2, + borderColor: a.primary ? colors.primary : colors.border, + opacity, + }]; + }} + > + + + + + {a.label} + + {soon && ( + SOON + )} + {sendDisabled && ( + + NO ROUTE + + )} + + ); + })} ); }