[#1164] Restoring UI state in TopAppBar

- Closes #1164
- This incorporates the new wallet restoring label into the custom TopAppBar UI component, so it’s accessible from all screens
- This also fixes the adjust brightness feature that previously stayed turned on when the user left to a surrounding tab screen (Send or Balances)
- Changelog update

Move DisableScreenTimeout into the parent HomeView

Persist restoring state

Fix infinite loading trx history UI state

Add New wallet syncing state

This also adds the wallet restoring state into the transaction history state calculation
This commit is contained in:
Honza Rychnovský 2024-04-05 13:09:08 +02:00 committed by GitHub
parent 84c7618037
commit 8c027003cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 1027 additions and 379 deletions

View File

@ -19,6 +19,8 @@ directly impact users rather than highlighting other key architectural updates.*
- Transitions between screens are now animated with a simple slide animation - Transitions between screens are now animated with a simple slide animation
- Proposal API from the Zcash SDK has been integrated together with handling error states for multi-transaction - Proposal API from the Zcash SDK has been integrated together with handling error states for multi-transaction
submission submission
- New Restoring Your Wallet label has been added to all post-onboarding screens to notify users about the current
state of the wallet-restoring process.
### Changed ### Changed
- The Transaction History UI has been incorporated into the Account screen and completely reworked according to the - The Transaction History UI has been incorporated into the Account screen and completely reworked according to the

View File

@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -7,6 +9,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
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.layout.size import androidx.compose.foundation.layout.size
@ -34,7 +37,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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
import co.electriccoin.zcash.ui.design.R import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.internal.SecondaryTypography import co.electriccoin.zcash.ui.design.theme.internal.SecondaryTypography
@ -49,6 +55,34 @@ private fun TopAppBarTextComposablePreview() {
} }
} }
@Preview
@Composable
private fun TopAppBarTextRestoringComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SmallTopAppBar(
titleText = "Screen A",
backText = "Back",
restoringLabel = "[RESTORING YOUR WALLET…]"
)
}
}
}
@Preview
@Composable
private fun TopAppBarTextRestoringLongComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SmallTopAppBar(
titleText = "Screen A",
backText = "Back",
restoringLabel = "[RESTORING YOUR WALLET LONG TEXT…]"
)
}
}
}
@Preview @Preview
@Composable @Composable
private fun TopAppBarLogoComposablePreview() { private fun TopAppBarLogoComposablePreview() {
@ -59,6 +93,20 @@ private fun TopAppBarLogoComposablePreview() {
} }
} }
@Preview
@Composable
private fun TopAppBarLogoRestoringComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SmallTopAppBar(
showTitleLogo = true,
backText = "Back",
restoringLabel = "[RESTORING YOUR WALLET…]"
)
}
}
}
@Preview @Preview
@Composable @Composable
private fun TopAppBarRegularMenuComposablePreview() { private fun TopAppBarRegularMenuComposablePreview() {
@ -213,10 +261,11 @@ private fun TopBarOneVisibleActionMenuExample(
} }
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun SmallTopAppBar( fun SmallTopAppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
restoringLabel: String? = null,
titleText: String? = null, titleText: String? = null,
showTitleLogo: Boolean = false, showTitleLogo: Boolean = false,
backText: String? = null, backText: String? = null,
@ -227,17 +276,40 @@ fun SmallTopAppBar(
) { ) {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
var restoringSpacerHeight: Dp = 0.dp
if (titleText != null) { if (titleText != null) {
Text( Text(
text = titleText.uppercase(), text = titleText.uppercase(),
style = SecondaryTypography.headlineSmall style = SecondaryTypography.headlineSmall
) )
restoringSpacerHeight = ZcashTheme.dimens.spacingTiny
} else if (showTitleLogo) { } else if (showTitleLogo) {
Icon( Icon(
painter = painterResource(id = R.drawable.zashi_text_logo), painter = painterResource(id = R.drawable.zashi_text_logo),
contentDescription = null, contentDescription = null,
modifier = Modifier.height(ZcashTheme.dimens.topAppBarZcashLogoHeight) modifier = Modifier.height(ZcashTheme.dimens.topAppBarZcashLogoHeight)
) )
restoringSpacerHeight = ZcashTheme.dimens.spacingSmall
}
if (restoringLabel != null) {
Spacer(modifier = Modifier.height(restoringSpacerHeight))
@Suppress("MagicNumber")
Text(
text = restoringLabel.uppercase(),
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
color = ZcashTheme.colors.restoringTopAppBarColor,
modifier = Modifier.fillMaxWidth(0.75f),
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} }
}, },
navigationIcon = { navigationIcon = {

View File

@ -18,6 +18,7 @@ data class ExtendedColors(
val circularProgressBarScreen: Color, val circularProgressBarScreen: Color,
val linearProgressBarTrack: Color, val linearProgressBarTrack: Color,
val linearProgressBarBackground: Color, val linearProgressBarBackground: Color,
val restoringTopAppBarColor: Color,
val chipIndex: Color, val chipIndex: Color,
val textCommon: Color, val textCommon: Color,
val textMedium: Color, val textMedium: Color,

View File

@ -54,6 +54,7 @@ internal object Dark {
val circularProgressBarScreen = Color(0xFFFFFFFF) val circularProgressBarScreen = Color(0xFFFFFFFF)
val linearProgressBarTrack = Color(0xFFD9D9D9) val linearProgressBarTrack = Color(0xFFD9D9D9)
val linearProgressBarBackground = Light.complementaryColor val linearProgressBarBackground = Light.complementaryColor
val restoringTopAppBarColor = Color(0xFF8A8888)
val callout = Color(0xFFFFFFFF) val callout = Color(0xFFFFFFFF)
val onCallout = Color(0xFFFFFFFF) val onCallout = Color(0xFFFFFFFF)
@ -112,6 +113,7 @@ internal object Light {
val circularProgressBarScreen = Color(0xFF000000) val circularProgressBarScreen = Color(0xFF000000)
val linearProgressBarTrack = Color(0xFFD9D9D9) val linearProgressBarTrack = Color(0xFFD9D9D9)
val linearProgressBarBackground = complementaryColor val linearProgressBarBackground = complementaryColor
val restoringTopAppBarColor = Color(0xFF8A8888)
val callout = Color(0xFFFFFFFF) val callout = Color(0xFFFFFFFF)
val onCallout = Color(0xFFFFFFFF) val onCallout = Color(0xFFFFFFFF)
@ -165,6 +167,7 @@ internal val DarkExtendedColorPalette =
circularProgressBarScreen = Dark.circularProgressBarScreen, circularProgressBarScreen = Dark.circularProgressBarScreen,
linearProgressBarTrack = Dark.linearProgressBarTrack, linearProgressBarTrack = Dark.linearProgressBarTrack,
linearProgressBarBackground = Dark.linearProgressBarBackground, linearProgressBarBackground = Dark.linearProgressBarBackground,
restoringTopAppBarColor = Dark.restoringTopAppBarColor,
chipIndex = Dark.textChipIndex, chipIndex = Dark.textChipIndex,
textCommon = Dark.textCommon, textCommon = Dark.textCommon,
textMedium = Dark.textMedium, textMedium = Dark.textMedium,
@ -207,6 +210,7 @@ internal val LightExtendedColorPalette =
circularProgressBarSmall = Light.circularProgressBarSmall, circularProgressBarSmall = Light.circularProgressBarSmall,
linearProgressBarTrack = Light.linearProgressBarTrack, linearProgressBarTrack = Light.linearProgressBarTrack,
linearProgressBarBackground = Light.linearProgressBarBackground, linearProgressBarBackground = Light.linearProgressBarBackground,
restoringTopAppBarColor = Light.restoringTopAppBarColor,
chipIndex = Light.textChipIndex, chipIndex = Light.textChipIndex,
textCommon = Light.textCommon, textCommon = Light.textCommon,
textMedium = Dark.textMedium, textMedium = Dark.textMedium,
@ -251,6 +255,7 @@ internal val LocalExtendedColors =
circularProgressBarSmall = Color.Unspecified, circularProgressBarSmall = Color.Unspecified,
linearProgressBarTrack = Color.Unspecified, linearProgressBarTrack = Color.Unspecified,
linearProgressBarBackground = Color.Unspecified, linearProgressBarBackground = Color.Unspecified,
restoringTopAppBarColor = Color.Unspecified,
chipIndex = Color.Unspecified, chipIndex = Color.Unspecified,
textCommon = Color.Unspecified, textCommon = Color.Unspecified,
textMedium = Color.Unspecified, textMedium = Color.Unspecified,

View File

@ -191,6 +191,7 @@ data class ExtendedTypography(
val radioButton: TextStyle, val radioButton: TextStyle,
// Grouping transaction item text styles to a wrapper class // Grouping transaction item text styles to a wrapper class
val transactionItemStyles: TransactionItemTextStyles, val transactionItemStyles: TransactionItemTextStyles,
val restoringTopAppBarStyle: TextStyle,
) )
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
@ -361,5 +362,10 @@ val LocalExtendedTypography =
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
), ),
), ),
restoringTopAppBarStyle =
SecondaryTypography.labelMedium.copy(
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold
)
) )
} }

View File

