[#1165] Syncing progressbar

* [#1165] Syncing progressbar

-  UI + logic + tests
- Closes  #1165

* Changelog update
This commit is contained in:
Honza Rychnovský 2024-02-08 11:28:00 +01:00 committed by GitHub
parent 1058802b19
commit cc333ea902
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 521 additions and 259 deletions

View File

@ -9,10 +9,18 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased] ## [Unreleased]
### Changed
- Update the Zcash SDK dependency to version 2.0.6, which adds more details on current balances
### Added
- The Balances screen now provides details on current balances like Change pending and Pending transactions
- The screen also adds a new Block synchronization progress bar and status, which were initially part of the Account
screen and redesigned
### Fixed ### Fixed
- Fixed character replacement in Zcash addresses on the Receive screen caused by ligatures in the app's primary font - Fixed character replacement in Zcash addresses on the Receive screen caused by ligatures in the app's primary font
using the secondary font. This will be revisited once a proper font is added. using the secondary font. This will be revisited once a proper font is added.
- Improved spacing of titles of bottom navigation tabs so they work better on smaller screens - Improved spacing of titles of bottom navigation tabs, so they work better on smaller screens
## [0.2.0 (541)] - 2024-01-30 ## [0.2.0 (541)] - 2024-01-30
- Update the Zcash SDK dependency to version 2.0.5, which improves the performance of block synchronization - Update the Zcash SDK dependency to version 2.0.5, which improves the performance of block synchronization

View File

@ -3,11 +3,15 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -35,7 +39,7 @@ fun CircularScreenProgressIndicator(modifier: Modifier = Modifier) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
color = ZcashTheme.colors.progressBarScreen, color = ZcashTheme.colors.circularProgressBarScreen,
modifier = modifier =
Modifier Modifier
.size(ZcashTheme.dimens.circularScreenProgressWidth) .size(ZcashTheme.dimens.circularScreenProgressWidth)
@ -46,7 +50,7 @@ fun CircularScreenProgressIndicator(modifier: Modifier = Modifier) {
@Composable @Composable
fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) { fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) {
CircularProgressIndicator( CircularProgressIndicator(
color = ZcashTheme.colors.progressBarSmall, color = ZcashTheme.colors.circularProgressBarSmall,
strokeWidth = 2.dp, strokeWidth = 2.dp,
modifier = modifier =
Modifier Modifier
@ -54,3 +58,32 @@ fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) {
.then(modifier) .then(modifier)
) )
} }
@Preview
@Composable
private fun LinearProgressIndicatorComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
@Suppress("MagicNumber")
LinearProgressIndicator(0.75f)
}
}
}
@Composable
fun LinearProgressIndicator(
progress: Float,
modifier: Modifier = Modifier
) {
LinearProgressIndicator(
progress = progress,
color = ZcashTheme.colors.linearProgressBarBackground,
trackColor = ZcashTheme.colors.linearProgressBarTrack,
strokeCap = StrokeCap.Butt,
modifier =
Modifier
.fillMaxWidth()
.height(ZcashTheme.dimens.linearProgressHeight)
.then(modifier)
)
}

View File

@ -125,6 +125,7 @@ fun BodySmall(
maxLines: Int = Int.MAX_VALUE, maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip, overflow: TextOverflow = TextOverflow.Clip,
textAlign: TextAlign = TextAlign.Start, textAlign: TextAlign = TextAlign.Start,
textFontWeight: FontWeight = FontWeight.Normal,
color: Color = MaterialTheme.colorScheme.onBackground, color: Color = MaterialTheme.colorScheme.onBackground,
) { ) {
Text( Text(
@ -135,6 +136,7 @@ fun BodySmall(
textAlign = textAlign, textAlign = textAlign,
modifier = modifier, modifier = modifier,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = textFontWeight
) )
} }

View File

