Messages and balances hotfixes

This commit is contained in:
Milan Cerovsky 2025-04-23 11:33:59 +02:00
parent 0ea9c7ef51
commit e80f583d54
26 changed files with 212 additions and 122 deletions

View File

@ -38,3 +38,7 @@ data class ZecAmountPair(
val main: String, val main: String,
val suffix: String val suffix: String
) )
@Suppress("MagicNumber")
val Zatoshi.Companion.typicalFee: Zatoshi
get() = Zatoshi(100000)

View File

@ -19,7 +19,6 @@ import co.electriccoin.zcash.ui.common.usecase.GetCoinbaseStatusUseCase
import co.electriccoin.zcash.ui.common.usecase.GetConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.GetConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetExchangeRateUseCase import co.electriccoin.zcash.ui.common.usecase.GetExchangeRateUseCase
import co.electriccoin.zcash.ui.common.usecase.GetFlexaStatusUseCase import co.electriccoin.zcash.ui.common.usecase.GetFlexaStatusUseCase
import co.electriccoin.zcash.ui.common.usecase.GetHomeMessageUseCase import co.electriccoin.zcash.ui.common.usecase.GetHomeMessageUseCase
@ -34,6 +33,7 @@ import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionDetailByIdUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransactionDetailByIdUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
@ -142,7 +142,7 @@ val useCaseModule =
factoryOf(::GetSelectedWalletAccountUseCase) factoryOf(::GetSelectedWalletAccountUseCase)
singleOf(::ObserveClearSendUseCase) singleOf(::ObserveClearSendUseCase)
singleOf(::PrefillSendUseCase) singleOf(::PrefillSendUseCase)
factoryOf(::GetCurrentTransactionsUseCase) factoryOf(::GetTransactionsUseCase)
factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback
factoryOf(::CreateProposalUseCase) factoryOf(::CreateProposalUseCase)
factoryOf(::OnZip321ScannedUseCase) factoryOf(::OnZip321ScannedUseCase)

View File

@ -9,7 +9,7 @@ import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewMode
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsViewModel import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsViewModel
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetViewModel import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetViewModel
import co.electriccoin.zcash.ui.screen.balances.action.BalanceActionViewModel import co.electriccoin.zcash.ui.screen.balances.spendable.SpendableBalanceViewModel
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
@ -23,6 +23,7 @@ import co.electriccoin.zcash.ui.screen.home.HomeViewModel
import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupDetailViewModel import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupDetailViewModel
import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupInfoViewModel import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupInfoViewModel
import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportOptInViewModel import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportOptInViewModel
import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringInfoViewModel
import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfoViewModel import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfoViewModel
import co.electriccoin.zcash.ui.screen.integrations.IntegrationsViewModel import co.electriccoin.zcash.ui.screen.integrations.IntegrationsViewModel
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
@ -156,6 +157,7 @@ val viewModelModule =
viewModelOf(::ExchangeRateSettingsViewModel) viewModelOf(::ExchangeRateSettingsViewModel)
viewModelOf(::WalletBackupDetailViewModel) viewModelOf(::WalletBackupDetailViewModel)
viewModelOf(::ErrorViewModel) viewModelOf(::ErrorViewModel)
viewModelOf(::BalanceActionViewModel) viewModelOf(::SpendableBalanceViewModel)
viewModelOf(::CrashReportOptInViewModel) viewModelOf(::CrashReportOptInViewModel)
viewModelOf(::WalletRestoringInfoViewModel)
} }

View File

