Skip to content

feat(zap): port iOS one-tap-zap + NIP-78 parity + wallet polish#556

Open
dmnyc wants to merge 15 commits into
barrydeen:mainfrom
dmnyc:feat/one-tap-zap
Open

feat(zap): port iOS one-tap-zap + NIP-78 parity + wallet polish#556
dmnyc wants to merge 15 commits into
barrydeen:mainfrom
dmnyc:feat/one-tap-zap

Conversation

@dmnyc
Copy link
Copy Markdown
Contributor

@dmnyc dmnyc commented May 22, 2026

Summary

Ports iOS PR barrydeen/wisp-ios#159 (feat(zap)) to Android — instant
zaps, the redesigned ZapSheet, per-account presets, NIP-78 cross-device
sync schema parity. Plus a round of wallet-tab polish landed during
testing.

  • NIP-78 schema parityAppSettingsPayload now carries
    defaultReaction, defaultReactionEnabled, quickReactions,
    frequency, colorScheme, animateAvatars. Fields with no Android
    UI are round-tripped opaquely so iOS settings survive an Android
    publish. CustomEmojiRepository participates in the same 4s
    debounced publish as the other prefs sources.
  • ZapDialog redesign — hero number is the editable BasicTextField
    (matches iOS) with a thousands-separator visual transform and a
    transparent selection background. Sheet locks to fillMaxSize so it
    opens at full height immediately (no stagger when auto-focus raises
    the keyboard). Per-row swipe-to-delete EditPresetsSheet replaces
    the single-row save dialog.
  • Per-account preset routingZapDialog now requires
    zapPrefsRepo: ZapPreferences. Fixes the bug where in-sheet "+"
    writes went to a global file but NIP-78 publish/restore used the
    per-account file, so iOS presets appeared not to sync. One-shot
    migration pulls any pre-fix global presets into the per-account file.
  • Long-press instant zapActionBar.combinedClickable: tap
    opens composer, long-press fires the configured quick zap with a
    HapticFeedbackType.LongPress confirmation. Plumbed through
    NoteActions.onZapInstant at every callsite (Navigation × 9 +
    FeedScreen, ThreadScreen, NotificationsScreen, ArticleScreen,
    SearchScreen, UserProfileScreen).
  • Wallet parity polish — NWC dashboard's lingering "Wallet" topBar
    hidden post-connect; iOS-red SwapHoriz Card row for default-Spark
    / NWC Switch on Wallet Settings (unified confirmation page red);
    QR-scan dialog inline in the NWC entry field; lud16 pill rendered
    for any mode that carries one; redundant NWC footer under the
    balance removed; adaptive recent-tx count (5 / 4 / 3 / 2 by screen
    height); WalletInfoRow truncation + column-aligned values;
    backup-to-relay now offered for default Spark wallets.
  • WALLET_PARITY.md §11.2 checklist updated for items landed.

Test plan

  • Sign in on a fresh install with an nsec that has presets +
    instant-zap configured on iOS — presets restore, one-tap amount
    and message land on Android.
  • Open ZapDialog: hero number editable, first keystroke replaces
    the seeded amount, no selection rect painted.
  • Sheet opens at full height immediately (no stagger when the
    keyboard rises).
  • Edit Presets sheet: swipe-to-delete from the trailing edge
    reveals the red panel; "+ Add preset" disables while a blank
    row exists; Done persists + triggers NIP-78 publish.
  • Long-press a post's bolt: haptic tick + instant zap fires;
    tap still opens composer; self-zap is dimmed and inert.
  • NWC: tap the trailing QR icon on the connection-string field,
    scan a `nostr+walletconnect://` URI — field populates.
  • NWC dashboard: lud16 pill renders below the balance; no
    "Nostr Wallet Connect" footer; "Wallet" topBar gone post-connect.
  • Wallet Settings → Switch (default Spark) lands back on the
    wallet-mode picker; same for NWC Disconnect.
  • Wallet Settings → Wallet Connection details: long relay URL
    truncates with ellipsis; labels align across rows.
  • Backup-to-Nostr-Relays visible for default Spark wallets.

