Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
80b50ea
feat(nip78): cross-device sync of UI prefs
dmnyc May 21, 2026
9ec4fea
feat(zaps): instant-zap settings + payload fields
dmnyc May 21, 2026
bbee9c8
feat(wallet-setup): primary Spark + More-options accordion
dmnyc May 21, 2026
2b482fb
feat(zap-sheet): instant-amount seed, in-sheet toggle, caps, confirma…
dmnyc May 21, 2026
754d24f
feat(post-card): tap composer / long-press instant zap + self-zap dis…
dmnyc May 21, 2026
38ef526
feat(bolt): white-core glow pulse for in-flight zap
dmnyc May 21, 2026
e591e91
feat(dev): DEBUG-only developer panel scaffold
dmnyc May 21, 2026
7f52e0a
fix(wallet): show top 5 recent transactions on dashboard footer inste…
dmnyc May 21, 2026
9391dcf
feat(zap-sheet): full iOS-layout rewrite + ModalBottomSheet for drag-…
dmnyc May 21, 2026
263169b
fix(zap-sheet): pin Zap button above keyboard + wire recipient on eve…
dmnyc May 21, 2026
3ab5c8f
fix(nip78): cross-coroutine-scope event drop + ARGB Int overflow
dmnyc May 21, 2026
ad246aa
feat(nip78): full schema parity with iOS one-tap-zap PR
dmnyc May 22, 2026
0be27d4
feat(zap): hero-input composer, Edit Presets sheet, per-account prese…
dmnyc May 22, 2026
810afda
feat(wallet): parity polish — switch flow, NWC dashboard, settings cl…
dmnyc May 22, 2026
4f88bf9
fix(wallet): move NWC paste field above the Recommended wallets list
dmnyc May 22, 2026
b8cb061
fix(settings): scope instant-zap defaults to active account
dmnyc May 25, 2026
d7c81ea
Merge main into fix/zap-defaults-account-isolation to resolve PR conf…
dmnyc May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions WALLET_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,25 +772,25 @@ existing wallet have no obvious entry point.

**Restructure per §2.6**:

