Skip to content

fix: 12 reported bugs + audit drain, iOS 26 chat, light theme, bitchat overhaul#190

Open
Kelbie wants to merge 524 commits into
mainfrom
fix/twelve-reported-issues
Open

fix: 12 reported bugs + audit drain, iOS 26 chat, light theme, bitchat overhaul#190
Kelbie wants to merge 524 commits into
mainfrom
fix/twelve-reported-issues

Conversation

@Kelbie
Copy link
Copy Markdown
Contributor

@Kelbie Kelbie commented May 10, 2026

Summary

Started as a fix for 12 user-reported wallet bugs (fix/twelve-reported-issues); grew into a sustained drain of the rolling __audits__/*.json corpus and then a deeper re-platforming pass — chat surfaces migrated to LegendList with iOS 26 keyboard fixes, full light-theme support, Liquid Glass capability variants, a defineVariants UI port, exhaustive lint/type hardening (10+ new bans), shared/styles/tokens.ts + __rules__/, an analyze-structure tooling overhaul with CI structural-health, NPC (NPubCash) host + claim flow, secure-store boot-FaceID fix, broader deeplink ingress, drawer X-style header + scene corners, the popup-registry collapse, a full BitChat BLE bridge rewrite (DM message store + delivery acks + per-profile keychain/identity scoping), NIP-05 recipient identity enrichment threaded through the send/payment flow, and a batch of wallet-surface UX upgrades (pull-to-AI refresh, currency-swap pill, tab-bar press anim, mint balance-split footer rework, mint info reveal polish). 524 commits, ~70 audit findings shipped end-to-end.

Why

The branch name is stale. Three pressures stacked onto the original 12-bug ticket:

  1. The __audits__/ corpus had accumulated dozens of unaddressed Critical/High/Medium findings — silent attacker-mint trust, on-boot crashes on Android, secrets leaking into log/storage dumps, dead null-guards, TOCTOU in key derivation, re-render storms from over-broad zustand selectors. Each was small in isolation but the un-cited backlog was load-bearing context for every new audit cycle. This PR drains it.
  2. iOS 26 broke FlatList in chat and the keyboard-lift math across our DM/AI/whitenoise surfaces — needed a LegendList port and unified composer math, which pulled in the Liquid Glass composer rework.
  3. The "magic-number / hardcoded color / raw fetch / raw Pressable" debt was preventing meaningful theme work and structural review. Shipping light theme without first landing shared/styles/tokens.ts, the defineVariants capability port, and the lint bans would have re-introduced the same debt.

Splitting now would be pure churn — the slices are independently reviewable as commits. Sliced one finding (or one tight cluster) per commit per codereview/fix.md.

What changed

Grouped by area. One commit per slice; one chore(audits) annotation per slice for the audit work. Headings call out the broad themes — see the Exhaustive commit list at the bottom for everything.

Security / safety fixes

  • bitchat: scope BLE keychain + Nostr per-geohash identity to the active profile (no more cross-profile DM/identity leak through the singleton native services).
  • mint: bound the rebalance-trust window — attacker-controlled intermediary mints can no longer be silently auto-trusted mid-rebalance (__audits__/12.json F-001). Canonicalise the trust-and-add path; restore Nostr pills for npub-publishing mints; make swapStatusStore the source of truth for the rebalance toast; bound mint-discovery fanout and guard late setState.
  • wallet: close BitcoinNearYou location-leak privacy seam.
  • settings: redact bearer instruments + geolocation from the storage-dump export; swap legacy clipboard; cross-platform key import (Alert.prompt was iOS-only); gate-mode popup leak; stop recovery from permanently trusting backend-supplied mints.
  • whitenoise: zero nsec on dispose; drop AnimatedEmoji's Google CDN fetch; bound inbox cold-start fetch; silence setup listener.
  • logger: redact secrets in summarizeString, freeze deduped entries, drop module-load side effects; share core state across child loggers; cap aiLog span.
  • logs: drop payment-value previews; narrow raw error objects.
  • payments: validate untrusted external strings at the trust boundary.
  • bitchat: platform-guard the native require so Android JS bundle stops crashing at boot.
  • nostr: verify NIP-17 seal sig and rumor id at unwrap boundary; harden secureStorage seam (BIP-39 read-side validation, sha256 mnemonic hash, persistent key index); validate BIP-39 checksum at every mnemonic-store seam; refuse to overwrite a corrupt mnemonic; self-heal corrupt SecureStore caches.
  • seed: keep debug mnemonic out of bundles; read master seed for reinstall.
  • auth: tear out non-functional passcode gate.
  • keys: require iOS biometric auth on secure-store reads (then later relax for boot-cascade — see secure-store correctness).
  • api-client: strip URL queries from logs; https-only mint info; relocate WS URL.
  • security: scheme-validate untrusted Linking.openURL inputs.

Correctness fixes

  • bitchat: full BLE bridge rewrite — dedicated DM message store (bitchatDmMessages), delivery acks routed back through the JS bridge, retry state surfaced on the message bubble.
  • nostr: dedupe key derivation + single-flight mnemonic reads (one FaceID prompt per boot wave instead of three); centralise 64-char hex pubkey validator; consolidate nip17 onto shared impl.
  • secure-store: drop biometric requirement on boot reads to stop FaceID cascades.
  • popup: clear lastPayloadRef on close; stack "Choose how to pay" above camera route modal; tighten popup-area hygiene.
  • nfc: honor UTF-16 NDEF text records; isolate transceive error mapping; own session lifetime in a deep module; consolidate NDEF write through canonical helper.
  • payments: keep swap-toast gate engaged on mid-flight dismissal + re-pop terminal state; wire mockFailMelt/mockFailSend through coco-payment-ux; reuse payment guard for DM send-money; stabilise relay-flush re-fire in contact-discovery hooks.
  • npc (NPubCash): switch host npubx.cash → npub.cash; mirror eNuts host coverage; durable since-cursor; real claim flow.
  • chat/ai: scope useAiSend stream + balance to a hook controller; preserve message history across dep churn; unstamp peer pubkey from own DM messages; stable keys + accurate item-size + single filter pipeline; unify keyboard-lift math across AI tab and modal-stack chats; extend delivery vocabulary to delivered/failed with explicit avatar-seed override.
  • splash: restore splash → QR morph; stop pre-mount native-splash hide.
  • bitchat: preserve message history across dep churn; stop yanking shared geohash subs.
  • android: wire CapsuleButton taps; drop dead LiquidButtonView props.
  • cashu: serialize manager init/cleanup via stored promises; close awaitRestoreReady race + re-init Coco on profile switch.
  • wallet: restore MintSelector android resolution.
  • deeplinks: broaden URI ingress to match Minibits / Macadamia coverage.
  • contacts: stop stale derived state from polluting virtualised lists; show reputation + followers on search rows via schemas v2; gate the white-noise contact behind the developer toggle; surface BitChat BLE peers in contacts + seed mock contacts in demo mode.
  • stores: drop DOMException usage in apiClient (Hermes lacks it); mint collision-safe local ids and roll back failed bitchat sends.
  • split-bill: reconcile payment state via coco events, not screen-scoped polling.
  • btcmap: bound details cache; reset on wipe; raise cluster cache cap.

Feature work (wallet UX / payments / mint)

  • wallet: pull-to-AI refresh, currency-swap pill, tab-bar press anim.
  • nav: defer the AI-pull shortcut until release-ready.
  • mint: rework balance-split footer with explicit Split / Reset / Focus actions; snappier mint-info reveal with custom green OK badge.
  • send: resolve recipient identity (NIP-05 + Nostr profile) inside the payment flow so payment confirmation shows the recipient's display name + avatar, not just hex; covered with a new coco-payment-ux test.
  • transactions: swipeable month pager.

Theming & light mode

  • theme: full light-theme support across chrome and content; retint success + green consumers to blue (then retint a subset back to green for done-state confirmation); neutral fiat pill; light-mode polish for QR card, bitcoin disc, receive tabs; center status pill on wallpaper preview cards; download wallpaper before flipping themeStore on apply.
  • tokens: introduce shared/styles/tokens.ts for non-color design tokens (spacing, radius, alpha, duration, zIndex, iconSize, hitSlop, shadow, minTouchTarget); rename color-shaped variables to mirror semantic source; consolidate brand colors (BITCOIN_ACCENT); replace hardcoded brand hexes with theme tokens; drop module-scope and hardcoded colors that survive theme flips.
  • offline: retint YOU ARE OFFLINE banner to blue.
  • wallpaper: break themeStorewallpaperStore cycle and drop vestigial provider; delete dead applyAlbum + seeded-shuffle distribution path.

Chat surfaces

  • LegendList migration for DM/whitenoise/AI surfaces to bypass iOS 26 FlatList dim; unified keyboard-lift math; iOS 26 keyboard blur fix.
  • Liquid Glass composer for bitchat / nostr-DM / whitenoise — content-sized input, SwiftUI press animation, single-ring glass buttons.
  • Unify sending → sent → delivered → failed delivery indicator across all surfaces; per-message avatar-seed override.
  • Extract ChatScreen wrapper to consolidate DM surfaces; collapse AI surface onto shared ChatScreen; collapse duplicate formatTimestamp into shared helper; extract useChatSurfacePerfLogger; stabilise chat-surface render lifecycle.
  • AI chat: tile pattern wallpaper behind chat surface; adapt composer bottom inset to tab-bar path.
  • Initial gifted-chat migration (later superseded by LegendList; the unused patch is now dropped).

Drawer & navigation chrome

  • X-style header, flat surface, unified menu scrim; mesh gradients for drawer surfaces; rounded scene edge with theme-aware hairline outline; device-radius scene corners; toggle haptics; wider swipe edge.
  • Tab bar: theme colors + rounded corners + press anim.
  • nav: validate deep-link route params with zod; close remaining route-boundary param-validation gaps; consolidate hex64 boundary schemas onto canonical primitive; consolidate drawer route file onto one anchor + segment match; consolidate token/quote route wrappers onto canonical shells; lift paymentRequest header overrides into the layout; drop redundant as any casts on typed-route pathnames.

Capability / Liquid Glass port (defineVariants)

  • Introduce capability port + defineVariants plumbing.
  • Migrate CapsuleButton, BalancePill, FiatCurrencyPill, CircleActionButton to defineVariants.
  • Adopt useCapabilities + useLiquidGlassModifiers at inline sites.
  • Finish capability migration; document the rules.
  • style(ui): cohere wallet surfaces and fix Liquid Glass dispatch.

Lint / type hardening

  • Land type-aware typescript-eslint rules across full repo.
  • Ban hardcoded hex colors outside the canonical theme homes.
  • Ban raw console.* in favour of the scoped logger.
  • Require fetchJson wrapper instead of raw fetch.
  • Require formatDate over Date#toLocale*String.
  • Require openExternalUrl wrapper instead of raw Linking.openURL.
  • Ban legacy react-native Animated in favour of Reanimated v4.
  • Ban borderWidth: 0.5; migrate the 4 existing call sites.
  • Add eslint-plugin-react-compiler at warn level.
  • Add eslint-plugin-react-perf at warn level.
  • Round out the no-restricted-imports tap and clipboard bans.
  • Enable tsc noImplicitOverride.
  • Forbid raw react-native Pressable / TouchableOpacity imports.
  • Clear 369 mechanical eslint suppressions (624 → 255).

Pressable / shared primitives

  • Add shared Pressable primitive that auto-guards onPress (single-flight, double-tap re-entry guard).
  • Migrate raw RN Pressable/TouchableOpacity to shared primitives.
  • Route Button + ButtonHandler guards through useSingleFlight; extend to Nostr publish + non-button surfaces.
  • Unify shared Pressable to absorb TouchableOpacity's behaviour; rewire Button to use it; drop haptics duplication.
  • Delete shared TouchableOpacity; narrow lint rule.

Refactors / hygiene (zustand selectors, store cycles, dead code)

  • stores: version+migrate+zod-merge baseline for persisted stores (extended to remaining 17 stores); collapse clearAllData into clearPersistedStore; redact catch errors and route logs through scoped child loggers; collapse zustand persist boilerplate onto persistConfig helper; close profile-scoped persist hygiene gaps; collapse double-scoped pubkey records in profile stores; organise persist migrations as append-only step chain; index scanHistoryStore by transactionId; narrow zustand selectors and subscriptions across the app; scope zustand subscriptions to primitive results.
  • feed (6 slices): harden image-overlay relay-trust seam; extract useThread; tighten NIP-10 thread parsing; unify worklet→JS API on scheduleOnRN; narrow image-overlay context; bound untrusted relay content at the parser/payment seam; drop dead handlers and no-op action refs; inline FeedFilters into FeedScreen; split nostr/shared.tsx kitchen-sink into 9 focused modules; consolidate home/user feed parsers into shared helper.
  • popup: collapse 58 named popup wrappers behind staticPopup/paramPopup registry; collapse per-domain popup catalog into the barrel; colocate engine and bridge into popups/ subfolder; delete 30 unused popup helpers and slim barrel; consolidate popup engine and unify status-toast nav; collapse popup wrappers behind a single factory; consolidate popup onClose seam onto typed close-reason; tint failed Menu rows + route reserved-proofs picker through actionMenuPopup.
  • cashu / coco-payment-ux: tighten presentation primitives; harden fail-safety at boundaries; drop type-laundering casts at coco seam; hoist Manager-internals seam out of coco-payment-ux; make amountEntry config reactive; scope confirm-handler async state; tighten coco-event handler discipline; type history-entry seam in defaultOps; harden react/tracker entry points; consolidate fetch primitives + drop laundering casts; inject logger at seam, drop raw console; group CocoPaymentUXProvider props by concern; drop operation:any casts; drop as-any SDK fallbacks in split-bill + rebalance flows; collapse Manager private-field reach-ins behind one seam; delete dead CocoManager methods and route reach-ins through one seam; bound external-server calls; cover the new recipient-identity enrichment with a dedicated test.
  • logger: split god-module; close any-cast cleanup; drop module-load side effects; share core state across child loggers; cap aiLog span.
  • ui / shared primitives: extract StatusToast shell from payment and swap toasts; consolidate toast scaffolding behind ToastSlab; consolidate section vocabulary; collapse stack primitives onto native flex gap; consolidate map subtree onto canonical primitives; forward expo-image props through the image primitive; subscribe to viewport via useWindowDimensions; un-export reflexive Props/types in shared primitives; gate Card on onPress, narrow Button/Spinner any, plug View prop leak; drop redundant React.memo wrappers; wire accessibility labels through Button + ListRow; close shared/ui a11y rollout punch list; SelectableCheck primitive; align fiat pill chrome with CircleActionButton family.
  • whitenoise: collapse lifecycle escape hatches; inline serialization helpers into single consumer; consolidate provider seam; zero nsec on dispose.
  • theme: migrate features/theme StyleSheet to uniwind className.
  • map: split MapScreen orchestrator into camera + markers hooks; tighten expo-maps seam typing; resolve offline + camera context above coco-payment-ux.
  • mint: lift MintRebalancePlanScreen orchestration into useMintRebalanceOrchestrator; canonicalise the trust-and-add path; type cashu-ts melt seam; type mintInfo against coco-core / cashu-ts seam; collapse duplicate audit-info helper into one canonical module; route fetchMintInfo through fetchJson; SWR cache above coco's mint-info HTTP refresh; route audit-score derivation through transformAuditData.
  • transactions: extract HistoryEntryTimeline state machine + clear cast/dead-code/silent-fallback debt; drop account JSON-blob route param; add swipeable month pager to TransactionsScreen; stabilise derived collections in mintSelect + split-bill picker.
  • chat/contacts: tighten ContactsScreen list-item types and drop typed-route casts; harden relay→UI trust boundary in contacts; lift useNostrProfile to shared/hooks; consolidate participant-status helpers; inline SEARCH_FILTERS_HEIGHT into filter components.
  • nfc: own session lifetime in a deep module.
  • camera: consolidate permission gateway + tighten lifecycle hygiene.
  • routstr: collapse store interface and stop double-persisting active session view.
  • splitBill: extract SlideToConfirm primitive (also used in SettingsRecoveryScreen/DeleteScreen); thread BitChat profile scope through BLE send.
  • crypto: drop the local nutpatch package, depend on published nutpatch@^1.0.0.
  • hygiene: drop the unused openai SDK; drop the unused react-native-gifted-chat patch; dedupe polyfill imports; retire features/bitchat StyleSheet; trim unused exports + delete dead helpers (multiple slices via knip); delete unreachable feature surfaces; retire unreachable wallet-health feature; collapse single-account wallet pager.
  • bitchat: full BLE bridge rewrite + dedicated DM message store + delivery acks; per-profile keychain + per-geohash identity scoping; rename overloaded ChatMessage.senderPubkey; delete orphan parallel chat implementation.
  • boot: move module-load initLog calls below imports.
  • providers: collapse migration gates onto InitializationGate; drop dead splash UI from initialization provider; resolve offline + camera context above coco-payment-ux.
  • receive: tighten screen seam — drop dead prop, inline shallow hook, type-driven state.
  • screens: adopt Screen and BottomButtons in whitenoise setup; hoist render-body logs into effects; use scoped loggers; resolve reported wallet issues.
  • animation: migrate three feature screens from legacy RN Animated to Reanimated v4.
  • payments: carry recipientPubkey through chat send-money seam; resolve recipient identity (NIP-05 + profile) inside the send flow.
  • nav: centralize route-boundary schema primitives.
  • hooks: hide render-time ref mirrors behind useLatestRef; narrow payment-status subscribers to event payloads; tighten payments-feature contact-discovery hooks.
  • net: route raw fetches through fetchJson + abort + zod.
  • storage: unify whitenoise namespaces under one envelope.
  • e2e: drop legacy yaml test runner from log-doctor.

Tooling & docs

  • Codereview (analyze-structure): expand with depth, smell, history, and llm reports; add structural-health score; default to verbose with opt-out flags; add --focus filter; split into metrics + extract modules; rewrite README; fold lookalikes into subcommand; track re-export edges and platform variants; consolidate analyze-structure / lookalikes / log-doctor into codereview/; correct re-export and aliased-import name extraction; exclude internal tooling and packages/*/lib from walks; extract WDA primitives (real cycle fix); break log-doctor static cycle via createRequire.
  • CI: structural-health posts analyze-structure --llm summary on PRs.
  • __rules__/: introduce, relocate capability rules; add responsive-scaling rule; document native-module profile-scope pattern in caching.md; wire CLAUDE.md.
  • Skills: add Matt Pocock skill set + auditor protocol; add zod, zustand, typescript and prompt-engineering skills.
  • Scripts: replace AUDIT.md/TASK.md with concise audit and fix prompts; bind Matt Pocock skills to fixer phases and self-check; bias audit and fix prompts toward deletion over addition; land coco-payment-ux mission and guiding principles in fixer prompt.
  • Audits: drop tracking — __audits__ is now gitignored; annotations live on disk only.
  • Knip: drop obsolete ignore entries and dead ignoreIssues overrides.
  • Docs: drop stale ContactRow spec; remove legacy cursor and claude rule files; relocate capability rules; codereview docs cleanup.