dmnyc added 15 commits May 21, 2026 00:04
Ports iOS commit #1 from feat/one-tap-zap. Publishes a single
NIP-44-encrypted kind 30078 event addressed by the
`wisp-app-settings:v1` d-tag whenever any synced setting changes
(debounced 4s — matches iOS). On launch, fetches and applies the
remote backup non-destructively.

Synced fields: zapIconStyle, largeText, themeName, accentColorARGB,
autoLoadMedia, videoAutoplay, mediaLayoutStyle, clientTagEnabled,
postUndoTimerEnabled, postUndoTimerSeconds, postUndoTimerForReplies,
fiatModeEnabled, fiatCurrency, zapPresetsCSV. (Quick-zap fields will
plug in via the same payload in commit barrydeen#2 — schema is already
forward-compatible.)

Wiring:
  • Nip78.kt — adds AppSettingsPayload (matches iOS keys), plus
    create/decrypt/filter helpers.
  • AppSettingsRepository — owns the signer/relay-pool refs,
    debouncer, build/publish/restore. Restore temporarily detaches
    its own sync callbacks so applying a remote backup doesn't kick
    off an immediate re-publish loop.
  • InterfacePreferences / FiatPreferences / ZapPreferences — gain
    an `onSyncedFieldChanged` hook fired only on synced-field
    setters. Non-synced setters (language, newNotesButtonHidden,
    liveStreamsHidden, autoTranslate) deliberately stay quiet.
  • ZapPreferences — gains CSV round-trip helpers (toCSV / applyCSV).
    CSV format matches the iOS spec: `<sats>` or `<sats>:<message>`,
    comma-separated. `applyCSV` suppresses its own sync callback to
    avoid a restore-publish loop.
  • FeedViewModel — constructs AppSettingsRepository, plumbs it into
    setSigner / clearSigner.
  • StartupCoordinator — calls `restoreSettingsBackup()` after
    relay-list fetch on both cold and warm starts (best-effort,
    fire-and-forget).
  • InterfaceScreen — new "Cross-device sync" section with a toggle
    bound to `isSyncSettingsToRelays()` (default on). Flipping on
    immediately fires a sync via the new onSyncRequested callback
    plumbed through Navigation.kt.

Also drops the "GIF picker keyboard race" item from
ANDROID_PORT_one_tap_zap.md — that race doesn't reproduce in Compose.
Ports iOS commit barrydeen#2 from feat/one-tap-zap. Adds the four AppSettings
properties iOS persists, plumbed through the NIP-78 payload introduced
in the previous commit:

  • quickZapEnabled  (Bool, default false)  — master toggle
  • quickZapAmountSats  (Long, default 100, hard-clamped to 1..10000)
  • quickZapAmountFiat  (Double, default 0.10)
  • quickZapMessage  (String, default "") — optional default note

Hard cap at 10,000 sats so an instant zap never bypasses the soft
10K-sats confirmation dialog in the ZapSheet (task barrydeen#4). Fiat clamp
happens at fire time against the cached exchange rate, not in the
setter — the live rate fluctuates and we don't want a stale rate to
silently re-clamp the saved amount.

Interface settings — new section titled "Zaps" (or "Payments" in
fiat mode, label flips the same way iOS does). Toggle row + gated
amount + message fields. Amount field shows fiat or sats based on
fiat mode; sats variant has a "Max 10000" supporting caption.

Wiring:
  • InterfacePreferences — 4 setters fire the same sync callback as
    the other synced fields, so any edit lands on relays within the
    4s debounce window.
  • AppSettingsRepository.buildPayload — emits all 4 quick-zap
    fields. AppSettingsRepository.applyPayload — restores them
    non-destructively from a remote backup.
  • InterfaceScreen — new "Zaps" / "Payments" section between
    Fiat Mode and the Zap Icon toggle.

No UI wiring yet for the long-press behavior or the in-sheet toggle
— those land in tasks barrydeen#4 (ZapSheet redesign) and barrydeen#5 (post-card
gesture).
Ports iOS commit barrydeen#3 from feat/one-tap-zap. Two-screen reshape that
makes the obvious next step visually obvious.