@ -11,8 +11,8 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.compose.BrightenScreen import co.electriccoin.zcash.ui.common.compose.BrightenScreen
import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -20,7 +20,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ScreenBrightnessTest : UiTestPrerequisites() { class ScreenBrightnessTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@ -31,19 +30,19 @@ class ScreenBrightnessTest : UiTestPrerequisites() {
runTest { runTest {
val testSetup = TestSetup(composeTestRule) val testSetup = TestSetup(composeTestRule)
assertEquals(1, testSetup.getSecureBrightnessCount()) assertEquals(ScreenBrightnessState.FULL, testSetup.getSecureBrightnessCount())
testSetup.mutableScreenBrightnessFlag.update { false } testSetup.mutableScreenBrightnessFlag.update { false }
composeTestRule.awaitIdle() composeTestRule.awaitIdle()
assertEquals(0, testSetup.getSecureBrightnessCount()) assertEquals(ScreenBrightnessState.NORMAL, testSetup.getSecureBrightnessCount())
} }
private class TestSetup(composeTestRule: ComposeContentTestRule) { private class TestSetup(composeTestRule: ComposeContentTestRule) {
val mutableScreenBrightnessFlag = MutableStateFlow(true) val mutableScreenBrightnessFlag = MutableStateFlow(true)
private val screenBrightness = ScreenBrightness() private val screenBrightness = ScreenBrightness
fun getSecureBrightnessCount() = screenBrightness.referenceCount.value fun getSecureBrightnessCount() = screenBrightness.referenceSwitch.value
init { init {
runTest { runTest {

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -24,10 +25,11 @@ class AboutViewTestSetup(
ZcashTheme { ZcashTheme {
About( About(
onBack = { onBackCount.incrementAndGet() }, onBack = { onBackCount.incrementAndGet() },
versionInfo = versionInfo,
configInfo = configInfo, configInfo = configInfo,
onPrivacyPolicy = {}, onPrivacyPolicy = {},
snackbarHostState = SnackbarHostState() snackbarHostState = SnackbarHostState(),
versionInfo = versionInfo,
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.account
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.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistoryUiStateFixture import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistoryUiStateFixture
@ -58,8 +59,6 @@ class AccountTestSetup(
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
fun DefaultContent() { fun DefaultContent() {
Account( Account(
walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = false,
goSettings = { goSettings = {
onSettingsCount.incrementAndGet() onSettingsCount.incrementAndGet()
}, },
@ -68,6 +67,8 @@ class AccountTestSetup(
onTransactionItemAction = { onTransactionItemAction = {
onItemClickCount.incrementAndGet() onItemClickCount.incrementAndGet()
}, },
walletSnapshot = walletSnapshot,
walletRestoringState = WalletRestoringState.NONE,
) )
} }

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.account.history package co.electriccoin.zcash.ui.screen.account.history
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.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
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.view.HistoryContainer import co.electriccoin.zcash.ui.screen.account.view.HistoryContainer
@ -30,7 +31,8 @@ class HistoryTestSetup(
transactionState = initialHistoryUiState, transactionState = initialHistoryUiState,
onTransactionItemAction = { onTransactionItemAction = {
onItemIdClickCount.incrementAndGet() onItemIdClickCount.incrementAndGet()
} },
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }

View File

@ -7,16 +7,16 @@ import kotlinx.collections.immutable.ImmutableList
internal object TransactionHistoryUiStateFixture { internal object TransactionHistoryUiStateFixture {
val TRANSACTIONS = TransactionsFixture.new() val TRANSACTIONS = TransactionsFixture.new()
val STATE = TransactionUiState.Prepared(TRANSACTIONS) val STATE = TransactionUiState.Done(TRANSACTIONS)
fun new( fun new(
transactions: ImmutableList<TransactionUi> = TRANSACTIONS, transactions: ImmutableList<TransactionUi> = TRANSACTIONS,
state: TransactionUiState = STATE state: TransactionUiState = STATE
) = when (state) { ) = when (state) {
is TransactionUiState.Loading -> state is TransactionUiState.Loading -> state
is TransactionUiState.Syncing -> state is TransactionUiState.Syncing -> state.copy(transactions)
is TransactionUiState.Prepared -> { is TransactionUiState.Done -> state.copy(transactions)
state.copy(transactions) TransactionUiState.DoneEmpty -> state
} TransactionUiState.SyncingEmpty -> state
} }
} }

View File

@ -39,7 +39,7 @@ class HistoryViewTest {
fun check_syncing_state() { fun check_syncing_state() {
newTestSetup( newTestSetup(
TransactionHistoryUiStateFixture.new( TransactionHistoryUiStateFixture.new(
state = TransactionUiState.Prepared(persistentListOf()), state = TransactionUiState.Syncing(persistentListOf()),
transactions = TransactionHistoryUiStateFixture.TRANSACTIONS transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
) )
) )
@ -62,7 +62,7 @@ class HistoryViewTest {
fun check_done_state_no_transactions() { fun check_done_state_no_transactions() {
newTestSetup( newTestSetup(
TransactionHistoryUiStateFixture.new( TransactionHistoryUiStateFixture.new(
state = TransactionUiState.Prepared(persistentListOf()), state = TransactionUiState.Syncing(persistentListOf()),
transactions = TransactionHistoryUiStateFixture.TRANSACTIONS transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
) )
) )
@ -85,7 +85,7 @@ class HistoryViewTest {
fun check_done_state_with_transactions() { fun check_done_state_with_transactions() {
newTestSetup( newTestSetup(
TransactionHistoryUiStateFixture.new( TransactionHistoryUiStateFixture.new(
state = TransactionUiState.Prepared(persistentListOf()), state = TransactionUiState.Syncing(persistentListOf()),
transactions = TransactionHistoryUiStateFixture.TRANSACTIONS transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
) )
) )

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.balances
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.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
@ -33,13 +34,13 @@ class BalancesTestSetup(
onSettingsCount.incrementAndGet() onSettingsCount.incrementAndGet()
}, },
isFiatConversionEnabled = isShowFiatConversion, isFiatConversionEnabled = isShowFiatConversion,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false, isUpdateAvailable = false,
isShowingErrorDialog = false,
setShowErrorDialog = {},
onShielding = {}, onShielding = {},
shieldState = ShieldState.Available, shieldState = ShieldState.Available,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isShowingErrorDialog = false, walletRestoringState = WalletRestoringState.NONE
setShowErrorDialog = {}
) )
} }

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.material3.SnackbarHostState 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.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -33,7 +34,7 @@ class ExportPrivateDataViewTestSetup(private val composeTestRule: ComposeContent
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
fun DefaultContent() { fun DefaultContent() {
ExportPrivateData( ExportPrivateData(
SnackbarHostState(), snackbarHostState = SnackbarHostState(),
onBack = { onBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
}, },
@ -42,7 +43,8 @@ class ExportPrivateDataViewTestSetup(private val composeTestRule: ComposeContent
}, },
onConfirm = { onConfirm = {
onConfirmCount.incrementAndGet() onConfirmCount.incrementAndGet()
} },
walletRestoringState = WalletRestoringState.NONE
) )
} }

View File

@ -5,6 +5,7 @@ import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddresses import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -27,7 +28,7 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
VersionInfoFixture.new(isDebuggable = true) VersionInfoFixture.new(isDebuggable = true)
) )
assertEquals(0, testSetup.getScreenBrightnessCount()) assertEquals(ScreenBrightnessState.NORMAL, testSetup.getScreenBrightness())
} }
@Test @Test
@ -41,13 +42,11 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
VersionInfoFixture.new(isDebuggable = true) VersionInfoFixture.new(isDebuggable = true)
) )
assertEquals(false, testSetup.getOnAdjustBrightness()) assertEquals(ScreenBrightnessState.NORMAL, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenBrightnessCount())
composeTestRule.clickAdjustBrightness() composeTestRule.clickAdjustBrightness()
assertEquals(true, testSetup.getOnAdjustBrightness()) assertEquals(ScreenBrightnessState.FULL, testSetup.getOnAdjustBrightness())
assertEquals(1, testSetup.getScreenBrightnessCount())
} }
private fun newTestSetup( private fun newTestSetup(

View File

@ -40,12 +40,10 @@ class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
VersionInfoFixture.new(isDebuggable = true) VersionInfoFixture.new(isDebuggable = true)
) )
assertEquals(false, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenTimeoutCount()) assertEquals(0, testSetup.getScreenTimeoutCount())
composeTestRule.clickAdjustBrightness() composeTestRule.clickAdjustBrightness()
assertEquals(true, testSetup.getOnAdjustBrightness())
assertEquals(1, testSetup.getScreenTimeoutCount()) assertEquals(1, testSetup.getScreenTimeoutCount())
} }

View File

@ -10,12 +10,13 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness
import co.electriccoin.zcash.ui.common.compose.LocalScreenTimeout import co.electriccoin.zcash.ui.common.compose.LocalScreenTimeout
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState
import co.electriccoin.zcash.ui.common.compose.ScreenTimeout import co.electriccoin.zcash.ui.common.compose.ScreenTimeout
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
class ReceiveViewTestSetup( class ReceiveViewTestSetup(
@ -25,17 +26,17 @@ class ReceiveViewTestSetup(
) { ) {
private val onSettingsCount = AtomicInteger(0) private val onSettingsCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0) private val onAddressDetailsCount = AtomicInteger(0)
private val screenBrightness = ScreenBrightness() private val screenBrightness = ScreenBrightness
private val screenTimeout = ScreenTimeout() private val screenTimeout = ScreenTimeout()
private val onAdjustBrightness = AtomicBoolean(false) private var onAdjustBrightness: ScreenBrightnessState = ScreenBrightnessState.NORMAL
fun getScreenBrightnessCount() = screenBrightness.referenceCount.value fun getScreenBrightness() = screenBrightness.referenceSwitch.value
fun getScreenTimeoutCount() = screenTimeout.referenceCount.value fun getScreenTimeoutCount() = screenTimeout.referenceCount.value
fun getOnAdjustBrightness(): Boolean { fun getOnAdjustBrightness(): ScreenBrightnessState {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return onAdjustBrightness.get() return onAdjustBrightness
} }
fun getOnSettingsCount(): Int { fun getOnSettingsCount(): Int {
@ -63,11 +64,14 @@ class ReceiveViewTestSetup(
onSettingsCount.getAndIncrement() onSettingsCount.getAndIncrement()
}, },
onAdjustBrightness = { onAdjustBrightness = {
onAdjustBrightness.getAndSet(it) onAdjustBrightness = onAdjustBrightness.getChange()
screenTimeout.disableScreenTimeout()
}, },
onAddrCopyToClipboard = {}, onAddrCopyToClipboard = {},
onQrImageShare = {}, onQrImageShare = {},
versionInfo = versionInfo screenBrightnessState = ScreenBrightnessState.NORMAL,
versionInfo = versionInfo,
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }

View File

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import cash.z.ecc.sdk.fixture.PersistableWalletFixture import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -39,10 +40,11 @@ class SeedRecoveryTestSetup(
SeedRecovery( SeedRecovery(
PersistableWalletFixture.new(), PersistableWalletFixture.new(),
onBack = { onBackCount.incrementAndGet() }, onBack = { onBackCount.incrementAndGet() },
onSeedCopy = { /* Not tested - debug mode feature only */ },
onBirthdayCopy = { onBirthdayCopyCount.incrementAndGet() }, onBirthdayCopy = { onBirthdayCopyCount.incrementAndGet() },
onDone = { onCompleteCallbackCount.incrementAndGet() }, onDone = { onCompleteCallbackCount.incrementAndGet() },
onSeedCopy = { /* Not tested - debug mode feature only */ },
versionInfo = versionInfo, versionInfo = versionInfo,
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }

View File

@ -8,6 +8,7 @@ import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -45,10 +46,11 @@ class SeedRecoveryViewsSecuredScreenTest : UiTestPrerequisites() {
SeedRecovery( SeedRecovery(
PersistableWalletFixture.new(), PersistableWalletFixture.new(),
onBack = {}, onBack = {},
onSeedCopy = {},
onBirthdayCopy = {}, onBirthdayCopy = {},
onDone = {}, onDone = {},
versionInfo = VersionInfoFixture.new() onSeedCopy = {},
versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }

View File

@ -12,6 +12,7 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
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.send.ext.Saver import co.electriccoin.zcash.ui.screen.send.ext.Saver
@ -137,6 +138,7 @@ class SendViewTestSetup(
setAmountState = {}, setAmountState = {},
memoState = MemoState.new(""), memoState = MemoState.new(""),
setMemoState = {}, setMemoState = {},
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }

View File

@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.fixture.ZecSendFixture import cash.z.ecc.sdk.fixture.ZecSendFixture
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.fixture.MockSynchronizer import co.electriccoin.zcash.ui.fixture.MockSynchronizer
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.send.WrapSend import co.electriccoin.zcash.ui.screen.send.WrapSend
@ -78,7 +79,8 @@ class SendViewIntegrationTest {
hasCameraFeature = true, hasCameraFeature = true,
goSettings = {}, goSettings = {},
monetarySeparators = monetarySeparators, monetarySeparators = monetarySeparators,
goSendConfirmation = {} goSendConfirmation = {},
walletRestoringState = WalletRestoringState.NONE,
) )
} }

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.settings package co.electriccoin.zcash.ui.screen.settings
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.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.view.Settings import co.electriccoin.zcash.ui.screen.settings.view.Settings
@ -87,7 +88,8 @@ class SettingsViewTestSetup(
}, },
onAnalyticsSettingsChanged = { onAnalyticsSettingsChanged = {
onAnalyticsChangedCount.incrementAndGet() onAnalyticsChangedCount.incrementAndGet()
} },
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.support.view
import androidx.compose.material3.SnackbarHostState 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.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -36,7 +37,8 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
fun DefaultContent() { fun DefaultContent() {
Support( Support(
snackbarHostState = SnackbarHostState(), isShowingDialog = false,
setShowDialog = {},
onBack = { onBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
}, },
@ -44,8 +46,8 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
onSendCount.incrementAndGet() onSendCount.incrementAndGet()
onSendMessage.set(it) onSendMessage.set(it)
}, },
isShowingDialog = false, snackbarHostState = SnackbarHostState(),
setShowDialog = {} walletRestoringState = WalletRestoringState.NONE
) )
} }

View File

@ -28,6 +28,7 @@ import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -53,7 +54,7 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
val homeViewModel by viewModels<HomeViewModel>() private val homeViewModel by viewModels<HomeViewModel>()
val walletViewModel by viewModels<WalletViewModel>() val walletViewModel by viewModels<WalletViewModel>()
@ -173,6 +174,7 @@ class MainActivity : ComponentActivity() {
) )
} else { } else {
walletViewModel.persistNewWallet() walletViewModel.persistNewWallet()
walletViewModel.persistWalletRestoringState(WalletRestoringState.INITIATING)
} }
} }
) )

View File