@ -49,8 +49,8 @@ import co.electriccoin.zcash.ui.screen.addressbook.WrapAddressBook
import co.electriccoin.zcash.ui.screen.advancedsettings.WrapAdvancedSettings import co.electriccoin.zcash.ui.screen.advancedsettings.WrapAdvancedSettings
import co.electriccoin.zcash.ui.screen.authentication.AuthenticationUseCase import co.electriccoin.zcash.ui.screen.authentication.AuthenticationUseCase
import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication
import co.electriccoin.zcash.ui.screen.balances.action.AndroidBalanceAction import co.electriccoin.zcash.ui.screen.balances.spendable.AndroidSpendableBalance
import co.electriccoin.zcash.ui.screen.balances.action.BalanceAction import co.electriccoin.zcash.ui.screen.balances.spendable.SpendableBalance
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
import co.electriccoin.zcash.ui.screen.connectkeystone.AndroidConnectKeystone import co.electriccoin.zcash.ui.screen.connectkeystone.AndroidConnectKeystone
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
@ -497,14 +497,14 @@ internal fun MainActivity.Navigation() {
) { ) {
AndroidErrorBottomSheet() AndroidErrorBottomSheet()
} }
dialog<BalanceAction>( dialog<SpendableBalance>(
dialogProperties = dialogProperties =
DialogProperties( DialogProperties(
dismissOnBackPress = false, dismissOnBackPress = false,
dismissOnClickOutside = false dismissOnClickOutside = false
) )
) { ) {
AndroidBalanceAction() AndroidSpendableBalance()
} }
composable<CrashReportOptIn> { composable<CrashReportOptIn> {
AndroidCrashReportOptIn() AndroidCrashReportOptIn()

View File

@ -57,6 +57,18 @@ sealed interface WalletAccount : Comparable<WalletAccount> {
val isShieldingAvailable: Boolean val isShieldingAvailable: Boolean
get() = totalTransparentBalance > Zatoshi(100000L) get() = totalTransparentBalance > Zatoshi(100000L)
val isAllShielded: Boolean
get() {
val isAllShielded = totalBalance == spendableShieldedBalance
val isAllShieldedWithTransparentDustLeft =
totalBalance > spendableShieldedBalance &&
spendableShieldedBalance == totalShieldedBalance &&
totalTransparentBalance > Zatoshi(0) &&
!isShieldingAvailable
return isAllShielded || isAllShieldedWithTransparentDustLeft
}
fun canSpend(amount: Zatoshi): Boolean = spendableShieldedBalance >= amount fun canSpend(amount: Zatoshi): Boolean = spendableShieldedBalance >= amount
} }

View File

@ -48,8 +48,7 @@ class SynchronizerProviderImpl(
emit(synchronizer) emit(synchronizer)
} }
} }
} }.flowOn(Dispatchers.IO)
.flowOn(Dispatchers.IO)
.stateIn( .stateIn(
scope = scope, scope = scope,
started = SharingStarted.Lazily, started = SharingStarted.Lazily,

View File

@ -415,3 +415,6 @@ sealed interface ShieldTransaction : Transaction {
override val overview: TransactionOverview, override val overview: TransactionOverview,
) : ShieldTransaction ) : ShieldTransaction
} }
val Transaction.isPending: Boolean
get() = this is SendTransaction.Pending || this is ShieldTransaction.Pending || this is ReceiveTransaction.Pending

View File