- [ ] Replace the top-level layout with the two-row picker (Spark /
- [x] Replace the top-level layout with the two-row picker (Spark /
NWC) described in §2.6 Screen 1.
- [ ] Add a Spark sub-screen matching §2.6 Screen 2 with four option
- [x] Add a Spark sub-screen matching §2.6 Screen 2 with four option
rows (or three when `hasKeypair() == false`).
- [ ] Move the `maybeAutoCreateDefaultWallet()` entry point to the new
- [x] Move the `maybeAutoCreateDefaultWallet()` entry point to the new
"Use my default wallet" row at the top of the Spark sub-screen.
Ignore `skipAutoCreate` on explicit tap.
- [ ] Add the string resources from §2.6 to `strings.xml`.
- [ ] Verify the existing flows still wire through:
- [x] Add the string resources from §2.6 to `strings.xml`.
- [x] Verify the existing flows still wire through:
- Create new wallet → existing BIP39-generate + confirm-backup flow
- Restore from seed phrase → existing 12-word entry flow
- Restore from relays → existing NIP-78 backup search flow
- Nostr Wallet Connect → existing NWC paste-string flow
- [ ] Disconnect flow on a default wallet says **"Switch Wallet"** and
- [x] Disconnect flow on a default wallet says **"Switch Wallet"** and
the body copy refers to the wallet as your *default wallet* —
never "Wisp wallet" or "wisp wallet".
- [ ] Settings section header renamed from "Danger Zone" to
- [x] Settings section header renamed from "Danger Zone" to
**"Disconnect Wallet"** (per §4.8).
- [ ] Dashboard welcome banner for default wallets per §3.5 (blue/accent
- [x] Dashboard welcome banner for default wallets per §3.5 (blue/accent
tint, key icon, "secured by your key" copy) — separate from the
existing amber warning banner for custom wallets.

Expand Down
132 changes: 122 additions & 10 deletions app/src/main/kotlin/com/wisp/app/Navigation.kt

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions app/src/main/kotlin/com/wisp/app/nostr/Nip78.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,97 @@ object Nip78 {
/** Extract the d-tag value from an event, or null. */
fun extractDTag(event: NostrEvent): String? =
event.tags.firstOrNull { it.size >= 2 && it[0] == "d" }?.get(1)

// ─── App Settings Backup ──────────────────────────────────────────────────

const val APP_SETTINGS_D_TAG = "wisp-app-settings:v1"

/**
* Versioned, NIP-44-encrypted UI-prefs payload stored as kind 30078.
* Every field is optional so older / newer clients round-trip without
* data loss. JSON keys match iOS Nip78Backup.AppSettingsPayload
* byte-for-byte so the backups are bit-compatible across platforms.
*/
@kotlinx.serialization.Serializable
data class AppSettingsPayload(
// Reactions (iOS-only UI today; Android round-trips defaultReaction
// + defaultReactionEnabled as opaque values so iOS settings survive
// an Android publish.)
val defaultReaction: String? = null,
val defaultReactionEnabled: Boolean? = null,
// Quick zaps
val quickZapEnabled: Boolean? = null,
val quickZapAmountSats: Long? = null,
val quickZapAmountFiat: Double? = null,
val quickZapMessage: String? = null,
val zapIconStyle: String? = null,
// Fiat prefs
val fiatModeEnabled: Boolean? = null,
val fiatCurrency: String? = null,
// Zap presets
val zapPresetsCSV: String? = null,
// Reaction popup state (Android: unicodeEmojis + frequency map in
// CustomEmojiRepository; iOS: EmojiRepository).
val quickReactions: List<String>? = null,
val frequency: Map<String, Int>? = null,
// Appearance
val largeText: Boolean? = null,
val themeName: String? = null,
// iOS-only light/dark override today; Android round-trips it.
val colorScheme: String? = null,
// iOS encodes ARGB as Swift `Int` (64-bit), so values like
// 0xFFFF9800 (4_294_940_672) overflow Kotlin's signed 32-bit
// Int. Use Long here and convert at the setter — the lower 32
// bits round-trip correctly even when Int reads them negative.
val accentColorARGB: Long? = null,
// Media
val autoLoadMedia: Boolean? = null,
val videoAutoplay: Boolean? = null,
// iOS-only toggle for animated avatars; Android always animates,
// so this is round-tripped opaquely.
val animateAvatars: Boolean? = null,
val mediaLayoutStyle: String? = null,
// Posting
val clientTagEnabled: Boolean? = null,
val postUndoTimerEnabled: Boolean? = null,
val postUndoTimerSeconds: Int? = null,
val postUndoTimerForReplies: Boolean? = null,
val version: Int? = 1
)

private val lenientJson = kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
encodeDefaults = false
}

/** Build and sign a kind 30078 event carrying the NIP-44-encrypted settings JSON. */
suspend fun createAppSettingsEvent(signer: NostrSigner, payload: AppSettingsPayload): NostrEvent {
val json = lenientJson.encodeToString(AppSettingsPayload.serializer(), payload)
val encrypted = signer.nip44Encrypt(json, signer.pubkeyHex)
val tags = listOf(
listOf("d", APP_SETTINGS_D_TAG),
listOf("encryption", "nip44")
)
return signer.signEvent(kind = KIND, content = encrypted, tags = tags)
}

/** Decrypt a kind 30078 app-settings event and parse the JSON payload. Returns null on any failure. */
suspend fun decryptAppSettings(signer: NostrSigner, event: NostrEvent): AppSettingsPayload? {
if (event.content.isBlank()) return null
return try {
val decrypted = signer.nip44Decrypt(event.content, event.pubkey)
lenientJson.decodeFromString(AppSettingsPayload.serializer(), decrypted)
} catch (e: Exception) {
android.util.Log.w("AppSettingsSync", "decryptAppSettings exception: ${e.javaClass.simpleName}: ${e.message}", e)
null
}
}

/** Filter to fetch this user's app-settings backup (single addressable event). */
fun appSettingsFilter(pubkeyHex: String): Filter = Filter(
kinds = listOf(KIND),
authors = listOf(pubkeyHex),
dTags = listOf(APP_SETTINGS_D_TAG),
limit = 1
)
}
Loading