[#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/common",
"src/main/res/ui/home", "src/main/res/ui/home",
"src/main/res/ui/onboarding", "src/main/res/ui/onboarding",
"src/main/res/ui/profile",
"src/main/res/ui/scan", "src/main/res/ui/scan",
"src/main/res/ui/receive",
"src/main/res/ui/restore", "src/main/res/ui/restore",
"src/main/res/ui/request", "src/main/res/ui/request",
"src/main/res/ui/seed", "src/main/res/ui/seed",

View File

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

View File

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

View File

@ -28,11 +28,7 @@ class HomeViewTest : UiTestPrerequisites() {
fun check_all_elementary_ui_elements_displayed() { fun check_all_elementary_ui_elements_displayed() {
newTestSetup() newTestSetup()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_scan_content_description)).also { composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_menu_content_description)).also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_profile_content_description)).also {
it.assertIsDisplayed() it.assertIsDisplayed()
} }
@ -44,43 +40,21 @@ class HomeViewTest : UiTestPrerequisites() {
it.assertIsDisplayed() it.assertIsDisplayed()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_request)).also { composeTestRule.onNodeWithText(getStringResource(R.string.home_button_receive)).also {
it.assertIsDisplayed() it.assertIsDisplayed()
} }
} }
@Test @Test
@MediumTest @MediumTest
fun hide_request_zec() { fun click_receive_button() {
newTestSetup(isRequestZecButtonEnabled = false)
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_request)).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun click_scan_button() {
val testSetup = newTestSetup() val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnScanCount()) assertEquals(0, testSetup.getOnReceiveCount())
composeTestRule.clickScan() composeTestRule.clickReceive()
assertEquals(1, testSetup.getOnScanCount()) assertEquals(1, testSetup.getOnReceiveCount())
}
@Test
@MediumTest
fun click_profile_button() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnProfileCount())
composeTestRule.clickProfile()
assertEquals(1, testSetup.getOnProfileCount())
} }
@Test @Test
@ -97,33 +71,84 @@ class HomeViewTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
fun click_request_button() { fun hamburger_seed() {
val testSetup = newTestSetup() 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()
} }
private fun newTestSetup(isRequestZecButtonEnabled: Boolean = true) = HomeTestSetup( assertEquals(1, testSetup.getOnSeedCount())
}
@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, composeTestRule,
WalletSnapshotFixture.new(), WalletSnapshotFixture.new()
isRequestZecButtonEnabled = isRequestZecButtonEnabled
).apply { ).apply {
setDefaultContent() setDefaultContent()
} }
} }
fun ComposeContentTestRule.clickScan() { private fun ComposeContentTestRule.openNavigationDrawer() {
onNodeWithContentDescription(getStringResource(R.string.home_scan_content_description)).also { onNodeWithContentDescription(getStringResource(R.string.home_menu_content_description)).also {
it.performClick() it.performClick()
} }
} }
private fun ComposeContentTestRule.clickProfile() { private fun ComposeContentTestRule.clickReceive() {
onNodeWithContentDescription(getStringResource(R.string.home_profile_content_description)).also { onNodeWithText(getStringResource(R.string.home_button_receive)).also {
it.performClick() it.performClick()
} }
} }
@ -134,10 +159,3 @@ private fun ComposeContentTestRule.clickSend() {
it.performClick() 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.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
@ -17,7 +17,7 @@ import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewScreenBrightnessTest : UiTestPrerequisites() { class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@ -41,15 +41,10 @@ class ProfileViewScreenBrightnessTest : UiTestPrerequisites() {
CompositionLocalProvider(LocalScreenBrightness provides screenBrightness) { CompositionLocalProvider(LocalScreenBrightness provides screenBrightness) {
ZcashTheme { ZcashTheme {
ZcashTheme { ZcashTheme {
Profile( Receive(
walletAddress, walletAddress,
onBack = { }, onBack = { },
onAddressDetails = { }, 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.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
@ -17,7 +17,7 @@ import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewScreenTimeoutTest : UiTestPrerequisites() { class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@ -42,15 +42,10 @@ class ProfileViewScreenTimeoutTest : UiTestPrerequisites() {
CompositionLocalProvider(LocalScreenTimeout provides screenTimeout) { CompositionLocalProvider(LocalScreenTimeout provides screenTimeout) {
ZcashTheme { ZcashTheme {
ZcashTheme { ZcashTheme {
Profile( Receive(
walletAddress, walletAddress,
onBack = { }, onBack = { },
onAddressDetails = { }, 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.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
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.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -36,33 +39,25 @@ class SettingsViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getOnBackCount()) 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 @Test
@MediumTest @MediumTest
fun rescan() = runTest { fun rescan() = runTest {
val testSetup = TestSetup(composeTestRule) val testSetup = TestSetup(composeTestRule)
if (ConfigurationEntries.IS_RESCAN_ENABLED.getValue(StringConfiguration(emptyMap<String, String>().toPersistentMap(), null))) {
assertEquals(0, testSetup.getRescanCount()) assertEquals(0, testSetup.getRescanCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.settings_overflow_content_description)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also { composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also {
it.performClick() it.performClick()
} }
assertEquals(1, testSetup.getRescanCount()) assertEquals(1, testSetup.getRescanCount())
} }
}
@Test @Test
@MediumTest @MediumTest
@ -152,12 +147,10 @@ class SettingsViewTest : UiTestPrerequisites() {
isBackgroundSyncEnabled = true, isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true, isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true, isAnalyticsEnabled = true,
isRescanEnabled = true,
onBack = { onBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
}, },
onBackupWallet = {
onBackupCount.incrementAndGet()
},
onRescanWallet = { onRescanWallet = {
onRescanCount.incrementAndGet() onRescanCount.incrementAndGet()
}, },

View File

@ -9,7 +9,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.HOME 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.REQUEST
import co.electriccoin.zcash.ui.NavigationTargets.SCAN import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED 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.about.WrapAbout
import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses
import co.electriccoin.zcash.ui.screen.home.WrapHome 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.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.seed.WrapSeed import co.electriccoin.zcash.ui.screen.seed.WrapSeed
@ -43,27 +43,18 @@ internal fun MainActivity.Navigation() {
NavHost(navController = navController, startDestination = HOME) { NavHost(navController = navController, startDestination = HOME) {
composable(HOME) { composable(HOME) {
WrapHome( WrapHome(
goScan = { navController.navigateJustOnce(SCAN) }, goSeedPhrase = { navController.navigateJustOnce(SEED) },
goProfile = { navController.navigateJustOnce(PROFILE) }, goSettings = { navController.navigateJustOnce(SETTINGS) },
goSupport = { navController.navigateJustOnce(SUPPORT) },
goAbout = { navController.navigateJustOnce(ABOUT) },
goReceive = { navController.navigateJustOnce(RECEIVE) },
goSend = { navController.navigateJustOnce(SEND) }, goSend = { navController.navigateJustOnce(SEND) },
goRequest = { navController.navigateJustOnce(REQUEST) }
) )
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) { if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
WrapCheckForUpdate() 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) { composable(WALLET_ADDRESS_DETAILS) {
WrapWalletAddresses( WrapWalletAddresses(
goBack = { goBack = {
@ -75,9 +66,6 @@ internal fun MainActivity.Navigation() {
WrapSettings( WrapSettings(
goBack = { goBack = {
navController.popBackStackJustOnce(SETTINGS) 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) { composable(REQUEST) {
WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) }) WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) })
} }
@ -146,14 +140,14 @@ private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: Strin
object NavigationTargets { object NavigationTargets {
const val HOME = "home" const val HOME = "home"
const val PROFILE = "profile"
const val WALLET_ADDRESS_DETAILS = "wallet_address_details" const val WALLET_ADDRESS_DETAILS = "wallet_address_details"
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val SEED = "seed" const val SEED = "seed"
const val RECEIVE = "receive"
const val REQUEST = "request" const val REQUEST = "request"
const val SEND = "send" 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 { object ConfigurationEntries {
val IS_APP_UPDATE_CHECK_ENABLED = BooleanConfigurationEntry(ConfigKey("is_update_check_enabled"), true) 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. * 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) 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 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.material3.DrawerValue
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.spackle.EmulatorWtfUtil import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.BuildConfig import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.common.closeDrawerMenu
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.home.view.Home 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.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel 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 import co.electriccoin.zcash.ui.screen.update.model.UpdateState
@Composable @Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapHome( internal fun MainActivity.WrapHome(
goScan: () -> Unit, goSeedPhrase: () -> Unit,
goProfile: () -> Unit, goSettings: () -> Unit,
goSupport: () -> Unit,
goAbout: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit, goSend: () -> Unit,
goRequest: () -> Unit
) { ) {
WrapHome( WrapHome(
this, this,
goScan = goScan, goSeedPhrase = goSeedPhrase,
goProfile = goProfile, goSettings = goSettings,
goSupport = goSupport,
goAbout = goAbout,
goReceive = goReceive,
goSend = goSend, goSend = goSend,
goRequest = goRequest
) )
} }
@Composable @Composable
@Suppress("LongParameterList")
internal fun WrapHome( internal fun WrapHome(
activity: ComponentActivity, activity: ComponentActivity,
goScan: () -> Unit, goSeedPhrase: () -> Unit,
goProfile: () -> Unit, goSettings: () -> Unit,
goSupport: () -> Unit,
goAbout: () -> Unit,
goReceive: () -> Unit,
goSend: () -> Unit, goSend: () -> Unit,
goRequest: () -> Unit
) { ) {
// we want to show information about app update, if available // we want to show information about app update, if available
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> { val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
@ -75,20 +86,29 @@ internal fun WrapHome(
val transactionSnapshot = walletViewModel.transactionSnapshot.collectAsStateWithLifecycle().value 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( Home(
walletSnapshot, walletSnapshot,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isRequestZecButtonEnabled = ConfigurationEntries.IS_REQUEST_ZEC_ENABLED.getValue(RemoteConfig.current),
transactionSnapshot, transactionSnapshot,
goScan = goScan, isUpdateAvailable = updateAvailable,
goRequest = goRequest, isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
goSend = goSend,
goProfile = goProfile,
isDebugMenuEnabled = isDebugMenuEnabled, isDebugMenuEnabled = isDebugMenuEnabled,
goSeedPhrase = goSeedPhrase,
goSettings = goSettings,
goSupport = goSupport,
goAbout = goAbout,
goReceive = goReceive,
goSend = goSend,
resetSdk = { resetSdk = {
walletViewModel.resetSdk() walletViewModel.resetSdk()
}, }
updateAvailable = updateAvailable
) )
activity.reportFullyDrawn() activity.reportFullyDrawn()

View File

@ -1,10 +1,11 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.home.view package co.electriccoin.zcash.ui.screen.home.view
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth 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.rememberScrollState
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.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.MoreVert
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api 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.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.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
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
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.crash.android.GlobalCrashReporter
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.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.Body
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol 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.HeaderWithZecIcon
import co.electriccoin.zcash.ui.design.component.PrimaryButton 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.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.home.HomeTag 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.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import kotlinx.coroutines.CoroutineScope
@Preview @Preview
@Composable @Composable
@ -68,17 +81,18 @@ fun ComposablePreview() {
ZcashTheme(darkTheme = true) { ZcashTheme(darkTheme = true) {
GradientSurface { GradientSurface {
Home( Home(
WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
transactionHistory = emptyList(),
isUpdateAvailable = false,
isKeepScreenOnDuringSync = false, isKeepScreenOnDuringSync = false,
isRequestZecButtonEnabled = false,
emptyList(),
goScan = {},
goProfile = {},
goSend = {},
goRequest = {},
resetSdk = {},
isDebugMenuEnabled = false, isDebugMenuEnabled = false,
updateAvailable = false goSeedPhrase = {},
goSettings = {},
goSupport = {},
goAbout = {},
goReceive = {},
goSend = {},
resetSdk = {}
) )
} }
} }
@ -89,43 +103,72 @@ fun ComposablePreview() {
@Composable @Composable
fun Home( fun Home(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnDuringSync: Boolean?,
isRequestZecButtonEnabled: Boolean,
transactionHistory: List<CommonTransaction>, transactionHistory: List<CommonTransaction>,
goScan: () -> Unit, isUpdateAvailable: Boolean,
goProfile: () -> Unit, isKeepScreenOnDuringSync: Boolean?,
goSend: () -> Unit,
goRequest: () -> Unit,
resetSdk: () -> Unit,
isDebugMenuEnabled: 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()
) { ) {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
HomeDrawer(
onCloseDrawer = { drawerState.closeDrawerMenu(scope) },
goSeedPhrase = goSeedPhrase,
goSettings = goSettings,
goSupport = goSupport,
goAbout = goAbout
)
},
content = {
Scaffold(topBar = { Scaffold(topBar = {
HomeTopAppBar(isDebugMenuEnabled, resetSdk) HomeTopAppBar(
isDebugMenuEnabled = isDebugMenuEnabled,
openDrawer = { drawerState.openDrawerMenu(scope) },
resetSdk = resetSdk
)
}) { paddingValues -> }) { paddingValues ->
HomeMainContent( HomeMainContent(
paddingValues, paddingValues,
walletSnapshot, walletSnapshot,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
isRequestZecButtonEnabled = isRequestZecButtonEnabled,
transactionHistory, transactionHistory,
goScan = goScan, isUpdateAvailable = isUpdateAvailable,
goProfile = goProfile, isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
goReceive = goReceive,
goSend = goSend, goSend = goSend,
goRequest = goRequest,
updateAvailable = updateAvailable
) )
} }
}
)
} }
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
private fun HomeTopAppBar( private fun HomeTopAppBar(
isDebugMenuEnabled: Boolean, isDebugMenuEnabled: Boolean,
openDrawer: () -> Unit,
resetSdk: () -> Unit, resetSdk: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) }, 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 = { actions = {
if (isDebugMenuEnabled) { if (isDebugMenuEnabled) {
DebugMenu(resetSdk) 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") @Suppress("LongParameterList")
@Composable @Composable
private fun HomeMainContent( private fun HomeMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnDuringSync: Boolean?,
isRequestZecButtonEnabled: Boolean,
transactionHistory: List<CommonTransaction>, transactionHistory: List<CommonTransaction>,
goScan: () -> Unit, isUpdateAvailable: Boolean,
goProfile: () -> Unit, isKeepScreenOnDuringSync: Boolean?,
goReceive: () -> Unit,
goSend: () -> Unit, goSend: () -> Unit,
goRequest: () -> Unit,
updateAvailable: Boolean
) { ) {
Column(Modifier.verticalScroll(rememberScrollState())) { Column(
Row(
Modifier Modifier
.fillMaxWidth() .verticalScroll(rememberScrollState())
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding())
) { ) {
IconButton(goScan) { Status(walletSnapshot, isUpdateAvailable)
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)
Spacer(modifier = Modifier.height(24.dp)) 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)) PrimaryButton(onClick = goSend, text = stringResource(R.string.home_button_send))
if (isRequestZecButtonEnabled) {
TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request))
}
History(transactionHistory) History(transactionHistory)
if (isKeepScreenOnDuringSync == true && isSyncing(walletSnapshot.status)) { 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") @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.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
@ -9,82 +9,52 @@ 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.MainActivity
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel 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 @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal fun MainActivity.WrapProfile( internal fun MainActivity.WrapReceive(
onBack: () -> Unit, onBack: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) { ) {
WrapProfile( WrapReceive(
this, this,
onBack = onBack, onBack = onBack,
onAddressDetails = onAddressDetails, onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport,
onAbout = onAbout
) )
} }
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal fun WrapProfile( internal fun WrapReceive(
activity: ComponentActivity, activity: ComponentActivity,
onBack: () -> Unit, onBack: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) { ) {
val viewModel by activity.viewModels<WalletViewModel>() val viewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value
WrapProfile( WrapReceive(
walletAddresses, walletAddresses,
onBack = onBack, onBack = onBack,
onAddressDetails = onAddressDetails, onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport,
onAbout = onAbout
) )
} }
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal fun WrapProfile( internal fun WrapReceive(
walletAddresses: WalletAddresses?, walletAddresses: WalletAddresses?,
onBack: () -> Unit, onBack: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) { ) {
if (null == walletAddresses) { if (null == walletAddresses) {
// Display loading indicator // Display loading indicator
} else { } else {
Profile( Receive(
walletAddresses.unified, walletAddresses.unified,
onBack = onBack, onBack = onBack,
onAddressDetails = onAddressDetails, 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.Bitmap
import android.graphics.Color 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.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter 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 { 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 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.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -6,7 +6,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api 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
@ -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.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.profile.util.AndroidQrCodeImageGenerator import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.profile.util.JvmQrCodeGenerator import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.screen.profile.util.ProfileConfiguration
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -43,15 +40,10 @@ import kotlin.math.roundToInt
fun ComposablePreview() { fun ComposablePreview() {
ZcashTheme(darkTheme = true) { ZcashTheme(darkTheme = true) {
GradientSurface { GradientSurface {
Profile( Receive(
walletAddress = runBlocking { WalletAddressFixture.unified() }, walletAddress = runBlocking { WalletAddressFixture.unified() },
onBack = {}, onBack = {},
onAddressDetails = {}, onAddressDetails = {},
onAddressBook = {},
onSettings = {},
onCoinholderVote = {},
onSupport = {},
onAbout = {}
) )
} }
} }
@ -59,44 +51,32 @@ fun ComposablePreview() {
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun Profile( fun Receive(
walletAddress: WalletAddress, walletAddress: WalletAddress,
onBack: () -> Unit, onBack: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit
) { ) {
Column { Column {
ProfileTopAppBar(onBack = onBack) ReceiveTopAppBar(onBack = onBack)
ProfileContents( ReceiveContents(
walletAddress = walletAddress, walletAddress = walletAddress,
onAddressDetails = onAddressDetails, 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 @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
private fun ProfileTopAppBar(onBack: () -> Unit) { private fun ReceiveTopAppBar(onBack: () -> Unit) {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.profile_title)) }, title = { Text(text = stringResource(id = R.string.receive_title)) },
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = onBack onClick = onBack
) { ) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, 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 @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun ProfileContents( private fun ReceiveContents(
walletAddress: WalletAddress, walletAddress: WalletAddress,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit,
onAbout: () -> Unit,
isAddressBookEnabled: Boolean,
isCoinholderVoteEnabled: Boolean
) { ) {
Column(Modifier.verticalScroll(rememberScrollState())) { Column(Modifier.verticalScroll(rememberScrollState())) {
QrCode(data = walletAddress.address, DEFAULT_QR_CODE_SIZE, Modifier.align(Alignment.CenterHorizontally)) QrCode(data = walletAddress.address, DEFAULT_QR_CODE_SIZE, Modifier.align(Alignment.CenterHorizontally))
@ -131,17 +104,7 @@ private fun ProfileContents(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
) )
PrimaryButton(onClick = onAddressDetails, text = stringResource(id = R.string.profile_see_address_details)) PrimaryButton(onClick = onAddressDetails, text = stringResource(id = R.string.receive_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))
} }
} }
@ -162,7 +125,7 @@ private fun QrCode(data: String, size: Dp, modifier: Modifier) {
Image( Image(
bitmap = qrCodeImage, bitmap = qrCodeImage,
contentDescription = stringResource(R.string.profile_qr_code_content_description), contentDescription = stringResource(R.string.receive_qr_code_content_description),
modifier = modifier 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.Body
import co.electriccoin.zcash.ui.design.component.ChipGrid import co.electriccoin.zcash.ui.design.component.ChipGrid
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.TertiaryButton import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -94,7 +93,6 @@ private fun SeedMainContent(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding())
) { ) {
Header(stringResource(R.string.seed_header))
Body(stringResource(R.string.seed_body)) Body(stringResource(R.string.seed_body))
ChipGrid(persistableWallet.seedPhrase.split) ChipGrid(persistableWallet.seedPhrase.split)

View File

@ -7,19 +7,19 @@ 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.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.home.viewmodel.WalletViewModel
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
@Composable @Composable
internal fun MainActivity.WrapSettings( internal fun MainActivity.WrapSettings(
goBack: () -> Unit, goBack: () -> Unit
goWalletBackup: () -> Unit
) { ) {
WrapSettings( WrapSettings(
activity = this, activity = this,
goBack = goBack, goBack = goBack,
goWalletBackup = goWalletBackup
) )
} }
@ -27,7 +27,6 @@ internal fun MainActivity.WrapSettings(
private fun WrapSettings( private fun WrapSettings(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit, goBack: () -> Unit,
goWalletBackup: () -> Unit
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>() val settingsViewModel by activity.viewModels<SettingsViewModel>()
@ -45,8 +44,8 @@ private fun WrapSettings(
isBackgroundSyncEnabled = isBackgroundSyncEnabled, isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing, isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled, isAnalyticsEnabled = isAnalyticsEnabled,
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
onBack = goBack, onBack = goBack,
onBackupWallet = goWalletBackup,
onRescanWallet = { onRescanWallet = {
walletViewModel.rescanBlockchain() walletViewModel.rescanBlockchain()
}, },

View File

@ -10,6 +10,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -18,7 +21,11 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.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
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.design.theme.ZcashTheme
@Preview("Settings") @Preview("Settings")
@ -42,8 +47,8 @@ fun PreviewSettings() {
isBackgroundSyncEnabled = true, isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true, isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true, isAnalyticsEnabled = true,
isRescanEnabled = true,
onBack = {}, onBack = {},
onBackupWallet = {},
onRescanWallet = {}, onRescanWallet = {},
onBackgroundSyncSettingsChanged = {}, onBackgroundSyncSettingsChanged = {},
onIsKeepScreenOnDuringSyncSettingsChanged = {}, onIsKeepScreenOnDuringSyncSettingsChanged = {},
@ -60,23 +65,25 @@ fun Settings(
isBackgroundSyncEnabled: Boolean, isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean, isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean, isAnalyticsEnabled: Boolean,
isRescanEnabled: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
onBackupWallet: () -> Unit,
onRescanWallet: () -> Unit, onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit, onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit, onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit onAnalyticsSettingsChanged: (Boolean) -> Unit
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
SettingsTopAppBar(onBack = onBack) SettingsTopAppBar(
isRescanEnabled = isRescanEnabled,
onBack = onBack,
onRescanWallet = onRescanWallet
)
}) { paddingValues -> }) { paddingValues ->
SettingsMainContent( SettingsMainContent(
paddingValues, paddingValues,
isBackgroundSyncEnabled = isBackgroundSyncEnabled, isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnDuringSyncEnabled, isKeepScreenOnDuringSyncEnabled = isKeepScreenOnDuringSyncEnabled,
isAnalyticsEnabled = isAnalyticsEnabled, isAnalyticsEnabled = isAnalyticsEnabled,
onBackupWallet = onBackupWallet,
onRescanWallet = onRescanWallet,
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged, onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged, onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged onAnalyticsSettingsChanged = onAnalyticsSettingsChanged
@ -86,7 +93,11 @@ fun Settings(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
private fun SettingsTopAppBar(onBack: () -> Unit) { private fun SettingsTopAppBar(
isRescanEnabled: Boolean,
onBack: () -> Unit,
onRescanWallet: () -> Unit
) {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.settings_header)) }, title = { Text(text = stringResource(id = R.string.settings_header)) },
navigationIcon = { navigationIcon = {
@ -98,10 +109,38 @@ private fun SettingsTopAppBar(onBack: () -> Unit) {
contentDescription = stringResource(R.string.settings_back_content_description) 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 @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun SettingsMainContent( private fun SettingsMainContent(
@ -109,8 +148,6 @@ private fun SettingsMainContent(
isBackgroundSyncEnabled: Boolean, isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean, isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean, isAnalyticsEnabled: Boolean,
onBackupWallet: () -> Unit,
onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit, onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit, onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit onAnalyticsSettingsChanged: (Boolean) -> Unit
@ -119,8 +156,6 @@ private fun SettingsMainContent(
Modifier Modifier
.padding(top = paddingValues.calculateTopPadding()) .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( SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_background_sync), label = stringResource(id = R.string.settings_enable_background_sync),
state = isBackgroundSyncEnabled, state = isBackgroundSyncEnabled,

View File

@ -1,8 +1,13 @@
<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_scan_content_description">Scan</string> <string name="home_menu_content_description">Open menu</string>
<string name="home_profile_content_description">Profile</string> <string name="home_button_receive">Receive</string>
<string name="home_button_send">Send</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_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> <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> <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_back_content_description">Back</string>
<string name="seed_header">Your Secret Recovery Phrase</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"> <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_title">Send ZEC</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_to">Who would you like to send ZEC to?</string> <string name="send_to">Who would you like to send ZEC to?</string>
<string name="send_amount">How much?</string> <string name="send_amount">How much?</string>
<string name="send_memo">Memo</string> <string name="send_memo">Memo</string>

View File

@ -2,10 +2,8 @@
<string name="settings_header">Settings</string> <string name="settings_header">Settings</string>
<string name="settings_back_content_description">Back</string> <string name="settings_back_content_description">Back</string>
<string name="settings_backup">Backup Wallet</string> <string name="settings_overflow_content_description">Additional settings</string>
<string name="settings_wipe">Wipe Wallet Data</string> <string name="settings_rescan">Rescan blockchain</string>
<string name="settings_rescan">Rescan Blockchain</string>
<string name="settings_enable_background_sync">Background sync</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_keep_screen_on">Keep screen on during sync</string>
<string name="settings_enable_analytics">Report crashes</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"> <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_back_content_description">Back</string>
<string name="support_hint">How can we help?</string> <string name="support_hint">How can we help?</string>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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_back_content_description">Back</string>
<string name="wallet_address_unified">Your Unified Address</string> <string name="wallet_address_unified">Your Unified Address</string>
<string name="wallet_address_header_includes">which includes</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 package co.electroniccoin.zcash.ui.screenshot
import android.content.Context import android.content.Context
@ -230,106 +232,29 @@ class ScreenshotTest : UiTestPrerequisites() {
backupScreenshots(resContext, tag, composeTestRule) backupScreenshots(resContext, tag, composeTestRule)
homeScreenshots(resContext, tag, composeTestRule) homeScreenshots(resContext, tag, composeTestRule)
// Profile screen // These are the buttons on the home screen
// navigateTo(MainActivity.NAV_PROFILE) navigateTo(NavigationTargets.SEND)
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 }
sendZecScreenshots(resContext, tag, composeTestRule) 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() it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Home 1") ScreenshotTest.takeScreenshot(tag, "Home 1")
} }
}
private fun profileScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.home_menu_content_description))).also {
// 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 {
it.assertExists() it.assertExists()
it.performClick()
ScreenshotTest.takeScreenshot(tag, "Home 2 - Menu")
} }
ScreenshotTest.takeScreenshot(tag, "Profile 1")
} }
private fun settingsScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { 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") 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) { private fun requestZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(resContext.getString(R.string.request_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.request_title))).also {
it.assertExists() it.assertExists()
@ -523,7 +445,29 @@ private fun requestZecScreenshots(resContext: Context, tag: String, composeTestR
ScreenshotTest.takeScreenshot(tag, "Request 1") 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 { composeTestRule.onNode(hasText(resContext.getString(R.string.send_title))).also {
it.assertExists() it.assertExists()
} }
@ -564,3 +508,11 @@ private fun aboutScreenshots(resContext: Context, tag: String, composeTestRule:
ScreenshotTest.takeScreenshot(tag, "About 1") 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")
}