[#1449] Display Synchronizer details in dialog

- Closes #1449
- Synchronizer status details are now available to users by pressing the simple status view. The details are displayed within the predefined Zashi dialog on the Balances and Account screens
- As this view also presents information about Zashi app updates available on Google Play, by pressing the view, the app redirects users to Google Play Zashi’s page
- As agreed, we’re moving towards more rounded corners in dialogs, which is part of these changes, too
- Added also several minor Balances screen UI improvements
- Improved biometric flow without any authentication method set on older Android versions
- Changelog update
This commit is contained in:
Honza Rychnovský 2024-05-24 10:41:57 +02:00 committed by GitHub
parent 00db536674
commit 7e9e89725b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 368 additions and 133 deletions

View File

@ -14,6 +14,14 @@ directly impact users rather than highlighting other key architectural updates.*
cases: Send funds, Recovery Phrase, Export Private Data, and Delete Wallet. cases: Send funds, Recovery Phrase, Export Private Data, and Delete Wallet.
- The app entry animation has been reworked to apply on every app access point, i.e. it will be displayed when - The app entry animation has been reworked to apply on every app access point, i.e. it will be displayed when
users return to an already set up app as well. users return to an already set up app as well.
- Synchronizer status details are now available to users by pressing the simple status view placed above the
synchronization progress bar. The details are displayed within a dialog window on the Balances and Account screens.
This view also occasionally presents information about a possible Zashi app update available on Google Play. The
app redirects users to the Google Play Zashi page by pressing the view.
### Changed
- The app dialog window has now a bit more rounded corners
- A few more minor UI improvements
## [1.0 (650)] - 2024-05-07 ## [1.0 (650)] - 2024-05-07

View File

@ -1,12 +1,13 @@
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -61,7 +62,7 @@ fun AppAlertDialog(
properties: DialogProperties = DialogProperties() properties: DialogProperties = DialogProperties()
) { ) {
AlertDialog( AlertDialog(
shape = RectangleShape, shape = RoundedCornerShape(corner = CornerSize(ZcashTheme.dimens.regularRippleEffectCorner)),
onDismissRequest = onDismissRequest?.let { onDismissRequest } ?: {}, onDismissRequest = onDismissRequest?.let { onDismissRequest } ?: {},
confirmButton = { confirmButton = {
confirmButtonText?.let { confirmButtonText?.let {

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
data class Dimens( data class Dimens(
// Default spacings: // Default spacings:
val spacingNone: Dp, val spacingNone: Dp,
val spacingMini: Dp,
val spacingXtiny: Dp, val spacingXtiny: Dp,
val spacingMin: Dp, val spacingMin: Dp,
val spacingTiny: Dp, val spacingTiny: Dp,
@ -61,6 +62,7 @@ data class Dimens(
private val defaultDimens = private val defaultDimens =
Dimens( Dimens(
spacingNone = 0.dp, spacingNone = 0.dp,
spacingMini = 1.dp,
spacingXtiny = 2.dp, spacingXtiny = 2.dp,
spacingTiny = 4.dp, spacingTiny = 4.dp,
spacingMin = 6.dp, spacingMin = 6.dp,

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.account package co.electriccoin.zcash.ui.screen.account
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
@ -70,6 +71,10 @@ class AccountTestSetup(
onTransactionItemAction = { onTransactionItemAction = {
onItemClickCount.incrementAndGet() onItemClickCount.incrementAndGet()
}, },
hideStatusDialog = {},
showStatusDialog = null,
onStatusClick = {},
snackbarHostState = SnackbarHostState(),
walletRestoringState = WalletRestoringState.NONE, walletRestoringState = WalletRestoringState.NONE,
walletSnapshot = WalletSnapshotFixture.new() walletSnapshot = WalletSnapshotFixture.new()
) )

View File

@ -30,6 +30,7 @@ class HistoryTestSetup(
ZcashTheme { ZcashTheme {
HistoryContainer( HistoryContainer(
transactionState = initialHistoryUiState, transactionState = initialHistoryUiState,
onStatusClick = {},
onTransactionItemAction = { onTransactionItemAction = {
onItemIdClickCount.incrementAndGet() onItemIdClickCount.incrementAndGet()
}, },

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.balances package co.electriccoin.zcash.ui.screen.balances
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
@ -35,7 +36,10 @@ class BalancesTestSetup(
onSettings = { onSettings = {
onSettingsCount.incrementAndGet() onSettingsCount.incrementAndGet()
}, },
isDetailedStatus = false, hideStatusDialog = {},
showStatusDialog = null,
onStatusClick = {},
snackbarHostState = SnackbarHostState(),
isFiatConversionEnabled = isShowFiatConversion, isFiatConversionEnabled = isShowFiatConversion,
isUpdateAvailable = false, isUpdateAvailable = false,
isShowingErrorDialog = false, isShowingErrorDialog = false,

View File

@ -31,8 +31,7 @@ class WalletDisplayValuesTest {
WalletDisplayValues.getNextValues( WalletDisplayValues.getNextValues(
context = getAppContext(), context = getAppContext(),
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = false, isUpdateAvailable = false
isDetailedStatus = false
) )
assertNotNull(values) assertNotNull(values)

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.update.util
import android.content.Intent import android.content.Intent
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.test.getAppContext import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.util.PlayStoreUtil
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Test import org.junit.Test

View File

@ -80,6 +80,7 @@ sealed class BalanceState(open val totalBalance: Zatoshi) {
} }
@Composable @Composable
@Suppress("LongMethod")
fun BalanceWidget( fun BalanceWidget(
balanceState: BalanceState, balanceState: BalanceState,
isReferenceToBalances: Boolean, isReferenceToBalances: Boolean,
@ -104,11 +105,22 @@ fun BalanceWidget(
text = stringResource(id = co.electriccoin.zcash.ui.R.string.balance_widget_available), text = stringResource(id = co.electriccoin.zcash.ui.R.string.balance_widget_available),
onClick = onReferenceClick, onClick = onReferenceClick,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingTiny) modifier =
Modifier
.padding(
vertical = ZcashTheme.dimens.spacingSmall,
horizontal = ZcashTheme.dimens.spacingMini,
)
) )
} else { } else {
Body( Body(
text = stringResource(id = co.electriccoin.zcash.ui.R.string.balance_widget_available), text = stringResource(id = co.electriccoin.zcash.ui.R.string.balance_widget_available),
modifier =
Modifier
.padding(
vertical = ZcashTheme.dimens.spacingSmall,
horizontal = ZcashTheme.dimens.spacingMini,
)
) )
} }

View File

@ -1,13 +1,19 @@
package co.electriccoin.zcash.ui.common.compose package co.electriccoin.zcash.ui.common.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -18,11 +24,13 @@ import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.sdk.extension.toPercentageWithDecimal import cash.z.ecc.sdk.extension.toPercentageWithDecimal
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.BodySmall import co.electriccoin.zcash.ui.design.component.BodySmall
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.SmallLinearProgressIndicator import co.electriccoin.zcash.ui.design.component.SmallLinearProgressIndicator
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
@Preview(device = Devices.PIXEL_4_XL) @Preview(device = Devices.PIXEL_4_XL)
@ -34,8 +42,8 @@ private fun BalanceWidgetPreview() {
) { ) {
SynchronizationStatus( SynchronizationStatus(
isUpdateAvailable = false, isUpdateAvailable = false,
isDetailedStatus = false, onStatusClick = {},
walletSnapshot = WalletSnapshotFixture.new() walletSnapshot = WalletSnapshotFixture.new(),
) )
} }
} }
@ -44,7 +52,7 @@ private fun BalanceWidgetPreview() {
@Composable @Composable
fun SynchronizationStatus( fun SynchronizationStatus(
isUpdateAvailable: Boolean, isUpdateAvailable: Boolean,
isDetailedStatus: Boolean, onStatusClick: (StatusAction) -> Unit,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
testTag: String? = null, testTag: String? = null,
@ -54,7 +62,6 @@ fun SynchronizationStatus(
context = LocalContext.current, context = LocalContext.current,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
isDetailedStatus = isDetailedStatus
) )
Column( Column(
@ -64,11 +71,14 @@ fun SynchronizationStatus(
if (walletDisplayValues.statusText.isNotEmpty()) { if (walletDisplayValues.statusText.isNotEmpty()) {
BodySmall( BodySmall(
text = walletDisplayValues.statusText, text = walletDisplayValues.statusText,
modifier = testTag?.let { Modifier.testTag(testTag) } ?: Modifier, textAlign = TextAlign.Center,
textAlign = TextAlign.Center modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onStatusClick(walletDisplayValues.statusAction) }
.padding(all = ZcashTheme.dimens.spacingSmall)
.then(testTag?.let { Modifier.testTag(testTag) } ?: Modifier),
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
} }
BodySmall( BodySmall(
@ -86,8 +96,27 @@ fun SynchronizationStatus(
progress = walletSnapshot.progress.decimal, progress = walletSnapshot.progress.decimal,
modifier = modifier =
Modifier.padding( Modifier.padding(
horizontal = ZcashTheme.dimens.spacingUpLarge horizontal = ZcashTheme.dimens.spacingDefault
) )
) )
} }
} }
@Composable
fun StatusDialog(
statusAction: StatusAction.Detailed,
onDone: () -> Unit
) {
AppAlertDialog(
title = stringResource(id = R.string.balances_status_error_dialog_title),
text = {
Column(
Modifier.verticalScroll(rememberScrollState())
) {
Text(text = statusAction.details)
}
},
confirmButtonText = stringResource(id = R.string.balances_status_dialog_button),
onConfirmButtonClick = onDone
)
}

View File

@ -185,8 +185,6 @@ class AuthenticationViewModel(
// Biometric authentication is disabled until the user unlocks with their device credential // Biometric authentication is disabled until the user unlocks with their device credential
// (i.e. PIN, pattern, or password). // (i.e. PIN, pattern, or password).
BiometricPrompt.ERROR_LOCKOUT_PERMANENT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT,
// The user does not have any biometrics enrolled
BiometricPrompt.ERROR_NO_BIOMETRICS,
// The device does not have the required authentication hardware // The device does not have the required authentication hardware
BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_NOT_PRESENT,
// The user pressed the negative button // The user pressed the negative button
@ -214,9 +212,12 @@ class AuthenticationViewModel(
// We could consider splitting ERROR_CANCELED from ERROR_USER_CANCELED // We could consider splitting ERROR_CANCELED from ERROR_USER_CANCELED
authenticationResult.value = AuthenticationResult.Canceled authenticationResult.value = AuthenticationResult.Canceled
} }
// The user does not have any biometrics enrolled
BiometricPrompt.ERROR_NO_BIOMETRICS,
// The device does not have pin, pattern, or password set up // The device does not have pin, pattern, or password set up
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> { BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
// Allow unauthenticated access if no authentication method is available on the device // Allow unauthenticated access if no authentication method is available on the device
// These 2 errors can come for a different Android SDK versions, but they mean the same
authenticationResult.value = AuthenticationResult.Success authenticationResult.value = AuthenticationResult.Success
} }
} }
@ -346,7 +347,7 @@ class AuthenticationViewModel(
} }
else -> { else -> {
Twig.error { "Unexpected biometric framework status" } Twig.error { "Unexpected biometric framework status" }
BiometricSupportResult.StatusExpected BiometricSupportResult.StatusUnexpected
} }
} }
} }
@ -411,5 +412,5 @@ private sealed class BiometricSupportResult {
data object StatusUnknown : BiometricSupportResult() data object StatusUnknown : BiometricSupportResult()
data object StatusExpected : BiometricSupportResult() data object StatusUnexpected : BiometricSupportResult()
} }

View File

@ -5,7 +5,10 @@ package co.electriccoin.zcash.ui.screen.account
import android.content.Context import android.content.Context
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
@ -21,6 +24,8 @@ import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.view.Account import co.electriccoin.zcash.ui.screen.account.view.Account
import co.electriccoin.zcash.ui.screen.account.view.TrxItemAction import co.electriccoin.zcash.ui.screen.account.view.TrxItemAction
import co.electriccoin.zcash.ui.screen.account.viewmodel.TransactionHistoryViewModel import co.electriccoin.zcash.ui.screen.account.viewmodel.TransactionHistoryViewModel
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.util.PlayStoreUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
@ -31,8 +36,6 @@ internal fun WrapAccount(
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
) { ) {
val scope = rememberCoroutineScope()
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>() val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>()
@ -56,7 +59,6 @@ internal fun WrapAccount(
context = activity.applicationContext, context = activity.applicationContext,
goBalances = goBalances, goBalances = goBalances,
goSettings = goSettings, goSettings = goSettings,
scope = scope,
synchronizer = synchronizer, synchronizer = synchronizer,
transactionHistoryViewModel = transactionHistoryViewModel, transactionHistoryViewModel = transactionHistoryViewModel,
transactionsUiState = transactionsUiState, transactionsUiState = transactionsUiState,
@ -70,11 +72,10 @@ internal fun WrapAccount(
@Composable @Composable
@VisibleForTesting @VisibleForTesting
@Suppress("LongParameterList") @Suppress("LongParameterList", "LongMethod")
internal fun WrapAccount( internal fun WrapAccount(
balanceState: BalanceState, balanceState: BalanceState,
context: Context, context: Context,
scope: CoroutineScope,
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
transactionsUiState: TransactionUiState, transactionsUiState: TransactionUiState,
@ -83,6 +84,14 @@ internal fun WrapAccount(
walletRestoringState: WalletRestoringState, walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot? walletSnapshot: WalletSnapshot?
) { ) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
// We could also improve this by `rememberSaveable` to preserve the dialog after a configuration change. But the
// dialog dismissing in such cases is not critical, and it would require creating StatusAction custom Saver
val showStatusDialog = remember { mutableStateOf<StatusAction.Detailed?>(null) }
if (null == synchronizer || null == walletSnapshot) { if (null == synchronizer || null == walletSnapshot) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available // TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
@ -92,6 +101,23 @@ internal fun WrapAccount(
Account( Account(
balanceState = balanceState, balanceState = balanceState,
transactionsUiState = transactionsUiState, transactionsUiState = transactionsUiState,
showStatusDialog = showStatusDialog.value,
hideStatusDialog = { showStatusDialog.value = null },
onStatusClick = { status ->
when (status) {
is StatusAction.Detailed -> showStatusDialog.value = status
StatusAction.AppUpdate -> {
openPlayStoreAppSite(
context = context,
snackbarHostState = snackbarHostState,
scope = scope
)
}
else -> {
// No action required
}
}
},
onTransactionItemAction = { action -> onTransactionItemAction = { action ->
when (action) { when (action) {
is TrxItemAction.TransactionIdClick -> { is TrxItemAction.TransactionIdClick -> {
@ -132,8 +158,26 @@ internal fun WrapAccount(
}, },
goBalances = goBalances, goBalances = goBalances,
goSettings = goSettings, goSettings = goSettings,
snackbarHostState = snackbarHostState,
walletRestoringState = walletRestoringState, walletRestoringState = walletRestoringState,
walletSnapshot = walletSnapshot walletSnapshot = walletSnapshot
) )
} }
} }
private fun openPlayStoreAppSite(
context: Context,
snackbarHostState: SnackbarHostState,
scope: CoroutineScope
) {
val storeIntent = PlayStoreUtil.newActivityIntent(context)
runCatching {
context.startActivity(storeIntent)
}.onFailure {
scope.launch {
snackbarHostState.showSnackbar(
message = context.getString(R.string.unable_to_open_play_store)
)
}
}
}

View File

@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -19,6 +21,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceState import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.compose.BalanceWidget import co.electriccoin.zcash.ui.common.compose.BalanceWidget
import co.electriccoin.zcash.ui.common.compose.StatusDialog
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.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.common.test.CommonTag
@ -30,6 +33,7 @@ import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
@Preview("Account No History") @Preview("Account No History")
@Composable @Composable
@ -37,12 +41,16 @@ private fun HistoryLoadingComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Account( Account(
balanceState = BalanceStateFixture.new(),
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
hideStatusDialog = {},
showStatusDialog = null,
onStatusClick = {},
onTransactionItemAction = {}, onTransactionItemAction = {},
snackbarHostState = SnackbarHostState(),
transactionsUiState = TransactionUiState.Loading, transactionsUiState = TransactionUiState.Loading,
walletRestoringState = WalletRestoringState.SYNCING, walletRestoringState = WalletRestoringState.SYNCING,
balanceState = BalanceStateFixture.new(),
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
) )
} }
@ -56,10 +64,14 @@ private fun HistoryListComposablePreview() {
GradientSurface { GradientSurface {
@Suppress("MagicNumber") @Suppress("MagicNumber")
Account( Account(
balanceState = BalanceState.Available(Zatoshi(123_000_000L), Zatoshi(123_000_000L)),
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
balanceState = BalanceState.Available(Zatoshi(123_000_000L), Zatoshi(123_000_000L)), hideStatusDialog = {},
showStatusDialog = null,
onStatusClick = {},
onTransactionItemAction = {}, onTransactionItemAction = {},
snackbarHostState = SnackbarHostState(),
transactionsUiState = TransactionUiState.Done(transactions = TransactionsFixture.new()), transactionsUiState = TransactionUiState.Done(transactions = TransactionsFixture.new()),
walletRestoringState = WalletRestoringState.NONE, walletRestoringState = WalletRestoringState.NONE,
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
@ -74,20 +86,30 @@ internal fun Account(
balanceState: BalanceState, balanceState: BalanceState,
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
hideStatusDialog: () -> Unit,
showStatusDialog: StatusAction.Detailed?,
onStatusClick: (StatusAction) -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit, onTransactionItemAction: (TrxItemAction) -> Unit,
snackbarHostState: SnackbarHostState,
transactionsUiState: TransactionUiState, transactionsUiState: TransactionUiState,
walletRestoringState: WalletRestoringState, walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
) { ) {
Scaffold(topBar = { Scaffold(
AccountTopAppBar( topBar = {
showRestoring = walletRestoringState == WalletRestoringState.RESTORING, AccountTopAppBar(
onSettings = goSettings showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
) onSettings = goSettings
}) { paddingValues -> )
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
) { paddingValues ->
AccountMainContent( AccountMainContent(
balanceState = balanceState, balanceState = balanceState,
goBalances = goBalances, goBalances = goBalances,
onStatusClick = onStatusClick,
onTransactionItemAction = onTransactionItemAction, onTransactionItemAction = onTransactionItemAction,
transactionState = transactionsUiState, transactionState = transactionsUiState,
walletRestoringState = walletRestoringState, walletRestoringState = walletRestoringState,
@ -99,6 +121,14 @@ internal fun Account(
// underlying transaction history composable // underlying transaction history composable
) )
) )
// Show synchronization status popup
if (showStatusDialog != null) {
StatusDialog(
statusAction = showStatusDialog,
onDone = hideStatusDialog
)
}
} }
} }
@ -135,6 +165,7 @@ private fun AccountMainContent(
balanceState: BalanceState, balanceState: BalanceState,
goBalances: () -> Unit, goBalances: () -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit, onTransactionItemAction: (TrxItemAction) -> Unit,
onStatusClick: (StatusAction) -> Unit,
transactionState: TransactionUiState, transactionState: TransactionUiState,
walletRestoringState: WalletRestoringState, walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
@ -157,6 +188,7 @@ private fun AccountMainContent(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
HistoryContainer( HistoryContainer(
onStatusClick = onStatusClick,
onTransactionItemAction = onTransactionItemAction, onTransactionItemAction = onTransactionItemAction,
transactionState = transactionState, transactionState = transactionState,
walletRestoringState = walletRestoringState, walletRestoringState = walletRestoringState,

View File

@ -62,6 +62,7 @@ import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.model.TrxItemState import co.electriccoin.zcash.ui.screen.account.model.TrxItemState
import co.electriccoin.zcash.ui.screen.balances.BalancesTag import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.screen.send.view.DEFAULT_LESS_THAN_FEE import co.electriccoin.zcash.ui.screen.send.view.DEFAULT_LESS_THAN_FEE
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -76,6 +77,7 @@ private fun ComposablePreview() {
GradientSurface { GradientSurface {
HistoryContainer( HistoryContainer(
onTransactionItemAction = {}, onTransactionItemAction = {},
onStatusClick = {},
transactionState = TransactionUiState.Loading, transactionState = TransactionUiState.Loading,
walletRestoringState = WalletRestoringState.SYNCING, walletRestoringState = WalletRestoringState.SYNCING,
walletSnapshot = WalletSnapshotFixture.new() walletSnapshot = WalletSnapshotFixture.new()
@ -92,6 +94,7 @@ private fun ComposableHistoryListPreview() {
HistoryContainer( HistoryContainer(
transactionState = TransactionUiState.Done(transactions = TransactionsFixture.new()), transactionState = TransactionUiState.Done(transactions = TransactionsFixture.new()),
onTransactionItemAction = {}, onTransactionItemAction = {},
onStatusClick = {},
walletRestoringState = WalletRestoringState.RESTORING, walletRestoringState = WalletRestoringState.RESTORING,
walletSnapshot = WalletSnapshotFixture.new() walletSnapshot = WalletSnapshotFixture.new()
) )
@ -107,7 +110,9 @@ private val dateFormat: DateFormat by lazy {
} }
@Composable @Composable
@Suppress("LongParameterList")
internal fun HistoryContainer( internal fun HistoryContainer(
onStatusClick: (StatusAction) -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit, onTransactionItemAction: (TrxItemAction) -> Unit,
transactionState: TransactionUiState, transactionState: TransactionUiState,
walletRestoringState: WalletRestoringState, walletRestoringState: WalletRestoringState,
@ -127,15 +132,19 @@ internal fun HistoryContainer(
Column( Column(
modifier = Modifier.background(color = ZcashTheme.colors.historySyncingColor) modifier = Modifier.background(color = ZcashTheme.colors.historySyncingColor)
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
// Do not show the app update information and the detailed sync status in the restoring status // Do not calculate and use the app update information here, as the sync bar won't be displayed after
// on Account screen // the wallet is fully restored
SynchronizationStatus( SynchronizationStatus(
isDetailedStatus = false,
isUpdateAvailable = false, isUpdateAvailable = false,
onStatusClick = onStatusClick,
testTag = BalancesTag.STATUS, testTag = BalancesTag.STATUS,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
modifier =
Modifier
.padding(horizontal = ZcashTheme.dimens.spacingDefault)
.animateContentSize()
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))

View File

@ -2,11 +2,14 @@
package co.electriccoin.zcash.ui.screen.balances package co.electriccoin.zcash.ui.screen.balances
import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -26,11 +29,14 @@ import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.screen.balances.view.Balances import co.electriccoin.zcash.ui.screen.balances.view.Balances
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.util.PlayStoreUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
@ -38,7 +44,6 @@ import org.jetbrains.annotations.VisibleForTesting
@Composable @Composable
internal fun WrapBalances( internal fun WrapBalances(
activity: ComponentActivity, activity: ComponentActivity,
isDetailedSyncStatus: Boolean,
goSettings: () -> Unit, goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
) { ) {
@ -69,7 +74,6 @@ internal fun WrapBalances(
checkUpdateViewModel = checkUpdateViewModel, checkUpdateViewModel = checkUpdateViewModel,
goSettings = goSettings, goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure, goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
isDetailedSyncStatus = isDetailedSyncStatus,
spendingKey = spendingKey, spendingKey = spendingKey,
synchronizer = synchronizer, synchronizer = synchronizer,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
@ -81,14 +85,14 @@ const val DEFAULT_SHIELDING_THRESHOLD = 100000L
@Composable @Composable
@VisibleForTesting @VisibleForTesting
@Suppress("LongParameterList", "LongMethod") // This function should be refactored into smaller chunks
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
internal fun WrapBalances( internal fun WrapBalances(
balanceState: BalanceState, balanceState: BalanceState,
checkUpdateViewModel: CheckUpdateViewModel, checkUpdateViewModel: CheckUpdateViewModel,
createTransactionsViewModel: CreateTransactionsViewModel, createTransactionsViewModel: CreateTransactionsViewModel,
goSettings: () -> Unit, goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
isDetailedSyncStatus: Boolean,
spendingKey: UnifiedSpendingKey?, spendingKey: UnifiedSpendingKey?,
synchronizer: Synchronizer?, synchronizer: Synchronizer?,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
@ -98,6 +102,8 @@ internal fun WrapBalances(
val context = LocalContext.current val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() }
// To show information about the app update, if available // To show information about the app update, if available
val isUpdateAvailable = val isUpdateAvailable =
checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let { checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let {
@ -130,6 +136,10 @@ internal fun WrapBalances(
setShowErrorDialog(true) setShowErrorDialog(true)
} }
// We could also improve this by `rememberSaveable` to preserve the dialog after a configuration change. But the
// dialog dismissing in such cases is not critical, and it would require creating StatusAction custom Saver
val showStatusDialog = remember { mutableStateOf<StatusAction.Detailed?>(null) }
if (null == synchronizer || null == walletSnapshot || null == spendingKey) { if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available // TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
@ -140,10 +150,12 @@ internal fun WrapBalances(
balanceState = balanceState, balanceState = balanceState,
isFiatConversionEnabled = isFiatConversionEnabled, isFiatConversionEnabled = isFiatConversionEnabled,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
isShowingErrorDialog = isShowingErrorDialog,
isDetailedStatus = isDetailedSyncStatus,
onSettings = goSettings, onSettings = goSettings,
isShowingErrorDialog = isShowingErrorDialog,
setShowErrorDialog = setShowErrorDialog, setShowErrorDialog = setShowErrorDialog,
showStatusDialog = showStatusDialog.value,
hideStatusDialog = { showStatusDialog.value = null },
snackbarHostState = snackbarHostState,
onShielding = { onShielding = {
scope.launch { scope.launch {
setShieldState(ShieldState.Running) setShieldState(ShieldState.Running)
@ -201,6 +213,21 @@ internal fun WrapBalances(
} }
} }
}, },
onStatusClick = { status ->
when (status) {
is StatusAction.Detailed -> showStatusDialog.value = status
StatusAction.AppUpdate -> {
openPlayStoreAppSite(
context = context,
snackbarHostState = snackbarHostState,
scope = scope
)
}
else -> {
// No action required
}
}
},
shieldState = shieldState, shieldState = shieldState,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
walletRestoringState = walletRestoringState, walletRestoringState = walletRestoringState,
@ -208,7 +235,7 @@ internal fun WrapBalances(
} }
} }
fun updateTransparentBalanceState( private fun updateTransparentBalanceState(
currentShieldState: ShieldState, currentShieldState: ShieldState,
walletSnapshot: WalletSnapshot? walletSnapshot: WalletSnapshot?
): ShieldState { ): ShieldState {
@ -219,3 +246,20 @@ fun updateTransparentBalanceState(
else -> currentShieldState else -> currentShieldState
} }
} }
private fun openPlayStoreAppSite(
context: Context,
snackbarHostState: SnackbarHostState,
scope: CoroutineScope
) {
val storeIntent = PlayStoreUtil.newActivityIntent(context)
runCatching {
context.startActivity(storeIntent)
}.onFailure {
scope.launch {
snackbarHostState.showSnackbar(
message = context.getString(R.string.unable_to_open_play_store)
)
}
}
}

View File

@ -18,20 +18,22 @@ data class WalletDisplayValues(
val progress: PercentDecimal, val progress: PercentDecimal,
val zecAmountText: String, val zecAmountText: String,
val statusText: String, val statusText: String,
val statusAction: StatusAction = StatusAction.None,
val fiatCurrencyAmountState: FiatCurrencyConversionRateState, val fiatCurrencyAmountState: FiatCurrencyConversionRateState,
val fiatCurrencyAmountText: String val fiatCurrencyAmountText: String
) { ) {
companion object { companion object {
@Suppress("MagicNumber", "LongMethod") @Suppress("LongMethod")
internal fun getNextValues( internal fun getNextValues(
context: Context, context: Context,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isUpdateAvailable: Boolean = false, isUpdateAvailable: Boolean = false,
isDetailedStatus: Boolean = false,
): WalletDisplayValues { ): WalletDisplayValues {
var progress = PercentDecimal.ZERO_PERCENT var progress = PercentDecimal.ZERO_PERCENT
val zecAmountText = walletSnapshot.totalBalance().toZecString() val zecAmountText = walletSnapshot.totalBalance().toZecString()
var statusText = "" var statusText = ""
var statusAction: StatusAction = StatusAction.None
// TODO [#578]: Provide Zatoshi -> USD fiat currency formatting // TODO [#578]: Provide Zatoshi -> USD fiat currency formatting
// TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578 // TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578
// We'll ideally provide a "fresh" currencyConversion object here // We'll ideally provide a "fresh" currencyConversion object here
@ -55,63 +57,63 @@ data class WalletDisplayValues(
) )
} }
statusText = context.getString(R.string.balances_status_syncing) statusText = context.getString(R.string.balances_status_syncing)
statusAction = StatusAction.Syncing
} }
Synchronizer.Status.SYNCED -> { Synchronizer.Status.SYNCED -> {
statusText = if (isUpdateAvailable) {
if (isUpdateAvailable) { statusText =
context.getString( context.getString(
R.string.balances_status_update, R.string.balances_status_update,
context.getString(R.string.app_name) context.getString(R.string.app_name)
) )
} else { statusAction = StatusAction.AppUpdate
context.getString(R.string.balances_status_synced) } else {
} statusText = context.getString(R.string.balances_status_synced)
statusAction = StatusAction.Synced
}
} }
Synchronizer.Status.DISCONNECTED -> { Synchronizer.Status.DISCONNECTED -> {
if (isDetailedStatus) {
statusText =
context.getString(
R.string.balances_status_error_detailed,
context.getString(R.string.balances_status_error_detailed_connection)
)
} else {
statusText =
context.getString(
R.string.balances_status_error_simple,
context.getString(R.string.app_name)
)
}
}
Synchronizer.Status.STOPPED -> {
if (isDetailedStatus) {
statusText = context.getString(R.string.balances_status_detailed_stopped)
} else {
statusText = context.getString(R.string.balances_status_syncing)
}
}
}
// More detailed error message
walletSnapshot.synchronizerError?.let {
if (isDetailedStatus) {
statusText =
context.getString(
R.string.balances_status_error_detailed,
walletSnapshot.synchronizerError.getCauseMessage()
?: context.getString(R.string.balances_status_error_detailed_unknown)
)
} else {
statusText = statusText =
context.getString( context.getString(
R.string.balances_status_error_simple, R.string.balances_status_error_simple,
context.getString(R.string.app_name) context.getString(R.string.app_name)
) )
statusAction =
StatusAction.Disconnected(
details = context.getString(R.string.balances_status_error_dialog_connection)
)
} }
Synchronizer.Status.STOPPED -> {
statusText = context.getString(R.string.balances_status_syncing)
statusAction =
StatusAction.Stopped(
details = context.getString(R.string.balances_status_dialog_stopped)
)
}
}
// More detailed error message
walletSnapshot.synchronizerError?.let {
statusText =
context.getString(
R.string.balances_status_error_simple,
context.getString(R.string.app_name)
)
statusAction =
StatusAction.Error(
details =
context.getString(
R.string.balances_status_error_dialog_cause,
walletSnapshot.synchronizerError.getCauseMessage()
?: context.getString(R.string.balances_status_error_dialog_unknown)
)
)
} }
return WalletDisplayValues( return WalletDisplayValues(
progress = progress, progress = progress,
zecAmountText = zecAmountText, zecAmountText = zecAmountText,
statusAction = statusAction,
statusText = statusText, statusText = statusText,
fiatCurrencyAmountState = fiatCurrencyAmountState, fiatCurrencyAmountState = fiatCurrencyAmountState,
fiatCurrencyAmountText = fiatCurrencyAmountText fiatCurrencyAmountText = fiatCurrencyAmountText
@ -120,6 +122,24 @@ data class WalletDisplayValues(
} }
} }
sealed class StatusAction {
data object None : StatusAction()
data object Syncing : StatusAction()
data object Synced : StatusAction()
data object AppUpdate : StatusAction()
sealed class Detailed(open val details: String) : StatusAction()
data class Disconnected(override val details: String) : Detailed(details)
data class Stopped(override val details: String) : Detailed(details)
data class Error(override val details: String) : Detailed(details)
}
private fun getFiatCurrencyRateValue( private fun getFiatCurrencyRateValue(
context: Context, context: Context,
fiatCurrencyAmountState: FiatCurrencyConversionRateState fiatCurrencyAmountState: FiatCurrencyConversionRateState

View File

@ -26,6 +26,8 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
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.runtime.getValue
@ -55,6 +57,7 @@ import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceState import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.compose.BalanceWidget import co.electriccoin.zcash.ui.common.compose.BalanceWidget
import co.electriccoin.zcash.ui.common.compose.StatusDialog
import co.electriccoin.zcash.ui.common.compose.SynchronizationStatus import co.electriccoin.zcash.ui.common.compose.SynchronizationStatus
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.model.WalletSnapshot
@ -81,6 +84,7 @@ import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.BalancesTag import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
@Preview("Balances") @Preview("Balances")
@ -89,17 +93,20 @@ private fun ComposableBalancesPreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Balances( Balances(
onSettings = {}, balanceState = BalanceStateFixture.new(),
isDetailedStatus = false,
isFiatConversionEnabled = false, isFiatConversionEnabled = false,
isUpdateAvailable = false, isUpdateAvailable = false,
isShowingErrorDialog = false, isShowingErrorDialog = false,
hideStatusDialog = {},
showStatusDialog = null,
setShowErrorDialog = {}, setShowErrorDialog = {},
onSettings = {},
onShielding = {}, onShielding = {},
onStatusClick = {},
shieldState = ShieldState.Available, shieldState = ShieldState.Available,
snackbarHostState = SnackbarHostState(),
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
walletRestoringState = WalletRestoringState.NONE, walletRestoringState = WalletRestoringState.NONE,
balanceState = BalanceStateFixture.new(),
) )
} }
} }
@ -111,17 +118,20 @@ private fun ComposableBalancesShieldFailurePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Balances( Balances(
onSettings = {}, balanceState = BalanceStateFixture.new(),
isDetailedStatus = false,
isFiatConversionEnabled = false, isFiatConversionEnabled = false,
isUpdateAvailable = false, isUpdateAvailable = false,
isShowingErrorDialog = true, isShowingErrorDialog = true,
hideStatusDialog = {},
showStatusDialog = null,
setShowErrorDialog = {}, setShowErrorDialog = {},
onSettings = {},
onShielding = {}, onShielding = {},
onStatusClick = {},
shieldState = ShieldState.Available, shieldState = ShieldState.Available,
snackbarHostState = SnackbarHostState(),
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
walletRestoringState = WalletRestoringState.NONE, walletRestoringState = WalletRestoringState.NONE,
balanceState = BalanceStateFixture.new(),
) )
} }
} }
@ -143,33 +153,41 @@ private fun ComposableBalancesShieldErrorDialogPreview() {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun Balances( fun Balances(
onSettings: () -> Unit, balanceState: BalanceState,
isDetailedStatus: Boolean,
isFiatConversionEnabled: Boolean, isFiatConversionEnabled: Boolean,
isUpdateAvailable: Boolean, isUpdateAvailable: Boolean,
isShowingErrorDialog: Boolean, isShowingErrorDialog: Boolean,
hideStatusDialog: () -> Unit,
showStatusDialog: StatusAction.Detailed?,
setShowErrorDialog: (Boolean) -> Unit, setShowErrorDialog: (Boolean) -> Unit,
onSettings: () -> Unit,
onShielding: () -> Unit, onShielding: () -> Unit,
onStatusClick: (StatusAction) -> Unit,
shieldState: ShieldState, shieldState: ShieldState,
snackbarHostState: SnackbarHostState,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
walletRestoringState: WalletRestoringState, walletRestoringState: WalletRestoringState,
balanceState: BalanceState,
) { ) {
Scaffold(topBar = { Scaffold(
BalancesTopAppBar( topBar = {
showRestoring = walletRestoringState == WalletRestoringState.RESTORING, BalancesTopAppBar(
onSettings = onSettings showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
) onSettings = onSettings
}) { paddingValues -> )
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
) { paddingValues ->
if (null == walletSnapshot) { if (null == walletSnapshot) {
CircularScreenProgressIndicator() CircularScreenProgressIndicator()
} else { } else {
BalancesMainContent( BalancesMainContent(
balanceState = balanceState, balanceState = balanceState,
isDetailedStatus = isDetailedStatus,
isFiatConversionEnabled = isFiatConversionEnabled, isFiatConversionEnabled = isFiatConversionEnabled,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
onShielding = onShielding, onShielding = onShielding,
onStatusClick = onStatusClick,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
shieldState = shieldState, shieldState = shieldState,
modifier = modifier =
@ -182,6 +200,14 @@ fun Balances(
walletRestoringState = walletRestoringState walletRestoringState = walletRestoringState
) )
// Show synchronization status popup
if (showStatusDialog != null) {
StatusDialog(
statusAction = showStatusDialog,
onDone = hideStatusDialog
)
}
// Show shielding error popup // Show shielding error popup
if (isShowingErrorDialog && shieldState is ShieldState.Failed) { if (isShowingErrorDialog && shieldState is ShieldState.Failed) {
ShieldingErrorDialog( ShieldingErrorDialog(
@ -257,10 +283,10 @@ private fun BalancesTopAppBar(
@Composable @Composable
private fun BalancesMainContent( private fun BalancesMainContent(
balanceState: BalanceState, balanceState: BalanceState,
isDetailedStatus: Boolean,
isFiatConversionEnabled: Boolean, isFiatConversionEnabled: Boolean,
isUpdateAvailable: Boolean, isUpdateAvailable: Boolean,
onShielding: () -> Unit, onShielding: () -> Unit,
onStatusClick: (StatusAction) -> Unit,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
shieldState: ShieldState, shieldState: ShieldState,
walletRestoringState: WalletRestoringState, walletRestoringState: WalletRestoringState,
@ -282,7 +308,7 @@ private fun BalancesMainContent(
onReferenceClick = {} onReferenceClick = {}
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
HorizontalDivider( HorizontalDivider(
color = ZcashTheme.colors.darkDividerColor, color = ZcashTheme.colors.darkDividerColor,
@ -304,11 +330,9 @@ private fun BalancesMainContent(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
) )
Spacer(modifier = Modifier.weight(1f, true))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
if (walletRestoringState == WalletRestoringState.RESTORING) { if (walletRestoringState == WalletRestoringState.RESTORING) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Small( Small(
text = stringResource(id = R.string.balances_status_restoring_text), text = stringResource(id = R.string.balances_status_restoring_text),
textFontWeight = FontWeight.Medium, textFontWeight = FontWeight.Medium,
@ -317,17 +341,20 @@ private fun BalancesMainContent(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = ZcashTheme.dimens.spacingDefault) .padding(horizontal = ZcashTheme.dimens.spacingSmall)
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
} else {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
} }
SynchronizationStatus( SynchronizationStatus(
walletSnapshot = walletSnapshot,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
isDetailedStatus = isDetailedStatus, onStatusClick = onStatusClick,
testTag = BalancesTag.STATUS testTag = BalancesTag.STATUS,
walletSnapshot = walletSnapshot,
modifier = Modifier.animateContentSize()
) )
} }
} }
@ -517,7 +544,6 @@ fun BalancesOverview(
context = LocalContext.current, context = LocalContext.current,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = false, isUpdateAvailable = false,
isDetailedStatus = false
) )
Column(Modifier.testTag(BalancesTag.FIAT_CONVERSION)) { Column(Modifier.testTag(BalancesTag.FIAT_CONVERSION)) {

View File

@ -18,7 +18,6 @@ import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.RestoreScreenBrightness import co.electriccoin.zcash.ui.common.compose.RestoreScreenBrightness
import co.electriccoin.zcash.ui.common.model.VersionInfo
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.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
@ -56,13 +55,6 @@ internal fun MainActivity.WrapHome(
val isRestoringInitialWarningSeen = homeViewModel.isRestoringInitialWarningSeen.collectAsStateWithLifecycle().value val isRestoringInitialWarningSeen = homeViewModel.isRestoringInitialWarningSeen.collectAsStateWithLifecycle().value
// Detailed sync status info is used if set in configuration or if the app is built as debuggable
// (i.e. mainly in development)
val isDetailedSyncStatus =
homeViewModel.isDetailedSyncStatus.collectAsStateWithLifecycle().value.run {
this ?: false || VersionInfo.new(this@WrapHome).isDebuggable
}
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
@ -98,7 +90,6 @@ internal fun MainActivity.WrapHome(
goSettings = goSettings, goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure, goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
homeScreenIndex = homeScreenIndex, homeScreenIndex = homeScreenIndex,
isDetailedSyncStatus = isDetailedSyncStatus,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isShowingRestoreInitDialog = isShowingRestoreInitDialog, isShowingRestoreInitDialog = isShowingRestoreInitDialog,
onPageChange = { onPageChange = {
@ -120,7 +111,6 @@ internal fun WrapHome(
goScan: () -> Unit, goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
homeScreenIndex: HomeScreenIndex, homeScreenIndex: HomeScreenIndex,
isDetailedSyncStatus: Boolean,
isKeepScreenOnWhileSyncing: Boolean?, isKeepScreenOnWhileSyncing: Boolean?,
isShowingRestoreInitDialog: Boolean, isShowingRestoreInitDialog: Boolean,
onPageChange: (HomeScreenIndex) -> Unit, onPageChange: (HomeScreenIndex) -> Unit,
@ -203,7 +193,6 @@ internal fun WrapHome(
screenContent = { screenContent = {
WrapBalances( WrapBalances(
activity = activity, activity = activity,
isDetailedSyncStatus = isDetailedSyncStatus,
goSettings = goSettings, goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure
) )

View File

@ -16,9 +16,9 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.screen.update.util.PlayStoreUtil
import co.electriccoin.zcash.ui.screen.update.view.Update import co.electriccoin.zcash.ui.screen.update.view.Update
import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
import co.electriccoin.zcash.ui.util.PlayStoreUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -111,7 +111,7 @@ internal fun WrapUpdate(
}, },
onLater = onLaterAction, onLater = onLaterAction,
onReference = { onReference = {
openPlayStoreAppPage( openPlayStoreAppSite(
activity.applicationContext, activity.applicationContext,
snackbarHostState, snackbarHostState,
scope scope
@ -120,7 +120,7 @@ internal fun WrapUpdate(
) )
} }
fun openPlayStoreAppPage( private fun openPlayStoreAppSite(
context: Context, context: Context,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
scope: CoroutineScope scope: CoroutineScope
@ -131,7 +131,7 @@ fun openPlayStoreAppPage(
}.onFailure { }.onFailure {
scope.launch { scope.launch {
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
message = context.getString(R.string.update_unable_to_open_play_store) message = context.getString(R.string.unable_to_open_play_store)
) )
} }
} }

View File

@ -234,7 +234,7 @@ private fun UpdateContentContent(
} else { } else {
ImageVector.vectorResource(R.drawable.ic_zashi_logo_update_available) ImageVector.vectorResource(R.drawable.ic_zashi_logo_update_available)
}, },
contentDescription = stringResource(id = R.string.update_image_content_description) contentDescription = null
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.update.util package co.electriccoin.zcash.ui.util
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

View File

@ -22,14 +22,23 @@
<string name="balances_status_synced">Synced</string> <string name="balances_status_synced">Synced</string>
<string name="balances_status_update">Please update <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> using Google Play</string> <string name="balances_status_update">Please update <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> using Google Play</string>
<string name="balances_status_error_simple"><xliff:g id="app_name" example="Zashi">%1$s</xliff:g> encountered an error while syncing, attempting to resolve…</string> <string name="balances_status_error_simple"><xliff:g id="app_name" example="Zashi">%1$s</xliff:g> encountered an error while syncing, attempting to resolve…</string>
<string name="balances_status_error_detailed" formatted="true">Error: <xliff:g id="error_type" example="Lost connection">%1$s</xliff:g></string>
<string name="balances_status_error_detailed_connection">Disconnected</string>
<string name="balances_status_error_detailed_unknown">Unknown cause</string>
<string name="balances_status_detailed_stopped">Synchronizer stopped</string>
<string name="balances_status_restoring_text">The restore process can take several hours on lower-powered devices, and even on powerful devices is likely to take more than an hour.</string> <string name="balances_status_restoring_text">The restore process can take several hours on lower-powered devices, and even on powerful devices is likely to take more than an hour.</string>
<string name="balances_shielding_successful">Shielding has been successfully submitted</string> <string name="balances_shielding_successful">Shielding has been successfully submitted</string>
<string name="balances_status_error_dialog_title">Error</string>
<string name="balances_status_error_dialog_connection">
Disconnected. Please check your internet connection.
</string>
<string name="balances_status_error_dialog_cause" formatted="true">
Error: <xliff:g id="error_cause" example="Block scanning problem">%1$s</xliff:g>
</string>
<string name="balances_status_error_dialog_unknown">
Unknown cause. Please contact our support team if the problem persists.
</string>
<string name="balances_status_dialog_stopped">Synchronization is stopped. It will resume soon.</string>
<string name="balances_status_dialog_button">OK</string>
<string name="balances_shielding_dialog_error_title">Failed to shield funds</string> <string name="balances_shielding_dialog_error_title">Failed to shield funds</string>
<string name="balances_shielding_dialog_error_text">Error: The attempt to shield the transparent funds failed. Try it again, please.</string> <string name="balances_shielding_dialog_error_text">Error: The attempt to shield the transparent funds failed. Try it again, please.</string>
<string name="balances_shielding_dialog_error_btn">OK</string> <string name="balances_shielding_dialog_error_btn">OK</string>

View File

@ -7,4 +7,5 @@
<!-- This is replaced by a resource overlay via app/build.gradle.kts --> <!-- This is replaced by a resource overlay via app/build.gradle.kts -->
<string name="support_email_address" /> <string name="support_email_address" />
<string name="restoring_wallet_label">[Restoring Your Wallet…]</string> <string name="restoring_wallet_label">[Restoring Your Wallet…]</string>
<string name="unable_to_open_play_store">Unable to launch Google Play store app…</string>
</resources> </resources>

View File

@ -2,7 +2,6 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="update_header">Update available</string> <string name="update_header">Update available</string>
<string name="update_critical_header">Update required</string> <string name="update_critical_header">Update required</string>
<string name="update_image_content_description"></string>
<string name="update_title_available"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> here.</string> <string name="update_title_available"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> here.</string>
<string name="update_title_required">It\'s not you, it\'s me.</string> <string name="update_title_required">It\'s not you, it\'s me.</string>
<string name="update_description_required"> <string name="update_description_required">
@ -17,5 +16,4 @@
<string name="update_download_button">Update</string> <string name="update_download_button">Update</string>
<string name="update_later_enabled_button">Remind me later</string> <string name="update_later_enabled_button">Remind me later</string>
<string name="update_later_disabled_button">(required)</string> <string name="update_later_disabled_button">(required)</string>
<string name="update_unable_to_open_play_store">Unable to launch Google Play store app.</string>
</resources> </resources>