@ -31,6 +31,7 @@ data class Dimens(
// Progress // Progress
val circularScreenProgressWidth: Dp, val circularScreenProgressWidth: Dp,
val circularSmallProgressWidth: Dp, val circularSmallProgressWidth: Dp,
val linearProgressHeight: Dp,
// TopAppBar: // TopAppBar:
val topAppBarZcashLogoHeight: Dp, val topAppBarZcashLogoHeight: Dp,
val topAppBarActionRippleCorner: Dp, val topAppBarActionRippleCorner: Dp,
@ -67,6 +68,7 @@ private val defaultDimens =
chipStroke = 0.5.dp, chipStroke = 0.5.dp,
circularScreenProgressWidth = 48.dp, circularScreenProgressWidth = 48.dp,
circularSmallProgressWidth = 14.dp, circularSmallProgressWidth = 14.dp,
linearProgressHeight = 14.dp,
topAppBarZcashLogoHeight = 24.dp, topAppBarZcashLogoHeight = 24.dp,
topAppBarActionRippleCorner = 28.dp, topAppBarActionRippleCorner = 28.dp,
textFieldDefaultHeight = 64.dp, textFieldDefaultHeight = 64.dp,

View File

@ -14,8 +14,10 @@ data class ExtendedColors(
val onTertiary: Color, val onTertiary: Color,
val callout: Color, val callout: Color,
val onCallout: Color, val onCallout: Color,
val progressBarSmall: Color, val circularProgressBarSmall: Color,
val progressBarScreen: Color, val circularProgressBarScreen: Color,
val linearProgressBarTrack: Color,
val linearProgressBarBackground: Color,
val chipIndex: Color, val chipIndex: Color,
val textCommon: Color, val textCommon: Color,
val textFieldHint: Color, val textFieldHint: Color,
@ -37,6 +39,7 @@ data class ExtendedColors(
val dividerColor: Color, val dividerColor: Color,
val darkDividerColor: Color, val darkDividerColor: Color,
val tabTextColor: Color, val tabTextColor: Color,
val panelBackgroundColor: Color,
) { ) {
@Composable @Composable
fun surfaceGradient() = fun surfaceGradient() =

View File

@ -30,7 +30,15 @@ internal object Dark {
val textDescription = Color(0xFF777777) val textDescription = Color(0xFF777777)
val textProgress = Color(0xFF8B8A8A) val textProgress = Color(0xFF8B8A8A)
val aboutTextColor = Color.Unspecified
val screenTitleColor = Color(0xFF040404)
val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728)
val dividerColor = Color(0xFFDDDDDD)
val darkDividerColor = Color(0xFF000000)
val tabTextColor = Color(0xFF040404)
val layoutStroke = Color(0xFFFFFFFF) val layoutStroke = Color(0xFFFFFFFF)
val panelBackgroundColor = Color(0xFFEAEAEA)
val primaryButton = Color(0xFFFFFFFF) val primaryButton = Color(0xFFFFFFFF)
val primaryButtonPressed = Color(0xFFFFFFFF) val primaryButtonPressed = Color(0xFFFFFFFF)
@ -46,8 +54,10 @@ internal object Dark {
val navigationButton = Color(0xFFFFFFFF) val navigationButton = Color(0xFFFFFFFF)
val navigationButtonPressed = Color(0xFFFFFFFF) val navigationButtonPressed = Color(0xFFFFFFFF)
val progressBarSmall = Color(0xFF8B8A8A) val circularProgressBarSmall = Color(0xFF8B8A8A)
val progressBarScreen = Color(0xFFFFFFFF) val circularProgressBarScreen = Color(0xFFFFFFFF)
val linearProgressBarTrack = Color(0xFFD9D9D9)
val linearProgressBarBackground = Light.complementaryColor
val callout = Color(0xFFFFFFFF) val callout = Color(0xFFFFFFFF)
val onCallout = Color(0xFFFFFFFF) val onCallout = Color(0xFFFFFFFF)
@ -64,15 +74,6 @@ internal object Dark {
val disabledButtonTextColor = Color(0xFFDDDDDD) val disabledButtonTextColor = Color(0xFFDDDDDD)
val buttonShadowColor = Color(0xFFFFFFFF) val buttonShadowColor = Color(0xFFFFFFFF)
// Proper values will be added later, see #998
val aboutTextColor = Color.Unspecified
val screenTitleColor = Color(0xFF040404)
val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728)
val dividerColor = Color(0xFFDDDDDD)
val darkDividerColor = Color(0xFF000000)
val tabTextColor = Color(0xFF040404)
} }
internal object Light { internal object Light {
@ -91,7 +92,15 @@ internal object Light {
val textDescription = Color(0xFF777777) val textDescription = Color(0xFF777777)
val textProgress = Color(0xFF8B8A8A) val textProgress = Color(0xFF8B8A8A)
val screenTitleColor = Color(0xFF040404)
val aboutTextColor = Color(0xFF4E4E4E)
val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728)
val dividerColor = Color(0xFFDDDDDD)
val darkDividerColor = Color(0xFF000000)
val tabTextColor = Color(0xFF040404)
val layoutStroke = Color(0xFF000000) val layoutStroke = Color(0xFF000000)
val panelBackgroundColor = Color(0xFFEAEAEA)
val primaryButton = Color(0xFF000000) val primaryButton = Color(0xFF000000)
val primaryButtonPressed = Color(0xFF000000) val primaryButtonPressed = Color(0xFF000000)
@ -107,8 +116,10 @@ internal object Light {
val navigationButton = Color(0xFFFFFFFF) val navigationButton = Color(0xFFFFFFFF)
val navigationButtonPressed = Color(0xFFFFFFFF) val navigationButtonPressed = Color(0xFFFFFFFF)
val progressBarSmall = Color(0xFF8B8A8A) val circularProgressBarSmall = Color(0xFF8B8A8A)
val progressBarScreen = Color(0xFF000000) val circularProgressBarScreen = Color(0xFF000000)
val linearProgressBarTrack = Color(0xFFD9D9D9)
val linearProgressBarBackground = complementaryColor
val callout = Color(0xFFFFFFFF) val callout = Color(0xFFFFFFFF)
val onCallout = Color(0xFFFFFFFF) val onCallout = Color(0xFFFFFFFF)
@ -124,14 +135,6 @@ internal object Light {
val disabledButtonColor = Color(0xFFB7B7B7) val disabledButtonColor = Color(0xFFB7B7B7)
val disabledButtonTextColor = Color(0xFFDDDDDD) val disabledButtonTextColor = Color(0xFFDDDDDD)
val buttonShadowColor = Color(0xFF000000) val buttonShadowColor = Color(0xFF000000)
val screenTitleColor = Color(0xFF040404)
val aboutTextColor = Color(0xFF4E4E4E)
val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728)
val dividerColor = Color(0xFFDDDDDD)
val darkDividerColor = Color(0xFF000000)
val tabTextColor = Color(0xFF040404)
} }
internal val DarkColorPalette = internal val DarkColorPalette =
@ -166,8 +169,10 @@ internal val DarkExtendedColorPalette =
onTertiary = Dark.textTertiaryButton, onTertiary = Dark.textTertiaryButton,
callout = Dark.callout, callout = Dark.callout,
onCallout = Dark.onCallout, onCallout = Dark.onCallout,
progressBarSmall = Dark.progressBarSmall, circularProgressBarSmall = Dark.circularProgressBarSmall,
progressBarScreen = Dark.progressBarScreen, circularProgressBarScreen = Dark.circularProgressBarScreen,
linearProgressBarTrack = Dark.linearProgressBarTrack,
linearProgressBarBackground = Dark.linearProgressBarBackground,
chipIndex = Dark.textChipIndex, chipIndex = Dark.textChipIndex,
textCommon = Dark.textCommon, textCommon = Dark.textCommon,
textFieldHint = Dark.textFieldHint, textFieldHint = Dark.textFieldHint,
@ -189,6 +194,7 @@ internal val DarkExtendedColorPalette =
dividerColor = Dark.dividerColor, dividerColor = Dark.dividerColor,
darkDividerColor = Dark.darkDividerColor, darkDividerColor = Dark.darkDividerColor,
tabTextColor = Dark.tabTextColor, tabTextColor = Dark.tabTextColor,
panelBackgroundColor = Dark.panelBackgroundColor,
) )
internal val LightExtendedColorPalette = internal val LightExtendedColorPalette =
@ -199,8 +205,10 @@ internal val LightExtendedColorPalette =
onTertiary = Light.textTertiaryButton, onTertiary = Light.textTertiaryButton,
callout = Light.callout, callout = Light.callout,
onCallout = Light.onCallout, onCallout = Light.onCallout,
progressBarScreen = Light.progressBarScreen, circularProgressBarScreen = Light.circularProgressBarScreen,
progressBarSmall = Light.progressBarSmall, circularProgressBarSmall = Light.circularProgressBarSmall,
linearProgressBarTrack = Light.linearProgressBarTrack,
linearProgressBarBackground = Light.linearProgressBarBackground,
chipIndex = Light.textChipIndex, chipIndex = Light.textChipIndex,
textCommon = Light.textCommon, textCommon = Light.textCommon,
textFieldHint = Light.textFieldHint, textFieldHint = Light.textFieldHint,
@ -222,6 +230,7 @@ internal val LightExtendedColorPalette =
dividerColor = Light.dividerColor, dividerColor = Light.dividerColor,
darkDividerColor = Light.darkDividerColor, darkDividerColor = Light.darkDividerColor,
tabTextColor = Light.tabTextColor, tabTextColor = Light.tabTextColor,
panelBackgroundColor = Light.panelBackgroundColor,
) )
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
@ -234,8 +243,10 @@ internal val LocalExtendedColors =
onTertiary = Color.Unspecified, onTertiary = Color.Unspecified,
callout = Color.Unspecified, callout = Color.Unspecified,
onCallout = Color.Unspecified, onCallout = Color.Unspecified,
progressBarScreen = Color.Unspecified, circularProgressBarScreen = Color.Unspecified,
progressBarSmall = Color.Unspecified, circularProgressBarSmall = Color.Unspecified,
linearProgressBarTrack = Color.Unspecified,
linearProgressBarBackground = Color.Unspecified,
chipIndex = Color.Unspecified, chipIndex = Color.Unspecified,
textCommon = Color.Unspecified, textCommon = Color.Unspecified,
textFieldHint = Color.Unspecified, textFieldHint = Color.Unspecified,
@ -256,6 +267,7 @@ internal val LocalExtendedColors =
complementaryColor = Color.Unspecified, complementaryColor = Color.Unspecified,
dividerColor = Color.Unspecified, dividerColor = Color.Unspecified,
darkDividerColor = Color.Unspecified, darkDividerColor = Color.Unspecified,
tabTextColor = Color.Unspecified tabTextColor = Color.Unspecified,
panelBackgroundColor = Color.Unspecified,
) )
} }