@ -15,12 +15,10 @@ import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSource
import co.electriccoin.zcash.ui.common.model.FastestServersState import co.electriccoin.zcash.ui.common.model.FastestServersState
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletAccount import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
@ -67,7 +65,6 @@ interface WalletRepository {
val allAccounts: Flow<List<WalletAccount>?> val allAccounts: Flow<List<WalletAccount>?>
val currentAccount: Flow<WalletAccount?> val currentAccount: Flow<WalletAccount?>
val currentWalletSnapshot: StateFlow<WalletSnapshot?>
/** /**
* A flow of the wallet block synchronization state. * A flow of the wallet block synchronization state.
@ -105,7 +102,6 @@ class WalletRepositoryImpl(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider, private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
private val restoreTimestampDataSource: RestoreTimestampDataSource, private val restoreTimestampDataSource: RestoreTimestampDataSource,
private val walletRestoringStateProvider: WalletRestoringStateProvider, private val walletRestoringStateProvider: WalletRestoringStateProvider,
private val walletSnapshotDataSource: WalletSnapshotDataSource,
) : WalletRepository { ) : WalletRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@ -185,8 +181,6 @@ class WalletRepositoryImpl(
initialValue = FastestServersState(servers = emptyList(), isLoading = true) initialValue = FastestServersState(servers = emptyList(), isLoading = true)
) )
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> = walletSnapshotDataSource.observe()
/** /**
* A flow of the wallet block synchronization state. * A flow of the wallet block synchronization state.
*/ */

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
class GetCurrentTransactionsUseCase( class GetTransactionsUseCase(
private val transactionRepository: TransactionRepository, private val transactionRepository: TransactionRepository,
private val metadataRepository: MetadataRepository, private val metadataRepository: MetadataRepository,
) { ) {

View File

@ -11,7 +11,7 @@ import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.balances.action.BalanceAction import co.electriccoin.zcash.ui.screen.balances.spendable.SpendableBalance
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
@ -50,13 +50,7 @@ class BalanceWidgetViewModel(
when { when {
!args.isBalanceButtonEnabled -> null !args.isBalanceButtonEnabled -> null
account == null -> null account == null -> null
account.totalBalance == account.spendableShieldedBalance -> null account.isAllShielded -> null
account.totalBalance > account.spendableShieldedBalance &&
account.spendableShieldedBalance == account.totalShieldedBalance &&
account.totalTransparentBalance > Zatoshi(0) &&
!account.isShieldingAvailable ->
null
account.totalBalance > account.spendableShieldedBalance && account.totalBalance > account.spendableShieldedBalance &&
!account.isShieldedPending && !account.isShieldedPending &&
account.totalShieldedBalance > Zatoshi(0) && account.totalShieldedBalance > Zatoshi(0) &&
@ -94,7 +88,7 @@ class BalanceWidgetViewModel(
showDust = args.showDust showDust = args.showDust
) )
private fun onBalanceButtonClick() = navigationRouter.forward(BalanceAction) private fun onBalanceButtonClick() = navigationRouter.forward(SpendableBalance)
} }
data class BalanceWidgetArgs( data class BalanceWidgetArgs(

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.balances.action package co.electriccoin.zcash.ui.screen.balances.spendable
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -9,11 +9,11 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AndroidBalanceAction() { fun AndroidSpendableBalance() {
val vm = koinViewModel<BalanceActionViewModel>() val vm = koinViewModel<SpendableBalanceViewModel>()
val state by vm.state.collectAsStateWithLifecycle() val state by vm.state.collectAsStateWithLifecycle()
BalanceActionView(state) SpendableBalanceView(state)
} }
@Serializable @Serializable
data object BalanceAction data object SpendableBalance

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.balances.action package co.electriccoin.zcash.ui.screen.balances.spendable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
@ -8,24 +8,24 @@ import co.electriccoin.zcash.ui.design.util.ImageResource
import co.electriccoin.zcash.ui.design.util.StringResource import co.electriccoin.zcash.ui.design.util.StringResource
@Immutable @Immutable
data class BalanceActionState( data class SpendableBalanceState(
val title: StringResource, val title: StringResource,
val message: StringResource, val message: StringResource,
val rows: List<BalanceActionRowState>, val rows: List<SpendableBalanceRowState>,
val shieldButton: BalanceShieldButtonState?, val shieldButton: SpendableBalanceShieldButtonState?,
val positive: ButtonState, val positive: ButtonState,
override val onBack: () -> Unit, override val onBack: () -> Unit,
) : ModalBottomSheetState ) : ModalBottomSheetState
@Immutable @Immutable
data class BalanceActionRowState( data class SpendableBalanceRowState(
val title: StringResource, val title: StringResource,
val icon: ImageResource, val icon: ImageResource,
val value: StringResource val value: StringResource
) )
@Immutable @Immutable
data class BalanceShieldButtonState( data class SpendableBalanceShieldButtonState(
val amount: Zatoshi, val amount: Zatoshi,
val onShieldClick: () -> Unit, val onShieldClick: () -> Unit,
) )

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.balances.action package co.electriccoin.zcash.ui.screen.balances.spendable
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -42,8 +42,8 @@ import co.electriccoin.zcash.ui.design.util.stringRes
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BalanceActionView( fun SpendableBalanceView(
state: BalanceActionState?, state: SpendableBalanceState?,
sheetState: SheetState = rememberScreenModalBottomSheetState(), sheetState: SheetState = rememberScreenModalBottomSheetState(),
) { ) {
ZashiScreenModalBottomSheet( ZashiScreenModalBottomSheet(
@ -56,7 +56,7 @@ fun BalanceActionView(
} }
@Composable @Composable
fun BottomSheetContent(state: BalanceActionState, modifier: Modifier = Modifier) { fun BottomSheetContent(state: SpendableBalanceState, modifier: Modifier = Modifier) {
Column( Column(
modifier = modifier =
modifier modifier
@ -97,7 +97,7 @@ fun BottomSheetContent(state: BalanceActionState, modifier: Modifier = Modifier)
} }
@Composable @Composable
private fun BalanceActionRow(state: BalanceActionRowState) { private fun BalanceActionRow(state: SpendableBalanceRowState) {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -137,7 +137,7 @@ private fun BalanceActionRow(state: BalanceActionRowState) {
} }
@Composable @Composable
private fun BalanceShieldButton(state: BalanceShieldButtonState) { private fun BalanceShieldButton(state: SpendableBalanceShieldButtonState) {
ZashiCard( ZashiCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentPadding = contentPadding =
@ -193,9 +193,9 @@ private fun BalanceShieldButton(state: BalanceShieldButtonState) {
@Composable @Composable
private fun Preview() = private fun Preview() =
ZcashTheme { ZcashTheme {
BalanceActionView( SpendableBalanceView(
state = state =
BalanceActionState( SpendableBalanceState(
title = stringRes("Title"), title = stringRes("Title"),
message = stringRes("Subtitle"), message = stringRes("Subtitle"),
positive = positive =
@ -205,19 +205,19 @@ private fun Preview() =
onBack = {}, onBack = {},
rows = rows =
listOf( listOf(
BalanceActionRowState( SpendableBalanceRowState(
title = stringRes("Row"), title = stringRes("Row"),
icon = loadingImageRes(), icon = loadingImageRes(),
value = stringRes("Value") value = stringRes("Value")
), ),
BalanceActionRowState( SpendableBalanceRowState(
title = stringRes("Row"), title = stringRes("Row"),
icon = imageRes(R.drawable.ic_balance_shield), icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes("Value") value = stringRes("Value")
) )
), ),
shieldButton = shieldButton =
BalanceShieldButtonState( SpendableBalanceShieldButtonState(
amount = Zatoshi(10000), amount = Zatoshi(10000),
onShieldClick = {} onShieldClick = {}
) )

View File

@ -1,12 +1,17 @@
package co.electriccoin.zcash.ui.screen.balances.action package co.electriccoin.zcash.ui.screen.balances.spendable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.extension.typicalFee
import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.model.WalletAccount import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.repository.isPending
import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.ListTransactionData
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.StringResource import co.electriccoin.zcash.ui.design.util.StringResource
@ -15,35 +20,41 @@ import co.electriccoin.zcash.ui.design.util.loadingImageRes
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class BalanceActionViewModel( class SpendableBalanceViewModel(
accountDataSource: AccountDataSource, accountDataSource: AccountDataSource,
getTransactions: GetTransactionsUseCase,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val shieldFunds: ShieldFundsUseCase, private val shieldFunds: ShieldFundsUseCase,
) : ViewModel() { ) : ViewModel() {
val state = val state =
accountDataSource.selectedAccount combine(
.mapNotNull { accountDataSource.selectedAccount,
createState(it) getTransactions.observe()
}.stateIn( ) { account, transactions ->
createState(account, transactions)
}.filterNotNull()
.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = initialValue =
createState( createState(
account =
accountDataSource.allAccounts.value accountDataSource.allAccounts.value
.orEmpty() .orEmpty()
.firstOrNull { it.isSelected } .firstOrNull { it.isSelected },
transactions = null
) )
) )
private fun createState(account: WalletAccount?): BalanceActionState? { private fun createState(account: WalletAccount?, transactions: List<ListTransactionData>?): SpendableBalanceState? {
if (account == null) return null if (account == null) return null
return SpendableBalanceState(
return BalanceActionState(
title = stringRes(R.string.balance_action_title), title = stringRes(R.string.balance_action_title),
message = createMessage(account), message = createMessage(account, transactions),
positive = createPositiveButton(account), positive = createPositiveButton(account),
onBack = ::onBack, onBack = ::onBack,
rows = createInfoRows(account), rows = createInfoRows(account),
@ -51,19 +62,26 @@ class BalanceActionViewModel(
) )
} }
private fun createMessage(account: WalletAccount): StringResource { private fun createMessage(account: WalletAccount, transactions: List<ListTransactionData>?): StringResource {
val pending = val pending =
when { when {
account.totalShieldedBalance == account.spendableShieldedBalance -> account.isAllShielded -> stringRes(R.string.balance_action_all_shielded)
stringRes(R.string.balance_action_all_shielded)
account.totalShieldedBalance > account.spendableShieldedBalance -> account.totalBalance > account.spendableShieldedBalance ->
if (transactions.orEmpty().any { it.transaction.isPending }) {
stringRes(R.string.balance_action_pending) stringRes(R.string.balance_action_pending)
} else {
stringRes(R.string.balance_action_syncing)
}
else -> null else -> null
} }
val shielding = stringRes(R.string.balance_action_shield_message).takeIf { account.isShieldingAvailable } val shielding =
stringRes(
R.string.balance_action_shield_message,
stringRes(Zatoshi.typicalFee)
).takeIf { account.isShieldingAvailable }
return if (pending != null && shielding != null) { return if (pending != null && shielding != null) {
pending + stringRes("\n\n") + shielding pending + stringRes("\n\n") + shielding
@ -85,21 +103,21 @@ class BalanceActionViewModel(
private fun createInfoRows(account: WalletAccount) = private fun createInfoRows(account: WalletAccount) =
listOfNotNull( listOfNotNull(
BalanceActionRowState( SpendableBalanceRowState(
title = stringRes(R.string.balance_action_info_shielded), title = stringRes(R.string.balance_action_info_shielded),
icon = imageRes(R.drawable.ic_balance_shield), icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance)) value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance))
), ),
when { when {
account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending -> account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending ->
BalanceActionRowState( SpendableBalanceRowState(
title = stringRes(R.string.balance_action_info_pending), title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(), icon = loadingImageRes(),
value = stringRes(R.string.general_zec, stringRes(account.pendingShieldedBalance)) value = stringRes(R.string.general_zec, stringRes(account.pendingShieldedBalance))
) )
account.totalShieldedBalance > account.spendableShieldedBalance -> account.totalShieldedBalance > account.spendableShieldedBalance ->
BalanceActionRowState( SpendableBalanceRowState(
title = stringRes(R.string.balance_action_info_pending), title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(), icon = loadingImageRes(),
value = value =
@ -113,8 +131,8 @@ class BalanceActionViewModel(
}, },
) )
private fun createShieldButtonState(account: WalletAccount): BalanceShieldButtonState? = private fun createShieldButtonState(account: WalletAccount): SpendableBalanceShieldButtonState? =
BalanceShieldButtonState( SpendableBalanceShieldButtonState(
amount = account.transparent.balance, amount = account.transparent.balance,
onShieldClick = ::onShieldClick onShieldClick = ::onShieldClick
).takeIf { account.isShieldingAvailable } ).takeIf { account.isShieldingAvailable }

View File

@ -7,11 +7,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState import androidx.compose.material3.SheetState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.NavigationRouter import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.Spacer import co.electriccoin.zcash.ui.design.component.Spacer
@ -24,28 +25,28 @@ import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.compose.koinInject import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AndroidWalletRestoringInfo() { fun AndroidWalletRestoringInfo() {
val navigationRouter = koinInject<NavigationRouter>() val viewModel = koinViewModel<WalletRestoringInfoViewModel>()
Content( val state by viewModel.state.collectAsStateWithLifecycle()
onBack = { navigationRouter.back() } Content(state)
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun Content( private fun Content(
onBack: () -> Unit, state: WalletRestoringInfoState,
sheetState: SheetState = rememberScreenModalBottomSheetState(), sheetState: SheetState = rememberScreenModalBottomSheetState(),
) { ) {
ZashiScreenModalBottomSheet( ZashiScreenModalBottomSheet(
sheetState = sheetState, sheetState = sheetState,
onDismissRequest = onBack onDismissRequest = state.onBack
) { ) {
Column( Column(
modifier = modifier =
@ -70,17 +71,17 @@ private fun Content(
stringResource(R.string.home_info_restoring_bullet_2), stringResource(R.string.home_info_restoring_bullet_2),
color = ZashiColors.Text.textTertiary color = ZashiColors.Text.textTertiary
) )
state.info?.let {
Spacer(32.dp) Spacer(32.dp)
ZashiInfoText( ZashiInfoText(it.getValue())
stringResource(R.string.home_info_restoring_note), }
)
Spacer(24.dp) Spacer(24.dp)
ZashiButton( ZashiButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state =
ButtonState( ButtonState(
text = stringRes(R.string.general_ok), text = stringRes(R.string.general_ok),
onClick = onBack onClick = state.onBack
) )
) )
} }
@ -93,7 +94,11 @@ private fun Content(
private fun Preview() = private fun Preview() =
ZcashTheme { ZcashTheme {
Content( Content(
state =
WalletRestoringInfoState(
onBack = {}, onBack = {},
info = stringRes(R.string.home_info_restoring_message)
)
) )
} }

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.ui.screen.home.restoring
import androidx.compose.runtime.Immutable
import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState
import co.electriccoin.zcash.ui.design.util.StringResource
@Immutable
data class WalletRestoringInfoState(
val info: StringResource?,
override val onBack: () -> Unit
) : ModalBottomSheetState

View File

@ -0,0 +1,37 @@
package co.electriccoin.zcash.ui.screen.home.restoring
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSource
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.util.stringRes
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class WalletRestoringInfoViewModel(
walletSnapshotDataSource: WalletSnapshotDataSource,
private val navigationRouter: NavigationRouter
) : ViewModel() {
val state =
walletSnapshotDataSource
.observe()
.map { createState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = createState(walletSnapshotDataSource.observe().value)
)
private fun createState(snapshot: WalletSnapshot?) =
WalletRestoringInfoState(
onBack = ::onBack,
info = stringRes(R.string.home_info_restoring_note).takeIf { snapshot?.isSpendable == false }
)
private fun onBack() = navigationRouter.back()
}

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.extension.typicalFee
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.Spacer import co.electriccoin.zcash.ui.design.component.Spacer
@ -72,7 +73,10 @@ private fun Content(state: ShieldFundsInfoState) {
) )
Spacer(24.dp) Spacer(24.dp)
Text( Text(
stringResource(R.string.home_info_transparent_message), stringRes(
R.string.home_info_transparent_message,
stringRes(Zatoshi.typicalFee)
).getValue(),
color = ZashiColors.Text.textTertiary, color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textMd style = ZashiTypography.textMd
) )

View File

@ -68,7 +68,8 @@ fun ReceiveShielded(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state =
TransactionDetailInfoRowState( TransactionDetailInfoRowState(
title = if (state.isPending) { title =
if (state.isPending) {
stringRes(R.string.transaction_detail_info_transaction_status) stringRes(R.string.transaction_detail_info_transaction_status)
} else { } else {
stringRes(R.string.transaction_detail_info_transaction_completed) stringRes(R.string.transaction_detail_info_transaction_completed)

View File

@ -54,7 +54,8 @@ fun ReceiveTransparent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state =
TransactionDetailInfoRowState( TransactionDetailInfoRowState(
title = if (state.isPending) { title =
if (state.isPending) {
stringRes(R.string.transaction_detail_info_transaction_status) stringRes(R.string.transaction_detail_info_transaction_status)
} else { } else {
stringRes(R.string.transaction_detail_info_transaction_completed) stringRes(R.string.transaction_detail_info_transaction_completed)

View File

@ -118,7 +118,8 @@ fun SendShielded(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state =
TransactionDetailInfoRowState( TransactionDetailInfoRowState(
title = if (state.isPending) { title =
if (state.isPending) {
stringRes(R.string.transaction_detail_info_transaction_status) stringRes(R.string.transaction_detail_info_transaction_status)
} else { } else {
stringRes(R.string.transaction_detail_info_transaction_completed) stringRes(R.string.transaction_detail_info_transaction_completed)

View File

@ -115,7 +115,8 @@ fun SendTransparent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state =
TransactionDetailInfoRowState( TransactionDetailInfoRowState(
title = if (state.isPending) { title =
if (state.isPending) {
stringRes(R.string.transaction_detail_info_transaction_status) stringRes(R.string.transaction_detail_info_transaction_status)
} else { } else {
stringRes(R.string.transaction_detail_info_transaction_completed) stringRes(R.string.transaction_detail_info_transaction_completed)

View File

@ -54,7 +54,8 @@ fun Shielding(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state =
TransactionDetailInfoRowState( TransactionDetailInfoRowState(
title = if (state.isPending) { title =
if (state.isPending) {
stringRes(R.string.transaction_detail_info_transaction_status) stringRes(R.string.transaction_detail_info_transaction_status)
} else { } else {
stringRes(R.string.transaction_detail_info_transaction_completed) stringRes(R.string.transaction_detail_info_transaction_completed)

View File

@ -10,7 +10,7 @@ import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
import co.electriccoin.zcash.ui.common.mapper.TransactionHistoryMapper import co.electriccoin.zcash.ui.common.mapper.TransactionHistoryMapper
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.repository.Transaction import co.electriccoin.zcash.ui.common.repository.Transaction
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class TransactionHistoryWidgetViewModel( class TransactionHistoryWidgetViewModel(
getCurrentTransactions: GetCurrentTransactionsUseCase, getTransactions: GetTransactionsUseCase,
getWalletRestoringState: GetWalletRestoringStateUseCase, getWalletRestoringState: GetWalletRestoringStateUseCase,
private val transactionHistoryMapper: TransactionHistoryMapper, private val transactionHistoryMapper: TransactionHistoryMapper,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
@ -31,7 +31,7 @@ class TransactionHistoryWidgetViewModel(
) : ViewModel() { ) : ViewModel() {
val state = val state =
combine( combine(
getCurrentTransactions.observe(), getTransactions.observe(),
getWalletRestoringState.observe(), getWalletRestoringState.observe(),
) { transactions, restoringState -> ) { transactions, restoringState ->
when { when {

View File

@ -43,7 +43,7 @@
<string name="home_info_transparent_subtitle">To protect user privacy, Zashi doesn\'t support spending transparent <string name="home_info_transparent_subtitle">To protect user privacy, Zashi doesn\'t support spending transparent
ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. </string> ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. </string>
<string name="home_info_transparent_message">This will create a shielding in-wallet transaction, consolidating your <string name="home_info_transparent_message">This will create a shielding in-wallet transaction, consolidating your
transparent and shielded funds. (Typical fee: .001 ZEC)</string> transparent and shielded funds. (Typical fee: %s ZEC)</string>
<string name="home_info_transparent_subheader">Transparent</string> <string name="home_info_transparent_subheader">Transparent</string>
<string name="home_info_disconnected_title">Wallet Disconnected</string> <string name="home_info_disconnected_title">Wallet Disconnected</string>
<string name="home_info_disconnected_subtitle">You appear to be offline. Please restore your internet connection <string name="home_info_disconnected_subtitle">You appear to be offline. Please restore your internet connection
@ -74,7 +74,8 @@
<string name="balance_action_title">Spendable Balance</string> <string name="balance_action_title">Spendable Balance</string>
<string name="balance_action_all_shielded">All your funds are shielded and spendable.</string> <string name="balance_action_all_shielded">All your funds are shielded and spendable.</string>
<string name="balance_action_pending">Pending transactions are getting mined and confirmed.</string> <string name="balance_action_pending">Pending transactions are getting mined and confirmed.</string>
<string name="balance_action_shield_message">Shield your transparent ZEC to make it spendable and private. Doing so will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)</string> <string name="balance_action_syncing">Zashi is scanning the blockchain to make sure your balances are displayed correctly.</string>
<string name="balance_action_info_shielded">Shielded ZEC (Spendable)</string> <string name="balance_action_shield_message">Shield your transparent ZEC to make it spendable and private. Doing so will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: %s ZEC)</string>
<string name="balance_action_info_shielded">Shielded (Spendable)</string>
<string name="balance_action_info_pending">Pending</string> <string name="balance_action_info_pending">Pending</string>
</resources> </resources>

View File

@ -43,7 +43,7 @@
<string name="home_info_transparent_subtitle">To protect user privacy, Zashi doesn\'t support spending transparent <string name="home_info_transparent_subtitle">To protect user privacy, Zashi doesn\'t support spending transparent
ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. </string> ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. </string>
<string name="home_info_transparent_message">This will create a shielding in-wallet transaction, consolidating your <string name="home_info_transparent_message">This will create a shielding in-wallet transaction, consolidating your
transparent and shielded funds. (Typical fee: .001 ZEC)</string> transparent and shielded funds. (Typical fee: %s ZEC)</string>
<string name="home_info_transparent_subheader">Transparent</string> <string name="home_info_transparent_subheader">Transparent</string>
<string name="home_info_disconnected_title">Wallet Disconnected</string> <string name="home_info_disconnected_title">Wallet Disconnected</string>
<string name="home_info_disconnected_subtitle">You appear to be offline. Please restore your internet connection <string name="home_info_disconnected_subtitle">You appear to be offline. Please restore your internet connection
@ -74,7 +74,8 @@
<string name="balance_action_title">Spendable Balance</string> <string name="balance_action_title">Spendable Balance</string>
<string name="balance_action_all_shielded">All your funds are shielded and spendable.</string> <string name="balance_action_all_shielded">All your funds are shielded and spendable.</string>
<string name="balance_action_pending">Pending transactions are getting mined and confirmed.</string> <string name="balance_action_pending">Pending transactions are getting mined and confirmed.</string>
<string name="balance_action_shield_message">Shield your transparent ZEC to make it spendable and private. Doing so will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)</string> <string name="balance_action_syncing">Zashi is scanning the blockchain to make sure your balances are displayed correctly.</string>
<string name="balance_action_info_shielded">Shielded ZEC (Spendable)</string> <string name="balance_action_shield_message">Shield your transparent ZEC to make it spendable and private. Doing so will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: %s ZEC)</string>
<string name="balance_action_info_shielded">Shielded (Spendable)</string>
<string name="balance_action_info_pending">Pending</string> <string name="balance_action_info_pending">Pending</string>
</resources> </resources>