Fix/lxmf v0.2.80#100
Merged
Merged
Conversation
Adds babel.config.js enabling @babel/plugin-transform-class-static-block (required by three.js in the mesh-map renderer) and declares the plugin as a direct devDependency so resolution is guaranteed on fresh installs. Fixes the silent iOS bundling failure (Metro HTTP 500) that left dev-clients running stale embedded JS.
Two related iOS bugs observed on a personal-team dev build: 1. CameraView mounted before permission resolved → black preview that never recovers until app restart. On iOS the component caches its permission state at mount; flipping null→granted at runtime doesn't re-init the preview. Gate the mount on `permission.granted === true` so the conditional flip causes a fresh mount with permission in place. 2. PeersDrawer's QR onResult silently dropped any QR that wasn't a plain LXMF hash — group QRs, Solana addresses, malformed/empty payloads all closed the modal with no feedback, leaving users thinking "scanner is broken." Surface explicit alerts: - lxmf-group → "looks like a channel QR, use Join channel" - solana → "wallet address, use Send screen" - unknown → show first 64 chars of raw payload so user can debug Also adds a __DEV__ console.log of the parsed result so we can adb logcat-trace exactly what came out of the scanner.
expo-camera@17 split audio recording into a separate package (expo-audio). iOS native side hard-links the ExpoAudio module even when the JS-level use is barcode-scanning only, so the dev-client build fails with "Cannot find native module expoAudio" without the peer dep installed. `npx expo install expo-audio` added 1.1.1 to deps and registered the config plugin in app.json. No source code changes — this is pure native bridge availability.
QRScannerModal was a free-floating <Modal>. Rendered inside the join-channel sheet (itself a <Modal>) it stacked two native iOS windows — black camera preview, and corrupted touch handling on dismiss (frozen screen, camera never released). A single modal (Send, peers drawer) worked; only nesting broke. - app/scan.tsx route + scan() promise API (src/services/qrScan), usable identically from any screen or modal; one camera/permission lifecycle; re-entrancy-guarded so overlapping calls can't stack routes. - Convert the join-channel sheet to app/join-channel.tsx (transparentModal owning its slide, like receive) so the scanner composes over it natively — no nested-modal conflict. - Update RecipientPicker, PeersDrawer, and the join flow to await scan(). Delete QRScannerModal and JoinGroupModal.
…ed lowercase default
…ory undershoot L3: a secureSet rejection when persisting the LXMF identity was swallowed in a console.warn. A failed write can spawn a fresh identity on next cold start — losing the mesh address and message continuity — with no user-visible signal. Route it to the existing LxmfErrorBanner via a new identityError state, cleared on a later successful persist. MSG-2: getPeerMessages fetched a fixed most-recent window (limit*2) then filtered to the peer, silently returning too few messages for a peer whose history sits deeper than that window. Grow the window until limit matches are found or a short page proves the store is drained.
Expo Router auto-registers every file in app/ as a deep-linkable route, so a production binary could open anonmesh://dev/* developer screens. Add a dev-group layout that redirects to the app shell unless __DEV__, keeping dev tools out of release builds.
MY_PC (a developer's local Reticulum hub) was derived solely from EXPO_PUBLIC_LOCAL_LXMF_HOST and unshifted into the node's interface list in every build. EXPO_PUBLIC_* values inline into the production bundle when present in the build env, so a leaked var could wire a dev machine as a hub in a release binary. Gate on __DEV__ per project conventions (use __DEV__, not EXPO_PUBLIC_*, for dev-only code).
Adds resetTutorial() (clears the tutorial-completed flag) and an app/dev index screen that lists dev screens and exposes reset + replay actions, so onboarding and the tutorial can be rehearsed repeatedly without reinstalling. Lives under the __DEV__-guarded dev route group, so it never ships to production.
The shebang hardcoded a teammate's pipx venv path (/home/m4gicred1/...), so the LXMF send helper only ran on one machine. Use /usr/bin/env python3.
The active BeaconRegistry card presented hardcoded figures — 0.000312 SOL 'earned', 24 co-signs, and derived rep/jitoSOL/yield — as if real, but the co-sign and staking economy is future work (see the in-file note, and the stake modal already reads 'not yet active'). Add a PREVIEW pill and a disclaimer so the illustrative figures no longer masquerade as real earnings; the reachable-peer count stays real. Exact copy/placement pending on-device visual confirmation.
…G-2) Pulls the adaptive-window logic out of getPeerMessages into a pure collectPeerMessages() and adds tier0 assertions — including the deep-peer case the old fixed limit*2 window silently dropped (old: 0 found, new: all 5). getPeerMessages now delegates to the tested helper.
getGroupMembers runs inside a MessagesScreen useMemo (during render) and called lxmf.getStatus(), which refreshes LxmfProvider state -> 'Cannot update a component while rendering a different component' warning. Read the cached lxmf.status instead (identical for our own addressHex). Pre-existing bug, surfaced by on-device testing of the messages thread.
…g loop) The adaptive-window loop re-fetched and re-scanned the whole message store on every conversation open (400->800->1600->... until drained), making threads take seconds to open on a busy mesh. Revert to a single most-recent-window fetch (limit*2). The deep-peer undershoot it tried to address needs a native per-peer query (catalogued), not a JS loop. Test updated to the bounded contract.
…primitives Remove the unused starter-template stack (themed-text/view, parallax scroll view, hello-wave, external-link, collapsible, icon-symbol, constants/theme, use-theme-color, use-color-scheme) — it was a second, competing color/font source with zero real importers. Add EmptyState / ErrorState / LoadingState to components/ui as the canonical, token-driven blocks for the app's empty / error / loading moments.
Tokenize colors, type sizes, radii and exact-match spacing on the onboarding and tutorial surface. Brand/shader/gradient colors and bespoke loading choreography left intact. No behavior change.
Route hex/type/radii/spacing through tokens across the messages tab and component set; replace the group-members empty block with the shared EmptyState primitive (copy/behavior preserved). Chat-bubble accent colors and camera/lightbox chrome kept intentional. No behavior change.
Route colors/type/radii/spacing through design tokens across the wallet tab, panels, tx detail, and home cards. Per-asset brand colors (SOL/USDC/etc.), tx-direction red/green, and on-primary button ink kept intentional. Wallet, balance, and transaction logic untouched.
Tokenize hex/type/radii/spacing on the Peers tab. Mesh-map visualization palette (per-handle node colors, signal-link tints) and sub-token canvas labels kept intentional. Cosign/transaction logic untouched.
Route colors/type/radii/spacing through tokens across settings, the security modals, contacts and receive; fullscreen modal scrims now use colors.overlay; contacts empty block uses the shared EmptyState primitive (copy preserved). QR-rendering colors kept literal for scan contrast. Auth/biometric/key logic untouched.
Tokenize hex/type/radii/spacing across the send recipient/amount/review/result components. Oversized amount-display numerals and derived error-alpha borders kept intentional. Transaction-build, validation and navigation logic untouched.
Tab bar, root error banner, and a few shared primitives: swap only already-token-equal values (zero visual change) plus normal tokenization of the non-reused chrome. Off-scale primitive dimensions and #000 shadows left deliberately to avoid app-wide ripple.
…scrubber, DSN kill-switch)
src/observability/{sentry,scrub,ErrorBoundary} + scrub tests; ErrorBoundary wired in _layout; metro/app.json/deps for Sentry. Crash-only (no product analytics), DSN gated via env kill-switch, PII scrubbed.
Any render/lifecycle throw in the provider tree now shows a recovery
screen ("Something went wrong / Try again") instead of white-screening
the app. Hard-coded dark colors so the fallback renders even if
ThemeProvider itself threw. Global ErrorUtils handler logs unhandled
async throws without swallowing them; preserves the previous handler in
__DEV__ so the dev overlay still works.
The three placeholder PendingCosign entries (0.25 / 1.05 / 0.005 SOL)
were rendered as if they were live requests. Replaced with an empty
array so the component falls through to its existing honest empty state
("Multisig co-signs not yet live") until the real data source is wired.
* feat(mesh-rpc): migrate MeshRpcAdapter to beaconRpcWait link protocol Replaces beaconBroadcastRpc (fire-and-forget LXMF broadcast) with beaconRpcWait (Reticulum Link + JSON-RPC 2.0 with zlib compression). - MeshRpcAdapter: swap BeaconBroadcastRpcFn → BeaconRpcWaitFn; rpc() now passes destHashHex and propagates isError as a thrown Error - LxmfContext: expose beaconRpcWait from useLxmf() in LxmfCtxValue - NetworkModeContext: destructure and pass beaconRpcWait to adapter Native module handles correlation, retransmit, and link management. No JS-side message routing needed. * fix(send): remove duplicate rpcAdapter param in buildSplTransferTransaction * fix(mesh-rpc): use beaconBroadcastRpc for RPC queries, keep beaconRpcWait for targeted ops
- New useConversationSummaries hook: derives last message preview, timestamp, and unread count per peer from native DB. Re-derives on messageReceived events via sliceNewEvents. markRead clears unread count in-memory when conversation is opened. - PeersDrawer: three memoized tabs replacing the flat peer list. Contacts shows only DMs with message history sorted by recency. Groups shows joined group channels. All Peers shows full discovery list. Each tab has a distinct empty state. All three lists wrapped in useMemo to prevent re-computation on unrelated renders. - MessagesScreen: wires hook, passes summaries/activeTab/onTabChange to drawer, calls markRead on peer pick. Default tab is Contacts.
* feat(mesh-payment): offline Solana payments through beacon relay (#79) - New executeMeshPayment() — fetch nonce via mesh RPC, derive ATAs, partially sign execute_payment tx with payer key, zero key immediately, send partial tx to beacon over LXMF. Beacon co-signs and submits to Solana. - Fix duplicate rpcAdapter params in buildSplTransferTransaction / estimateSplTransferFeeLamports / sendSplTransfer destructuring. Key never transmitted over mesh; only the partial transaction is relayed. * feat(mesh-rpc): migrate MeshRpcAdapter to beaconRpcWait link protocol (#80) * feat(mesh-rpc): migrate MeshRpcAdapter to beaconRpcWait link protocol Replaces beaconBroadcastRpc (fire-and-forget LXMF broadcast) with beaconRpcWait (Reticulum Link + JSON-RPC 2.0 with zlib compression). - MeshRpcAdapter: swap BeaconBroadcastRpcFn → BeaconRpcWaitFn; rpc() now passes destHashHex and propagates isError as a thrown Error - LxmfContext: expose beaconRpcWait from useLxmf() in LxmfCtxValue - NetworkModeContext: destructure and pass beaconRpcWait to adapter Native module handles correlation, retransmit, and link management. No JS-side message routing needed. * fix(send): remove duplicate rpcAdapter param in buildSplTransferTransaction * feat(mesh-rpc): beacon broadcast RPC + sendTransaction fix (#81) * feat(mesh-rpc): migrate MeshRpcAdapter to beaconRpcWait link protocol Replaces beaconBroadcastRpc (fire-and-forget LXMF broadcast) with beaconRpcWait (Reticulum Link + JSON-RPC 2.0 with zlib compression). - MeshRpcAdapter: swap BeaconBroadcastRpcFn → BeaconRpcWaitFn; rpc() now passes destHashHex and propagates isError as a thrown Error - LxmfContext: expose beaconRpcWait from useLxmf() in LxmfCtxValue - NetworkModeContext: destructure and pass beaconRpcWait to adapter Native module handles correlation, retransmit, and link management. No JS-side message routing needed. * fix(send): remove duplicate rpcAdapter param in buildSplTransferTransaction * fix(mesh-rpc): use beaconBroadcastRpc for RPC queries, keep beaconRpcWait for targeted ops * feat(messages): tab filtering — Contacts / Groups / All Peers - New useConversationSummaries hook: derives last message preview, timestamp, and unread count per peer from native DB. Re-derives on messageReceived events via sliceNewEvents. markRead clears unread count in-memory when conversation is opened. - PeersDrawer: three memoized tabs replacing the flat peer list. Contacts shows only DMs with message history sorted by recency. Groups shows joined group channels. All Peers shows full discovery list. Each tab has a distinct empty state. All three lists wrapped in useMemo to prevent re-computation on unrelated renders. - MessagesScreen: wires hook, passes summaries/activeTab/onTabChange to drawer, calls markRead on peer pick. Default tab is Contacts. --------- Co-authored-by: Excelsior <33706074+epicexcelsior@users.noreply.github.com>
Opening any peer conversation calls touchPeer(destHash), which persists the hash to AsyncStorage (CONTACTED_PEERS). On next derive, buildSummaries adds a stub ConvSummary for touched peers with no message history yet, so they appear in the Contacts tab immediately and survive app restarts.
PII scrubber matched only strings — a Solana secretKey (Uint8Array(64)) or seed (randomBytes(32)) under a generic key (data/args/frame-local) shipped to Sentry unredacted. Redact any binary buffer + long all-numeric array (key serialized as number[]); 2 new tests cover it (10 total, all pass). Also align the stale Solana-Pay brand-casing assertions to canonical lowercase 'anonmesh' (inherited from f3b2107; same fix as night2/design) so the tier0 gate passes.
refactor(scanner): route-based QR scanner (fix black preview + frozen screen)
Resolve scanner overlap in favor of #85's route-based scanner: - drop JoinGroupModal.tsx (superseded by the /join-channel route) - MessagesScreen: join via router.push('/join-channel'); keep night2's isRunning/bleActive honest-empty-state props
Reconcile design-system consistency with night2 hardening (17-file overlap):
- keep design wrappers (ConfirmSheet/Toast/AppBottomSheet/ScreenHeader/tokens)
- night2 honesty + logic wins on content: contacts copy (recipient address,
no devnet), MessagesScreen {title}/{subtitle} honest empty states,
NodesScreen ble-recovery card
- BeaconRegistry: took night2 whole — its no-fake-data feeList must not revert
to design's fabricated stake/earnings values; AppBottomSheet re-migration of
the honest sheet is a follow-up
- drop JoinGroupModal + QRScannerModal (superseded by #85 route scanner)
Reconcile crash observability with night2 hardening: - ErrorBoundary: keep telemetry's Sentry-wired boundary (scrubbed capture) - keep night2's errorHandler.ts global handler — Sentry catches render throws, errorHandler catches async/unhandled rejections Sentry's boundary misses; _layout calls both initObservability() and installGlobalErrorHandler() - app.json: keep both plugins (@sentry/react-native + expo-audio)
Record-merge (-s ours): night2's tree already incorporates staging@211bdc5 (verified — no dropped files, no dep regressions). Re-establishes staging as an ancestor after an earlier history-rewrite dropped the merge link, so the PR merges cleanly.
Record-merge (-s ours): tree already incorporates staging via night2; re-links staging as ancestor so the PR merges cleanly.
Record-merge (-s ours): tree already incorporates staging via night2; re-links staging as ancestor so the PR merges cleanly.
Confidential payments aren't in the send path (Arcium = beacon privacy, not payment confidentiality). Reframe to the honest, shipped capability set: encrypted mesh messaging + off-grid payments. Clears the honesty-check gate.
Confidential payments aren't in the send path (Arcium = beacon privacy, not payment confidentiality). Reframe to the honest, shipped capability set: encrypted mesh messaging + off-grid payments. Clears the honesty-check gate.
Confidential payments aren't in the send path (Arcium = beacon privacy, not payment confidentiality). Reframe to the honest, shipped capability set: encrypted mesh messaging + off-grid payments. Clears the honesty-check gate.
stability + onboarding P0s + P1 double-send money fix (11 lanes)
design-system consistency: ScreenHeader · tokens · Toast · ConfirmSheet · sheet primitives
crash-only observability: Sentry ErrorBoundary + PII scrubber (DSN kill-switch)
…nounces, app 1.0.3 (#93) * fix(lxmf): correct v0.2.73 integration, message corruption, dead code Message corruption: - Add single canonical decodeBody util (strict base64 guard + fatal UTF-8 decode, raw fallback). Route MessagesScreen and useConversationSummaries through it, replacing two divergent decoders. - Delete utils/lxmfDecode.ts (third decoder, zero imports) and the looksReadable heuristic. Wiring: - Drive rnodeConnected from onRNodeConnected/onRNodeDisconnected events (seed once on BLE active) instead of polling connectedRNodeCount() every 1s. - Enable propagation relay: setPropagationNode(true) before start(), and syncPropagation() on app foreground to pull store-and-forward messages. - Surface getConnectedRNodes/unpairNusRNode/syncPropagation on context; UNPAIR now disconnects the native RNode. - Fix broken sliceNewEvents call in useConversationSummaries (was passing refs instead of values, never updated — re-derived every render). - Fix NetworkModeContext passing undefined beaconRpcWait to MeshRpcAdapter; use the destructured beaconBroadcastRpc. TCP path: - Dev-only one-shot logger of announceReceived/messageReceived key sets to confirm the real (untyped) native payload shape over TCP. - A received message defaults a new peer to 'reticulum' (not 'ble') since the message proves reachability, not transport. - Reconcile sends stuck queued/stale over TCP against the native DB acked flag, since a messageDelivered proof may never arrive multi-hop. * fix(lxmf): tag peer transport from the event interface field, not hops TCP-only and RNode peers were shown as BLE. Two heuristics caused it: - resolveVia treated any hops===0 peer as BLE when the BLE radio was on - a re-tag effect stamped every hops===0 peer as BLE whenever blePeerCount>0 Announce events carry no usable hops field, so all peers defaulted to hops=0 and got flagged BLE the moment one real BLE peer connected. The native announce/message event reports the interface the packet arrived on. Read it (probe common key names, substring-map the value to ble/rnode/reticulum) and make it the authoritative source for a peer's transport: - resolveVia now returns eventVia(e) ?? existing ?? 'reticulum' (no hops→ble guess) - applyMessageReceived tags/updates via from the interface field - applyLogAnnounce (parsed log line, no interface) preserves the existing tag - deleted the blanket BLE re-tag effect - dev logger now prints the matched interface field + mapped via to confirm shape * fix(lxmf): guard phantom setPropagationNode/syncPropagation native calls The 0.2.73 type defs declare setPropagationNode and syncPropagation, but neither the iOS nor Android native module registers them — calling the hook wrapper hits an undefined native function and surfaces "syncPropagation is not a function" in the UI error banner. Feature-detect with nativeHasFn() before calling: skip the propagation-node enable on start() and the foreground syncPropagation() when the native build lacks them. They auto-activate once a native build implements them. getConnectedRNodes/unpairNusRNode are registered on both platforms, so they stay unguarded. * fix(messages): close composer/tab-bar gap, show placeholder for undecryptable bodies Gap: MessagesScreen added a View of height insets.bottom below the Composer, but the screen sits above the custom tab bar which already consumes insets.bottom. Double-counted, so the composer floated ~insets.bottom above the tab bar. Removed the spacer (and the now-unused kbShown state + Keyboard listener + import). Body: a received body that is strict base64 but not valid UTF-8 is binary/ciphertext the native layer couldn't decrypt. decodeBody was dumping the raw base64 as the message text (the "doesn't make sense" garbage). It now returns a clear placeholder, "🔒 Encrypted message (couldn't decrypt)", for that case. Real text (base64 of UTF-8) and plain non-base64 bodies are unchanged. * chore(lxmf): bump to 0.2.75, remove dead code 0.2.75 registers setPropagationNode/syncPropagation on both iOS and Android, so the nativeHasFn guards now pass and propagation actually runs. Dead-code cleanup (lint now 0 warnings across the messaging path): - remove unused PROGRAM_ID_HEX const - remove unused ExecutePaymentAccounts/ExecutePaymentParams type mirrors (BeaconExecutePaymentAccounts/Params are the ones in use) - remove unused MediaMsg import (MessagesScreen) - remove unused Icon import (SettingsScreen) - document the intentional per-member useMemo deps with an eslint-disable * fix(messages): capture active peer once per incoming-event batch The incoming-message effect read activePeerHexRef.current per iteration. A tap that runs pickPeer (which mutates the ref synchronously) mid-batch could misroute the remaining same-batch messages to the wrong thread. Snapshot the active peer once at the top of the batch so all messages in a batch route consistently. (Investigation of "only one received message shows" points the dominant cause at the native layer — the LXMF wire timestamp is discarded and all burst messages get SystemTime::now() seconds, plus the 200-cap event buffer's by-reference slicing. Fix prompt for the package is in the plan file.) * feat(lxmf): track events by monotonic id, fixing dropped/duplicated bursts (0.2.76) 0.2.76 stamps every LxmfEvent with a strictly-increasing native id and raises the hook buffer cap to 1000. Replace the head-by-reference sliceNewEvents (not eviction-safe — a burst overflowing the buffer could drop or duplicate unread messages) with an id watermark. - new eventsAfter(events, lastSeenId): returns events with id > lastSeenId in arrival order (oldest-first), O(new) and eviction-safe; delete sliceNewEvents. - migrate all four consumers to a lastSeenId ref: MessagesScreen, LxmfContext, useConversationSummaries, useMessageNotifications. Mount semantics preserved (watermark starts at 0 → process backlog once); notifications advance the watermark even when disabled to avoid replay. - oldest-first ordering also fixes the prior reverse-append of burst messages. Bumps @magicred-1/react-native-lxmf to 0.2.76 (native rebuild required). * fix(lxmf): don't drop id-less events — restore peer announcements Regression from the id-watermark migration: eventsAfter `continue`-skipped any event without a numeric `id`, and the shipped 0.2.76 native build does not stamp `id` on announceReceived (the type is `id?: number`). So every peer announce was silently discarded before reaching the peer map — the peer list stopped populating. (Beacons kept showing because they flow via lxmf.beacons/mergeBeacon, not eventsAfter — the diagnostic tell.) - eventsAfter now includes id-less events instead of skipping them; `id` only bounds the already-seen tail. Safe: the message/summary/notification consumers type-filter to messageReceived (id-bearing), and the announce/peer consumer is idempotent (upsert by hash), so re-surfacing an id-less announce can't dupe. - add highestEventId() to advance the watermark by the max id in the buffer (at(-1) could be an id-less event → undefined → watermark never advanced). - seed lastSeenId at -1 (was 0) so a genuine id=0 first event isn't treated seen. Native follow-ups (not app-fixable, tracked in the prompt): 0.2.76 should stamp id on announces, decode msgpack announce app_data to a clean name, and map the rnodeConnected/rnodeDisconnected events in the Android bridge. * chore(app): bump version to 1.0.3 * chore(deps): sync lockfile to react-native-lxmf 0.2.78 * chore(app): set ios buildNumber + android versionCode to 3
- app.json: remove Sentry config plugin (injected Upload Debug Symbols Xcode script that requires SENTRY_AUTH_TOKEN) + drop ignored buildNumber - package.json: remove @sentry/react-native dep - metro.config.js: getSentryExpoConfig -> getDefaultConfig - sentry.ts: replace with no-op shim (preserves Sentry.wrap/withScope/ captureException API so _layout.tsx and ErrorBoundary.tsx are unchanged)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@magicred-1/react-native-lxmfto^0.2.80@sentry/react-native(breaks EAS iOS archive — Xcode sentry-cli script fails without auth token)fastestsmallesttextencoderdecoderwas missing →npm cifailed)Test plan
npm cipasses in CI