[#62] Wallet details scaffold
Implements a details screen showing various wallet addresses Some known issues requiring followup in future pull requests: - #159 Colors for light theme have not been defined yet - #160 Color blocks have 2x borders along the top/bottom of the middle item - #161 Some address types display placeholder values
This commit is contained in:
parent
205344c201
commit
ee96e915ae
|
@ -1,16 +1,12 @@
|
|||
# Speed up builds. Keep these flags here for quick debugging of issues.
|
||||
org.gradle.vfs.watch=true
|
||||
# https://github.com/gradle/gradle/issues/13382
|
||||
org.gradle.vfs.watch=false
|
||||
org.gradle.configureondemand=false
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx3g
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
|
||||
|
||||
kotlin.mpp.stability.nowarn=true
|
||||
|
||||
kapt.include.compile.classpath=false
|
||||
kapt.incremental.apt=true
|
||||
kapt.use.worker.api=true
|
||||
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.builder.sdkDownload=true
|
||||
|
|
|
@ -21,6 +21,7 @@ dependencies {
|
|||
implementation(libs.zcash.bip39)
|
||||
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
|
||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||
androidTestUtil(libs.androidx.test.orchestrator) {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressesFixture
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class WalletAddressesTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun security() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
val actual = WalletAddressesFixture.new().toString()
|
||||
assertFalse(actual.contains(walletAddresses.shieldedOrchard))
|
||||
assertFalse(actual.contains(walletAddresses.shieldedSapling))
|
||||
assertFalse(actual.contains(walletAddresses.transparent))
|
||||
assertFalse(actual.contains(walletAddresses.unified))
|
||||
assertFalse(actual.contains(walletAddresses.viewingKey))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
@SmallTest
|
||||
fun new() = runTest {
|
||||
val expected = WalletAddressesFixture.new()
|
||||
val actual = WalletAddresses.new(PersistableWalletFixture.new())
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package cash.z.ecc.sdk.fixture
|
||||
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
|
||||
object WalletAddressesFixture {
|
||||
// These fixture values are derived from the secret defined in PersistableWalletFixture
|
||||
|
||||
const val UNIFIED = "Unified GitHub Issue #161"
|
||||
const val SHIELDED_ORCHARD = "Shielded Orchard GitHub Issue #161"
|
||||
@Suppress("MaxLineLength")
|
||||
const val SHIELDED_SAPLING = "ztestsapling1475xtm56czrzmleqzzlu4cxvjjfsy2p6rv78q07232cpsx5ee52k0mn5jyndq09mampkgvrxnwg"
|
||||
const val TRANSPARENT = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
|
||||
const val VIEWING_KEY = "03feaa290589a20f795f302ba03847b0a6c9c2b571d75d80bc4ebb02382d0549da"
|
||||
|
||||
fun new(
|
||||
unified: String = UNIFIED,
|
||||
shieldedOrchard: String = SHIELDED_ORCHARD,
|
||||
shieldedSapling: String = SHIELDED_SAPLING,
|
||||
transparent: String = TRANSPARENT,
|
||||
viewingKey: String = VIEWING_KEY
|
||||
) = WalletAddresses(unified, shieldedOrchard, shieldedSapling, transparent, viewingKey)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class WalletAddresses(
|
||||
val unified: String,
|
||||
val shieldedOrchard: String,
|
||||
val shieldedSapling: String,
|
||||
val transparent: String,
|
||||
val viewingKey: String
|
||||
) {
|
||||
// Override to prevent leaking details in logs
|
||||
override fun toString() = "WalletAddresses"
|
||||
|
||||
companion object {
|
||||
suspend fun new(persistableWallet: PersistableWallet): WalletAddresses {
|
||||
// Dispatcher needed because SecureRandom is loaded, which is slow and performs IO
|
||||
// https://github.com/zcash/kotlin-bip39/issues/13
|
||||
val bip39Seed = withContext(Dispatchers.IO) {
|
||||
Mnemonics.MnemonicCode(persistableWallet.seedPhrase.joinToString()).toSeed()
|
||||
}
|
||||
|
||||
// Dispatchers needed until an SDK is published with the implementation of
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/269
|
||||
val viewingKey = withContext(Dispatchers.IO) {
|
||||
DerivationTool.deriveUnifiedViewingKeys(bip39Seed, persistableWallet.network)[0]
|
||||
}.extpub
|
||||
|
||||
val shieldedSaplingAddress = withContext(Dispatchers.IO) {
|
||||
DerivationTool.deriveShieldedAddress(bip39Seed, persistableWallet.network)
|
||||
}
|
||||
|
||||
val transparentAddress = withContext(Dispatchers.IO) {
|
||||
DerivationTool.deriveTransparentAddress(bip39Seed, persistableWallet.network)
|
||||
}
|
||||
|
||||
// TODO [#161]: Pending SDK support, fix providing correct values for the unified
|
||||
return WalletAddresses(
|
||||
unified = "Unified GitHub Issue #161",
|
||||
shieldedOrchard = "Shielded Orchard GitHub Issue #161",
|
||||
shieldedSapling = shieldedSaplingAddress,
|
||||
transparent = transparentAddress,
|
||||
viewingKey = viewingKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,7 +31,8 @@ android {
|
|||
"src/main/res/ui/common",
|
||||
"src/main/res/ui/home",
|
||||
"src/main/res/ui/onboarding",
|
||||
"src/main/res/ui/restore"
|
||||
"src/main/res/ui/restore",
|
||||
"src/main/res/ui/wallet_address"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
package cash.z.ecc.ui.screen.wallet_address.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.sdk.fixture.WalletAddressesFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.test.getStringResource
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class WalletAddressViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun initial_screen_setup() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_orchard)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_sapling)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_transparent)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_viewing_key)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedOrchard).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun unified_collapses() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun shielded_orchard_expands() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedOrchard).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_orchard)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedOrchard).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun shielded_sapling_expands() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_sapling)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun transparent_expands() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_transparent)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun viewing_expands() {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_viewing_key)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.wallet_address_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
private fun newTestSetup(initialState: WalletAddresses = WalletAddressesFixture.new()) = TestSetup(composeTestRule, initialState)
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule, initialState: WalletAddresses) {
|
||||
|
||||
private var onBackCount = 0
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
WalletAddresses(
|
||||
initialState,
|
||||
onBack = {
|
||||
onBackCount++
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
|||
import cash.z.ecc.ui.screen.restore.view.RestoreWallet
|
||||
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
|
||||
import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel
|
||||
import cash.z.ecc.ui.screen.wallet_address.view.WalletAddresses
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import cash.z.ecc.ui.util.AndroidApiVersion
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -199,7 +200,21 @@ class MainActivity : ComponentActivity() {
|
|||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = "home") {
|
||||
composable("home") { WrapHome({}, {}, {}, {}) }
|
||||
composable("home") {
|
||||
WrapHome(
|
||||
goScan = {},
|
||||
goProfile = { navController.navigate("wallet_address_details") },
|
||||
goSend = {},
|
||||
goRequest = {}
|
||||
)
|
||||
}
|
||||
composable("wallet_address_details") {
|
||||
WrapWalletAddresses(
|
||||
goBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,6 +240,21 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapWalletAddresses(
|
||||
goBack: () -> Unit,
|
||||
) {
|
||||
val walletAddresses = walletViewModel.addresses.collectAsState().value
|
||||
if (null == walletAddresses) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
WalletAddresses(
|
||||
walletAddresses,
|
||||
goBack
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
internal val SPLASH_SCREEN_DELAY = 0.seconds
|
||||
|
|
|
@ -31,3 +31,29 @@ fun Body(
|
|||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = ZcashTheme.typography.listItem,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = ZcashTheme.typography.listItem,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
|||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.sdk.SynchronizerCompanion
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.ui.common.ANDROID_STATE_FLOW_TIMEOUT_MILLIS
|
||||
import cash.z.ecc.ui.preference.EncryptedPreferenceKeys
|
||||
import cash.z.ecc.ui.preference.EncryptedPreferenceSingleton
|
||||
|
@ -31,6 +32,7 @@ import kotlinx.coroutines.flow.filterIsInstance
|
|||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
@ -120,6 +122,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
emptyList()
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val addresses: StateFlow<WalletAddresses?> = secretState
|
||||
.filterIsInstance<SecretState.Ready>()
|
||||
.map { WalletAddresses.new(it.persistableWallet) }
|
||||
.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = ANDROID_STATE_FLOW_TIMEOUT_MILLIS),
|
||||
null
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a wallet asynchronously and then persists it. Clients observe
|
||||
* [secretState] to see the side effects. This would be used for a user creating a new wallet.
|
||||
|
|
|
@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
@ -123,13 +122,10 @@ fun RestoreWallet(
|
|||
|
||||
@Composable
|
||||
private fun RestoreTopAppBar(onBack: () -> Unit, onClear: () -> Unit) {
|
||||
TopAppBar {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.restore_header)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
|
@ -137,18 +133,11 @@ private fun RestoreTopAppBar(onBack: () -> Unit, onClear: () -> Unit) {
|
|||
contentDescription = stringResource(R.string.restore_back_content_description)
|
||||
)
|
||||
}
|
||||
|
||||
Text(text = stringResource(id = R.string.restore_header))
|
||||
|
||||
Spacer(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
},
|
||||
actions = {
|
||||
NavigationButton(onClick = onClear, stringResource(R.string.restore_button_clear))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
@file:Suppress("TooManyFunctions", "PackageNaming")
|
||||
|
||||
package cash.z.ecc.ui.screen.wallet_address.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDropDownCircle
|
||||
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.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressesFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.screen.common.Body
|
||||
import cash.z.ecc.ui.screen.common.GradientSurface
|
||||
import cash.z.ecc.ui.screen.common.ListHeader
|
||||
import cash.z.ecc.ui.screen.common.ListItem
|
||||
import cash.z.ecc.ui.theme.MINIMAL_WEIGHT
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ComposablePreview() {
|
||||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
WalletAddresses(
|
||||
WalletAddressesFixture.new(),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WalletAddresses(walletAddresses: WalletAddresses, onBack: () -> Unit) {
|
||||
Column {
|
||||
WalletDetailTopAppBar(onBack)
|
||||
WalletDetailAddresses(walletAddresses)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletDetailTopAppBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_address_title)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.wallet_address_back_content_description),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val BIG_INDICATOR_WIDTH = 24.dp
|
||||
private val SMALL_INDICATOR_WIDTH = 16.dp
|
||||
|
||||
@Composable
|
||||
private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
Image(
|
||||
painter = ColorPainter(ZcashTheme.colors.highlight),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(BIG_INDICATOR_WIDTH)
|
||||
)
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
ExpandableRow(
|
||||
title = stringResource(R.string.wallet_address_unified),
|
||||
content = walletAddresses.unified,
|
||||
isInitiallyExpanded = true
|
||||
)
|
||||
|
||||
Box(Modifier.height(IntrinsicSize.Min)) {
|
||||
Divider(modifier = Modifier.fillMaxHeight())
|
||||
ListHeader(text = stringResource(R.string.wallet_address_header_includes))
|
||||
}
|
||||
|
||||
OrchardAddress(walletAddresses.shieldedOrchard)
|
||||
SaplingAddress(walletAddresses.shieldedSapling)
|
||||
TransparentAddress(walletAddresses.transparent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(thickness = 8.dp)
|
||||
|
||||
ViewingKey(walletAddresses.viewingKey)
|
||||
}
|
||||
|
||||
// Note: The addresses code below has opportunities to be made more DRY.
|
||||
// Refactoring that is being held off until issue #160 is fixed, since knowledge
|
||||
// of row position will be needed.
|
||||
|
||||
@Composable
|
||||
private fun OrchardAddress(orchardAddress: String) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
SmallIndicator(ZcashTheme.colors.addressHighlightOrchard)
|
||||
ExpandableRow(
|
||||
title = stringResource(R.string.wallet_address_shielded_orchard),
|
||||
content = orchardAddress,
|
||||
isInitiallyExpanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaplingAddress(saplingAddress: String) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
SmallIndicator(ZcashTheme.colors.addressHighlightSapling)
|
||||
|
||||
ExpandableRow(
|
||||
title = stringResource(R.string.wallet_address_shielded_sapling),
|
||||
content = saplingAddress,
|
||||
isInitiallyExpanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransparentAddress(transparentAddress: String) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
SmallIndicator(ZcashTheme.colors.addressHighlightTransparent)
|
||||
ExpandableRow(
|
||||
title = stringResource(R.string.wallet_address_transparent),
|
||||
content = transparentAddress,
|
||||
isInitiallyExpanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ViewingKey(viewingKey: String) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
Image(
|
||||
painter = ColorPainter(ZcashTheme.colors.addressHighlightViewing),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.width(SMALL_INDICATOR_WIDTH)
|
||||
)
|
||||
ExpandableRow(
|
||||
title = stringResource(R.string.wallet_address_viewing_key),
|
||||
content = viewingKey,
|
||||
isInitiallyExpanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandableRow(
|
||||
title: String,
|
||||
content: String,
|
||||
isInitiallyExpanded: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var expandedState by rememberSaveable { mutableStateOf(isInitiallyExpanded) }
|
||||
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
expandedState = !expandedState
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.defaultMinSize(minHeight = 48.dp)) {
|
||||
ListItem(text = title)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
ExpandableArrow(expandedState)
|
||||
}
|
||||
if (expandedState) {
|
||||
Body(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmallIndicator(color: Color) {
|
||||
// TODO [#160]: Border is not the right implementation here, as it causes double thickness for the middle item
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(SMALL_INDICATOR_WIDTH)
|
||||
.border(1.dp, ZcashTheme.colors.addressHighlightBorder),
|
||||
painter = ColorPainter(color),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
|
||||
private const val NINETY_DEGREES = 90f
|
||||
|
||||
@Composable
|
||||
private fun ExpandableArrow(isExpanded: Boolean) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowDropDownCircle,
|
||||
contentDescription = if (isExpanded) {
|
||||
stringResource(id = R.string.wallet_address_hide)
|
||||
} else {
|
||||
stringResource(id = R.string.wallet_address_show)
|
||||
},
|
||||
modifier = if (isExpanded) {
|
||||
Modifier
|
||||
} else {
|
||||
Modifier.rotate(NINETY_DEGREES)
|
||||
},
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
|
@ -41,6 +41,13 @@ object Dark {
|
|||
|
||||
val overlay = Color(0x22000000)
|
||||
val highlight = Color(0xFFFFD800)
|
||||
|
||||
val addressHighlightBorder = Color(0xFF525252)
|
||||
val addressHighlightUnified = Color(0xFFFFD800)
|
||||
val addressHighlightOrchard = Color(0xFFFFD800)
|
||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||
val addressHighlightTransparent = Color(0xFF97999A)
|
||||
val addressHighlightViewing = Color(0xFF504062)
|
||||
}
|
||||
|
||||
object Light {
|
||||
|
@ -80,4 +87,12 @@ object Light {
|
|||
|
||||
val overlay = Color(0x22000000)
|
||||
val highlight = Color(0xFFFFD800)
|
||||
|
||||
// [TODO #159]: The colors are wrong for light theme
|
||||
val addressHighlightBorder = Color(0xFF525252)
|
||||
val addressHighlightUnified = Color(0xFFFFD800)
|
||||
val addressHighlightOrchard = Color(0xFFFFD800)
|
||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||
val addressHighlightTransparent = Color(0xFF97999A)
|
||||
val addressHighlightViewing = Color(0xFF504062)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,13 @@ data class ExtendedColors(
|
|||
val progressBackground: Color,
|
||||
val chipIndex: Color,
|
||||
val overlay: Color,
|
||||
val highlight: Color
|
||||
val highlight: Color,
|
||||
val addressHighlightBorder: Color,
|
||||
val addressHighlightUnified: Color,
|
||||
val addressHighlightOrchard: Color,
|
||||
val addressHighlightSapling: Color,
|
||||
val addressHighlightTransparent: Color,
|
||||
val addressHighlightViewing: Color
|
||||
) {
|
||||
@Composable
|
||||
fun surfaceGradient() = Brush.verticalGradient(
|
||||
|
@ -69,7 +75,13 @@ val DarkExtendedColorPalette = ExtendedColors(
|
|||
progressBackground = Dark.progressBackground,
|
||||
chipIndex = Dark.textChipIndex,
|
||||
overlay = Dark.overlay,
|
||||
highlight = Dark.highlight
|
||||
highlight = Dark.highlight,
|
||||
addressHighlightBorder = Dark.addressHighlightBorder,
|
||||
addressHighlightUnified = Dark.addressHighlightUnified,
|
||||
addressHighlightOrchard = Dark.addressHighlightOrchard,
|
||||
addressHighlightSapling = Dark.addressHighlightSapling,
|
||||
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
||||
addressHighlightViewing = Dark.addressHighlightViewing
|
||||
)
|
||||
|
||||
val LightExtendedColorPalette = ExtendedColors(
|
||||
|
@ -84,7 +96,13 @@ val LightExtendedColorPalette = ExtendedColors(
|
|||
progressBackground = Light.progressBackground,
|
||||
chipIndex = Light.textChipIndex,
|
||||
overlay = Light.overlay,
|
||||
highlight = Light.highlight
|
||||
highlight = Light.highlight,
|
||||
addressHighlightBorder = Light.addressHighlightBorder,
|
||||
addressHighlightUnified = Light.addressHighlightUnified,
|
||||
addressHighlightOrchard = Light.addressHighlightOrchard,
|
||||
addressHighlightSapling = Light.addressHighlightSapling,
|
||||
addressHighlightTransparent = Light.addressHighlightTransparent,
|
||||
addressHighlightViewing = Light.addressHighlightViewing
|
||||
)
|
||||
|
||||
val LocalExtendedColors = staticCompositionLocalOf {
|
||||
|
@ -100,7 +118,13 @@ val LocalExtendedColors = staticCompositionLocalOf {
|
|||
progressBackground = Color.Unspecified,
|
||||
chipIndex = Color.Unspecified,
|
||||
overlay = Color.Unspecified,
|
||||
highlight = Color.Unspecified
|
||||
highlight = Color.Unspecified,
|
||||
addressHighlightBorder = Color.Unspecified,
|
||||
addressHighlightUnified = Color.Unspecified,
|
||||
addressHighlightOrchard = Color.Unspecified,
|
||||
addressHighlightSapling = Color.Unspecified,
|
||||
addressHighlightTransparent = Color.Unspecified,
|
||||
addressHighlightViewing = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ val Typography = Typography(
|
|||
@Immutable
|
||||
data class ExtendedTypography(
|
||||
val chipIndex: TextStyle,
|
||||
val listItem: TextStyle,
|
||||
)
|
||||
|
||||
val LocalExtendedTypography = staticCompositionLocalOf {
|
||||
|
@ -52,6 +53,9 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
|||
fontSize = 10.sp,
|
||||
baselineShift = BaselineShift.Superscript,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
listItem = Typography.body1.copy(
|
||||
fontSize = 24.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="wallet_address_title">Wallet Addresses</string>
|
||||
<string name="wallet_address_back_content_description">Back</string>
|
||||
<string name="wallet_address_unified">Your Unified Address</string>
|
||||
<string name="wallet_address_header_includes">which includes</string>
|
||||
<string name="wallet_address_shielded_orchard">Shielded Orchard (NU5)</string>
|
||||
<string name="wallet_address_shielded_sapling">Shielded Sapling (NU1)</string>
|
||||
<string name="wallet_address_transparent">Transparent</string>
|
||||
<string name="wallet_address_viewing_key">Viewing Key Only (Sapling)</string>
|
||||
<string name="wallet_address_show">Show address</string>
|
||||
<string name="wallet_address_hide">Hide address</string>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue