[#1079] Home screen bottom nav bar

* Changelog update

* [#1118] Account screen Balance Text line break

- Closes #1118

* [#1117] Reusable Loading screen indicator

- Closes #1117

* [#1116] Balances screen structure

- Closes #1116

* [#1079] Bottom bar - tabs navigation

- Closes #1079

* [#1079] Rework UI tests

* [#1079] File follow-ups

* [#1079] Fix static lint tools warnings

* [#1079] Improve Home sub-screens indexing

* [#1079] Reorg navigation into Home components

* [#1079] Align with Figma design

* [#1079] Update screenshot and UI tests
This commit is contained in:
Honza Rychnovský 2023-12-18 14:31:25 +01:00 committed by GitHub
parent 94616c4a7a
commit 4cf608b733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1063 additions and 802 deletions

View File

@ -9,6 +9,9 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased] ## [Unreleased]
### Changed
- Home screen navigation switched from the Side menu to the Bottom Navigation Tabs menu
## [0.2.0 (505)] - 2023-12-11 ## [0.2.0 (505)] - 2023-12-11
### Added ### Added

View File

@ -3,4 +3,5 @@ package co.electriccoin.zcash.ui.design.component
object CommonTag { object CommonTag {
const val CHIP_LAYOUT = "chip_layout" const val CHIP_LAYOUT = "chip_layout"
const val CHIP = "chip" const val CHIP = "chip"
const val TOP_APP_BAR = "top_app_bar"
} }

View File

@ -0,0 +1,36 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview
@Composable
private fun CircularScreenProgressIndicatorComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
CircularScreenProgressIndicator()
}
}
}
@Composable
fun CircularScreenProgressIndicator(modifier: Modifier = Modifier) {
Box(
modifier =
Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.width(ZcashTheme.dimens.circularScreenProgressWidth)
)
}
}

View File

@ -1,5 +1,9 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -13,6 +17,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
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.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -178,6 +183,7 @@ fun Reference(
* @param amount of ZECs to be displayed * @param amount of ZECs to be displayed
* @param modifier to modify the Text UI element as needed * @param modifier to modify the Text UI element as needed
*/ */
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun HeaderWithZecIcon( fun HeaderWithZecIcon(
amount: String, amount: String,
@ -187,7 +193,8 @@ fun HeaderWithZecIcon(
text = stringResource(R.string.amount_with_zec_currency_symbol, amount), text = stringResource(R.string.amount_with_zec_currency_symbol, amount),
style = ZcashTheme.extendedTypography.zecBalance, style = ZcashTheme.extendedTypography.zecBalance,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
modifier = modifier maxLines = 1,
modifier = Modifier.basicMarquee().then(modifier)
) )
} }
@ -203,3 +210,25 @@ fun BodyWithFiatCurrencySymbol(
modifier = modifier modifier = modifier
) )
} }
@Composable
fun NavigationTabText(
text: String,
selected: Boolean,
modifier: Modifier = Modifier
) {
Text(
text = text,
style = ZcashTheme.extendedTypography.textNavTab,
fontWeight =
if (selected) {
FontWeight.Black
} else {
FontWeight.Normal
},
maxLines = 1,
overflow = TextOverflow.Visible,
color = ZcashTheme.colors.tabTextColor,
modifier = modifier
)
}

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
@ -30,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -111,6 +113,31 @@ private fun TopAppBarHamburgerMenuComposablePreview() {
} }
} }
@Preview
@Composable
private fun TopAppBarHamburgerPlusActionComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SmallTopAppBar(
titleText = "Screen E",
hamburgerMenuActions = {
TopBarHamburgerMenuExample(
actionCallback = {}
)
},
regularActions = {
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = "Content description text"
)
}
}
)
}
}
}
@Composable @Composable
private fun TopBarHamburgerMenuExample( private fun TopBarHamburgerMenuExample(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -236,7 +263,13 @@ fun SmallTopAppBar(
} }
} }
}, },
actions = hamburgerMenuActions ?: regularActions ?: {}, actions = {
modifier = modifier regularActions?.invoke(this)
hamburgerMenuActions?.invoke(this)
},
modifier =
Modifier
.testTag(CommonTag.TOP_APP_BAR)
.then(modifier)
) )
} }

View File

@ -28,6 +28,8 @@ data class Dimens(
// Chip // Chip
val chipShadowElevation: Dp, val chipShadowElevation: Dp,
val chipStroke: Dp, val chipStroke: Dp,
// Progress
val circularScreenProgressWidth: Dp,
// TopAppBar: // TopAppBar:
val topAppBarZcashLogoHeight: Dp, val topAppBarZcashLogoHeight: Dp,
val topAppBarActionRippleCorner: Dp, val topAppBarActionRippleCorner: Dp,
@ -59,6 +61,7 @@ private val defaultDimens =
buttonHeight = 50.dp, buttonHeight = 50.dp,
chipShadowElevation = 4.dp, chipShadowElevation = 4.dp,
chipStroke = 0.5.dp, chipStroke = 0.5.dp,
circularScreenProgressWidth = 48.dp,
topAppBarZcashLogoHeight = 24.dp, topAppBarZcashLogoHeight = 24.dp,
topAppBarActionRippleCorner = 28.dp, topAppBarActionRippleCorner = 28.dp,
textFieldDefaultHeight = 215.dp, textFieldDefaultHeight = 215.dp,

View File

@ -35,6 +35,9 @@ data class ExtendedColors(
val screenTitleColor: Color, val screenTitleColor: Color,
val aboutTextColor: Color, val aboutTextColor: Color,
val welcomeAnimationColor: Color, val welcomeAnimationColor: Color,
val complementaryColor: Color,
val dividerColor: Color,
val tabTextColor: Color,
) { ) {
@Composable @Composable
fun surfaceGradient() = fun surfaceGradient() =

View File

@ -11,6 +11,9 @@ import co.electriccoin.zcash.ui.design.theme.ExtendedColors
// TODO [#998]: Check and enhance screen dark mode // TODO [#998]: Check and enhance screen dark mode
// TODO [#998]: https://github.com/Electric-Coin-Company/zashi-android/issues/998 // TODO [#998]: https://github.com/Electric-Coin-Company/zashi-android/issues/998
// TODO [#1091]: Clear unused color resources
// TODO [#1091]: https://github.com/Electric-Coin-Company/zashi-android/issues/1091
internal object Dark { internal object Dark {
val backgroundStart = Color(0xFF000000) val backgroundStart = Color(0xFF000000)
val backgroundEnd = Color(0xFF000000) val backgroundEnd = Color(0xFF000000)
@ -65,12 +68,13 @@ internal object Dark {
val buttonShadowColor = Color(0xFFFFFFFF) val buttonShadowColor = Color(0xFFFFFFFF)
// to be added later // Proper values will be added later, see #998
val aboutTextColor = Color.Unspecified val aboutTextColor = Color.Unspecified
val screenTitleColor = Color(0xFF040404) val screenTitleColor = Color(0xFF040404)
val welcomeAnimationColor = Color(0xFF231F20) val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728)
val dividerColor = Color(0xFFDDDDDD)
val tabTextColor = Color(0xFF040404)
} }
internal object Light { internal object Light {
@ -131,10 +135,11 @@ internal object Light {
val buttonShadowColor = Color(0xFF000000) val buttonShadowColor = Color(0xFF000000)
val screenTitleColor = Color(0xFF040404) val screenTitleColor = Color(0xFF040404)
val aboutTextColor = Color(0xFF4E4E4E) val aboutTextColor = Color(0xFF4E4E4E)
val welcomeAnimationColor = Color(0xFF231F20) val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728)
val dividerColor = Color(0xFFDDDDDD)
val tabTextColor = Color(0xFF040404)
} }
internal val DarkColorPalette = internal val DarkColorPalette =
@ -189,7 +194,10 @@ internal val DarkExtendedColorPalette =
buttonShadowColor = Dark.buttonShadowColor, buttonShadowColor = Dark.buttonShadowColor,
screenTitleColor = Dark.screenTitleColor, screenTitleColor = Dark.screenTitleColor,
aboutTextColor = Dark.aboutTextColor, aboutTextColor = Dark.aboutTextColor,
welcomeAnimationColor = Dark.welcomeAnimationColor welcomeAnimationColor = Dark.welcomeAnimationColor,
complementaryColor = Dark.complementaryColor,
dividerColor = Dark.dividerColor,
tabTextColor = Dark.tabTextColor,
) )
internal val LightExtendedColorPalette = internal val LightExtendedColorPalette =
@ -220,7 +228,10 @@ internal val LightExtendedColorPalette =
buttonShadowColor = Light.buttonShadowColor, buttonShadowColor = Light.buttonShadowColor,
screenTitleColor = Light.screenTitleColor, screenTitleColor = Light.screenTitleColor,
aboutTextColor = Light.aboutTextColor, aboutTextColor = Light.aboutTextColor,
welcomeAnimationColor = Light.welcomeAnimationColor welcomeAnimationColor = Light.welcomeAnimationColor,
complementaryColor = Light.complementaryColor,
dividerColor = Light.dividerColor,
tabTextColor = Dark.tabTextColor,
) )
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
@ -254,5 +265,8 @@ internal val LocalExtendedColors =
screenTitleColor = Color.Unspecified, screenTitleColor = Color.Unspecified,
aboutTextColor = Color.Unspecified, aboutTextColor = Color.Unspecified,
welcomeAnimationColor = Color.Unspecified, welcomeAnimationColor = Color.Unspecified,
complementaryColor = Color.Unspecified,
dividerColor = Color.Unspecified,
tabTextColor = Color.Unspecified
) )
} }

View File

