[#947] About screen UI/logic/tests

* [#947] About screen UI (partially implemented)

* [#947] About screen UI

* [#947] Fixed failing test case

* [#947] Added Zashi text logo

---------

Co-authored-by: Honza Rychnovský <rychnovsky.honza@gmail.com>

* [#947] UI: About Screen

* [#1005] Remove DebugMenu from Home screen

- The new design does not require it on the Home screen
- Partially moved to the Settings and About screens

* [#392] Update About screen texts

Closes #392 as not needed now

* [#1006] Enhance VersionInfo with other fields

* [#947] About screen UI + logic + tests

* Adopt VersionInfo on different screens

* VersionInfo test

Update .gitignore

---------

Co-authored-by: Venkat-corebts <143575548+Venkat-corebts@users.noreply.github.com>
This commit is contained in:
Honza Rychnovský 2023-10-17 15:56:04 +02:00 committed by GitHub
parent e871c4eb45
commit 16bf1afa90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 413 additions and 281 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ local.properties
/.idea/androidTestResultsUserPreferences.xml
google-services.json
/.idea/kotlinc.xml
/.idea/other.xml

View File

@ -6,7 +6,6 @@ import co.electriccoin.zcash.configuration.test.fixture.BooleanDefaultEntryFixtu
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
@ -34,7 +33,6 @@ class MergingConfigurationProviderTest {
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun getFlow_ordering() = runTest {
val configurationProvider = MergingConfigurationProvider(
persistentListOf(
@ -53,7 +51,6 @@ class MergingConfigurationProviderTest {
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun getFlow_empty() = runTest {
val configurationProvider = MergingConfigurationProvider(
emptyList<ConfigurationProvider>().toPersistentList()
@ -65,7 +62,6 @@ class MergingConfigurationProviderTest {
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun getUpdatedAt_newest() = runTest {
val older = "2023-01-15T08:38:45.415Z".toInstant()
val newer = "2023-01-17T08:38:45.415Z".toInstant()

View File

@ -27,6 +27,7 @@ data class Dimens(
val defaultButtonHeight: Dp,
val zcashLogoHeight: Dp,
val zcashLogoWidth: Dp,
val zcashTextLogoHeight: Dp
)
private val defaultDimens = Dimens(
@ -45,6 +46,7 @@ private val defaultDimens = Dimens(
defaultButtonHeight = 50.dp,
zcashLogoHeight = 100.dp,
zcashLogoWidth = 60.dp,
zcashTextLogoHeight = 30.dp
)
private val normalDimens = defaultDimens

View File

@ -31,6 +31,7 @@ data class ExtendedColors(
val disabledButtonTextColor: Color,
val buttonShadowColor: Color,
val screenTitleColor: Color,
val aboutTextColor: Color,
val welcomeAnimationColor: Color,
) {
@Composable

View File

@ -60,6 +60,9 @@ internal object Dark {
val buttonShadowColor = Color(0xFFFFFFFF)
// to be added later
val aboutTextColor = Color.Unspecified
val screenTitleColor = Color(0xFF040404)
val welcomeAnimationColor = Color(0xFF231F20)
@ -122,6 +125,8 @@ internal object Light {
val screenTitleColor = Color(0xFF040404)
val aboutTextColor = Color(0xFF4E4E4E)
val welcomeAnimationColor = Color(0xFF231F20)
}
@ -171,6 +176,7 @@ internal val DarkExtendedColorPalette = ExtendedColors(
reference = Dark.reference,
buttonShadowColor = Dark.buttonShadowColor,
screenTitleColor = Dark.screenTitleColor,
aboutTextColor = Dark.aboutTextColor,
welcomeAnimationColor = Dark.welcomeAnimationColor
)
@ -198,6 +204,7 @@ internal val LightExtendedColorPalette = ExtendedColors(
reference = Light.reference,
buttonShadowColor = Light.buttonShadowColor,
screenTitleColor = Light.screenTitleColor,
aboutTextColor = Light.aboutTextColor,
welcomeAnimationColor = Light.welcomeAnimationColor
)
@ -227,6 +234,7 @@ internal val LocalExtendedColors = staticCompositionLocalOf {
reference = Color.Unspecified,
buttonShadowColor = Color.Unspecified,
screenTitleColor = Color.Unspecified,
aboutTextColor = Color.Unspecified,
welcomeAnimationColor = Color.Unspecified,
)
}

View File

@ -112,6 +112,7 @@ data class ExtendedTypography(
val chipIndex: TextStyle,
val listItem: TextStyle,
val zecBalance: TextStyle,
val aboutText: TextStyle,
val buttonText: TextStyle,
val checkboxText: TextStyle,
val securityWarningText: TextStyle
@ -141,6 +142,10 @@ val LocalExtendedTypography = staticCompositionLocalOf {
fontWeight = FontWeight.Normal,
fontSize = 30.sp
),
aboutText = PrimaryTypography.bodyLarge.copy(
fontSize = 14.sp,
lineHeight = 20.sp
),
buttonText = PrimaryTypography.bodySmall.copy(
fontSize = 14.sp
),

View File

@ -0,0 +1,25 @@
package co.electriccoin.zcash.ui.common.model
import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.test.getAppContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
class VersionInfoTest {
@Test
@SmallTest
fun sanity_check_version_info_in_testing() {
val versionInfo = VersionInfo.new(getAppContext())
// We expect some VersionInfo object parameters to be empty during the testing
// isDebuggable is not tested as it's not static during UI testing in CI or locally
assertEquals("null", versionInfo.versionName)
assertEquals(0, versionInfo.versionCode)
assertNotEquals(versionInfo.gitSha, "")
assertTrue(versionInfo.gitCommitCount >= 1)
}
}

View File

@ -1,84 +0,0 @@
package co.electriccoin.zcash.ui.screen.about
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 co.electriccoin.zcash.build.gitSha
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
import co.electriccoin.zcash.ui.screen.about.view.About
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
class AboutViewTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun setup() {
newTestSetup()
composeTestRule.onNodeWithText(VersionInfoFixture.VERSION_NAME, substring = true).also {
it.assertExists()
}
composeTestRule.onNodeWithText(gitSha, substring = true).also {
it.assertExists()
}
}
@Test
@MediumTest
fun back() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.about_back_content_description)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
private fun newTestSetup() = TestSetup(
composeTestRule,
VersionInfoFixture.new(),
ConfigInfoFixture.new()
)
private class TestSetup(
private val composeTestRule: ComposeContentTestRule,
versionInfo: VersionInfo,
configInfo: ConfigInfo
) {
private val onBackCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
About(versionInfo = versionInfo, configInfo = configInfo) {
onBackCount.incrementAndGet()
}
}
}
}
}
}

View File

@ -0,0 +1,97 @@
package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.AboutTag
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule
import kotlin.test.Test
import kotlin.test.assertEquals
class AboutViewTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun default_ui_state_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithText(getStringResource(R.string.about_back).uppercase()).also {
it.assertExists()
}
composeTestRule.onNodeWithContentDescription(
label = getStringResource(R.string.about_app_logo_content_description)
).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.about_description)).also {
it.assertExists()
}
}
@Test
@MediumTest
fun version_setup_test() {
newTestSetup()
composeTestRule.onNodeWithText(VersionInfoFixture.VERSION_NAME, substring = true).also {
it.assertExists()
}
composeTestRule.onNodeWithText(VersionInfoFixture.VERSION_CODE.toString(), substring = true).also {
it.assertExists()
}
}
@Test
@MediumTest
fun back_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.about_back_content_description)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@SmallTest
fun debug_menu_visible_test() {
newTestSetup(isDebuggable = true)
composeTestRule.onNodeWithTag(AboutTag.DEBUG_MENU_TAG).also {
it.assertExists()
}
}
@Test
@SmallTest
fun debug_menu_not_visible_test() {
newTestSetup(isDebuggable = false)
composeTestRule.onNodeWithTag(AboutTag.DEBUG_MENU_TAG).also {
it.assertDoesNotExist()
}
}
private fun newTestSetup(isDebuggable: Boolean = false) = AboutViewTestSetup(
composeTestRule,
VersionInfoFixture.new(isDebuggable = isDebuggable),
ConfigInfoFixture.new()
)
}

View File

@ -0,0 +1,33 @@
package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
import java.util.concurrent.atomic.AtomicInteger
class AboutViewTestSetup(
private val composeTestRule: ComposeContentTestRule,
versionInfo: VersionInfo,
configInfo: ConfigInfo
) {
private val onBackCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
About(
onBack = { onBackCount.incrementAndGet() },
versionInfo = versionInfo,
configInfo = configInfo
)
}
}
}
}

View File

@ -71,7 +71,6 @@ class HomeTestSetup(
isKeepScreenOnDuringSync = false,
isFiatConversionEnabled = isShowFiatConversion,
isCircularProgressBarEnabled = isCircularProgressBar,
isDebugMenuEnabled = false,
goSettings = {
onSettingsCount.incrementAndGet()
},
@ -93,7 +92,6 @@ class HomeTestSetup(
goHistory = {
onHistoryCount.incrementAndGet()
},
resetSdk = {},
drawerState = drawerValues.drawerState,
scope = drawerValues.scope
)

View File

@ -3,10 +3,8 @@ package co.electriccoin.zcash.ui.screen.securitywarning.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.spackle.getPackageInfoCompat
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@ -50,9 +48,7 @@ class SecurityWarningViewTestSetup(private val composeTestRule: ComposeContentTe
onConfirm = {
onConfirmCount.incrementAndGet()
},
versionInfo = VersionInfo.new(
getAppContext().packageManager.getPackageInfoCompat(getAppContext().packageName, 0L)
)
versionInfo = VersionInfoFixture.new()
)
}

View File

@ -74,6 +74,9 @@ internal fun MainActivity.Navigation() {
WrapSettings(
goBack = {
navController.popBackStackJustOnce(SETTINGS)
},
goAbout = {
navController.navigateJustOnce(ABOUT)
}
)
}

View File

@ -0,0 +1,37 @@
package co.electriccoin.zcash.ui.common.model
import android.content.Context
import android.content.pm.ApplicationInfo
import co.electriccoin.zcash.build.gitCommitCount
import co.electriccoin.zcash.build.gitSha
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.spackle.getPackageInfoCompat
import co.electriccoin.zcash.spackle.versionCodeCompat
data class VersionInfo(
val versionName: String,
val versionCode: Long,
val isDebuggable: Boolean,
val gitSha: String,
val gitCommitCount: Long
) {
companion object {
fun new(context: Context): VersionInfo {
val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0L)
val applicationInfo = context.applicationInfo
return VersionInfo(
versionName = packageInfo.versionName ?: "null", // Should only be null during tests
versionCode = packageInfo.versionCodeCompat, // Should only be 0 during tests
isDebuggable = (
(0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) &&
!FirebaseTestLabUtil.isFirebaseTestLab(context.applicationContext) &&
!EmulatorWtfUtil.isEmulatorWtf(context.applicationContext)
),
gitSha = gitSha,
gitCommitCount = gitCommitCount.toLong()
)
}
}
}

View File

@ -1,15 +1,27 @@
package co.electriccoin.zcash.ui.fixture
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.VersionInfo
// Magic Number doesn't matter here for hard-coded fixture values
@Suppress("MagicNumber")
object VersionInfoFixture {
const val VERSION_NAME = "1.0.3"
const val VERSION_CODE = 3L
const val VERSION_NAME = "1.0.0"
const val VERSION_CODE = 1L
const val IS_DEBUGGABLE = false
const val GIT_SHA = "635dac0eb9ddc2bc6da5177f0dd495d8b76af4dc"
const val GIT_COMMIT_COUNT = 1L
fun new(
versionName: String = VERSION_NAME,
versionCode: Long = VERSION_CODE
) = VersionInfo(versionName, versionCode)
versionCode: Long = VERSION_CODE,
isDebuggable: Boolean = IS_DEBUGGABLE,
gitSha: String = GIT_SHA,
gitCommitCount: Long = GIT_COMMIT_COUNT
) = VersionInfo(
versionName,
versionCode,
isDebuggable,
gitSha,
gitCommitCount
)
}

View File

@ -0,0 +1,8 @@
package co.electriccoin.zcash.ui.screen.about
/**
* These are only used for automated testing.
*/
object AboutTag {
const val DEBUG_MENU_TAG = "debug_menu"
}

View File

@ -6,9 +6,8 @@ import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.spackle.getPackageInfoCompat
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.screen.about.view.About
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@ -24,13 +23,17 @@ internal fun WrapAbout(
activity: ComponentActivity,
goBack: () -> Unit
) {
val packageInfo = activity.packageManager.getPackageInfoCompat(activity.packageName, 0L)
val configurationProvider = AndroidConfigurationFactory.getInstance(activity.applicationContext)
About(VersionInfo.new(packageInfo), ConfigInfo.new(configurationProvider), goBack)
val configInfo = ConfigInfo.new(AndroidConfigurationFactory.getInstance(activity.applicationContext))
val versionInfo = VersionInfo.new(activity.applicationContext)
// Allows an implicit way to force configuration refresh by simply visiting the About screen
LaunchedEffect(key1 = true) {
AndroidConfigurationFactory.getInstance(activity.applicationContext).hintToRefresh()
}
About(
onBack = goBack,
versionInfo = versionInfo,
configInfo = configInfo
)
}

View File

@ -1,13 +0,0 @@
package co.electriccoin.zcash.ui.screen.about.model
import android.content.pm.PackageInfo
import co.electriccoin.zcash.spackle.versionCodeCompat
data class VersionInfo(val versionName: String, val versionCode: Long) {
companion object {
fun new(packageInfo: PackageInfo) = VersionInfo(
packageInfo.versionName ?: "null", // Should only be null during tests
packageInfo.versionCodeCompat
)
}
}

View File

@ -1,34 +1,47 @@
package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.material.icons.filled.MoreVert
import androidx.compose.material3.CenterAlignedTopAppBar
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.Scaffold
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.build.gitSha
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
import co.electriccoin.zcash.ui.screen.about.AboutTag
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@Preview("About")
@ -37,9 +50,9 @@ private fun AboutPreview() {
ZcashTheme(darkTheme = false) {
GradientSurface {
About(
onBack = {},
versionInfo = VersionInfoFixture.new(),
configInfo = ConfigInfoFixture.new(),
goBack = {}
configInfo = ConfigInfoFixture.new()
)
}
}
@ -47,16 +60,19 @@ private fun AboutPreview() {
@Composable
fun About(
onBack: () -> Unit,
versionInfo: VersionInfo,
configInfo: ConfigInfo,
goBack: () -> Unit
configInfo: ConfigInfo
) {
Scaffold(topBar = {
AboutTopAppBar(onBack = goBack)
AboutTopAppBar(
onBack = onBack,
versionInfo = versionInfo,
configInfo = configInfo
)
}) { paddingValues ->
AboutMainContent(
versionInfo,
configInfo,
versionInfo = versionInfo,
modifier = Modifier
.fillMaxHeight()
.verticalScroll(
@ -65,8 +81,8 @@ fun About(
.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.spacingDefault,
end = ZcashTheme.dimens.spacingDefault
start = ZcashTheme.dimens.spacingHuge,
end = ZcashTheme.dimens.spacingHuge
)
)
}
@ -74,52 +90,122 @@ fun About(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun AboutTopAppBar(onBack: () -> Unit) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.about_title)) },
private fun AboutTopAppBar(
onBack: () -> Unit,
versionInfo: VersionInfo,
configInfo: ConfigInfo
) {
CenterAlignedTopAppBar(
title = {
Text(
text = stringResource(id = R.string.about_title).uppercase(),
style = ZcashTheme.typography.primary.titleSmall,
color = ZcashTheme.colors.screenTitleColor
)
},
navigationIcon = {
IconButton(
onClick = onBack
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.about_back_content_description)
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.about_back_content_description)
)
}
Text(
text = stringResource(id = R.string.about_back).uppercase(),
style = ZcashTheme.typography.primary.bodyMedium
)
}
},
actions = {
if (versionInfo.isDebuggable) {
DebugMenu(versionInfo, configInfo)
}
}
)
}
@Composable
private fun DebugMenu(
versionInfo: VersionInfo,
configInfo: ConfigInfo
) {
Column(
modifier = Modifier.testTag(AboutTag.DEBUG_MENU_TAG)
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = {
Column {
Text(stringResource(R.string.about_debug_menu_build, versionInfo.gitSha))
Text(configInfo.toSupportString())
}
},
onClick = {
expanded = false
}
)
}
}
}
@Composable
fun AboutMainContent(
versionInfo: VersionInfo,
configInfo: ConfigInfo,
modifier: Modifier = Modifier
) {
Column(modifier) {
Icon(painterResource(id = R.drawable.zashi_logo), contentDescription = null)
Text(stringResource(id = R.string.app_name))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Header(stringResource(id = R.string.about_version_header))
Body(stringResource(R.string.about_version_format, versionInfo.versionName, versionInfo.versionCode))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Header(stringResource(id = R.string.about_build_header))
Body(gitSha)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
configInfo.configurationUpdatedAt?.let { updatedAt ->
Header(stringResource(id = R.string.about_build_configuration))
Body(updatedAt.toString())
val logoContentDescription = stringResource(R.string.about_app_logo_content_description)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.semantics(mergeDescendants = true) {
contentDescription = logoContentDescription
}
) {
Image(
painter = painterResource(id = R.drawable.zashi_logo_without_text),
contentDescription = null,
Modifier
.height(ZcashTheme.dimens.zcashLogoHeight)
.width(ZcashTheme.dimens.zcashLogoWidth)
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingDefault))
Image(
painter = painterResource(id = R.drawable.zashi_text_logo),
contentDescription = null,
modifier = Modifier.height(ZcashTheme.dimens.zcashTextLogoHeight)
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Header(stringResource(id = R.string.about_legal_header))
Body(stringResource(id = R.string.about_legal_info))
Text(
text = stringResource(
R.string.about_version_format,
versionInfo.versionName,
versionInfo.versionCode
),
style = ZcashTheme.typography.primary.titleSmall
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Text(
text = stringResource(id = R.string.about_description),
color = ZcashTheme.colors.aboutTextColor,
style = ZcashTheme.extendedTypography.aboutText
)
}
}

View File

@ -10,11 +10,7 @@ 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.common.closeDrawerMenu
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
@ -86,23 +82,14 @@ internal fun WrapHome(
if (null == walletSnapshot) {
// Display loading indicator
} else {
val context = LocalContext.current
// We might eventually want to check the debuggable property of the manifest instead
// of relying on BuildConfig.
val isDebugMenuEnabled = BuildConfig.DEBUG &&
!FirebaseTestLabUtil.isFirebaseTestLab(context) &&
!EmulatorWtfUtil.isEmulatorWtf(context)
val drawerValues = drawerBackHandler()
Home(
walletSnapshot,
walletSnapshot = walletSnapshot,
isUpdateAvailable = updateAvailable,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
isFiatConversionEnabled = isFiatConversionEnabled,
isCircularProgressBarEnabled = isCircularProgressBarEnabled,
isDebugMenuEnabled = isDebugMenuEnabled,
goSeedPhrase = goSeedPhrase,
goSettings = goSettings,
goSupport = goSupport,
@ -110,9 +97,6 @@ internal fun WrapHome(
goReceive = goReceive,
goSend = goSend,
goHistory = goHistory,
resetSdk = {
walletViewModel.resetSdk()
},
drawerState = drawerValues.drawerState,
scope = drawerValues.scope
)

View File

@ -19,14 +19,11 @@ 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.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
@ -40,9 +37,7 @@ 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
import androidx.compose.ui.Modifier
@ -56,7 +51,6 @@ import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.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.common.closeDrawerMenu
@ -83,7 +77,6 @@ private fun ComposablePreview() {
walletSnapshot = WalletSnapshotFixture.new(),
isUpdateAvailable = false,
isKeepScreenOnDuringSync = false,
isDebugMenuEnabled = false,
isFiatConversionEnabled = false,
isCircularProgressBarEnabled = false,
goSeedPhrase = {},
@ -93,7 +86,6 @@ private fun ComposablePreview() {
goReceive = {},
goSend = {},
goHistory = {},
resetSdk = {},
drawerState = rememberDrawerState(DrawerValue.Closed),
scope = rememberCoroutineScope()
)
@ -109,7 +101,6 @@ fun Home(
isKeepScreenOnDuringSync: Boolean?,
isFiatConversionEnabled: Boolean,
isCircularProgressBarEnabled: Boolean,
isDebugMenuEnabled: Boolean,
goSeedPhrase: () -> Unit,
goSettings: () -> Unit,
goSupport: () -> Unit,
@ -117,7 +108,6 @@ fun Home(
goReceive: () -> Unit,
goSend: () -> Unit,
goHistory: () -> Unit,
resetSdk: () -> Unit,
drawerState: DrawerState,
scope: CoroutineScope
) {
@ -135,9 +125,7 @@ fun Home(
content = {
Scaffold(topBar = {
HomeTopAppBar(
isDebugMenuEnabled = isDebugMenuEnabled,
openDrawer = { drawerState.openDrawerMenu(scope) },
resetSdk = resetSdk
openDrawer = { drawerState.openDrawerMenu(scope) }
)
}) { paddingValues ->
HomeMainContent(
@ -164,9 +152,7 @@ fun Home(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun HomeTopAppBar(
isDebugMenuEnabled: Boolean,
openDrawer: () -> Unit,
resetSdk: () -> Unit,
openDrawer: () -> Unit
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
@ -180,59 +166,10 @@ private fun HomeTopAppBar(
contentDescription = stringResource(R.string.home_menu_content_description)
)
}
},
actions = {
if (isDebugMenuEnabled) {
DebugMenu(resetSdk)
}
}
)
}
@Composable
private fun DebugMenu(
resetSdk: () -> Unit
) {
Column {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Throw Uncaught Exception") },
onClick = {
// Supposed to be generic, for manual debugging only
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("Manually crashed from debug menu")
}
)
DropdownMenuItem(
text = { Text("Report Caught Exception") },
onClick = {
// Eventually this shouldn't rely on the Android implementation, but rather an expect/actual
// should be used at the crash API level.
GlobalCrashReporter.reportCaughtException(
RuntimeException("Manually caught exception from debug menu")
)
expanded = false
}
)
DropdownMenuItem(
text = { Text("Reset SDK") },
onClick = {
resetSdk()
expanded = false
}
)
}
}
}
@Composable
private fun HomeDrawer(
onCloseDrawer: () -> Unit,

View File

@ -6,7 +6,6 @@ import android.content.Context
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.fixture.WalletFixture
@ -17,10 +16,9 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
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.common.model.VersionInfo
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.home.model.OnboardingState
@ -43,13 +41,7 @@ internal fun WrapOnboarding(
val walletViewModel by activity.viewModels<WalletViewModel>()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
val applicationContext = LocalContext.current.applicationContext
// We might eventually want to check the debuggable property of the manifest instead
// of relying on BuildConfig.
val isDebugMenuEnabled = BuildConfig.DEBUG &&
!FirebaseTestLabUtil.isFirebaseTestLab(applicationContext) &&
!EmulatorWtfUtil.isEmulatorWtf(applicationContext)
val versionInfo = VersionInfo.new(activity.applicationContext)
// TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383
// TODO [#383]: Refactoring of UI state retention into rememberSaveable fields
@ -63,12 +55,12 @@ internal fun WrapOnboarding(
// Firebase Test Lab or Google Play pre-launch report, we want to skip creating
// a new or restoring an existing wallet screens by persisting an existing wallet
// with a mock seed.
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
if (FirebaseTestLabUtil.isFirebaseTestLab(activity.applicationContext)) {
persistExistingWalletWithSeedPhrase(
applicationContext,
activity.applicationContext,
walletViewModel,
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext))
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
)
} else {
onboardingViewModel.setIsImporting(true)
@ -79,10 +71,10 @@ internal fun WrapOnboarding(
val onFixtureWallet = {
persistExistingWalletWithSeedPhrase(
applicationContext,
activity.applicationContext,
walletViewModel,
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext))
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
)
}
@ -100,7 +92,7 @@ internal fun WrapOnboarding(
} else {
LongOnboarding(
onboardingState = onboardingViewModel.onboardingState,
isDebugMenuEnabled = isDebugMenuEnabled,
isDebugMenuEnabled = versionInfo.isDebuggable,
onImportWallet = onImportWallet,
onCreateWallet = onCreateWallet,
onFixtureWallet = onFixtureWallet

View File

@ -8,10 +8,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.spackle.getPackageInfoCompat
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.screen.securitywarning.util.WebBrowserUtil
import co.electriccoin.zcash.ui.screen.securitywarning.view.SecurityWarning
import kotlinx.coroutines.CoroutineScope
@ -35,14 +34,12 @@ internal fun WrapSecurityWarning(
onBack: () -> Unit,
onConfirm: () -> Unit
) {
val packageInfo = activity.packageManager.getPackageInfoCompat(activity.packageName, 0L)
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
SecurityWarning(
snackbarHostState = snackbarHostState,
versionInfo = VersionInfo.new(packageInfo),
versionInfo = VersionInfo.new(activity.applicationContext),
onBack = onBack,
onAcknowledged = {
// Needed for UI testing only

View File

@ -36,13 +36,13 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.CheckBox
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
@Preview("Security Warning")
@Composable

View File

@ -6,8 +6,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -17,11 +17,13 @@ import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@Composable
internal fun MainActivity.WrapSettings(
goBack: () -> Unit
goBack: () -> Unit,
goAbout: () -> Unit,
) {
WrapSettings(
activity = this,
goBack = goBack,
goAbout = goAbout
)
}
@ -29,10 +31,13 @@ internal fun MainActivity.WrapSettings(
private fun WrapSettings(
activity: ComponentActivity,
goBack: () -> Unit,
goAbout: () -> Unit,
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val versionInfo = VersionInfo.new(activity.applicationContext)
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val isBackgroundSyncEnabled = settingsViewModel.isBackgroundSync.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
@ -48,7 +53,7 @@ private fun WrapSettings(
} else {
Settings(
TroubleshootingParameters(
isEnabled = BuildConfig.DEBUG,
isEnabled = versionInfo.isDebuggable,
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled,
@ -59,7 +64,7 @@ private fun WrapSettings(
onDocumentation = {},
onPrivacyPolicy = {},
onFeedback = {},
onAbout = {},
onAbout = goAbout,
onRescanWallet = {
walletViewModel.rescanBlockchain()
},

View File

@ -1,13 +1,14 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="about_title">About</string>
<string name="about_back">Back</string>
<string name="about_back_content_description">Back</string>
<string name="about_icon_content_description">Zcash icon</string>
<string name="about_version_header">Version</string>
<string name="about_version_format" formatted="true"><xliff:g id="version_name" example="1.0">%1$s</xliff:g> (<xliff:g id="version_code" example="1.0">%2$d</xliff:g>)</string>
<string name="about_build_header">Build</string>
<string name="about_build_configuration">Configuration</string>
<string name="about_legal_header">Legal</string>
<!-- TODO [#392] Update with real legal info. -->
<!-- TODO [#392] https://github.com/zcash/secant-android-wallet/issues/392 -->
<string name="about_legal_info">GitHub Issue #392</string>
<string name="about_app_logo_content_description">Zcash logo</string>
<string name="about_version_format" formatted="true">Version <xliff:g example="1.0" id="version_name">%1$s
</xliff:g> (<xliff:g example="1.0" id="version_code">%2$d</xliff:g>)</string>
<string name="about_debug_menu_build">Build: <xliff:g example="635dac0eb9ddc2bc6da5177f0dd495d8b76af4dc"
id="git_commit_hash">%1$s</xliff:g></string>
<string name="about_description">Send and receive ZEC on Zashi! Zashi is a minimal-design, self-custody, ZEC-only
shielded wallet that keeps your transaction history and wallet balance private. Built by Zcashers, for
Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in
user-feedback mechanism to enable more features, more quickly.</string>
</resources>

View File

@ -680,7 +680,9 @@ private fun supportScreenshots(resContext: Context, tag: String, composeTestRule
}
private fun aboutScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(resContext.getString(R.string.about_title))).also {
composeTestRule.onNode(
hasText(resContext.getString(R.string.about_title).uppercase())
).also {
it.assertExists()
}