[#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.
|
# 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.configureondemand=false
|
||||||
org.gradle.caching=true
|
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.jvmargs=-Xmx3g
|
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
|
||||||
|
|
||||||
kotlin.mpp.stability.nowarn=true
|
kotlin.mpp.stability.nowarn=true
|
||||||
|
|
||||||
kapt.include.compile.classpath=false
|
|
||||||
kapt.incremental.apt=true
|
|
||||||
kapt.use.worker.api=true
|
|
||||||
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.builder.sdkDownload=true
|
android.builder.sdkDownload=true
|
||||||
|
|
|
@ -21,6 +21,7 @@ dependencies {
|
||||||
implementation(libs.zcash.bip39)
|
implementation(libs.zcash.bip39)
|
||||||
|
|
||||||
androidTestImplementation(libs.bundles.androidx.test)
|
androidTestImplementation(libs.bundles.androidx.test)
|
||||||
|
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
|
||||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||||
androidTestUtil(libs.androidx.test.orchestrator) {
|
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/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/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.view.RestoreWallet
|
||||||
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
|
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
|
||||||
import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel
|
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.theme.ZcashTheme
|
||||||
import cash.z.ecc.ui.util.AndroidApiVersion
|
import cash.z.ecc.ui.util.AndroidApiVersion
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -199,7 +200,21 @@ class MainActivity : ComponentActivity() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "home") {
|
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 {
|
companion object {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal val SPLASH_SCREEN_DELAY = 0.seconds
|
internal val SPLASH_SCREEN_DELAY = 0.seconds
|
||||||
|
|
|
@ -31,3 +31,29 @@ fun Body(
|
||||||
modifier = modifier
|
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.android.sdk.type.WalletBalance
|
||||||
import cash.z.ecc.sdk.SynchronizerCompanion
|
import cash.z.ecc.sdk.SynchronizerCompanion
|
||||||
import cash.z.ecc.sdk.model.PersistableWallet
|
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.common.ANDROID_STATE_FLOW_TIMEOUT_MILLIS
|
||||||
import cash.z.ecc.ui.preference.EncryptedPreferenceKeys
|
import cash.z.ecc.ui.preference.EncryptedPreferenceKeys
|
||||||
import cash.z.ecc.ui.preference.EncryptedPreferenceSingleton
|
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.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapConcat
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
@ -120,6 +122,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
emptyList()
|
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
|
* 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.
|
* [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.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
@ -123,13 +122,10 @@ fun RestoreWallet(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RestoreTopAppBar(onBack: () -> Unit, onClear: () -> Unit) {
|
private fun RestoreTopAppBar(onBack: () -> Unit, onClear: () -> Unit) {
|
||||||
TopAppBar {
|
TopAppBar(
|
||||||
Row(
|
title = { Text(text = stringResource(id = R.string.restore_header)) },
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
navigationIcon = {
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
onClick = onBack
|
onClick = onBack
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -137,18 +133,11 @@ private fun RestoreTopAppBar(onBack: () -> Unit, onClear: () -> Unit) {
|
||||||
contentDescription = stringResource(R.string.restore_back_content_description)
|
contentDescription = stringResource(R.string.restore_back_content_description)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
Text(text = stringResource(id = R.string.restore_header))
|
actions = {
|
||||||
|
|
||||||
Spacer(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(MINIMAL_WEIGHT)
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationButton(onClick = onClear, stringResource(R.string.restore_button_clear))
|
NavigationButton(onClick = onClear, stringResource(R.string.restore_button_clear))
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@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 overlay = Color(0x22000000)
|
||||||
val highlight = Color(0xFFFFD800)
|
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 {
|
object Light {
|
||||||
|
@ -80,4 +87,12 @@ object Light {
|
||||||
|
|
||||||
val overlay = Color(0x22000000)
|
val overlay = Color(0x22000000)
|
||||||
val highlight = Color(0xFFFFD800)
|
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 progressBackground: Color,
|
||||||
val chipIndex: Color,
|
val chipIndex: Color,
|
||||||
val overlay: 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
|
@Composable
|
||||||
fun surfaceGradient() = Brush.verticalGradient(
|
fun surfaceGradient() = Brush.verticalGradient(
|
||||||
|
@ -69,7 +75,13 @@ val DarkExtendedColorPalette = ExtendedColors(
|
||||||
progressBackground = Dark.progressBackground,
|
progressBackground = Dark.progressBackground,
|
||||||
chipIndex = Dark.textChipIndex,
|
chipIndex = Dark.textChipIndex,
|
||||||
overlay = Dark.overlay,
|
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(
|
val LightExtendedColorPalette = ExtendedColors(
|
||||||
|
@ -84,7 +96,13 @@ val LightExtendedColorPalette = ExtendedColors(
|
||||||
progressBackground = Light.progressBackground,
|
progressBackground = Light.progressBackground,
|
||||||
chipIndex = Light.textChipIndex,
|
chipIndex = Light.textChipIndex,
|
||||||
overlay = Light.overlay,
|
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 {
|
val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
|
@ -100,7 +118,13 @@ val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
progressBackground = Color.Unspecified,
|
progressBackground = Color.Unspecified,
|
||||||
chipIndex = Color.Unspecified,
|
chipIndex = Color.Unspecified,
|
||||||
overlay = 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
|
@Immutable
|
||||||
data class ExtendedTypography(
|
data class ExtendedTypography(
|
||||||
val chipIndex: TextStyle,
|
val chipIndex: TextStyle,
|
||||||
|
val listItem: TextStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalExtendedTypography = staticCompositionLocalOf {
|
val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
|
@ -52,6 +53,9 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
baselineShift = BaselineShift.Superscript,
|
baselineShift = BaselineShift.Superscript,
|
||||||
fontWeight = FontWeight.Bold
|
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