@ -9,7 +9,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.googlefonts.Font import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.design.R import co.electriccoin.zcash.ui.design.R
@ -129,6 +128,12 @@ internal val SecondaryTypography =
fontFamily = ArchivoFontFamily, fontFamily = ArchivoFontFamily,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp fontSize = 16.sp
),
labelMedium =
TextStyle(
fontFamily = ArchivoFontFamily,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
) )
) )
@ -140,7 +145,6 @@ data class Typography(
@Immutable @Immutable
data class ExtendedTypography( data class ExtendedTypography(
val chipIndex: TextStyle,
val listItem: TextStyle, val listItem: TextStyle,
val zecBalance: TextStyle, val zecBalance: TextStyle,
val aboutText: TextStyle, val aboutText: TextStyle,
@ -150,6 +154,7 @@ data class ExtendedTypography(
val textFieldHint: TextStyle, val textFieldHint: TextStyle,
val textFieldValue: TextStyle, val textFieldValue: TextStyle,
val textFieldBirthday: TextStyle, val textFieldBirthday: TextStyle,
val textNavTab: TextStyle,
) )
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
@ -165,12 +170,6 @@ val LocalTypographies =
val LocalExtendedTypography = val LocalExtendedTypography =
staticCompositionLocalOf { staticCompositionLocalOf {
ExtendedTypography( ExtendedTypography(
chipIndex =
PrimaryTypography.bodyLarge.copy(
fontSize = 10.sp,
baselineShift = BaselineShift.Superscript,
fontWeight = FontWeight.Bold
),
listItem = listItem =
PrimaryTypography.bodyLarge.copy( PrimaryTypography.bodyLarge.copy(
fontSize = 24.sp fontSize = 24.sp
@ -209,5 +208,9 @@ val LocalExtendedTypography =
fontSize = 17.sp, fontSize = 17.sp,
), ),
textFieldBirthday = SecondaryTypography.headlineMedium.copy(), textFieldBirthday = SecondaryTypography.headlineMedium.copy(),
textNavTab =
SecondaryTypography.labelSmall.copy(
fontSize = 13.sp
),
) )
} }

View File

@ -31,11 +31,12 @@ android {
setOf( setOf(
"src/main/res/ui/about", "src/main/res/ui/about",
"src/main/res/ui/account", "src/main/res/ui/account",
"src/main/res/ui/new_wallet_recovery", "src/main/res/ui/balances",
"src/main/res/ui/common", "src/main/res/ui/common",
"src/main/res/ui/export_data", "src/main/res/ui/export_data",
"src/main/res/ui/history", "src/main/res/ui/history",
"src/main/res/ui/home", "src/main/res/ui/home",
"src/main/res/ui/new_wallet_recovery",
"src/main/res/ui/onboarding", "src/main/res/ui/onboarding",
"src/main/res/ui/receive", "src/main/res/ui/receive",
"src/main/res/ui/request", "src/main/res/ui/request",

View File

@ -0,0 +1,69 @@
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.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.view.Account
import java.util.concurrent.atomic.AtomicInteger
class AccountTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot,
private val isShowFiatConversion: Boolean
) {
private val onSettingsCount = AtomicInteger(0)
private val onReceiveCount = AtomicInteger(0)
private val onSendCount = AtomicInteger(0)
private val onHistoryCount = AtomicInteger(0)
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onSettingsCount.get()
}
fun getOnReceiveCount(): Int {
composeTestRule.waitForIdle()
return onReceiveCount.get()
}
fun getOnSendCount(): Int {
composeTestRule.waitForIdle()
return onSendCount.get()
}
fun getOnHistoryCount(): Int {
composeTestRule.waitForIdle()
return onHistoryCount.get()
}
fun getWalletSnapshot(): WalletSnapshot {
composeTestRule.waitForIdle()
return walletSnapshot
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
Account(
walletSnapshot,
isUpdateAvailable = false,
isKeepScreenOnDuringSync = false,
isFiatConversionEnabled = isShowFiatConversion,
goSettings = {
onSettingsCount.incrementAndGet()
},
goHistory = {
onHistoryCount.incrementAndGet()
},
)
}
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
DefaultContent()
}
}
}
}

View File

@ -0,0 +1,68 @@
package co.electriccoin.zcash.ui.screen.account.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.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
class AccountViewIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
AccountTestSetup(
composeTestRule,
walletSnapshot,
isShowFiatConversion = false
)
// 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(AccountTag.SINGLE_LINE_TEXT).also {
it.assertIsDisplayed()
it.assertWidthIsAtLeast(1.dp)
}
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.home.model package co.electriccoin.zcash.ui.screen.account.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.home_status_syncing_catchup))) assertTrue(values.statusText.startsWith(getStringResource(R.string.account_status_syncing_catchup)))
// 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,102 @@
package co.electriccoin.zcash.ui.screen.account.view
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
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.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
class AccountViewTest : 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()
}
composeTestRule.onNodeWithTag(AccountTag.STATUS_VIEWS).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithTag(AccountTag.FIAT_CONVERSION).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithText(getStringResource(R.string.account_button_history), ignoreCase = true).also {
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun hide_fiat_conversion() {
newTestSetup(isShowFiatConversion = false)
composeTestRule.onNodeWithTag(AccountTag.FIAT_CONVERSION).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun click_history_button() {
val testSetup = newTestSetup()
Assert.assertEquals(0, testSetup.getOnHistoryCount())
composeTestRule.clickHistory()
Assert.assertEquals(1, testSetup.getOnHistoryCount())
}
@Test
@MediumTest
fun hamburger_settings_test() {
val testSetup = newTestSetup()
Assert.assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.clickSettingsTopAppBarMenu()
Assert.assertEquals(1, testSetup.getOnSettingsCount())
}
private fun newTestSetup(
isShowFiatConversion: Boolean = true,
walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new()
) = AccountTestSetup(
composeTestRule,
walletSnapshot = walletSnapshot,
isShowFiatConversion = isShowFiatConversion
).apply {
setDefaultContent()
}
}
private fun ComposeContentTestRule.clickHistory() {
onNodeWithText(getStringResource(R.string.home_button_history), ignoreCase = true).also {
it.performScrollTo()
it.performClick()
}
}

View File

@ -1,30 +1,19 @@
package co.electriccoin.zcash.ui.screen.home package co.electriccoin.zcash.ui.screen.home
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.home.view.Home
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
class HomeTestSetup( class HomeTestSetup(
private val composeTestRule: ComposeContentTestRule, private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot,
private val isShowFiatConversion: Boolean
) { ) {
private val onSettingsCount = AtomicInteger(0) private val onAccountsCount = AtomicInteger(0)
private val onReceiveCount = AtomicInteger(0)
private val onSendCount = AtomicInteger(0) private val onSendCount = AtomicInteger(0)
private val onHistoryCount = AtomicInteger(0) private val onReceiveCount = AtomicInteger(0)
private val onBalancesCount = AtomicInteger(0)
fun getOnSettingsCount(): Int { fun getOnAccountCount(): Int {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return onSettingsCount.get() return onAccountsCount.get()
}
fun getOnReceiveCount(): Int {
composeTestRule.waitForIdle()
return onReceiveCount.get()
} }
fun getOnSendCount(): Int { fun getOnSendCount(): Int {
@ -32,37 +21,27 @@ class HomeTestSetup(
return onSendCount.get() return onSendCount.get()
} }
fun getOnHistoryCount(): Int { fun getOnReceiveCount(): Int {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return onHistoryCount.get() return onReceiveCount.get()
} }
fun getWalletSnapshot(): WalletSnapshot { fun getOnBalancesCount(): Int {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return walletSnapshot return onBalancesCount.get()
} }
// TODO [#1125]: Home screen navigation: Add integration test
// TODO [#1125]: https://github.com/Electric-Coin-Company/zashi-android/issues/1125
// TODO [#1126]: Home screen view: Add view test
// TODO [#1126]: https://github.com/Electric-Coin-Company/zashi-android/issues/1126
/*
@Composable @Composable
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
fun DefaultContent() { fun DefaultContent() {
Home( Home()
walletSnapshot,
isUpdateAvailable = false,
isKeepScreenOnDuringSync = false,
isFiatConversionEnabled = isShowFiatConversion,
goSettings = {
onSettingsCount.incrementAndGet()
},
goReceive = {
onReceiveCount.incrementAndGet()
},
goSend = {
onSendCount.incrementAndGet()
},
goHistory = {
onHistoryCount.incrementAndGet()
},
)
} }
fun setDefaultContent() { fun setDefaultContent() {
@ -72,4 +51,5 @@ class HomeTestSetup(
} }
} }
} }
*/
} }

View File

@ -1,28 +1,17 @@
package co.electriccoin.zcash.ui.screen.home.integration package co.electriccoin.zcash.ui.screen.home.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.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.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.home.HomeTag
import co.electriccoin.zcash.ui.screen.home.HomeTestSetup
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test
class HomeViewIntegrationTest : UiTestPrerequisites() { class HomeViewIntegrationTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
// TODO [#1125]: Home screen navigation: Add integration test
// TODO [#1125]: https://github.com/Electric-Coin-Company/zashi-android/issues/1125
/*
private fun newTestSetup(walletSnapshot: WalletSnapshot) = private fun newTestSetup(walletSnapshot: WalletSnapshot) =
HomeTestSetup( HomeTestSetup(
composeTestRule, composeTestRule,
@ -30,7 +19,6 @@ class HomeViewIntegrationTest : UiTestPrerequisites() {
isShowFiatConversion = false isShowFiatConversion = false
) )
// This is just basic sanity check that we still have UI set up as expected after the state restore
@Test @Test
@MediumTest @MediumTest
fun wallet_snapshot_restoration() { fun wallet_snapshot_restoration() {
@ -60,9 +48,10 @@ class HomeViewIntegrationTest : UiTestPrerequisites() {
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress) assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal) assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
composeTestRule.onNodeWithTag(HomeTag.SINGLE_LINE_TEXT).also { composeTestRule.onNodeWithTag(AccountTag.SINGLE_LINE_TEXT).also {
it.assertIsDisplayed() it.assertIsDisplayed()
it.assertWidthIsAtLeast(1.dp) it.assertWidthIsAtLeast(1.dp)
} }
} }
*/
} }

View File

@ -1,147 +1,40 @@
@file:Suppress("UnusedPrivateMember")
package co.electriccoin.zcash.ui.screen.home.view package co.electriccoin.zcash.ui.screen.home.view
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
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.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.home.HomeTag
import co.electriccoin.zcash.ui.screen.home.HomeTestSetup
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class HomeViewTest : UiTestPrerequisites() { class HomeViewTest {
@get:Rule // TODO [#1126]: Home screen view: Add view test
val composeTestRule = createComposeRule() // TODO [#1126]: https://github.com/Electric-Coin-Company/zashi-android/issues/1126
@Test
@MediumTest
fun check_all_elementary_ui_elements_displayed() {
newTestSetup()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_menu_content_description)).also {
it.assertIsDisplayed()
} }
composeTestRule.onNodeWithTag(HomeTag.STATUS_VIEWS).also { private fun ComposeContentTestRule.clickAccount() {
it.assertIsDisplayed() onNodeWithText(getStringResource(R.string.home_tab_account), ignoreCase = true).also {
}
composeTestRule.onNodeWithTag(HomeTag.FIAT_CONVERSION).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_send), ignoreCase = true).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_receive), ignoreCase = true).also {
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun hide_fiat_conversion() {
newTestSetup(isShowFiatConversion = false)
composeTestRule.onNodeWithTag(HomeTag.FIAT_CONVERSION).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun click_receive_button() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.clickReceive()
assertEquals(1, testSetup.getOnReceiveCount())
}
@Test
@MediumTest
fun click_send_button() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnSendCount())
composeTestRule.clickSend()
assertEquals(1, testSetup.getOnSendCount())
}
@Test
@MediumTest
fun click_history_button() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnHistoryCount())
composeTestRule.clickHistory()
assertEquals(1, testSetup.getOnHistoryCount())
}
@Test
@MediumTest
fun hamburger_settings() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.openSettings()
assertEquals(1, testSetup.getOnSettingsCount())
}
private fun newTestSetup(
isShowFiatConversion: Boolean = true,
walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new()
) = HomeTestSetup(
composeTestRule,
walletSnapshot = walletSnapshot,
isShowFiatConversion = isShowFiatConversion
).apply {
setDefaultContent()
}
}
private fun ComposeContentTestRule.openSettings() {
onNodeWithContentDescription(getStringResource(R.string.home_menu_content_description)).also {
it.performClick()
}
}
private fun ComposeContentTestRule.clickReceive() {
onNodeWithText(getStringResource(R.string.home_button_receive), ignoreCase = true).also {
it.performClick() it.performClick()
} }
} }
private fun ComposeContentTestRule.clickSend() { private fun ComposeContentTestRule.clickSend() {
onNodeWithText(getStringResource(R.string.home_button_send), ignoreCase = true).also { onNodeWithText(getStringResource(R.string.home_tab_send), ignoreCase = true).also {
it.performScrollTo() it.performScrollTo()
it.performClick() it.performClick()
} }
} }
private fun ComposeContentTestRule.clickHistory() { private fun ComposeContentTestRule.clickReceive() {
onNodeWithText(getStringResource(R.string.home_button_history), ignoreCase = true).also { onNodeWithText(getStringResource(R.string.home_tab_receive), ignoreCase = true).also {
it.performScrollTo() it.performClick()
}
}
private fun ComposeContentTestRule.clickBalances() {
onNodeWithText(getStringResource(R.string.home_tab_balances), ignoreCase = true).also {
it.performClick() it.performClick()
} }
} }

