[#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
- Proposal API from the Zcash SDK has been integrated together with handling error states for multi-transaction
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
- 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
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.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.res.painterResource
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.unit.Dp
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
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
@Composable
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
@Composable
private fun TopAppBarRegularMenuComposablePreview() {
@ -213,10 +261,11 @@ private fun TopBarOneVisibleActionMenuExample(
}
@Composable
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
fun SmallTopAppBar(
modifier: Modifier = Modifier,
restoringLabel: String? = null,
titleText: String? = null,
showTitleLogo: Boolean = false,
backText: String? = null,
@ -227,17 +276,40 @@ fun SmallTopAppBar(
) {
CenterAlignedTopAppBar(
title = {
if (titleText != null) {
Text(
text = titleText.uppercase(),
style = SecondaryTypography.headlineSmall
)
} else if (showTitleLogo) {
Icon(
painter = painterResource(id = R.drawable.zashi_text_logo),
contentDescription = null,
modifier = Modifier.height(ZcashTheme.dimens.topAppBarZcashLogoHeight)
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
var restoringSpacerHeight: Dp = 0.dp
if (titleText != null) {
Text(
text = titleText.uppercase(),
style = SecondaryTypography.headlineSmall
)
restoringSpacerHeight = ZcashTheme.dimens.spacingTiny
} else if (showTitleLogo) {
Icon(
painter = painterResource(id = R.drawable.zashi_text_logo),
contentDescription = null,
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 = {

View File

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

View File

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

View File

@ -191,6 +191,7 @@ data class ExtendedTypography(
val radioButton: TextStyle,
// Grouping transaction item text styles to a wrapper class
val transactionItemStyles: TransactionItemTextStyles,
val restoringTopAppBarStyle: TextStyle,
)
@Suppress("CompositionLocalAllowlist")
@ -361,5 +362,10 @@ val LocalExtendedTypography =
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.LocalScreenBrightness
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 kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
@ -20,7 +20,6 @@ import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ScreenBrightnessTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@ -31,19 +30,19 @@ class ScreenBrightnessTest : UiTestPrerequisites() {
runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(1, testSetup.getSecureBrightnessCount())
assertEquals(ScreenBrightnessState.FULL, testSetup.getSecureBrightnessCount())
testSetup.mutableScreenBrightnessFlag.update { false }
composeTestRule.awaitIdle()
assertEquals(0, testSetup.getSecureBrightnessCount())
assertEquals(ScreenBrightnessState.NORMAL, testSetup.getSecureBrightnessCount())
}
private class TestSetup(composeTestRule: ComposeContentTestRule) {
val mutableScreenBrightnessFlag = MutableStateFlow(true)
private val screenBrightness = ScreenBrightness()
private val screenBrightness = ScreenBrightness
fun getSecureBrightnessCount() = screenBrightness.referenceCount.value
fun getSecureBrightnessCount() = screenBrightness.referenceSwitch.value
init {
runTest {

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.test.junit4.ComposeContentTestRule
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.screen.support.model.ConfigInfo
import java.util.concurrent.atomic.AtomicInteger
@ -24,10 +25,11 @@ class AboutViewTestSetup(
ZcashTheme {
About(
onBack = { onBackCount.incrementAndGet() },
versionInfo = versionInfo,
configInfo = configInfo,
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.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.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistoryUiStateFixture
@ -58,8 +59,6 @@ class AccountTestSetup(
@Suppress("TestFunctionName")
fun DefaultContent() {
Account(
walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = false,
goSettings = {
onSettingsCount.incrementAndGet()
},
@ -68,6 +67,8 @@ class AccountTestSetup(
onTransactionItemAction = {
onItemClickCount.incrementAndGet()
},
walletSnapshot = walletSnapshot,
walletRestoringState = WalletRestoringState.NONE,
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@ -33,7 +34,7 @@ class ExportPrivateDataViewTestSetup(private val composeTestRule: ComposeContent
@Suppress("TestFunctionName")
fun DefaultContent() {
ExportPrivateData(
SnackbarHostState(),
snackbarHostState = SnackbarHostState(),
onBack = {
onBackCount.incrementAndGet()
},
@ -42,7 +43,8 @@ class ExportPrivateDataViewTestSetup(private val composeTestRule: ComposeContent
},
onConfirm = {
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.model.WalletAddresses
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.fixture.VersionInfoFixture
import kotlinx.coroutines.test.runTest
@ -27,7 +28,7 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
VersionInfoFixture.new(isDebuggable = true)
)
assertEquals(0, testSetup.getScreenBrightnessCount())
assertEquals(ScreenBrightnessState.NORMAL, testSetup.getScreenBrightness())
}
@Test
@ -41,13 +42,11 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
VersionInfoFixture.new(isDebuggable = true)
)
assertEquals(false, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenBrightnessCount())
assertEquals(ScreenBrightnessState.NORMAL, testSetup.getOnAdjustBrightness())
composeTestRule.clickAdjustBrightness()
assertEquals(true, testSetup.getOnAdjustBrightness())
assertEquals(1, testSetup.getScreenBrightnessCount())
assertEquals(ScreenBrightnessState.FULL, testSetup.getOnAdjustBrightness())
}
private fun newTestSetup(

View File

@ -40,12 +40,10 @@ class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
VersionInfoFixture.new(isDebuggable = true)
)
assertEquals(false, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenTimeoutCount())
composeTestRule.clickAdjustBrightness()
assertEquals(true, testSetup.getOnAdjustBrightness())
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.LocalScreenTimeout
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.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.test.getStringResource
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class ReceiveViewTestSetup(
@ -25,17 +26,17 @@ class ReceiveViewTestSetup(
) {
private val onSettingsCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0)
private val screenBrightness = ScreenBrightness()
private val screenBrightness = ScreenBrightness
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 getOnAdjustBrightness(): Boolean {
fun getOnAdjustBrightness(): ScreenBrightnessState {
composeTestRule.waitForIdle()
return onAdjustBrightness.get()
return onAdjustBrightness
}
fun getOnSettingsCount(): Int {
@ -63,11 +64,14 @@ class ReceiveViewTestSetup(
onSettingsCount.getAndIncrement()
},
onAdjustBrightness = {
onAdjustBrightness.getAndSet(it)
onAdjustBrightness = onAdjustBrightness.getChange()
screenTimeout.disableScreenTimeout()
},
onAddrCopyToClipboard = {},
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 cash.z.ecc.sdk.fixture.PersistableWalletFixture
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 java.util.concurrent.atomic.AtomicInteger
@ -39,10 +40,11 @@ class SeedRecoveryTestSetup(
SeedRecovery(
PersistableWalletFixture.new(),
onBack = { onBackCount.incrementAndGet() },
onSeedCopy = { /* Not tested - debug mode feature only */ },
onBirthdayCopy = { onBirthdayCopyCount.incrementAndGet() },
onDone = { onCompleteCallbackCount.incrementAndGet() },
onSeedCopy = { /* Not tested - debug mode feature only */ },
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.ui.common.compose.LocalScreenSecurity
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.fixture.VersionInfoFixture
import kotlinx.coroutines.test.runTest
@ -45,10 +46,11 @@ class SeedRecoveryViewsSecuredScreenTest : UiTestPrerequisites() {
SeedRecovery(
PersistableWalletFixture.new(),
onBack = {},
onSeedCopy = {},
onBirthdayCopy = {},
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.ZecSend
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.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.send.ext.Saver
@ -137,6 +138,7 @@ class SendViewTestSetup(
setAmountState = {},
memoState = MemoState.new(""),
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.ZcashNetwork
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.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.send.WrapSend
@ -78,7 +79,8 @@ class SendViewIntegrationTest {
hasCameraFeature = true,
goSettings = {},
monetarySeparators = monetarySeparators,
goSendConfirmation = {}
goSendConfirmation = {},
walletRestoringState = WalletRestoringState.NONE,
)
}

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.settings
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.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.view.Settings
@ -87,7 +88,8 @@ class SettingsViewTestSetup(
},
onAnalyticsSettingsChanged = {
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.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
@ -36,7 +37,8 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
@Suppress("TestFunctionName")
fun DefaultContent() {
Support(
snackbarHostState = SnackbarHostState(),
isShowingDialog = false,
setShowDialog = {},
onBack = {
onBackCount.incrementAndGet()
},
@ -44,8 +46,8 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
onSendCount.incrementAndGet()
onSendMessage.set(it)
},
isShowingDialog = false,
setShowDialog = {}
snackbarHostState = SnackbarHostState(),
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.ui.common.compose.BindCompLocalProvider
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.SecretState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -53,7 +54,7 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
val homeViewModel by viewModels<HomeViewModel>()
private val homeViewModel by viewModels<HomeViewModel>()
val walletViewModel by viewModels<WalletViewModel>()
@ -173,6 +174,7 @@ class MainActivity : ComponentActivity() {
)
} else {
walletViewModel.persistNewWallet()
walletViewModel.persistWalletRestoringState(WalletRestoringState.INITIATING)
}
}
)

View File

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

View File

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

View File

@ -6,28 +6,32 @@ import androidx.compose.runtime.compositionLocalOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
class ScreenBrightness {
private val mutableReferenceCount: MutableStateFlow<Int> = MutableStateFlow(0)
val referenceCount = mutableReferenceCount.asStateFlow()
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")
sealed class ScreenBrightnessState {
fun getChange(): ScreenBrightnessState {
return when (this) {
NORMAL -> FULL
FULL -> NORMAL
}
}
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")
val LocalScreenBrightness = compositionLocalOf { ScreenBrightness() }
val LocalScreenBrightness = compositionLocalOf { ScreenBrightness }
@Composable
fun BrightenScreen() {
@ -37,3 +41,9 @@ fun BrightenScreen() {
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.extension.throttle
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.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
@ -98,6 +99,23 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
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.
*/
@ -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.
*/
fun rescanBlockchain() {
viewModelScope.launch {
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.PreferenceKey
import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
object StandardPreferenceKeys {
/**
@ -15,6 +16,16 @@ object StandardPreferenceKeys {
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
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 androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
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.view.About
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@ -21,13 +25,22 @@ import kotlinx.coroutines.launch
@Composable
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
internal fun WrapAbout(
activity: ComponentActivity,
goBack: () -> Unit
goBack: () -> Unit,
walletRestoringState: WalletRestoringState,
) {
val configInfo = ConfigInfo.new(AndroidConfigurationFactory.getInstance(activity.applicationContext))
val versionInfo = VersionInfo.new(activity.applicationContext)
@ -38,6 +51,7 @@ internal fun WrapAbout(
}
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
About(
@ -52,6 +66,7 @@ internal fun WrapAbout(
)
},
snackbarHostState = snackbarHostState,
walletRestoringState = walletRestoringState,
)
}

View File

@ -41,6 +41,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R
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.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -55,29 +56,33 @@ private fun AboutPreview() {
GradientSurface {
About(
onBack = {},
versionInfo = VersionInfoFixture.new(),
configInfo = ConfigInfoFixture.new(),
snackbarHostState = SnackbarHostState(),
onPrivacyPolicy = {},
snackbarHostState = SnackbarHostState(),
versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE,
)
}
}
}
@Composable
@Suppress("LongParameterList")
fun About(
onBack: () -> Unit,
configInfo: ConfigInfo,
onPrivacyPolicy: () -> Unit,
snackbarHostState: SnackbarHostState,
versionInfo: VersionInfo,
walletRestoringState: WalletRestoringState,
) {
Scaffold(
topBar = {
AboutTopAppBar(
onBack = onBack,
versionInfo = versionInfo,
configInfo = configInfo
configInfo = configInfo,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
@ -105,9 +110,16 @@ fun About(
private fun AboutTopAppBar(
onBack: () -> Unit,
versionInfo: VersionInfo,
configInfo: ConfigInfo
configInfo: ConfigInfo,
showRestoring: Boolean
) {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.about_title).uppercase(),
backText = stringResource(id = R.string.about_back).uppercase(),
backContentDescriptionText = stringResource(R.string.about_back_content_description),
@ -116,7 +128,7 @@ private fun AboutTopAppBar(
if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) {
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 co.electriccoin.zcash.spackle.ClipboardManagerUtil
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.viewmodel.WalletViewModel
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.TrxItemAction
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.launch
import org.jetbrains.annotations.VisibleForTesting
@ -36,12 +36,8 @@ internal fun WrapAccount(
val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val transactionsUiState = transactionHistoryViewModel.transactionUiState.collectAsStateWithLifecycle().value
@ -50,16 +46,18 @@ internal fun WrapAccount(
transactionHistoryViewModel.processTransactionState(value)
}
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapAccount(
context = activity.applicationContext,
goBalances = goBalances,
goSettings = goSettings,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
scope = scope,
synchronizer = synchronizer,
transactionHistoryViewModel = transactionHistoryViewModel,
transactionsUiState = transactionsUiState,
walletSnapshot = walletSnapshot,
walletRestoringState = walletRestoringState
)
// For benchmarking purposes
@ -78,7 +76,7 @@ internal fun WrapAccount(
synchronizer: Synchronizer?,
transactionHistoryViewModel: TransactionHistoryViewModel,
walletSnapshot: WalletSnapshot?,
isKeepScreenOnWhileSyncing: Boolean?,
walletRestoringState: WalletRestoringState,
) {
if (null == synchronizer || null == walletSnapshot) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -88,7 +86,6 @@ internal fun WrapAccount(
} else {
Account(
walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
transactionsUiState = transactionsUiState,
onTransactionItemAction = { action ->
when (action) {
@ -130,6 +127,7 @@ internal fun WrapAccount(
},
goBalances = goBalances,
goSettings = goSettings,
walletRestoringState = walletRestoringState
)
}
}

View File

@ -5,7 +5,13 @@ import kotlinx.collections.immutable.ImmutableList
sealed interface 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 {
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(override val transactions: ImmutableList<TransactionOverviewExt>) : Prepared(transactions)
data class Done(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState
}

View File

@ -15,10 +15,9 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.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.test.CommonTag
import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -36,11 +35,11 @@ private fun HistoryLoadingComposablePreview() {
GradientSurface {
Account(
walletSnapshot = WalletSnapshotFixture.new(),
isKeepScreenOnWhileSyncing = false,
goBalances = {},
goSettings = {},
transactionsUiState = TransactionUiState.Loading,
onTransactionItemAction = {}
onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.SYNCING
)
}
}
@ -54,11 +53,11 @@ private fun HistoryListComposablePreview() {
@Suppress("MagicNumber")
Account(
walletSnapshot = WalletSnapshotFixture.new(),
isKeepScreenOnWhileSyncing = false,
goBalances = {},
goSettings = {},
transactionsUiState = TransactionUiState.Prepared(transactions = TransactionsFixture.new()),
transactionsUiState = TransactionUiState.Done(transactions = TransactionsFixture.new()),
onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.NONE
)
}
}
@ -69,19 +68,22 @@ private fun HistoryListComposablePreview() {
internal fun Account(
goBalances: () -> Unit,
goSettings: () -> Unit,
isKeepScreenOnWhileSyncing: Boolean?,
onTransactionItemAction: (TrxItemAction) -> Unit,
transactionsUiState: TransactionUiState,
walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot,
) {
Scaffold(topBar = {
AccountTopAppBar(onSettings = goSettings)
AccountTopAppBar(
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
onSettings = goSettings
)
}) { paddingValues ->
AccountMainContent(
walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
goBalances = goBalances,
transactionState = transactionsUiState,
walletRestoringState = walletRestoringState,
onTransactionItemAction = onTransactionItemAction,
modifier =
Modifier.padding(
@ -94,8 +96,17 @@ internal fun Account(
}
@Composable
private fun AccountTopAppBar(onSettings: () -> Unit) {
private fun AccountTopAppBar(
onSettings: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
showTitleLogo = true,
hamburgerMenuActions = {
IconButton(
@ -115,11 +126,11 @@ private fun AccountTopAppBar(onSettings: () -> Unit) {
@Suppress("LongParameterList")
private fun AccountMainContent(
walletSnapshot: WalletSnapshot,
isKeepScreenOnWhileSyncing: Boolean?,
goBalances: () -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit,
transactionState: TransactionUiState,
modifier: Modifier = Modifier
walletRestoringState: WalletRestoringState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
@ -139,12 +150,9 @@ private fun AccountMainContent(
HistoryContainer(
transactionState = transactionState,
walletRestoringState = walletRestoringState,
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.toZecString
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.GradientSurface
import co.electriccoin.zcash.ui.design.component.StyledBalance
@ -70,7 +71,8 @@ private fun ComposablePreview() {
GradientSurface {
HistoryContainer(
transactionState = TransactionUiState.Loading,
onTransactionItemAction = {}
onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.SYNCING
)
}
}
@ -82,8 +84,9 @@ private fun ComposableHistoryListPreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
HistoryContainer(
transactionState = TransactionUiState.Prepared(transactions = TransactionsFixture.new()),
onTransactionItemAction = {}
transactionState = TransactionUiState.Done(transactions = TransactionsFixture.new()),
onTransactionItemAction = {},
walletRestoringState = WalletRestoringState.NONE
)
}
}
@ -102,7 +105,8 @@ private val dateFormat: DateFormat by lazy {
internal fun HistoryContainer(
transactionState: TransactionUiState,
onTransactionItemAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier
walletRestoringState: WalletRestoringState,
modifier: Modifier = Modifier,
) {
Box(
modifier =
@ -114,42 +118,61 @@ internal fun HistoryContainer(
)
) {
when (transactionState) {
TransactionUiState.Loading, TransactionUiState.Syncing -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
CircularMidProgressIndicator(
modifier = Modifier.testTag(HistoryTag.PROGRESS),
)
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 -> {
if (transactionState.transactions.isEmpty()) {
Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.account_history_empty),
style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
color = ZcashTheme.colors.textCommon,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
} else {
HistoryList(
transactions = transactionState.transactions,
onAction = onTransactionItemAction,
)
}
HistoryList(
transactions = transactionState.transactions,
onAction = onTransactionItemAction,
)
}
is TransactionUiState.DoneEmpty -> {
EmptyTransactionHistory()
}
}
}
}
@Composable
private fun LoadingTransactionHistory() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
CircularMidProgressIndicator(
modifier = Modifier.testTag(HistoryTag.PROGRESS),
)
}
}
@Composable
private fun EmptyTransactionHistory() {
Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.account_history_empty),
style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
color = ZcashTheme.colors.textCommon,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Composable
private fun HistoryList(
transactions: ImmutableList<TransactionUi>,

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.TransactionOverview
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.TransactionUiState
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.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toList
class TransactionHistoryViewModel(application: Application) : AndroidViewModel(application) {
private val state: MutableStateFlow<State> = MutableStateFlow(State.LOADING)
private val transactions: MutableStateFlow<ImmutableList<TransactionUi>> = MutableStateFlow(persistentListOf())
val transactionUiState: StateFlow<TransactionUiState> =
transactions.map {
if (it.isEmpty()) {
TransactionUiState.Syncing
} else {
TransactionUiState.Prepared(it)
state.combine(transactions) { state: State, transactions: ImmutableList<TransactionUi> ->
when (state) {
State.LOADING -> TransactionUiState.Loading
State.SYNCING -> TransactionUiState.Syncing(transactions)
State.SYNCING_EMPTY -> TransactionUiState.SyncingEmpty
State.DONE -> TransactionUiState.Done(transactions)
State.DONE_EMPTY -> TransactionUiState.DoneEmpty
}
}.stateIn(
viewModelScope,
@ -40,26 +45,49 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
)
fun processTransactionState(dataState: TransactionHistorySyncState) {
transactions.value =
when (dataState) {
TransactionHistorySyncState.Loading -> persistentListOf()
is TransactionHistorySyncState.Prepared -> {
dataState.transactions.map { data ->
val existingTransaction =
transactions.value.find {
data.overview.rawId == it.overview.rawId
}
TransactionUi.new(
data = data,
expandableState = existingTransaction?.expandableState ?: TrxItemState.COLLAPSED,
messages = existingTransaction?.messages,
)
}.toPersistentList()
when (dataState) {
TransactionHistorySyncState.Loading -> {
state.value = State.LOADING
transactions.value = persistentListOf()
}
is TransactionHistorySyncState.Syncing -> {
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()
}
}
}
}
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.map { item ->
if (item.overview.rawId == newTransaction.overview.rawId) {
@ -87,7 +115,7 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
val messages = loadMessageForTransaction(synchronizer, updated.overview)
updatedWithMessages = updated.copy(messages = messages)
}
updateTransaction(updatedWithMessages)
updateTransactionInList(updatedWithMessages)
} else {
Twig.warn { "Transaction not found" }
}
@ -101,3 +129,11 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
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
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
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
@Composable
internal fun WrapAdvancedSettings(
internal fun MainActivity.WrapAdvancedSettings(
goBack: () -> Unit,
goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit,
goChooseServer: () -> Unit,
) {
WrapSettings(
val walletViewModel by viewModels<WalletViewModel>()
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapAdvancedSettings(
goBack = goBack,
goExportPrivateData = goExportPrivateData,
goChooseServer = goChooseServer,
goSeedRecovery = goSeedRecovery,
walletRestoringState = walletRestoringState
)
}
@Composable
@Suppress("LongParameterList")
private fun WrapSettings(
private fun WrapAdvancedSettings(
goBack: () -> Unit,
goExportPrivateData: () -> Unit,
goChooseServer: () -> Unit,
goSeedRecovery: () -> Unit,
walletRestoringState: WalletRestoringState,
) {
BackHandler {
goBack()
@ -37,6 +47,7 @@ private fun WrapSettings(
onBack = goBack,
onSeedRecovery = goSeedRecovery,
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.tooling.preview.Preview
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.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
@ -34,8 +35,9 @@ private fun PreviewAdvancedSettings() {
AdvancedSettings(
onBack = {},
onExportPrivateData = {},
onSeedRecovery = {},
onChooseServer = {},
onSeedRecovery = {},
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -47,10 +49,12 @@ fun AdvancedSettings(
onExportPrivateData: () -> Unit,
onChooseServer: () -> Unit,
onSeedRecovery: () -> Unit,
walletRestoringState: WalletRestoringState,
) {
Scaffold(topBar = {
AdvancedSettingsTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
}) { paddingValues ->
AdvancedSettingsMainContent(
@ -73,13 +77,22 @@ fun AdvancedSettings(
}
@Composable
private fun AdvancedSettingsTopAppBar(onBack: () -> Unit) {
private fun AdvancedSettingsTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
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(),
backContentDescriptionText = stringResource(R.string.advanced_settings_back_content_description),
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.Zatoshi
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.viewmodel.CheckUpdateViewModel
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.sendconfirmation.model.SubmitResult
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.model.UpdateState
import kotlinx.coroutines.delay
@ -47,6 +47,8 @@ internal fun WrapBalances(
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application,
@ -54,17 +56,15 @@ internal fun WrapBalances(
)
}
val settingsViewModel by activity.viewModels<SettingsViewModel>()
WrapBalances(
checkUpdateViewModel = checkUpdateViewModel,
createTransactionsViewModel = createTransactionsViewModel,
goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
spendingKey = spendingKey,
settingsViewModel = settingsViewModel,
synchronizer = synchronizer,
walletSnapshot = walletSnapshot
walletSnapshot = walletSnapshot,
walletRestoringState = walletRestoringState,
)
}
@ -78,10 +78,10 @@ internal fun WrapBalances(
createTransactionsViewModel: CreateTransactionsViewModel,
goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit,
settingsViewModel: SettingsViewModel,
spendingKey: UnifiedSpendingKey?,
synchronizer: Synchronizer?,
walletSnapshot: WalletSnapshot?,
walletRestoringState: WalletRestoringState,
) {
val scope = rememberCoroutineScope()
@ -91,8 +91,6 @@ internal fun WrapBalances(
it?.appUpdateInfo != null && it.state == UpdateState.Prepared
}
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current)
val (shieldState, setShieldState) =
@ -128,7 +126,6 @@ internal fun WrapBalances(
Balances(
onSettings = goSettings,
isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
isShowingErrorDialog = isShowingErrorDialog,
setShowErrorDialog = setShowErrorDialog,
@ -187,6 +184,7 @@ internal fun WrapBalances(
},
shieldState = shieldState,
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.tooling.preview.Preview
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.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.extension.toPercentageWithDecimal
import co.electriccoin.zcash.ui.R
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.changePendingBalance
import co.electriccoin.zcash.ui.common.model.spendableBalance
@ -86,13 +85,13 @@ private fun ComposableBalancesPreview() {
Balances(
onSettings = {},
isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
isShowingErrorDialog = false,
setShowErrorDialog = {},
onShielding = {},
shieldState = ShieldState.Available,
walletSnapshot = WalletSnapshotFixture.new(),
isShowingErrorDialog = false,
setShowErrorDialog = {},
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -106,13 +105,13 @@ private fun ComposableBalancesShieldFailurePreview() {
Balances(
onSettings = {},
isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
isShowingErrorDialog = true,
setShowErrorDialog = {},
onShielding = {},
shieldState = ShieldState.Available,
walletSnapshot = WalletSnapshotFixture.new(),
isShowingErrorDialog = true,
setShowErrorDialog = {},
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -123,23 +122,25 @@ private fun ComposableBalancesShieldFailurePreview() {
fun Balances(
onSettings: () -> Unit,
isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean,
isShowingErrorDialog: Boolean,
setShowErrorDialog: (Boolean) -> Unit,
onShielding: () -> Unit,
shieldState: ShieldState,
walletSnapshot: WalletSnapshot?,
walletRestoringState: WalletRestoringState,
) {
Scaffold(topBar = {
BalancesTopAppBar(onSettings = onSettings)
BalancesTopAppBar(
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
onSettings = onSettings
)
}) { paddingValues ->
if (null == walletSnapshot) {
CircularScreenProgressIndicator()
} else {
BalancesMainContent(
isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
onShielding = onShielding,
walletSnapshot = walletSnapshot,
@ -197,10 +198,19 @@ fun ShieldingErrorDialog(
}
@Composable
private fun BalancesTopAppBar(onSettings: () -> Unit) {
private fun BalancesTopAppBar(
onSettings: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar(
showTitleLogo = false,
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.balances_title),
showTitleLogo = false,
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
@ -211,7 +221,7 @@ private fun BalancesTopAppBar(onSettings: () -> Unit) {
contentDescription = stringResource(id = R.string.settings_menu_content_description)
)
}
}
},
)
}
@ -219,7 +229,6 @@ private fun BalancesTopAppBar(onSettings: () -> Unit) {
@Composable
private fun BalancesMainContent(
isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean,
onShielding: () -> Unit,
walletSnapshot: WalletSnapshot,
@ -272,10 +281,6 @@ private fun BalancesMainContent(
walletSnapshot = walletSnapshot,
isUpdateAvailable = isUpdateAvailable,
)
if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout()
}
}
}

View File

@ -3,6 +3,7 @@
package co.electriccoin.zcash.ui.screen.chooseserver
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.sdk.type.fromResources
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.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.chooseserver.view.ChooseServer
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapChooseServer(goBack: () -> Unit) {
val walletViewModel by viewModels<WalletViewModel>()
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapChooseServer(
activity = this,
goBack = goBack,
@ -39,7 +46,8 @@ internal fun MainActivity.WrapChooseServer(goBack: () -> Unit) {
},
onWalletPersist = {
walletViewModel.persistExistingWallet(it)
}
},
walletRestoringState = walletRestoringState
)
}
@ -52,6 +60,7 @@ private fun WrapChooseServer(
onWalletPersist: (PersistableWallet) -> Unit,
secretState: SecretState,
synchronizer: Synchronizer?,
walletRestoringState: WalletRestoringState,
) {
if (synchronizer == null || secretState !is SecretState.Ready) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -118,7 +127,8 @@ private fun WrapChooseServer(
isShowingErrorDialog = isShowingErrorDialog,
setShowErrorDialog = setShowErrorDialog,
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 co.electriccoin.lightwallet.client.model.LightWalletEndpoint
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.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -62,6 +63,7 @@ private fun PreviewChooseServer() {
setShowErrorDialog = {},
isShowingSuccessDialog = false,
setShowSuccessDialog = {},
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -79,10 +81,14 @@ fun ChooseServer(
setShowErrorDialog: (Boolean) -> Unit,
isShowingSuccessDialog: Boolean,
setShowSuccessDialog: (Boolean) -> Unit,
walletRestoringState: WalletRestoringState,
) {
Scaffold(
topBar = {
ChooseServerTopAppBar(onBack = onBack)
ChooseServerTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
}
) { paddingValues ->
ChooseServerMainContent(
@ -120,13 +126,22 @@ fun ChooseServer(
}
@Composable
private fun ChooseServerTopAppBar(onBack: () -> Unit) {
private fun ChooseServerTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
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(),
backContentDescriptionText = stringResource(R.string.choose_server_back_content_description),
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.R
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.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData
@ -29,10 +30,18 @@ internal fun MainActivity.WrapExportPrivateData(
goBack: () -> Unit,
onConfirm: () -> Unit
) {
val walletViewModel by viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapExportPrivateData(
this,
onBack = goBack,
onShare = onConfirm
onShare = onConfirm,
synchronizer = synchronizer,
walletRestoringState = walletRestoringState,
)
}
@ -40,11 +49,10 @@ internal fun MainActivity.WrapExportPrivateData(
internal fun WrapExportPrivateData(
activity: ComponentActivity,
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) {
// 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
@ -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.unit.sp
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.component.Body
import co.electriccoin.zcash.ui.design.component.CheckBox
@ -42,6 +43,7 @@ private fun ExportPrivateDataPreview() {
onBack = {},
onAgree = {},
onConfirm = {},
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -56,9 +58,15 @@ fun ExportPrivateData(
onBack: () -> Unit,
onAgree: (Boolean) -> Unit,
onConfirm: () -> Unit,
walletRestoringState: WalletRestoringState,
) {
Scaffold(
topBar = { ExportPrivateDataTopAppBar(onBack = onBack) },
topBar = {
ExportPrivateDataTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
ExportPrivateDataContent(
@ -79,8 +87,17 @@ fun ExportPrivateData(
}
@Composable
private fun ExportPrivateDataTopAppBar(onBack: () -> Unit) {
private fun ExportPrivateDataTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
backText = stringResource(R.string.export_data_back).uppercase(),
backContentDescriptionText = stringResource(R.string.export_data_back_content_description),
onBack = onBack,

View File

@ -8,10 +8,16 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.ui.MainActivity
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.WalletViewModel
import co.electriccoin.zcash.ui.common.viewmodel.isSynced
import co.electriccoin.zcash.ui.screen.account.WrapAccount
import co.electriccoin.zcash.ui.screen.balances.WrapBalances
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.send.WrapSend
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.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -26,7 +33,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
@Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapHome(
onPageChange: (HomeScreenIndex) -> Unit,
goBack: () -> Unit,
goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit,
@ -34,15 +40,39 @@ internal fun MainActivity.WrapHome(
goSendConfirmation: (ZecSend) -> Unit,
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(
this,
onPageChange = onPageChange,
goBack = goBack,
goScan = goScan,
goSendConfirmation = goSendConfirmation,
goSettings = goSettings,
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,
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
homeScreenIndex: HomeScreenIndex,
isKeepScreenOnWhileSyncing: Boolean?,
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
val forceHomePageIndexFlow: MutableSharedFlow<ForcePage?> =
MutableSharedFlow(
@ -70,7 +101,7 @@ internal fun WrapHome(
val forceIndex = forceHomePageIndexFlow.collectAsState(initial = null).value
val homeGoBack: () -> Unit = {
when (homeViewModel.screenIndex.value) {
when (homeScreenIndex) {
HomeScreenIndex.ACCOUNT -> goBack()
HomeScreenIndex.SEND,
HomeScreenIndex.RECEIVE,
@ -82,6 +113,11 @@ internal fun WrapHome(
homeGoBack()
}
// Reset the screen brightness for all pages except Receive which maintain the screen brightness by itself
if (homeScreenIndex != HomeScreenIndex.RECEIVE) {
RestoreScreenBrightness()
}
val tabs =
persistentListOf(
TabItem(
@ -92,7 +128,7 @@ internal fun WrapHome(
WrapAccount(
activity = activity,
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
goSettings = goSettings,
goSettings = goSettings
)
}
),
@ -119,7 +155,7 @@ internal fun WrapHome(
screenContent = {
WrapReceive(
activity = activity,
onSettings = goSettings,
onSettings = goSettings
)
}
),
@ -140,7 +176,9 @@ internal fun WrapHome(
Home(
subScreens = tabs,
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.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import cash.z.ecc.android.sdk.Synchronizer
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.NavigationTabText
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.HomeScreenIndex
import co.electriccoin.zcash.ui.screen.home.model.TabItem
@ -42,9 +46,11 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Home(
subScreens = persistentListOf(),
isKeepScreenOnWhileSyncing = false,
forcePage = null,
onPageChange = {}
onPageChange = {},
subScreens = persistentListOf(),
walletSnapshot = WalletSnapshotFixture.new(),
)
}
}
@ -54,9 +60,11 @@ private fun ComposablePreview() {
@Suppress("LongMethod")
@Composable
fun Home(
subScreens: ImmutableList<TabItem>,
isKeepScreenOnWhileSyncing: Boolean?,
forcePage: ForcePage?,
onPageChange: (HomeScreenIndex) -> Unit,
subScreens: ImmutableList<TabItem>,
walletSnapshot: WalletSnapshot?,
) {
val pagerState =
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) {
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.common.model.OnboardingState
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.onboarding.view.ShortOnboarding
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
@ -117,4 +118,5 @@ internal fun persistExistingWalletWithSeedPhrase(
walletInitMode = WalletInitMode.RestoreWallet
)
walletViewModel.persistExistingWallet(restoredWallet)
walletViewModel.persistWalletRestoringState(WalletRestoringState.RESTORING)
}

View File

@ -164,7 +164,7 @@ private fun OnboardingMainContent(
if (isDebugMenuEnabled) {
DebugMenu(onFixtureWallet)
}
}
},
)
Column(
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.getInternalCacheDirSuspend
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.viewmodel.WalletViewModel
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
@ -33,8 +35,15 @@ internal fun WrapReceive(
activity: ComponentActivity,
onSettings: () -> Unit,
) {
val viewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value
val walletViewModel by activity.viewModels<WalletViewModel>()
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 scope = rememberCoroutineScope()
@ -42,9 +51,13 @@ internal fun WrapReceive(
val versionInfo = VersionInfo.new(activity.applicationContext)
Receive(
walletAddress = walletAddresses,
snackbarHostState = snackbarHostState,
onAdjustBrightness = { /* Just for testing purposes */ },
screenBrightnessState = screenBrightnessState,
onAdjustBrightness = {
when (it) {
ScreenBrightnessState.NORMAL -> brightnessViewModel.restoreBrightness()
ScreenBrightnessState.FULL -> brightnessViewModel.fullBrightness()
}
},
onAddrCopyToClipboard = { address ->
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
@ -72,7 +85,10 @@ internal fun WrapReceive(
}
},
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.rememberScrollState
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.IconButton
import androidx.compose.material3.Scaffold
@ -23,9 +20,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.common.compose.BrightenScreen
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.WalletRestoringState
import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -64,13 +61,15 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Receive(
screenBrightnessState = ScreenBrightnessState.NORMAL,
walletAddress = runBlocking { WalletAddressesFixture.new() },
snackbarHostState = SnackbarHostState(),
onSettings = {},
onAdjustBrightness = {},
onAddrCopyToClipboard = {},
onQrImageShare = {},
versionInfo = VersionInfoFixture.new()
versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE
)
}
}
@ -79,25 +78,24 @@ private fun ComposablePreview() {
@Suppress("LongParameterList")
@Composable
fun Receive(
screenBrightnessState: ScreenBrightnessState,
walletAddress: WalletAddresses?,
snackbarHostState: SnackbarHostState,
onSettings: () -> Unit,
onAdjustBrightness: (Boolean) -> Unit,
onAdjustBrightness: (ScreenBrightnessState) -> Unit,
onAddrCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
versionInfo: VersionInfo,
walletRestoringState: WalletRestoringState,
) {
val (brightness, setBrightness) = rememberSaveable { mutableStateOf(false) }
Scaffold(
topBar = {
ReceiveTopAppBar(
adjustBrightness = brightness,
onSettings = onSettings,
onBrightness = {
onAdjustBrightness(!brightness)
setBrightness(!brightness)
onAdjustBrightness(screenBrightnessState.getChange())
},
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
versionInfo = versionInfo,
)
},
@ -110,7 +108,7 @@ fun Receive(
walletAddress = walletAddress,
onAddressCopyToClipboard = onAddrCopyToClipboard,
onQrImageShare = onQrImageShare,
adjustBrightness = brightness,
screenBrightnessState = screenBrightnessState,
versionInfo = versionInfo,
modifier =
Modifier.padding(
@ -126,30 +124,19 @@ fun Receive(
@Composable
private fun ReceiveTopAppBar(
adjustBrightness: Boolean,
onSettings: () -> Unit,
onBrightness: () -> Unit,
versionInfo: VersionInfo
versionInfo: VersionInfo,
showRestoring: Boolean
) {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.receive_title),
regularActions = {
if (versionInfo.isDebuggable) {
IconButton(
onClick = onBrightness
) {
Icon(
imageVector =
if (adjustBrightness) {
Icons.Default.BrightnessLow
} else {
Icons.Default.BrightnessHigh
},
contentDescription = stringResource(R.string.receive_brightness_content_description)
)
}
}
},
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
@ -160,7 +147,19 @@ private fun ReceiveTopAppBar(
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,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
adjustBrightness: Boolean,
screenBrightnessState: ScreenBrightnessState,
versionInfo: VersionInfo,
modifier: Modifier = Modifier,
) {
@ -182,7 +181,7 @@ private fun ReceiveContents(
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (adjustBrightness) {
if (screenBrightnessState == ScreenBrightnessState.FULL) {
BrightenScreen()
DisableScreenTimeout()
}

View File

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

View File

@ -4,10 +4,12 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
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.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
@ -16,31 +18,44 @@ import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery
@Composable
internal fun MainActivity.WrapSeedRecovery(
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
@Suppress("LongParameterList")
private fun WrapSeedRecovery(
activity: ComponentActivity,
goBack: () -> Unit,
onDone: () -> Unit
onDone: () -> Unit,
walletRestoringState: WalletRestoringState,
synchronizer: Synchronizer?,
secretState: SecretState,
) {
val versionInfo = VersionInfo.new(activity.applicationContext)
val walletViewModel by activity.viewModels<WalletViewModel>()
val persistableWallet =
run {
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
if (secretState is SecretState.Ready) {
secretState.persistableWallet
} else {
null
}
if (secretState is SecretState.Ready) {
secretState.persistableWallet
} else {
null
}
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer || null == persistableWallet) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -67,6 +82,7 @@ private fun WrapSeedRecovery(
},
onDone = onDone,
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.shouldSecureScreen
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.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BodySmall
@ -66,6 +67,7 @@ private fun ComposablePreview() {
onDone = {},
onSeedCopy = {},
versionInfo = VersionInfoFixture.new(),
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -86,6 +88,7 @@ fun SeedRecovery(
onDone: () -> Unit,
onSeedCopy: () -> Unit,
versionInfo: VersionInfo,
walletRestoringState: WalletRestoringState,
) {
Scaffold(
topBar = {
@ -93,6 +96,7 @@ fun SeedRecovery(
onBack = onBack,
onSeedCopy = onSeedCopy,
versionInfo = versionInfo,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
}
) { paddingValues ->
@ -118,9 +122,16 @@ private fun SeedRecoveryTopAppBar(
onBack: () -> Unit,
onSeedCopy: () -> Unit,
versionInfo: VersionInfo,
showRestoring: Boolean,
modifier: Modifier = Modifier,
) {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
modifier = modifier,
backText = stringResource(id = R.string.seed_recovery_back).uppercase(),
backContentDescriptionText = stringResource(R.string.seed_recovery_back_content_description),
@ -131,7 +142,7 @@ private fun SeedRecoveryTopAppBar(
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.toZecString
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.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -48,10 +49,10 @@ internal fun WrapSend(
goSendConfirmation: (ZecSend) -> Unit,
goSettings: () -> Unit,
) {
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
val walletViewModel by activity.viewModels<WalletViewModel>()
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
val synchronizer = walletViewModel.synchronizer.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
val monetarySeparators = MonetarySeparators.current(Locale.US)
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSend(
sendArguments,
synchronizer,
@ -83,7 +86,8 @@ internal fun WrapSend(
goSettings,
goSendConfirmation,
hasCameraFeature,
monetarySeparators
monetarySeparators,
walletRestoringState
)
}
@ -102,7 +106,8 @@ internal fun WrapSend(
goSettings: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
hasCameraFeature: Boolean,
monetarySeparators: MonetarySeparators
monetarySeparators: MonetarySeparators,
walletRestoringState: WalletRestoringState,
) {
val scope = rememberCoroutineScope()
@ -213,7 +218,8 @@ internal fun WrapSend(
setAmountState = setAmountState,
onQrScannerOpen = goToQrScanner,
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.ui.R
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.canSpend
import co.electriccoin.zcash.ui.common.model.spendableBalance
@ -94,7 +95,8 @@ private fun PreviewSendForm() {
setAmountState = {},
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
setMemoState = {},
memoState = MemoState.new("Test message")
memoState = MemoState.new("Test message"),
walletRestoringState = WalletRestoringState.NONE
)
}
}
@ -106,7 +108,6 @@ private fun PreviewSendForm() {
@Suppress("LongParameterList")
@Composable
fun Send(
walletSnapshot: WalletSnapshot,
sendStage: SendStage,
onCreateZecSend: (ZecSend) -> Unit,
focusManager: FocusManager,
@ -121,9 +122,14 @@ fun Send(
amountState: AmountState,
setMemoState: (MemoState) -> Unit,
memoState: MemoState,
walletRestoringState: WalletRestoringState,
walletSnapshot: WalletSnapshot,
) {
Scaffold(topBar = {
SendTopAppBar(onSettings = onSettings)
SendTopAppBar(
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
onSettings = onSettings
)
}) { paddingValues ->
SendMainContent(
walletSnapshot = walletSnapshot,
@ -153,8 +159,17 @@ fun Send(
}
@Composable
private fun SendTopAppBar(onSettings: () -> Unit) {
private fun SendTopAppBar(
onSettings: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.send_stage_send_title),
hamburgerMenuActions = {
IconButton(
@ -166,7 +181,7 @@ private fun SendTopAppBar(onSettings: () -> Unit) {
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.ui.MainActivity
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.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.send.ext.Saver
@ -52,13 +53,15 @@ internal fun MainActivity.WrapSendConfirmation(
val createTransactionsViewModel by viewModels<CreateTransactionsViewModel>()
val viewModel by viewModels<SupportViewModel>()
val supportViewModel by viewModels<SupportViewModel>()
val synchronizer = walletViewModel.synchronizer.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(
activity = this,
@ -69,6 +72,7 @@ internal fun MainActivity.WrapSendConfirmation(
spendingKey = spendingKey,
supportMessage = supportMessage,
synchronizer = synchronizer,
walletRestoringState = walletRestoringState,
)
}
@ -84,6 +88,7 @@ internal fun WrapSendConfirmation(
spendingKey: UnifiedSpendingKey?,
supportMessage: SupportInfo?,
synchronizer: Synchronizer?,
walletRestoringState: WalletRestoringState
) {
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 co.electriccoin.zcash.ui.R
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.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.Body
@ -124,9 +125,16 @@ fun SendConfirmation(
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend?,
walletRestoringState: WalletRestoringState,
) {
Scaffold(
topBar = { SendConfirmationTopAppBar(onBack, stage) },
topBar = {
SendConfirmationTopAppBar(
onBack = onBack,
stage = stage,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
SendConfirmationMainContent(
@ -152,26 +160,49 @@ fun SendConfirmation(
@Composable
private fun SendConfirmationTopAppBar(
onBack: () -> Unit,
stage: SendConfirmationStage
stage: SendConfirmationStage,
showRestoring: Boolean
) {
when (stage) {
SendConfirmationStage.Confirmation,
SendConfirmationStage.Sending,
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 -> {
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 -> {
SmallTopAppBar(
restoringLabel =
if (showRestoring) {
stringResource(id = R.string.restoring_wallet_label)
} else {
null
},
titleText = stringResource(id = R.string.send_confirmation_multiple_error_title),
backText = stringResource(id = R.string.send_confirmation_multiple_error_back),
backContentDescriptionText =
stringResource(
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 co.electriccoin.zcash.ui.MainActivity
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.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
@ -24,40 +25,47 @@ internal fun MainActivity.WrapSettings(
goBack: () -> Unit,
goFeedback: () -> Unit,
) {
val walletViewModel by viewModels<WalletViewModel>()
val settingsViewModel by viewModels<SettingsViewModel>()
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
WrapSettings(
activity = this,
goAbout = goAbout,
goAdvancedSettings = goAdvancedSettings,
goBack = goBack,
goFeedback = goFeedback,
settingsViewModel = settingsViewModel,
walletViewModel = walletViewModel,
walletRestoringState = walletRestoringState
)
}
@Composable
@Suppress("LongParameterList")
private fun WrapSettings(
activity: ComponentActivity,
goAbout: () -> Unit,
goAdvancedSettings: () -> Unit,
goBack: () -> 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 isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value
val versionInfo = VersionInfo.new(activity.applicationContext)
BackHandler {
goBack()
}
@Suppress("ComplexCondition")
if (null == synchronizer ||
null == isAnalyticsEnabled ||
if (null == isAnalyticsEnabled ||
null == isBackgroundSyncEnabled ||
null == isKeepScreenOnWhileSyncing
) {
@ -90,7 +98,8 @@ private fun WrapSettings(
},
onAnalyticsSettingsChanged = {
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.tooling.preview.Preview
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.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
@ -60,6 +61,7 @@ private fun PreviewSettings() {
isAnalyticsEnabled = false,
isRescanEnabled = false
),
walletRestoringState = WalletRestoringState.NONE,
)
}
}
@ -77,6 +79,7 @@ fun Settings(
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
troubleshootingParameters: TroubleshootingParameters,
walletRestoringState: WalletRestoringState,
) {
Scaffold(topBar = {
SettingsTopAppBar(
@ -86,6 +89,7 @@ fun Settings(
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged,
onRescanWallet = onRescanWallet,
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
}) { paddingValues ->
SettingsMainContent(
@ -116,12 +120,20 @@ private fun SettingsTopAppBar(
onAnalyticsSettingsChanged: (Boolean) -> Unit,
onRescanWallet: () -> Unit,
onBack: () -> Unit,
showRestoring: Boolean
) {
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(),
backContentDescriptionText = stringResource(R.string.settings_back_content_description),
onBack = onBack,
showTitleLogo = true,
regularActions = {
if (troubleshootingParameters.isEnabled) {
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 co.electriccoin.zcash.ui.MainActivity
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.SupportInfoType
import co.electriccoin.zcash.ui.screen.support.view.Support
@ -23,17 +25,31 @@ import kotlinx.coroutines.launch
@Composable
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
internal fun WrapSupport(
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 scope = rememberCoroutineScope()
val (isShowingDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) }
@ -44,7 +60,7 @@ internal fun WrapSupport(
setShowDialog = setShowDialog,
onBack = goBack,
onSend = { userMessage ->
val fullMessage = formatMessage(userMessage, supportMessage)
val fullMessage = formatMessage(userMessage, supportInfo)
val mailIntent =
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
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.rememberScrollState
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.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
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.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview("Support")
@ -41,11 +45,12 @@ private fun PreviewSupport() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Support(
snackbarHostState = SnackbarHostState(),
isShowingDialog = false,
setShowDialog = {},
onBack = {},
onSend = {},
isShowingDialog = false,
setShowDialog = {}
snackbarHostState = SnackbarHostState(),
walletRestoringState = WalletRestoringState.NONE
)
}
}
@ -65,18 +70,23 @@ private fun PreviewSupportPopup() {
}
@Composable
@Suppress("LongParameterList")
fun Support(
isShowingDialog: Boolean,
setShowDialog: (Boolean) -> Unit,
onBack: () -> Unit,
onSend: (String) -> Unit,
snackbarHostState: SnackbarHostState,
walletRestoringState: WalletRestoringState,
) {
val (message, setMessage) = rememberSaveable { mutableStateOf("") }
Scaffold(
topBar = {
SupportTopAppBar(onBack = onBack)
SupportTopAppBar(
onBack = onBack,
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
@ -103,20 +113,21 @@ fun Support(
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun SupportTopAppBar(onBack: () -> Unit) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.support_header)) },
navigationIcon = {
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.support_back_content_description)
)
}
}
private fun SupportTopAppBar(
onBack: () -> Unit,
showRestoring: Boolean
) {
SmallTopAppBar(
restoringLabel =
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,
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
Column(
Modifier
.fillMaxHeight()
@ -137,7 +150,19 @@ private fun SupportMainContent(
.then(modifier),
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))
@ -146,14 +171,13 @@ private fun SupportMainContent(
onValueChange = setMessage,
modifier =
Modifier
.fillMaxWidth(),
.fillMaxWidth()
.focusRequester(focusRequester),
placeholder = { Text(text = stringResource(id = R.string.support_hint)) },
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Body(stringResource(id = R.string.support_disclaimer))
Spacer(
modifier =
Modifier
@ -166,11 +190,19 @@ private fun SupportMainContent(
PrimaryButton(
onClick = { setShowDialog(true) },
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))
}
LaunchedEffect(Unit) {
// Causes the TextFiled to focus on the first screen visit
focusRequester.requestFocus()
}
}
@Composable

View File

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

View File

@ -7,4 +7,5 @@
<string name="balance_widget_available">Available Balance:</string>
<!-- This is replaced by a resource overlay via app/build.gradle.kts -->
<string name="support_email_address" />
<string name="restoring_wallet_label">[Restoring Your Wallet…]</string>
</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">
<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_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
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_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>
</resources>

View File

@ -507,7 +507,7 @@ private fun supportScreenshots(
tag: String,
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()
}