@ -74,9 +74,6 @@ internal fun MainActivity.Navigation() {
WrapHome( WrapHome(
goBack = { finish() }, goBack = { finish() },
goScan = { navController.navigateJustOnce(SCAN) }, goScan = { navController.navigateJustOnce(SCAN) },
onPageChange = {
homeViewModel.screenIndex.value = it
},
goSendConfirmation = { zecSend -> goSendConfirmation = { zecSend ->
navController.currentBackStackEntry?.savedStateHandle?.let { handle -> navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
fillInHandleForConfirmation(handle, zecSend, SendConfirmationStage.Confirmation) fillInHandleForConfirmation(handle, zecSend, SendConfirmationStage.Confirmation)
@ -140,7 +137,7 @@ internal fun MainActivity.Navigation() {
}, },
goChooseServer = { goChooseServer = {
navController.navigateJustOnce(CHOOSE_SERVER) navController.navigateJustOnce(CHOOSE_SERVER)
} },
) )
} }
composable(CHOOSE_SERVER) { composable(CHOOSE_SERVER) {
@ -157,7 +154,7 @@ internal fun MainActivity.Navigation() {
}, },
onDone = { onDone = {
navController.popBackStackJustOnce(SEED_RECOVERY) navController.popBackStackJustOnce(SEED_RECOVERY)
} },
) )
} }
composable(REQUEST) { composable(REQUEST) {

View File

@ -15,7 +15,7 @@ fun ComponentActivity.BindCompLocalProvider(content: @Composable () -> Unit) {
val screenSecurity = ScreenSecurity() val screenSecurity = ScreenSecurity()
observeScreenSecurityFlag(screenSecurity) observeScreenSecurityFlag(screenSecurity)
val screenBrightness = ScreenBrightness() val screenBrightness = ScreenBrightness
observeScreenBrightnessFlag(screenBrightness) observeScreenBrightnessFlag(screenBrightness)
val screenTimeout = ScreenTimeout() val screenTimeout = ScreenTimeout()
@ -46,7 +46,9 @@ private fun ComponentActivity.observeScreenSecurityFlag(screenSecurity: ScreenSe
} }
private fun ComponentActivity.observeScreenBrightnessFlag(screenBrightness: ScreenBrightness) { private fun ComponentActivity.observeScreenBrightnessFlag(screenBrightness: ScreenBrightness) {
screenBrightness.referenceCount.map { it > 0 }.collectWith(lifecycleScope) { maxBrightness -> screenBrightness.referenceSwitch
.map { it == ScreenBrightnessState.FULL }
.collectWith(lifecycleScope) { maxBrightness ->
if (maxBrightness) { if (maxBrightness) {
window.attributes = window.attributes =
window.attributes.apply { window.attributes.apply {

View File

@ -6,28 +6,32 @@ import androidx.compose.runtime.compositionLocalOf
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
class ScreenBrightness { sealed class ScreenBrightnessState {
private val mutableReferenceCount: MutableStateFlow<Int> = MutableStateFlow(0) fun getChange(): ScreenBrightnessState {
return when (this) {
val referenceCount = mutableReferenceCount.asStateFlow() NORMAL -> FULL
FULL -> NORMAL
fun fullBrightness() {
mutableReferenceCount.update { it + 1 }
}
fun restoreBrightness() {
val after = mutableReferenceCount.updateAndGet { it - 1 }
if (after < 0) {
error("Released brightness reference count too many times")
} }
} }
data object FULL : ScreenBrightnessState()
data object NORMAL : ScreenBrightnessState()
}
object ScreenBrightness {
private val mutableSwitch: MutableStateFlow<ScreenBrightnessState> = MutableStateFlow(ScreenBrightnessState.NORMAL)
val referenceSwitch = mutableSwitch.asStateFlow()
fun fullBrightness() = mutableSwitch.update { ScreenBrightnessState.FULL }
fun restoreBrightness() = mutableSwitch.update { ScreenBrightnessState.NORMAL }
} }
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
val LocalScreenBrightness = compositionLocalOf { ScreenBrightness() } val LocalScreenBrightness = compositionLocalOf { ScreenBrightness }
@Composable @Composable
fun BrightenScreen() { fun BrightenScreen() {
@ -37,3 +41,9 @@ fun BrightenScreen() {
onDispose { screenBrightness.restoreBrightness() } onDispose { screenBrightness.restoreBrightness() }
} }
} }
@Composable
fun RestoreScreenBrightness() {
val screenBrightness = LocalScreenBrightness.current
screenBrightness.restoreBrightness()
}

View File

@ -0,0 +1,23 @@
package co.electriccoin.zcash.ui.common.model
/**
* Common wallet restoring state enum. This describes whether the current block synchronization run is in the
* restoring state or a subsequent synchronization state.
*
* WARN: Do NOT reorder or change the values; doing so would update their ordinal numbers, which could break the
* wallet UI.
*/
enum class WalletRestoringState {
NONE,
INITIATING, // New wallet syncing
RESTORING, // Existing wallet syncing
SYNCING; // Follow-up syncing
fun isRunningRestoring() = this == NONE || this == RESTORING
fun toNumber() = ordinal
companion object {
fun fromNumber(ordinal: Int) = entries[ordinal]
}
}

View File

@ -29,6 +29,7 @@ import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.extension.throttle import co.electriccoin.zcash.ui.common.extension.throttle
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
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.preference.EncryptedPreferenceKeys import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
@ -98,6 +99,23 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
null null
) )
/**
* A flow of the wallet block synchronization state.
*/
val walletRestoringState: StateFlow<WalletRestoringState> =
flow {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
emitAll(
StandardPreferenceKeys.WALLET_RESTORING_STATE.observe(preferenceProvider).map { persistedNumber ->
WalletRestoringState.fromNumber(persistedNumber)
}
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
WalletRestoringState.NONE
)
/** /**
* A flow of the wallet onboarding state. * A flow of the wallet onboarding state.
*/ */
@ -288,12 +306,28 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
} }
} }
/**
* Asynchronously notes that the wallet has completed the initial wallet restoring block synchronization run.
*
* Note that in the current SDK implementation, we don't have any information about the block synchronization
* state from the SDK, and thus, we need to note the wallet restoring state here on the client side.
*/
fun persistWalletRestoringState(walletRestoringState: WalletRestoringState) {
val application = getApplication<Application>()
viewModelScope.launch {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
StandardPreferenceKeys.WALLET_RESTORING_STATE.putValue(preferenceProvider, walletRestoringState.toNumber())
}
}
/** /**
* This method only has an effect if the synchronizer currently is loaded. * This method only has an effect if the synchronizer currently is loaded.
*/ */
fun rescanBlockchain() { fun rescanBlockchain() {
viewModelScope.launch { viewModelScope.launch {
walletCoordinator.rescanBlockchain() walletCoordinator.rescanBlockchain()
persistWalletRestoringState(WalletRestoringState.RESTORING)
} }
} }
@ -435,4 +469,6 @@ private fun Synchronizer.toWalletSnapshot() =
) )
} }
private fun Synchronizer.Status.isSyncing() = this == Synchronizer.Status.SYNCING fun Synchronizer.Status.isSyncing() = this == Synchronizer.Status.SYNCING
fun Synchronizer.Status.isSynced() = this == Synchronizer.Status.SYNCED

View File

@ -4,6 +4,7 @@ import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
object StandardPreferenceKeys { object StandardPreferenceKeys {
/** /**
@ -15,6 +16,16 @@ object StandardPreferenceKeys {
OnboardingState.NONE.toNumber() OnboardingState.NONE.toNumber()
) )
/**
* State defining whether the current block synchronization run is in the restoring state or a subsequent
* synchronization state.
*/
val WALLET_RESTORING_STATE =
IntegerPreferenceDefault(
PreferenceKey("wallet_restoring_state"),
WalletRestoringState.RESTORING.toNumber()
)
// Default to true until https://github.com/Electric-Coin-Company/zashi-android/issues/304 // Default to true until https://github.com/Electric-Coin-Company/zashi-android/issues/304
val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(PreferenceKey("is_analytics_enabled"), true) val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(PreferenceKey("is_analytics_enabled"), true)

View File

@ -4,15 +4,19 @@ package co.electriccoin.zcash.ui.screen.about
import android.content.Context import android.content.Context
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
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.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
import co.electriccoin.zcash.ui.screen.about.view.About import co.electriccoin.zcash.ui.screen.about.view.About
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@ -21,13 +25,22 @@ import kotlinx.coroutines.launch
@Composable @Composable
internal fun MainActivity.WrapAbout(goBack: () -> Unit) { internal fun MainActivity.WrapAbout(goBack: () -> Unit) {
WrapAbout(this, goBack) val walletViewModel by viewModels<WalletViewModel>()
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapAbout(
activity = this,
goBack = goBack,
walletRestoringState = walletRestoringState
)
} }
@Composable @Composable
internal fun WrapAbout( internal fun WrapAbout(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit goBack: () -> Unit,
walletRestoringState: WalletRestoringState,
) { ) {
val configInfo = ConfigInfo.new(AndroidConfigurationFactory.getInstance(activity.applicationContext)) val configInfo = ConfigInfo.new(AndroidConfigurationFactory.getInstance(activity.applicationContext))
val versionInfo = VersionInfo.new(activity.applicationContext) val versionInfo = VersionInfo.new(activity.applicationContext)
@ -38,6 +51,7 @@ internal fun WrapAbout(
} }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
About( About(
@ -52,6 +66,7 @@ internal fun WrapAbout(
) )
}, },
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
walletRestoringState = walletRestoringState,
) )
} }

View File

@ -41,6 +41,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -55,29 +56,33 @@ private fun AboutPreview() {
GradientSurface { GradientSurface {
About( About(
onBack = {}, onBack = {},
versionInfo = VersionInfoFixture.new(),
configInfo = ConfigInfoFixture.new(), configInfo = ConfigInfoFixture.new(),
snackbarHostState = SnackbarHostState(),
onPrivacyPolicy = {}, onPrivacyPolicy = {},
snackbarHostState = SnackbarHostState(),
versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }
} }
@Composable @Composable
@Suppress("LongParameterList")
fun About( fun About(
onBack: () -> Unit, onBack: () -> Unit,
configInfo: ConfigInfo, configInfo: ConfigInfo,
onPrivacyPolicy: () -> Unit, onPrivacyPolicy: () -> Unit,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
versionInfo: VersionInfo, versionInfo: VersionInfo,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
AboutTopAppBar( AboutTopAppBar(
onBack = onBack, onBack = onBack,
versionInfo = versionInfo, versionInfo = versionInfo,
configInfo = configInfo configInfo = configInfo,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
) )
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
@ -105,9 +110,16 @@ fun About(
private fun AboutTopAppBar( private fun AboutTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
versionInfo: VersionInfo, versionInfo: VersionInfo,
configInfo: ConfigInfo configInfo: ConfigInfo,
showRestoring: Boolean
) { ) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.about_title).uppercase(), titleText = stringResource(id = R.string.about_title).uppercase(),
backText = stringResource(id = R.string.about_back).uppercase(), backText = stringResource(id = R.string.about_back).uppercase(),
backContentDescriptionText = stringResource(R.string.about_back_content_description), backContentDescriptionText = stringResource(R.string.about_back_content_description),
@ -116,7 +128,7 @@ private fun AboutTopAppBar(
if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) { if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) {
DebugMenu(versionInfo, configInfo) DebugMenu(versionInfo, configInfo)
} }
} },
) )
} }

View File