Settings / misc

  • settings: point Contact the Developer at @SovranBitcoin; scrub hygiene rot across settings screens.
  • android: match iOS launcher icon (black S on white).
  • buttons: let bottom button row size as flex | flex | fixed.
  • wallet: add spacer between pending and confirmed transaction sections.
  • mint-select: unblock Select Mint open and restyle inspect button.
  • onboarding: tighten claim-username surface (abort, validate, redact).
  • search: contacts search fixes.
  • eas: pass --non-interactive to dev and production build scripts.

Audits

  • One chore(audits): annotate completion status per slice marks the corresponding __audits__/NN.json finding(s) as complete/partial/stale. __audits__/ is gitignored so the annotations live on disk only.

Testing

  • yarn lint
  • yarn type-check
  • yarn knip
  • yarn pretty
  • iOS simulator smoke — wallet send/receive, mint add, contacts search, NFC, AI chat, DM chat, whitenoise inbox, swap with status toast, BitChat BLE chat send/receive with delivery acks, send-money with NIP-05 recipient resolution
  • iOS 26 device — keyboard lift on every chat surface, LegendList scroll, Liquid Glass composer animation
  • Android emulator boot (regression check for the bitchat native-require crash) + launcher icon
  • Light theme — flip on every screen; check QR card, bitcoin disc, receive tabs, status pills on wallpaper previews; done-state surfaces are green, not blue
  • FaceID boot wave — single prompt, not three (secure-store single-flight)
  • NPC claim — durable cursor across kill, mirrored host coverage
  • Deep links — wallet URIs matching Minibits / Macadamia patterns
  • Profile switch — BitChat BLE keychain + per-geohash Nostr identity reset; no DM/identity carryover from prior profile
  • Mint balance-split footer — Split / Reset / Focus actions each round-trip correctly
  • Wallet pull-to-AI gesture (currently gated; verify gate before release)

Cashu / Lightning / Nostr specific

  • Touches mint interaction — rebalance trust path (__audits__/12.json, __audits__/24.json); SWR mint-info cache; trust-and-add canonicalisation; mint-discovery fanout bound; balance-split footer rework
  • Touches Nostr — key derivation single-flight (__audits__/54.json, __audits__/55.json); contacts relay seam; image-overlay relay trust; NIP-17 seal sig verification; nip17 consolidation; secureStorage hardening; BIP-39 checksum at every seam; per-geohash NIP-17 identity derivation scoped to the active profile in the BitChat Nostr bridge; NIP-05 recipient resolution in the send flow
  • Touches BIP-39/BIP-32 derivation and expo-secure-storeNostrKeysProvider + shared/lib/nostr/secureStorage.ts single-flight; iOS biometric requirement re-tuned for boot
  • Touches clipboard / QR / NDEF parsing — NFC UTF-16 fix; NDEF write helper consolidation; legacy clipboard swap in settings; broader deeplink URI ingress
  • Touches coco-payment-ux — extensive refactor (see above); recipient-identity enrichment + test; does not touch the upstream coco/ checkout
  • Touches Lightning — coco-payment-ux external-server bounding; mock failure wiring

Breaking changes

None expected. Store public surfaces preserved; the themeStorewallpaperStore cycle break is internal. Removed exports were dead (knip-flagged). The nutpatch switch from a local package to a published ^1.0.0 requires yarn install after pull; the API surface is unchanged.

Rollout

Standard. No remote-config flag; no schema migration. Persist migrations are append-only step chain — existing users will run through them on first boot. The BitChat profile-scoped keychain takes effect on first profile-aware startBLE/startNostr call — pre-existing single-namespace keychain entries are left in place and ignored (no migration required; native services derive fresh per-profile keys).

Linked issues

