[#774] Redesign home with hamburger menu

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Carter Jernigan 2023-02-28 08:54:07 -05:00 committed by GitHub
parent 9b966b4087
commit 6d01f210fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 614 additions and 712 deletions

View File

@ -31,8 +31,8 @@ android {
"src/main/res/ui/common",
"src/main/res/ui/home",
"src/main/res/ui/onboarding",
"src/main/res/ui/profile",
"src/main/res/ui/scan",
"src/main/res/ui/receive",
"src/main/res/ui/restore",
"src/main/res/ui/request",
"src/main/res/ui/seed",

View File

@ -10,21 +10,37 @@ import java.util.concurrent.atomic.AtomicInteger
class HomeTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot,
private val isRequestZecButtonEnabled: Boolean,
) {
private val onScanCount = AtomicInteger(0)
private val onProfileCount = AtomicInteger(0)
private val onAboutCount = AtomicInteger(0)
private val onSeedCount = AtomicInteger(0)
private val onSettingsCount = AtomicInteger(0)
private val onSupportCount = AtomicInteger(0)
private val onReceiveCount = AtomicInteger(0)
private val onSendCount = AtomicInteger(0)
private val onRequestCount = AtomicInteger(0)
fun getOnScanCount(): Int {
fun getOnAboutCount(): Int {
composeTestRule.waitForIdle()
return onScanCount.get()
return onAboutCount.get()
}
fun getOnProfileCount(): Int {
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onProfileCount.get()
return onSettingsCount.get()
}
fun getOnSupportCount(): Int {
composeTestRule.waitForIdle()
return onSupportCount.get()
}
fun getOnSeedCount(): Int {
composeTestRule.waitForIdle()
return onSeedCount.get()
}
fun getOnReceiveCount(): Int {
composeTestRule.waitForIdle()
return onReceiveCount.get()
}
fun getOnSendCount(): Int {
@ -32,11 +48,6 @@ class HomeTestSetup(
return onSendCount.get()
}
fun getOnRequestCount(): Int {
composeTestRule.waitForIdle()
return onRequestCount.get()
}
fun getWalletSnapshot(): WalletSnapshot {
composeTestRule.waitForIdle()
return walletSnapshot
@ -46,24 +57,29 @@ class HomeTestSetup(
fun getDefaultContent() {
Home(
walletSnapshot,
isKeepScreenOnDuringSync = false,
isRequestZecButtonEnabled = isRequestZecButtonEnabled,
transactionHistory = emptyList(),
goScan = {
onScanCount.incrementAndGet()
isKeepScreenOnDuringSync = false,
isUpdateAvailable = false,
goSettings = {
onSettingsCount.incrementAndGet()
},
goProfile = {
onProfileCount.incrementAndGet()
goSeedPhrase = {
onSeedCount.incrementAndGet()
},
goSupport = {
onSupportCount.incrementAndGet()
},
goAbout = {
onAboutCount.incrementAndGet()
},
goReceive = {
onReceiveCount.incrementAndGet()
},
goSend = {
onSendCount.incrementAndGet()
},
goRequest = {
onRequestCount.incrementAndGet()
},
resetSdk = {},
isDebugMenuEnabled = false,
updateAvailable = false
)
}

View File

@ -26,8 +26,7 @@ class HomeViewIntegrationTest : UiTestPrerequisites() {
private fun newTestSetup(walletSnapshot: WalletSnapshot) = HomeTestSetup(
composeTestRule,
walletSnapshot,
isRequestZecButtonEnabled = false
walletSnapshot
)
// This is just basic sanity check that we still have UI set up as expected after the state restore

View File

@ -28,11 +28,7 @@ class HomeViewTest : UiTestPrerequisites() {
fun check_all_elementary_ui_elements_displayed() {
newTestSetup()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_scan_content_description)).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_profile_content_description)).also {
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_menu_content_description)).also {
it.assertIsDisplayed()
}
@ -44,43 +40,21 @@ class HomeViewTest : UiTestPrerequisites() {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_request)).also {
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_receive)).also {
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun hide_request_zec() {
newTestSetup(isRequestZecButtonEnabled = false)
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_request)).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun click_scan_button() {
fun click_receive_button() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnScanCount())
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.clickScan()
composeTestRule.clickReceive()
assertEquals(1, testSetup.getOnScanCount())
}
@Test
@MediumTest
fun click_profile_button() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnProfileCount())
composeTestRule.clickProfile()
assertEquals(1, testSetup.getOnProfileCount())
assertEquals(1, testSetup.getOnReceiveCount())
}
@Test
@ -97,33 +71,84 @@ class HomeViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun click_request_button() {
fun hamburger_seed() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnRequestCount())
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.clickRequest()
composeTestRule.openNavigationDrawer()
assertEquals(1, testSetup.getOnRequestCount())
composeTestRule.onNodeWithText(getStringResource(R.string.home_menu_seed_phrase)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnSeedCount())
}
private fun newTestSetup(isRequestZecButtonEnabled: Boolean = true) = HomeTestSetup(
@Test
@MediumTest
fun hamburger_settings() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.openNavigationDrawer()
composeTestRule.onNodeWithText(getStringResource(R.string.home_menu_settings)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnSettingsCount())
}
@Test
@MediumTest
fun hamburger_support() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.openNavigationDrawer()
composeTestRule.onNodeWithText(getStringResource(R.string.home_menu_support)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnSupportCount())
}
@Test
@MediumTest
fun hamburger_about() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.openNavigationDrawer()
composeTestRule.onNodeWithText(getStringResource(R.string.home_menu_about)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnAboutCount())
}
private fun newTestSetup() = HomeTestSetup(
composeTestRule,
WalletSnapshotFixture.new(),
isRequestZecButtonEnabled = isRequestZecButtonEnabled
WalletSnapshotFixture.new()
).apply {
setDefaultContent()
}
}
fun ComposeContentTestRule.clickScan() {
onNodeWithContentDescription(getStringResource(R.string.home_scan_content_description)).also {
private fun ComposeContentTestRule.openNavigationDrawer() {
onNodeWithContentDescription(getStringResource(R.string.home_menu_content_description)).also {
it.performClick()
}
}
private fun ComposeContentTestRule.clickProfile() {
onNodeWithContentDescription(getStringResource(R.string.home_profile_content_description)).also {
private fun ComposeContentTestRule.clickReceive() {
onNodeWithText(getStringResource(R.string.home_button_receive)).also {
it.performClick()
}
}
@ -134,10 +159,3 @@ private fun ComposeContentTestRule.clickSend() {
it.performClick()
}
}
private fun ComposeContentTestRule.clickRequest() {
onNodeWithText(getStringResource(R.string.home_button_request)).also {
it.performScrollTo()
it.performClick()
}
}

View File

@ -1,231 +0,0 @@
package co.electriccoin.zcash.ui.screen.profile.view
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.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.profile.util.ProfileConfiguration
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
/*
* Note: It is difficult to test the QR code from automated tests. There is a manual test case
* for that currently. A future enhancement could take a screenshot and try to analyze the
* screenshot contents.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun setup() = runTest {
val walletAddress = WalletAddressFixture.unified()
newTestSetup(walletAddress)
// Enable substring for ellipsizing
composeTestRule.onNodeWithText(walletAddress.address, substring = true).also {
it.assertExists()
}
}
@Test
@MediumTest
fun back() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.profile_back_content_description)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun address_details() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnAddressDetailsCount())
composeTestRule.onNodeWithText(getStringResource(R.string.profile_see_address_details)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnAddressDetailsCount())
}
@Test
@MediumTest
fun address_book() = runTest {
if (ProfileConfiguration.IS_ADDRESS_BOOK_ENABLED) {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnAddressBookCount())
composeTestRule.onNodeWithText(getStringResource(R.string.profile_address_book)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnAddressBookCount())
}
}
@Test
@MediumTest
fun settings() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnSettingsCount())
composeTestRule.onNodeWithText(getStringResource(R.string.profile_settings)).also {
it.performScrollTo()
it.performClick()
}
assertEquals(1, testSetup.getOnSettingsCount())
}
@Test
@MediumTest
fun coinholder_vote() = runTest {
if (ProfileConfiguration.IS_COINHOLDER_VOTE_ENABLED) {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnCoinholderVoteCount())
composeTestRule.onNodeWithText(getStringResource(R.string.profile_coinholder_vote)).also {
it.performScrollTo()
it.performClick()
}
assertEquals(1, testSetup.getOnCoinholderVoteCount())
}
}
@Test
@MediumTest
fun support() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnSupportCount())
composeTestRule.onNodeWithText(getStringResource(R.string.profile_support)).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
assertEquals(1, testSetup.getOnSupportCount())
}
@Test
@MediumTest
fun about() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnAboutCount())
composeTestRule.onNodeWithText(getStringResource(R.string.profile_about)).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
assertEquals(1, testSetup.getOnAboutCount())
}
private fun newTestSetup(walletAddress: WalletAddress) = TestSetup(composeTestRule, walletAddress)
private class TestSetup(private val composeTestRule: ComposeContentTestRule, walletAddress: WalletAddress) {
private val onBackCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0)
private val onAddressBookCount = AtomicInteger(0)
private val onSettingsCount = AtomicInteger(0)
private val onCoinholderVoteCount = AtomicInteger(0)
private val onSupportCount = AtomicInteger(0)
private val onAboutCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnAddressDetailsCount(): Int {
composeTestRule.waitForIdle()
return onAddressDetailsCount.get()
}
fun getOnAddressBookCount(): Int {
composeTestRule.waitForIdle()
return onAddressBookCount.get()
}
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onSettingsCount.get()
}
fun getOnCoinholderVoteCount(): Int {
composeTestRule.waitForIdle()
return onCoinholderVoteCount.get()
}
fun getOnSupportCount(): Int {
composeTestRule.waitForIdle()
return onSupportCount.get()
}
fun getOnAboutCount(): Int {
composeTestRule.waitForIdle()
return onAboutCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
Profile(
walletAddress,
onBack = {
onBackCount.getAndIncrement()
},
onAddressDetails = {
onAddressDetailsCount.getAndIncrement()
},
onAddressBook = {
onAddressBookCount.getAndIncrement()
},
onSettings = {
onSettingsCount.getAndIncrement()
},
onCoinholderVote = {
onCoinholderVoteCount.getAndIncrement()
},
onSupport = {
onSupportCount.getAndIncrement()
},
onAbout = {
onAboutCount.getAndIncrement()
}
)
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.view
package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@ -17,7 +17,7 @@ import org.junit.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewScreenBrightnessTest : UiTestPrerequisites() {
class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@ -41,15 +41,10 @@ class ProfileViewScreenBrightnessTest : UiTestPrerequisites() {
CompositionLocalProvider(LocalScreenBrightness provides screenBrightness) {
ZcashTheme {
ZcashTheme {
Profile(
Receive(
walletAddress,
onBack = { },
onAddressDetails = { },
onAddressBook = { },
onSettings = { },
onCoinholderVote = {},
onSupport = { },
onAbout = { }
)
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.view
package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@ -17,7 +17,7 @@ import org.junit.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewScreenTimeoutTest : UiTestPrerequisites() {
class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@ -42,15 +42,10 @@ class ProfileViewScreenTimeoutTest : UiTestPrerequisites() {
CompositionLocalProvider(LocalScreenTimeout provides screenTimeout) {
ZcashTheme {
ZcashTheme {
Profile(
Receive(
walletAddress,
onBack = { },
onAddressDetails = { },
onAddressBook = { },
onSettings = { },
onCoinholderVote = {},
onSupport = { },
onAbout = { }
)
}
}

View File

@ -0,0 +1,104 @@
package co.electriccoin.zcash.ui.screen.receive.view
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.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
/*
* Note: It is difficult to test the QR code from automated tests. There is a manual test case
* for that currently. A future enhancement could take a screenshot and try to analyze the
* screenshot contents.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class ReceiveViewTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun setup() = runTest {
val walletAddress = WalletAddressFixture.unified()
newTestSetup(walletAddress)
// Enable substring for ellipsizing
composeTestRule.onNodeWithText(walletAddress.address, substring = true).also {
it.assertExists()
}
}
@Test
@MediumTest
fun back() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.receive_back_content_description)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun address_details() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnAddressDetailsCount())
composeTestRule.onNodeWithText(getStringResource(R.string.receive_see_address_details)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnAddressDetailsCount())
}
private fun newTestSetup(walletAddress: WalletAddress) = TestSetup(composeTestRule, walletAddress)
private class TestSetup(private val composeTestRule: ComposeContentTestRule, walletAddress: WalletAddress) {
private val onBackCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnAddressDetailsCount(): Int {
composeTestRule.waitForIdle()
return onAddressDetailsCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
Receive(
walletAddress,
onBack = {
onBackCount.getAndIncrement()
},
onAddressDetails = {
onAddressDetailsCount.getAndIncrement()
}
)
}
}
}
}
}

View File

@ -6,10 +6,13 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
@ -36,32 +39,24 @@ class SettingsViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun backup() = runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getBackupCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_backup)).also {
it.performClick()
}
assertEquals(1, testSetup.getBackupCount())
}
@Test
@MediumTest
fun rescan() = runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getRescanCount())
if (ConfigurationEntries.IS_RESCAN_ENABLED.getValue(StringConfiguration(emptyMap<String, String>().toPersistentMap(), null))) {
assertEquals(0, testSetup.getRescanCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also {
it.performClick()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.settings_overflow_content_description)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also {
it.performClick()
}
assertEquals(1, testSetup.getRescanCount())
}
assertEquals(1, testSetup.getRescanCount())
}
@Test
@ -152,12 +147,10 @@ class SettingsViewTest : UiTestPrerequisites() {
isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true,
isRescanEnabled = true,
onBack = {
onBackCount.incrementAndGet()
},
onBackupWallet = {
onBackupCount.incrementAndGet()
},
onRescanWallet = {
onRescanCount.incrementAndGet()
},

View File

@ -9,7 +9,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.HOME
import co.electriccoin.zcash.ui.NavigationTargets.PROFILE
import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED
@ -22,7 +22,7 @@ import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.about.WrapAbout
import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses
import co.electriccoin.zcash.ui.screen.home.WrapHome
import co.electriccoin.zcash.ui.screen.profile.WrapProfile
import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.seed.WrapSeed
@ -43,27 +43,18 @@ internal fun MainActivity.Navigation() {
NavHost(navController = navController, startDestination = HOME) {
composable(HOME) {
WrapHome(
goScan = { navController.navigateJustOnce(SCAN) },
goProfile = { navController.navigateJustOnce(PROFILE) },
goSeedPhrase = { navController.navigateJustOnce(SEED) },
goSettings = { navController.navigateJustOnce(SETTINGS) },
goSupport = { navController.navigateJustOnce(SUPPORT) },
goAbout = { navController.navigateJustOnce(ABOUT) },
goReceive = { navController.navigateJustOnce(RECEIVE) },
goSend = { navController.navigateJustOnce(SEND) },
goRequest = { navController.navigateJustOnce(REQUEST) }
)
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
WrapCheckForUpdate()
}
}
composable(PROFILE) {
WrapProfile(
onBack = { navController.popBackStackJustOnce(PROFILE) },
onAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) },
onAddressBook = { },
onSettings = { navController.navigateJustOnce(SETTINGS) },
onCoinholderVote = { },
onSupport = { navController.navigateJustOnce(SUPPORT) },
onAbout = { navController.navigateJustOnce(ABOUT) }
)
}
composable(WALLET_ADDRESS_DETAILS) {
WrapWalletAddresses(
goBack = {
@ -75,9 +66,6 @@ internal fun MainActivity.Navigation() {
WrapSettings(
goBack = {
navController.popBackStackJustOnce(SETTINGS)
},
goWalletBackup = {
navController.navigateJustOnce(SEED)
}
)
}
@ -88,6 +76,12 @@ internal fun MainActivity.Navigation() {
}
)
}
composable(RECEIVE) {
WrapReceive(
onBack = { navController.popBackStackJustOnce(RECEIVE) },
onAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) }
)
}
composable(REQUEST) {
WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) })
}
@ -146,14 +140,14 @@ private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: Strin
object NavigationTargets {
const val HOME = "home"
const val PROFILE = "profile"
const val WALLET_ADDRESS_DETAILS = "wallet_address_details"
const val SETTINGS = "settings"
const val SEED = "seed"
const val RECEIVE = "receive"
const val REQUEST = "request"
const val SEND = "send"

View File

@ -0,0 +1,19 @@
package co.electriccoin.zcash.ui.common
import androidx.compose.material3.DrawerState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
internal fun DrawerState.openDrawerMenu(scope: CoroutineScope) {
if (isOpen) {
return
}
scope.launch { open() }
}
internal fun DrawerState.closeDrawerMenu(scope: CoroutineScope) {
if (isClosed) {
return
}
scope.launch { close() }
}

View File

@ -6,13 +6,13 @@ import co.electriccoin.zcash.configuration.model.entry.ConfigKey
object ConfigurationEntries {
val IS_APP_UPDATE_CHECK_ENABLED = BooleanConfigurationEntry(ConfigKey("is_update_check_enabled"), true)
/*
* Disabled because we don't have the URI parser support in the SDK yet.
*/
val IS_REQUEST_ZEC_ENABLED = BooleanConfigurationEntry(ConfigKey("is_request_zec_enabled"), false)
/*
* The full onboarding flow is functional and tested, but it is disabled by default for an initially minimal feature set.
*/
val IS_FULL_ONBOARDING_ENABLED = BooleanConfigurationEntry(ConfigKey("is_full_onboarding_enabled"), false)
/*
* A troubleshooting step. If we fix our bugs, this should be unnecessary.
*/
val IS_RESCAN_ENABLED = BooleanConfigurationEntry(ConfigKey("is_rescan_enabled"), true)
}

View File

@ -3,16 +3,19 @@
package co.electriccoin.zcash.ui.screen.home
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.common.closeDrawerMenu
import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -21,28 +24,36 @@ import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
@Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapHome(
goScan: () -> Unit,
goProfile: () -> Unit,
goSeedPhrase: () -> Unit,
goSettings: () -> Unit,
goSupport: () -> Unit,
goAbout: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
WrapHome(
this,
goScan = goScan,
goProfile = goProfile,
goSeedPhrase = goSeedPhrase,
goSettings = goSettings,
goSupport = goSupport,
goAbout = goAbout,
goReceive = goReceive,
goSend = goSend,
goRequest = goRequest
)
}
@Composable
@Suppress("LongParameterList")
internal fun WrapHome(
activity: ComponentActivity,
goScan: () -> Unit,
goProfile: () -> Unit,
goSeedPhrase: () -> Unit,
goSettings: () -> Unit,
goSupport: () -> Unit,
goAbout: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
// we want to show information about app update, if available
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
@ -75,20 +86,29 @@ internal fun WrapHome(
val transactionSnapshot = walletViewModel.transactionSnapshot.collectAsStateWithLifecycle().value
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// override Android back navigation action to close drawer, if opened
BackHandler(drawerState.isOpen) {
drawerState.closeDrawerMenu(scope)
}
Home(
walletSnapshot,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isRequestZecButtonEnabled = ConfigurationEntries.IS_REQUEST_ZEC_ENABLED.getValue(RemoteConfig.current),
transactionSnapshot,
goScan = goScan,
goRequest = goRequest,
goSend = goSend,
goProfile = goProfile,
isUpdateAvailable = updateAvailable,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isDebugMenuEnabled = isDebugMenuEnabled,
goSeedPhrase = goSeedPhrase,
goSettings = goSettings,
goSupport = goSupport,
goAbout = goAbout,
goReceive = goReceive,
goSend = goSend,
resetSdk = {
walletViewModel.resetSdk()
},
updateAvailable = updateAvailable
}
)
activity.reportFullyDrawn()

View File

@ -1,10 +1,11 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.home.view
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
@ -16,21 +17,32 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContactSupport
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -48,19 +60,20 @@ import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.crash.android.GlobalCrashReporter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.common.closeDrawerMenu
import co.electriccoin.zcash.ui.common.openDrawerMenu
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.HeaderWithZecIcon
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.home.HomeTag
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import kotlinx.coroutines.CoroutineScope
@Preview
@Composable
@ -68,17 +81,18 @@ fun ComposablePreview() {
ZcashTheme(darkTheme = true) {
GradientSurface {
Home(
WalletSnapshotFixture.new(),
walletSnapshot = WalletSnapshotFixture.new(),
transactionHistory = emptyList(),
isUpdateAvailable = false,
isKeepScreenOnDuringSync = false,
isRequestZecButtonEnabled = false,
emptyList(),
goScan = {},
goProfile = {},
goSend = {},
goRequest = {},
resetSdk = {},
isDebugMenuEnabled = false,
updateAvailable = false
goSeedPhrase = {},
goSettings = {},
goSupport = {},
goAbout = {},
goReceive = {},
goSend = {},
resetSdk = {}
)
}
}
@ -89,43 +103,72 @@ fun ComposablePreview() {
@Composable
fun Home(
walletSnapshot: WalletSnapshot,
isKeepScreenOnDuringSync: Boolean?,
isRequestZecButtonEnabled: Boolean,
transactionHistory: List<CommonTransaction>,
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit,
resetSdk: () -> Unit,
isUpdateAvailable: Boolean,
isKeepScreenOnDuringSync: Boolean?,
isDebugMenuEnabled: Boolean,
updateAvailable: Boolean
goSeedPhrase: () -> Unit,
goSettings: () -> Unit,
goSupport: () -> Unit,
goAbout: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit,
resetSdk: () -> Unit,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
scope: CoroutineScope = rememberCoroutineScope()
) {
Scaffold(topBar = {
HomeTopAppBar(isDebugMenuEnabled, resetSdk)
}) { paddingValues ->
HomeMainContent(
paddingValues,
walletSnapshot,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
isRequestZecButtonEnabled = isRequestZecButtonEnabled,
transactionHistory,
goScan = goScan,
goProfile = goProfile,
goSend = goSend,
goRequest = goRequest,
updateAvailable = updateAvailable
)
}
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
HomeDrawer(
onCloseDrawer = { drawerState.closeDrawerMenu(scope) },
goSeedPhrase = goSeedPhrase,
goSettings = goSettings,
goSupport = goSupport,
goAbout = goAbout
)
},
content = {
Scaffold(topBar = {
HomeTopAppBar(
isDebugMenuEnabled = isDebugMenuEnabled,
openDrawer = { drawerState.openDrawerMenu(scope) },
resetSdk = resetSdk
)
}) { paddingValues ->
HomeMainContent(
paddingValues,
walletSnapshot,
transactionHistory,
isUpdateAvailable = isUpdateAvailable,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
goReceive = goReceive,
goSend = goSend,
)
}
}
)
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun HomeTopAppBar(
isDebugMenuEnabled: Boolean,
openDrawer: () -> Unit,
resetSdk: () -> Unit,
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
navigationIcon = {
IconButton(
onClick = openDrawer
) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = stringResource(R.string.home_menu_content_description)
)
}
},
actions = {
if (isDebugMenuEnabled) {
DebugMenu(resetSdk)
@ -174,55 +217,82 @@ private fun DebugMenu(
}
}
@Composable
private fun HomeDrawer(
onCloseDrawer: () -> Unit,
goSeedPhrase: () -> Unit,
goSettings: () -> Unit,
goSupport: () -> Unit,
goAbout: () -> Unit,
) {
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
NavigationDrawerItem(
icon = { Icon(Icons.Default.Password, contentDescription = null) },
label = { Text(stringResource(id = R.string.home_menu_seed_phrase)) },
selected = false,
onClick = {
onCloseDrawer()
goSeedPhrase()
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
label = { Text(stringResource(id = R.string.home_menu_settings)) },
selected = false,
onClick = {
onCloseDrawer()
goSettings()
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
icon = { Icon(Icons.Default.ContactSupport, contentDescription = null) },
label = { Text(stringResource(id = R.string.home_menu_support)) },
selected = false,
onClick = {
onCloseDrawer()
goSupport()
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
icon = { Icon(Icons.Default.Info, contentDescription = null) },
label = { Text(stringResource(id = R.string.home_menu_about)) },
selected = false,
onClick = {
onCloseDrawer()
goAbout()
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
@Suppress("LongParameterList")
@Composable
private fun HomeMainContent(
paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot,
isKeepScreenOnDuringSync: Boolean?,
isRequestZecButtonEnabled: Boolean,
transactionHistory: List<CommonTransaction>,
goScan: () -> Unit,
goProfile: () -> Unit,
isUpdateAvailable: Boolean,
isKeepScreenOnDuringSync: Boolean?,
goReceive: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit,
updateAvailable: Boolean
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
Row(
Modifier
.fillMaxWidth()
.padding(top = paddingValues.calculateTopPadding())
) {
IconButton(goScan) {
Icon(
imageVector = Icons.Filled.QrCodeScanner,
contentDescription = stringResource(R.string.home_scan_content_description)
)
}
Spacer(
Modifier
.fillMaxWidth()
.weight(MINIMAL_WEIGHT)
)
IconButton(goProfile) {
Icon(
imageVector = Icons.Filled.Person,
contentDescription = stringResource(R.string.home_profile_content_description)
)
}
}
Status(walletSnapshot, updateAvailable)
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(top = paddingValues.calculateTopPadding())
) {
Status(walletSnapshot, isUpdateAvailable)
Spacer(modifier = Modifier.height(24.dp))
PrimaryButton(onClick = goReceive, text = stringResource(R.string.home_button_receive))
PrimaryButton(onClick = goSend, text = stringResource(R.string.home_button_send))
if (isRequestZecButtonEnabled) {
TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request))
}
History(transactionHistory)
if (isKeepScreenOnDuringSync == true && isSyncing(walletSnapshot.status)) {

View File

@ -1,6 +0,0 @@
package co.electriccoin.zcash.ui.screen.profile.util
object ProfileConfiguration {
const val IS_ADDRESS_BOOK_ENABLED = false
const val IS_COINHOLDER_VOTE_ENABLED = false
}

View File

@ -1,6 +1,6 @@
@file:Suppress("ktlint:filename")
package co.electriccoin.zcash.ui.screen.profile
package co.electriccoin.zcash.ui.screen.receive
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
@ -9,82 +9,52 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.profile.view.Profile
import co.electriccoin.zcash.ui.screen.receive.view.Receive
@Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapProfile(
internal fun MainActivity.WrapReceive(
onBack: () -> Unit,
onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) {
WrapProfile(
WrapReceive(
this,
onBack = onBack,
onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport,
onAbout = onAbout
)
}
@Composable
@Suppress("LongParameterList")
internal fun WrapProfile(
internal fun WrapReceive(
activity: ComponentActivity,
onBack: () -> Unit,
onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) {
val viewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value
WrapProfile(
WrapReceive(
walletAddresses,
onBack = onBack,
onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport,
onAbout = onAbout
)
}
@Composable
@Suppress("LongParameterList")
internal fun WrapProfile(
internal fun WrapReceive(
walletAddresses: WalletAddresses?,
onBack: () -> Unit,
onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) {
if (null == walletAddresses) {
// Display loading indicator
} else {
Profile(
Receive(
walletAddresses.unified,
onBack = onBack,
onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport,
onAbout = onAbout
)
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.util
package co.electriccoin.zcash.ui.screen.receive.util
import android.graphics.Bitmap
import android.graphics.Color

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.util
package co.electriccoin.zcash.ui.screen.receive.util
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.util
package co.electriccoin.zcash.ui.screen.receive.util
interface QrCodeGenerator {
/**

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.util
package co.electriccoin.zcash.ui.screen.receive.util
import androidx.compose.ui.graphics.ImageBitmap

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.profile.view
package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
@ -6,7 +6,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -30,11 +29,9 @@ import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.profile.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.profile.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.screen.profile.util.ProfileConfiguration
import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
import kotlinx.coroutines.runBlocking
import kotlin.math.roundToInt
@ -43,15 +40,10 @@ import kotlin.math.roundToInt
fun ComposablePreview() {
ZcashTheme(darkTheme = true) {
GradientSurface {
Profile(
Receive(
walletAddress = runBlocking { WalletAddressFixture.unified() },
onBack = {},
onAddressDetails = {},
onAddressBook = {},
onSettings = {},
onCoinholderVote = {},
onSupport = {},
onAbout = {}
)
}
}
@ -59,44 +51,32 @@ fun ComposablePreview() {
@Composable
@Suppress("LongParameterList")
fun Profile(
fun Receive(
walletAddress: WalletAddress,
onBack: () -> Unit,
onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) {
Column {
ProfileTopAppBar(onBack = onBack)
ProfileContents(
ReceiveTopAppBar(onBack = onBack)
ReceiveContents(
walletAddress = walletAddress,
onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport,
onAbout = onAbout,
isAddressBookEnabled = ProfileConfiguration.IS_ADDRESS_BOOK_ENABLED,
isCoinholderVoteEnabled = ProfileConfiguration.IS_COINHOLDER_VOTE_ENABLED
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun ProfileTopAppBar(onBack: () -> Unit) {
private fun ReceiveTopAppBar(onBack: () -> Unit) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.profile_title)) },
title = { Text(text = stringResource(id = R.string.receive_title)) },
navigationIcon = {
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.profile_back_content_description)
contentDescription = stringResource(R.string.receive_back_content_description)
)
}
}
@ -107,16 +87,9 @@ private val DEFAULT_QR_CODE_SIZE = 320.dp
@Composable
@Suppress("LongParameterList")
private fun ProfileContents(
private fun ReceiveContents(
walletAddress: WalletAddress,
onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit,
isAddressBookEnabled: Boolean,
isCoinholderVoteEnabled: Boolean
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
QrCode(data = walletAddress.address, DEFAULT_QR_CODE_SIZE, Modifier.align(Alignment.CenterHorizontally))
@ -131,17 +104,7 @@ private fun ProfileContents(
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
PrimaryButton(onClick = onAddressDetails, text = stringResource(id = R.string.profile_see_address_details))
if (isAddressBookEnabled) {
TertiaryButton(onClick = onAddressBook, text = stringResource(id = R.string.profile_address_book))
}
TertiaryButton(onClick = onSettings, text = stringResource(id = R.string.profile_settings))
Divider()
if (isCoinholderVoteEnabled) {
TertiaryButton(onClick = onCoinholderVote, text = stringResource(id = R.string.profile_coinholder_vote))
}
TertiaryButton(onClick = onSupport, text = stringResource(id = R.string.profile_support))
TertiaryButton(onClick = onAbout, text = stringResource(id = R.string.profile_about))
PrimaryButton(onClick = onAddressDetails, text = stringResource(id = R.string.receive_see_address_details))
}
}
@ -162,7 +125,7 @@ private fun QrCode(data: String, size: Dp, modifier: Modifier) {
Image(
bitmap = qrCodeImage,
contentDescription = stringResource(R.string.profile_qr_code_content_description),
contentDescription = stringResource(R.string.receive_qr_code_content_description),
modifier = modifier
)
}

View File

@ -24,7 +24,6 @@ import co.electriccoin.zcash.ui.common.SecureScreen
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.ChipGrid
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -94,7 +93,6 @@ private fun SeedMainContent(
.verticalScroll(rememberScrollState())
.padding(top = paddingValues.calculateTopPadding())
) {
Header(stringResource(R.string.seed_header))
Body(stringResource(R.string.seed_body))
ChipGrid(persistableWallet.seedPhrase.split)

View File

@ -7,19 +7,19 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.settings.view.Settings
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@Composable
internal fun MainActivity.WrapSettings(
goBack: () -> Unit,
goWalletBackup: () -> Unit
goBack: () -> Unit
) {
WrapSettings(
activity = this,
goBack = goBack,
goWalletBackup = goWalletBackup
)
}
@ -27,7 +27,6 @@ internal fun MainActivity.WrapSettings(
private fun WrapSettings(
activity: ComponentActivity,
goBack: () -> Unit,
goWalletBackup: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
@ -45,8 +44,8 @@ private fun WrapSettings(
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled,
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
onBack = goBack,
onBackupWallet = goWalletBackup,
onRescanWallet = {
walletViewModel.rescanBlockchain()
},

View File

@ -10,6 +10,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -18,7 +21,11 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -29,8 +36,6 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview("Settings")
@ -42,8 +47,8 @@ fun PreviewSettings() {
isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true,
isRescanEnabled = true,
onBack = {},
onBackupWallet = {},
onRescanWallet = {},
onBackgroundSyncSettingsChanged = {},
onIsKeepScreenOnDuringSyncSettingsChanged = {},
@ -60,23 +65,25 @@ fun Settings(
isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean,
isRescanEnabled: Boolean,
onBack: () -> Unit,
onBackupWallet: () -> Unit,
onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit
) {
Scaffold(topBar = {
SettingsTopAppBar(onBack = onBack)
SettingsTopAppBar(
isRescanEnabled = isRescanEnabled,
onBack = onBack,
onRescanWallet = onRescanWallet
)
}) { paddingValues ->
SettingsMainContent(
paddingValues,
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnDuringSyncEnabled,
isAnalyticsEnabled = isAnalyticsEnabled,
onBackupWallet = onBackupWallet,
onRescanWallet = onRescanWallet,
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged
@ -86,7 +93,11 @@ fun Settings(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun SettingsTopAppBar(onBack: () -> Unit) {
private fun SettingsTopAppBar(
isRescanEnabled: Boolean,
onBack: () -> Unit,
onRescanWallet: () -> Unit
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.settings_header)) },
navigationIcon = {
@ -98,10 +109,38 @@ private fun SettingsTopAppBar(onBack: () -> Unit) {
contentDescription = stringResource(R.string.settings_back_content_description)
)
}
},
actions = {
if (isRescanEnabled) {
TroubleshootingMenu(onRescanWallet)
}
}
)
}
@Composable
private fun TroubleshootingMenu(
onRescanWallet: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.settings_overflow_content_description))
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_rescan)) },
onClick = {
onRescanWallet()
expanded = false
}
)
}
}
@Composable
@Suppress("LongParameterList")
private fun SettingsMainContent(
@ -109,8 +148,6 @@ private fun SettingsMainContent(
isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean,
onBackupWallet: () -> Unit,
onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit
@ -119,8 +156,6 @@ private fun SettingsMainContent(
Modifier
.padding(top = paddingValues.calculateTopPadding())
) {
PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup))
TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan))
SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_background_sync),
state = isBackgroundSyncEnabled,

View File

@ -1,8 +1,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="home_scan_content_description">Scan</string>
<string name="home_profile_content_description">Profile</string>
<string name="home_menu_content_description">Open menu</string>
<string name="home_button_receive">Receive</string>
<string name="home_button_send">Send</string>
<string name="home_button_request">Request ZEC</string>
<string name="home_menu_seed_phrase">My secret phrase</string>
<string name="home_menu_settings">Settings</string>
<string name="home_menu_about">About</string>
<string name="home_menu_support">Contact support</string>
<string name="home_status_syncing_format" formatted="true">Syncing - <xliff:g id="synced_percent" example="50">%1$d</xliff:g>%%</string> <!-- double %% for escaping -->
<string name="home_status_syncing_catchup">Syncing</string>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="profile_title">Profile</string>
<string name="profile_back_content_description">Back</string>
<string name="profile_qr_code_content_description">QR code for unified address</string>
<string name="profile_caption">Your UA Address</string>
<string name="profile_see_address_details">See Address Details</string>
<string name="profile_address_book">Address Book</string>
<string name="profile_settings">Settings</string>
<string name="profile_coinholder_vote">Coinholder Vote</string>
<string name="profile_support">Support</string>
<string name="profile_about">About</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_title">Receive</string>
<string name="receive_back_content_description">Back</string>
<string name="receive_qr_code_content_description">QR code for unified address</string>
<string name="receive_caption">Your UA Address</string>
<string name="receive_see_address_details">See Address Details</string>
</resources>

View File

@ -1,5 +1,5 @@
<resources>
<string name="seed_title">Backup Wallet</string>
<string name="seed_title">My secret phrase</string>
<string name="seed_back_content_description">Back</string>
<string name="seed_header">Your Secret Recovery Phrase</string>

View File

@ -1,6 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="send_title">Send ZEC</string>
<string name="send_back_content_description">Back</string>
<string name="send_scan_content_description">Scan</string>
<string name="send_to">Who would you like to send ZEC to?</string>
<string name="send_amount">How much?</string>
<string name="send_memo">Memo</string>

View File

@ -2,10 +2,8 @@
<string name="settings_header">Settings</string>
<string name="settings_back_content_description">Back</string>
<string name="settings_backup">Backup Wallet</string>
<string name="settings_wipe">Wipe Wallet Data</string>
<string name="settings_rescan">Rescan Blockchain</string>
<string name="settings_overflow_content_description">Additional settings</string>
<string name="settings_rescan">Rescan blockchain</string>
<string name="settings_enable_background_sync">Background sync</string>
<string name="settings_enable_keep_screen_on">Keep screen on during sync</string>
<string name="settings_enable_analytics">Report crashes</string>

View File

@ -1,5 +1,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="support_header">Support</string>
<string name="support_header">Contact support</string>
<string name="support_back_content_description">Back</string>
<string name="support_hint">How can we help?</string>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="wallet_address_title">Wallet Addresses</string>
<string name="wallet_address_title">My wallet address</string>
<string name="wallet_address_back_content_description">Back</string>
<string name="wallet_address_unified">Your Unified Address</string>
<string name="wallet_address_header_includes">which includes</string>

View File

@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package co.electroniccoin.zcash.ui.screenshot
import android.content.Context
@ -230,106 +232,29 @@ class ScreenshotTest : UiTestPrerequisites() {
backupScreenshots(resContext, tag, composeTestRule)
homeScreenshots(resContext, tag, composeTestRule)
// Profile screen
// navigateTo(MainActivity.NAV_PROFILE)
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.home_profile_content_description))).also {
it.assertExists()
it.performClick()
}
profileScreenshots(resContext, tag, composeTestRule)
// Settings is a subscreen of profile
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_settings))).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
settingsScreenshots(resContext, tag, composeTestRule)
// Back to profile
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.settings_back_content_description))).also {
it.assertExists()
it.performClick()
}
// Address Details is a subscreen of profile
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_see_address_details))).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
addressDetailsScreenshots(resContext, tag, composeTestRule)
// Back to profile
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.wallet_address_back_content_description))).also {
it.assertExists()
it.performClick()
}
// Contact Support is a subscreen of profile
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_support))).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
supportScreenshots(resContext, tag, composeTestRule)
// Back to profile
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.support_back_content_description))).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also {
it.assertExists()
}
// About is a subscreen of profile
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_about))).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
aboutScreenshots(resContext, tag, composeTestRule)
// Back to profile
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.about_back_content_description))).also {
it.assertExists()
it.performClick()
}
// Back to home
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.settings_back_content_description))).also {
it.assertExists()
it.performClick()
}
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
if (ConfigurationEntries.IS_REQUEST_ZEC_ENABLED.getValue(emptyConfiguration)) {
composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_request))).also {
it.assertExists()
it.performClick()
}
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
requestZecScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.HOME)
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
}
composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also {
it.assertExists()
it.performScrollTo()
it.performClick()
}
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.synchronizer.value != null }
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.spendingKey.value != null }
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
// These are the buttons on the home screen
navigateTo(NavigationTargets.SEND)
sendZecScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.HOME)
navigateTo(NavigationTargets.RECEIVE)
receiveZecScreenshots(resContext, tag, composeTestRule)
// These are the hamburger menu items
// We could manually click on each one, which is a better integration test but a worse screenshot test
navigateTo(NavigationTargets.SEED)
seedScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.SETTINGS)
settingsScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.SUPPORT)
supportScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.ABOUT)
aboutScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.WALLET_ADDRESS_DETAILS)
addressDetailsScreenshots(resContext, tag, composeTestRule)
}
}
@ -486,17 +411,12 @@ private fun homeScreenshots(resContext: Context, tag: String, composeTestRule: A
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Home 1")
}
}
private fun profileScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
// Note: increased timeout limit to satisfy time needed for SDK initialization
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.addresses.value != null }
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also {
composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.home_menu_content_description))).also {
it.assertExists()
it.performClick()
ScreenshotTest.takeScreenshot(tag, "Home 2 - Menu")
}
ScreenshotTest.takeScreenshot(tag, "Profile 1")
}
private fun settingsScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
@ -515,6 +435,8 @@ private fun addressDetailsScreenshots(resContext: Context, tag: String, composeT
ScreenshotTest.takeScreenshot(tag, "Addresses 1")
}
// This screen is not currently navigable from the app
@Suppress("UnusedPrivateMember")
private fun requestZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(resContext.getString(R.string.request_title))).also {
it.assertExists()
@ -523,7 +445,29 @@ private fun requestZecScreenshots(resContext: Context, tag: String, composeTestR
ScreenshotTest.takeScreenshot(tag, "Request 1")
}
private fun sendZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
private fun receiveZecScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.addresses.value != null }
composeTestRule.onNode(hasText(resContext.getString(R.string.receive_title))).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Receive 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.receive_see_address_details)).also {
it.performClick()
}
composeTestRule.waitForIdle()
ScreenshotTest.takeScreenshot(tag, "Address details")
}
private fun sendZecScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.synchronizer.value != null }
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.spendingKey.value != null }
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
composeTestRule.onNode(hasText(resContext.getString(R.string.send_title))).also {
it.assertExists()
}
@ -564,3 +508,11 @@ private fun aboutScreenshots(resContext: Context, tag: String, composeTestRule:
ScreenshotTest.takeScreenshot(tag, "About 1")
}
private fun seedScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(resContext.getString(R.string.seed_title))).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Seed 1")
}