@ -12,6 +12,7 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import co.electriccoin.zcash.spackle.ClipboardManagerUtil import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
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.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
@ -19,7 +20,6 @@ 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.settings.viewmodel.SettingsViewModel
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
@ -36,12 +36,8 @@ internal fun WrapAccount(
val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>() val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val transactionsUiState = transactionHistoryViewModel.transactionUiState.collectAsStateWithLifecycle().value val transactionsUiState = transactionHistoryViewModel.transactionUiState.collectAsStateWithLifecycle().value
@ -50,16 +46,18 @@ internal fun WrapAccount(
transactionHistoryViewModel.processTransactionState(value) transactionHistoryViewModel.processTransactionState(value)
} }
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapAccount( WrapAccount(
context = activity.applicationContext, context = activity.applicationContext,
goBalances = goBalances, goBalances = goBalances,
goSettings = goSettings, goSettings = goSettings,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
scope = scope, scope = scope,
synchronizer = synchronizer, synchronizer = synchronizer,
transactionHistoryViewModel = transactionHistoryViewModel, transactionHistoryViewModel = transactionHistoryViewModel,
transactionsUiState = transactionsUiState, transactionsUiState = transactionsUiState,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
walletRestoringState = walletRestoringState
) )
// For benchmarking purposes // For benchmarking purposes
@ -78,7 +76,7 @@ internal fun WrapAccount(
synchronizer: Synchronizer?, synchronizer: Synchronizer?,
transactionHistoryViewModel: TransactionHistoryViewModel, transactionHistoryViewModel: TransactionHistoryViewModel,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
isKeepScreenOnWhileSyncing: Boolean?, walletRestoringState: WalletRestoringState,
) { ) {
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
@ -88,7 +86,6 @@ internal fun WrapAccount(
} else { } else {
Account( Account(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
transactionsUiState = transactionsUiState, transactionsUiState = transactionsUiState,
onTransactionItemAction = { action -> onTransactionItemAction = { action ->
when (action) { when (action) {
@ -130,6 +127,7 @@ internal fun WrapAccount(
}, },
goBalances = goBalances, goBalances = goBalances,
goSettings = goSettings, goSettings = goSettings,
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -5,7 +5,13 @@ import kotlinx.collections.immutable.ImmutableList
sealed interface TransactionUiState { sealed interface TransactionUiState {
data object Loading : TransactionUiState data object Loading : TransactionUiState
data object Syncing : TransactionUiState data object SyncingEmpty : TransactionUiState
data class Prepared(val transactions: ImmutableList<TransactionUi>) : TransactionUiState data object DoneEmpty : TransactionUiState
sealed class Prepared(open val transactions: ImmutableList<TransactionUi>) : TransactionUiState
data class Syncing(override val transactions: ImmutableList<TransactionUi>) : Prepared(transactions)
data class Done(override val transactions: ImmutableList<TransactionUi>) : Prepared(transactions)
} }

View File

@ -6,9 +6,7 @@ import kotlinx.collections.immutable.ImmutableList
sealed interface TransactionHistorySyncState { sealed interface TransactionHistorySyncState {
data object Loading : TransactionHistorySyncState data object Loading : TransactionHistorySyncState
sealed class Prepared(open val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState data class Syncing(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState
data class Syncing(override val transactions: ImmutableList<TransactionOverviewExt>) : Prepared(transactions) data class Done(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState
data class Done(override val transactions: ImmutableList<TransactionOverviewExt>) : Prepared(transactions)
} }

View File

@ -15,10 +15,9 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.Synchronizer
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceWidget import co.electriccoin.zcash.ui.common.compose.BalanceWidget
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout 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
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -36,11 +35,11 @@ private fun HistoryLoadingComposablePreview() {
GradientSurface { GradientSurface {
Account( Account(
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
isKeepScreenOnWhileSyncing = false,
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
transactionsUiState = TransactionUiState.Loading, transactionsUiState = TransactionUiState.Loading,
onTransactionItemAction = {} onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.SYNCING
) )
} }
} }
@ -54,11 +53,11 @@ private fun HistoryListComposablePreview() {
@Suppress("MagicNumber") @Suppress("MagicNumber")
Account( Account(
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
isKeepScreenOnWhileSyncing = false,
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
transactionsUiState = TransactionUiState.Prepared(transactions = TransactionsFixture.new()), transactionsUiState = TransactionUiState.Done(transactions = TransactionsFixture.new()),
onTransactionItemAction = {}, onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }
@ -69,19 +68,22 @@ private fun HistoryListComposablePreview() {
internal fun Account( internal fun Account(
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
isKeepScreenOnWhileSyncing: Boolean?,
onTransactionItemAction: (TrxItemAction) -> Unit, onTransactionItemAction: (TrxItemAction) -> Unit,
transactionsUiState: TransactionUiState, transactionsUiState: TransactionUiState,
walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
AccountTopAppBar(onSettings = goSettings) AccountTopAppBar(
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
onSettings = goSettings
)
}) { paddingValues -> }) { paddingValues ->
AccountMainContent( AccountMainContent(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
goBalances = goBalances, goBalances = goBalances,
transactionState = transactionsUiState, transactionState = transactionsUiState,
walletRestoringState = walletRestoringState,
onTransactionItemAction = onTransactionItemAction, onTransactionItemAction = onTransactionItemAction,
modifier = modifier =
Modifier.padding( Modifier.padding(
@ -94,8 +96,17 @@ internal fun Account(
} }
@Composable @Composable
private fun AccountTopAppBar(onSettings: () -> Unit) { private fun AccountTopAppBar(
onSettings: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
showTitleLogo = true, showTitleLogo = true,
hamburgerMenuActions = { hamburgerMenuActions = {
IconButton( IconButton(
@ -115,11 +126,11 @@ private fun AccountTopAppBar(onSettings: () -> Unit) {
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun AccountMainContent( private fun AccountMainContent(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnWhileSyncing: Boolean?,
goBalances: () -> Unit, goBalances: () -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit, onTransactionItemAction: (TrxItemAction) -> Unit,
transactionState: TransactionUiState, transactionState: TransactionUiState,
modifier: Modifier = Modifier walletRestoringState: WalletRestoringState,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
@ -139,12 +150,9 @@ private fun AccountMainContent(
HistoryContainer( HistoryContainer(
transactionState = transactionState, transactionState = transactionState,
walletRestoringState = walletRestoringState,
onTransactionItemAction = onTransactionItemAction, onTransactionItemAction = onTransactionItemAction,
) )
if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout()
}
} }
} }

View File

@ -45,6 +45,7 @@ import cash.z.ecc.android.sdk.model.TransactionState
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString import cash.z.ecc.android.sdk.model.toZecString
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.component.CircularMidProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularMidProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.StyledBalance import co.electriccoin.zcash.ui.design.component.StyledBalance
@ -70,7 +71,8 @@ private fun ComposablePreview() {
GradientSurface { GradientSurface {
HistoryContainer( HistoryContainer(
transactionState = TransactionUiState.Loading, transactionState = TransactionUiState.Loading,
onTransactionItemAction = {} onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.SYNCING
) )
} }
} }
@ -82,8 +84,9 @@ private fun ComposableHistoryListPreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
HistoryContainer( HistoryContainer(
transactionState = TransactionUiState.Prepared(transactions = TransactionsFixture.new()), transactionState = TransactionUiState.Done(transactions = TransactionsFixture.new()),
onTransactionItemAction = {} onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }
@ -102,7 +105,8 @@ private val dateFormat: DateFormat by lazy {
internal fun HistoryContainer( internal fun HistoryContainer(
transactionState: TransactionUiState, transactionState: TransactionUiState,
onTransactionItemAction: (TrxItemAction) -> Unit, onTransactionItemAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier walletRestoringState: WalletRestoringState,
modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = modifier =
@ -114,21 +118,49 @@ internal fun HistoryContainer(
) )
) { ) {
when (transactionState) { when (transactionState) {
TransactionUiState.Loading, TransactionUiState.Syncing -> { TransactionUiState.Loading -> {
LoadingTransactionHistory()
}
TransactionUiState.SyncingEmpty -> {
if (walletRestoringState == WalletRestoringState.INITIATING) {
// In case we are syncing a new wallet, it's empty
EmptyTransactionHistory()
} else {
// Intentionally leaving the UI empty otherwise
}
}
is TransactionUiState.Prepared -> {
HistoryList(
transactions = transactionState.transactions,
onAction = onTransactionItemAction,
)
}
is TransactionUiState.DoneEmpty -> {
EmptyTransactionHistory()
}
}
}
}
@Composable
private fun LoadingTransactionHistory() {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
CircularMidProgressIndicator( CircularMidProgressIndicator(
modifier = Modifier.testTag(HistoryTag.PROGRESS), modifier = Modifier.testTag(HistoryTag.PROGRESS),
) )
} }
} }
is TransactionUiState.Prepared -> {
if (transactionState.transactions.isEmpty()) { @Composable
private fun EmptyTransactionHistory() {
Column { Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -136,18 +168,9 @@ internal fun HistoryContainer(
style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular, style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
color = ZcashTheme.colors.textCommon, color = ZcashTheme.colors.textCommon,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
) )
} }
} else {
HistoryList(
transactions = transactionState.transactions,
onAction = onTransactionItemAction,
)
}
}
}
}
} }
@Composable @Composable

View File

@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.TransactionOverview
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi 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
@ -19,19 +20,23 @@ import kotlinx.coroutines.flow.MutableStateFlow
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
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
class TransactionHistoryViewModel(application: Application) : AndroidViewModel(application) { class TransactionHistoryViewModel(application: Application) : AndroidViewModel(application) {
private val state: MutableStateFlow<State> = MutableStateFlow(State.LOADING)
private val transactions: MutableStateFlow<ImmutableList<TransactionUi>> = MutableStateFlow(persistentListOf()) private val transactions: MutableStateFlow<ImmutableList<TransactionUi>> = MutableStateFlow(persistentListOf())
val transactionUiState: StateFlow<TransactionUiState> = val transactionUiState: StateFlow<TransactionUiState> =
transactions.map { state.combine(transactions) { state: State, transactions: ImmutableList<TransactionUi> ->
if (it.isEmpty()) { when (state) {
TransactionUiState.Syncing State.LOADING -> TransactionUiState.Loading
} else { State.SYNCING -> TransactionUiState.Syncing(transactions)
TransactionUiState.Prepared(it) State.SYNCING_EMPTY -> TransactionUiState.SyncingEmpty
State.DONE -> TransactionUiState.Done(transactions)
State.DONE_EMPTY -> TransactionUiState.DoneEmpty
} }
}.stateIn( }.stateIn(
viewModelScope, viewModelScope,
@ -40,26 +45,49 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
) )
fun processTransactionState(dataState: TransactionHistorySyncState) { fun processTransactionState(dataState: TransactionHistorySyncState) {
transactions.value =
when (dataState) { when (dataState) {
TransactionHistorySyncState.Loading -> persistentListOf() TransactionHistorySyncState.Loading -> {
is TransactionHistorySyncState.Prepared -> { state.value = State.LOADING
dataState.transactions.map { data -> transactions.value = persistentListOf()
val existingTransaction = }
transactions.value.find { is TransactionHistorySyncState.Syncing -> {
data.overview.rawId == it.overview.rawId if (dataState.transactions.isEmpty()) {
state.value = State.SYNCING_EMPTY
} else {
state.value = State.SYNCING
transactions.value =
dataState.transactions
.map { data -> getOrUpdateTransactionItem(data) }
.toPersistentList()
}
}
is TransactionHistorySyncState.Done -> {
if (dataState.transactions.isEmpty()) {
state.value = State.DONE_EMPTY
} else {
state.value = State.DONE
transactions.value =
dataState.transactions
.map { data -> getOrUpdateTransactionItem(data) }
.toPersistentList()
} }
TransactionUi.new(
data = data,
expandableState = existingTransaction?.expandableState ?: TrxItemState.COLLAPSED,
messages = existingTransaction?.messages,
)
}.toPersistentList()
} }
} }
} }
private fun updateTransaction(newTransaction: TransactionUi) { private fun getOrUpdateTransactionItem(data: TransactionOverviewExt): TransactionUi {
val existingTransaction =
transactions.value.find {
data.overview.rawId == it.overview.rawId
}
return TransactionUi.new(
data = data,
expandableState = existingTransaction?.expandableState ?: TrxItemState.COLLAPSED,
messages = existingTransaction?.messages,
)
}
private fun updateTransactionInList(newTransaction: TransactionUi) {
transactions.value = transactions.value =
transactions.value.map { item -> transactions.value.map { item ->
if (item.overview.rawId == newTransaction.overview.rawId) { if (item.overview.rawId == newTransaction.overview.rawId) {
@ -87,7 +115,7 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
val messages = loadMessageForTransaction(synchronizer, updated.overview) val messages = loadMessageForTransaction(synchronizer, updated.overview)
updatedWithMessages = updated.copy(messages = messages) updatedWithMessages = updated.copy(messages = messages)
} }
updateTransaction(updatedWithMessages) updateTransactionInList(updatedWithMessages)
} else { } else {
Twig.warn { "Transaction not found" } Twig.warn { "Transaction not found" }
} }
@ -101,3 +129,11 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
Twig.info { "Transaction messages count: ${it.size}" } Twig.info { "Transaction messages count: ${it.size}" }
} }
} }
private enum class State {
LOADING,
SYNCING,
SYNCING_EMPTY,
DONE,
DONE_EMPTY,
}

View File

@ -3,31 +3,41 @@
package co.electriccoin.zcash.ui.screen.advancedsettings package co.electriccoin.zcash.ui.screen.advancedsettings
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings
@Composable @Composable
internal fun WrapAdvancedSettings( internal fun MainActivity.WrapAdvancedSettings(
goBack: () -> Unit, goBack: () -> Unit,
goExportPrivateData: () -> Unit, goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit, goSeedRecovery: () -> Unit,
goChooseServer: () -> Unit, goChooseServer: () -> Unit,
) { ) {
WrapSettings( val walletViewModel by viewModels<WalletViewModel>()
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapAdvancedSettings(
goBack = goBack, goBack = goBack,
goExportPrivateData = goExportPrivateData, goExportPrivateData = goExportPrivateData,
goChooseServer = goChooseServer, goChooseServer = goChooseServer,
goSeedRecovery = goSeedRecovery, goSeedRecovery = goSeedRecovery,
walletRestoringState = walletRestoringState
) )
} }
@Composable @Composable
@Suppress("LongParameterList") private fun WrapAdvancedSettings(
private fun WrapSettings(
goBack: () -> Unit, goBack: () -> Unit,
goExportPrivateData: () -> Unit, goExportPrivateData: () -> Unit,
goChooseServer: () -> Unit, goChooseServer: () -> Unit,
goSeedRecovery: () -> Unit, goSeedRecovery: () -> Unit,
walletRestoringState: WalletRestoringState,
) { ) {
BackHandler { BackHandler {
goBack() goBack()
@ -37,6 +47,7 @@ private fun WrapSettings(
onBack = goBack, onBack = goBack,
onSeedRecovery = goSeedRecovery, onSeedRecovery = goSeedRecovery,
onExportPrivateData = goExportPrivateData, onExportPrivateData = goExportPrivateData,
onChooseServer = goChooseServer onChooseServer = goChooseServer,
walletRestoringState = walletRestoringState,
) )
} }

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
@ -34,8 +35,9 @@ private fun PreviewAdvancedSettings() {
AdvancedSettings( AdvancedSettings(
onBack = {}, onBack = {},
onExportPrivateData = {}, onExportPrivateData = {},
onSeedRecovery = {},
onChooseServer = {}, onChooseServer = {},
onSeedRecovery = {},
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }
@ -47,10 +49,12 @@ fun AdvancedSettings(
onExportPrivateData: () -> Unit, onExportPrivateData: () -> Unit,
onChooseServer: () -> Unit, onChooseServer: () -> Unit,
onSeedRecovery: () -> Unit, onSeedRecovery: () -> Unit,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
AdvancedSettingsTopAppBar( AdvancedSettingsTopAppBar(
onBack = onBack, onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
) )
}) { paddingValues -> }) { paddingValues ->
AdvancedSettingsMainContent( AdvancedSettingsMainContent(
@ -73,13 +77,22 @@ fun AdvancedSettings(
} }
@Composable @Composable
private fun AdvancedSettingsTopAppBar(onBack: () -> Unit) { private fun AdvancedSettingsTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
modifier = Modifier.testTag(AdvancedSettingsTag.ADVANCED_SETTINGS_TOP_APP_BAR),
showTitleLogo = true,
backText = stringResource(id = R.string.advanced_settings_back).uppercase(), backText = stringResource(id = R.string.advanced_settings_back).uppercase(),
backContentDescriptionText = stringResource(R.string.advanced_settings_back_content_description), backContentDescriptionText = stringResource(R.string.advanced_settings_back_content_description),
onBack = onBack, onBack = onBack,
showTitleLogo = true,
modifier = Modifier.testTag(AdvancedSettingsTag.ADVANCED_SETTINGS_TOP_APP_BAR)
) )
} }

View File

@ -14,6 +14,7 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
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.CheckUpdateViewModel import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -24,7 +25,6 @@ import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
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.settings.viewmodel.SettingsViewModel
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 kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -47,6 +47,8 @@ internal fun WrapBalances(
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> { val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory( CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application, activity.application,
@ -54,17 +56,15 @@ internal fun WrapBalances(
) )
} }
val settingsViewModel by activity.viewModels<SettingsViewModel>()
WrapBalances( WrapBalances(
checkUpdateViewModel = checkUpdateViewModel, checkUpdateViewModel = checkUpdateViewModel,
createTransactionsViewModel = createTransactionsViewModel, createTransactionsViewModel = createTransactionsViewModel,
goSettings = goSettings, goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure, goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
spendingKey = spendingKey, spendingKey = spendingKey,
settingsViewModel = settingsViewModel,
synchronizer = synchronizer, synchronizer = synchronizer,
walletSnapshot = walletSnapshot walletSnapshot = walletSnapshot,
walletRestoringState = walletRestoringState,
) )
} }
@ -78,10 +78,10 @@ internal fun WrapBalances(
createTransactionsViewModel: CreateTransactionsViewModel, createTransactionsViewModel: CreateTransactionsViewModel,
goSettings: () -> Unit, goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
settingsViewModel: SettingsViewModel,
spendingKey: UnifiedSpendingKey?, spendingKey: UnifiedSpendingKey?,
synchronizer: Synchronizer?, synchronizer: Synchronizer?,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
walletRestoringState: WalletRestoringState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -91,8 +91,6 @@ internal fun WrapBalances(
it?.appUpdateInfo != null && it.state == UpdateState.Prepared it?.appUpdateInfo != null && it.state == UpdateState.Prepared
} }
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current) val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current)
val (shieldState, setShieldState) = val (shieldState, setShieldState) =
@ -128,7 +126,6 @@ internal fun WrapBalances(
Balances( Balances(
onSettings = goSettings, onSettings = goSettings,
isFiatConversionEnabled = isFiatConversionEnabled, isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
isShowingErrorDialog = isShowingErrorDialog, isShowingErrorDialog = isShowingErrorDialog,
setShowErrorDialog = setShowErrorDialog, setShowErrorDialog = setShowErrorDialog,
@ -187,6 +184,7 @@ internal fun WrapBalances(
}, },
shieldState = shieldState, shieldState = shieldState,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
walletRestoringState = walletRestoringState,
) )
} }
} }

View File

@ -47,14 +47,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
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
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString import cash.z.ecc.android.sdk.model.toZecString
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.compose.BalanceWidget import co.electriccoin.zcash.ui.common.compose.BalanceWidget
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout 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.model.changePendingBalance import co.electriccoin.zcash.ui.common.model.changePendingBalance
import co.electriccoin.zcash.ui.common.model.spendableBalance import co.electriccoin.zcash.ui.common.model.spendableBalance
@ -86,13 +85,13 @@ private fun ComposableBalancesPreview() {
Balances( Balances(
onSettings = {}, onSettings = {},
isFiatConversionEnabled = false, isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false, isUpdateAvailable = false,
isShowingErrorDialog = false,
setShowErrorDialog = {},
onShielding = {}, onShielding = {},
shieldState = ShieldState.Available, shieldState = ShieldState.Available,
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
isShowingErrorDialog = false, walletRestoringState = WalletRestoringState.NONE,
setShowErrorDialog = {},
) )
} }
} }
@ -106,13 +105,13 @@ private fun ComposableBalancesShieldFailurePreview() {
Balances( Balances(
onSettings = {}, onSettings = {},
isFiatConversionEnabled = false, isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false, isUpdateAvailable = false,
isShowingErrorDialog = true,
setShowErrorDialog = {},
onShielding = {}, onShielding = {},
shieldState = ShieldState.Available, shieldState = ShieldState.Available,
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
isShowingErrorDialog = true, walletRestoringState = WalletRestoringState.NONE,
setShowErrorDialog = {},
) )
} }
} }
@ -123,23 +122,25 @@ private fun ComposableBalancesShieldFailurePreview() {
fun Balances( fun Balances(
onSettings: () -> Unit, onSettings: () -> Unit,
isFiatConversionEnabled: Boolean, isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean, isUpdateAvailable: Boolean,
isShowingErrorDialog: Boolean, isShowingErrorDialog: Boolean,
setShowErrorDialog: (Boolean) -> Unit, setShowErrorDialog: (Boolean) -> Unit,
onShielding: () -> Unit, onShielding: () -> Unit,
shieldState: ShieldState, shieldState: ShieldState,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
BalancesTopAppBar(onSettings = onSettings) BalancesTopAppBar(
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
onSettings = onSettings
)
}) { paddingValues -> }) { paddingValues ->
if (null == walletSnapshot) { if (null == walletSnapshot) {
CircularScreenProgressIndicator() CircularScreenProgressIndicator()
} else { } else {
BalancesMainContent( BalancesMainContent(
isFiatConversionEnabled = isFiatConversionEnabled, isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
onShielding = onShielding, onShielding = onShielding,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
@ -197,10 +198,19 @@ fun ShieldingErrorDialog(
} }
@Composable @Composable
private fun BalancesTopAppBar(onSettings: () -> Unit) { private fun BalancesTopAppBar(
onSettings: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar( SmallTopAppBar(
showTitleLogo = false, restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.balances_title), titleText = stringResource(id = R.string.balances_title),
showTitleLogo = false,
hamburgerMenuActions = { hamburgerMenuActions = {
IconButton( IconButton(
onClick = onSettings, onClick = onSettings,
@ -211,7 +221,7 @@ private fun BalancesTopAppBar(onSettings: () -> Unit) {
contentDescription = stringResource(id = R.string.settings_menu_content_description) contentDescription = stringResource(id = R.string.settings_menu_content_description)
) )
} }
} },
) )
} }
@ -219,7 +229,6 @@ private fun BalancesTopAppBar(onSettings: () -> Unit) {
@Composable @Composable
private fun BalancesMainContent( private fun BalancesMainContent(
isFiatConversionEnabled: Boolean, isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean, isUpdateAvailable: Boolean,
onShielding: () -> Unit, onShielding: () -> Unit,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
@ -272,10 +281,6 @@ private fun BalancesMainContent(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
) )
if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout()
}
} }
} }