View File

@ -38,19 +38,19 @@ class ReceiveViewTest {
@Test @Test
@MediumTest @MediumTest
fun back() = fun click_settings_test() =
runTest { runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified()) val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnBackCount()) assertEquals(0, testSetup.getOnSettingsCount())
composeTestRule.onNodeWithContentDescription( composeTestRule.onNodeWithContentDescription(
getStringResource(R.string.receive_back_content_description) getStringResource(R.string.settings_menu_content_description)
).also { ).also {
it.performClick() it.performClick()
} }
assertEquals(1, testSetup.getOnBackCount()) assertEquals(1, testSetup.getOnSettingsCount())
} }
@Test @Test

View File

@ -19,7 +19,7 @@ class ReceiveViewTestSetup(
private val composeTestRule: ComposeContentTestRule, private val composeTestRule: ComposeContentTestRule,
walletAddress: WalletAddress walletAddress: WalletAddress
) { ) {
private val onBackCount = AtomicInteger(0) private val onSettingsCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0) private val onAddressDetailsCount = AtomicInteger(0)
private val screenBrightness = ScreenBrightness() private val screenBrightness = ScreenBrightness()
private val screenTimeout = ScreenTimeout() private val screenTimeout = ScreenTimeout()
@ -34,9 +34,9 @@ class ReceiveViewTestSetup(
return onAdjustBrightness.get() return onAdjustBrightness.get()
} }
fun getOnBackCount(): Int { fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return onBackCount.get() return onSettingsCount.get()
} }
fun getOnAddressDetailsCount(): Int { fun getOnAddressDetailsCount(): Int {
@ -54,8 +54,8 @@ class ReceiveViewTestSetup(
ZcashTheme { ZcashTheme {
Receive( Receive(
walletAddress, walletAddress,
onBack = { onSettings = {
onBackCount.getAndIncrement() onSettingsCount.getAndIncrement()
}, },
onAddressDetails = { onAddressDetails = {
onAddressDetailsCount.getAndIncrement() onAddressDetailsCount.getAndIncrement()

View File

@ -4,6 +4,7 @@ import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextClearance
@ -20,6 +21,12 @@ internal fun ComposeContentTestRule.clickBack() {
} }
} }
internal fun ComposeContentTestRule.clickSettingsTopAppBarMenu() {
onNodeWithContentDescription(getStringResource(R.string.settings_menu_content_description)).also {
it.performClick()
}
}
internal fun ComposeContentTestRule.clickScanner() { internal fun ComposeContentTestRule.clickScanner() {
onNodeWithContentDescription(getStringResource(R.string.send_scan_content_description)).also { onNodeWithContentDescription(getStringResource(R.string.send_scan_content_description)).also {
it.performClick() it.performClick()
@ -70,25 +77,25 @@ internal fun ComposeContentTestRule.setMemo(memo: String) {
} }
internal fun ComposeContentTestRule.clickCreateAndSend() { internal fun ComposeContentTestRule.clickCreateAndSend() {
onNodeWithText(getStringResource(R.string.send_create), ignoreCase = true).also { onNodeWithTag(SendTag.SEND_FORM_BUTTON).also {
it.performClick() it.performClick()
} }
} }
internal fun ComposeContentTestRule.clickConfirmation() { internal fun ComposeContentTestRule.clickConfirmation() {
onNodeWithText(getStringResource(R.string.send_confirmation_button), ignoreCase = true).also { onNodeWithTag(SendTag.SEND_CONFIRMATION_BUTTON).also {
it.performClick() it.performClick()
} }
} }
internal fun ComposeContentTestRule.assertOnForm() { internal fun ComposeContentTestRule.assertOnForm() {
onNodeWithText(getStringResource(R.string.send_create), ignoreCase = true).also { onNodeWithTag(SendTag.SEND_FORM_BUTTON).also {
it.assertExists() it.assertExists()
} }
} }
internal fun ComposeContentTestRule.assertOnConfirmation() { internal fun ComposeContentTestRule.assertOnConfirmation() {
onNodeWithText(getStringResource(R.string.send_confirmation_button), ignoreCase = true).also { onNodeWithTag(SendTag.SEND_CONFIRMATION_BUTTON).also {
it.assertExists() it.assertExists()
} }
} }
@ -112,13 +119,13 @@ internal fun ComposeContentTestRule.assertOnSendFailure() {
} }
internal fun ComposeContentTestRule.assertSendEnabled() { internal fun ComposeContentTestRule.assertSendEnabled() {
onNodeWithText(getStringResource(R.string.send_create), ignoreCase = true).also { onNodeWithTag(SendTag.SEND_FORM_BUTTON).also {
it.assertIsEnabled() it.assertIsEnabled()
} }
} }
internal fun ComposeContentTestRule.assertSendDisabled() { internal fun ComposeContentTestRule.assertSendDisabled() {
onNodeWithText(getStringResource(R.string.send_create), ignoreCase = true).also { onNodeWithTag(SendTag.SEND_FORM_BUTTON).also {
it.assertIsNotEnabled() it.assertIsNotEnabled()
} }
} }

View File

@ -24,6 +24,7 @@ class SendViewTestSetup(
private val hasCameraFeature: Boolean private val hasCameraFeature: Boolean
) { ) {
private val onBackCount = AtomicInteger(0) private val onBackCount = AtomicInteger(0)
private val onSettingsCount = AtomicInteger(0)
private val onCreateCount = AtomicInteger(0) private val onCreateCount = AtomicInteger(0)
private val onScannerCount = AtomicInteger(0) private val onScannerCount = AtomicInteger(0)
val mutableActionExecuted = MutableStateFlow(false) val mutableActionExecuted = MutableStateFlow(false)
@ -39,6 +40,11 @@ class SendViewTestSetup(
return onBackCount.get() return onBackCount.get()
} }
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onSettingsCount.get()
}
fun getOnCreateCount(): Int { fun getOnCreateCount(): Int {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return onCreateCount.get() return onCreateCount.get()
@ -96,6 +102,7 @@ class SendViewTestSetup(
zecSend = zecSend, zecSend = zecSend,
onZecSendChange = setZecSend, onZecSendChange = setZecSend,
onBack = onBackAction, onBack = onBackAction,
onSettings = { onSettingsCount.incrementAndGet() },
onCreateAndSend = { onCreateAndSend = {
onCreateCount.incrementAndGet() onCreateCount.incrementAndGet()
lastZecSend = it lastZecSend = it

View File

@ -57,7 +57,8 @@ class SendViewIntegrationTest {
spendingKey = spendingKey, spendingKey = spendingKey,
goToQrScanner = {}, goToQrScanner = {},
goBack = {}, goBack = {},
hasCameraFeature = true hasCameraFeature = true,
goSettings = {}
) )
} }

View File

@ -4,6 +4,7 @@ import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
@ -18,6 +19,8 @@ import cash.z.ecc.sdk.fixture.ZecSendFixture
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.SendTag.SEND_FAILED_BUTTON
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
import co.electriccoin.zcash.ui.screen.send.assertOnForm import co.electriccoin.zcash.ui.screen.send.assertOnForm
@ -30,6 +33,7 @@ import co.electriccoin.zcash.ui.screen.send.clickBack
import co.electriccoin.zcash.ui.screen.send.clickConfirmation import co.electriccoin.zcash.ui.screen.send.clickConfirmation
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
import co.electriccoin.zcash.ui.screen.send.clickScanner import co.electriccoin.zcash.ui.screen.send.clickScanner
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendStage import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.setAmount import co.electriccoin.zcash.ui.screen.send.setAmount
@ -38,7 +42,6 @@ import co.electriccoin.zcash.ui.screen.send.setValidAddress
import co.electriccoin.zcash.ui.screen.send.setValidAmount import co.electriccoin.zcash.ui.screen.send.setValidAmount
import co.electriccoin.zcash.ui.screen.send.setValidMemo import co.electriccoin.zcash.ui.screen.send.setValidMemo
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -74,7 +77,7 @@ class SendViewTest : UiTestPrerequisites() {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val testSetup = newTestSetup() val testSetup = newTestSetup()
composeTestRule.onNodeWithText(getStringResource(R.string.send_create), ignoreCase = true).also { composeTestRule.onNodeWithTag(SendTag.SEND_FORM_BUTTON).also {
it.assertExists() it.assertExists()
it.assertIsNotEnabled() it.assertIsNotEnabled()
} }
@ -82,7 +85,6 @@ class SendViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
@OptIn(ExperimentalCoroutinesApi::class)
fun create_request_no_memo() = fun create_request_no_memo() =
runTest { runTest {
val testSetup = newTestSetup() val testSetup = newTestSetup()
@ -117,7 +119,6 @@ class SendViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
@OptIn(ExperimentalCoroutinesApi::class)
fun create_request_with_memo() = fun create_request_with_memo() =
runTest { runTest {
val testSetup = newTestSetup() val testSetup = newTestSetup()
@ -154,7 +155,6 @@ class SendViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
@OptIn(ExperimentalCoroutinesApi::class)
fun check_regex_functionality_valid_inputs() = fun check_regex_functionality_valid_inputs() =
runTest { runTest {
val testSetup = newTestSetup() val testSetup = newTestSetup()
@ -213,7 +213,6 @@ class SendViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
@OptIn(ExperimentalCoroutinesApi::class)
fun check_regex_functionality_invalid_inputs() = fun check_regex_functionality_invalid_inputs() =
runTest { runTest {
val testSetup = newTestSetup() val testSetup = newTestSetup()
@ -252,7 +251,6 @@ class SendViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
@OptIn(ExperimentalCoroutinesApi::class)
fun max_memo_length() = fun max_memo_length() =
runTest { runTest {
val testSetup = newTestSetup() val testSetup = newTestSetup()
@ -294,14 +292,14 @@ class SendViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
fun back_on_form() { fun on_settings_click_test() {
val testSetup = newTestSetup() val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount()) assertEquals(0, testSetup.getOnSettingsCount())
composeTestRule.clickBack() composeTestRule.clickSettingsTopAppBarMenu()
assertEquals(1, testSetup.getOnBackCount()) assertEquals(1, testSetup.getOnSettingsCount())
} }
@Test @Test
@ -405,7 +403,7 @@ class SendViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBackCount()) assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnSendFailure() composeTestRule.assertOnSendFailure()
composeTestRule.onNodeWithText(getStringResource(R.string.send_failure_button), ignoreCase = true).also { composeTestRule.onNodeWithTag(SEND_FAILED_BUTTON).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }

View File

@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui package co.electriccoin.zcash.ui
import android.annotation.SuppressLint
import android.widget.Toast import android.widget.Toast
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -9,7 +8,6 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.NavigationArguments.SEND_AMOUNT import co.electriccoin.zcash.ui.NavigationArguments.SEND_AMOUNT
import co.electriccoin.zcash.ui.NavigationArguments.SEND_MEMO import co.electriccoin.zcash.ui.NavigationArguments.SEND_MEMO
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
@ -17,26 +15,22 @@ import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA
import co.electriccoin.zcash.ui.NavigationTargets.HISTORY import co.electriccoin.zcash.ui.NavigationTargets.HISTORY
import co.electriccoin.zcash.ui.NavigationTargets.HOME import co.electriccoin.zcash.ui.NavigationTargets.HOME
import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
import co.electriccoin.zcash.ui.NavigationTargets.SCAN import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
import co.electriccoin.zcash.ui.NavigationTargets.SEND
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
import co.electriccoin.zcash.ui.NavigationTargets.WALLET_ADDRESS_DETAILS import co.electriccoin.zcash.ui.NavigationTargets.WALLET_ADDRESS_DETAILS
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.about.WrapAbout import co.electriccoin.zcash.ui.screen.about.WrapAbout
import co.electriccoin.zcash.ui.screen.account.WrapAccount
import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
import co.electriccoin.zcash.ui.screen.history.WrapHistory import co.electriccoin.zcash.ui.screen.history.WrapHistory
import co.electriccoin.zcash.ui.screen.receive.WrapReceive import co.electriccoin.zcash.ui.screen.home.WrapHome
import co.electriccoin.zcash.ui.screen.request.WrapRequest import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery
import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.settings.WrapSettings import co.electriccoin.zcash.ui.screen.settings.WrapSettings
import co.electriccoin.zcash.ui.screen.support.WrapSupport import co.electriccoin.zcash.ui.screen.support.WrapSupport
@ -48,19 +42,31 @@ internal fun MainActivity.Navigation() {
val context = LocalContext.current val context = LocalContext.current
val navController = val navController =
rememberNavController().also { rememberNavController().also {
// This suppress is necessary, as this is how we set up the nav controller for tests.
@SuppressLint("RestrictedApi")
navControllerForTesting = it navControllerForTesting = it
} }
NavHost(navController = navController, startDestination = HOME) { NavHost(navController = navController, startDestination = HOME) {
composable(HOME) { composable(HOME) { backStackEntry ->
WrapAccount( WrapHome(
onPageChange = {
homeViewModel.screenIndex.value = it
},
goAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) },
goBack = { finish() },
goHistory = { navController.navigateJustOnce(HISTORY) }, goHistory = { navController.navigateJustOnce(HISTORY) },
goReceive = { navController.navigateJustOnce(RECEIVE) },
goSend = { navController.navigateJustOnce(SEND) },
goSettings = { navController.navigateJustOnce(SETTINGS) }, goSettings = { navController.navigateJustOnce(SETTINGS) },
goScan = { navController.navigateJustOnce(SCAN) },
sendArgumentsWrapper =
SendArgumentsWrapper(
recipientAddress = backStackEntry.savedStateHandle[SEND_RECIPIENT_ADDRESS],
amount = backStackEntry.savedStateHandle[SEND_AMOUNT],
memo = backStackEntry.savedStateHandle[SEND_MEMO]
),
) )
// Remove used Send screen parameters passed from the Scan screen if some exist
backStackEntry.savedStateHandle.remove<String>(SEND_RECIPIENT_ADDRESS)
backStackEntry.savedStateHandle.remove<String>(SEND_AMOUNT)
backStackEntry.savedStateHandle.remove<String>(SEND_MEMO)
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) { if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
WrapCheckForUpdate() WrapCheckForUpdate()
@ -112,33 +118,9 @@ internal fun MainActivity.Navigation() {
} }
) )
} }
composable(RECEIVE) {
WrapReceive(
onBack = { navController.popBackStackJustOnce(RECEIVE) },
onAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) }
)
}
composable(REQUEST) { composable(REQUEST) {
WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) }) WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) })
} }
composable(SEND) { backStackEntry ->
WrapSend(
goToQrScanner = {
Twig.debug { "Opening Qr Scanner Screen" }
navController.navigateJustOnce(SCAN)
},
goBack = { navController.popBackStackJustOnce(SEND) },
sendArgumentsWrapper =
SendArgumentsWrapper(
recipientAddress = backStackEntry.savedStateHandle[SEND_RECIPIENT_ADDRESS],
amount = backStackEntry.savedStateHandle[SEND_AMOUNT],
memo = backStackEntry.savedStateHandle[SEND_MEMO]
)
)
backStackEntry.savedStateHandle.remove<String>(SEND_RECIPIENT_ADDRESS)
backStackEntry.savedStateHandle.remove<String>(SEND_AMOUNT)
backStackEntry.savedStateHandle.remove<String>(SEND_MEMO)
}
composable(SUPPORT) { composable(SUPPORT) {
// Pop back stack won't be right if we deep link into support // Pop back stack won't be right if we deep link into support
WrapSupport(goBack = { navController.popBackStackJustOnce(SUPPORT) }) WrapSupport(goBack = { navController.popBackStackJustOnce(SUPPORT) })
@ -208,6 +190,7 @@ object NavigationArguments {
object NavigationTargets { object NavigationTargets {
const val ABOUT = "about" const val ABOUT = "about"
const val ACCOUNT = "account"
const val EXPORT_PRIVATE_DATA = "export_private_data" const val EXPORT_PRIVATE_DATA = "export_private_data"
const val HISTORY = "history" const val HISTORY = "history"
const val HOME = "home" const val HOME = "home"

View File

@ -5,4 +5,5 @@ package co.electriccoin.zcash.ui.common.test
*/ */
object CommonTag { object CommonTag {
const val WALLET_BIRTHDAY = "wallet_birthday" const val WALLET_BIRTHDAY = "wallet_birthday"
const val SETTINGS_TOP_BAR_BUTTON = "settings_top_bar_button"
} }

View File

@ -8,6 +8,8 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
@ -15,8 +17,14 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class HomeViewModel(application: Application) : AndroidViewModel(application) { class HomeViewModel(application: Application) : AndroidViewModel(application) {
/**
* Current Home sub-screen index in flow.
*/
val screenIndex: MutableStateFlow<HomeScreenIndex> = MutableStateFlow(HomeScreenIndex.ACCOUNT)
/** /**
* A flow of whether background sync is enabled * A flow of whether background sync is enabled
* Current Home sub-screen index in flow.
*/ */
val isBackgroundSyncEnabled: StateFlow<Boolean?> = val isBackgroundSyncEnabled: StateFlow<Boolean?> =
flow { flow {

View File

@ -57,7 +57,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
// To make this more multiplatform compatible, we need to remove the dependency on Context // To make this more multiplatform compatible, we need to remove the dependency on Context
// for loading the preferences. // for loading the preferences.
@ -151,7 +150,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
null null
) )
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) @OptIn(ExperimentalCoroutinesApi::class)
val walletSnapshot: StateFlow<WalletSnapshot?> = val walletSnapshot: StateFlow<WalletSnapshot?> =
synchronizer synchronizer
.flatMapLatest { .flatMapLatest {

View File

@ -7,5 +7,4 @@ object AccountTag {
const val STATUS_VIEWS = "status_views" const val STATUS_VIEWS = "status_views"
const val SINGLE_LINE_TEXT = "single_line_text" const val SINGLE_LINE_TEXT = "single_line_text"
const val FIAT_CONVERSION = "fiat_conversion" const val FIAT_CONVERSION = "fiat_conversion"
const val SETTINGS_TOP_BAR_BUTTON = "settings_top_bar_button"
} }

View File

@ -6,43 +6,23 @@ 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.MainActivity
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.configuration.RemoteConfig
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.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.update.model.UpdateState
@Composable @Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapAccount(
goSettings: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit
) {
WrapAccount(
this,
goSettings = goSettings,
goReceive = goReceive,
goSend = goSend,
goHistory = goHistory,
)
}
@Composable
@Suppress("LongParameterList")
internal fun WrapAccount( internal fun WrapAccount(
activity: ComponentActivity, activity: ComponentActivity,
goSettings: () -> Unit, goSettings: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit, goHistory: () -> Unit,
) { ) {
// we want to show information about app update, if available // Show information about the app update, if available
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> { val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory( CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application, activity.application,
@ -63,7 +43,8 @@ internal fun WrapAccount(
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current) val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current)
if (null == walletSnapshot) { if (null == walletSnapshot) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
Account( Account(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
@ -71,11 +52,10 @@ internal fun WrapAccount(
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing, isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isFiatConversionEnabled = isFiatConversionEnabled, isFiatConversionEnabled = isFiatConversionEnabled,
goSettings = goSettings, goSettings = goSettings,
goReceive = goReceive,
goSend = goSend,
goHistory = goHistory goHistory = goHistory
) )
// For benchmarking purposes
activity.reportFullyDrawn() activity.reportFullyDrawn()
} }
} }

View File

@ -24,6 +24,7 @@ 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.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.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
@ -31,7 +32,6 @@ import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.HeaderWithZecIcon import co.electriccoin.zcash.ui.design.component.HeaderWithZecIcon
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.component.TertiaryButton
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
@ -48,8 +48,6 @@ private fun ComposablePreview() {
isKeepScreenOnDuringSync = false, isKeepScreenOnDuringSync = false,
isFiatConversionEnabled = false, isFiatConversionEnabled = false,
goSettings = {}, goSettings = {},
goReceive = {},
goSend = {},
goHistory = {} goHistory = {}
) )
} }
@ -64,8 +62,6 @@ fun Account(
isKeepScreenOnDuringSync: Boolean?, isKeepScreenOnDuringSync: Boolean?,
isFiatConversionEnabled: Boolean, isFiatConversionEnabled: Boolean,
goSettings: () -> Unit, goSettings: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit goHistory: () -> Unit
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
@ -76,8 +72,6 @@ fun Account(
isUpdateAvailable = isUpdateAvailable, isUpdateAvailable = isUpdateAvailable,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync, isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
isFiatConversionEnabled = isFiatConversionEnabled, isFiatConversionEnabled = isFiatConversionEnabled,
goReceive = goReceive,
goSend = goSend,
goHistory = goHistory, goHistory = goHistory,
modifier = modifier =
Modifier.padding( Modifier.padding(
@ -97,11 +91,11 @@ private fun AccountTopAppBar(onSettings: () -> Unit) {
hamburgerMenuActions = { hamburgerMenuActions = {
IconButton( IconButton(
onClick = onSettings, onClick = onSettings,
modifier = Modifier.testTag(AccountTag.SETTINGS_TOP_BAR_BUTTON) modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
) { ) {
Icon( Icon(
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon), painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
contentDescription = stringResource(id = R.string.account_menu_content_description) contentDescription = stringResource(id = R.string.settings_menu_content_description)
) )
} }
} }
@ -115,8 +109,6 @@ private fun AccountMainContent(
isUpdateAvailable: Boolean, isUpdateAvailable: Boolean,
isKeepScreenOnDuringSync: Boolean?, isKeepScreenOnDuringSync: Boolean?,
isFiatConversionEnabled: Boolean, isFiatConversionEnabled: Boolean,
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit, goHistory: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -142,21 +134,7 @@ private fun AccountMainContent(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
PrimaryButton( PrimaryButton(onClick = goHistory, text = stringResource(R.string.account_button_history))
onClick = goSend,
text = stringResource(R.string.account_button_send)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
PrimaryButton(
onClick = goReceive,
text = stringResource(R.string.account_button_receive)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
TertiaryButton(onClick = goHistory, text = stringResource(R.string.account_button_history))
if (isKeepScreenOnDuringSync == true && walletSnapshot.status == Synchronizer.Status.SYNCING) { if (isKeepScreenOnDuringSync == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout() DisableScreenTimeout()

View File

@ -10,6 +10,7 @@ import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses
@Composable @Composable
@ -27,7 +28,8 @@ private fun WrapWalletAddresses(
val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
if (null == walletAddresses) { if (null == walletAddresses) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
WalletAddresses( WalletAddresses(
walletAddresses, walletAddresses,

View File

@ -0,0 +1,18 @@
@file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.ui.screen.balances
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import co.electriccoin.zcash.ui.screen.balances.view.Balances
@Suppress("UNUSED_PARAMETER")
@Composable
internal fun WrapBalances(
activity: ComponentActivity,
goSettings: () -> Unit,
) {
Balances(
onSettings = goSettings,
)
}

View File

@ -0,0 +1,89 @@
package co.electriccoin.zcash.ui.screen.balances.view
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview("Balances")
@Composable
private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Balances(
onSettings = {},
)
}
}
}
// TODO [#1127]: Implement Balances screen
// TODO [#1127]: https://github.com/Electric-Coin-Company/zashi-android/issues/1127
@Composable
fun Balances(onSettings: () -> Unit) {
Scaffold(topBar = {
BalancesTopAppBar(onSettings = onSettings)
}) { paddingValues ->
BalancesMainContent(
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingHuge,
start = ZcashTheme.dimens.screenHorizontalSpacing,
end = ZcashTheme.dimens.screenHorizontalSpacing
)
)
}
}
@Composable
private fun BalancesTopAppBar(onSettings: () -> Unit) {
SmallTopAppBar(
showTitleLogo = true,
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
) {
Icon(
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
contentDescription = stringResource(id = R.string.settings_menu_content_description)
)
}
}
)
}
@Composable
private fun BalancesMainContent(modifier: Modifier = Modifier) {
Box(
Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.Center
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Body(stringResource(id = R.string.not_implemented_yet))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
}

View File

@ -16,6 +16,7 @@ import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.exportdata.util.FileShareUtil import co.electriccoin.zcash.ui.screen.exportdata.util.FileShareUtil
import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -45,7 +46,8 @@ internal fun WrapExportPrivateData(
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (synchronizer == null) { if (synchronizer == null) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View File

@ -3,79 +3,162 @@
package co.electriccoin.zcash.ui.screen.home package co.electriccoin.zcash.ui.screen.home
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
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.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.screen.account.WrapAccount
import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.screen.balances.WrapBalances
import co.electriccoin.zcash.ui.screen.home.model.TabItem
import co.electriccoin.zcash.ui.screen.home.view.Home import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable
internal fun MainActivity.WrapHome( internal fun MainActivity.WrapHome(
onPageChange: (HomeScreenIndex) -> Unit,
goAddressDetails: () -> Unit,
goBack: () -> Unit,
goHistory: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
goReceive: () -> Unit, goScan: () -> Unit,
goSend: () -> Unit, sendArgumentsWrapper: SendArgumentsWrapper
goHistory: () -> Unit
) { ) {
WrapHome( WrapHome(
this, this,
goSettings = goSettings, onPageChange = onPageChange,
goReceive = goReceive, goAddressDetails = goAddressDetails,
goSend = goSend, goBack = goBack,
goHistory = goHistory, goHistory = goHistory,
goScan = goScan,
goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper
) )
} }
@Suppress("LongParameterList", "LongMethod")
@Composable @Composable
@Suppress("LongParameterList")
internal fun WrapHome( internal fun WrapHome(
activity: ComponentActivity, activity: ComponentActivity,
goSettings: () -> Unit, goAddressDetails: () -> Unit,
goReceive: () -> Unit, goBack: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit, goHistory: () -> Unit,
goSettings: () -> Unit,
goScan: () -> Unit,
onPageChange: (HomeScreenIndex) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper
) { ) {
// we want to show information about app update, if available val homeViewModel by activity.viewModels<HomeViewModel>()
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory( // Flow for propagating the new page index to the pager in the view layer
activity.application, val forceHomePageIndexFlow: MutableSharedFlow<ForcePage?> =
AppUpdateCheckerImp.new() MutableSharedFlow(
Int.MAX_VALUE,
Int.MAX_VALUE,
BufferOverflow.SUSPEND
) )
val forceIndex = forceHomePageIndexFlow.collectAsState(initial = null).value
val homeGoBack: () -> Unit = {
when (homeViewModel.screenIndex.value) {
HomeScreenIndex.ACCOUNT -> goBack()
HomeScreenIndex.SEND,
HomeScreenIndex.RECEIVE,
HomeScreenIndex.BALANCES -> forceHomePageIndexFlow.tryEmit(ForcePage())
} }
val updateAvailable =
checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let {
it?.appUpdateInfo != null && it.state == UpdateState.Prepared
} }
val walletViewModel by activity.viewModels<WalletViewModel>() BackHandler {
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value homeGoBack()
}
val settingsViewModel by activity.viewModels<SettingsViewModel>() val tabs =
persistentListOf(
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value TabItem(
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current) index = HomeScreenIndex.ACCOUNT,
title = stringResource(id = R.string.home_tab_account),
if (null == walletSnapshot) { testTag = HomeTag.TAB_ACCOUNT,
// Display loading indicator screenContent = {
} else { WrapAccount(
Home( activity = activity,
walletSnapshot = walletSnapshot, goHistory = goHistory,
isUpdateAvailable = updateAvailable,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isFiatConversionEnabled = isFiatConversionEnabled,
goSettings = goSettings, goSettings = goSettings,
goReceive = goReceive, )
goSend = goSend, }
goHistory = goHistory ),
TabItem(
index = HomeScreenIndex.SEND,
title = stringResource(id = R.string.home_tab_send),
testTag = HomeTag.TAB_SEND,
screenContent = {
WrapSend(
activity = activity,
goToQrScanner = goScan,
goBack = homeGoBack,
goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper
)
}
),
TabItem(
index = HomeScreenIndex.RECEIVE,
title = stringResource(id = R.string.home_tab_receive),
testTag = HomeTag.TAB_RECEIVE,
screenContent = {
WrapReceive(
activity = activity,
onSettings = goSettings,
onAddressDetails = goAddressDetails,
)
}
),
TabItem(
index = HomeScreenIndex.BALANCES,
title = stringResource(id = R.string.home_tab_balances),
testTag = HomeTag.TAB_BALANCES,
screenContent = {
WrapBalances(
activity = activity,
goSettings = goSettings
)
}
)
) )
activity.reportFullyDrawn() Home(
subScreens = tabs,
forcePage = forceIndex,
onPageChange = onPageChange
)
}
/**
* Wrapper class used to pass forced pages index into the view layer
*/
class ForcePage(
val currentPage: HomeScreenIndex = HomeScreenIndex.ACCOUNT,
)
/**
* Enum of the Home screen sub-screens
*/
enum class HomeScreenIndex {
// WARN: Be careful when re-ordering these, as the ordinal number states for their order
ACCOUNT,
SEND,
RECEIVE,
BALANCES, ;
companion object {
fun fromIndex(index: Int) = entries[index]
} }
} }

View File

@ -4,8 +4,8 @@ package co.electriccoin.zcash.ui.screen.home
* These are only used for automated testing. * These are only used for automated testing.
*/ */
object HomeTag { object HomeTag {
const val STATUS_VIEWS = "status_views" const val TAB_ACCOUNT = "tab_account"
const val SINGLE_LINE_TEXT = "single_line_text" const val TAB_SEND = "tab_send"
const val FIAT_CONVERSION = "fiat_conversion" const val TAB_RECEIVE = "tab_receive"
const val SETTINGS_TOP_BAR_BUTTON = "settings_top_bar_button" const val TAB_BALANCES = "tab_balances"
} }

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.ui.screen.home.model
import androidx.compose.runtime.Composable
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
data class TabItem(
val index: HomeScreenIndex,
val title: String,
val testTag: String,
val screenContent: @Composable () -> Unit
)

View File

@ -1,117 +0,0 @@
package co.electriccoin.zcash.ui.screen.home.model
import android.content.Context
import androidx.compose.ui.text.intl.Locale
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.toFiatCurrencyState
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.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.model.spendableBalance
import co.electriccoin.zcash.ui.common.model.totalBalance
import co.electriccoin.zcash.ui.common.toKotlinLocale
data class WalletDisplayValues(
val progress: PercentDecimal,
val zecAmountText: String,
val statusText: String,
val fiatCurrencyAmountState: FiatCurrencyConversionRateState,
val fiatCurrencyAmountText: String
) {
companion object {
@Suppress("MagicNumber", "LongMethod")
internal fun getNextValues(
context: Context,
walletSnapshot: WalletSnapshot,
updateAvailable: Boolean
): WalletDisplayValues {
var progress = PercentDecimal.ZERO_PERCENT
val zecAmountText = walletSnapshot.totalBalance().toZecString()
var statusText = ""
// TODO [#578]: Provide Zatoshi -> USD fiat currency formatting
// TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578
// We'll ideally provide a "fresh" currencyConversion object here
val fiatCurrencyAmountState =
walletSnapshot.spendableBalance().toFiatCurrencyState(
null,
Locale.current.toKotlinLocale(),
MonetarySeparators.current()
)
var fiatCurrencyAmountText = getFiatCurrencyRateValue(context, fiatCurrencyAmountState)
when (walletSnapshot.status) {
Synchronizer.Status.SYNCING -> {
progress = walletSnapshot.progress
// we add "so far" to the amount
if (fiatCurrencyAmountState != FiatCurrencyConversionRateState.Unavailable) {
fiatCurrencyAmountText =
context.getString(
R.string.home_status_syncing_amount_suffix,
fiatCurrencyAmountText
)
}
statusText =
context.getString(
R.string.home_status_syncing_format,
walletSnapshot.progress.toPercentageWithDecimal()
)
}
Synchronizer.Status.SYNCED -> {
statusText =
if (updateAvailable) {
context.getString(R.string.home_status_update)
} else {
context.getString(R.string.home_status_up_to_date)
}
}
Synchronizer.Status.DISCONNECTED -> {
statusText =
context.getString(
R.string.home_status_error,
context.getString(R.string.home_status_error_connection)
)
}
Synchronizer.Status.STOPPED -> {
statusText = context.getString(R.string.home_status_stopped)
}
}
// more detailed error message
walletSnapshot.synchronizerError?.let {
statusText =
context.getString(
R.string.home_status_error,
walletSnapshot.synchronizerError.getCauseMessage()
?: context.getString(R.string.home_status_error_unknown)
)
}
return WalletDisplayValues(
progress = progress,
zecAmountText = zecAmountText,
statusText = statusText,
fiatCurrencyAmountState = fiatCurrencyAmountState,
fiatCurrencyAmountText = fiatCurrencyAmountText
)
}
}
}
private fun getFiatCurrencyRateValue(
context: Context,
fiatCurrencyAmountState: FiatCurrencyConversionRateState
): String {
return fiatCurrencyAmountState.let { state ->
when (state) {
is FiatCurrencyConversionRateState.Current -> state.formattedFiatValue
is FiatCurrencyConversionRateState.Stale -> state.formattedFiatValue
is FiatCurrencyConversionRateState.Unavailable -> {
context.getString(R.string.fiat_currency_conversion_rate_unavailable)
}
}
}
}

View File

@ -1,43 +1,40 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.home.view package co.electriccoin.zcash.ui.screen.home.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.pager.PageSize
import androidx.compose.material3.Icon import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.material3.IconButton import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
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.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.Synchronizer import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
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.HeaderWithZecIcon import co.electriccoin.zcash.ui.design.component.NavigationTabText
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.screen.home.ForcePage
import co.electriccoin.zcash.ui.screen.home.HomeTag import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues import co.electriccoin.zcash.ui.screen.home.model.TabItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Preview("Home") @Preview("Home")
@Composable @Composable
@ -45,184 +42,115 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Home( Home(
walletSnapshot = WalletSnapshotFixture.new(), subScreens = persistentListOf(),
isUpdateAvailable = false, forcePage = null,
isKeepScreenOnDuringSync = false, onPageChange = {}
isFiatConversionEnabled = false,
goSettings = {},
goReceive = {},
goSend = {},
goHistory = {}
) )
} }
} }
} }
@Suppress("LongParameterList") @OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod")
@Composable @Composable
fun Home( fun Home(
walletSnapshot: WalletSnapshot, subScreens: ImmutableList<TabItem>,
isUpdateAvailable: Boolean, forcePage: ForcePage?,
isKeepScreenOnDuringSync: Boolean?, onPageChange: (HomeScreenIndex) -> Unit,
isFiatConversionEnabled: Boolean,
goSettings: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit
) { ) {
Scaffold(topBar = { val pagerState =
HomeTopAppBar(onSettings = goSettings) rememberPagerState(
}) { paddingValues -> initialPage = 0,
HomeMainContent( initialPageOffsetFraction = 0f,
walletSnapshot = walletSnapshot, pageCount = { subScreens.size }
isUpdateAvailable = isUpdateAvailable,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
isFiatConversionEnabled = isFiatConversionEnabled,
goReceive = goReceive,
goSend = goSend,
goHistory = goHistory,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingHuge,
start = ZcashTheme.dimens.screenHorizontalSpacing,
end = ZcashTheme.dimens.screenHorizontalSpacing
)
) )
// Listening for the current page change
LaunchedEffect(pagerState) {
snapshotFlow {
pagerState.currentPage
}.distinctUntilChanged()
.collect { page ->
Twig.info { "Current pager page: $page" }
onPageChange(HomeScreenIndex.fromIndex(page))
} }
} }
@Composable // Force page change e.g. when system back navigation event detected
private fun HomeTopAppBar(onSettings: () -> Unit) { forcePage?.let {
SmallTopAppBar( LaunchedEffect(forcePage) {
showTitleLogo = true, pagerState.animateScrollToPage(forcePage.currentPage.ordinal)
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
modifier = Modifier.testTag(HomeTag.SETTINGS_TOP_BAR_BUTTON)
) {
Icon(
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
contentDescription = stringResource(id = R.string.home_menu_content_description)
)
} }
} }
val coroutineScope = rememberCoroutineScope()
Scaffold(
bottomBar = {
Column {
Divider(
thickness = DividerDefaults.Thickness,
color = ZcashTheme.colors.dividerColor
) )
} TabRow(
selectedTabIndex = pagerState.currentPage,
@Suppress("LongParameterList") // Don't use the predefined divider, as it's fixed position is below the tabs bar
@Composable divider = {},
private fun HomeMainContent( indicator = { tabPositions ->
walletSnapshot: WalletSnapshot, TabRowDefaults.Indicator(
isUpdateAvailable: Boolean,
isKeepScreenOnDuringSync: Boolean?,
isFiatConversionEnabled: Boolean,
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
Modifier
.fillMaxHeight()
.verticalScroll(
rememberScrollState()
)
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Status(walletSnapshot, isUpdateAvailable, isFiatConversionEnabled)
Spacer(
modifier = modifier =
Modifier Modifier
.fillMaxHeight() .tabIndicatorOffset(tabPositions[pagerState.currentPage])
.weight(MINIMAL_WEIGHT) .padding(horizontal = ZcashTheme.dimens.spacingDefault),
color = ZcashTheme.colors.complementaryColor
) )
},
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
PrimaryButton(
onClick = goSend,
text = stringResource(R.string.home_button_send)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
PrimaryButton(
onClick = goReceive,
text = stringResource(R.string.home_button_receive)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
TertiaryButton(onClick = goHistory, text = stringResource(R.string.home_button_history))
if (isKeepScreenOnDuringSync == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {
DisableScreenTimeout()
}
}
}
@Composable
@Suppress("LongMethod", "MagicNumber")
private fun Status(
walletSnapshot: WalletSnapshot,
updateAvailable: Boolean,
isFiatConversionEnabled: Boolean
) {
val walletDisplayValues =
WalletDisplayValues.getNextValues(
LocalContext.current,
walletSnapshot,
updateAvailable
)
Column(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .navigationBarsPadding()
.testTag(HomeTag.STATUS_VIEWS), .padding(all = ZcashTheme.dimens.spacingDefault)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) subScreens.forEachIndexed { index, item ->
val selected = index == pagerState.currentPage
if (walletDisplayValues.zecAmountText.isNotEmpty()) { Tab(
HeaderWithZecIcon(amount = walletDisplayValues.zecAmountText) selected = selected,
} text = {
NavigationTabText(
if (isFiatConversionEnabled) { text = item.title,
Column(Modifier.testTag(HomeTag.FIAT_CONVERSION)) { selected = selected
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
when (walletDisplayValues.fiatCurrencyAmountState) {
is FiatCurrencyConversionRateState.Current -> {
BodyWithFiatCurrencySymbol(
amount = walletDisplayValues.fiatCurrencyAmountText
) )
} },
is FiatCurrencyConversionRateState.Stale -> { modifier =
// Note: we should show information about staleness too Modifier
BodyWithFiatCurrencySymbol( .padding(all = 0.dp)
amount = walletDisplayValues.fiatCurrencyAmountText .testTag(item.testTag),
) onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
}
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(HomeTag.SINGLE_LINE_TEXT)
) )
} }
} }
} }
}
) { paddingValues ->
HorizontalPager(
state = pagerState,
pageSpacing = 0.dp,
pageSize = PageSize.Fill,
pageNestedScrollConnection =
PagerDefaults.pageNestedScrollConnection(
Orientation.Horizontal
),
pageContent = { index ->
subScreens[index].screenContent()
},
key = { index ->
subScreens[index].title
},
beyondBoundsPageCount = 1,
modifier =
Modifier
.padding(
bottom = paddingValues.calculateBottomPadding()
)
)
}
}

View File

@ -7,28 +7,14 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.WalletAddresses import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.receive.view.Receive import co.electriccoin.zcash.ui.screen.receive.view.Receive
@Composable @Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapReceive(
onBack: () -> Unit,
onAddressDetails: () -> Unit,
) {
WrapReceive(
this,
onBack = onBack,
onAddressDetails = onAddressDetails,
)
}
@Composable
@Suppress("LongParameterList")
internal fun WrapReceive( internal fun WrapReceive(
activity: ComponentActivity, activity: ComponentActivity,
onBack: () -> Unit, onSettings: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
) { ) {
val viewModel by activity.viewModels<WalletViewModel>() val viewModel by activity.viewModels<WalletViewModel>()
@ -36,24 +22,24 @@ internal fun WrapReceive(
WrapReceive( WrapReceive(
walletAddresses, walletAddresses,
onBack = onBack, onSettings = onSettings,
onAddressDetails = onAddressDetails, onAddressDetails = onAddressDetails,
) )
} }
@Composable @Composable
@Suppress("LongParameterList")
internal fun WrapReceive( internal fun WrapReceive(
walletAddresses: WalletAddresses?, walletAddresses: WalletAddresses?,
onBack: () -> Unit, onSettings: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
) { ) {
if (null == walletAddresses) { if (null == walletAddresses) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
Receive( Receive(
walletAddresses.unified, walletAddresses.unified,
onBack = onBack, onSettings = onSettings,
onAddressDetails = onAddressDetails, onAddressDetails = onAddressDetails,
onAdjustBrightness = { /* Just for testing */ } onAdjustBrightness = { /* Just for testing */ }
) )

View File

@ -22,6 +22,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -32,6 +34,7 @@ import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.BrightenScreen import co.electriccoin.zcash.ui.common.BrightenScreen
import co.electriccoin.zcash.ui.common.DisableScreenTimeout import co.electriccoin.zcash.ui.common.DisableScreenTimeout
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.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -50,7 +53,7 @@ private fun ComposablePreview() {
GradientSurface { GradientSurface {
Receive( Receive(
walletAddress = runBlocking { WalletAddressFixture.unified() }, walletAddress = runBlocking { WalletAddressFixture.unified() },
onBack = {}, onSettings = {},
onAddressDetails = {}, onAddressDetails = {},
onAdjustBrightness = {}, onAdjustBrightness = {},
) )
@ -61,7 +64,7 @@ private fun ComposablePreview() {
@Composable @Composable
fun Receive( fun Receive(
walletAddress: WalletAddress, walletAddress: WalletAddress,
onBack: () -> Unit, onSettings: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAdjustBrightness: (Boolean) -> Unit, onAdjustBrightness: (Boolean) -> Unit,
) { ) {
@ -71,7 +74,7 @@ fun Receive(
Column { Column {
ReceiveTopAppBar( ReceiveTopAppBar(
adjustBrightness = brightness, adjustBrightness = brightness,
onBack = onBack, onSettings = onSettings,
onBrightness = { onBrightness = {
onAdjustBrightness(!brightness) onAdjustBrightness(!brightness)
setBrightness(!brightness) setBrightness(!brightness)
@ -97,14 +100,11 @@ fun Receive(
@Composable @Composable
private fun ReceiveTopAppBar( private fun ReceiveTopAppBar(
adjustBrightness: Boolean, adjustBrightness: Boolean,
onBack: () -> Unit, onSettings: () -> Unit,
onBrightness: () -> Unit onBrightness: () -> Unit
) { ) {
SmallTopAppBar( SmallTopAppBar(
titleText = stringResource(id = R.string.receive_title), titleText = stringResource(id = R.string.receive_title),
backText = stringResource(id = R.string.receive_back),
backContentDescriptionText = stringResource(id = R.string.receive_back_content_description),
onBack = onBack,
regularActions = { regularActions = {
IconButton( IconButton(
onClick = onBrightness onClick = onBrightness
@ -119,6 +119,17 @@ private fun ReceiveTopAppBar(
contentDescription = stringResource(R.string.receive_brightness_content_description) contentDescription = stringResource(R.string.receive_brightness_content_description)
) )
} }
},
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
) {
Icon(
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
contentDescription = stringResource(id = R.string.settings_menu_content_description)
)
}
} }
) )
} }

View File

@ -12,6 +12,7 @@ import cash.z.ecc.sdk.model.ZecRequest
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.request.view.Request import co.electriccoin.zcash.ui.screen.request.view.Request
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -29,7 +30,8 @@ private fun WrapRequest(
val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
if (null == walletAddresses) { if (null == walletAddresses) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
Request( Request(
walletAddresses.unified, walletAddresses.unified,

View File

@ -10,6 +10,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
import co.electriccoin.zcash.ui.screen.scan.view.Scan import co.electriccoin.zcash.ui.screen.scan.view.Scan
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,7 +41,8 @@ fun WrapScan(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
if (synchronizer == null) { if (synchronizer == null) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
Scan( Scan(
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,

View File

@ -9,6 +9,7 @@ import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery
@Composable @Composable
@ -39,7 +40,8 @@ private fun WrapSeedRecovery(
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer || null == persistableWallet) { if (null == synchronizer || null == persistableWallet) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
SeedRecovery( SeedRecovery(
persistableWallet, persistableWallet,

View File

@ -18,9 +18,9 @@ import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.sdk.extension.send import cash.z.ecc.sdk.extension.send
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.spendableBalance import co.electriccoin.zcash.ui.common.model.spendableBalance
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.send.ext.Saver import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendStage import co.electriccoin.zcash.ui.screen.send.model.SendStage
@ -28,20 +28,12 @@ import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
internal fun MainActivity.WrapSend( internal fun WrapSend(
sendArgumentsWrapper: SendArgumentsWrapper?,
goToQrScanner: () -> Unit,
goBack: () -> Unit
) {
WrapSend(this, sendArgumentsWrapper, goToQrScanner, goBack)
}
@Composable
private fun WrapSend(
activity: ComponentActivity, activity: ComponentActivity,
sendArgumentsWrapper: SendArgumentsWrapper?, sendArgumentsWrapper: SendArgumentsWrapper?,
goToQrScanner: () -> Unit, goToQrScanner: () -> Unit,
goBack: () -> Unit goBack: () -> Unit,
goSettings: () -> Unit,
) { ) {
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
@ -53,7 +45,16 @@ private fun WrapSend(
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
WrapSend(sendArgumentsWrapper, synchronizer, spendableBalance, spendingKey, goToQrScanner, goBack, hasCameraFeature) WrapSend(
sendArgumentsWrapper,
synchronizer,
spendableBalance,
spendingKey,
goToQrScanner,
goBack,
goSettings,
hasCameraFeature
)
} }
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -66,6 +67,7 @@ internal fun WrapSend(
spendingKey: UnifiedSpendingKey?, spendingKey: UnifiedSpendingKey?,
goToQrScanner: () -> Unit, goToQrScanner: () -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
goSettings: () -> Unit,
hasCameraFeature: Boolean hasCameraFeature: Boolean
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -83,11 +85,13 @@ internal fun WrapSend(
when (sendStage) { when (sendStage) {
SendStage.Form -> goBack() SendStage.Form -> goBack()
SendStage.Confirmation -> setSendStage(SendStage.Form) SendStage.Confirmation -> setSendStage(SendStage.Form)
SendStage.Sending -> { // no action - wait until done SendStage.Sending -> { /* no action - wait until the sending is done */ }
}
SendStage.SendFailure -> setSendStage(SendStage.Form) SendStage.SendFailure -> setSendStage(SendStage.Form)
SendStage.SendSuccessful -> goBack() SendStage.SendSuccessful -> {
setZecSend(null)
setSendStage(SendStage.Form)
goBack()
}
} }
} }
@ -96,7 +100,8 @@ internal fun WrapSend(
} }
if (null == synchronizer || null == spendableBalance || null == spendingKey) { if (null == synchronizer || null == spendableBalance || null == spendingKey) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
Send( Send(
mySpendableBalance = spendableBalance, mySpendableBalance = spendableBalance,
@ -106,6 +111,7 @@ internal fun WrapSend(
zecSend = zecSend, zecSend = zecSend,
onZecSendChange = setZecSend, onZecSendChange = setZecSend,
onBack = onBackAction, onBack = onBackAction,
onSettings = goSettings,
onCreateAndSend = { onCreateAndSend = {
scope.launch { scope.launch {
Twig.debug { "Sending transaction" } Twig.debug { "Sending transaction" }

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.ui.screen.send
/**
* These are only used for automated testing.
*/
object SendTag {
const val SEND_FORM_BUTTON = "send_form_button"
const val SEND_CONFIRMATION_BUTTON = "send_confirmation_button"
const val SEND_FAILED_BUTTON = "send_failed_button"
const val SEND_SUCCESS_BUTTON = "send_success_button"
}

View File

@ -14,14 +14,11 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.QrCodeScanner import androidx.compose.material.icons.outlined.QrCodeScanner
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -30,6 +27,8 @@ import androidx.compose.runtime.setValue
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.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -46,14 +45,17 @@ import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.fixture.MemoFixture import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.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.Body
import co.electriccoin.zcash.ui.design.component.FormTextField import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
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
import co.electriccoin.zcash.ui.screen.send.ext.valueOrEmptyChar import co.electriccoin.zcash.ui.screen.send.ext.valueOrEmptyChar
@ -76,6 +78,7 @@ private fun PreviewSendForm() {
onCreateAndSend = {}, onCreateAndSend = {},
onQrScannerOpen = {}, onQrScannerOpen = {},
onBack = {}, onBack = {},
onSettings = {},
hasCameraFeature = true hasCameraFeature = true
) )
} }
@ -123,7 +126,7 @@ private fun PreviewSendFailure() {
private fun PreviewSendConfirmation() { private fun PreviewSendConfirmation() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
Confirmation( SendConfirmation(
zecSend = zecSend =
ZecSend( ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() }, destination = runBlocking { WalletAddressFixture.sapling() },
@ -146,6 +149,7 @@ fun Send(
zecSend: ZecSend?, zecSend: ZecSend?,
onZecSendChange: (ZecSend) -> Unit, onZecSendChange: (ZecSend) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
onSettings: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit, onCreateAndSend: (ZecSend) -> Unit,
onQrScannerOpen: () -> Unit, onQrScannerOpen: () -> Unit,
hasCameraFeature: Boolean hasCameraFeature: Boolean
@ -153,7 +157,8 @@ fun Send(
Scaffold(topBar = { Scaffold(topBar = {
SendTopAppBar( SendTopAppBar(
onBack = onBack, onBack = onBack,
showBackNavigationButton = sendStage != SendStage.Sending onSettings = onSettings,
showBackNavigationButton = (sendStage != SendStage.Sending && sendStage != SendStage.Form)
) )
}) { paddingValues -> }) { paddingValues ->
SendMainContent( SendMainContent(
@ -184,28 +189,33 @@ fun Send(
} }
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun SendTopAppBar( private fun SendTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
onSettings: () -> Unit,
showBackNavigationButton: Boolean = true showBackNavigationButton: Boolean = true
) { ) {
SmallTopAppBar(
titleText = stringResource(id = R.string.send_title),
onBack = onBack,
backText =
if (showBackNavigationButton) { if (showBackNavigationButton) {
TopAppBar( stringResource(id = R.string.send_back)
title = { Text(text = stringResource(id = R.string.send_title)) }, } else {
navigationIcon = { null
},
backContentDescriptionText = stringResource(id = R.string.send_back_content_description),
hamburgerMenuActions = {
IconButton( IconButton(
onClick = onBack onClick = onSettings,
modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
contentDescription = stringResource(R.string.send_back_content_description) contentDescription = stringResource(id = R.string.settings_menu_content_description)
) )
} }
} }
) )
} else {
TopAppBar(title = { Text(text = stringResource(id = R.string.send_title)) })
}
} }
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -239,7 +249,7 @@ private fun SendMainContent(
) )
} }
(sendStage == SendStage.Confirmation) -> { (sendStage == SendStage.Confirmation) -> {
Confirmation( SendConfirmation(
zecSend = zecSend, zecSend = zecSend,
onConfirmation = { onConfirmation = {
onSendStageChange(SendStage.Sending) onSendStageChange(SendStage.Sending)
@ -426,13 +436,14 @@ private fun SendForm(
// Check for ABBREVIATION_INDEX goes away once proper address validation is in place. // Check for ABBREVIATION_INDEX goes away once proper address validation is in place.
// For now, it just prevents a crash on the confirmation screen. // For now, it just prevents a crash on the confirmation screen.
enabled = amountZecString.isNotBlank() && recipientAddressString.length > ABBREVIATION_INDEX, enabled = amountZecString.isNotBlank() && recipientAddressString.length > ABBREVIATION_INDEX,
outerPaddingValues = PaddingValues(top = dimens.spacingNone) outerPaddingValues = PaddingValues(top = dimens.spacingNone),
modifier = Modifier.testTag(SendTag.SEND_FORM_BUTTON)
) )
} }
} }
@Composable @Composable
private fun Confirmation( private fun SendConfirmation(
zecSend: ZecSend, zecSend: ZecSend,
onConfirmation: () -> Unit, onConfirmation: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -465,7 +476,10 @@ private fun Confirmation(
) )
PrimaryButton( PrimaryButton(
modifier = Modifier.padding(top = dimens.spacingSmall), modifier =
Modifier
.padding(top = dimens.spacingSmall)
.testTag(SendTag.SEND_CONFIRMATION_BUTTON),
onClick = onConfirmation, onClick = onConfirmation,
text = stringResource(id = R.string.send_confirmation_button), text = stringResource(id = R.string.send_confirmation_button),
outerPaddingValues = PaddingValues(top = dimens.spacingSmall) outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
@ -566,7 +580,10 @@ private fun SendSuccessful(
) )
PrimaryButton( PrimaryButton(
modifier = Modifier.padding(top = dimens.spacingSmall), modifier =
Modifier
.padding(top = dimens.spacingSmall)
.testTag(SendTag.SEND_SUCCESS_BUTTON),
text = stringResource(R.string.send_successful_button), text = stringResource(R.string.send_successful_button),
onClick = onDone, onClick = onDone,
outerPaddingValues = PaddingValues(top = dimens.spacingSmall) outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
@ -616,7 +633,10 @@ private fun SendFailure(
) )
PrimaryButton( PrimaryButton(
modifier = Modifier.padding(top = dimens.spacingSmall), modifier =
Modifier
.padding(top = dimens.spacingSmall)
.testTag(SendTag.SEND_FAILED_BUTTON),
text = stringResource(R.string.send_failure_button), text = stringResource(R.string.send_failure_button),
onClick = onDone, onClick = onDone,
outerPaddingValues = PaddingValues(top = dimens.spacingSmall) outerPaddingValues = PaddingValues(top = dimens.spacingSmall)

View File

@ -3,6 +3,7 @@
package co.electriccoin.zcash.ui.screen.settings package co.electriccoin.zcash.ui.screen.settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
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
@ -11,6 +12,7 @@ import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.view.Settings import co.electriccoin.zcash.ui.screen.settings.view.Settings
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@ -60,13 +62,18 @@ private fun WrapSettings(
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value
BackHandler {
goBack()
}
@Suppress("ComplexCondition") @Suppress("ComplexCondition")
if (null == synchronizer || if (null == synchronizer ||
null == isAnalyticsEnabled || null == isAnalyticsEnabled ||
null == isBackgroundSyncEnabled || null == isBackgroundSyncEnabled ||
null == isKeepScreenOnWhileSyncing null == isKeepScreenOnWhileSyncing
) { ) {
// Display loading indicator // Improve this by allowing screen composition and updating it after the data is available
CircularScreenProgressIndicator()
} else { } else {
Settings( Settings(
TroubleshootingParameters( TroubleshootingParameters(

View File

@ -1,7 +1,4 @@
<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_menu_content_description">Open Settings</string>
<string name="account_button_receive">Receive</string>
<string name="account_button_send">Send</string>
<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"> <string name="account_status_syncing_format" formatted="true">Syncing - <xliff:g id="synced_percent" example="50.25">

View File

@ -0,0 +1,3 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="balances_title">Balances</string>
</resources>

View File

@ -3,4 +3,5 @@
<string name="empty_char">-</string> <string name="empty_char">-</string>
<string name="zcash_logo_content_description">Zcash logo</string> <string name="zcash_logo_content_description">Zcash logo</string>
<string name="not_implemented_yet">Not implemented yet.</string> <string name="not_implemented_yet">Not implemented yet.</string>
<string name="settings_menu_content_description">Open Settings</string>
</resources> </resources>

View File

@ -1,24 +1,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="home_menu_content_description">Open Settings</string>
<string name="home_button_receive">Receive</string>
<string name="home_button_send">Send</string>
<string name="home_button_history">Transaction History</string> <string name="home_button_history">Transaction History</string>
<string name="home_status_syncing_format" formatted="true">Syncing - <xliff:g id="synced_percent" example="50.25"> <string name="home_tab_account">Account</string>
%1$s</xliff:g>%%</string> <!-- double %% for escaping --> <string name="home_tab_send">Send</string>
<string name="home_status_syncing_catchup">Syncing</string> <string name="home_tab_receive">Receive</string>
<string name="home_status_syncing_amount_suffix" formatted="true"><xliff:g id="amount_prefix" example="123$">%1$s</xliff:g> so far</string> <string name="home_tab_balances">Balances</string>
<string name="home_status_syncing_additional_information">We will show you funds as we discover them.</string>
<string name="home_status_up_to_date">Up-to-date</string>
<string name="home_status_sending_format" formatted="true">Sending <xliff:g id="sending_amount" example=".023">%1$s</xliff:g></string>
<string name="home_status_receiving_format" formatted="true">Receiving <xliff:g id="receiving_amount" example=".023">%1$s</xliff:g> ZEC</string>
<string name="home_status_shielding_format" formatted="true">Shielding <xliff:g id="shielding_amount" example=".023">%1$s</xliff:g> ZEC</string>
<string name="home_status_update">Please Update</string>
<string name="home_status_error" formatted="true">Error: <xliff:g id="error_type" example="Lost connection">%1$s</xliff:g></string>
<string name="home_status_error_connection">Disconnected</string>
<string name="home_status_error_unknown">Unknown cause</string>
<string name="home_status_stopped">Synchronizer stopped</string>
<string name="home_status_updating_blockheight">Updating blockheight</string>
<string name="home_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="home_status_spendable" formatted="true">Fully spendable in <xliff:g id="spendable_time" example="2 minutes">%1$s</xliff:g></string>
</resources> </resources>

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="receive_title">Receive</string> <string name="receive_title">Receive</string>
<string name="receive_back">Back</string>
<string name="receive_back_content_description">Back</string>
<string name="receive_brightness_content_description">Adjust brightness</string> <string name="receive_brightness_content_description">Adjust brightness</string>
<string name="receive_qr_code_content_description">QR code for address</string> <string name="receive_qr_code_content_description">QR code for address</string>
<string name="receive_caption">Your Address</string> <string name="receive_caption">Your Address</string>

View File

@ -1,5 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="send_title">Send ZEC</string> <string name="send_title">Send</string>
<string name="send_back">Back</string>
<string name="send_back_content_description">Back</string> <string name="send_back_content_description">Back</string>
<string name="send_scan_content_description">Scan</string> <string name="send_scan_content_description">Scan</string>
<string name="send_balance" formatted="true"><xliff:g id="balance" example="12.345">%1$s</xliff:g> ZEC Available</string> <string name="send_balance" formatted="true"><xliff:g id="balance" example="12.345">%1$s</xliff:g> ZEC Available</string>

View File

@ -7,9 +7,10 @@ import android.os.Build
import android.os.LocaleList import android.os.LocaleList
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
@ -40,9 +41,11 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
import co.electriccoin.zcash.ui.design.component.UiMode import co.electriccoin.zcash.ui.design.component.UiMode
import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.home.HomeTag
import co.electriccoin.zcash.ui.screen.restore.RestoreTag import co.electriccoin.zcash.ui.screen.restore.RestoreTag
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
import co.electriccoin.zcash.ui.screen.settings.SettingsTag import co.electriccoin.zcash.ui.screen.send.SendTag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -95,6 +98,13 @@ class ScreenshotTest : UiTestPrerequisites() {
} }
} }
private fun ComposeContentTestRule.navigateInHomeTab(destinationTag: String) {
onNodeWithTag(destinationTag).also {
it.assertExists()
it.performClick()
}
}
private fun runWith( private fun runWith(
uiMode: UiMode, uiMode: UiMode,
locale: String, locale: String,
@ -265,17 +275,22 @@ class ScreenshotTest : UiTestPrerequisites() {
return return
} }
// These are the home screen bottom navigation sub-screens
onboardingScreenshots(resContext, tag, composeTestRule) onboardingScreenshots(resContext, tag, composeTestRule)
recoveryScreenshots(resContext, tag, composeTestRule) recoveryScreenshots(resContext, tag, composeTestRule)
homeScreenshots(resContext, tag, composeTestRule)
// These are the buttons on the home screen composeTestRule.navigateInHomeTab(HomeTag.TAB_ACCOUNT)
navigateTo(NavigationTargets.SEND) accountScreenshots(tag, composeTestRule)
composeTestRule.navigateInHomeTab(HomeTag.TAB_SEND)
sendZecScreenshots(resContext, tag, composeTestRule) sendZecScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.RECEIVE) composeTestRule.navigateInHomeTab(HomeTag.TAB_RECEIVE)
receiveZecScreenshots(resContext, tag, composeTestRule) receiveZecScreenshots(resContext, tag, composeTestRule)
composeTestRule.navigateInHomeTab(HomeTag.TAB_BALANCES)
balancesScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.WALLET_ADDRESS_DETAILS) navigateTo(NavigationTargets.WALLET_ADDRESS_DETAILS)
addressDetailsScreenshots(resContext, tag, composeTestRule) addressDetailsScreenshots(resContext, tag, composeTestRule)
@ -283,7 +298,7 @@ class ScreenshotTest : UiTestPrerequisites() {
transactionHistoryScreenshots(resContext, tag, composeTestRule) transactionHistoryScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.SETTINGS) navigateTo(NavigationTargets.SETTINGS)
settingsScreenshots(tag, composeTestRule) settingsScreenshots(resContext, tag, composeTestRule)
// These are the Settings screen items // These are the Settings screen items
// We could manually click on each one, which is a better integration test but a worse screenshot test // We could manually click on each one, which is a better integration test but a worse screenshot test
@ -362,8 +377,7 @@ private fun recoveryScreenshots(
} }
} }
private fun homeScreenshots( private fun accountScreenshots(
resContext: Context,
tag: String, tag: String,
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity> composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>
) { ) {
@ -374,17 +388,32 @@ private fun homeScreenshots(
composeTestRule.activity.walletViewModel.walletSnapshot.value != null composeTestRule.activity.walletViewModel.walletSnapshot.value != null
} }
composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send), ignoreCase = true)).also { composeTestRule.onNodeWithTag(AccountTag.STATUS_VIEWS).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Home 1") ScreenshotTest.takeScreenshot(tag, "Account 1")
}
}
private fun balancesScreenshots(
resContext: Context,
tag: String,
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>
) {
// TODO [#1127]: Implement Balances screen
// TODO [#1127]: https://github.com/Electric-Coin-Company/zashi-android/issues/1127
composeTestRule.onNodeWithText(resContext.getString(R.string.not_implemented_yet)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Balances 1")
} }
} }
private fun settingsScreenshots( private fun settingsScreenshots(
resContext: Context,
tag: String, tag: String,
composeTestRule: ComposeTestRule composeTestRule: ComposeTestRule
) { ) {
composeTestRule.onNode(hasTestTag(SettingsTag.SETTINGS_TOP_APP_BAR)).also { composeTestRule.onNode(hasText(resContext.getString(R.string.settings_backup_wallet), ignoreCase = true)).also {
it.assertExists() it.assertExists()
} }
@ -438,19 +467,16 @@ private fun receiveZecScreenshots(
composeTestRule.activity.walletViewModel.addresses.value != null composeTestRule.activity.walletViewModel.addresses.value != null
} }
composeTestRule.onNode(hasText(resContext.getString(R.string.receive_title), ignoreCase = true)).also { composeTestRule.onNode(
hasContentDescription(
value = resContext.getString(R.string.receive_qr_code_content_description),
ignoreCase = true
)
).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot(tag, "Receive 1") ScreenshotTest.takeScreenshot(tag, "Receive 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.receive_see_address_details), ignoreCase = true).also {
it.performClick()
}
composeTestRule.waitForIdle()
ScreenshotTest.takeScreenshot(tag, "Address details")
} }
private fun sendZecScreenshots( private fun sendZecScreenshots(
@ -495,7 +521,7 @@ private fun sendZecScreenshots(
// Screenshot: Fulfilled form // Screenshot: Fulfilled form
ScreenshotTest.takeScreenshot(tag, "Send 2") ScreenshotTest.takeScreenshot(tag, "Send 2")
composeTestRule.onNodeWithText(resContext.getString(R.string.send_create), ignoreCase = true).also { composeTestRule.onNodeWithTag(SendTag.SEND_FORM_BUTTON).also {
it.performClick() it.performClick()
} }