Skip to content

Commit bc9d161

Browse files
author
Noman R
committed
feat: Watch-only screen for individual accounts with menu actions (rename, archive) in asset account details
1 parent 8325b53 commit bc9d161

File tree

8 files changed

+357
-5
lines changed

8 files changed

+357
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88
#### Added
99
- Asset account details and list screens to view account-specific asset transactions and balances
1010
- Load more transactions functionality in asset account details screen
11+
- Asset account details screen with menu actions (rename, watch-only, archive) for individual accounts
1112

1213
#### Fixed
1314

common/src/commonMain/composeResources/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,7 @@
14881488
<string name="id_you_can_still_receive_funds_but">You can still receive funds, but they won&apos;t be shown on your total balance.</string>
14891489
<string name="id_you_can_sweep_s_of_your_funds">You can sweep %1$s of your funds to your onchain account.</string>
14901490
<string name="id_you_can_use_your_wallet_to">You can use your wallet to anonymously sign and authorize an action on:</string>
1491+
<string name="id_you_can_use_your_descriptor">You can use your descriptor to view your balance in the Blockstream app without the ability to spend.</string>
14911492
<string name="id_you_cannot_add_more_than_one">You cannot add more than one Lightning Account.</string>
14921493
<string name="id_you_cannot_create_or_restore_a">You cannot create or restore a wallet on %1$s as you already have a PIN protected wallet.</string>
14931494
<string name="id_you_cannot_receive_more_than_s">You cannot receive more than %1$s (%2$s) on this account. Reduce the amount and try again.</string>

common/src/commonMain/kotlin/com/blockstream/common/extensions/GdkExtensions.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ fun Account.hasHistory(session: GdkSession): Boolean {
132132
return bip44Discovered == true || isFunded(session) || session.accountTransactions(this).value.isNotEmpty()
133133
}
134134