View File

@ -3,6 +3,7 @@
package co.electriccoin.zcash.ui.screen.chooseserver package co.electriccoin.zcash.ui.screen.chooseserver
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -18,17 +19,23 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.ServerValidation import cash.z.ecc.android.sdk.type.ServerValidation
import cash.z.ecc.sdk.type.fromResources import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.chooseserver.view.ChooseServer import co.electriccoin.zcash.ui.screen.chooseserver.view.ChooseServer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
internal fun MainActivity.WrapChooseServer(goBack: () -> Unit) { internal fun MainActivity.WrapChooseServer(goBack: () -> Unit) {
val walletViewModel by viewModels<WalletViewModel>()
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapChooseServer( WrapChooseServer(
activity = this, activity = this,
goBack = goBack, goBack = goBack,
@ -39,7 +46,8 @@ internal fun MainActivity.WrapChooseServer(goBack: () -> Unit) {
}, },
onWalletPersist = { onWalletPersist = {
walletViewModel.persistExistingWallet(it) walletViewModel.persistExistingWallet(it)
} },
walletRestoringState = walletRestoringState
) )
} }
@ -52,6 +60,7 @@ private fun WrapChooseServer(
onWalletPersist: (PersistableWallet) -> Unit, onWalletPersist: (PersistableWallet) -> Unit,
secretState: SecretState, secretState: SecretState,
synchronizer: Synchronizer?, synchronizer: Synchronizer?,
walletRestoringState: WalletRestoringState,
) { ) {
if (synchronizer == null || secretState !is SecretState.Ready) { if (synchronizer == null || secretState !is SecretState.Ready) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -118,7 +127,8 @@ private fun WrapChooseServer(
isShowingErrorDialog = isShowingErrorDialog, isShowingErrorDialog = isShowingErrorDialog,
setShowErrorDialog = setShowErrorDialog, setShowErrorDialog = setShowErrorDialog,
isShowingSuccessDialog = isShowingSuccessDialog, isShowingSuccessDialog = isShowingSuccessDialog,
setShowSuccessDialog = setShowSuccessDialog setShowSuccessDialog = setShowSuccessDialog,
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -34,6 +34,7 @@ import cash.z.ecc.sdk.extension.isValid
import cash.z.ecc.sdk.fixture.PersistableWalletFixture import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.component.AppAlertDialog import co.electriccoin.zcash.ui.design.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.FormTextField import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -62,6 +63,7 @@ private fun PreviewChooseServer() {
setShowErrorDialog = {}, setShowErrorDialog = {},
isShowingSuccessDialog = false, isShowingSuccessDialog = false,
setShowSuccessDialog = {}, setShowSuccessDialog = {},
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }
@ -79,10 +81,14 @@ fun ChooseServer(
setShowErrorDialog: (Boolean) -> Unit, setShowErrorDialog: (Boolean) -> Unit,
isShowingSuccessDialog: Boolean, isShowingSuccessDialog: Boolean,
setShowSuccessDialog: (Boolean) -> Unit, setShowSuccessDialog: (Boolean) -> Unit,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
ChooseServerTopAppBar(onBack = onBack) ChooseServerTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
} }
) { paddingValues -> ) { paddingValues ->
ChooseServerMainContent( ChooseServerMainContent(
@ -120,13 +126,22 @@ fun ChooseServer(
} }
@Composable @Composable
private fun ChooseServerTopAppBar(onBack: () -> Unit) { private fun ChooseServerTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
modifier = Modifier.testTag(ChooseServerTag.CHOOSE_SERVER_TOP_APP_BAR),
showTitleLogo = true,
backText = stringResource(id = R.string.choose_server_back).uppercase(), backText = stringResource(id = R.string.choose_server_back).uppercase(),
backContentDescriptionText = stringResource(R.string.choose_server_back_content_description), backContentDescriptionText = stringResource(R.string.choose_server_back_content_description),
onBack = onBack, onBack = onBack,
showTitleLogo = true,
modifier = Modifier.testTag(ChooseServerTag.CHOOSE_SERVER_TOP_APP_BAR)
) )
} }

View File