Refs the rolling __audits__/*.json corpus, specifically findings in:
02, 12, 13, 14, 16, 17, 20, 23, 24, 25, 26, 27, 31, 32, 33, 34, 36, 37, 41, 42, 43, 44, 46, 47, 48, 49, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63 and follow-on slices through ~70 total findings. Also addresses the original 12 user-reported wallet bugs.

Reviewer notes

  • Sliced one finding (or one tight cluster) per commit per codereview/fix.md. Reviewing commit-by-commit is much more tractable than the squashed 524-commit diff.
  • This PR is far over the conventions' 400-LOC guideline. Splitting now would be pure churn — the slices are already independently reviewable as commits.
  • Branch name fix/twelve-reported-issues is stale; the work expanded as the audit corpus grew and as the iOS 26 / theming / capability porting / BitChat rewrite needs surfaced.
  • The capability port (defineVariants), lint hardening, and shared/styles/tokens.ts landings are load-bearing for the light-theme work — review them in roughly that order.
  • The BitChat BLE bridge rewrite + DM store + delivery acks + per-profile scoping is a cohesive sub-thread; review the bitchat-prefixed commits together rather than interleaved with audit slices.

Self-review checklist

  • Title follows Conventional Commits
  • No new runtime dependencies (one removed — openai; one swapped — local nutpatch → published nutpatch@^1.0.0)
  • No coco/ submodule changes
  • BIP-39 / BIP-32 / nsec paths reviewed against __audits__/54.json + 55.json and follow-on findings
  • No console.log, no new any, no commented-out code introduced (now enforced by lint)
  • Not under 400 LOC — see Reviewer notes

Exhaustive commit list

Newest first. Audit-annotation commits (chore(audits): annotate completion status) are interleaved between feature commits and are omitted below for brevity; the feature commit always precedes its annotation.

BitChat / contacts (recent)

  • 0704f2b6 fix(bitchat): scope BLE and Nostr identity to active profile
  • 9cf8bf99 fix(contacts): gate white noise behind developer toggle
  • 05dee3ba feat(contacts): surface BitChat BLE peers and seed mock contacts in demo mode
  • d05176fa refactor(bitchat): rewrite BLE bridge + add DM message store and delivery acks
  • db4a46b7 fix(bitchat): platform-guard native require so Android boots
  • a8a47da7 fix(bitchat): preserve message history across dep churn
  • c55c7081 fix(bitchat): unstamp peer pubkey from own DM messages
  • 47786d07 refactor(bitchat): dedupe message merge, drop dead diagnostic interval
  • 273d61e3 refactor(bitchat): clean bitchat-module surface and rename overloaded ChatMessage.senderPubkey
  • 52d0d887 refactor(bitchat): delete orphan parallel chat implementation
  • be6e781e refactor(hygiene): consolidate brand colors and retire features/bitchat StyleSheet

Wallet UX / mint UX (recent)

  • c6a31b44 feat(mint): rework balance-split footer with Split/Reset/Focus actions
  • b0b86dd4 fix(nav): defer AI pull shortcut until release
  • 478731c6 feat(wallet): pull-to-AI refresh, currency-swapper pill, tab-bar press anim
  • 9284b06c fix(mint): snappier info reveal + custom green OK badge

Send / payments (recent)

  • adfca416 feat(send): resolve recipient identity (NIP-05 + profile) in payment flow
  • 8ae15e4a test(coco-payment-ux): cover recipient identity enrichment

Chat delivery polish (recent)

  • 75c10939 feat(chat): extend delivery vocabulary to delivered/failed + avatar seed override

Theme follow-ups (recent)

  • 9fef8e2a fix(theme): retint success/done surfaces from blue back to green

Hygiene (recent)

  • a1b9ecf1 chore: drop unused react-native-gifted-chat patch
  • 0fd9e885 refactor(crypto): drop local nutpatch package, depend on published nutpatch@^1.0.0

iOS 26 / chat / LegendList

  • 1c825ff0 refactor(chat): migrate DM surfaces to LegendList, fix iOS 26 keyboard blur
  • 962151e9 fix(chat): unify keyboard-lift math across AI tab and modal-stack chats
  • 8bf317e3 fix(ai): drop chrome changes bundled into LegendList port
  • 4c07290e fix(ai): port chat to LegendList to bypass iOS 26 FlatList dim
  • e9e0ce0f feat(ai): tile pattern wallpaper behind chat surface
  • beb85ee7 fix(ai): adapt composer bottom inset to tab-bar path
  • b0ba7ba6 feat(chat): migrate DM + AI surfaces to react-native-gifted-chat
  • d1ce9809 refactor(chat): collapse AI surface onto shared ChatScreen + Button compact size
  • 189f8f9b refactor(chat): extract ChatScreen wrapper to consolidate DM surfaces
  • d129b8e6 refactor(chat): unify DM surfaces on shared bubble + header
  • 3e95b82b refactor(chat): collapse duplicate formatTimestamp into shared chat helper
  • 533a0e56 refactor(chat): extract useChatSurfacePerfLogger
  • d27c8310 perf(chat): stabilise chat-surface render lifecycle

Liquid Glass composer (chat)

  • cae747fc refactor(chat): SwiftUI TextField inside the liquid-glass composer bubble
  • 18114416 fix(chat): let taps in input padding reach the SwiftUI press animation
  • 1f5e23d5 feat(chat): input is also a glass button + 2pt more breathing room
  • d6bb1ce9 Revert "fix(chat): trust buttonStyle('glass') for animation"
  • 7325c970 fix(chat): trust buttonStyle('glass') for animation; input is also a glass button
  • 9a81bcad fix(chat): single-ring glass buttons that still animate
  • f038ae7f fix(chat): single glass circle per button (drop double-ring on press)
  • 1591197f fix(chat): drop tints on the composer glass material
  • a17160f2 fix(chat): drop the press-glow on the composer glass buttons
  • 292e1ebf fix(chat): stop the glass shapes blending into one persistent blob
  • 8b8f68a7 fix(chat): bouncy scale-in for send button via SwiftUI-side animation
  • 83209681 fix(chat): wire animation modifier so the glass-morph actually animates
  • fe08b2c2 fix(chat): liquid-glass composer morph animation + content-sized input
  • e18b03a0 feat(chat): liquid-glass composer for bitchat / nostr-DM / whitenoise
  • 21b422cd feat(chat): unify sending → sent delivery indicator across all surfaces
  • c803c496 Revert "refactor(whitenoise): drop AnimatedEmoji's Google CDN fetch…"

Drawer / navigation chrome

  • 890d169e feat(drawer): device-radius scene corners, toggle haptics, wider swipe edge
  • a491da73 feat(drawer): rounded scene edge with theme-aware hairline outline
  • 906559f1 feat(drawer): X-style header, flat surface, unified menu scrim
  • d17107ab feat(ui): use mesh gradients for drawer surfaces
  • f664a72d Style tab bar with theme colors and rounded corners

Light theme & tokens

  • 1cea90d5 fix(offline): retint YOU ARE OFFLINE banner to blue
  • 63304562 feat(theme): retint success + green consumers to blue, neutral fiat pill
  • 99eed5fe fix(theme): light-mode polish for QR card, bitcoin disc, and receive tabs
  • 6a2ceb4c feat(theme): light theme support across chrome and content
  • 67d685e1 fix(theme): center status pill on wallpaper preview cards
  • f27222fa fix(theme): download wallpaper before flipping themeStore on apply
  • 874dbf67 refactor(theme): rename color-shaped variables to mirror semantic source
  • 873085ff refactor(theme): introduce shared/styles/tokens.ts for non-color design tokens
  • 75802662 fix(theme): kill module-scope and hardcoded colors that survive theme flips
  • 5bc55235 refactor(brand-colors): consolidate raw '#F7931A' into BITCOIN_ACCENT token
  • 72725a99 style(theme): drop hardcoded #3B82F6 in theme picker
  • b60a2aa0 refactor(ui): replace hardcoded brand hexes with theme tokens
  • f7e3b4d1 refactor(theme): delete dead applyAlbum + seeded-shuffle distribution path
  • 5a1c68eb refactor(theme): break themeStore↔wallpaperStore cycle
  • ea824d4a refactor(theme): migrate features/theme StyleSheet to uniwind className
  • 7a86e8fe fix(wallet): align fiat pill chrome with CircleActionButton family

Capability / Liquid Glass port

  • aec70d8d refactor(ui): finish capability migration + document the rules
  • f08b3db5 refactor(ui): adopt useCapabilities + useLiquidGlassModifiers at inline sites
  • ff6fb4dc refactor(ui): migrate CircleActionButton to defineVariants (selector form)
  • 025ef694 refactor(ui): migrate BalancePill + FiatCurrencyPill to defineVariants
  • 851f163b refactor(ui): migrate CapsuleButton to defineVariants
  • 9e26d0f8 refactor(ui): introduce capability port + defineVariants plumbing
  • 0a7a2d6f style(ui): cohere wallet surfaces and fix Liquid Glass dispatch

NPC (NPubCash)

  • 086fa37f fix(npc): mirror eNuts host + durable since-cursor + real claim flow
  • 8b16e5b6 fix(npc): switch NPubCash host from npubx.cash to npub.cash

Secure store / boot

  • 25d8a814 fix(secure-store): drop biometric requirement to stop FaceID cascades on boot
  • 0a85163c fix(nostr): dedupe key derivation and single-flight mnemonic reads
  • 634603c3 security(keys): require ios biometric auth on secure store reads
  • 959fe8fd fix(nostr): harden secureStorage seam — read-side BIP-39, sha256 mnemonic hash
  • a8b76314 fix(nostr): refuse to overwrite a corrupt mnemonic in ensureMnemonicExists
  • ee158241 fix(nostr): validate BIP-39 checksum at every mnemonic-store seam
  • d170376b fix(nostr): self-heal corrupt SecureStore caches and add input validation
  • aea594de refactor(nostr): consolidate secure-storage seam onto canonical adapter
  • e2be9ac4 refactor(nostr): centralise 64-char hex pubkey validation
  • 6df46a86 refactor(nostr): consolidate nip17 onto shared canonical impl
  • 112885f5 fix(nostr): verify NIP-17 seal sig and rumor id at unwrap boundary
  • 1b8f8d9a fix(seed): keep debug mnemonic out of bundles, read master seed for reinstall
  • 997a47bb refactor(auth): tear out non-functional passcode gate
  • b291b0b4 refactor(camera): consolidate permission gateway + tighten lifecycle hygiene

Deeplinks / URI ingress

  • 5a676b4a fix(deeplinks): broaden URI ingress to match Minibits/Macadamia coverage
  • 3c5002bb fix(security): scheme-validate untrusted Linking.openURL inputs
  • 7df64614 refactor(nav): validate deep-link route params with zod
  • 0dddea5f refactor(nav): validate payment-flow deep-link params with zod
  • 17e87fd7 refactor(nav): close remaining route-boundary param-validation gaps
  • 5b3c8fa6 refactor(nav): consolidate hex64 boundary schemas onto canonical primitive
  • 70d209e6 refactor(nav): centralize route-boundary schema primitives

Lint / type hardening

  • ff9d4dd8 chore(lint): clear 369 mechanical eslint suppressions (624 → 255)
  • e85aedb8 chore(lint): round out the no-restricted-imports tap and clipboard bans
  • c71d3968 chore(types): enable tsc noImplicitOverride
  • 04605aa8 chore(lint): add eslint-plugin-react-perf at warn level
  • 2fc684d6 chore(lint): add eslint-plugin-react-compiler at warn level
  • b1596039 chore(lint): ban borderWidth: 0.5; migrate the 4 existing call sites
  • 634f6bfe chore(lint): ban legacy react-native Animated in favour of Reanimated v4
  • 88952c01 chore(lint): require openExternalUrl wrapper instead of raw Linking.openURL
  • 739e9455 chore(lint): require formatDate over Date#toLocale*String
  • 1465878a chore(lint): require fetchJson wrapper instead of raw fetch
  • 434cffcc chore(lint): ban raw console.* in favour of the scoped logger
  • 09890a8f chore(lint): ban hardcoded hex colors outside the canonical theme homes
  • 4aa74bdb chore(lint): land type-aware typescript-eslint rules across full repo
  • bb0a57aa chore(ci): forbid raw react-native Pressable / TouchableOpacity imports

Pressable / shared primitives

  • 3fd3c1ff refactor(ui): delete shared TouchableOpacity, narrow lint rule
  • 2e36b076 refactor(ui): migrate TouchableOpacity callers to shared Pressable
  • 2878c0f3 refactor(ui): rewire Button to use shared Pressable, drop haptics duplication
  • 047fa09f refactor(ui): unify shared Pressable to absorb TouchableOpacity's behaviour
  • 8fd85284 refactor(ui): migrate raw RN Pressable/TouchableOpacity to shared primitives
  • b1ba1d07 refactor(ui): add shared Pressable primitive that auto-guards onPress
  • ea42808d refactor(ui): guard shared TouchableOpacity with useSingleFlight
  • 5f135f9c refactor(ui): route Button + ButtonHandler guards through useSingleFlight
  • 4249c89b refactor(ui): extend single-flight to nostr publish + non-button surfaces
  • 13f9fa9f refactor(ui): guard interactive primitives against double-tap re-entry

Popup

  • 8fb0ec61 fix(popup): stack "Choose how to pay" above the camera route modal
  • c87c7055 fix(popup): tint failed Menu rows + route reserved-proofs picker through actionMenuPopup
  • 428494d2 fix(popup): clear lastPayloadRef on close and tighten popup-area hygiene
  • 10648248 refactor(popup): collapse 58 named popup wrappers behind staticPopup/paramPopup registry
  • 0423e38a refactor(popup): collapse per-domain popup catalog into the barrel
  • 6bcb5960 refactor(popup): colocate engine and bridge into popups/ subfolder
  • ce3d02ce refactor(popups): delete 30 unused popup helpers and slim barrel
  • ca2ecf15 refactor(ui): consolidate popup engine and unify status-toast nav
  • f0f53d44 refactor(ui): collapse popup wrappers behind a single factory
  • 61fe91da refactor(ui): consolidate popup onClose seam onto typed close-reason

Cashu / coco-payment-ux

  • cd71082d fix(cashu): serialize manager init/cleanup via stored promises
  • b6c9b535 fix(coco): close awaitRestoreReady race + re-init Coco on profile switch
  • 27bec5d9 fix(split-bill): reconcile payment state via coco events, not screen-scoped polling
  • e739f165 fix(payments): wire mockFailMelt/mockFailSend through coco-payment-ux
  • 487524dd fix(payments): keep swap-toast gate engaged on mid-flight dismissal
  • 5cd2ad63 refactor(mint): type cashu-ts melt seam, drop residual any in coco bootstrap
  • 4973b2db refactor(mint): type mintInfo against coco-core / cashu-ts seam
  • 5e762cdb docs(coco-payment-ux): match grouped CocoPaymentUXProvider props
  • 772c2f4b refactor(coco-payment-ux): group CocoPaymentUXProvider props by concern
  • 7e064c9f refactor(providers): harden coco-payment-ux react/tracker entry points
  • 338e2bb9 refactor(cashu): tighten coco-event handler discipline at payment seam
  • 23658223 refactor(cashu): scope confirm-handler async state in coco-payment-ux
  • d7d97184 refactor(cashu): make amountEntry config reactive in coco-payment-ux
  • 58f1e641 fix(cashu): tighten coco-payment-ux presentation primitives
  • d5dcbfaa refactor(cashu): tighten snapshot discipline in coco-payment-ux machinery
  • b5ca041a fix(cashu): harden coco-payment-ux fail-safety at boundaries
  • 1f578d8e refactor(cashu): drop type-laundering casts at coco-payment-ux seam
  • b17f8dcd refactor(cashu): hoist Manager-internals seam out of coco-payment-ux
  • db64864e refactor(cashu): type history-entry seam in coco-payment-ux defaultOps
  • c20e3e22 refactor(hooks): narrow payment-status subscribers to event payloads
  • f5007112 refactor(hooks): tighten payments-feature contact-discovery hooks
  • 31fde611 fix(providers): resolve offline + camera context above coco-payment-ux
  • 27ea51ec refactor(cashu): drop operation:any casts on coco event payloads
  • 7840fa4e refactor(cashu): drop as-any sdk fallbacks in split-bill + rebalance flows
  • d40a202d refactor(cashu): delete dead CocoManager methods and route reach-ins through one seam
  • f27ea8e8 refactor(lightning): bound external-server calls in coco-payment-ux
  • 8a8cc833 refactor(cashu): collapse Manager private-field reach-ins behind one seam
  • 6f3b95df refactor(cashu): inject logger at coco-payment-ux seam, drop raw console
  • a9d17a77 refactor(cashu): consolidate fetch primitives + drop laundering casts at coco seam
  • 2dd02c3c refactor(whitenoise): inline serialization helpers into single consumer
  • 5874a0d2 perf(mint): add SWR cache above coco's mint-info HTTP refresh

Mint / payments / wallet

  • 4c576b7d fix(wallet): add spacer between pending and confirmed transaction sections
  • f487671b fix(mint-select): unblock Select Mint open and restyle inspect button
  • 86c0a4a7 fix(mint): bound the rebalance trust window so attacker mints can't linger
  • 0d36402e refactor(mint): canonicalise the trust-and-add path
  • 504ea4ae fix(mint): make swapStatusStore the source of truth for rebalance toast
  • 29cee9d7 fix(mint): restore Nostr pills for npub-publishing mints; consolidate extractor
  • 7c721ea8 fix(mint): bound mint-discovery fanout and guard late setState
  • 0db0a74a refactor(mint): collapse duplicate audit-info helper into one canonical module
  • 57a4a928 refactor(mint): route fetchMintInfo through fetchJson
  • ef79f4be refactor(mint): route audit-score derivation through transformAuditData
  • d7b9d604 refactor(mint): lift MintRebalancePlanScreen orchestration into useMintRebalanceOrchestrator hook
  • cdb5515d fix(wallet): restore MintSelector android resolution
  • 8e272397 refactor(payments): validate untrusted external strings at trust boundaries
  • eee1fa58 feat(mint): unified swap status toast with per-leg progress
  • 532d31c1 feat(payments): carry recipientPubkey through chat send-money seam
  • cad2951a perf(payments): stabilise relay-flush re-fire in contact-discovery hooks
  • 1ba8b9a3 fix(send): reuse payment guard for DM send money research
  • 69a595b5 fix(screens): resolve reported wallet issues
  • b503a11c fix(settings): stop recovery from permanently trusting backend-supplied mints

Stores / persist

  • a43f3c19 refactor(stores): organise persist migrations as append-only step chain
  • ab19c27e refactor(stores): close profile-scoped persist hygiene gaps
  • 77d94677 perf(stores): narrow zustand selectors and store subscriptions
  • 02c3e0e2 refactor(stores): scope zustand subscriptions to primitive results
  • b2f688c8 refactor(stores): collapse double-scoped pubkey records in profile stores
  • a7d9e98e refactor(stores): collapse zustand persist boilerplate onto persistConfig helper
  • ddca6c40 refactor(stores): collapse scanHistoryStore — drop dead processed field, normalise dedupe
  • 2e6fd40b refactor(transactions): index scanHistoryStore by transactionId
  • 80b4f25d refactor(stores): drop dead clearAllData and zero-callsite store actions
  • b0a4e386 refactor(stores): collapse clearAllData into clearPersistedStore
  • 520c57a1 refactor(stores): extend version+migrate+zod-merge to remaining 17 stores
  • 95c14ea3 refactor(stores): version+migrate+zod-merge baseline for persisted stores
  • 20662da9 refactor(stores): redact catch errors and route logs through scoped child loggers
  • 0836024a fix(stores): route remaining hand-rolled local ids through mintLocalId
  • 3f9a0557 fix(stores): mint collision-safe local ids and roll back failed bitchat sends
  • 6050f846 fix(contacts,feed): stop stale derived state from polluting virtualised lists
  • 1317d126 fix(stores): drop DOMException usage in apiClient; hermes lacks it
  • 498457c1 refactor(stores): thread AbortSignal and default timeout through apiClient
  • f71fb859 fix(api-client): strip URL queries from logs, https-only mint info, relocate WS URL

Feed / contacts / nostr

  • a9548f89 search fixes.
  • cbcd07b3 fix(contacts): show reputation + followers on search rows via schemas v2
  • 7fc34d4e refactor(contacts): harden relay→UI trust boundary, narrow types, dedup helpers
  • cb8ca4af refactor(contacts): tighten ContactsScreen list-item types and drop typed-route casts
  • 383ad950 refactor(contacts,feed): inline SEARCH_FILTERS_HEIGHT into filter components
  • 62cbbe0c refactor(feed,user): lift useNostrProfile to shared/hooks
  • 35c84169 refactor(feed): split nostr/shared.tsx kitchen-sink into 9 focused modules
  • 1ab29372 refactor(feed): inline FeedFilters into FeedScreen
  • 47bba4b2 refactor(feed): unify image-overlay worklet→JS API on scheduleOnRN
  • 1e5167bb refactor(feed): drop dead handlers, no-op actions ref, and timestamp-sum cache-buster
  • b6fa2133 refactor(feed): bound untrusted relay content at the parser/payment seam
  • 0afa6165 refactor(feed): narrow image-overlay context surface
  • 9c30a5b0 refactor(feed): harden image-overlay relay-trust + drop dead branches in provider
  • 1d8625f5 refactor(feed): extract useThread hook + tighten NIP-10 thread parsing
  • c32e1623 refactor(nostr): collapse home/user feed parsers into shared helper
  • 61441050 refactor(nostr-cache): expose cache singletons instead of rebound methods
  • 4d36bf1e refactor(screens): retire routstr branch from user messages screen

Whitenoise / bitchat

  • e9e8a9bb fix(whitenoise): zero nsec on dispose, consolidate provider seam
  • dc03539f refactor(whitenoise): bound inbox cold-start fetch and silence setup listener
  • 38ab13f3 refactor(whitenoise): drop AnimatedEmoji's Google CDN fetch and render brand/picker emojis locally
  • 515b558b refactor(nostr): collapse whitenoise lifecycle escape hatches
  • 69c0d5a8 refactor(screens): adopt Screen and BottomButtons in whitenoise setup

Transactions / receive / send

  • 718d3a60 feat(transactions): add swipeable month pager to TransactionsScreen
  • 34172866 refactor(transactions): drop account JSON-blob route param
  • 14c6447f refactor(transactions): extract HistoryEntryTimeline state machine
  • c861ec5d fix(transactions): stable keys, accurate item-size, single filter pipeline
  • 55069422 perf(memo): stabilize derived collections in mintSelect + split-bill picker
  • 1a53f930 refactor(receive): tighten screen seam — drop dead prop, inline shallow hook
  • a146f11e refactor(send): rename Sovran provider, drop redundant hook re-exports
  • fdc0e26c refactor(settings): extract SlideToConfirm primitive from DeleteScreen and SettingsRecoveryScreen
  • bbac6c25 refactor(splitBill): consolidate participant status helpers

Logger / logs

  • 7409349c refactor(logger): split god-module + close any-cast cleanup + cap aiLog span
  • f576c41a fix(logger): redact secrets in summarizeString and freeze deduped entries
  • aa4b2d6f refactor(logger): drop module-load side effects + production hot-path overhead
  • 5dec1cd6 refactor(logger): share core state across child loggers
  • e56fd460 refactor(boot): move module-load initLog calls below imports
  • 62f657ed refactor: scope domain logs through the registered child loggers
  • 14b88f86 refactor: route scoped loggers through the shared registry
  • c8c9fb55 refactor(screens): hoist render-body logs into effects; use scoped loggers
  • 03c27f75 refactor(logging): drop render-body log calls from screen components
  • dded751d fix(logs): drop payment-value previews and narrow raw error objects
  • 6aba6375 refactor(ui): drop deprecated Screen alias for the logger Log component

Settings / onboarding / android

  • d50b14f5 fix(settings): point Contact the Developer at @SovranBitcoin
  • 0ef81f77 fix(android): match iOS launcher icon (black S on white)
  • c87f80fa fix(settings): redact bearer instruments + geolocation from storage dump
  • 62ed9d27 fix(settings): cross-platform key import + gate-mode popup leak
  • eb091ae7 fix(settings): scrub hygiene rot across settings screens
  • 70e5e2df fix(onboarding): tighten claim-username surface (abort, validate, redact)
  • 3333c31a build(eas): pass --non-interactive to dev and production build scripts

NFC

  • 8c9f087a fix(nfc): honor utf-16 ndef text records and isolate transceive error mapping
  • 91c9ce00 refactor(nfc): own session lifetime in a deep module
  • b4f7e1d1 refactor(nfc): consolidate ndef write through canonical helper

Map / btcmap / privacy

  • 91ae8cd2 fix(btcmap): bound details cache, reset on wipe, raise cluster cache cap
  • 4bf501a7 fix(wallet): close BitcoinNearYou location-leak privacy seam
  • 858cd895 refactor(map): split MapScreen orchestrator into camera + markers hooks
  • c41295c7 refactor(map): tighten expo-maps seam typing

AI

  • d24d2c83 fix(ai): scope useAiSend stream + balance to a hook controller

Routstr

  • d1e3a0a8 refactor(routstr): collapse store interface and stop double-persisting active session view

Splash / boot

  • 35009555 fix(splash): restore splash → QR morph and stop pre-mount native-splash hide
  • f3b9553d refactor(providers): collapse migration gates onto InitializationGate
  • cc00f3da refactor(providers): drop dead splash UI from initialization provider

Shared UI primitives

  • 8868f0d5 fix(buttons): let bottom button row size as flex | flex | fixed
  • 0633beb7 fix(ui): align ScrollEdgeFade default blur intensity with BottomButtons
  • 386cda15 fix(ai): use full blur background to match feed
  • 9f78e3e2 fix(ui): isolate ButtonHandler loading spinner per-button
  • 2010ea37 fix(ui,map,whitenoise): cancel async writes on effect cleanup
  • c267d441 fix(ui): wire accessibility labels through Button + ListRow primitives
  • 349a81f5 feat(a11y): close shared/ui a11y rollout punch list
  • 493ea508 refactor(ui): drop redundant React.memo wrappers
  • 276f64c6 refactor(ui): gate Card on onPress, narrow Button/Spinner any, plug View prop leak
  • 0c47df6a refactor(ui): un-export reflexive Props/types in shared/ui primitives
  • c5479815 refactor(ui): extract StatusToast shell from payment and swap toasts
  • 810b35b9 refactor(ui): collapse toast scaffolding behind a ToastSlab primitive
  • 451b09b2 refactor(ui): consolidate section vocabulary onto canonical shared seam
  • b9848350 refactor(ui): collapse stack primitives onto native flex gap
  • 20844aa7 refactor(ui): consolidate map subtree onto canonical primitives
  • d6a47204 refactor(ui): extract protocol-prefix display formatting from DetailsList primitive
  • 3c5731de refactor(ui): SelectableCheck primitive, fix split-bill picker re-render
  • 22c63ae2 refactor(icons): add internal: namespace, share animated-status shapes
  • e33fbd5e refactor(date): consolidate to shared/lib/date.ts with locale-aware formatting
  • ccd09dbd refactor(ui): forward expo-image props through the image primitive
  • dee4cf6f refactor(ui): subscribe to viewport via useWindowDimensions
  • 578b782e refactor(ui): tighten ButtonHandler onPress to drop no-op close arg

Nav / routing

  • 2ccf8c06 refactor(nav): lift paymentRequest header overrides into the layout
  • f9a8a256 refactor(nav): consolidate token/quote route wrappers onto canonical shells
  • 14fe6448 refactor(nav): consolidate drawer route file onto one anchor and segment match
  • f4ce838d refactor(nav): drop wrapper as any cast on NetworkSheet route.replace
  • 6fd5bc37 refactor(nav): drop redundant as any casts on typed-route pathnames

Hooks / net / storage

  • c2932a64 refactor(hooks): hide render-time ref mirrors behind useLatestRef
  • a45d64cd refactor(net): route raw fetches through fetchJson + abort + zod
  • 257ed529 refactor(storage): unify whitenoise namespaces under one envelope

Animation

  • bc2ef580 refactor(animation): migrate three feature screens from legacy RN Animated to Reanimated v4

Hygiene / dead code

  • d11743a2 chore(hygiene): drop unused openai SDK + dedupe polyfill imports
  • f1b74edc chore(hygiene): un-export knip-flagged dead values across receive/send/whitenoise/map/nav/stores/persist/primitives
  • dfde903d chore(hygiene): un-export knip-flagged dead exports across ai/theme/transactions
  • ae0bdcc3 chore(hygiene): delete dead exports flagged by knip
  • 0ec1c058 refactor(hygiene): trim dead exports and dup re-exports
  • 947dcfb7 refactor(hygiene): trim unused exports + delete dead helper
  • f455c53d refactor(hygiene): drop unused barrel re-exports and finish btcMapStore dead state
  • c1b5a04c refactor: delete unreachable feature surfaces
  • 13fa9a3b refactor: delete dead public surfaces flagged by knip
  • 5aaa7747 refactor: retire unreachable wallet-health feature
  • 5f13140d refactor(wallet): collapse single-account pager and prune dead module surface
  • 3f940423 refactor(redux): inline deprecated action-type strings

Tooling — analyze-structure / codereview / rules

  • f5f24202 docs(rules): add responsive-scaling rule
  • ce853837 docs: relocate capability rules to rules/, wire CLAUDE.md
  • 6f4f423a ci(structural-health): post analyze-structure --llm summary on PRs
  • bad0ae17 chore(knip): drop obsolete ignore entries and dead ignoreIssues overrides
  • b14bdb57 chore(codereview): exclude internal tooling from analyze-structure walk
  • 5cb7671e chore(codereview): exclude packages/*/lib build output from walks
  • b0672467 refactor(codereview): split lookalikes into its own module + scripts shims
  • 933601b3 refactor(codereview): extract WDA primitives into wda.ts (real cycle fix)
  • e91bff75 refactor(codereview): break log-doctor static cycle via createRequire
  • a3ecb695 fix(codereview): track re-export edges and platform variants in analyze-structure
  • f3bfd3c6 fix(codereview): correct re-export and aliased-import name extraction
  • 45af0172 refactor(codereview): split analyze-structure into metrics + extract modules; rewrite README
  • 5a3dfa45 refactor(codereview): drop scripts/ shims, fold lookalikes into analyze-structure subcommand
  • 868dab69 refactor(codereview): consolidate analyze-structure / lookalikes / log-doctor into codereview/
  • 489e16e8 Add --focus filter to analyze-structure
  • dd7c0a18 refactor: default analyze-structure to verbose with opt-out flags
  • e292fb09 feat(scripts): expand analyze-structure with depth, smell, history, and llm reports
  • d76077b4 feat(scripts): add structural-health score to analyze-structure
  • d9d86119 feat(scripts): replace AUDIT.md/TASK.md with concise audit and fix prompts
  • 4a5e9987 fix(scripts): bind matt pocock skills to fixer phases and self-check
  • cbe07dc3 fix(scripts): force-add gitignored audits in fixer audit-status commit
  • d73d260a fix(scripts): land coco-payment-ux mission and guiding principles in fixer prompt
  • 04082c61 docs(scripts): bias audit and fix prompts toward deletion over addition
  • 8f099b6b Expand review dimensions from 10 to 14
  • 7ad0ec72 Require Matt Pocock process-skill loads (Pass 0)
  • a02ab56f Add structural cross-cites and boy-scout rule
  • 2a05d9bc chore(skills): add zod, zustand, typescript and prompt-engineering skills
  • 1d156ba3 chore(skills): add matt-pocock skill set and auditor protocol
  • c3a3b417 docs(rules): remove legacy cursor and claude rule files
  • d4b31479 docs: drop stale ContactRow spec
  • e8ab80d9 chore(audits): stop tracking audits findings
  • 44425cee docs(codereview): drop audit-status commit + force-add machinery
  • d01c9a22 chore(scripts): drop orphaned palette exploration scripts and JSON snapshots
  • d7f2307c refactor(e2e): drop legacy yaml test runner from log-doctor