View File

@ -10,7 +10,6 @@ import java.util.concurrent.atomic.AtomicInteger
class AccountTestSetup( class AccountTestSetup(
private val composeTestRule: ComposeContentTestRule, private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot, private val walletSnapshot: WalletSnapshot,
private val isShowFiatConversion: Boolean
) { ) {
private val onSettingsCount = AtomicInteger(0) private val onSettingsCount = AtomicInteger(0)
private val onReceiveCount = AtomicInteger(0) private val onReceiveCount = AtomicInteger(0)
@ -46,10 +45,8 @@ class AccountTestSetup(
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
fun DefaultContent() { fun DefaultContent() {
Account( Account(
walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = false, isKeepScreenOnWhileSyncing = false,
isKeepScreenOnDuringSync = false,
isFiatConversionEnabled = isShowFiatConversion,
goSettings = { goSettings = {
onSettingsCount.incrementAndGet() onSettingsCount.incrementAndGet()
}, },

View File

@ -7,15 +7,12 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -27,7 +24,6 @@ class AccountViewIntegrationTest : UiTestPrerequisites() {
AccountTestSetup( AccountTestSetup(
composeTestRule, composeTestRule,
walletSnapshot, walletSnapshot,
isShowFiatConversion = false
) )
// This is just basic sanity check that we still have UI set up as expected after the state restore // This is just basic sanity check that we still have UI set up as expected after the state restore
@ -37,8 +33,9 @@ class AccountViewIntegrationTest : UiTestPrerequisites() {
val restorationTester = StateRestorationTester(composeTestRule) val restorationTester = StateRestorationTester(composeTestRule)
val walletSnapshot = val walletSnapshot =
WalletSnapshotFixture.new( WalletSnapshotFixture.new(
status = Synchronizer.Status.SYNCING, saplingBalance = WalletSnapshotFixture.SAPLING_BALANCE,
progress = PercentDecimal(0.5f) orchardBalance = WalletSnapshotFixture.ORCHARD_BALANCE,
transparentBalance = WalletSnapshotFixture.TRANSPARENT_BALANCE
) )
val testSetup = newTestSetup(walletSnapshot) val testSetup = newTestSetup(walletSnapshot)
@ -46,21 +43,17 @@ class AccountViewIntegrationTest : UiTestPrerequisites() {
testSetup.DefaultContent() testSetup.DefaultContent()
} }
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status) assertEquals(WalletSnapshotFixture.SAPLING_BALANCE, testSetup.getWalletSnapshot().saplingBalance)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status) assertEquals(WalletSnapshotFixture.ORCHARD_BALANCE, testSetup.getWalletSnapshot().orchardBalance)
assertEquals(WalletSnapshotFixture.TRANSPARENT_BALANCE, testSetup.getWalletSnapshot().transparentBalance)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
restorationTester.emulateSavedInstanceStateRestore() restorationTester.emulateSavedInstanceStateRestore()
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status) assertEquals(WalletSnapshotFixture.SAPLING_BALANCE, testSetup.getWalletSnapshot().saplingBalance)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status) assertEquals(WalletSnapshotFixture.ORCHARD_BALANCE, testSetup.getWalletSnapshot().orchardBalance)
assertEquals(WalletSnapshotFixture.TRANSPARENT_BALANCE, testSetup.getWalletSnapshot().transparentBalance)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress) composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
composeTestRule.onNodeWithTag(AccountTag.SINGLE_LINE_TEXT).also {
it.assertIsDisplayed() it.assertIsDisplayed()
it.assertWidthIsAtLeast(1.dp) it.assertWidthIsAtLeast(1.dp)
} }

View File

@ -38,11 +38,7 @@ class AccountViewTest : UiTestPrerequisites() {
it.assertIsDisplayed() it.assertIsDisplayed()
} }
composeTestRule.onNodeWithTag(AccountTag.STATUS_VIEWS).also { composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithTag(AccountTag.FIAT_CONVERSION).also {
it.assertIsDisplayed() it.assertIsDisplayed()
} }
@ -51,16 +47,6 @@ class AccountViewTest : UiTestPrerequisites() {
} }
} }
@Test
@MediumTest
fun hide_fiat_conversion() {
newTestSetup(isShowFiatConversion = false)
composeTestRule.onNodeWithTag(AccountTag.FIAT_CONVERSION).also {
it.assertDoesNotExist()
}
}
@Test @Test
@MediumTest @MediumTest
fun click_history_button() { fun click_history_button() {
@ -78,23 +64,20 @@ class AccountViewTest : UiTestPrerequisites() {
fun hamburger_settings_test() { fun hamburger_settings_test() {
val testSetup = newTestSetup() val testSetup = newTestSetup()
Assert.assertEquals(0, testSetup.getOnReceiveCount()) Assert.assertEquals(0, testSetup.getOnSettingsCount())
composeTestRule.clickSettingsTopAppBarMenu() composeTestRule.clickSettingsTopAppBarMenu()
Assert.assertEquals(1, testSetup.getOnSettingsCount()) Assert.assertEquals(1, testSetup.getOnSettingsCount())
} }
private fun newTestSetup( private fun newTestSetup(walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new()) =
isShowFiatConversion: Boolean = true, AccountTestSetup(
walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new() composeTestRule,
) = AccountTestSetup( walletSnapshot = walletSnapshot,
composeTestRule, ).apply {
walletSnapshot = walletSnapshot, setDefaultContent()
isShowFiatConversion = isShowFiatConversion }
).apply {
setDefaultContent()
}
} }
private fun ComposeContentTestRule.clickHistory() { private fun ComposeContentTestRule.clickHistory() {

View File

@ -0,0 +1,48 @@
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.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.balances.view.Balances
import java.util.concurrent.atomic.AtomicInteger
class BalancesTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot,
private val isShowFiatConversion: Boolean
) {
private val onSettingsCount = AtomicInteger(0)
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onSettingsCount.get()
}
fun getWalletSnapshot(): WalletSnapshot {
composeTestRule.waitForIdle()
return walletSnapshot
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
Balances(
isFiatConversionEnabled = isShowFiatConversion,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
onSettings = {
onSettingsCount.incrementAndGet()
},
walletSnapshot = walletSnapshot,
)
}
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
DefaultContent()
}
}
}
}

