Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9efb9c4
Add wallet descriptor management with security fixes
wksantiago Feb 24, 2026
712ac4e
Code cleanup and FlowRow fix for network chips
wksantiago Feb 24, 2026
e600d7b
Add error handling to refreshDescriptors
wksantiago Feb 24, 2026
550803e
Surface actual error messages in wallet descriptor toasts
wksantiago Feb 24, 2026
4074643
Add DescriptorSessionManager unit tests
wksantiago Feb 24, 2026
7683c47
Fix review findings: polling guard, double-tap prevention, input vali…
wksantiago Feb 24, 2026
03df530
Keep dialogs open on failure for propose and delete
wksantiago Feb 24, 2026
9eecf45
Re-throw CancellationException in descriptor runCatching blocks
wksantiago Feb 25, 2026
791581f
Deduplicate pending proposals and guard descriptor polling
wksantiago Feb 25, 2026
e2998b2
fix: thread safety, error handling, and ordering in descriptor code
wksantiago Feb 25, 2026
3baf1e0
Clean up descriptor session state, callback resilience, and code simp…
wksantiago Feb 25, 2026
2c42d1a
Log descriptor callback registration failures
wksantiago Feb 25, 2026
b09c377
Fix security and code quality issues from PR review
wksantiago Feb 25, 2026
935ff0a
Simplify descriptor state updates and deduplicate proposal actions
wksantiago Feb 25, 2026
798dd03
Fix proposal cleanup and descriptor count on transient failures
wksantiago Feb 25, 2026
94b1fdf
Implement onXpubAnnounced callback
wksantiago Feb 25, 2026
2d61aff
Fix supportingText double-brace lambda syntax
wksantiago Feb 25, 2026
2f120f7
Revert supportingText lambda syntax change
wksantiago Feb 25, 2026
afb6fe6
Fix security and reliability issues in wallet descriptor flow
kwsantiago Feb 25, 2026
d249af4
Simplify descriptor state and add missing active guard
kwsantiago Feb 25, 2026
f25d88f
Fix callback reactivation on screen re-entry and supportingText lambdas
kwsantiago Feb 25, 2026
57057b1
Fix flag reset on CancellationException and remove redundant Idle guard
kwsantiago Feb 25, 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
26 changes: 26 additions & 0 deletions app/src/main/kotlin/io/privkey/keep/ConnectionCards.kt
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,32 @@ fun BunkerCard(status: BunkerStatus, onClick: () -> Unit) {
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WalletDescriptorCard(descriptorCount: Int, onClick: () -> Unit) {
Card(modifier = Modifier.fillMaxWidth(), onClick = onClick) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text("Wallet Descriptors", style = MaterialTheme.typography.titleMedium)
Text(
"Manage multisig wallet descriptors",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
if (descriptorCount > 0) "$descriptorCount" else "Manage",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
}

@Composable
fun Nip55SettingsCard(
onSignPolicyClick: () -> Unit,
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/kotlin/io/privkey/keep/KeepMobileApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.privkey.keep

import android.app.Application
import android.util.Log
import io.privkey.keep.descriptor.DescriptorSessionManager
import io.privkey.keep.nip46.BunkerService
import io.privkey.keep.nip55.AutoSigningSafeguards
import io.privkey.keep.nip55.CallerVerificationStore
Expand Down Expand Up @@ -321,6 +322,7 @@ class KeepMobileApp : Application() {
_connectionState.value = ConnectionState()
BunkerService.stop(this)
bunkerConfigStore?.setEnabled(false)
DescriptorSessionManager.clearAll()
withContext(Dispatchers.IO) {
runAccountSwitchCleanup("revoke permissions") { permissionStore?.revokeAllPermissions() }
runAccountSwitchCleanup("clear app settings") { permissionStore?.clearAllAppSettings() }
Expand Down
53 changes: 48 additions & 5 deletions app/src/main/kotlin/io/privkey/keep/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import io.privkey.keep.descriptor.DescriptorSessionManager
import io.privkey.keep.descriptor.WalletDescriptorScreen
import io.privkey.keep.navigation.Route
import io.privkey.keep.nip46.BunkerScreen
import io.privkey.keep.nip46.BunkerService
Expand All @@ -57,6 +59,7 @@ import io.privkey.keep.uniffi.BunkerStatus
import io.privkey.keep.uniffi.KeepMobile
import io.privkey.keep.uniffi.PeerInfo
import io.privkey.keep.uniffi.ShareInfo
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -315,6 +318,8 @@ fun MainScreen(
var biometricTimeout by remember { mutableStateOf(biometricTimeoutStore.getTimeout()) }
var biometricLockOnLaunch by remember { mutableStateOf(biometricTimeoutStore.isLockOnLaunchEnabled()) }
var showBunkerScreen by remember { mutableStateOf(false) }
var showWalletDescriptorScreen by remember { mutableStateOf(false) }
var descriptorCount by remember { mutableIntStateOf(0) }
val bunkerUrl by BunkerService.bunkerUrl.collectAsState()
val bunkerStatus by BunkerService.status.collectAsState()
var proxyEnabled by remember { mutableStateOf(proxyConfigStore.isEnabled()) }
Expand Down Expand Up @@ -391,23 +396,42 @@ fun MainScreen(
}

LaunchedEffect(Unit) {
DescriptorSessionManager.clearAll()
DescriptorSessionManager.activate()
runCatching {
withContext(Dispatchers.IO) {
keepMobile.walletDescriptorSetCallbacks(DescriptorSessionManager.createCallbacks())
}
}.onSuccess {
DescriptorSessionManager.setCallbacksRegistered(true)
}.onFailure {
if (it is CancellationException) throw it
Log.e("MainActivity", "Failed to set descriptor callbacks", it)
DescriptorSessionManager.setCallbacksRegistered(false)
}
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
repeat(Int.MAX_VALUE) {
val (newHasShare, newShareInfo, newAccounts, newActiveKey, newPeers, newPendingCount) = withContext(Dispatchers.IO) {
val (newHasShare, newShareInfo, newAccounts, newActiveKey, newPeers, newPendingCount, newDescriptorCount) = withContext(Dispatchers.IO) {
val h = keepMobile.hasShare()
val s = keepMobile.getShareInfo()
val a = storage.listAllShares().map { it.toAccountInfo() }
val k = storage.getActiveShareKey()
val p = if (h) keepMobile.getPeers() else emptyList()
val pc = if (h) keepMobile.getPendingRequests().size else 0
PollResult(h, s, a, k, p, pc)
val dc = if (h) {
runCatching { keepMobile.walletDescriptorList().size }
.onFailure { if (it is CancellationException) throw it }
.getOrDefault(descriptorCount)
} else 0
PollResult(h, s, a, k, p, pc, dc)
}
hasShare = newHasShare
shareInfo = newShareInfo
allAccounts = newAccounts
activeAccountKey = newActiveKey
peers = newPeers
pendingCount = newPendingCount
descriptorCount = newDescriptorCount
refreshCertificatePins()
profileRelays = withContext(Dispatchers.IO) { loadProfileRelays(newActiveKey) }
delay(10_000)
Expand Down Expand Up @@ -552,6 +576,14 @@ fun MainScreen(
return
}

if (showWalletDescriptorScreen) {
WalletDescriptorScreen(
keepMobile = keepMobile,
onDismiss = { showWalletDescriptorScreen = false }
)
return
}

if (showAccountSwitcher) {
AccountSwitcherSheet(
accounts = allAccounts,
Expand Down Expand Up @@ -696,11 +728,13 @@ fun MainScreen(
AppsTab(
hasShare = hasShare,
bunkerStatus = bunkerStatus,
descriptorCount = descriptorCount,
onConnectedAppsClick = { showConnectedApps = true },
onSignPolicyClick = { showSignPolicyScreen = true },
onPermissionsClick = { showPermissionsScreen = true },
onHistoryClick = { showHistoryScreen = true },
onBunkerClick = { showBunkerScreen = true }
onBunkerClick = { showBunkerScreen = true },
onWalletDescriptorClick = { showWalletDescriptorScreen = true }
)
}

Expand Down Expand Up @@ -906,11 +940,13 @@ private fun HomeTab(
private fun AppsTab(
hasShare: Boolean,
bunkerStatus: BunkerStatus,
descriptorCount: Int,
onConnectedAppsClick: () -> Unit,
onSignPolicyClick: () -> Unit,
onPermissionsClick: () -> Unit,
onHistoryClick: () -> Unit,
onBunkerClick: () -> Unit
onBunkerClick: () -> Unit,
onWalletDescriptorClick: () -> Unit
) {
Column(
modifier = Modifier
Expand Down Expand Up @@ -938,6 +974,12 @@ private fun AppsTab(
status = bunkerStatus,
onClick = onBunkerClick
)
Spacer(modifier = Modifier.height(16.dp))

WalletDescriptorCard(
descriptorCount = descriptorCount,
onClick = onWalletDescriptorClick
)
} else {
Card(modifier = Modifier.fillMaxWidth()) {
Column(
Expand Down Expand Up @@ -1170,7 +1212,8 @@ private data class PollResult(
val allAccounts: List<AccountInfo>,
val activeAccountKey: String?,
val peers: List<PeerInfo>,
val pendingCount: Int
val pendingCount: Int,
val descriptorCount: Int
)

@Composable
Expand Down
Loading