Structure / knip cleanup

  • f50119ec chore(structure): delete dead code surfaced by knip + analyze-structure
  • 315180c0 chore(structure): kill duplicate export names, fix re-export classifier
  • 9a0f0f72 chore(structure): drop unused exports + resolve duplicate names

Audit-status annotations

  • ~250 chore(audits): annotate completion status commits (one per shipped slice). __audits__/ is gitignored; annotations live on disk only.

Branch base

  • 38797b50 fix: address 12 user-reported bugs across mint, send/receive, swap, split-bill, navigation (the original 12-bug commit that named the branch)

Kelbie added 30 commits May 4, 2026 19:01
…elper

ChatMessageBubble carried a local formatTimestamp expecting unix ms;
UserMessagesScreen carried an identical function expecting unix seconds.
Same display contract, drifted unit — a textbook ubiquitous-language miss
that a single audit comment had been tracking as partial.

Extract one formatChatTimestamp(unixMs) into shared/ui/composed/chat/ and
delete both copies. UserMessagesScreen call sites now multiply Nostr
created_at by 1000 at the boundary, making the seconds-to-ms conversion
explicit instead of hiding it inside a formatter.

Boy-scout (skill:improve-codebase-architecture): ChatMessageBubble.tsx —
deletion test passed, the local helper had no leverage; sibling file is
the obvious home and lookalikes drops two of three formatTimestamp rows.