View File

@ -0,0 +1,68 @@
package co.electriccoin.zcash.ui.screen.balances.integration
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.BalancesTestSetup
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
class BalancesViewIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
BalancesTestSetup(
composeTestRule,
walletSnapshot,
isShowFiatConversion = true
)
// This is just basic sanity check that we still have UI set up as expected after the state restore
@Test
@MediumTest
fun wallet_snapshot_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val walletSnapshot =
WalletSnapshotFixture.new(
status = Synchronizer.Status.SYNCING,
progress = PercentDecimal(0.5f)
)
val testSetup = newTestSetup(walletSnapshot)
restorationTester.setContent {
testSetup.DefaultContent()
}
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
restorationTester.emulateSavedInstanceStateRestore()
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
composeTestRule.onNodeWithTag(BalancesTag.STATUS).also {
it.assertIsDisplayed()
it.assertWidthIsAtLeast(1.dp)
}
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.account.model package co.electriccoin.zcash.ui.screen.balances.model
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
@ -37,7 +37,7 @@ class WalletDisplayValuesTest {
assertNotNull(values) assertNotNull(values)
assertEquals(1f, values.progress.decimal) assertEquals(1f, values.progress.decimal)
assertEquals(walletSnapshot.totalBalance().toZecString(), values.zecAmountText) assertEquals(walletSnapshot.totalBalance().toZecString(), values.zecAmountText)
assertTrue(values.statusText.startsWith(getStringResource(R.string.account_status_syncing_catchup))) assertTrue(values.statusText.startsWith(getStringResource(R.string.balances_status_syncing)))
// TODO [#578]: Provide Zatoshi -> USD fiat currency formatting // TODO [#578]: Provide Zatoshi -> USD fiat currency formatting
// TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578 // TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578
assertEquals(FiatCurrencyConversionRateState.Unavailable, values.fiatCurrencyAmountState) assertEquals(FiatCurrencyConversionRateState.Unavailable, values.fiatCurrencyAmountState)

View File

@ -0,0 +1,69 @@
package co.electriccoin.zcash.ui.screen.balances.view
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.BalancesTestSetup
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import kotlin.test.DefaultAsserter.assertEquals
// TODO [#1227]: Cover Balances UI and logic with tests
// TODO [#1227]: https://github.com/Electric-Coin-Company/zashi-android/issues/1227
class BalancesViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun check_all_elementary_ui_elements_displayed() {
newTestSetup()
composeTestRule.onNodeWithTag(CommonTag.TOP_APP_BAR)
.also {
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun hide_fiat_conversion() {
newTestSetup(isShowFiatConversion = false)
composeTestRule.onNodeWithTag(BalancesTag.FIAT_CONVERSION).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun hamburger_settings_test() {
val testSetup = newTestSetup()
assertEquals("Failed in comparison", 0, testSetup.getOnSettingsCount())
composeTestRule.clickSettingsTopAppBarMenu()
Assert.assertEquals(1, testSetup.getOnSettingsCount())
}
private fun newTestSetup(
isShowFiatConversion: Boolean = true,
walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new()
) = BalancesTestSetup(
composeTestRule,
walletSnapshot = walletSnapshot,
isShowFiatConversion = isShowFiatConversion
).apply {
setDefaultContent()
}
}

View File

@ -4,7 +4,5 @@ package co.electriccoin.zcash.ui.screen.account
* These are only used for automated testing. * These are only used for automated testing.
*/ */
object AccountTag { object AccountTag {
const val STATUS_VIEWS = "status_views" const val BALANCE_VIEWS = "balance_views"
const val SINGLE_LINE_TEXT = "single_line_text"
const val FIAT_CONVERSION = "fiat_conversion"
} }

View File

@ -6,15 +6,10 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.account.view.Account import co.electriccoin.zcash.ui.screen.account.view.Account
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel 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
@Composable @Composable
internal fun WrapAccount( internal fun WrapAccount(
@ -23,25 +18,12 @@ internal fun WrapAccount(
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
) { ) {
// Show information about the app update, if available
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application,
AppUpdateCheckerImp.new()
)
}
val updateAvailable =
checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let {
it?.appUpdateInfo != null && it.state == UpdateState.Prepared
}
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val settingsViewModel by activity.viewModels<SettingsViewModel>() val settingsViewModel by activity.viewModels<SettingsViewModel>()
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current)
if (null == walletSnapshot) { if (null == walletSnapshot) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -51,9 +33,7 @@ internal fun WrapAccount(
} else { } else {
Account( Account(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = updateAvailable, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isFiatConversionEnabled = isFiatConversionEnabled,
goBalances = goBalances, goBalances = goBalances,
goHistory = goHistory, goHistory = goHistory,
goSettings = goSettings, goSettings = goSettings,

View File

@ -14,28 +14,23 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.BalanceWidget import co.electriccoin.zcash.ui.common.BalanceWidget
import co.electriccoin.zcash.ui.common.DisableScreenTimeout import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.model.WalletDisplayValues
@Preview("Account") @Preview("Account")
@Composable @Composable
@ -44,9 +39,7 @@ private fun ComposablePreview() {
GradientSurface { GradientSurface {
Account( Account(
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
isUpdateAvailable = false, isKeepScreenOnWhileSyncing = false,
isKeepScreenOnDuringSync = false,
isFiatConversionEnabled = false,
goHistory = {}, goHistory = {},
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
@ -55,13 +48,10 @@ private fun ComposablePreview() {
} }
} }
@Suppress("LongParameterList")
@Composable @Composable
fun Account( fun Account(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isUpdateAvailable: Boolean, isKeepScreenOnWhileSyncing: Boolean?,
isKeepScreenOnDuringSync: Boolean?,
isFiatConversionEnabled: Boolean,
goBalances: () -> Unit, goBalances: () -> Unit,
goHistory: () -> Unit, goHistory: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
@ -71,9 +61,7 @@ fun Account(
}) { paddingValues -> }) { paddingValues ->
AccountMainContent( AccountMainContent(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isUpdateAvailable = isUpdateAvailable, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
isFiatConversionEnabled = isFiatConversionEnabled,
goHistory = goHistory, goHistory = goHistory,
goBalances = goBalances, goBalances = goBalances,
modifier = modifier =
@ -105,13 +93,10 @@ private fun AccountTopAppBar(onSettings: () -> Unit) {
) )
} }
@Suppress("LongParameterList")
@Composable @Composable
private fun AccountMainContent( private fun AccountMainContent(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isUpdateAvailable: Boolean, isKeepScreenOnWhileSyncing: Boolean?,
isKeepScreenOnDuringSync: Boolean?,
isFiatConversionEnabled: Boolean,
goBalances: () -> Unit, goBalances: () -> Unit,
goHistory: () -> Unit, goHistory: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -125,7 +110,7 @@ private fun AccountMainContent(
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Status(walletSnapshot, isUpdateAvailable, isFiatConversionEnabled, goBalances) BalancesStatus(walletSnapshot, goBalances)
Spacer( Spacer(
modifier = modifier =
@ -138,72 +123,29 @@ private fun AccountMainContent(
PrimaryButton(onClick = goHistory, text = stringResource(R.string.account_button_history)) PrimaryButton(onClick = goHistory, text = stringResource(R.string.account_button_history))
if (isKeepScreenOnDuringSync == true && walletSnapshot.status == Synchronizer.Status.SYNCING) { if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout() DisableScreenTimeout()
} }
} }
} }
@Composable @Composable
@Suppress("LongMethod", "MagicNumber") @Suppress("LongMethod")
private fun Status( private fun BalancesStatus(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
updateAvailable: Boolean,
isFiatConversionEnabled: Boolean,
goBalances: () -> Unit goBalances: () -> Unit
) { ) {
val walletDisplayValues =
WalletDisplayValues.getNextValues(
LocalContext.current,
walletSnapshot,
updateAvailable
)
Column( Column(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(AccountTag.STATUS_VIEWS), .testTag(AccountTag.BALANCE_VIEWS),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (walletDisplayValues.zecAmountText.isNotEmpty()) { BalanceWidget(
BalanceWidget( walletSnapshot = walletSnapshot,
walletSnapshot = walletSnapshot, isReferenceToBalances = true,
isReferenceToBalances = true, onReferenceClick = goBalances
onReferenceClick = goBalances )
)
}
if (isFiatConversionEnabled) {
Column(Modifier.testTag(AccountTag.FIAT_CONVERSION)) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
when (walletDisplayValues.fiatCurrencyAmountState) {
is FiatCurrencyConversionRateState.Current -> {
BodyWithFiatCurrencySymbol(
amount = walletDisplayValues.fiatCurrencyAmountText
)
}
is FiatCurrencyConversionRateState.Stale -> {
// Note: we should show information about staleness too
BodyWithFiatCurrencySymbol(
amount = walletDisplayValues.fiatCurrencyAmountText
)
}
is FiatCurrencyConversionRateState.Unavailable -> {
Body(text = walletDisplayValues.fiatCurrencyAmountText)
}
}
}
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
if (walletDisplayValues.statusText.isNotEmpty()) {
Body(
text = walletDisplayValues.statusText,
modifier = Modifier.testTag(AccountTag.SINGLE_LINE_TEXT)
)
}
} }
} }

View File

@ -6,19 +6,45 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.balances.view.Balances import co.electriccoin.zcash.ui.screen.balances.view.Balances
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
@Composable @Composable
internal fun WrapBalances( internal fun WrapBalances(
activity: ComponentActivity, activity: ComponentActivity,
goSettings: () -> Unit, goSettings: () -> Unit,
) { ) {
// Show information about the app update, if available
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application,
AppUpdateCheckerImp.new()
)
}
val isUpdateAvailable =
checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let {
it?.appUpdateInfo != null && it.state == UpdateState.Prepared
}
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current)
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
Balances( Balances(
walletSnapshot = walletSnapshot, isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
onSettings = goSettings, onSettings = goSettings,
walletSnapshot = walletSnapshot,
) )
} }

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.balances
/**
* These are only used for automated testing.
*/
object BalancesTag {
const val STATUS = "status"
const val FIAT_CONVERSION = "fiat_conversion"
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.account.model package co.electriccoin.zcash.ui.screen.balances.model
import android.content.Context import android.content.Context
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
@ -8,7 +8,6 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.toFiatCurrencyState import cash.z.ecc.android.sdk.model.toFiatCurrencyState
import cash.z.ecc.android.sdk.model.toZecString 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.R
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.model.spendableBalance import co.electriccoin.zcash.ui.common.model.spendableBalance
@ -48,47 +47,43 @@ data class WalletDisplayValues(
when (walletSnapshot.status) { when (walletSnapshot.status) {
Synchronizer.Status.SYNCING -> { Synchronizer.Status.SYNCING -> {
progress = walletSnapshot.progress progress = walletSnapshot.progress
// we add "so far" to the amount // We add "so far" to the amount
if (fiatCurrencyAmountState != FiatCurrencyConversionRateState.Unavailable) { if (fiatCurrencyAmountState != FiatCurrencyConversionRateState.Unavailable) {
fiatCurrencyAmountText = fiatCurrencyAmountText =
context.getString( context.getString(
R.string.account_status_syncing_amount_suffix, R.string.balances_status_syncing_amount_suffix,
fiatCurrencyAmountText fiatCurrencyAmountText
) )
} }
statusText = statusText = context.getString(R.string.balances_status_syncing)
context.getString(
R.string.account_status_syncing_format,
walletSnapshot.progress.toPercentageWithDecimal()
)
} }
Synchronizer.Status.SYNCED -> { Synchronizer.Status.SYNCED -> {
statusText = statusText =
if (updateAvailable) { if (updateAvailable) {
context.getString(R.string.account_status_update) context.getString(R.string.balances_status_update)
} else { } else {
context.getString(R.string.account_status_up_to_date) context.getString(R.string.balances_status_synced)
} }
} }
Synchronizer.Status.DISCONNECTED -> { Synchronizer.Status.DISCONNECTED -> {
statusText = statusText =
context.getString( context.getString(
R.string.account_status_error, R.string.balances_status_error,
context.getString(R.string.account_status_error_connection) context.getString(R.string.balances_status_error_connection)
) )
} }
Synchronizer.Status.STOPPED -> { Synchronizer.Status.STOPPED -> {
statusText = context.getString(R.string.account_status_stopped) statusText = context.getString(R.string.balances_status_stopped)
} }
} }
// more detailed error message // More detailed error message
walletSnapshot.synchronizerError?.let { walletSnapshot.synchronizerError?.let {
statusText = statusText =
context.getString( context.getString(
R.string.account_status_error, R.string.balances_status_error,
walletSnapshot.synchronizerError.getCauseMessage() walletSnapshot.synchronizerError.getCauseMessage()
?: context.getString(R.string.account_status_error_unknown) ?: context.getString(R.string.balances_status_error_unknown)
) )
} }

View File

@ -1,12 +1,12 @@
package co.electriccoin.zcash.ui.screen.balances.view package co.electriccoin.zcash.ui.screen.balances.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -26,12 +26,17 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.toZecString 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.R
import co.electriccoin.zcash.ui.common.BalanceWidget import co.electriccoin.zcash.ui.common.BalanceWidget
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.model.changePendingBalance import co.electriccoin.zcash.ui.common.model.changePendingBalance
import co.electriccoin.zcash.ui.common.model.spendableBalance import co.electriccoin.zcash.ui.common.model.spendableBalance
@ -39,14 +44,17 @@ import co.electriccoin.zcash.ui.common.model.valuePendingBalance
import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.BodySmall import co.electriccoin.zcash.ui.design.component.BodySmall
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.CircularSmallProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularSmallProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.LinearProgressIndicator
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.StyledBalance import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.model.WalletDisplayValues import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
@Preview("Balances") @Preview("Balances")
@Composable @Composable
@ -54,20 +62,23 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Balances( Balances(
walletSnapshot = WalletSnapshotFixture.new(),
onSettings = {}, onSettings = {},
isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
walletSnapshot = WalletSnapshotFixture.new(),
) )
} }
} }
} }
// TODO [#1127]: Implement Balances screen
// TODO [#1127]: https://github.com/Electric-Coin-Company/zashi-android/issues/1127
@Composable @Composable
fun Balances( fun Balances(
onSettings: () -> Unit,
isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
onSettings: () -> Unit
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
BalancesTopAppBar(onSettings = onSettings) BalancesTopAppBar(onSettings = onSettings)
@ -76,6 +87,9 @@ fun Balances(
CircularScreenProgressIndicator() CircularScreenProgressIndicator()
} else { } else {
BalancesMainContent( BalancesMainContent(
isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
modifier = modifier =
Modifier.padding( Modifier.padding(
@ -110,15 +124,12 @@ private fun BalancesTopAppBar(onSettings: () -> Unit) {
@Composable @Composable
private fun BalancesMainContent( private fun BalancesMainContent(
isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
val walletDisplayValues =
WalletDisplayValues.getNextValues(
LocalContext.current,
walletSnapshot
)
Column( Column(
modifier = modifier =
Modifier Modifier
@ -129,13 +140,11 @@ private fun BalancesMainContent(
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
if (walletDisplayValues.zecAmountText.isNotEmpty()) { BalanceWidget(
BalanceWidget( walletSnapshot = walletSnapshot,
walletSnapshot = walletSnapshot, isReferenceToBalances = false,
isReferenceToBalances = false, onReferenceClick = {}
onReferenceClick = {} )
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
@ -146,27 +155,48 @@ private fun BalancesMainContent(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
BalancesOverview(walletSnapshot) BalancesOverview(
walletSnapshot = walletSnapshot,
isFiatConversionEnabled = isFiatConversionEnabled,
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Column( Column(
modifier = Modifier.fillMaxSize(), modifier =
Modifier
.fillMaxWidth()
.height(166.dp)
.background(color = ZcashTheme.colors.panelBackgroundColor),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Body( Body(
text = stringResource(id = R.string.balances_coming_soon), text = stringResource(id = R.string.balances_coming_soon),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Spacer(modifier = Modifier.weight(1f, true))
SyncStatus(
walletSnapshot = walletSnapshot,
isUpdateAvailable = isUpdateAvailable,
)
if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout()
}
} }
} }
@Composable @Composable
fun BalancesOverview(walletSnapshot: WalletSnapshot) { fun BalancesOverview(
walletSnapshot: WalletSnapshot,
isFiatConversionEnabled: Boolean,
) {
Column { Column {
SpendableBalanceRow(walletSnapshot) SpendableBalanceRow(walletSnapshot)
@ -178,6 +208,36 @@ fun BalancesOverview(walletSnapshot: WalletSnapshot) {
// aka value pending // aka value pending
PendingTransactionsRow(walletSnapshot) PendingTransactionsRow(walletSnapshot)
if (isFiatConversionEnabled) {
val walletDisplayValues =
WalletDisplayValues.getNextValues(
LocalContext.current,
walletSnapshot,
false
)
Column(Modifier.testTag(BalancesTag.FIAT_CONVERSION)) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
when (walletDisplayValues.fiatCurrencyAmountState) {
is FiatCurrencyConversionRateState.Current -> {
BodyWithFiatCurrencySymbol(
amount = walletDisplayValues.fiatCurrencyAmountText
)
}
is FiatCurrencyConversionRateState.Stale -> {
// Note: we should show information about staleness too
BodyWithFiatCurrencySymbol(
amount = walletDisplayValues.fiatCurrencyAmountText
)
}
is FiatCurrencyConversionRateState.Unavailable -> {
Body(text = walletDisplayValues.fiatCurrencyAmountText)
}
}
}
}
} }
} }
@ -289,3 +349,48 @@ fun PendingTransactionsRow(walletSnapshot: WalletSnapshot) {
} }
} }
} }
@Composable
fun SyncStatus(
isUpdateAvailable: Boolean,
walletSnapshot: WalletSnapshot,
) {
val walletDisplayValues =
WalletDisplayValues.getNextValues(
LocalContext.current,
walletSnapshot,
isUpdateAvailable
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
if (walletDisplayValues.statusText.isNotEmpty()) {
BodySmall(
text = walletDisplayValues.statusText,
modifier = Modifier.testTag(BalancesTag.STATUS)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
}
BodySmall(
text =
stringResource(
id = R.string.balances_status_syncing_percentage,
walletSnapshot.progress.toPercentageWithDecimal()
),
textFontWeight = FontWeight.Black
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
LinearProgressIndicator(
progress = walletSnapshot.progress.decimal,
modifier =
Modifier.padding(
horizontal = ZcashTheme.dimens.spacingXlarge
)
)
}
}

View File

@ -68,7 +68,6 @@ import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.model.WalletDisplayValues
import co.electriccoin.zcash.ui.screen.send.SendTag import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
import co.electriccoin.zcash.ui.screen.send.ext.abbreviated import co.electriccoin.zcash.ui.screen.send.ext.abbreviated
@ -361,21 +360,13 @@ private fun SendForm(
.then(modifier), .then(modifier),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
val walletDisplayValues =
WalletDisplayValues.getNextValues(
context = LocalContext.current,
walletSnapshot = walletSnapshot
)
Spacer(modifier = Modifier.height(dimens.spacingDefault)) Spacer(modifier = Modifier.height(dimens.spacingDefault))
if (walletDisplayValues.zecAmountText.isNotEmpty()) { BalanceWidget(
BalanceWidget( walletSnapshot = walletSnapshot,
walletSnapshot = walletSnapshot, isReferenceToBalances = true,
isReferenceToBalances = true, onReferenceClick = goBalances
onReferenceClick = goBalances )
)
}
Spacer(modifier = Modifier.height(dimens.spacingXlarge)) Spacer(modifier = Modifier.height(dimens.spacingXlarge))

View File

@ -1,21 +1,3 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="account_button_history">Transaction History</string> <string name="account_button_history">Transaction History</string>
<string name="account_status_syncing_format" formatted="true">Syncing - <xliff:g id="synced_percent" example="50.25">
%1$s</xliff:g>%%</string> <!-- double %% for escaping -->
<string name="account_status_syncing_catchup">Syncing</string>
<string name="account_status_syncing_amount_suffix" formatted="true"><xliff:g id="amount_prefix" example="123$">%1$s</xliff:g> so far</string>
<string name="account_status_syncing_additional_information">We will show you funds as we discover them.</string>
<string name="account_status_up_to_date">Up-to-date</string>
<string name="account_status_sending_format" formatted="true">Sending <xliff:g id="sending_amount" example=".023">%1$s</xliff:g></string>
<string name="account_status_receiving_format" formatted="true">Receiving <xliff:g id="receiving_amount" example=".023">%1$s</xliff:g> ZEC</string>
<string name="account_status_shielding_format" formatted="true">Shielding <xliff:g id="shielding_amount" example=".023">%1$s</xliff:g> ZEC</string>
<string name="account_status_update">Please Update</string>
<string name="account_status_error" formatted="true">Error: <xliff:g id="error_type" example="Lost connection">%1$s</xliff:g></string>
<string name="account_status_error_connection">Disconnected</string>
<string name="account_status_error_unknown">Unknown cause</string>
<string name="account_status_stopped">Synchronizer stopped</string>
<string name="account_status_updating_blockheight">Updating blockheight</string>
<string name="account_status_fiat_currency_price_out_of_date" formatted="true"><xliff:g id="fiat_currency" example="USD">%1$s</xliff:g> price out-of-date</string>
<string name="account_status_spendable" formatted="true">Fully spendable in <xliff:g id="spendable_time" example="2 minutes">%1$s</xliff:g></string>
</resources> </resources>

View File

@ -3,6 +3,22 @@
<string name="balances_shielded_spendable">Shielded zec (spendable)</string> <string name="balances_shielded_spendable">Shielded zec (spendable)</string>
<string name="balances_change_pending">Change pending</string> <string name="balances_change_pending">Change pending</string>
<string name="balances_pending_transactions">Pending transactions</string> <string name="balances_pending_transactions">Pending transactions</string>
<string name="balances_coming_soon">Coming soon:\n\nTransparent <string name="balances_coming_soon">Transparent funds shielding\n(coming soon)</string>
funds shielding,\nBlock synchronization indicator</string>
<string name="balances_status_syncing" formatted="true">Syncing…</string>
<string name="balances_status_syncing_amount_suffix" formatted="true"><xliff:g id="amount_prefix" example="123$">%1$s</xliff:g> so far</string>
<string name="balances_status_syncing_percentage" formatted="true"><xliff:g id="synced_percent" example="50.25">
%1$s</xliff:g>%%</string> <!-- double %% for escaping -->
<string name="balances_status_synced">Synced</string>
<string name="balances_status_sending_format" formatted="true">Sending <xliff:g id="sending_amount" example=".023">%1$s</xliff:g></string>
<string name="balances_status_receiving_format" formatted="true">Receiving <xliff:g id="receiving_amount" example=".023">%1$s</xliff:g> ZEC</string>
<string name="balances_status_shielding_format" formatted="true">Shielding <xliff:g id="shielding_amount" example=".023">%1$s</xliff:g> ZEC</string>
<string name="balances_status_update">Please Update via Play Store</string>
<string name="balances_status_error" formatted="true">Error: <xliff:g id="error_type" example="Lost connection">%1$s</xliff:g></string>
<string name="balances_status_error_connection">Disconnected</string>
<string name="balances_status_error_unknown">Unknown cause</string>
<string name="balances_status_stopped">Synchronizer stopped</string>
<string name="balances_status_updating_blockheight">Updating blockheight</string>
<string name="balances_status_fiat_currency_price_out_of_date" formatted="true"><xliff:g id="fiat_currency" example="USD">%1$s</xliff:g> price out-of-date</string>
<string name="balances_status_spendable" formatted="true">Fully spendable in <xliff:g id="spendable_time" example="2 minutes">%1$s</xliff:g></string>
</resources> </resources>

View File

@ -384,7 +384,7 @@ private fun accountScreenshots(
composeTestRule.activity.walletViewModel.walletSnapshot.value != null composeTestRule.activity.walletViewModel.walletSnapshot.value != null
} }
composeTestRule.onNodeWithTag(AccountTag.STATUS_VIEWS).also { composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Account 1") ScreenshotTest.takeScreenshot(tag, "Account 1")
} }