@ -15,6 +15,7 @@ import cash.z.ecc.sdk.type.fromResources
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.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData
@ -29,10 +30,18 @@ internal fun MainActivity.WrapExportPrivateData(
goBack: () -> Unit, goBack: () -> Unit,
onConfirm: () -> Unit onConfirm: () -> Unit
) { ) {
val walletViewModel by viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapExportPrivateData( WrapExportPrivateData(
this, this,
onBack = goBack, onBack = goBack,
onShare = onConfirm onShare = onConfirm,
synchronizer = synchronizer,
walletRestoringState = walletRestoringState,
) )
} }
@ -40,11 +49,10 @@ internal fun MainActivity.WrapExportPrivateData(
internal fun WrapExportPrivateData( internal fun WrapExportPrivateData(
activity: ComponentActivity, activity: ComponentActivity,
onBack: () -> Unit, onBack: () -> Unit,
onShare: () -> Unit onShare: () -> Unit,
synchronizer: Synchronizer?,
walletRestoringState: WalletRestoringState,
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (synchronizer == null) { if (synchronizer == null) {
// 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
@ -72,7 +80,8 @@ internal fun WrapExportPrivateData(
} }
} }
} }
} },
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -23,6 +23,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.CheckBox import co.electriccoin.zcash.ui.design.component.CheckBox
@ -42,6 +43,7 @@ private fun ExportPrivateDataPreview() {
onBack = {}, onBack = {},
onAgree = {}, onAgree = {},
onConfirm = {}, onConfirm = {},
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }
@ -56,9 +58,15 @@ fun ExportPrivateData(
onBack: () -> Unit, onBack: () -> Unit,
onAgree: (Boolean) -> Unit, onAgree: (Boolean) -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold( Scaffold(
topBar = { ExportPrivateDataTopAppBar(onBack = onBack) }, topBar = {
ExportPrivateDataTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues -> ) { paddingValues ->
ExportPrivateDataContent( ExportPrivateDataContent(
@ -79,8 +87,17 @@ fun ExportPrivateData(
} }
@Composable @Composable
private fun ExportPrivateDataTopAppBar(onBack: () -> Unit) { private fun ExportPrivateDataTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
backText = stringResource(R.string.export_data_back).uppercase(), backText = stringResource(R.string.export_data_back).uppercase(),
backContentDescriptionText = stringResource(R.string.export_data_back_content_description), backContentDescriptionText = stringResource(R.string.export_data_back_content_description),
onBack = onBack, onBack = onBack,

View File

@ -8,10 +8,16 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.ZecSend 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.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.common.viewmodel.isSynced
import co.electriccoin.zcash.ui.screen.account.WrapAccount import co.electriccoin.zcash.ui.screen.account.WrapAccount
import co.electriccoin.zcash.ui.screen.balances.WrapBalances import co.electriccoin.zcash.ui.screen.balances.WrapBalances
import co.electriccoin.zcash.ui.screen.home.model.TabItem import co.electriccoin.zcash.ui.screen.home.model.TabItem
@ -19,6 +25,7 @@ import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.receive.WrapReceive import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.send.WrapSend import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.model.SendArguments import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -26,7 +33,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal fun MainActivity.WrapHome( internal fun MainActivity.WrapHome(
onPageChange: (HomeScreenIndex) -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
@ -34,15 +40,39 @@ internal fun MainActivity.WrapHome(
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
sendArguments: SendArguments sendArguments: SendArguments
) { ) {
val homeViewModel by viewModels<HomeViewModel>()
val walletViewModel by viewModels<WalletViewModel>()
val settingsViewModel by viewModels<SettingsViewModel>()
val homeScreenIndex = homeViewModel.screenIndex.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
// Once the wallet is fully synced and still in restoring state, persist the new state
if (walletSnapshot?.status?.isSynced() == true && walletRestoringState.isRunningRestoring()) {
walletViewModel.persistWalletRestoringState(WalletRestoringState.SYNCING)
}
WrapHome( WrapHome(
this, this,
onPageChange = onPageChange,
goBack = goBack, goBack = goBack,
goScan = goScan, goScan = goScan,
goSendConfirmation = goSendConfirmation, goSendConfirmation = goSendConfirmation,
goSettings = goSettings, goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure, goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
sendArguments = sendArguments homeScreenIndex = homeScreenIndex,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
onPageChange = {
homeViewModel.screenIndex.value = it
},
sendArguments = sendArguments,
walletSnapshot = walletSnapshot
) )
} }
@ -55,11 +85,12 @@ internal fun WrapHome(
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
goScan: () -> Unit, goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
homeScreenIndex: HomeScreenIndex,
isKeepScreenOnWhileSyncing: Boolean?,
onPageChange: (HomeScreenIndex) -> Unit, onPageChange: (HomeScreenIndex) -> Unit,
sendArguments: SendArguments sendArguments: SendArguments,
walletSnapshot: WalletSnapshot?,
) { ) {
val homeViewModel by activity.viewModels<HomeViewModel>()
// Flow for propagating the new page index to the pager in the view layer // Flow for propagating the new page index to the pager in the view layer
val forceHomePageIndexFlow: MutableSharedFlow<ForcePage?> = val forceHomePageIndexFlow: MutableSharedFlow<ForcePage?> =
MutableSharedFlow( MutableSharedFlow(
@ -70,7 +101,7 @@ internal fun WrapHome(
val forceIndex = forceHomePageIndexFlow.collectAsState(initial = null).value val forceIndex = forceHomePageIndexFlow.collectAsState(initial = null).value
val homeGoBack: () -> Unit = { val homeGoBack: () -> Unit = {
when (homeViewModel.screenIndex.value) { when (homeScreenIndex) {
HomeScreenIndex.ACCOUNT -> goBack() HomeScreenIndex.ACCOUNT -> goBack()
HomeScreenIndex.SEND, HomeScreenIndex.SEND,
HomeScreenIndex.RECEIVE, HomeScreenIndex.RECEIVE,
@ -82,6 +113,11 @@ internal fun WrapHome(
homeGoBack() homeGoBack()
} }
// Reset the screen brightness for all pages except Receive which maintain the screen brightness by itself
if (homeScreenIndex != HomeScreenIndex.RECEIVE) {
RestoreScreenBrightness()
}
val tabs = val tabs =
persistentListOf( persistentListOf(
TabItem( TabItem(
@ -92,7 +128,7 @@ internal fun WrapHome(
WrapAccount( WrapAccount(
activity = activity, activity = activity,
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) }, goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
goSettings = goSettings, goSettings = goSettings
) )
} }
), ),
@ -119,7 +155,7 @@ internal fun WrapHome(
screenContent = { screenContent = {
WrapReceive( WrapReceive(
activity = activity, activity = activity,
onSettings = goSettings, onSettings = goSettings
) )
} }
), ),
@ -140,7 +176,9 @@ internal fun WrapHome(
Home( Home(
subScreens = tabs, subScreens = tabs,
forcePage = forceIndex, forcePage = forceIndex,
onPageChange = onPageChange isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
onPageChange = onPageChange,
walletSnapshot = walletSnapshot
) )
} }

View File

@ -24,10 +24,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Dimension
import cash.z.ecc.android.sdk.Synchronizer
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.NavigationTabText import co.electriccoin.zcash.ui.design.component.NavigationTabText
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.screen.home.ForcePage import co.electriccoin.zcash.ui.screen.home.ForcePage
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
import co.electriccoin.zcash.ui.screen.home.model.TabItem import co.electriccoin.zcash.ui.screen.home.model.TabItem
@ -42,9 +46,11 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Home( Home(
subScreens = persistentListOf(), isKeepScreenOnWhileSyncing = false,
forcePage = null, forcePage = null,
onPageChange = {} onPageChange = {},
subScreens = persistentListOf(),
walletSnapshot = WalletSnapshotFixture.new(),
) )
} }
} }
@ -54,9 +60,11 @@ private fun ComposablePreview() {
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun Home( fun Home(
subScreens: ImmutableList<TabItem>, isKeepScreenOnWhileSyncing: Boolean?,
forcePage: ForcePage?, forcePage: ForcePage?,
onPageChange: (HomeScreenIndex) -> Unit, onPageChange: (HomeScreenIndex) -> Unit,
subScreens: ImmutableList<TabItem>,
walletSnapshot: WalletSnapshot?,
) { ) {
val pagerState = val pagerState =
rememberPagerState( rememberPagerState(
@ -169,4 +177,10 @@ fun Home(
} }
} }
} }
if (isKeepScreenOnWhileSyncing == true &&
walletSnapshot?.status == Synchronizer.Status.SYNCING
) {
DisableScreenTimeout()
}
} }

View File

@ -121,7 +121,7 @@ private fun NewWalletRecoveryTopAppBar(
if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) { if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) {
DebugMenu(onCopyToClipboard = onSeedCopy) DebugMenu(onCopyToClipboard = onSeedCopy)
} }
} },
) )
} }

View File

@ -20,6 +20,7 @@ import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.view.ShortOnboarding import co.electriccoin.zcash.ui.screen.onboarding.view.ShortOnboarding
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
@ -117,4 +118,5 @@ internal fun persistExistingWalletWithSeedPhrase(
walletInitMode = WalletInitMode.RestoreWallet walletInitMode = WalletInitMode.RestoreWallet
) )
walletViewModel.persistExistingWallet(restoredWallet) walletViewModel.persistExistingWallet(restoredWallet)
walletViewModel.persistWalletRestoringState(WalletRestoringState.RESTORING)
} }

View File

@ -164,7 +164,7 @@ private fun OnboardingMainContent(
if (isDebugMenuEnabled) { if (isDebugMenuEnabled) {
DebugMenu(onFixtureWallet) DebugMenu(onFixtureWallet)
} }
} },
) )
Column( Column(
modifier = modifier, modifier = modifier,

View File

@ -16,9 +16,11 @@ import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.spackle.getInternalCacheDirSuspend import co.electriccoin.zcash.spackle.getInternalCacheDirSuspend
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.receive.view.Receive import co.electriccoin.zcash.ui.screen.receive.view.Receive
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
import co.electriccoin.zcash.ui.util.FileShareUtil import co.electriccoin.zcash.ui.util.FileShareUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -33,8 +35,15 @@ internal fun WrapReceive(
activity: ComponentActivity, activity: ComponentActivity,
onSettings: () -> Unit, onSettings: () -> Unit,
) { ) {
val viewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value
val brightnessViewModel by activity.viewModels<ScreenBrightnessViewModel>()
val screenBrightnessState = brightnessViewModel.screenBrightnessState.collectAsStateWithLifecycle().value
val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -42,9 +51,13 @@ internal fun WrapReceive(
val versionInfo = VersionInfo.new(activity.applicationContext) val versionInfo = VersionInfo.new(activity.applicationContext)
Receive( Receive(
walletAddress = walletAddresses, screenBrightnessState = screenBrightnessState,
snackbarHostState = snackbarHostState, onAdjustBrightness = {
onAdjustBrightness = { /* Just for testing purposes */ }, when (it) {
ScreenBrightnessState.NORMAL -> brightnessViewModel.restoreBrightness()
ScreenBrightnessState.FULL -> brightnessViewModel.fullBrightness()
}
},
onAddrCopyToClipboard = { address -> onAddrCopyToClipboard = { address ->
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
@ -72,7 +85,10 @@ internal fun WrapReceive(
} }
}, },
onSettings = onSettings, onSettings = onSettings,
versionInfo = versionInfo snackbarHostState = snackbarHostState,
versionInfo = versionInfo,
walletAddress = walletAddresses,
walletRestoringState = walletRestoringState,
) )
} }

View File

@ -13,9 +13,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrightnessHigh
import androidx.compose.material.icons.filled.BrightnessLow
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
@ -23,9 +20,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState 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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
@ -44,7 +39,9 @@ import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BrightenScreen import co.electriccoin.zcash.ui.common.compose.BrightenScreen
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -64,13 +61,15 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Receive( Receive(
screenBrightnessState = ScreenBrightnessState.NORMAL,
walletAddress = runBlocking { WalletAddressesFixture.new() }, walletAddress = runBlocking { WalletAddressesFixture.new() },
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
onSettings = {}, onSettings = {},
onAdjustBrightness = {}, onAdjustBrightness = {},
onAddrCopyToClipboard = {}, onAddrCopyToClipboard = {},
onQrImageShare = {}, onQrImageShare = {},
versionInfo = VersionInfoFixture.new() versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }
@ -79,25 +78,24 @@ private fun ComposablePreview() {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun Receive( fun Receive(
screenBrightnessState: ScreenBrightnessState,
walletAddress: WalletAddresses?, walletAddress: WalletAddresses?,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
onSettings: () -> Unit, onSettings: () -> Unit,
onAdjustBrightness: (Boolean) -> Unit, onAdjustBrightness: (ScreenBrightnessState) -> Unit,
onAddrCopyToClipboard: (String) -> Unit, onAddrCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit, onQrImageShare: (ImageBitmap) -> Unit,
versionInfo: VersionInfo, versionInfo: VersionInfo,
walletRestoringState: WalletRestoringState,
) { ) {
val (brightness, setBrightness) = rememberSaveable { mutableStateOf(false) }
Scaffold( Scaffold(
topBar = { topBar = {
ReceiveTopAppBar( ReceiveTopAppBar(
adjustBrightness = brightness,
onSettings = onSettings, onSettings = onSettings,
onBrightness = { onBrightness = {
onAdjustBrightness(!brightness) onAdjustBrightness(screenBrightnessState.getChange())
setBrightness(!brightness)
}, },
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
versionInfo = versionInfo, versionInfo = versionInfo,
) )
}, },
@ -110,7 +108,7 @@ fun Receive(
walletAddress = walletAddress, walletAddress = walletAddress,
onAddressCopyToClipboard = onAddrCopyToClipboard, onAddressCopyToClipboard = onAddrCopyToClipboard,
onQrImageShare = onQrImageShare, onQrImageShare = onQrImageShare,
adjustBrightness = brightness, screenBrightnessState = screenBrightnessState,
versionInfo = versionInfo, versionInfo = versionInfo,
modifier = modifier =
Modifier.padding( Modifier.padding(
@ -126,30 +124,19 @@ fun Receive(
@Composable @Composable
private fun ReceiveTopAppBar( private fun ReceiveTopAppBar(
adjustBrightness: Boolean,
onSettings: () -> Unit, onSettings: () -> Unit,
onBrightness: () -> Unit, onBrightness: () -> Unit,
versionInfo: VersionInfo versionInfo: VersionInfo,
showRestoring: Boolean
) { ) {
SmallTopAppBar( SmallTopAppBar(
titleText = stringResource(id = R.string.receive_title), restoringLabel =
regularActions = { if (showRestoring) {
if (versionInfo.isDebuggable) { stringResource(id = R.string.restoring_wallet_label)
IconButton(
onClick = onBrightness
) {
Icon(
imageVector =
if (adjustBrightness) {
Icons.Default.BrightnessLow
} else { } else {
Icons.Default.BrightnessHigh null
},
contentDescription = stringResource(R.string.receive_brightness_content_description)
)
}
}
}, },
titleText = stringResource(id = R.string.receive_title),
hamburgerMenuActions = { hamburgerMenuActions = {
IconButton( IconButton(
onClick = onSettings, onClick = onSettings,
@ -160,7 +147,19 @@ private fun ReceiveTopAppBar(
contentDescription = stringResource(id = R.string.settings_menu_content_description) contentDescription = stringResource(id = R.string.settings_menu_content_description)
) )
} }
},
regularActions = {
if (versionInfo.isDebuggable) {
IconButton(
onClick = onBrightness
) {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_adjust_brightness),
contentDescription = stringResource(R.string.receive_brightness_content_description)
)
} }
}
},
) )
} }
@ -170,7 +169,7 @@ private fun ReceiveContents(
walletAddress: WalletAddresses, walletAddress: WalletAddresses,
onAddressCopyToClipboard: (String) -> Unit, onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit, onQrImageShare: (ImageBitmap) -> Unit,
adjustBrightness: Boolean, screenBrightnessState: ScreenBrightnessState,
versionInfo: VersionInfo, versionInfo: VersionInfo,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -182,7 +181,7 @@ private fun ReceiveContents(
.then(modifier), .then(modifier),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (adjustBrightness) { if (screenBrightnessState == ScreenBrightnessState.FULL) {
BrightenScreen() BrightenScreen()
DisableScreenTimeout() DisableScreenTimeout()
} }

View File

@ -294,6 +294,7 @@ private fun RestoreSeedTopAppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
SmallTopAppBar( SmallTopAppBar(
modifier = modifier,
backText = stringResource(id = R.string.restore_back).uppercase(), backText = stringResource(id = R.string.restore_back).uppercase(),
backContentDescriptionText = stringResource(R.string.restore_back_content_description), backContentDescriptionText = stringResource(R.string.restore_back_content_description),
onBack = onBack, onBack = onBack,
@ -302,7 +303,6 @@ private fun RestoreSeedTopAppBar(
onSeedClear = onClear onSeedClear = onClear
) )
}, },
modifier = modifier,
) )
} }
@ -312,10 +312,10 @@ private fun RestoreSeedBirthdayTopAppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
SmallTopAppBar( SmallTopAppBar(
modifier = modifier,
backText = stringResource(id = R.string.restore_back).uppercase(), backText = stringResource(id = R.string.restore_back).uppercase(),
backContentDescriptionText = stringResource(R.string.restore_back_content_description), backContentDescriptionText = stringResource(R.string.restore_back_content_description),
onBack = onBack, onBack = onBack,
modifier = modifier
) )
} }

View File

@ -4,10 +4,12 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import co.electriccoin.zcash.spackle.ClipboardManagerUtil import co.electriccoin.zcash.spackle.ClipboardManagerUtil
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.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
@ -16,31 +18,44 @@ import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery
@Composable @Composable
internal fun MainActivity.WrapSeedRecovery( internal fun MainActivity.WrapSeedRecovery(
goBack: () -> Unit, goBack: () -> Unit,
onDone: () -> Unit onDone: () -> Unit,
) { ) {
WrapSeedRecovery(this, goBack, onDone) val walletViewModel by viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSeedRecovery(
activity = this,
goBack = goBack,
onDone = onDone,
secretState = secretState,
synchronizer = synchronizer,
walletRestoringState = walletRestoringState
)
} }
@Composable @Composable
@Suppress("LongParameterList")
private fun WrapSeedRecovery( private fun WrapSeedRecovery(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit, goBack: () -> Unit,
onDone: () -> Unit onDone: () -> Unit,
walletRestoringState: WalletRestoringState,
synchronizer: Synchronizer?,
secretState: SecretState,
) { ) {
val versionInfo = VersionInfo.new(activity.applicationContext) val versionInfo = VersionInfo.new(activity.applicationContext)
val walletViewModel by activity.viewModels<WalletViewModel>()
val persistableWallet = val persistableWallet =
run {
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
if (secretState is SecretState.Ready) { if (secretState is SecretState.Ready) {
secretState.persistableWallet secretState.persistableWallet
} else { } else {
null null
} }
}
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer || null == persistableWallet) { if (null == synchronizer || null == persistableWallet) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -67,6 +82,7 @@ private fun WrapSeedRecovery(
}, },
onDone = onDone, onDone = onDone,
versionInfo = versionInfo, versionInfo = versionInfo,
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -42,6 +42,7 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.SecureScreen import co.electriccoin.zcash.ui.common.compose.SecureScreen
import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BodySmall import co.electriccoin.zcash.ui.design.component.BodySmall
@ -66,6 +67,7 @@ private fun ComposablePreview() {
onDone = {}, onDone = {},
onSeedCopy = {}, onSeedCopy = {},
versionInfo = VersionInfoFixture.new(), versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }
@ -86,6 +88,7 @@ fun SeedRecovery(
onDone: () -> Unit, onDone: () -> Unit,
onSeedCopy: () -> Unit, onSeedCopy: () -> Unit,
versionInfo: VersionInfo, versionInfo: VersionInfo,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@ -93,6 +96,7 @@ fun SeedRecovery(
onBack = onBack, onBack = onBack,
onSeedCopy = onSeedCopy, onSeedCopy = onSeedCopy,
versionInfo = versionInfo, versionInfo = versionInfo,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
) )
} }
) { paddingValues -> ) { paddingValues ->
@ -118,9 +122,16 @@ private fun SeedRecoveryTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
onSeedCopy: () -> Unit, onSeedCopy: () -> Unit,
versionInfo: VersionInfo, versionInfo: VersionInfo,
showRestoring: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
modifier = modifier, modifier = modifier,
backText = stringResource(id = R.string.seed_recovery_back).uppercase(), backText = stringResource(id = R.string.seed_recovery_back).uppercase(),
backContentDescriptionText = stringResource(R.string.seed_recovery_back_content_description), backContentDescriptionText = stringResource(R.string.seed_recovery_back_content_description),
@ -131,7 +142,7 @@ private fun SeedRecoveryTopAppBar(
onCopyToClipboard = onSeedCopy onCopyToClipboard = onSeedCopy
) )
} }
} },
) )
} }