Mode picker (Screen 1):
  • Spark row is now a full-bleed orange card with white text + icon,
    layered shadow glow (two stacked shadows in `WispThemeColors.zapColor`
    — wide outer + tighter inner — for the iOS bloom).
  • NWC keeps its dark surface-variant peer treatment. Not buried
    under "More options" per spec — it's a genuine alternative.

Spark sub-screen (Screen 2):
  • "Use my default wallet" gets the same primary treatment (orange
    fill + glow stack) so the nsec-derived recovery path reads as
    the obvious first choice.
  • Create new / Restore from seed phrase / Restore from relays
    collapse under a "More options" disclosure with a chevron that
    rotates 0→180° via animateFloatAsState. Expand defaults to
    open if there's no default-wallet option visible (so users who
    can't derive still see all paths immediately).
  • AnimatedVisibility wraps the inner Column for the expand/collapse
    transition (expandVertically + fadeIn / shrinkVertically +
    fadeOut), matching the iOS "fade rows in/out" cue.

Extracted as `WalletPrimaryRow`, shared by both screens.

Vertical-centering of the pick stack (iOS spec calls out a
GeometryReader-backed ScrollView) isn't implemented here — the
existing Spark sub-screen already uses weighted spacers within a
non-scrolling Column, which centers naturally when content fits
the viewport; smaller phones fall back to scroll via the parent's
verticalScroll.
…tion, friendly errors

Ports the behavioral half of iOS commit barrydeen#4 from feat/one-tap-zap.
The full layout rewrite (hero amount, recipient row, FlowLayout
preset strip, privacy dropdown, hidden TextField with 450ms focus
deferral, register-style cents on every keystroke, EditPresetsSheet,
scroll-dismisses-keyboard) is deferred — the existing ZapDialog
layout still works and the new behaviors are the user-facing
material change.