135+
fun Account.hasUnconfirmedTransactions(session: GdkSession): Boolean {
136+
return session.accountTransactions(this).value.data()?.any { transaction ->
137+
transaction.getConfirmations(session) == 0L
138+
} == true
139+
}
140+
135141
fun String.getAssetNameOrNull(session: GdkSession?): String? {
136142
return if (session == null || this.isPolicyAsset(session)) {
137143
if (this == BTC_POLICY_ASSET) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.blockstream.common.models.assetaccounts
2+
3+
import blockstream_green.common.generated.resources.Res
4+
import blockstream_green.common.generated.resources.id_watchonly
5+
import com.blockstream.common.data.GreenWallet
6+
import com.blockstream.common.extensions.previewAccountAsset
7+
import com.blockstream.common.extensions.previewWallet
8+
import com.blockstream.common.gdk.data.AccountAsset
9+
import com.blockstream.common.models.GreenViewModel
10+
import com.blockstream.ui.navigation.NavData
11+
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
12+
import com.rickclephas.kmp.observableviewmodel.launch
13+
import kotlinx.coroutines.flow.MutableStateFlow
14+
import kotlinx.coroutines.flow.StateFlow
15+
import org.jetbrains.compose.resources.getString
16+
17+
abstract class AccountDescriptorViewModelAbstract(
18+
greenWallet: GreenWallet,
19+
accountAsset: AccountAsset
20+
) : GreenViewModel(greenWalletOrNull = greenWallet, accountAssetOrNull = accountAsset) {
21+
override fun screenName(): String = "AccountDescriptor"
22+
23+
@NativeCoroutinesState
24+
abstract val descriptor: StateFlow<String>
25+
}
26+
27+
class AccountDescriptorViewModel(
28+
greenWallet: GreenWallet,
29+
accountAsset: AccountAsset
30+
) : AccountDescriptorViewModelAbstract(greenWallet = greenWallet, accountAsset = accountAsset) {
31+
32+
override val descriptor: StateFlow<String> = MutableStateFlow(
33+
account.outputDescriptors ?: ""
34+
)
35+
36+
init {
37+
viewModelScope.launch {
38+
_navData.value = NavData(
39+
title = getString(Res.string.id_watchonly),
40+
subtitle = account.name
41+
)
42+
}
43+
44+
bootstrap()
45+
}
46+
}
47+
48+
class AccountDescriptorViewModelPreview(
49+
greenWallet: GreenWallet,
50+
accountAsset: AccountAsset
51+
) : AccountDescriptorViewModelAbstract(greenWallet = greenWallet, accountAsset = accountAsset) {
52+
53+
override val descriptor: StateFlow<String> = MutableStateFlow(
54+
"wpkh([73c5da0a/84'/1'/0']tpubDC8msFGeGuwnKG9Upg7DM2b4DaRqg3CUZa5g8v2SRQ6K4NSkxUgd7HsL2XVWbVm39yBA4LAxysQAm397zwQSQoQgewGiYZqrA9DsP4zbQ1M/0/*)#2e4n992d"
55+
)
56+
57+
companion object {
58+
fun preview() = AccountDescriptorViewModelPreview(
59+
greenWallet = previewWallet(),
60+
accountAsset = previewAccountAsset()
61+
)
62+
}
63+
}

common/src/commonMain/kotlin/com/blockstream/common/models/assetaccounts/AssetAccountDetailsViewModel.kt

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package com.blockstream.common.models.assetaccounts
22

3+
import blockstream_green.common.generated.resources.Res
4+
import blockstream_green.common.generated.resources.binoculars
5+
import blockstream_green.common.generated.resources.box_arrow_down
6+
import blockstream_green.common.generated.resources.id_archive_account
7+
import blockstream_green.common.generated.resources.id_rename_account
8+
import blockstream_green.common.generated.resources.id_watchonly
9+
import blockstream_green.common.generated.resources.text_aa
310
import com.blockstream.common.data.DataState
411
import com.blockstream.common.data.Denomination
512
import com.blockstream.common.data.EnrichedAsset
613
import com.blockstream.common.data.GreenWallet
14+
import com.blockstream.common.events.Events
15+
import com.blockstream.common.extensions.hasUnconfirmedTransactions
716
import com.blockstream.common.extensions.ifConnected
17+
import com.blockstream.common.gdk.data.Account
818
import com.blockstream.common.gdk.data.AccountAsset
919
import com.blockstream.common.gdk.data.AccountBalance
1020
import com.blockstream.common.looks.transaction.TransactionLook
@@ -14,14 +24,17 @@ import com.blockstream.common.sideeffects.SideEffects
1424
import com.blockstream.common.utils.toAmountLook
1525
import com.blockstream.green.utils.Loggable
1626
import com.blockstream.ui.events.Event
27+
import com.blockstream.ui.navigation.NavAction
1728
import com.blockstream.ui.navigation.NavData
29+
import org.jetbrains.compose.resources.getString
1830
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
1931
import com.rickclephas.kmp.observableviewmodel.coroutineScope
2032
import com.rickclephas.kmp.observableviewmodel.launch
2133
import com.rickclephas.kmp.observableviewmodel.stateIn
2234
import kotlinx.coroutines.flow.SharingStarted
2335
import kotlinx.coroutines.flow.StateFlow
2436
import kotlinx.coroutines.flow.combine
37+
import kotlinx.coroutines.flow.filter
2538
import kotlinx.coroutines.flow.launchIn
2639
import kotlinx.coroutines.flow.map
2740
import kotlinx.coroutines.flow.onEach
@@ -51,6 +64,9 @@ abstract class AssetAccountDetailsViewModelAbstract(
5164

5265
@NativeCoroutinesState
5366
abstract val hasMoreTransactions: StateFlow<Boolean>
67+
68+
@NativeCoroutinesState
69+
abstract val accounts: StateFlow<List<Account>>
5470
}
5571

5672
class AssetAccountDetailsViewModel(
@@ -74,6 +90,10 @@ class AssetAccountDetailsViewModel(
7490
override val showBuyButton: StateFlow<Boolean> = accountAsset.map {
7591
it?.asset?.isBitcoin == true
7692
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), accountAsset.value?.asset?.isBitcoin == true)
93+
94+
override val accounts: StateFlow<List<Account>> =
95+
session.accounts.filter { session.isConnected }
96+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf())
7797

7898
private val hideAmounts: StateFlow<Boolean> = settingsManager.appSettingsStateFlow.map {
7999
it.hideAmounts
@@ -88,11 +108,22 @@ class AssetAccountDetailsViewModel(
88108
override val totalBalanceFiat: StateFlow<String?> = _totalBalanceFiat
89109

90110
init {
91-
viewModelScope.launch {
92-
val assetName = accountAsset.value?.asset?.name(session)?.toString() ?: accountAsset.value?.assetId ?: ""
93-
_navData.value = NavData(
94-
title = assetName, subtitle = account.name
95-
)
111+
session.ifConnected {
112+
combine(accountAsset, accounts, isWatchOnly) { accountAsset, accounts, watchOnly ->
113+
viewModelScope.launch {
114+
val assetName = accountAsset?.asset?.name(session)?.toString() ?: accountAsset?.assetId ?: ""
115+
val accountName = accountAsset?.account?.name ?: account.name
116+
_navData.value = NavData(
117+
title = assetName,
118+
subtitle = accountName,
119+
actions = getMenuActions(
120+
account = account,
121+
accountAsset = accountAsset,
122+
watchOnly = watchOnly
123+
)
124+
)
125+
}
126+
}.launchIn(viewModelScope.coroutineScope)
96127
}
97128

98129
session.ifConnected {
@@ -196,5 +227,55 @@ class AssetAccountDetailsViewModel(
196227
session.getTransactions(account = account, isReset = false, isLoadMore = true)
197228
}
198229

230+
private suspend fun getMenuActions(
231+
account: Account,
232+
accountAsset: AccountAsset?,
233+
watchOnly: Boolean
234+
): List<NavAction> {
235+
if (account.isLightning) {
236+
return emptyList()
237+
}
238+
239+
return listOfNotNull(
240+
NavAction(
241+
title = getString(Res.string.id_rename_account),
242+
icon = Res.drawable.text_aa,
243+
isMenuEntry = true,
244+
onClick = {
245+
postEvent(NavigateDestinations.RenameAccount(greenWallet = greenWallet, account = account))
246+
}
247+
).takeIf { !watchOnly },
248+
249+
NavAction(
250+
title = getString(Res.string.id_watchonly),
251+
icon = Res.drawable.binoculars,
252+
isMenuEntry = true,
253+
onClick = {
254+
if (account.isSinglesig) {
255+
accountAsset?.also {
256+
postEvent(NavigateDestinations.AccountDescriptor(greenWallet = greenWallet, accountAsset = it))
257+
}
258+
} else {
259+
// For multisig accounts, show the watch-only credentials bottom sheet
260+
postEvent(NavigateDestinations.WatchOnlyCredentialsSettings(greenWallet = greenWallet, network = account.network))
261+
}
262+
}
263+
).takeIf { !watchOnly },
264+
265+
NavAction(
266+
title = getString(Res.string.id_archive_account),
267+
icon = Res.drawable.box_arrow_down,
268+
isMenuEntry = true,
269+
onClick = {
270+
postEvent(Events.ArchiveAccount(account))
271+
}
272+
).takeIf {
273+
!watchOnly &&
274+
!account.isFunded(session) &&
275+
!account.hasUnconfirmedTransactions(session)
276+
}
277+
)
278+
}
279+
199280
companion object : Loggable()
200281
}

common/src/commonMain/kotlin/com/blockstream/common/navigation/Destinations.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ sealed class NavigateDestinations : NavigateDestination() {
290290
data class AssetAccountDetails(val greenWallet: GreenWallet, val accountAsset: AccountAsset) :
291291
NavigateDestination()
292292

293+
@Serializable
294+
data class AccountDescriptor(val greenWallet: GreenWallet, val accountAsset: AccountAsset) :
295+
NavigateDestination()
296+
293297
@Serializable
294298
data class Camera(
295299
val isDecodeContinuous: Boolean = false,

compose/src/commonMain/kotlin/com/blockstream/compose/navigation/Router.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.blockstream.common.models.add.XpubViewModel
2727
import com.blockstream.common.models.addresses.AddressesViewModel
2828
import com.blockstream.common.models.addresses.SignMessageViewModel
2929
import com.blockstream.common.models.archived.ArchivedAccountsViewModel
30+
import com.blockstream.common.models.assetaccounts.AccountDescriptorViewModel
3031
import com.blockstream.common.models.assetaccounts.AssetAccountDetailsViewModel
3132
import com.blockstream.common.models.assetaccounts.AssetAccountListViewModel
3233
import com.blockstream.common.models.camera.CameraViewModel
@@ -106,6 +107,7 @@ import com.blockstream.compose.screens.add.ReviewAddAccountScreen
106107
import com.blockstream.compose.screens.add.XpubScreen
107108
import com.blockstream.compose.screens.addresses.AddressesScreen
108109
import com.blockstream.compose.screens.archived.ArchivedAccountsScreen
110+
import com.blockstream.compose.screens.assetaccounts.AccountDescriptorScreen
109111
import com.blockstream.compose.screens.assetaccounts.AssetAccountDetailsScreen
110112
import com.blockstream.compose.screens.assetaccounts.AssetAccountListScreen
111113
import com.blockstream.compose.screens.devices.DeviceInfoScreen
@@ -901,6 +903,17 @@ fun Router(
901903
}
902904
)
903905
}
906+
appComposable<NavigateDestinations.AccountDescriptor> {
907+
val args = it.toRoute<NavigateDestinations.AccountDescriptor>()
908+
AccountDescriptorScreen(
909+
viewModel = viewModel {
910+
AccountDescriptorViewModel(
911+
greenWallet = args.greenWallet,
912+
accountAsset = args.accountAsset
913+
)
914+
}
915+
)
916+
}
904917
appBottomSheet<NavigateDestinations.JadeFirmwareUpdate> {
905918
val args = it.toRoute<NavigateDestinations.JadeFirmwareUpdate>()
906919
JadeFirmwareUpdateBottomSheet(

0 commit comments

Comments
 (0)