Skip to content

Add mobile vault screen for deposit, withdraw, and escrow management - #1

Open
BigManly4 wants to merge 45 commits into
mainfrom
mobile/vault-screen-477
Open

Add mobile vault screen for deposit, withdraw, and escrow management#1
BigManly4 wants to merge 45 commits into
mainfrom
mobile/vault-screen-477

Conversation

@BigManly4

@BigManly4 BigManly4 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Ports the vault sub-account feature from the Next.js web wallet to the Expo mobile app, following the conventions already established by the bulk-payout and swap mobile ports in this repo.

Recreates the work from Miracle656#521, whose branch was deleted before merge.

What was ported

  • frontend/mobile/lib/vault.ts — typed vault domain logic ported from frontend/wallet/lib/vault.ts: XLM/stroops conversion, address validation, delay parsing, and the delay/countdown/status formatting helpers used to render the withdrawal queue. Deploy, deposit, queue withdrawal, cancel withdrawal, and execute withdrawal are all present as typed functions with the same shapes (VaultDetails, VaultWithdrawal, VaultConfig) as the wallet lib.
  • frontend/mobile/lib/escrow.ts — claimable-balance escrow helpers (createEscrow, claimEscrow, reclaimEscrow, buildEscrowClaimants, buildClaimLink). Mirrors the sdk implementation locally instead of reaching outside the mobile package, since mobile already depends on @stellar/stellar-sdk directly.
  • frontend/mobile/app/vault.tsx — a React Native screen covering: create/attach a vault, view balance (total, available, queued, delay), a deposit form, and the full withdrawal lifecycle (queue with a delay, live countdown to unlock, execute once ready, cancel while pending). Styled to match the existing dark-theme mobile screens.

How signing is stubbed, and why

Mobile does not yet have the passkey/session-based signing infrastructure the web wallet uses. Chain submission and reads are behind injectable async parameters — VaultSubmit and VaultFetch in lib/vault.ts — the same style as executeBulkPayout(rows, submitBatch) in the existing lib/bulkPayout.ts. The screen supplies stub implementations today so the full deposit/withdraw/queue/execute/cancel flow is exercisable end to end in the UI, and a real Soroban RPC-backed implementation can be swapped in later without touching the screen or the typed domain logic.

Also references (does not auto-close, different repo): Miracle656#477

closes #2

BigManly4 and others added 30 commits July 27, 2026 17:36
Port the vault sub-account flow from the Next.js wallet (frontend/wallet/app/vault/page.tsx
and frontend/wallet/lib/vault.ts) to Expo/React Native, following the conventions already
established by the bulk-payout and swap mobile ports.

- frontend/mobile/lib/vault.ts: typed vault domain logic ported from the wallet lib
  (amount/delay parsing and formatting, address validation, withdrawal status/countdown
  helpers). Chain submission and reads are behind injectable VaultSubmit/VaultFetch
  parameters, matching the executeBulkPayout(rows, submitBatch) pattern already used in
  lib/bulkPayout.ts, since mobile has no passkey/session signing infrastructure ported yet.

- frontend/mobile/lib/escrow.ts: claimable-balance escrow helpers (createEscrow, claimEscrow,
  reclaimEscrow, buildEscrowClaimants). The wallet's version is a one-line re-export of
  sdk/src/claimableBalance.ts; that re-export does not typecheck from frontend/mobile because
  the mobile TS program only resolves @stellar/stellar-sdk from frontend/mobile/node_modules
  and there is no hoisted root node_modules, so this file mirrors that implementation locally
  using the @stellar/stellar-sdk dependency mobile already has.

- frontend/mobile/app/vault.tsx: React Native screen covering vault creation/attach, balance
  display, deposit, and the full withdrawal lifecycle (queue with delay, live countdown,
  execute once ready, cancel while pending). Styled to match the existing dark-theme mobile
  screens. Signing/submission and vault reads go through stub functions declared at the top
  of the screen (stubSubmitVaultTx, stubFetchVaultDetails) so the flow is exercisable today
  and can be swapped for real Soroban RPC calls once mobile signing lands.

Verified: npm run typecheck is clean, and npx expo export --platform web bundles /vault as a
static route with no errors. Not verified: an interactive simulator/device render.
…racle656#504)