View File

@ -22,6 +22,7 @@ import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.proposeSend import cash.z.ecc.android.sdk.model.proposeSend
import cash.z.ecc.android.sdk.model.toZecString import cash.z.ecc.android.sdk.model.toZecString
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
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
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -48,10 +49,10 @@ internal fun WrapSend(
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
) { ) {
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
@ -71,6 +72,8 @@ internal fun WrapSend(
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171 // TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
val monetarySeparators = MonetarySeparators.current(Locale.US) val monetarySeparators = MonetarySeparators.current(Locale.US)
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSend( WrapSend(
sendArguments, sendArguments,
synchronizer, synchronizer,
@ -83,7 +86,8 @@ internal fun WrapSend(
goSettings, goSettings,
goSendConfirmation, goSendConfirmation,
hasCameraFeature, hasCameraFeature,
monetarySeparators monetarySeparators,
walletRestoringState
) )
} }
@ -102,7 +106,8 @@ internal fun WrapSend(
goSettings: () -> Unit, goSettings: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
hasCameraFeature: Boolean, hasCameraFeature: Boolean,
monetarySeparators: MonetarySeparators monetarySeparators: MonetarySeparators,
walletRestoringState: WalletRestoringState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -213,7 +218,8 @@ internal fun WrapSend(
setAmountState = setAmountState, setAmountState = setAmountState,
onQrScannerOpen = goToQrScanner, onQrScannerOpen = goToQrScanner,
goBalances = goBalances, goBalances = goBalances,
hasCameraFeature = hasCameraFeature hasCameraFeature = hasCameraFeature,
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -52,6 +52,7 @@ import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceWidget import co.electriccoin.zcash.ui.common.compose.BalanceWidget
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.model.canSpend import co.electriccoin.zcash.ui.common.model.canSpend
import co.electriccoin.zcash.ui.common.model.spendableBalance import co.electriccoin.zcash.ui.common.model.spendableBalance
@ -94,7 +95,8 @@ private fun PreviewSendForm() {
setAmountState = {}, setAmountState = {},
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()), amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
setMemoState = {}, setMemoState = {},
memoState = MemoState.new("Test message") memoState = MemoState.new("Test message"),
walletRestoringState = WalletRestoringState.NONE
) )
} }
} }
@ -106,7 +108,6 @@ private fun PreviewSendForm() {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun Send( fun Send(
walletSnapshot: WalletSnapshot,
sendStage: SendStage, sendStage: SendStage,
onCreateZecSend: (ZecSend) -> Unit, onCreateZecSend: (ZecSend) -> Unit,
focusManager: FocusManager, focusManager: FocusManager,
@ -121,9 +122,14 @@ fun Send(
amountState: AmountState, amountState: AmountState,
setMemoState: (MemoState) -> Unit, setMemoState: (MemoState) -> Unit,
memoState: MemoState, memoState: MemoState,
walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot,
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
SendTopAppBar(onSettings = onSettings) SendTopAppBar(
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
onSettings = onSettings
)
}) { paddingValues -> }) { paddingValues ->
SendMainContent( SendMainContent(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
@ -153,8 +159,17 @@ fun Send(
} }
@Composable @Composable
private fun SendTopAppBar(onSettings: () -> Unit) { private fun SendTopAppBar(
onSettings: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.send_stage_send_title), titleText = stringResource(id = R.string.send_stage_send_title),
hamburgerMenuActions = { hamburgerMenuActions = {
IconButton( IconButton(
@ -166,7 +181,7 @@ private fun SendTopAppBar(onSettings: () -> Unit) {
contentDescription = stringResource(id = R.string.settings_menu_content_description) contentDescription = stringResource(id = R.string.settings_menu_content_description)
) )
} }
} },
) )
} }

View File