Refs: __audits__/20.json#F-006
Promote the private assertPubkeyHex helper in shared/lib/nostr/secureStorage.ts
to an exported `isNostrPubkeyHex` type predicate, and migrate the two inline
duplicates that previously accepted any 64-char string regardless of charset:

- features/feed/components/HomeFeed.tsx:107 (getCategoryPubkeysFromSpec) — was
  `value.length !== 64`, which let UTF-8 mojibake or e.g. `01javascript:…`
  padded to 64 chars flow into Primal `mega_feed_directive` and NDK `#e`
  filters as a "pubkey".
- features/settings/screens/SettingsKeyringScreen.tsx:286 (raw-hex private-key
  import path) — was inlining its own `length === 64 && /^[0-9a-fA-F]+$/` check.

The predicate keeps the `secureStorage.assertPubkeyHex` throwing API as a
one-line consumer so trust-boundary callers can choose between predicate
narrowing and assertion. A colocated jest test pins the exact attack shape
the audit named (a 64-char string with non-hex characters must be rejected).

Refs: __audits__/26.json#F-005
… extractor

Two file-local `extractNostrPubkey` copies had drifted: `shared/lib/getMintCatalog.ts`
required canonical 64-char hex (the right trust gate, but it silently dropped any
mint operator publishing `npub1…` per NUT-06 — followers/reputation pills vanished
from the picker after b5ca041), while `features/mint/hooks/useMintProfiles.ts`
accepted any non-empty string (impersonation surface still open on the search
screen).