What landed:

  • **Instant-amount seed on open.** First mount with no
    initialSatsHint pre-fills `customAmount` from
    `interfacePrefs.getQuickZapAmountSats()` (or the fiat
    equivalent in cents), and `message` from
    `interfacePrefs.getQuickZapMessage()`. Treats the configured
    instant-zap amount as the "preferred opening amount" even when
    quick zaps are disabled — matches iOS.

  • **In-sheet Instant-zaps toggle.** New row above the action
    buttons, bound directly to `InterfacePreferences.isQuickZapEnabled`
    so flipping it from the sheet propagates to the post-card
    long-press behavior (and the NIP-78 backup) without navigating
    to settings. Label flips with fiat mode.

  • **1,000,000-sat hard cap.** Zap button disables and a red
    "Max 1,000,000 sats per zap" caption surfaces above the action
    row when the effective amount crosses the cap. Hard cap, not
    a confirmation.

  • **10,000-sat soft confirmation.** Below 10K the Zap button
    fires immediately. At/above 10K it routes through an
    AlertDialog ("Zap N sats? — This is a large amount, double-check
    before sending") with Send / Cancel. Below the cap so users
    can recover from a stray preset tap.

  • **`friendlyZapErrorMessage()` utility.** Mirrors iOS's
    `ZapAnimationStore.friendlyMessage(for:)` substring-match table
    plus the Swift-enum description fallback (extracts `("…")`
    when present). Internal so post-card error pills + future
    layouts can both call it.
…abled

Ports iOS commit barrydeen#5 from feat/one-tap-zap. Splits the zap-glyph
gesture into two paths and renders the button as disabled for the
user's own posts.

ActionBar:
  • New optional `onZapLongPress: (() -> Unit)?` parameter — null
    means "no long-press behavior, tap-only" (existing call sites
    keep working without changes).
  • Zap glyph switches from IconButton to a Box with
    `combinedClickable`, supporting onClick (open composer) AND
    onLongClick (fire instant zap). When `onZapLongPress` is null,
    the long-press handler is omitted entirely so the glyph behaves
    exactly as before.
  • `longPressFired` flag pinned in remember{} — Compose, like
    SwiftUI, fires both onClick AND onLongClick on release of a
    long-press, so the tap handler short-circuits the second fire
    when the flag is set.
  • Disabled tint moved from 0.4f to 0.35f opacity to match the
    iOS self-zap rendering.

PostCard:
  • Plumbs `onZapLongPress` through to ActionBar.
  • Self-zap disabled: `zapEnabled = zapEnabled && !isOwnEvent`
    so the user's own posts render the glyph at low opacity AND
    both tap + long-press are short-circuited (the long-press
    handler returns null when zapEnabled is false in ActionBar).

What's NOT in this commit:
  • The actual "instant zap fires the configured amount" wiring at
    call sites (RichContent's WispActions etc.). The gesture
    infrastructure is in place; plugging it in requires reaching
    into ZapSender / WalletViewModel and is a separate, larger
    commit that touches every call site that constructs WispActions.
Ports iOS commit barrydeen#6 from feat/one-tap-zap. Replaces the multi-layer
Canvas bolt animation that was smearing the silhouette at scale
peaks. New approach: always-white silhouette + three stacked
zap-color shadows underneath, driven by a single sin-eased
oscillator.

Math (period 0.9s):
  sine    ∈ [-1, 1]
  phase   ∈ [0, 1]      = (sine + 1) / 2
  iconScale       = 1.0 + 0.10 * sine   (0.90 → 1.10)
  verticalOffset  = -0.5 * sine          (±0.5dp centered on baseline)

Shadow layers (Canvas strokes — drawn outer → inner so the white
core sits on top):
  outer  — radius 8 + 6*phase dp,  α = 0.30 + 0.50*phase
  medium — radius 4 + 3*phase dp,  α = 0.55 + 0.45*phase
  inner  — radius 1.5dp constant,  α = 0.95
  core   — solid white silhouette, untinted

Vertical motion held to ±0.5dp so the icon doesn't lift off the
action-bar baseline and misalign with neighbouring glyphs. The
white IS the luminous core; the warm halos do the heat work.

LinearEasing on the sineAngle (not FastOutSlowInEasing) — the
sine function itself supplies the easing curve. Wrapping with
another easing would double-stack and visibly stutter.
Ports iOS commit barrydeen#7 from feat/one-tap-zap. Empty placeholder so
future throwaway experiments have somewhere to land instead of
sprouting one-off entry points across production code.

  • New `Routes.DEVELOPER_TOOLS` route.
  • New `DeveloperToolsScreen` composable — top-app-bar with a
    back arrow and a single muted "No tools yet" caption. Drop
    test buttons / log dumps / force-state toggles directly into
    the Box.
  • `InterfaceScreen` gains an optional `onOpenDeveloperTools`
    callback. When `BuildConfig.DEBUG == true` AND the callback is
    non-null, a "Developer" section + "Developer tools" row renders
    at the bottom of the settings list (above the Wisp version
    line). Release builds skip the section entirely — the wrapping
    `if (BuildConfig.DEBUG)` gate compiles out in R8.

Nothing inside the screen yet. That's the point.
…ad of 1

The dashboard footer was capped at one transaction. iOS shows up
to five rows inline before the "View all" affordance — matching
that here. Tapping any row (or "View all") still expands to the
full transactions screen.
…dismiss

Previous pass only added behaviors (instant-amount seed, in-sheet
toggle, caps, confirmation). The layout still used a centered
Dialog that filled the screen and offered no dismiss gesture —
"too tall and impossible to dismiss". This commit rebuilds the
composer from scratch to match the iOS reference screenshot.

Container: switch from `Dialog` to `ModalBottomSheet`. Gives you:
  • Drag handle at the top (Material3 supplies it).
  • Swipe-down dismiss + scrim-tap dismiss.
  • Partial-height presentation so the sheet doesn't take over
    the whole viewport.

Layout (top to bottom, mirrors iOS spec §2.6 of the port doc):
  1. **Toolbar** — "Close" pill on the left, orange-tinted
     "Presets" pill on the right (opens the Save-preset dialog).
  2. **Recipient row** (when `recipientPubkey` + `profileLookup`
     are provided) — 32dp avatar, display name + lud16 stacked,
     trailing copy-icon button that pushes the lud16 to the
     clipboard. Hidden gracefully when no profile data is wired.
  3. **Hero amount** — 56sp orange rounded-bold number with a
     muted-orange unit caption ("sats" or fiat code) underneath.
  4. **Preset strip** — wrapping FlowRow of pills. Last chip is
     `Custom` with an inline + badge that saves the current
     amount as a new preset (disabled at 8-preset max or when
     the amount already exists).
  5. **Custom amount field** — inline OutlinedTextField, only
     visible when the Custom chip is selected. Digit-only.
  6. **Message field** — single-line OutlinedTextField with
     "Message (optional)" placeholder. Preset taps auto-fill
     their default message only when the field is blank, so a
     mid-type tap doesn't clobber what the user wrote.
  7. **Privacy dropdown** — single-row pill with eye / eye-slash
     / lock icons and helper subtext. Material3 DropdownMenu
     opens on tap. Hidden when `forcePrivate` is on.
  8. **Instant zaps toggle** — bound to the existing
     `interfacePrefs.isQuickZapEnabled` setting (and therefore
     to the NIP-78 sync). Flipping it here propagates without
     re-opening Interface settings.
  9. **Zap button** — full-width, accent fill, white bolt + sats
     copy. Disabled when amount is 0 or over the 1M hard cap. At
     >10K it routes through the existing soft-confirmation alert.

Stripped:
  • LightningBackground (decorative animated dots).
  • AnimatedBoltHeader (centered pulsing bolt).
  • drawMiniBolt + bespoke ZapPresetChip / ZapChipButton scaffolding.
None of these matched the iOS reference; the new layout is simpler
and reads cleaner at a glance.

Signature kept compatible — added two optional params
(`recipientPubkey`, `profileLookup`); existing callers keep
working, the recipient row just hides. Wired the FeedScreen and
thread Navigation.kt call sites to pass profile data; remaining
call sites (groups, DM, profile, hashtag, set-feed, article,
notifications) still compile but won't show the recipient row
until they're updated.
…ry call site

Two follow-ups to the ZapSheet rewrite:

1. **Zap button no longer hides behind the keyboard.** The previous
   layout put the Zap button at the end of a single Column inside
   the bottom sheet, so when the amount field focused, the
   keyboard pushed the whole content up and the button went
   off-screen with no scroll to reach it.

   Restructured the sheet body into two rows: a scrollable upper
   region (toolbar, recipient, hero, presets, message, privacy,
   instant-zaps toggle) holding `weight(1f, fill = false)` and a
   pinned lower region with the cap warning + Zap button. The
   outer Column gets `imePadding()` so the whole stack floats
   above the IME — button stays visible, upper region scrolls if
   the keyboard cuts into it.

2. **Recipient row now renders on every call site.** Only 3 of
   ~12 ZapDialog call sites were passing `recipientPubkey` +
   `profileLookup` in the rewrite commit — the row hid silently
   on the other 9. Wired the rest:
     • Navigation.kt — search, hashtag feed, set feed, article,
       live stream (uses streamer override pubkey when set),
       notifications (post + DM target).
     • FeedScreen — zap-poll target.
     • UserProfileScreen — post zap (eventRepo lookup) AND
       profile-direct zap (embedded profile shortcut).
     • DmConversationScreen — uses the peerProfile already in
       scope.
Two bugs preventing the NIP-78 app-settings backup from restoring
from a relay-side payload published by iOS:

1. **Collector dropped every relay reply.** restoreSettingsBackup
   was launching its `relayEvents` + `eoseSignals` collectors on
   the repo's own `Dispatchers.Default` scope, then calling
   `yield()` on the suspending function before firing the REQ. The
   yield only dispatches the calling coroutine — the collectors on
   the separate scope had no guarantee of being subscribed yet, so
   the first batch of relay replies (which arrive ~3-4s later, way
   after the collector "should" be live) reached a
   `MutableSharedFlow(extraBufferCapacity = 4096, replay = 0)` with
   no subscribers and got dropped on the floor. Result: events = 0,
   "no matching d-tag event found", even though SUBLOG showed
   1-2 events arriving per relay.

   Wrap the body in `coroutineScope { ... }` so the collector
   launches are children of the calling coroutine. yield now
   actually dispatches them before sendToAll fires the REQ. iOS
   payloads start arriving correctly.

2. **`accentColorARGB` overflowed Kotlin Int.** iOS encodes the
   ARGB color as Swift `Int` (64-bit), so `0xFFFF9800`
   (4_294_940_672 unsigned) ships as a JSON number that
   kotlinx.serialization can't deserialize into the Kotlin `Int?`
   we declared — `Int.MAX_VALUE` is 2_147_483_647. Decryption
   succeeded, JSON parsing died.

   Declare the field as `Long?`; in `applyPayload`, narrow with
   `it.toInt()` (the lower 32 bits round-trip correctly even when
   Int reads the value as negative). When building the payload to
   publish, widen with `getAccentColor().toLong() and 0xFFFFFFFFL`
   so iOS reads it as a non-negative number on the round-trip.

Diagnostic logging on the restore path stays in — useful next
time something goes sideways. Bumped the silent swallow in
`Nip78.decryptAppSettings` to a Log.w so future
serialization-class mismatches surface immediately.
Match iOS PR 159's AppSettingsPayload byte-for-byte so a kind-30078
backup round-trips across platforms without losing iOS-only fields.

New AppSettingsPayload fields: defaultReaction, defaultReactionEnabled,
quickReactions, frequency, colorScheme, animateAvatars. Fields with no
Android UI yet are stored opaquely via InterfacePreferences setters so
the next Android publish doesn't strip them out.

CustomEmojiRepository now exposes onSyncedFieldChanged + an
applyQuickReactions helper. add/remove/setUnicodeEmojis and
recordEmojiUsage fire the sync callback so emoji-list mutations
participate in the same 4s debounced publish as the other prefs
sources. AppSettingsRepository takes customEmojiRepo as a constructor
arg and wires the callback alongside interfacePrefs / fiatPrefs /
zapPrefs.
…ts, long-press instant zap

Ports the iOS one-tap-zap composer + preset model to Android.

ZapDialog redesign
- Hero amount is the editable input (BasicTextField with a thousands-
  separator VisualTransformation). First-keystroke-replaces-seed is
  preserved via TextRange(0, n); LocalTextSelectionColors overrides the
  selection background to transparent so the seeded select-all doesn't
  paint an ugly box over the orange number.
- Removed the redundant inline "Custom (sats)" OutlinedTextField — the
  hero IS the input now, matching iOS.
- Sheet locks to fillMaxSize so it opens at full height immediately;
  prevents the stagger where the sheet jumped taller 450ms in when
  auto-focus raised the keyboard.
- EditPresetsSheet replaces SaveZapPresetDialog. Modal bottom sheet
  titled "Edit Presets" with a Done pill, per-row swipe-to-delete via
  SwipeToDismissBox (iOS-red #FF3B30 panel, trailing edge only), and a
  "+ Add preset" row that disables itself while a blank entry exists.

Per-account preset routing
- ZapDialog now requires zapPrefsRepo: ZapPreferences. The previous
  in-dialog ZapPreferences(context) wrote to the un-scoped "zap_prefs"
  file, while AppSettingsRepository read from "zap_prefs_<pubkey>" —
  preset writes from the "+" chip never reached the NIP-78 publish
  path and restored presets never showed up in the dialog. All 14
  callsites (Navigation x9, FeedScreen x2, UserProfileScreen x2,
  DmConversationScreen x1) now pass feedViewModel.zapPrefs.
- One-shot migration in ZapPreferences copies any pre-existing global
  zap_prefs entries into the per-account file on first read, marked
  with migrated_from_global_v1 so it never repeats.

Long-press instant zap
- ActionBar's zap glyph uses combinedClickable: onClick opens the
  composer, onLongClick fires the instant zap.
  LocalHapticFeedback.performHapticFeedback(LongPress) on the
  long-press latch so the user feels confirmation before the network
  round-trip.
- NoteActions gains onZapInstant (defaults to onZap so existing
  callers fall through to the composer). Plumbed through PostCard's
  onZapLongPress at every callsite — Navigation, FeedScreen (FeedItem
  also picked up onZapLongPress), ThreadScreen, NotificationsScreen,
  ArticleScreen, SearchScreen, UserProfileScreen. Each instant
  callback reads interfacePrefs and fires sendZap immediately when
  isQuickZapEnabled, else falls through to opening the composer.
…eanup

Post-connect navigation
- When isConnected flips true while currentPage is NwcSetup or
  SparkSetup, clear the back stack and set currentPage = Home. Fixes
  the "Wallet" TopAppBar showing over the connected NWC dashboard
  because the setup screen lingered until the user navigated away.

Switch / Disconnect entry on Wallet Settings
- iOS-style Card row with SwapHoriz icon + "Switch to a different
  wallet" in #FF3B30 for both recoverable cases (default Spark and
  NWC). Section header "Disconnect Wallet" above, explanatory caption
  below. The destructive non-default Spark delete keeps the filled-red
  Button because the seed can't be re-derived from nsec — losing it is
  irreversible.
- Confirmation page unified on #FF3B30 across all three flows (icon
  halo, title, CTA, and "back up first" warning) instead of branching
  between Material primary orange and Material darker red #D32F2F.
- Switching from a default Spark wallet routes back to the wallet-mode
  picker (Home + NotConnected → renders WalletModeSelectionContent)
  rather than dropping the user on the Spark sub-screen.

NWC entry
- QR scan dialog reachable from the connection-string field's trailing
  icon. Reuses the existing QrScanner component; success populates
  onConnectionStringChange so paste + scan are both available.

NWC dashboard
- Lightning-address pill rendered for any wallet mode that carries a
  lud16 (Spark via Breez SDK, NWC via parsed URI). Removed the
  redundant "Nostr Wallet Connect" footer below the balance — the
  in-page top row already brands the connection (NWC logo + node
  alias).
- Adaptive recent-tx count via LocalConfiguration.screenHeightDp
  (5 / 4 / 3 / 2 rows by tier) so smaller phones don't crowd out
  the balance + Send/Receive controls.

Wallet Settings polish
- WalletInfoRow uses a fixed-width label column (widthIn min 110dp,
  trailing padding) and right-aligned values with maxLines = 1 +
  TextOverflow.Ellipsis so long relay URLs / lightning addresses
  truncate cleanly and labels align across rows.
- Backup-to-relay now offered for default Spark wallets too — matches
  iOS, gives users belt-and-braces durability beyond the nsec.

Strings + docs
- wallet_switch_wallet copy expanded to "Switch to a different
  wallet"; new wallet_disconnect_section + wallet_switch_wallet_caption.
- WALLET_PARITY.md §11.2 checklist items marked done for the parity
  work landed here and in the preceding two commits.
The connection-string OutlinedTextField + Connect button used to live
under the 5-row Recommended list, which on smaller phones put them
below the fold — paste/Connect required scrolling past the wallet
suggestions every time. Reordered so the most-common action (paste an
existing nostr+walletconnect:// URI) sits in the upper half of the
viewport, with the wallet suggestions kept as a secondary "if you
don't have one yet" affordance below.
dmnyc added a commit to dmnyc/wisp that referenced this pull request May 25, 2026
Follow-up to barrydeen#556. iOS hides the cross-device-sync toggle and exposes
the sync only through a manual "Restore from relays" affordance — this
PR brings Android to that same layout.

Repo side
- Drop the `interfacePrefs.isSyncSettingsToRelays()` gates in
  `scheduleSettingsSync()` and `restoreSettingsBackup()` — sync is now
  always on while a signer is bound.
- `restoreSettingsBackup()` returns `Boolean` (true = snapshot was
  found and applied, false = nothing on relays / repo not ready) so
  the UI can report the outcome.

UI side
- `InterfaceScreen`: replace the "Cross-device sync / Sync settings to
  relays" `Switch` block with a description + `Restore from relays`
  `Button` matching the iOS design. Section is positioned just above
  the Developer row (last section before the developer tools entry).
- New `onRestoreRequested: (suspend () -> Boolean)?` callback wired by
  `Navigation` to `feedViewModel.appSettingsRepo.restoreSettingsBackup()`.

The unused `InterfacePreferences.isSyncSettingsToRelays()` /
`setSyncSettingsToRelays()` accessors are left in place for now — they
default to true, so nothing reads them, and keeping them avoids a
schema migration when this lands.
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