@ -25,6 +25,7 @@ import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
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.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.send.ext.Saver import co.electriccoin.zcash.ui.screen.send.ext.Saver
@ -52,13 +53,15 @@ internal fun MainActivity.WrapSendConfirmation(
val createTransactionsViewModel by viewModels<CreateTransactionsViewModel>() val createTransactionsViewModel by viewModels<CreateTransactionsViewModel>()
val viewModel by viewModels<SupportViewModel>() val supportViewModel by viewModels<SupportViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
val supportMessage = viewModel.supportInfo.collectAsStateWithLifecycle().value val supportMessage = supportViewModel.supportInfo.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSendConfirmation( WrapSendConfirmation(
activity = this, activity = this,
@ -69,6 +72,7 @@ internal fun MainActivity.WrapSendConfirmation(
spendingKey = spendingKey, spendingKey = spendingKey,
supportMessage = supportMessage, supportMessage = supportMessage,
synchronizer = synchronizer, synchronizer = synchronizer,
walletRestoringState = walletRestoringState,
) )
} }
@ -84,6 +88,7 @@ internal fun WrapSendConfirmation(
spendingKey: UnifiedSpendingKey?, spendingKey: UnifiedSpendingKey?,
supportMessage: SupportInfo?, supportMessage: SupportInfo?,
synchronizer: Synchronizer?, synchronizer: Synchronizer?,
walletRestoringState: WalletRestoringState
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -193,7 +198,8 @@ internal fun WrapSendConfirmation(
} }
} }
} }
} },
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -44,6 +44,7 @@ import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.AppAlertDialog import co.electriccoin.zcash.ui.design.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
@ -124,9 +125,16 @@ fun SendConfirmation(
stage: SendConfirmationStage, stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>, submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend?, zecSend: ZecSend?,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold( Scaffold(
topBar = { SendConfirmationTopAppBar(onBack, stage) }, topBar = {
SendConfirmationTopAppBar(
onBack = onBack,
stage = stage,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues -> ) { paddingValues ->
SendConfirmationMainContent( SendConfirmationMainContent(
@ -152,26 +160,49 @@ fun SendConfirmation(
@Composable @Composable
private fun SendConfirmationTopAppBar( private fun SendConfirmationTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
stage: SendConfirmationStage stage: SendConfirmationStage,
showRestoring: Boolean
) { ) {
when (stage) { when (stage) {
SendConfirmationStage.Confirmation, SendConfirmationStage.Confirmation,
SendConfirmationStage.Sending, SendConfirmationStage.Sending,
is SendConfirmationStage.Failure -> { is SendConfirmationStage.Failure -> {
SmallTopAppBar(titleText = stringResource(id = R.string.send_stage_confirmation_title)) SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.send_stage_confirmation_title),
)
} }
SendConfirmationStage.MultipleTrxFailure -> { SendConfirmationStage.MultipleTrxFailure -> {
SmallTopAppBar(titleText = stringResource(id = R.string.send_confirmation_multiple_error_title)) SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.send_confirmation_multiple_error_title),
)
} }
SendConfirmationStage.MultipleTrxFailureReported -> { SendConfirmationStage.MultipleTrxFailureReported -> {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.send_confirmation_multiple_error_title), titleText = stringResource(id = R.string.send_confirmation_multiple_error_title),
backText = stringResource(id = R.string.send_confirmation_multiple_error_back), backText = stringResource(id = R.string.send_confirmation_multiple_error_back),
backContentDescriptionText = backContentDescriptionText =
stringResource( stringResource(
id = R.string.send_confirmation_multiple_error_back_content_description id = R.string.send_confirmation_multiple_error_back_content_description
), ),
onBack = onBack onBack = onBack,
) )
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.configuration.RemoteConfig
@ -24,40 +25,47 @@ internal fun MainActivity.WrapSettings(
goBack: () -> Unit, goBack: () -> Unit,
goFeedback: () -> Unit, goFeedback: () -> Unit,
) { ) {
val walletViewModel by viewModels<WalletViewModel>()
val settingsViewModel by viewModels<SettingsViewModel>()
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSettings( WrapSettings(
activity = this, activity = this,
goAbout = goAbout, goAbout = goAbout,
goAdvancedSettings = goAdvancedSettings, goAdvancedSettings = goAdvancedSettings,
goBack = goBack, goBack = goBack,
goFeedback = goFeedback, goFeedback = goFeedback,
settingsViewModel = settingsViewModel,
walletViewModel = walletViewModel,
walletRestoringState = walletRestoringState
) )
} }
@Composable @Composable
@Suppress("LongParameterList")
private fun WrapSettings( private fun WrapSettings(
activity: ComponentActivity, activity: ComponentActivity,
goAbout: () -> Unit, goAbout: () -> Unit,
goAdvancedSettings: () -> Unit, goAdvancedSettings: () -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
goFeedback: () -> Unit, goFeedback: () -> Unit,
settingsViewModel: SettingsViewModel,
walletViewModel: WalletViewModel,
walletRestoringState: WalletRestoringState,
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val versionInfo = VersionInfo.new(activity.applicationContext)
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val isBackgroundSyncEnabled = settingsViewModel.isBackgroundSync.collectAsStateWithLifecycle().value val isBackgroundSyncEnabled = settingsViewModel.isBackgroundSync.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value
val versionInfo = VersionInfo.new(activity.applicationContext)
BackHandler { BackHandler {
goBack() goBack()
} }
@Suppress("ComplexCondition") if (null == isAnalyticsEnabled ||
if (null == synchronizer ||
null == isAnalyticsEnabled ||
null == isBackgroundSyncEnabled || null == isBackgroundSyncEnabled ||
null == isKeepScreenOnWhileSyncing null == isKeepScreenOnWhileSyncing
) { ) {
@ -90,7 +98,8 @@ private fun WrapSettings(
}, },
onAnalyticsSettingsChanged = { onAnalyticsSettingsChanged = {
settingsViewModel.setAnalyticsEnabled(it) settingsViewModel.setAnalyticsEnabled(it)
} },
walletRestoringState = walletRestoringState
) )
} }
} }

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
@ -60,6 +61,7 @@ private fun PreviewSettings() {
isAnalyticsEnabled = false, isAnalyticsEnabled = false,
isRescanEnabled = false isRescanEnabled = false
), ),
walletRestoringState = WalletRestoringState.NONE,
) )
} }
} }
@ -77,6 +79,7 @@ fun Settings(
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit, onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit, onAnalyticsSettingsChanged: (Boolean) -> Unit,
troubleshootingParameters: TroubleshootingParameters, troubleshootingParameters: TroubleshootingParameters,
walletRestoringState: WalletRestoringState,
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
SettingsTopAppBar( SettingsTopAppBar(
@ -86,6 +89,7 @@ fun Settings(
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged, onAnalyticsSettingsChanged = onAnalyticsSettingsChanged,
onRescanWallet = onRescanWallet, onRescanWallet = onRescanWallet,
onBack = onBack, onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
) )
}) { paddingValues -> }) { paddingValues ->
SettingsMainContent( SettingsMainContent(
@ -116,12 +120,20 @@ private fun SettingsTopAppBar(
onAnalyticsSettingsChanged: (Boolean) -> Unit, onAnalyticsSettingsChanged: (Boolean) -> Unit,
onRescanWallet: () -> Unit, onRescanWallet: () -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
showRestoring: Boolean
) { ) {
SmallTopAppBar( SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
modifier = Modifier.testTag(SettingsTag.SETTINGS_TOP_APP_BAR),
showTitleLogo = true,
backText = stringResource(id = R.string.settings_back).uppercase(), backText = stringResource(id = R.string.settings_back).uppercase(),
backContentDescriptionText = stringResource(R.string.settings_back_content_description), backContentDescriptionText = stringResource(R.string.settings_back_content_description),
onBack = onBack, onBack = onBack,
showTitleLogo = true,
regularActions = { regularActions = {
if (troubleshootingParameters.isEnabled) { if (troubleshootingParameters.isEnabled) {
TroubleshootingMenu( TroubleshootingMenu(
@ -133,7 +145,6 @@ private fun SettingsTopAppBar(
) )
} }
}, },
modifier = Modifier.testTag(SettingsTag.SETTINGS_TOP_APP_BAR)
) )
} }

View File

@ -0,0 +1,20 @@
package co.electriccoin.zcash.ui.screen.settings.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
import kotlinx.coroutines.flow.MutableStateFlow
class ScreenBrightnessViewModel(application: Application) : AndroidViewModel(application) {
private val screenBrightness: MutableStateFlow<ScreenBrightness> = MutableStateFlow(ScreenBrightness)
val screenBrightnessState = screenBrightness.value.referenceSwitch
fun fullBrightness() {
screenBrightness.value.fullBrightness()
}
fun restoreBrightness() {
screenBrightness.value.restoreBrightness()
}
}

View File

@ -14,6 +14,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
import co.electriccoin.zcash.ui.screen.support.view.Support import co.electriccoin.zcash.ui.screen.support.view.Support
@ -23,17 +25,31 @@ import kotlinx.coroutines.launch
@Composable @Composable
internal fun MainActivity.WrapSupport(goBack: () -> Unit) { internal fun MainActivity.WrapSupport(goBack: () -> Unit) {
WrapSupport(this, goBack) val supportViewModel by viewModels<SupportViewModel>()
val walletViewModel by viewModels<WalletViewModel>()
val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSupport(
activity = this,
goBack = goBack,
supportInfo = supportInfo,
walletRestoringState = walletRestoringState
)
} }
@Composable @Composable
internal fun WrapSupport( internal fun WrapSupport(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit goBack: () -> Unit,
supportInfo: SupportInfo?,
walletRestoringState: WalletRestoringState,
) { ) {
val viewModel by activity.viewModels<SupportViewModel>()
val supportMessage = viewModel.supportInfo.collectAsStateWithLifecycle().value
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val (isShowingDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } val (isShowingDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) }
@ -44,7 +60,7 @@ internal fun WrapSupport(
setShowDialog = setShowDialog, setShowDialog = setShowDialog,
onBack = goBack, onBack = goBack,
onSend = { userMessage -> onSend = { userMessage ->
val fullMessage = formatMessage(userMessage, supportMessage) val fullMessage = formatMessage(userMessage, supportInfo)
val mailIntent = val mailIntent =
EmailUtil.newMailActivityIntent( EmailUtil.newMailActivityIntent(
@ -67,7 +83,8 @@ internal fun WrapSupport(
) )
} }
} }
} },
walletRestoringState = walletRestoringState
) )
} }

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.support.view package co.electriccoin.zcash.ui.screen.support.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -9,30 +10,33 @@ 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.AppAlertDialog import co.electriccoin.zcash.ui.design.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.FormTextField import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview("Support") @Preview("Support")
@ -41,11 +45,12 @@ private fun PreviewSupport() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Support( Support(
snackbarHostState = SnackbarHostState(), isShowingDialog = false,
setShowDialog = {},
onBack = {}, onBack = {},
onSend = {}, onSend = {},
isShowingDialog = false, snackbarHostState = SnackbarHostState(),
setShowDialog = {} walletRestoringState = WalletRestoringState.NONE
) )
} }
} }
@ -65,18 +70,23 @@ private fun PreviewSupportPopup() {
} }
@Composable @Composable
@Suppress("LongParameterList")
fun Support( fun Support(
isShowingDialog: Boolean, isShowingDialog: Boolean,
setShowDialog: (Boolean) -> Unit, setShowDialog: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
onSend: (String) -> Unit, onSend: (String) -> Unit,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
walletRestoringState: WalletRestoringState,
) { ) {
val (message, setMessage) = rememberSaveable { mutableStateOf("") } val (message, setMessage) = rememberSaveable { mutableStateOf("") }
Scaffold( Scaffold(
topBar = { topBar = {
SupportTopAppBar(onBack = onBack) SupportTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
}, },
snackbarHost = { SnackbarHost(snackbarHostState) } snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues -> ) { paddingValues ->
@ -103,20 +113,21 @@ fun Support(
} }
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) private fun SupportTopAppBar(
private fun SupportTopAppBar(onBack: () -> Unit) { onBack: () -> Unit,
TopAppBar( showRestoring: Boolean
title = { Text(text = stringResource(id = R.string.support_header)) },
navigationIcon = {
IconButton(
onClick = onBack
) { ) {
Icon( SmallTopAppBar(
imageVector = Icons.Filled.ArrowBack, restoringLabel =
contentDescription = stringResource(R.string.support_back_content_description) if (showRestoring) {
) stringResource(id = R.string.restoring_wallet_label)
} } else {
} null
},
titleText = stringResource(id = R.string.support_header),
backText = stringResource(id = R.string.support_back).uppercase(),
backContentDescriptionText = stringResource(R.string.support_back_content_description),
onBack = onBack,
) )
} }
@ -128,6 +139,8 @@ private fun SupportMainContent(
setShowDialog: (Boolean) -> Unit, setShowDialog: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val focusRequester = remember { FocusRequester() }
Column( Column(
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
@ -137,7 +150,19 @@ private fun SupportMainContent(
.then(modifier), .then(modifier),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Body(stringResource(id = R.string.support_information)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Image(
imageVector = ImageVector.vectorResource(R.drawable.zashi_logo_sign),
contentDescription = null,
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
Body(
text = stringResource(id = R.string.support_information),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
@ -146,14 +171,13 @@ private fun SupportMainContent(
onValueChange = setMessage, onValueChange = setMessage,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
placeholder = { Text(text = stringResource(id = R.string.support_hint)) }, placeholder = { Text(text = stringResource(id = R.string.support_hint)) },
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Body(stringResource(id = R.string.support_disclaimer))
Spacer( Spacer(
modifier = modifier =
Modifier Modifier
@ -166,11 +190,19 @@ private fun SupportMainContent(
PrimaryButton( PrimaryButton(
onClick = { setShowDialog(true) }, onClick = { setShowDialog(true) },
text = stringResource(id = R.string.support_send), text = stringResource(id = R.string.support_send),
modifier = Modifier.fillMaxWidth() modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular)
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
} }
LaunchedEffect(Unit) {
// Causes the TextFiled to focus on the first screen visit
focusRequester.requestFocus()
}
} }
@Composable @Composable

View File

@ -131,7 +131,7 @@ private fun UpdateTopAppBar(updateInfo: UpdateInfo) {
R.string.update_header R.string.update_header
} }
} }
) ),
) )
} }

View File

@ -7,4 +7,5 @@
<string name="balance_widget_available">Available Balance:</string> <string name="balance_widget_available">Available Balance:</string>
<!-- 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>
</resources> </resources>

View File

@ -0,0 +1,43 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M10,3.585C9.73,3.585 9.51,3.365 9.51,3.095V0.49C9.51,0.219 9.73,0 10,0C10.27,0 10.49,0.219 10.49,0.49V3.095C10.49,3.365 10.27,3.585 10,3.585Z"
android:fillColor="#000000"/>
<path
android:pathData="M10,20C9.73,20 9.51,19.78 9.51,19.51V16.905C9.51,16.634 9.73,16.415 10,16.415C10.27,16.415 10.49,16.634 10.49,16.905V19.51C10.49,19.78 10.27,20 10,20Z"
android:fillColor="#000000"/>
<path
android:pathData="M10,13.811C12.105,13.811 13.811,12.105 13.811,10C13.811,7.895 12.105,6.189 10,6.189C7.895,6.189 6.189,7.895 6.189,10C6.189,12.105 7.895,13.811 10,13.811Z"
android:fillColor="#000000"/>
<path
android:pathData="M10,14.301C7.629,14.301 5.699,12.371 5.699,10C5.699,7.629 7.629,5.699 10,5.699C12.371,5.699 14.301,7.629 14.301,10C14.301,12.371 12.371,14.301 10,14.301ZM10,6.679C8.168,6.679 6.679,8.168 6.679,10C6.679,11.831 8.168,13.321 10,13.321C11.831,13.321 13.321,11.831 13.321,10C13.321,8.168 11.831,6.679 10,6.679Z"
android:fillColor="#000000"/>
<path
android:pathData="M10,13.404C11.88,13.404 13.403,11.88 13.403,10C13.403,8.121 11.88,6.597 10,6.597V13.404Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M3.095,10.49H0.49C0.219,10.49 0,10.27 0,10C0,9.73 0.219,9.51 0.49,9.51H3.095C3.365,9.51 3.585,9.73 3.585,10C3.585,10.27 3.365,10.49 3.095,10.49Z"
android:fillColor="#000000"/>
<path
android:pathData="M19.51,10.49H16.905C16.634,10.49 16.415,10.27 16.415,10C16.415,9.73 16.634,9.51 16.905,9.51H19.51C19.78,9.51 20,9.73 20,10C20,10.27 19.78,10.49 19.51,10.49Z"
android:fillColor="#000000"/>
<path
android:pathData="M5.117,5.607C4.992,5.607 4.867,5.559 4.771,5.464L2.928,3.622C2.737,3.431 2.737,3.12 2.928,2.929C3.119,2.738 3.43,2.738 3.621,2.929L5.463,4.772C5.654,4.963 5.654,5.273 5.463,5.464C5.367,5.56 5.242,5.607 5.116,5.607H5.117Z"
android:fillColor="#000000"/>
<path
android:pathData="M16.725,17.215C16.6,17.215 16.474,17.167 16.378,17.072L14.536,15.229C14.345,15.038 14.345,14.728 14.536,14.537C14.727,14.346 15.037,14.346 15.228,14.537L17.071,16.379C17.262,16.57 17.262,16.881 17.071,17.072C16.975,17.168 16.849,17.215 16.724,17.215H16.725Z"
android:fillColor="#000000"/>
<path
android:pathData="M3.275,17.215C3.15,17.215 3.024,17.167 2.928,17.072C2.737,16.881 2.737,16.57 2.928,16.379L4.771,14.537C4.962,14.346 5.272,14.346 5.463,14.537C5.654,14.728 5.654,15.038 5.463,15.229L3.621,17.072C3.525,17.168 3.4,17.215 3.274,17.215H3.275Z"
android:fillColor="#000000"/>
<path
android:pathData="M14.882,5.607C14.757,5.607 14.632,5.559 14.536,5.464C14.345,5.273 14.345,4.963 14.536,4.772L16.378,2.929C16.569,2.738 16.88,2.738 17.071,2.929C17.262,3.12 17.262,3.431 17.071,3.622L15.228,5.464C15.132,5.56 15.007,5.607 14.882,5.607H14.882Z"
android:fillColor="#000000"/>
</group>
</vector>

View File

@ -1,5 +1,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="support_header">Contact support</string> <string name="support_header">Support</string>
<string name="support_back">Back</string>
<string name="support_back_content_description">Back</string> <string name="support_back_content_description">Back</string>
<string name="support_hint">How can we help?</string> <string name="support_hint">How can we help?</string>
@ -10,6 +11,5 @@
<string name="support_confirmation_explanation"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> is about to <string name="support_confirmation_explanation"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> is about to
open your e-mail app with a pre-filled message.\n\nBe sure to hit send within your e-mail app.</string> open your e-mail app with a pre-filled message.\n\nBe sure to hit send within your e-mail app.</string>
<string name="support_information">Please let us know about any problems you have had, or features you want to see in the future.</string> <string name="support_information">Please let us know about any problems you have had, or features you want to see in the future.</string>
<string name="support_disclaimer">Information provided is handled in accordance with our Privacy Policy.</string>
<string name="support_unable_to_open_email">Unable to launch email app.</string> <string name="support_unable_to_open_email">Unable to launch email app.</string>
</resources> </resources>

View File

@ -507,7 +507,7 @@ private fun supportScreenshots(
tag: String, tag: String,
composeTestRule: ComposeTestRule composeTestRule: ComposeTestRule
) { ) {
composeTestRule.onNode(hasText(resContext.getString(R.string.support_header))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.support_header).uppercase())).also {
it.assertExists() it.assertExists()
} }