Replace both with `shared/lib/nostr/extractMintNostrPubkey.ts`, which accepts
canonical hex via the existing `isNostrPubkeyHex` predicate and `npub1…` decoded
through `nip19.decode` with checksum validation; rejects everything else
(LN addresses, URLs, malformed bech32, mojibake, wrong-prefix bech32). The
shared helper closes the lookalikes 2-way collision and lets both call sites
agree on one trust posture.

Boy-scout (skill:improve-codebase-architecture): shared/lib/getMintCatalog.ts —
drop the local `ContactEntry` interface and `NOSTR_HEX_PUBKEY_REGEX` constant,
move the structural input type into the helper.
Boy-scout (skill:improve-codebase-architecture): features/mint/hooks/useMintProfiles.ts —
drop the local `MintContactList` / `MintInfoForProfile` types that existed only
to feed the deleted extractor.

Refs: __audits__/60.json#F-001, __audits__/60.json#F-002
Render-time and catch-block logging at payment+feed boundaries was
leaking bytes into log.txt and Sentry breadcrumbs:

- PaymentInfo.tsx:118 logged `preview: selectedValue.slice(0, 30)` on
  every render, where selectedValue is a cashuB token, bolt11 invoice,
  Lightning Address, or payment-request URI.
- QRCode.tsx:124 had the same `preview: address.slice(0, 30)` shape on
  every address change (sister site, same pattern, not separately cited).
- HomeFeed.tsx:321 and :527 logged `{ error }` directly from the unknown
  caught in loadFeed / loadMoreItems, which can carry stack/message/url
  bytes from the WebSocket failure with no redaction.

The logger's SECRET_STRING_PATTERNS guard inside summarizeString only
fires when the call site hands the FULL value (and the string exceeds
maxStringLength=120). Manual `value.slice(0, 30)` bypasses the seam by
construction. Fix is to drop the preview field at every call site and
narrow unknown errors before logging.

PaymentInfo also migrates its three log.{info,debug} calls onto the
existing paymentLog child logger (already used by the popup neighbours
in shared/lib/popup/popups/payment.ts), per the audit's recommendation.

Refs: __audits__/46.json#F-003 [High], __audits__/26.json#F-017 [Low]
… mapping

decodeTextRecord previously logged a warn and decoded UTF-16 bytes as UTF-8
anyway — silent garbage out for any tag written with the UTF-16 encoding bit.
Now it dispatches to a small decodeUtf16 helper that honors FE FF / FF FE
BOMs and defaults to big-endian per the NFC Forum Text RTD. Buffer has no
native utf16be, so BE input is byte-swapped in place before utf16le decode.

buildTextNdef gains an optional { lang } override (default NDEF_TEXT_LANG
= 'en' in constants.ts) so the magic literal is named at the call site, and
validates the ASCII-bytes ≤63 limit imposed by the status byte's lang-length
field.

apdu.ts's catch block extracts the substring-match cascade into a named
mapTransceiveError helper. The brittleness is real and unfixable at the
consumer level — react-native-nfc-manager surfaces transceive failures as
plain strings on both platforms with no error code on the JS side — but it
now lives in one documented place, ready to swap if the library ever
exposes structured codes.

New test in __tests__/nfcNdefDecode.test.ts locks down UTF-8 round-trip
(short + normal records), UTF-16 BE-no-BOM / BE-BOM / LE-BOM decode paths,
and the explicit-lang round-trip.

Refs: __audits__/48.json#F-005, __audits__/48.json#F-007,
__audits__/48.json#F-008, __audits__/48.json#F-012
…n and SettingsRecoveryScreen

SlideToDelete and SlideToRecover were near-identical: same THUMB_SIZE/TRACK_PADDING
constants, same Pan-gesture/threshold/spring config, same track/thumb/textContainer
styles. Both wrapped a redundant GestureHandlerRootView (the app root and drawer
root each provide one already), and both shipped a StyleSheet.create block in
files that otherwise use Uniwind className.

Collapsed both onto a single SlideToConfirm in shared/ui/composed; static layout
moved to Uniwind classes; redundant gesture-root wrappers and unused
gesture/animation imports removed from both screens.

Refs: __audits__/24.json#F-010, __audits__/24.json#F-015, __audits__/24.json#F-023
… ChatMessage.senderPubkey

ChatMessage.senderPubkey carried three semantically distinct identities (BLE
peer-id, per-geohash Nostr pubkey, '' for own echoes) and was treated as a
pubkey by ChatMessageBubble's avatar seed. Renamed to senderId end-to-end
(ChatMessage, ChatBubbleMessage, useMessageGrouping, useBitChat,
GeohashChatScreen, WhitenoiseDMScreen) so the field name matches its
transport-agnostic role; protocol-truth pubkeys on NostrMessageEvent /
NostrPrivateMessageEvent stay as senderPubkey.

Same slice tightens the bridge surface: addBLEPeerListener now takes a typed
BLEPeerEvent (was `event: any`); BLEPeer / BLEMessageEvent / BLEDiagnostics /
BLEPeerEvent live in src/types.ts (consolidated from BitChatModule.ts);
deleted unused public exports (nativeEncodeGeohash, nativeDecodeGeohash,
getNeighbors, getClosestRelays, getClosestRelaysForGeohash, decodeGeohash,
stopBLE, getBLEDiagnostics, stopNostr, BitChatEventMap, Participant,
RelayStatus) — zero non-iOS callers.

Refs: __audits__/63.json#F-002, __audits__/63.json#F-004, __audits__/63.json#F-006, __audits__/63.json#F-007, __audits__/63.json#F-010
…ing shared geohash subs

Three lifecycle bugs in useBitChat clustered around cleanup doing too much:

1. Each per-transport effect cleared the React message buffer on every
   teardown. Deps like `nickname` resolving from useBitchatNickname after
   first render churned the effect and wiped history — for ble-dm, where
   BLE has no replay path, that loss was permanent. Reset is now a single
   identity-keyed effect (transport/dmPeerID/geohash); per-transport
   cleanups only tear down the subscription.

2. The public-nostr cleanup called leaveGeohash() unconditionally. Native
   side (BitChatNostrBridge) keeps one active geohash that fans out to
   BOTH the public chat sub (`geo-{g}`) AND the gift-wrap DM sub
   (`geo-dm-{g}`). Closing the public screen while a nostr-dm thread on
   the same geohash was open silently tore down the DM subscription. The
   nostr-dm cleanup already documented this contract; the public branch
   now matches.

3. bitchatDM passed `geohash ?? 'mesh'` to GeohashChatScreen so a
   missing geohash for transport='nostr-dm' would have subscribed to an
   attacker-populatable 'mesh' pseudo-channel. The zod refine on the
   route already forces nostr-dm to carry geohash, so the fallback was
   dead defensive code — but the type lied about it. Widened
   useBitChat/GeohashChatScreen geohash to string | undefined so the
   hook's existing !geohash short-circuit handles missing input
   honestly.

Refs: __audits__/13.json#F-003, __audits__/13.json#F-005, __audits__/49.json#F-003
…gial wallpaper provider

Extract the per-unit wallpaper resolver into shared/lib/theme/resolveUnitWallpaper.ts
(pure functions) plus shared/lib/theme/useUnitWallpaper.ts (Zustand hook). themeStore
no longer imports wallpaperStore — the only sovran-app cycle in analyze-structure goes
away. wallpaperStore keeps reading themeStore as a one-way arrow.

ProfileWallpaperProvider was a Context wrapper around the same useThemeStore selector
that nobody read (the hook bypassed the Context entirely). Deleting it removes one
provider layer from AccountScopedProviders compose chain. Account.tsx and ThemeProvider
import the hook directly from the new module; themeDraft consolidates onto the new
getCatalogThemesForAlbum helper, dropping its inline duplicate.

Adds __tests__/resolveUnitWallpaper.test.ts covering the four-step fallback chain
(override → first-stored override → newest album → 'dark') plus the built-in colors
album short-circuit.

analyze-structure: Cycles 2→1, Architecture 40→55, Overall 41→45.

Refs: __audits__/16.json#F-004, __audits__/41.json#F-004
…sum cache-buster

Three intent-vs-behavior bugs in features/feed where the shape of the code
implied dynamic behaviour but the runtime behaviour was dead, wasted, or wrong.

- createPrimalRelayClient was assigning ws.onerror/onclose twice: once before
  the openPromise constructor, then again inside it. Wire all three handlers
  (onopen/onerror/onclose) exactly once inside the constructor, with the
  openSettled-guarded settle() clearing the open timeout. Same failure
  semantics in both pre-open and post-open paths.

- engagementRevision summed entry.updatedAt timestamps as a cache-busting
  identifier; the sum could decrement when entries were removed and is not
  what the LegendList consumer wants from a "revision". Replace with a
  ref-counter that bumps on each useMemo recompute. (Substitutes the audit's
  in-store counter shape with an in-hook ref-counter to avoid a persist-shape
  change — same monotonic-identifier semantics.)

- The actions = useRef(useNostrSocialStore.getState()) + refreshing useEffect
  was performing a per-render no-op: Zustand 5 store-method references are
  stable across renders, so re-snapshotting the store just to read .actions
  off it adds nothing over reading useNostrSocialStore.getState() inline.
  Delete the ref and inline the seven call sites.

Boy-scout (skill:improve-codebase-architecture): drop the redundant `export`
keyword on getVideoUrlsFromContent, prettifyUrl, normalizeRawPrimalEvent, and
getFirstTagValue in nostr/shared.tsx — analyze-structure flagged them as
"exported but only used internally", confirmed by zero external imports.

Refs: __audits__/26.json#F-012, __audits__/26.json#F-014, __audits__/26.json#F-019
…d drop type re-export drift

AnimatedImageOverlay.tsx was using runOnJS from react-native-reanimated
while sibling provider.tsx already used scheduleOnRN from
react-native-worklets — same package, two equivalent scheduling APIs
side by side. Standardise on scheduleOnRN across all 19 worklet→JS call
sites so the package has one canonical surface (Reanimated v4's recommended
API moved to react-native-worklets).

Also delete the duplicate type re-export block in provider.tsx that
mirrored five types already exported from ./types and re-exported by
the index barrel — three import paths collapsed to one. External
consumers already routed through the barrel; internal siblings already
import from ./types.

Boy-scout (skill:improve-codebase-architecture): provider.tsx — reorder
the five session/timeout refs above registerThumbnailLayout so the
closure no longer references thumbnailLayoutsRef declared 65 lines later
(temporal hazard for readers; useRef hoisting made it work).

Net -8 lines across two files. No behaviour change; existing
imageOverlayComputeExpandedSize.test.ts continues to pass.

Refs: __audits__/58.json#F-003, __audits__/58.json#F-005, __audits__/58.json#F-008
…r brand/picker emojis locally

AnimatedEmoji.tsx wrapped expo-image around a fonts.gstatic.com URL keyed by
emoji codepoints, leaking the user's emoji selection pattern to Google for every
unique emoji rendered (Noto animated WebP CDN). Two consumers — MarmotIcon
(whitenoise brand glyph) and the emoji-picker popup's copy-confirmation icon.
Both render fine through the OS emoji font (the existing `failed` fallback in
AnimatedEmoji at lines 22-24 was already this exact path), so promote the
fallback to the primary path and delete the primitive. Net -49 LOC, no network
dependency, no behaviour change on offline cold-start.