* i18n: add Spanish translation for marketing site (Miracle656#307)

* fix(website): wire /es to the Spanish message bundle

The site uses the App Router, where the `i18n` key in next.config.js is
ignored (it is Pages-Router-only), so `/es` 404'd and the new message
bundles were never read by any component.

- Drop the no-op `i18n` config key
- Add `lib/i18n.ts` — typed message bundles keyed by locale, with `en.json`
  as the source-of-truth shape
- Extract the landing page into `components/LandingPage.tsx`, driven by
  messages instead of hardcoded copy; `WhyVeil` takes the same messages
- Add `app/es/page.tsx` (and per-locale metadata + hreflang alternates);
  `/` stays English
- Add an EN/ES switcher to the navbar
- Split `howItWorks.title` into title1/title2 so the gold highlight keeps
  working, and add accessible names for the comparison table
- Correct Spanish copy: "Cómo Nos Compararmos" -> "Cómo Nos Comparamos",
  plus several awkward renderings of biometric/seedless/finality

---------

Co-authored-by: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com>
Co-authored-by: Miracle656 <Miracle656@users.noreply.github.com>
…racle656#414)

Bumps [@stryker-mutator/typescript-checker](https://github.com/stryker-mutator/stryker-js/tree/HEAD/packages/typescript-checker) from 8.7.1 to 9.6.1.
- [Release notes](https://github.com/stryker-mutator/stryker-js/releases)
- [Changelog](https://github.com/stryker-mutator/stryker-js/blob/master/packages/typescript-checker/CHANGELOG.md)
- [Commits](https://github.com/stryker-mutator/stryker-js/commits/v9.6.1/packages/typescript-checker)

---
updated-dependencies:
- dependency-name: "@stryker-mutator/typescript-checker"
  dependency-version: 9.6.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#418)

Bumps [@stellar/stellar-sdk](https://github.com/stellar/js-stellar-sdk) from 15.1.0 to 16.1.0.
- [Release notes](https://github.com/stellar/js-stellar-sdk/releases)
- [Changelog](https://github.com/stellar/js-stellar-sdk/blob/main/CHANGELOG.md)
- [Commits](stellar/js-stellar-sdk@v15.1.0...v16.1.0)

---
updated-dependencies:
- dependency-name: "@stellar/stellar-sdk"
  dependency-version: 16.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…iracle656#422)

Bumps [next](https://github.com/vercel/next.js) from 14.2.35 to 16.2.12.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](vercel/next.js@v14.2.35...v16.2.12)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.9
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 29.7.0 to 30.4.1.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.4.1/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-version: 30.4.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The suite has been red on main since at least 2026-07-23, tainting every
sdk/ and frontend/wallet PR. Six root causes:

- No NEXT_PUBLIC_FACTORY_CONTRACT_ID in the workflow, so
  computeWalletAddress('') threw "invalid version byte. expected 16, got
  undefined" and every wallet-creation test failed. Uses the same public
  testnet address ci.yml already sets.

- The Soroban stub matched on a URL glob that never fired: the RPC URL has
  no path, so the browser normalises it to a trailing slash. Requests
  escaped to the real testnet and failed with txNoAccount. Matching is now
  by hostname.

- Every hardcoded XDR blob in the stubs was undecodable ("attempt to read
  outside the boundary of the buffer"). They are now built with the XDR
  types, so simulate/send/getTransaction actually stub out.

- The Horizon account stub omitted data_attr, which checkMemoRequired
  dereferences on every payment; and classic G... payments post to Horizon
  /transactions, which nothing stubbed at all.

- onboarding.spec.ts had a local addVirtualAuthenticator that attached the
  CDP session to a throwaway page, so the page under test had no
  authenticator and hung on "Waiting for biometric...". It now uses the
  shared _authenticator helper.

- Tests asserted UI that never existed: a one-step send form (it is
  Review -> Confirm & sign) and a /dashboard redirect from the recover
  button (it opens /recover).

Also: the send form's labels were not associated with their inputs, so
getByLabel found nothing — wired up with htmlFor/id, which the a11y audit
wants anyway. E2E now runs against a production build; next dev compiled
routes on first request and regularly blew the test timeout.

17/17 passing on two consecutive runs, down from 9.6m to ~3m.
The 'exposes no plaintext to the server' case searched the serialized
envelope for 'USD'. That string is base64, drawn from a 64-symbol
alphabet, so a 3-letter token appears in a ~700-character encoding by
chance often enough to fail CI at random -- as it did on PR Miracle656#537, whose
diff touches no SDK code.

Search the decoded ciphertext bytes instead. Same guarantee, and a
chance collision now needs three specific bytes out of 256 rather than
out of 64.
Adds a /settings/backup screen that seals the wallet's non-secret state
into an AES-256-GCM envelope under a user passphrase and hands the file
to the system share sheet.

The SDK's backup module builds its envelope on crypto.subtle, which
Hermes does not implement, so lib/backup.ts ports the primitives to
audited pure-JS crypto (@noble) while keeping the wire format identical:
version 1, AES-256-GCM over a PBKDF2-SHA256 key, base64 fields. A test
decrypts our output with Web Crypto to hold that guarantee, so a file
exported on mobile restores in the web wallet and vice versa.

assertNoSecretMaterial is carried over unchanged and runs before every
encryption, so private key material cannot reach a backup file.

Also adds the Jest harness the mobile app was missing (jest-expo, with
@noble opted into the transform allow-list since it ships ESM only).
* fix(ci): re-enable invisible wallet tests

* fix(ci): align arbitrary dev dependency

Pin arbitrary to the 1.3.x line required by soroban-sdk 22 so the contracts workspace resolves again.

Assisted-by: GPT-5 Codex via Codex

---------

Co-authored-by: kpassito <260419977+kpassito@users.noreply.github.com>
Co-authored-by: Ebube <iupacnumen2020@gmail.com>
The mobile shell rendered everything in the system font. Load the four
brand typefaces at startup with useFonts and hold render behind the splash
screen so the UI never flashes a system font.

- theme/typography.ts: font assets + role→family map + role text styles,
  mirroring the web wallet (Lora 600 italic headings, Anton accent caps at
  0.08em, Inter body, Inconsolata addresses/hashes).
- app/_layout.tsx: preventAutoHideAsync + useFonts(fontAssets); render held
  until fonts resolve, then hideAsync.
- app/fonts.tsx: a /fonts specimen screen rendering all four roles.

Closes Miracle656#432

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
Ports the web wallet's useTheme hook and ThemeToggle button to React
Native, persisting the choice under the same veil_theme storage key.

The web version applies its theme by setting data-theme on the document
root and letting CSS variables restyle everything. React Native has
neither a document nor cascading variables, so the palette becomes data:
lib/theme.ts defines a ThemeColors record per theme and screens build
their StyleSheets from it. The dark values are the ones the app already
shipped with, so dark mode looks exactly as it did and only light mode
is new.

The active theme lives in module state read through
useSyncExternalStore rather than a context provider, which keeps
useTheme() callable anywhere with nothing to wrap the tree in — the same
ergonomics as the web hook — while still driving every subscriber from
one source of truth.

Restyles the existing screens (home, swap, bulk payout) and the root
layout, so the toggle changes the whole app rather than one component.
The Stack's contentStyle carries the background so a route that is still
loading never flashes the opposite theme.

A tap applies immediately and persists in the background: a failed write
costs the preference on next launch rather than the interaction now.

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* feat(mobile): load brand fonts at startup via expo-font

The mobile shell rendered everything in the system font. Load the four
brand typefaces at startup with useFonts and hold render behind the splash
screen so the UI never flashes a system font.

- theme/typography.ts: font assets + role→family map + role text styles,
  mirroring the web wallet (Lora 600 italic headings, Anton accent caps at
  0.08em, Inter body, Inconsolata addresses/hashes).
- app/_layout.tsx: preventAutoHideAsync + useFonts(fontAssets); render held
  until fonts resolve, then hideAsync.
- app/fonts.tsx: a /fonts specimen screen rendering all four roles.

Closes Miracle656#432

* feat(mobile): add shared UI primitives

Build the reusable components every screen composes from, as native ports
of the web wallet's core CSS classes so feature screens don't re-implement
layout chrome.

- theme/colors.ts: native palette mirroring globals.css :root vars.
- components/ui/Screen.tsx (.wallet-shell shell + safe area), Card.tsx
  (.card/.card-md), Button.tsx (.btn-gold/.btn-ghost gold variants),
  AddressChip.tsx (.address-chip, Inconsolata + tap-to-copy via
  expo-clipboard), Highlight.tsx (.hl gold underlay).
- app/_layout.tsx: wrap in SafeAreaProvider (required by Screen).
- app/ui.tsx: /ui gallery rendering every primitive for side-by-side check.

Closes Miracle656#433

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* feat(mobile): load brand fonts at startup via expo-font

The mobile shell rendered everything in the system font. Load the four
brand typefaces at startup with useFonts and hold render behind the splash
screen so the UI never flashes a system font.

- theme/typography.ts: font assets + role→family map + role text styles,
  mirroring the web wallet (Lora 600 italic headings, Anton accent caps at
  0.08em, Inter body, Inconsolata addresses/hashes).
- app/_layout.tsx: preventAutoHideAsync + useFonts(fontAssets); render held
  until fonts resolve, then hideAsync.
- app/fonts.tsx: a /fonts specimen screen rendering all four roles.

Closes Miracle656#432

* feat(mobile): add shared UI primitives

Build the reusable components every screen composes from, as native ports
of the web wallet's core CSS classes so feature screens don't re-implement
layout chrome.

- theme/colors.ts: native palette mirroring globals.css :root vars.
- components/ui/Screen.tsx (.wallet-shell shell + safe area), Card.tsx
  (.card/.card-md), Button.tsx (.btn-gold/.btn-ghost gold variants),
  AddressChip.tsx (.address-chip, Inconsolata + tap-to-copy via
  expo-clipboard), Highlight.tsx (.hl gold underlay).
- app/_layout.tsx: wrap in SafeAreaProvider (required by Screen).
- app/ui.tsx: /ui gallery rendering every primitive for side-by-side check.

Closes Miracle656#433

* feat(mobile): build the swap UI surface

Port the layout of the web wallet's swap page to mobile: in/out token
selectors (tap to open a token sheet), the amount input, a direction-flip
control, and an empty-quote state. Quoting (backlog Miracle656#45) and execution
(backlog Miracle656#46) fill in the receive amount and quote area later, so those
render an empty state for now. Composes the shared UI primitives (Screen,
Card, Button) and the typography/colour tokens.

Closes Miracle656#472

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* feat(mobile): load brand fonts at startup via expo-font

The mobile shell rendered everything in the system font. Load the four
brand typefaces at startup with useFonts and hold render behind the splash
screen so the UI never flashes a system font.

- theme/typography.ts: font assets + role→family map + role text styles,
  mirroring the web wallet (Lora 600 italic headings, Anton accent caps at
  0.08em, Inter body, Inconsolata addresses/hashes).
- app/_layout.tsx: preventAutoHideAsync + useFonts(fontAssets); render held
  until fonts resolve, then hideAsync.
- app/fonts.tsx: a /fonts specimen screen rendering all four roles.

Closes Miracle656#432

* feat(mobile): add shared UI primitives

Build the reusable components every screen composes from, as native ports
of the web wallet's core CSS classes so feature screens don't re-implement
layout chrome.

- theme/colors.ts: native palette mirroring globals.css :root vars.
- components/ui/Screen.tsx (.wallet-shell shell + safe area), Card.tsx
  (.card/.card-md), Button.tsx (.btn-gold/.btn-ghost gold variants),
  AddressChip.tsx (.address-chip, Inconsolata + tap-to-copy via
  expo-clipboard), Highlight.tsx (.hl gold underlay).
- app/_layout.tsx: wrap in SafeAreaProvider (required by Screen).
- app/ui.tsx: /ui gallery rendering every primitive for side-by-side check.

Closes Miracle656#433

* feat(mobile): add the settings shell

Establish the settings screen structure, ported from the web wallet's
settings page: configuration grouped into Security / Preferences / General
sections of navigable rows, plus a wallet-address card. Selecting a row
opens that section's detail (in-page navigation mirroring the web page's
section state); later settings issues fill each with real controls.
Composes the shared UI primitives (Screen, Card, AddressChip).

Closes Miracle656#488

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…6#525)

* feat(mobile): encrypted wallet backup export

Adds a /settings/backup screen that seals the wallet's non-secret state
into an AES-256-GCM envelope under a user passphrase and hands the file
to the system share sheet.

The SDK's backup module builds its envelope on crypto.subtle, which
Hermes does not implement, so lib/backup.ts ports the primitives to
audited pure-JS crypto (@noble) while keeping the wire format identical:
version 1, AES-256-GCM over a PBKDF2-SHA256 key, base64 fields. A test
decrypts our output with Web Crypto to hold that guarantee, so a file
exported on mobile restores in the web wallet and vice versa.

assertNoSecretMaterial is carried over unchanged and runs before every
encryption, so private key material cannot reach a backup file.

Also adds the Jest harness the mobile app was missing (jest-expo, with
@noble opted into the transform allow-list since it ships ESM only).

* feat(mobile): restore wallet from an encrypted backup file

Completes the backup story: the /settings/backup screen gains a Restore
panel that picks a backup file, decrypts it, and writes the wallet state
back to device storage.

Ports decryptBackup, deserializeBackup, restoreBackup, and bindNewSigner
from sdk/src/backup.ts to the React Native crypto path, keeping the
SDK's error contract intact. Every failure that could hand the caller a
half-restored wallet raises BackupTamperError rather than returning
partial data: a failed GCM tag, plaintext that is not JSON, and
plaintext that is JSON but missing the fields a wallet needs.

Nothing is written to storage until decryption succeeds, so a wrong
passphrase or a damaged file leaves the device untouched. The screen
tells the user exactly that instead of inviting a retry.

Signer enrolment is injected through restoreFromFile's enrollSigner
option rather than assumed. The app has no passkey registration flow
yet, and the web wallet takes the same shape when WebAuthn is
unavailable.

Adds 28 tests covering the round trip, each individually authenticated
envelope field, malformed envelopes, and bindNewSigner.

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
Add an AsyncStorage-backed useContacts hook and a contacts screen
that renders the saved address book. This is the base persistence
layer that add/edit and the contact picker build on top of.

Co-authored-by: AdaBliss <adabliss@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* feat(mobile): add persisted contacts list

Add an AsyncStorage-backed useContacts hook and a contacts screen
that renders the saved address book. This is the base persistence
layer that add/edit and the contact picker build on top of.

* feat(mobile): add create, edit, and delete for contacts

Extend the contacts screen with an add/edit form and a delete
action. Address validation and persistence are handled by
useContacts, so changes survive an app relaunch.

---------

Co-authored-by: AdaBliss <adabliss@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* feat(mobile): add persisted contacts list

Add an AsyncStorage-backed useContacts hook and a contacts screen
that renders the saved address book. This is the base persistence
layer that add/edit and the contact picker build on top of.

* feat(mobile): add create, edit, and delete for contacts

Extend the contacts screen with an add/edit form and a delete
action. Address validation and persistence are handled by
useContacts, so changes survive an app relaunch.

* feat(mobile): add reusable ContactPicker component

Provide a searchable bottom-sheet picker that resolves a saved
contact to its Stellar address, so send and bulk-payout flows can
offer contact selection without duplicating the list/search UI.

---------

Co-authored-by: AdaBliss <adabliss@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
Port the web wallet's receive page to native: show the wallet address as
copyable text and a scannable QR, with a share action. The QR encodes a
destination-only SEP-7 pay URI (byte-compatible with the web wallet's
buildSep7PayUri) so a payer's wallet opens pre-filled.

- app/(tabs)/receive.tsx: QR (react-native-qrcode-svg), copy (expo-clipboard),
  share via the native share sheet. Self-contained styling (the shared UI
  primitives aren't in main yet), representative address pending wallet wiring.
- deps: react-native-qrcode-svg, react-native-svg, expo-clipboard.

Closes Miracle656#469

Co-authored-by: chizzy0011 <chisomnteh@gmail.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…tbox (Miracle656#542)

Loss of connectivity currently surfaces as silently failing network calls
with no explanation and no way to recover the work in flight.

Add a NetInfo-backed connectivity provider that wraps the app, a dedicated
offline route ported from the wallet's offline page, and a durable outbox so
actions taken while offline are queued instead of dropped.

- lib/outbox.ts: AsyncStorage-persisted FIFO queue with pluggable per-type
  handlers, serial flushing, bounded retries, and re-entrancy protection so a
  concurrent flush cannot double-send.
- lib/connectivity.tsx: ConnectivityProvider + useConnectivity(). Treats an
  undetermined NetInfo state as online so the offline screen only appears on a
  positive "no connection" signal, and flushes the outbox on the offline to
  online transition.
- app/offline.tsx: offline screen with the wallet's copy, a dependency-free
  no-signal glyph, a queued-action summary, and a retry that re-probes NetInfo.
- app/_layout.tsx: pushes /offline when connectivity drops and pops it when it
  returns, preserving the underlying route.

Adds a jest-expo test harness with unit coverage for the outbox.

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…6#543)

Port the browser wallet's WalletConnect v2 integration to React Native so the
mobile wallet can pair with external dApps and sign for web apps from the phone.

- lib/polyfills.ts: the shims WalletConnect and the Stellar SDK need on Hermes.
  react-native-compat has to be evaluated first, so the import order is fixed
  here and this module is imported before anything else in lib/walletConnect.ts.
- lib/walletConnectHelpers.ts: pure URI, session and request parsing kept free
  of native imports so the rules are unit-testable. Pasted or scanned deep links
  that wrap the pairing URI in a query parameter are unwrapped.
- lib/walletConnect.ts: client lifecycle, pairing, session approve/reject/
  disconnect and the passkey signing pipeline. Sessions persist to AsyncStorage,
  hashing goes through expo-crypto, and the passkey assertion comes from an
  injected signer so this module stays independent of any one passkey
  implementation. Incoming requests are held in a subscribable queue rather than
  dispatched as one-shot events, so an approval UI that mounts later still sees
  them.
- lib/walletStore.ts: wallet address and fee-payer secret in the OS keychain via
  expo-secure-store rather than web storage.
- lib/network.ts: network config reading EXPO_PUBLIC_* variables.
- hooks/useWalletConnect.ts, components/ConnectDAppModal.tsx: scan with
  expo-camera or paste a URI, then approve or reject the session proposal.
- app/index.tsx: Connect dApp entry point and the list of connected dApps.

Adds a jest-expo test harness with unit coverage for the parsing helpers.

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…656#545)

Port the web pools page to native: list liquidity pools and manage
positions. Each pool shows APR, TVL and fee; tapping one opens a sheet with
Add / Remove tabs.

- Add: enter the first asset amount, the second is derived proportionally
  from the pool reserves; submit is gated on a positive amount.
- Remove: enter a percentage of your position; the amounts you receive are
  computed from your pool ownership; gated on holding a position.

Self-contained (shared UI primitives aren't in main yet); representative
pool data pending AMM/wallet wiring.

Closes Miracle656#476

Co-authored-by: chizzy0011 <chisomnteh@gmail.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…656#544)

* feat(mobile): WalletConnect pairing and connect dApp modal

Port the browser wallet's WalletConnect v2 integration to React Native so the
mobile wallet can pair with external dApps and sign for web apps from the phone.

- lib/polyfills.ts: the shims WalletConnect and the Stellar SDK need on Hermes.
  react-native-compat has to be evaluated first, so the import order is fixed
  here and this module is imported before anything else in lib/walletConnect.ts.
- lib/walletConnectHelpers.ts: pure URI, session and request parsing kept free
  of native imports so the rules are unit-testable. Pasted or scanned deep links
  that wrap the pairing URI in a query parameter are unwrapped.
- lib/walletConnect.ts: client lifecycle, pairing, session approve/reject/
  disconnect and the passkey signing pipeline. Sessions persist to AsyncStorage,
  hashing goes through expo-crypto, and the passkey assertion comes from an
  injected signer so this module stays independent of any one passkey
  implementation. Incoming requests are held in a subscribable queue rather than
  dispatched as one-shot events, so an approval UI that mounts later still sees
  them.
- lib/walletStore.ts: wallet address and fee-payer secret in the OS keychain via
  expo-secure-store rather than web storage.
- lib/network.ts: network config reading EXPO_PUBLIC_* variables.
- hooks/useWalletConnect.ts, components/ConnectDAppModal.tsx: scan with
  expo-camera or paste a URI, then approve or reject the session proposal.
- app/index.tsx: Connect dApp entry point and the list of connected dApps.

Adds a jest-expo test harness with unit coverage for the parsing helpers.

* feat(mobile): dApp approval modal signing via device passkey

Nothing a connected dApp asks for should be signed without the user seeing it.
Add the approval surface that gates every incoming WalletConnect sign request.

- components/WalletConnectApprovalModal.tsx: mounted once at the root, watches
  the pending-request queue and presents requests one at a time in arrival
  order. Payments show amount and destination, contract calls show the contract
  address and function name, and anything undecodable is labelled an unknown
  contract interaction to review carefully rather than presented as safe.
  Rejecting answers the dApp with USER_REJECTED so it fails cleanly instead of
  hanging.
- lib/passkey.ts: device passkey signer built on react-native-passkeys, the
  React Native equivalent of the browser's navigator.credentials.get(). The
  Soroban authorization-entry hash is the WebAuthn challenge, so the assertion
  covers exactly the payload the wallet contract verifies, with no separate
  decorative biometric prompt. A dismissed sheet is a rejection, not an error.
- lib/webauthn.ts: base64url and hex codecs plus DER to raw 64-byte r||s
  conversion, ported from sdk/src/utils.ts. Keeps the low-S normalisation, which
  matters because Soroban's secp256r1_verify rejects the malleable high-S form
  authenticators sometimes return.

Unit coverage for the encoding and signature conversion, including the
sign-byte, short-component and high-S cases.

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* chore(mobile): add ESLint and Prettier tooling

* fix(mobile): use ESLint flat config so `expo lint` runs

The branch pinned eslint ^9 but shipped a legacy .eslintrc.js, which ESLint 9
no longer reads:

  ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

Replace it with eslint.config.js extending eslint-config-expo/flat, and sync
the lockfile with the new devDependencies. `npx expo lint` now exits 0.

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
* feat(mobile): navigation shell (Miracle656#434)

Lay down the full route tree so every web page has a place to live
before its screen is built. Establishes the tab bar for primary
destinations and stack routes for the rest, letting later issues
drop content into existing routes instead of also having to wire
navigation each time.

- Root app/_layout.tsx wraps the Stack in SafeAreaProvider
- app/index.tsx redirects to /(tabs)/dashboard
- (tabs)/ group: dashboard, send, receive, settings with dark tab bar
- Stack stubs for every web wallet page: swap, multisig, vault, earn,
  pools, buy, withdraw, contacts, recover, lock, agent, token/[id]
- Shared components/ScreenScaffold.tsx + components/TabIcon.tsx
- metro.config.js wires tsconfig path aliases for SDK 57 runtime
- tsconfig.json registers @/components/* mapping

every web route has a corresponding mobile route reachable via
navigation (NavRow entries on dashboard + settings tabs prove it).

* fix(mobile): resolve PR Miracle656#507 typecheck blockers

- frontend/mobile/app/swap.tsx: take upstream/main's real Soroswap quoting screen (replaces local ComingSoonBadge stub; the conflict from main is now resolved).

- frontend/mobile/tsconfig.json: drop the deprecated baseUrl line (TS5101 was aborting the compile before anything else ran).

- frontend/mobile/components/TabIcon.tsx: widen color to ColorValue (fixes TS2322 at app/(tabs)/_layout.tsx:38, 45, 52, 59).

Verified: npm run typecheck exits 0 with these exact changes.

* chore(mobile): drop unrelated package-lock libc-field churn

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
recover.tsx tripped react/no-unescaped-entities on the apostrophe in
"What you'll need"; buy.tsx imported colors and Text without using either.

`npx expo lint` is now clean, which it needs to be for the config added in Miracle656#506
to mean anything.
@walletconnect/keyvaluestorage peers on @react-native-async-storage/async-storage
1.x, but only the root 3.1.1 was locked, so npm 10 (what CI installs) rejected
the tree:

  npm error Missing: @react-native-async-storage/async-storage@1.24.0 from lock file

npm 11 resolves this without complaint, which is why it went unnoticed locally.
Regenerated with npm 10; `npm ci` now succeeds.
…e656#508)

* feat(mobile): register veil:// scheme and universal/app links

Register the custom scheme and platform universal/app links so payment
requests and other external entry points open the right screen from both
a cold start and a warm resume.

- app.config.ts replaces app.json so the deep-linking surface is derived
  from the same constants the runtime resolver uses. Declares the veil://
  and web+stellar: schemes, iOS associated domains, an autoVerify Android
  intent filter, and the bundle identifier / package name both need.
- lib/deepLinks.ts resolves an inbound URL to an in-app route. Inbound
  links are untrusted, so it matches a fixed allowlist of routes and
  copies only the query parameters each route declares; foreign hosts,
  unknown schemes, and over-long URLs fall back to the home route rather
  than navigating. Query parsing is hand-rolled because React Native's
  URLSearchParams accessors throw.
- app/+native-intent.ts wires it into expo-router, which calls it for
  both launch paths, and can never throw during launch.
- Placeholder /pay, /send, /receive, and /create-wallet routes give the
  links somewhere to land; /pay forwards a request into the send form and
  preserves the raw SEP-7 URI for the handler in backlog Miracle656#38.
- apple-app-site-association and assetlinks.json are served by the wallet
  web app, with next.config.js pinning their Content-Type. Both carry
  placeholders documented in the mobile README.

* ci(mobile): use npm ci without the npm install fallback

`npm ci || npm install` would silently paper over lockfile drift, which is the
one thing the job is there to catch. Verified `npm ci` resolves cleanly against
frontend/mobile/package-lock.json.

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
… config (Miracle656#514)

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
attyolu and others added 14 commits July 30, 2026 17:49
* feat(wallet): add CAP-46 NFT gallery route and metadata detection (Miracle656#349)

* feat(mobile): add QuickActions dashboard row component (Miracle656#460)

---------

Co-authored-by: flourishbar <you@example.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…Miracle656#518)

Adds lib/sep7.ts (ported from frontend/wallet/lib/sep7.ts) to parse both
web+stellar:pay?... URIs and this app's own veil://pay?... deep link
scheme, and wires a Linking listener into the root layout that routes a
matched link to /send with the destination/amount/asset/memo pre-filled.
Registers "web+stellar" as an additional URL scheme in app.json so the OS
opens such links in this app.

A minimal send.tsx placeholder renders whatever got pre-filled; the
interactive recipient/amount form and submit flow are separate follow-up
issues.

Co-authored-by: Vincent ibochi <290086463+ibochivincent-lang@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…56#452) (Miracle656#520)

Port the web wallet's landing/hero experience to mobile as an onboarding
flow. First-time users see the branded intro before creating a wallet;
returning users skip straight past it.

- Add (onboarding)/welcome.tsx with logo, tagline, feature highlights,
  and Get Started / Recover actions
- Add (onboarding)/_layout.tsx for the onboarding route group
- Update app/index.tsx to check AsyncStorage for wallet state and route
  first-time users to welcome, returning users to dashboard
- Add @react-native-async-storage/async-storage dependency

Refs: Miracle656#452, backlog item 24

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
Adds a mobile buy screen that lets users bring fiat into the wallet
through a SEP-24 anchor. lib/sep24.ts ports the deposit-side protocol
logic from frontend/wallet/lib/sep24.ts (anchor TOML discovery, SEP-10
challenge validation/signing, interactive deposit request, status
polling) as pure, injectable-signer functions with no browser-only
APIs. app/buy.tsx drives the flow: enter anchor domain/amount/asset,
kick off the interactive deposit, launch the returned URL with
expo-web-browser's openAuthSessionAsync, and poll for the resulting
transaction status once the user returns to the app.

Adds expo-web-browser as a dependency (installed via `expo install`
for SDK-compatible versioning).

Co-authored-by: BigManly4 <294554482+BigManly4@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…weep (Miracle656#523)

Add frontend/mobile/app/withdraw.tsx implementing the fiat off-ramp flow:
asset/amount entry, anchor discovery, SEP-24 interactive withdrawal
request, launching the anchor's interactive URL via expo-web-browser, and
status polling until the anchor reports the transaction complete.

Add frontend/mobile/lib/sep24.ts, ported from frontend/wallet/lib/sep24.ts
with just the pieces the withdraw (and future deposit) flow needs: TOML
discovery, SEP-10 JWT exchange, interactive withdraw/deposit requests, and
transaction status polling. The wallet version signs the SEP-10 challenge
with a browser WebAuthn passkey; the mobile port takes an injectable
signChallenge callback instead, following the same pattern as
executeBulkPayout's submitBatch parameter.

Add frontend/mobile/lib/sweepContractBalance.ts, ported from
frontend/wallet/lib/sweepContractBalance.ts. The RPC-only balance check
(getContractBalance) is ported as-is; building/signing/submitting the
transfer transaction is delegated to an injectable signAndSubmit callback
since the wallet's Soroban-auth signing depends on crypto.subtle and
WebAuthn, neither available on React Native and no mobile signing infra
exists yet.

The withdraw screen wires both stubbed signers as inline no-ops for now,
matching the existing bulk-payout screen's convention of passing a stub
submit function.

Add expo-web-browser as a dependency (via npx expo install to match the
installed Expo SDK).

Co-authored-by: BigManly4 <294554482+BigManly4@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…56#526)

The web wallet picks its network from NEXT_PUBLIC_NETWORK at build time.
That does not carry to mobile: a build is a store submission, and a
tester moving between testnet and mainnet cannot wait on one.

Adds lib/network.ts, which resolves the active network from a persisted
AsyncStorage override, then EXPO_PUBLIC_NETWORK, then testnet. Reads stay
synchronous — hydration runs once on first import and notifies
subscribers when it lands, so nothing has to await before calling
getNetwork().

Switching re-points the RPC URL, factory contract, Horizon URL, and
network passphrase together, and lib/soroswap.ts now builds its client
per call instead of pinning the network at module load, so quotes follow
the switch. Consumers holding chain-derived state subscribe through
subscribeToNetwork or the useNetwork hook and refetch, since a switch
invalidates everything read from the previous chain.

The in-memory network is updated only after the write succeeds, so a
failed write cannot leave the app pointed somewhere it will forget on
next launch. Mainnet ships without a default RPC or factory, and
/settings/network says so rather than letting the failure surface later
inside a request.

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…ent (Miracle656#528)

* feat(mobile): port activity feed store and create ActivityFeed component

- Create frontend/mobile/lib/activityFeed.ts with hydrate/append lifecycle ported from frontend/wallet
- Wire to Wraith GET /transfers/:address endpoint with 15s polling
- Create ActivityFeed component with filter pills, skeleton loading, swap display
- Update mobile dashboard to integrate the feed

Closes Miracle656#461

* fix(activity-feed): robust amount/timestamp parsing, swap detection, env docs

* fix(activity-feed): address PR review — move feed to dashboard tab, use wallet address from store, add fetch timeout

* Merge origin/main into activity-feed-mobile

- Restore QuickActions on the dashboard. This branch predates Miracle656#517 and its
  dashboard rewrite dropped the component and its import, which would have
  removed the quick-actions row.
- fetchTransfers swallowed every failure and returned [], so an unreachable
  indexer, an HTTP error and a genuinely empty wallet all rendered as
  "No transactions yet." It now throws; useInitActivityFeed already catches
  into its error state, which was being returned but never used.
- ActivityFeed takes an error prop and renders a distinct message, and the
  dashboard passes it through.

tsc clean; jest 11 suites / 198 tests; expo lint clean.

* chore(mobile): resync package-lock after the merge

The lockfile auto-merged into a state npm 10 rejected:

  npm error `npm ci` can only install packages when your package.json and
  package-lock.json ... are in sync
  npm error Missing: @jest/transform@29.7.0 from lock file

Regenerated with npm 10 so it matches what CI installs.

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…le656#431, Miracle656#455) (Miracle656#530)

* feat(mobile): add brand theme constants and first-run tutorial

- Add constants/theme.ts with brand colors, surfaces, spacing, radii
- Add constants/typography.ts with font families and role-based styles
- Add components/OnboardingTutorial.tsx with 3-step dismissible coach marks
- Export __e2eSkipTutorial flag for e2e test bypass

Closes Miracle656#431
Closes Miracle656#455

* Merge origin/main into fix/issue-431-455-brand-theme-and-tutorial

Miracle656#431 is already satisfied on main by theme/colors.ts and theme/typography.ts,
so drop the duplicate constants/ copies and keep the tutorial:

- constants/theme.ts duplicated a palette main already has (theme/colors.ts is
  a superset — same gold/nearBlack/offWhite/lilac/teal/warmGrey/navy plus the
  surface, border and gold-tint tokens). constants/typography.ts additionally
  declared web CSS font stacks ('Lora, Georgia, serif'), which React Native
  cannot resolve; theme/typography.ts uses the loaded family names.
- Add theme/spacing.ts for the spacing/radii scale, which main genuinely
  lacked, so the tutorial has somewhere to import layout tokens from.

Make the tutorial meet Miracle656#455's acceptance, which it did not as written:

- Nothing mounted it and nothing persisted dismissal — the component only had
  an onComplete prop with a comment saying the parent would store it. Added
  FirstRunTutorial, a self-gating wrapper that reads/writes veil_seen_tutorial
  in AsyncStorage, and mounted it on the dashboard.
- Added testID="tutorial-overlay" and testID="tutorial-skip-button", which is
  what an external driver like Maestro can actually use; the exported
  __e2eSkipTutorial module flag is only reachable from in-process tests.
- Fixed a rules-of-hooks violation: the __e2eSkipTutorial early return sat
  above four useCallback calls, so hook order changed with the flag. Moved it
  below every hook.

tsc clean; jest 11 suites / 198 tests; expo lint clean.

---------

Co-authored-by: MrG139 <mrg139@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…6#540)

Stop bad payments before they happen: validate the send recipient (on-chain
G/M/C address via StrKey, or a well-formed SEP-2 federated name*domain) and
let users pick a saved contact instead of typing.

- lib/address.ts: isValidDestination (ports the SDK's rule + federated format).
- components/ContactPicker.tsx: bottom-sheet list of saved contacts.
- app/(tabs)/send.tsx: recipient field that shows an error and blocks submit
  on an invalid address; 'Choose contact' fills the recipient.

Closes Miracle656#467

Co-authored-by: chizzy0011 <chisomnteh@gmail.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
Add the reusable transaction surfaces used across the app: a preview card
shown before signing, and a detail sheet opened from a history feed. Porting
them as native bottom sheets keeps transaction presentation consistent.

- components/TxPreviewCard.tsx: presentational confirm-step preview.
- components/TxDetailSheet.tsx: @gorhom BottomSheetModal over a TxRecord
  (type mirrors the web wallet), opened via a ref.
- app/_layout.tsx: wrap in GestureHandlerRootView + BottomSheetModalProvider
  (required by the sheet).
- app/(tabs)/activity.tsx: demo — renders the preview and a feed whose items
  open the detail sheet on tap.
- dep: @gorhom/bottom-sheet.

Closes Miracle656#470

Co-authored-by: chizzy0011 <chisomnteh@gmail.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
)

Creates QrScanner component that replaces the web BarcodeDetector approach
with expo-camera's CameraView + onBarcodeScanned for native mobile QR scanning.
Includes camera permission handling, viewfinder overlay, and manual address
entry fallback.

Closes Miracle656#468

Co-authored-by: iredis <iredis@users.noreply.github.com>
Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
)

Adds /agent, the mobile client for the Claude-powered assistant in
packages/agent. It speaks the same protocol as the web wallet's agent
page and reuses its storage keys, so a profile set up in the browser
carries over rather than the user being onboarded twice.

The transport is split out into lib/agentSocket.ts because a phone's
socket drops constantly -- backgrounding the app is enough. It
reconnects with jittered exponential backoff instead of the web page's
fixed 2s retry, queues messages composed while offline and flushes them
on reconnect, and tracks in-flight requests: the agent server keeps no
outbox, so a reply interrupted by a drop never arrives and the screen
now says so instead of leaving the thinking indicator up forever.

The socket constructor, timers, and backoff jitter are all injectable,
which is what makes the reconnect paths testable without a server.

Agent markdown is rendered as nested Text runs rather than the web
page's dangerouslySetInnerHTML, so model output can never be
interpreted as markup.

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…Miracle656#551)

* feat(mobile): security settings for lock timeout and biometric unlock

Adds /settings/security, where the user tunes the app-lock policy: how
long the app may sit idle before locking (5 / 15 / 30 minutes, or
never) and whether unlocking must present a biometric factor. The
timeout uses the same veil_idle_lock_minutes key as the web wallet, so
the choice carries between clients.

lib/appLock.ts holds the policy and the idle watcher the lock screen
(backlog Miracle656#28) consumes. Changing the timeout takes effect immediately
rather than at the next lock: watchers subscribe to the settings store
and reschedule, including firing straight away when a shortened timeout
leaves the deadline already in the past.

Two things differ from the web wallet's lib/idle-lock.ts. Activity is
reported explicitly through noteActivity(), because React Native has no
global mouse/keyboard event stream to listen to. And backgrounding is
not treated as activity the way visibilitychange is on the web -- JS
timers are frozen or killed while the app is away, so the watcher
records when it left the foreground and locks on return if the idle
period already elapsed. Without that, a phone could sit in a pocket for
an hour and come back unlocked.

The biometric requirement is stored as a preference rather than gated
on a capability check: whether the device has usable biometrics can
change between now and the unlock, so the lock screen resolves that
when it prompts and falls back to the passkey rather than locking the
user out of their funds.

* Merge origin/main into feat/mobile-security-settings

Two gaps that stopped the settings taking effect:

- hydrateLockSettings() was never called, so the app started on the defaults
  every launch and a saved timeout only applied once the user re-picked it.
  Called from app/_layout.tsx alongside hydrateNetwork().
- /settings/security was unreachable. The settings tab rendered its rows as
  Pressables that only set local state, so the "Security & lock" row went
  nowhere. Rows now take an optional href and navigate when present.

tsc clean; jest 15 suites / 266 tests; expo lint clean.

---------

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
…e656#552)

Adds /multisig, which turns the read-only multisig view into a working
coordination tool: an owner raises a transfer, owners approve it, and
the approval that reaches the threshold executes it.

There is no separate Execute action because the contract has no execute
entry point -- contracts/multisig-wallet performs the transfer inside
the sign_transaction invocation that reaches the threshold. The screen
names that approval for what it is ("Approve and execute") and warns
that it sends the funds immediately, rather than offering a button the
contract cannot back.

Three fixes carried over from the web port:

- propose_transaction takes caller as its first argument and calls
  caller.require_auth(). The web version omits it, so every proposal it
  builds is rejected. It is passed here.
- The owner is the source account rather than a separate fee payer, so
  prepareTransaction resolves require_auth against the source account
  and no second signature is needed. This also drops the fallback that
  minted a throwaway keypair from Friendbot.
- Amounts convert through integer string arithmetic instead of
  parseFloat(x) * 10_000_000, which misrounds ordinary values such as
  0.7 at stroop precision.

Approval preconditions -- owner membership, duplicate approvals, and
whether the wallet can actually cover a transfer that is about to
execute -- are checked locally, so a rejection is immediate and
specific instead of arriving as a contract panic after a fee.

Deployment stays on the desktop wizard; the contract address is read
from the veil_multisig_contract key the web wallet already uses.

Co-authored-by: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com>
# Conflicts:
#	frontend/mobile/app/vault.tsx
@BigManly4
BigManly4 force-pushed the mobile/vault-screen-477 branch from d7720b3 to a50486e Compare August 3, 2026 15:37
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.

49. Vault screen