Refs: __audits__/17.json#F-016, __audits__/33.json#F-014, __audits__/52.json#F-013
Three audit findings shared one root cause: the streaming send had no
lifecycle handle. Fixed together so the hook owns one consistent flow.

- Hook-scoped AbortController aborts a prior in-flight stream when a new
  send/retry starts, threads `signal` through sendMessage to fetch, and
  is aborted from a cleanup useEffect on unmount. Backgrounding or
  unmounting mid-stream now stops billing instead of writing tokens into
  a stale tree.
- Balance refresh promise is tracked in a ref; the next streamIntoPlaceholder
  awaits it before snapshotting `balanceBeforeMsats`. Retry-during-
  balance-refresh used to capture the same pre-send balance as the first
  call and double-count the cost diff.
- Stream finalize logic moved to features/ai/lib/finalize.ts:pickFinalizeMessage
  with a regression test. Reasoning-only streams (DeepSeek-R1 truncated
  after thinking; o-series with low-effort caps) now persist
  `{content:'', reasoningContent}` so the bubble's `hasContent` check
  renders the reasoning section without the apologetic
  "(No response received)" body. Cost-stamp finalize re-uses the same
  payload so the second write doesn't undo the first.

AbortError from any of the three (sendMessage, for-await loop,
checkBalance) is treated as a normal lifecycle event: the placeholder is
removed, the span tags `outcome: 'aborted'`, and no failure popup fires.

Refs: __audits__/34.json#F-003 (High), __audits__/34.json#F-004 (Medium),
__audits__/34.json#F-013 (Low)
Engine and bridge were positioned at shared/lib/popup/ root but consumed
only by popups/* wrappers (15/15 of engine, 3/4 of bridge); external
callers always go through the index.ts barrel. The split was purely
organisational and obscured the public-vs-internal boundary of the popup
module — analyze-structure flagged both files as MOVE candidates.

Refs: __audits__/42.json#F-006
WhitenoiseSigner now copies the user's nsec into a buffer it owns and
exposes dispose() that zeros the buffer + trips a guard so subsequent
sign / nip44 calls throw. WhitenoiseProvider's existing useEffect
cleanup calls disposeSigner on profile-switch and unmount so the
previous account's key bytes don't sit in process memory until GC
reclaims the slab. Defense-in-depth — Hermes does not zero freed
memory, and nostr-tools' nip44.v2.utils.getConversationKey may cache
derived secrets internally (UNVERIFIED upstream); zeroing the input is
the minimal step we control. Regression test pins the disposal seam.

useWhitenoiseDM no longer takes accountIndex as a parameter — reads it
from useWhitenoise() context like the sibling hooks. The lazy-init
useRef for WhitenoiseDmIndex is now a useMemo keyed on accountIndex,
so a future provider re-mount with a different account creates a fresh
index instead of reusing the previous account's. Closes a class of
silent-corruption bugs across the provider/hook seam.

upsertMessage uses sorted insertion (O(n) per upsert) instead of full
sort. resolveInboxRelays() helper extracted in client/network.ts so the
inbox watcher and DM-send path apply the same try/catch + fallback.

Refs: __audits__/33.json#F-004, __audits__/33.json#F-008,
__audits__/33.json#F-012, __audits__/33.json#F-018
Kelbie added 2 commits May 11, 2026 10:12
Route paymentOptionsPopup / paymentFallbackPopup through PopupHost's
standalone <BottomSheet> (FullWindowOverlay enabled) instead of the
heroui Menu lane, which uses disableFullWindowOverlay and renders
behind iOS route modals like the send-flow camera.
NPubCash receive was broken after 8b16e5b pointed the plugin at npub.cash,
which doesn't serve the sync API. Centralize the host as npubx.cash (matching
eNuts), key the sync cursor by pubkey so every profile gets its own durable
position, and replace the mocked username claim + localhost punt with the real
npubcash-sdk NPCClient.setUsername flow. The Select Mint screen now hides the
add-mint and inspect chrome under NPC scope and disables (0.5 opacity + reason)
mints that don't speak NUT-17 websockets, so the receiving mint can actually
auto-redeem paid quotes.
@Kelbie Kelbie changed the title Refactor refactor: ship 37 audit findings across wallet surfaces May 11, 2026
Kelbie and others added 26 commits May 11, 2026 22:19
iOS 26 uniformly dims any RN FlatList/VirtualizedList in the AI tab's
wrapper tree (affects inverted AND non-inverted lists; not the same
UIScrollEdgeEffect bug from facebook/react-native#54181 — we disabled
all four edge effects via patch-package and the dim survived).

LegendList sidesteps the dim entirely because it uses its own
virtualization rather than RN's FlatList path. Rebuild AiChatScreen
directly on it instead of through the shared <ChatScreen /> (GiftedChat).
Other chat surfaces (BitChat, WhiteNoise, Nostr DM, geohash) live in
(user-flow) modal stacks where the dim doesn't reproduce, so they keep
using the shared ChatScreen and aren't touched.

Per-file:

- features/ai/screens/AiChatScreen.tsx: full rewrite onto LegendList v3
  beta. Uses the canonical chat pattern (initialScrollAtEnd,
  alignItemsAtEnd, maintainScrollAtEnd, maintainVisibleContentPosition,
  recycleItems) — no manual scrollToEnd chasers. Composer + list both
  ride the keyboard via a single shared Reanimated translateY (driven
  from useReanimatedKeyboardAnimation height/progress), since RN's
  position:absolute children don't lift with KeyboardAvoidingView's
  paddingBottom. On mount, archive any in-progress conversation via
  routstrStore.createSession() so the surface always opens empty.

- app/(drawer)/(tabs)/ai/_layout.tsx: scrollEdgeEffects:hidden on all
  four edges of the AI Stack.Screen. iOS 26 defaults this to 'automatic'
  which renders a UIKit gradient material (opaque-to-transparent) at
  scroll edges; on AI that showed up as a top fade on the chat content.

- app/(drawer)/(tabs)/_layout.tsx: AI NativeTabs.Trigger gets
  disableAutomaticContentInsets + scrollEdgeEffects:hidden on the tab
  bar. Same iOS 26 edge-effect mechanism, just for the tab bar surface.

- shared/ui/composed/chat/ChatScreen.tsx: iOS 26 added a new
  auto-management path that re-applies scrollIndicator vibrancy via
  automaticallyAdjustsScrollIndicatorInsets even when
  contentInsetAdjustmentBehavior is 'never'. Force-disable both indicator
  auto-adjust and pin static insets to zero so UIKit has no insets to
  drive material from.
Strip three pieces from 4c07290 that weren't strictly necessary for the
AI tab fix:

- app/(drawer)/(tabs)/ai/_layout.tsx — revert to `headerTransparent: true`
  and drop the `scrollEdgeEffects:hidden` Stack.Screen prop. The fade
  these were aimed at was never verified visually.

- app/(drawer)/(tabs)/_layout.tsx — drop the AI NativeTabs.Trigger props
  (`disableAutomaticContentInsets`, `unstable_nativeProps.scrollEdgeEffects`).
  Pre-session work bundled in under the iOS-26 theme; not introduced or
  verified by the LegendList port itself.

- shared/ui/composed/chat/ChatScreen.tsx — revert the scrollIndicator
  auto-adjust tweak. Affects only the GiftedChat-based ChatScreen which
  AI no longer consumes (other surfaces — BitChat / WhiteNoise /
  Nostr DM / geohash — are unchanged from main).

Also tidy AiChatScreen.tsx after the revert: remove the now-unused
`topInset` local + the stale `headerTransparent: false` / v2
`waitForInitialLayout` comments. No behavior change.
Adds a `PatternBackground` composed component that loads
`assets/icons/internal/pattern.svg` once, rewrites every `fill="#hex"` to
`fill="currentColor"`, strips the full-canvas backdrop path, and lets
react-native-svg's native `<pattern>` rasterize one tile that the GPU
repeats — so the source's 216 paths cost one parse per mount instead of
one parse per repeat. Each path now carries `fill-opacity="0.08"` so
overlapping shapes build up depth instead of flattening under a single
wrapper alpha.

Mounted on the AI tab behind the chat list and composer. Theme-driven by
default: `color` defaults to `useThemeColor('foreground')` so the
pattern inverts naturally between light and dark themes.

Drops the centered robot empty-state — the pattern carries the visual
weight on an empty session. Aligns `AiLayout` Stack `contentStyle` with
the surface token used by `SearchLayout` for Feed/Contacts so transition
bleed-through doesn't show a transparent fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the "≈ <fiat>" pill read as the same family as the split-bill /
swap / theme buttons in every capability state: neutral muted border on
flat, a real BlurView capsule on iOS non-liquid (was a fake tinted
flat), and matching glass on liquid. Green identity now lives in the
chrome tint (0.28) instead of double-greening with green text — text
uses foreground, mirroring how the action buttons render their icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a neutral grayscale Light palette, surfaces it in the built-in
themes, and replaces hard-coded chrome with theme-aware tokens so the
app renders correctly when `background` is light.

- New `useColorScheme` hook (BT.601 luma of the `background` token).
- Thread `colorScheme` into SwiftUI glass surfaces (BalancePill,
  CircleActionButton, PrimaryBalance EcashStatusPill) and
  `liquid-glass-text` via a `colorScheme` prop, since the app pins
  `userInterfaceStyle: dark` at the window level.
- Default `View blur` tint to `systemThickMaterialLight` on light
  themes; `CircleActionButton.blur` switches to a light tint.
- QRButton inverts foreground/background with the theme; QRCode and
  the keyring QR are pinned dark-on-white for scanner reliability.
- CustomKeyboard, FeedScreen, ContactsScreen, MonthlyChart, and
  ScreenStates pull `foreground` / `muted` / `danger` / `success`
  instead of `opacity(foreground, ...)` or hex literals.
- SearchLayout sets `headerTitleStyle` and `headerTintColor`
  explicitly so the native bar title doesn't render invisible.
- BitcoinNearYou gates the iOS blend stack and adds a `screen`-blend
  light lift; Android stays plain.
- AmountFormatter / AmountEntryView use `success` for receive
  amounts; FiatCurrencyPill swaps `green-500@0.28` -> `green-400@0.4`.
- themeEngine light overlay shadow uses an inset bright edge + soft
  white glow + 4 %-black ring (dark drop-shadow read as muddy).
- Extract `UnderlineTabs` from ContactsScreen and migrate
  ContactsScreen + ReceiveScreen to it; ReceiveScreen's P2PK row
  gains a `GradientCard` wrapper.
- wallpaperSync drops albums whose `author.pubkey` doesn't match
  Sovran's published pubkey, with a debug log of dropped authors.
- patches: react-native-gifted-chat switches its `AnimatedFlatList`
  from RNGH's FlatList to RN's FlatList so the inverted scroll
  transform lands on the native list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tabs

- QR card uses GradientCard on light theme so the code reads on frosted
  chrome instead of vanishing into a flat white surface
- Bitcoin currency disc keeps a white "B" since the disc is always orange,
  preventing a near-black glyph on orange in light mode
- ReceiveScreen tab row gets horizontal margin so it aligns with the
  surrounding content gutter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuous-corner radius on the scene's left side, surface-colored backdrop
so the rounded gaps blend with the drawer, and an inset hairline
(separator-secondary) hosted on the overlay that traces the curve when the
drawer opens. Drawer dim and menu scrim flip to white in light mode.
Patches @react-navigation/drawer to forward overlayStyle from screenOptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Map `--success` to blue in the design system so every `variant="success"`
surface (StatusToast, PaymentStatusIcon, Badge, etc.) picks up blue
automatically, and swap every direct `green-*` reader to `blue-*` so the
old green accents disappear without losing the green scale itself.

Reshape the fiat "≈" pill to a neutral white glass wash with adaptive
text (#FFF on dark / system, #000 on light) and wire `tint(success)`
on the SwiftUI Menu so the selected currency row highlights blue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the OfflineShell accent from `red-300` to `blue-300` so the
offline banner + screen border match the new blue success/info palette.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e edge

Pull the scene + overlay corner radius from `expo-screen-corner-radius`
so the drawer hugs the physical display curve instead of a fixed token
(falls back to `radius['2xl']` on Android <12 or non-rounded displays).
Fire a single Light-impact haptic on every drawer state transition
(gesture, hamburger, overlay tap all converge on the same status flip),
and widen `swipeEdgeWidth` 40 → 128 so the open gesture is easier to
catch from the screen edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drive the composer + list lift via a Reanimated translate that reads
keyboard height directly, instead of relying on KeyboardAvoidingView's
padding to lift an absolutely-positioned composer. The KAV path was
unreliable — composer ended up either pinned under the keyboard or
floating well above it depending on whether RN resolves position:absolute
against the border or padding box in the current context (full-screen
vs user-flow modal stack).

AiChatScreen also folds the SovranTabBar height into the lift formula
on the non-NativeTabs path so the composer lands flush at the keyboard
top instead of overshooting by the tab bar's height.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d blur

Replaces react-native-gifted-chat (inverted FlatList + internal KAV) with
LegendList for every DM-like chat surface (BitChat, Nostr DM, WhiteNoise,
geohash), matching AiChatScreen's architecture so the whole app uses one
list library.

The migration also resolves an iOS 26 chrome artifact where, on keyboard
dismissal, a backdrop material the height of the keyboard would persist
behind the chat content. Two changes were required:

  - Static `StyleSheet.absoluteFillObject` backdrop sibling at the bottom
    of ChatScreen's z-stack (mirrors AiChatScreen's `<PatternBackground />`).
    Without a static solid sibling, iOS 26 perceives any animated child as
    a translucent moving layer and captures a backdrop snapshot.

  - Lift the list wrapper with `top: keyboardHeight.value` (Yoga layout
    property) instead of `transform: [{ translateY: ... }]` (CALayer
    transform). Transforms promote the wrapper to its own compositor layer,
    which iOS 26 captures during keyboard transitions; layout offsets stay
    on the same render layer and avoid the snapshot.

Composer uses react-native-keyboard-controller's `<KeyboardStickyView>`
instead of a hand-rolled Reanimated translate.

Drops the `react-native-gifted-chat` dependency. Also cleans up
ModalLayoutWrapper to only mount its debug-overlay Views when `debug` is
true — the empty leaves were blocking react-native-screens' scroll-view
finder from reaching any underlying scroll view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tpatch@^1.0.0

The published nutpatch only exposes an OutputCreator HybridObject (targeting
cashu-ts 4.x), not the lower-level primitives our local fork added —
hashToCurve/blind/unblind/hashE/dleq, pbkdf2HmacSha512, nip44Ecdh, chacha20,
hmacSha256, batchDeriveLegacy. Rather than maintain a vendored Nitro module
with monocypher + secp256k1 to keep parity, fall back to JS for these paths
and park nutpatch@1.0.0 as a dependency until we upgrade cashu-ts.

Removed:
- packages/nutpatch/ — vendored Nitro module
- shared/lib/cashu/nativeCrypto.ts — Crypto HybridObject bridge
- Native NIP-44 v2 path in shared/lib/nostr/nip17.ts (probeNative, native
  ECDH/ChaCha20/HMAC, hkdfExpandNative, nip44DecryptNative) → nostr-tools
  nip44.v2 fallback
- Native PBKDF2 path in shared/lib/nostr/keyDerivation.ts → bip39
  mnemonicToSeedSync; the in-memory rootSeed cache stays so PBKDF2 runs
  once per profile switch
- __CASHU_NATIVE bridge + batchDeriveLegacy branch from cashu-ts patch

Kept in cashu-ts patch: __CASHU_PERF telemetry (consumed by
SettingsRecoveryScreen + log-doctor) and the JS-only _deriveBoth /
_masterCache / _keysetCache BIP-32 derivation cache.

Perf regressions on Hermes (acknowledged):
- BIP-39 mnemonicToSeed: ms → ~3s cold (cached after first call)
- NIP-44 encrypt/decrypt: sub-ms → 5–15ms ECDH + JS ChaCha20/HMAC
- Cashu hashToCurve/blind/unblind/hashE: ~100× slower (noble-curves)
- NUT-13 batch derive: 1 native call → N JS derivations (still keyset-cached)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the success token (`--success` / `--success-foreground`) and a
handful of consumer screens (rebalance plan, recovery, mint-add validator,
transfer separator, Badge variant="success", SelectableCheck "success")
back to green. The prior blue mapping shipped with the light-theme polish
sweep made positive states indistinguishable from primary/link surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flow

Adds two fire-and-forget machine operations to coco-payment-ux:
`resolveRecipientPubkey` (Lightning Address → Nostr hex pubkey via NIP-05)
and `resolveRecipientProfile` (pubkey → kind-0 metadata). Both fan out
from `send()` when a melt target is set, so amount-entry, mint-select,
melt-preview, and payment-request screens render "Pay <name>" with an
avatar instead of an opaque lud16 string. Default implementations ship
with the package (nip05.ts, recipient.ts); the wallet wires the SWR-cache-
backed profile resolver in CocoPaymentUX via `parseRawMetadata`.

Receive-side history headers gain a `showRecipientAvatar` opt-out so the
new avatar doesn't replace the mint-quote / receive-token icon (those are
self-receive flows with no counterparty).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…very acks

Native BLE bridge rewrite (BitChatBLEBridge.swift): drops the standalone
geohash helper, folds nostr-bridge cleanup into the main module, and
emits a per-message delivery-status stream (sending → sent → delivered →
failed). `sendBLEPrivateMessage` now takes a caller-supplied messageId
so the JS layer can correlate acks back to the optimistic bubble.

JS side adds a persisted `bitchatDmMessages` store (thread state keyed
by 16-hex peerID — peerIDs aren't pubkeys, so they live alongside the
nostr DM cache rather than inside it) and a `useBitchatDmContacts` hook
backed by a UserDefaults-mirrored DM-history map so peers we've messaged
survive app kills.

`useSplitBillOrchestrator` and the geohash/network screens are updated
for the new `sendBLEPrivateMessage(peerID, content, nickname, messageId)`
signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eed override

`ChatBubbleMessage.deliveryStatus` grows from sending/sent to also include
`'delivered'` (double-check glyph after counterparty decrypt-ack) and
`'failed'` (warning glyph — typically a BitChat handshake that never
completed). Nostr-based DMs still only ever surface sending/sent; the
extra states fire from the new BLE delivery-status stream.

`DmChatHeader` gains a `seed` prop so the header avatar can match
in-thread bubble avatars when the row's identity isn't a pubkey (e.g.
BLE peerID DMs). `ChatMessageBubble` is re-exported from the package
index so feature screens can import it without reaching into the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s anim

- `PullToAiRefreshControl` — a drop-in `RefreshControl` that navigates to
  the AI tab on pull. Replaces an earlier `Gesture.Fling()` wrapper that
  lost to inner scroll views. Wired into the Wallet, Feed, and (later
  commit) Contacts top-level lists.
- `CurrencySwapperPill` — sat↔fiat toggle for the amount-entry screen,
  built on `BalancePill` chrome (`ctaLabel` mode) so the liquid/blur/flat
  variants stay aligned with the wallet header pill. Adds a `ctaLabel`
  rendering path to `BalancePill` and surfaces `ctaIcon` color in the
  flat variant.
- `MintSelector` accepts `height` / `contentHeight` overrides so the
  amount-entry pill can render at a non-default size with correct inner
  padding.
- `SovranTabBar` icons now spring-scale on press (88% on press-in, snap
  back via spring on press-out) instead of a static `surface-secondary`
  fill. Mirrors the iOS 17 tab-bar feel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…emo mode

ContactsScreen now merges three sources into the Recent / All tabs:
NIP-17/NIP-04 nostr contacts, accepted Marmot DM counterparties, and
persisted BitChat BLE-DM peers. BLE rows live in a separate namespace
(`ble:<peerID>`) because peerIDs are 16-hex BLE identifiers, not Schnorr
pubkeys, and route to the BLE DM screen instead of the nostr one. The
ContactRow API adds a matching `bleIdentity` constructor.

`mockDataStore` is extended with a small curated set of mock contacts
(real npubs decoded to hex once, hand-authored threads, kind-0 metadata
seeded into the SWR cache). `useRecentContacts` and `UserMessagesScreen`
short-circuit on mock pubkeys when `mockMode` is on so the demo flow has
real-looking conversations without ever publishing to the network.

Also wires `PullToAiRefreshControl` into both list variants in
ContactsScreen so the AI pull works there too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The avatar fade (600 → 320ms) and the verified-badge spring (delay
300 → 80ms, snappier damping/stiffness) felt sluggish after the rest
of the screen had landed. New timings make the badge feel attached to
the avatar rather than a delayed afterthought.

Adds an opt-in OK-badge override (`okBg`, `okIcon`, `okOutline`) so the
verified-mint marker stays green even though the app-wide `success`
token is now blue. The override draws a solid green-400 disc with a
green-100 checkmark and a background-tinted ring, matching the seam
between the badge and the avatar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DM and AI chat surfaces migrated to LegendList in earlier commits
(1c825ff, 4c07290). The gifted-chat patch hasn't applied to any
runtime code since — patch-package emits a warning on install — so
removing it cleans up the install log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add deterministic NIP-05 resolver coverage, including the real odell@primal.net response shape, plus machine and screen-action tests for recipient pubkey/profile propagation.

Security-impact: low

Touches-keys: false
Replaces the two-button Equalize/Rebalance footer with a labeled
CircleActionButton row (Split / Reset / Focus) above a single Next
CTA, matching the wallet home secondary-action treatment. Adds
mirrorBalances and concentrateOnPrimary helpers to the distribution
store so all three actions write a deterministic 10,000bp split.

Also tidies the per-mint item: Min on the left / Max on the right
with icons sharing the label color, and wraps the help copy in
<Card variant="info"> with clearer wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Queue the pull-to-AI navigation while the user is still dragging and commit it from the scroll release callback. Wire the release handlers through the wallet, feed, and contacts scroll hosts so the refresh threshold no longer switches tabs mid-gesture.

Security-impact: none

Touches-keys: false
Add a persisted developer setting for White Noise visibility and expose it beside the existing mock no-glass control.

Use the setting to hide White Noise from contact filters, request/contact rows, the setup banner, and the profile send menu until the developer toggle is enabled.

Security-impact: none

Touches-keys: false
Native bitchat singletons previously shared one keychain/storage namespace
across profiles, leaking DM history and transport identity. Thread the
active profile scope through startBLE/startNostr and recreate the native
services on scope change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Kelbie Kelbie changed the title refactor: ship 37 audit findings across wallet surfaces fix: 12 reported bugs + audit drain, iOS 26 chat, light theme, bitchat overhaul May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant