[#145] Profile scaffold
This commit is contained in:
parent
35055c3cfe
commit
d46cf8187c
|
@ -0,0 +1,5 @@
|
|||
# QR Code Contents
|
||||
1. Configure a wallet in the Zcash app
|
||||
1. Visit the profile screen
|
||||
1. Using another device, scan the QR code
|
||||
1. Verify the QR code contents match the QA address displayed below the QR code
|
|
@ -88,6 +88,7 @@ KOTLINX_COROUTINES_VERSION=1.6.0
|
|||
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
||||
ZCASH_BIP39_VERSION=1.0.2
|
||||
ZCASH_SDK_VERSION=1.3.0-beta19
|
||||
ZXING_VERSION=3.4.1
|
||||
|
||||
# Toolchain is the Java version used to build the application, which is separate from the
|
||||
# Java version used to run the application. Android requires a minimum of 11.
|
||||
|
|
|
@ -9,20 +9,19 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class WalletAddressesTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun security() {
|
||||
fun security() = runTest {
|
||||
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.shieldedSapling.address))
|
||||
assertFalse(actual.contains(walletAddresses.transparent.address))
|
||||
assertFalse(actual.contains(walletAddresses.unified.address))
|
||||
assertFalse(actual.contains(walletAddresses.viewingKey))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
@SmallTest
|
||||
fun new() = runTest {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package cash.z.ecc.sdk.fixture
|
||||
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
|
||||
object WalletAddressFixture {
|
||||
// These fixture values are derived from the secret defined in PersistableWalletFixture
|
||||
|
||||
// TODO [#161]: Pending SDK support
|
||||
const val UNIFIED_ADDRESS_STRING = "Unified GitHub Issue #161"
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
const val SHIELDED_SAPLING_ADDRESS_STRING = "ztestsapling1475xtm56czrzmleqzzlu4cxvjjfsy2p6rv78q07232cpsx5ee52k0mn5jyndq09mampkgvrxnwg"
|
||||
const val TRANSPARENT_ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
|
||||
|
||||
suspend fun unified() = WalletAddress.Unified.new(UNIFIED_ADDRESS_STRING)
|
||||
suspend fun shieldedSapling() = WalletAddress.ShieldedSapling.new(SHIELDED_SAPLING_ADDRESS_STRING)
|
||||
suspend fun transparent() = WalletAddress.Transparent.new(TRANSPARENT_ADDRESS_STRING)
|
||||
}
|
|
@ -1,22 +1,21 @@
|
|||
package cash.z.ecc.sdk.fixture
|
||||
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
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,
|
||||
suspend fun new(
|
||||
unified: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING,
|
||||
shieldedSapling: String = WalletAddressFixture.SHIELDED_SAPLING_ADDRESS_STRING,
|
||||
transparent: String = WalletAddressFixture.TRANSPARENT_ADDRESS_STRING,
|
||||
viewingKey: String = VIEWING_KEY
|
||||
) = WalletAddresses(unified, shieldedOrchard, shieldedSapling, transparent, viewingKey)
|
||||
) = WalletAddresses(
|
||||
WalletAddress.Unified.new(unified),
|
||||
WalletAddress.ShieldedSapling.new(shieldedSapling),
|
||||
WalletAddress.Transparent.new(transparent),
|
||||
viewingKey
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
sealed class WalletAddress(val address: String) {
|
||||
class Unified private constructor(address: String) : WalletAddress(address) {
|
||||
companion object {
|
||||
suspend fun new(address: String): WalletAddress.Unified {
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
// TODO [#342]: refactor SDK to enable direct calls for address verification
|
||||
return WalletAddress.Unified(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShieldedSapling private constructor(address: String) : WalletAddress(address) {
|
||||
companion object {
|
||||
suspend fun new(address: String): WalletAddress.ShieldedSapling {
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
// TODO [#342]: refactor SDK to enable direct calls for address verification
|
||||
return WalletAddress.ShieldedSapling(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Transparent private constructor(address: String) : WalletAddress(address) {
|
||||
companion object {
|
||||
suspend fun new(address: String): WalletAddress.Transparent {
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
// TODO [#342]: refactor SDK to enable direct calls for address verification
|
||||
return WalletAddress.Transparent(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WalletAddress
|
||||
|
||||
if (address != other.address) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = address.hashCode()
|
||||
}
|
|
@ -7,10 +7,9 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class WalletAddresses(
|
||||
val unified: String,
|
||||
val shieldedOrchard: String,
|
||||
val shieldedSapling: String,
|
||||
val transparent: String,
|
||||
val unified: WalletAddress.Unified,
|
||||
val shieldedSapling: WalletAddress.ShieldedSapling,
|
||||
val transparent: WalletAddress.Transparent,
|
||||
val viewingKey: String
|
||||
) {
|
||||
// Override to prevent leaking details in logs
|
||||
|
@ -32,16 +31,19 @@ data class WalletAddresses(
|
|||
|
||||
val shieldedSaplingAddress = withContext(Dispatchers.IO) {
|
||||
DerivationTool.deriveShieldedAddress(bip39Seed, persistableWallet.network)
|
||||
}.let {
|
||||
WalletAddress.ShieldedSapling.new(it)
|
||||
}
|
||||
|
||||
val transparentAddress = withContext(Dispatchers.IO) {
|
||||
DerivationTool.deriveTransparentAddress(bip39Seed, persistableWallet.network)
|
||||
}.let {
|
||||
WalletAddress.Transparent.new(it)
|
||||
}
|
||||
|
||||
// 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",
|
||||
unified = WalletAddress.Unified.new("Unified GitHub Issue #161"),
|
||||
shieldedSapling = shieldedSaplingAddress,
|
||||
transparent = transparentAddress,
|
||||
viewingKey = viewingKey
|
||||
|
|
|
@ -71,6 +71,7 @@ dependencyResolutionManagement {
|
|||
val kotlinxCoroutinesVersion = extra["KOTLINX_COROUTINES_VERSION"].toString()
|
||||
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
|
||||
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
|
||||
val zxingVersion = extra["ZXING_VERSION"].toString()
|
||||
|
||||
// Standalone versions
|
||||
version("jacoco", jacocoVersion)
|
||||
|
@ -103,6 +104,7 @@ dependencyResolutionManagement {
|
|||
alias("zcash-sdk").to("cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||
alias("zcash-bip39").to("cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
|
||||
alias("zcash-walletplgns").to("cash.z.ecc.android:zcash-android-wallet-plugins:$zcashBip39Version")
|
||||
alias("zxing").to("com.google.zxing:core:$zxingVersion")
|
||||
// Test libraries
|
||||
alias("androidx-compose-test-junit").to("androidx.compose.ui:ui-test-junit4:$androidxComposeVersion")
|
||||
alias("androidx-compose-test-manifest").to("androidx.compose.ui:ui-test-manifest:$androidxComposeVersion")
|
||||
|
|
|
@ -31,6 +31,7 @@ android {
|
|||
"src/main/res/ui/common",
|
||||
"src/main/res/ui/home",
|
||||
"src/main/res/ui/onboarding",
|
||||
"src/main/res/ui/profile",
|
||||
"src/main/res/ui/restore",
|
||||
"src/main/res/ui/wallet_address"
|
||||
)
|
||||
|
@ -52,6 +53,7 @@ dependencies {
|
|||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.zcash.sdk)
|
||||
implementation(libs.zcash.bip39)
|
||||
implementation(libs.zxing)
|
||||
|
||||
implementation(projects.preferenceApiLib)
|
||||
implementation(projects.preferenceImplAndroidLib)
|
||||
|
|
|
@ -5,7 +5,7 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Test
|
||||
import kotlin.time.Duration
|
||||
|
||||
class MainActivityTest {
|
||||
class MainActivityCompanionTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
|
@ -0,0 +1,200 @@
|
|||
package cash.z.ecc.ui.screen.profile.view
|
||||
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.test.getStringResource
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/*
|
||||
* Note: It is difficult to test the QR code from automated tests. There is a manual test case
|
||||
* for that currently. A future enhancement could take a screenshot and try to analyze the
|
||||
* screenshot contents.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ProfileViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun setup() = runTest {
|
||||
val walletAddress = WalletAddressFixture.unified()
|
||||
newTestSetup(walletAddress)
|
||||
|
||||
// Enable substring for ellipsizing
|
||||
composeTestRule.onNodeWithText(walletAddress.address, substring = true).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressFixture.unified())
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.profile_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun address_details() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressFixture.unified())
|
||||
|
||||
assertEquals(0, testSetup.getOnAddressDetailsCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.profile_see_address_details)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnAddressDetailsCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun address_book() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressFixture.unified())
|
||||
|
||||
assertEquals(0, testSetup.getOnAddressBookCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.profile_address_book)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnAddressBookCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun settings() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressFixture.unified())
|
||||
|
||||
assertEquals(0, testSetup.getOnSettingsCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.profile_settings)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnSettingsCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun coinholder_vote() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressFixture.unified())
|
||||
|
||||
assertEquals(0, testSetup.getOnCoinholderVoteCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.profile_coinholder_vote)).also {
|
||||
it.performScrollTo()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnCoinholderVoteCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun support() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressFixture.unified())
|
||||
|
||||
assertEquals(0, testSetup.getOnSupportCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.profile_support)).also {
|
||||
it.performScrollTo()
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnSupportCount())
|
||||
}
|
||||
|
||||
private fun newTestSetup(walletAddress: WalletAddress) = TestSetup(composeTestRule, walletAddress)
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule, walletAddress: WalletAddress) {
|
||||
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onAddressDetailsCount = AtomicInteger(0)
|
||||
private val onAddressBookCount = AtomicInteger(0)
|
||||
private val onSettingsCount = AtomicInteger(0)
|
||||
private val onCoinholderVoteCount = AtomicInteger(0)
|
||||
private val onSupportCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
}
|
||||
|
||||
fun getOnAddressDetailsCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onAddressDetailsCount.get()
|
||||
}
|
||||
|
||||
fun getOnAddressBookCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onAddressBookCount.get()
|
||||
}
|
||||
|
||||
fun getOnSettingsCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onSettingsCount.get()
|
||||
}
|
||||
|
||||
fun getOnCoinholderVoteCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCoinholderVoteCount.get()
|
||||
}
|
||||
|
||||
fun getOnSupportCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onSupportCount.get()
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
Profile(
|
||||
walletAddress,
|
||||
onBack = {
|
||||
onBackCount.getAndIncrement()
|
||||
},
|
||||
onAddressDetails = {
|
||||
onAddressDetailsCount.getAndIncrement()
|
||||
},
|
||||
onAddressBook = {
|
||||
onAddressBookCount.getAndIncrement()
|
||||
},
|
||||
onSettings = {
|
||||
onSettingsCount.getAndIncrement()
|
||||
},
|
||||
onCoinholderVote = {
|
||||
onCoinholderVoteCount.getAndIncrement()
|
||||
},
|
||||
onSupport = {
|
||||
onSupportCount.getAndIncrement()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,26 +11,26 @@ 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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class WalletAddressViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun initial_screen_setup() {
|
||||
fun initial_screen_setup() = runTest {
|
||||
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()
|
||||
}
|
||||
|
@ -41,17 +41,14 @@ class WalletAddressViewTest {
|
|||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedOrchard).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
|
||||
|
@ -61,11 +58,11 @@ class WalletAddressViewTest {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun unified_collapses() {
|
||||
fun unified_collapses() = runTest {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
|
@ -74,38 +71,18 @@ class WalletAddressViewTest {
|
|||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun shielded_orchard_expands() {
|
||||
fun shielded_sapling_expands() = runTest {
|
||||
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 {
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
|
@ -114,18 +91,18 @@ class WalletAddressViewTest {
|
|||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun transparent_expands() {
|
||||
fun transparent_expands() = runTest {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
|
@ -134,14 +111,14 @@ class WalletAddressViewTest {
|
|||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
||||
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun viewing_expands() {
|
||||
fun viewing_expands() = runTest {
|
||||
val walletAddresses = WalletAddressesFixture.new()
|
||||
newTestSetup(walletAddresses)
|
||||
|
||||
|
@ -161,8 +138,8 @@ class WalletAddressViewTest {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back() {
|
||||
val testSetup = newTestSetup()
|
||||
fun back() = runTest {
|
||||
val testSetup = newTestSetup(WalletAddressesFixture.new())
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
|
@ -173,7 +150,7 @@ class WalletAddressViewTest {
|
|||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
private fun newTestSetup(initialState: WalletAddresses = WalletAddressesFixture.new()) = TestSetup(composeTestRule, initialState)
|
||||
private fun newTestSetup(initialState: WalletAddresses) = TestSetup(composeTestRule, initialState)
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule, initialState: WalletAddresses) {
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import cash.z.ecc.ui.screen.home.viewmodel.SecretState
|
|||
import cash.z.ecc.ui.screen.home.viewmodel.WalletViewModel
|
||||
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
|
||||
import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import cash.z.ecc.ui.screen.profile.view.Profile
|
||||
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
|
||||
|
@ -203,11 +204,21 @@ class MainActivity : ComponentActivity() {
|
|||
composable("home") {
|
||||
WrapHome(
|
||||
goScan = {},
|
||||
goProfile = { navController.navigate("wallet_address_details") },
|
||||
goProfile = { navController.navigate("profile") },
|
||||
goSend = {},
|
||||
goRequest = {}
|
||||
)
|
||||
}
|
||||
composable("profile") {
|
||||
WrapProfile(
|
||||
onBack = { navController.popBackStack() },
|
||||
onAddressDetails = { navController.navigate("wallet_address_details") },
|
||||
onAddressBook = { },
|
||||
onSettings = { },
|
||||
onCoinholderVote = { }
|
||||
) {
|
||||
}
|
||||
}
|
||||
composable("wallet_address_details") {
|
||||
WrapWalletAddresses(
|
||||
goBack = {
|
||||
|
@ -240,6 +251,32 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
private fun WrapProfile(
|
||||
onBack: () -> Unit,
|
||||
onAddressDetails: () -> Unit,
|
||||
onAddressBook: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
onCoinholderVote: () -> Unit,
|
||||
onSupport: () -> Unit
|
||||
) {
|
||||
val walletAddresses = walletViewModel.addresses.collectAsState().value
|
||||
if (null == walletAddresses) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
Profile(
|
||||
walletAddresses.unified,
|
||||
onBack = onBack,
|
||||
onAddressDetails = onAddressDetails,
|
||||
onAddressBook = onAddressBook,
|
||||
onSettings = onSettings,
|
||||
onCoinholderVote = onCoinholderVote,
|
||||
onSupport = onSupport
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapWalletAddresses(
|
||||
goBack: () -> Unit,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cash.z.ecc.ui.screen.common
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
|
@ -9,9 +10,25 @@ import androidx.compose.material.MaterialTheme
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ButtonComposablePreview() {
|
||||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
Column {
|
||||
PrimaryButton(onClick = { }, text = "Primary")
|
||||
SecondaryButton(onClick = { }, text = "Secondary")
|
||||
TertiaryButton(onClick = { }, text = "Tertiary")
|
||||
NavigationButton(onClick = { }, text = "Navigation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrimaryButton(
|
||||
onClick: () -> Unit,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package cash.z.ecc.ui.screen.profile.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
|
||||
object AndroidQrCodeImageGenerator : QrCodeImageGenerator {
|
||||
override fun generate(bitArray: BooleanArray, sizePixels: Int): ImageBitmap {
|
||||
val colorArray = bitArray.toBlackAndWhiteColorArray()
|
||||
|
||||
return Bitmap.createBitmap(colorArray, sizePixels, sizePixels, Bitmap.Config.ARGB_8888)
|
||||
.asImageBitmap()
|
||||
}
|
||||
}
|
||||
|
||||
private fun BooleanArray.toBlackAndWhiteColorArray() = IntArray(size) {
|
||||
if (this[it]) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package cash.z.ecc.ui.screen.profile.util
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
|
||||
object JvmQrCodeGenerator : QrCodeGenerator {
|
||||
override fun generate(data: String, sizePixels: Int): BooleanArray {
|
||||
val bitMatrix = QRCodeWriter().let {
|
||||
it.encode(data, BarcodeFormat.QR_CODE, sizePixels, sizePixels)
|
||||
}
|
||||
|
||||
return BooleanArray(sizePixels * sizePixels).apply {
|
||||
var booleanArrayPosition = 0
|
||||
for (bitMatrixX in 0 until sizePixels) {
|
||||
for (bitMatrixY in 0 until sizePixels) {
|
||||
this[booleanArrayPosition] = bitMatrix.get(bitMatrixX, bitMatrixY)
|
||||
booleanArrayPosition++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package cash.z.ecc.ui.screen.profile.util
|
||||
|
||||
interface QrCodeGenerator {
|
||||
/**
|
||||
* @param data Data to encode into the QR code.
|
||||
* @param sizePixels Size in pixels of the QR code.
|
||||
* @return A QR code pixel matrix, represented as an array of booleans where false is white and true is black.
|
||||
*/
|
||||
fun generate(data: String, sizePixels: Int): BooleanArray
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package cash.z.ecc.ui.screen.profile.util
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
interface QrCodeImageGenerator {
|
||||
fun generate(bitArray: BooleanArray, sizePixels: Int): ImageBitmap
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package cash.z.ecc.ui.screen.profile.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
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.PrimaryButton
|
||||
import cash.z.ecc.ui.screen.common.TertiaryButton
|
||||
import cash.z.ecc.ui.screen.profile.util.AndroidQrCodeImageGenerator
|
||||
import cash.z.ecc.ui.screen.profile.util.JvmQrCodeGenerator
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ComposablePreview() {
|
||||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
Profile(
|
||||
walletAddress = runBlocking { WalletAddressFixture.unified() },
|
||||
onBack = {},
|
||||
onAddressDetails = {},
|
||||
onAddressBook = {},
|
||||
onSettings = {},
|
||||
onCoinholderVote = {},
|
||||
onSupport = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun Profile(
|
||||
walletAddress: WalletAddress,
|
||||
onBack: () -> Unit,
|
||||
onAddressDetails: () -> Unit,
|
||||
onAddressBook: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
onCoinholderVote: () -> Unit,
|
||||
onSupport: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
ProfileTopAppBar(onBack)
|
||||
ProfileContents(
|
||||
walletAddress = walletAddress,
|
||||
onAddressDetails = onAddressDetails,
|
||||
onAddressBook = onAddressBook,
|
||||
onSettings = onSettings,
|
||||
onCoinholderVote = onCoinholderVote,
|
||||
onSupport = onSupport
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileTopAppBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.profile_title)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.profile_back_content_description),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val DEFAULT_QR_CODE_SIZE = 320.dp
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
private fun ProfileContents(
|
||||
walletAddress: WalletAddress,
|
||||
onAddressDetails: () -> Unit,
|
||||
onAddressBook: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
onCoinholderVote: () -> Unit,
|
||||
onSupport: () -> Unit
|
||||
) {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
QrCode(data = walletAddress.address, DEFAULT_QR_CODE_SIZE, Modifier.align(Alignment.CenterHorizontally))
|
||||
Body(text = stringResource(id = R.string.wallet_address_unified), Modifier.align(Alignment.CenterHorizontally))
|
||||
// TODO [#163]: Ellipsize center of the string
|
||||
Text(
|
||||
text = walletAddress.address,
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
PrimaryButton(onClick = onAddressDetails, text = stringResource(id = R.string.profile_see_address_details))
|
||||
TertiaryButton(onClick = onAddressBook, text = stringResource(id = R.string.profile_address_book))
|
||||
TertiaryButton(onClick = onSettings, text = stringResource(id = R.string.profile_settings))
|
||||
Divider()
|
||||
TertiaryButton(onClick = onCoinholderVote, text = stringResource(id = R.string.profile_coinholder_vote))
|
||||
TertiaryButton(onClick = onSupport, text = stringResource(id = R.string.profile_support))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QrCode(data: String, size: Dp, modifier: Modifier) {
|
||||
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
|
||||
|
||||
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
|
||||
|
||||
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
|
||||
// representation. This should have minimal performance impact since the QR code is relatively
|
||||
// small and we only generate QR codes infrequently.
|
||||
|
||||
val qrCodePixelArray = JvmQrCodeGenerator.generate(data, sizePixels)
|
||||
val qrCodeImage = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, sizePixels)
|
||||
|
||||
Image(
|
||||
bitmap = qrCodeImage,
|
||||
contentDescription = stringResource(R.string.profile_qr_code_content_description),
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
|
@ -14,7 +14,6 @@ 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
|
||||
|
@ -47,6 +46,7 @@ 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
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -54,7 +54,7 @@ fun ComposablePreview() {
|
|||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
WalletAddresses(
|
||||
WalletAddressesFixture.new(),
|
||||
runBlocking { WalletAddressesFixture.new() },
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
|
@ -79,7 +79,6 @@ private fun WalletDetailTopAppBar(onBack: () -> Unit) {
|
|||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
|
@ -114,7 +113,7 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
|||
Column(Modifier.fillMaxWidth()) {
|
||||
ExpandableRow(
|
||||
title = stringResource(R.string.wallet_address_unified),
|
||||
content = walletAddresses.unified,
|
||||
content = walletAddresses.unified.address,
|
||||
isInitiallyExpanded = true
|
||||
)
|
||||
|
||||
|
@ -123,9 +122,8 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
|||
ListHeader(text = stringResource(R.string.wallet_address_header_includes))
|
||||
}
|
||||
|
||||
OrchardAddress(walletAddresses.shieldedOrchard)
|
||||
SaplingAddress(walletAddresses.shieldedSapling)
|
||||
TransparentAddress(walletAddresses.transparent)
|
||||
SaplingAddress(walletAddresses.shieldedSapling.address)
|
||||
TransparentAddress(walletAddresses.transparent.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,22 +137,6 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
|||
// 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(
|
||||
|
|
|
@ -44,7 +44,6 @@ object Dark {
|
|||
|
||||
val addressHighlightBorder = Color(0xFF525252)
|
||||
val addressHighlightUnified = Color(0xFFFFD800)
|
||||
val addressHighlightOrchard = Color(0xFFFFD800)
|
||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||
val addressHighlightTransparent = Color(0xFF97999A)
|
||||
val addressHighlightViewing = Color(0xFF504062)
|
||||
|
@ -91,7 +90,6 @@ object Light {
|
|||
// [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)
|
||||
|
|
|
@ -49,7 +49,6 @@ data class ExtendedColors(
|
|||
val highlight: Color,
|
||||
val addressHighlightBorder: Color,
|
||||
val addressHighlightUnified: Color,
|
||||
val addressHighlightOrchard: Color,
|
||||
val addressHighlightSapling: Color,
|
||||
val addressHighlightTransparent: Color,
|
||||
val addressHighlightViewing: Color
|
||||
|
@ -78,7 +77,6 @@ val DarkExtendedColorPalette = ExtendedColors(
|
|||
highlight = Dark.highlight,
|
||||
addressHighlightBorder = Dark.addressHighlightBorder,
|
||||
addressHighlightUnified = Dark.addressHighlightUnified,
|
||||
addressHighlightOrchard = Dark.addressHighlightOrchard,
|
||||
addressHighlightSapling = Dark.addressHighlightSapling,
|
||||
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
||||
addressHighlightViewing = Dark.addressHighlightViewing
|
||||
|
@ -99,7 +97,6 @@ val LightExtendedColorPalette = ExtendedColors(
|
|||
highlight = Light.highlight,
|
||||
addressHighlightBorder = Light.addressHighlightBorder,
|
||||
addressHighlightUnified = Light.addressHighlightUnified,
|
||||
addressHighlightOrchard = Light.addressHighlightOrchard,
|
||||
addressHighlightSapling = Light.addressHighlightSapling,
|
||||
addressHighlightTransparent = Light.addressHighlightTransparent,
|
||||
addressHighlightViewing = Light.addressHighlightViewing
|
||||
|
@ -121,7 +118,6 @@ val LocalExtendedColors = staticCompositionLocalOf {
|
|||
highlight = Color.Unspecified,
|
||||
addressHighlightBorder = Color.Unspecified,
|
||||
addressHighlightUnified = Color.Unspecified,
|
||||
addressHighlightOrchard = Color.Unspecified,
|
||||
addressHighlightSapling = Color.Unspecified,
|
||||
addressHighlightTransparent = Color.Unspecified,
|
||||
addressHighlightViewing = Color.Unspecified
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="profile_title">Profile</string>
|
||||
<string name="profile_back_content_description">Back</string>
|
||||
<string name="profile_qr_code_content_description">QR code for unified address</string>
|
||||
<string name="profile_caption">Your UA Address</string>
|
||||
<string name="profile_see_address_details">See Address Details</string>
|
||||
<string name="profile_address_book">Address Book</string>
|
||||
<string name="profile_settings">Settings</string>
|
||||
<string name="profile_coinholder_vote">Coinholder Vote</string>
|
||||
<string name="profile_support">Support</string>
|
||||
|
||||
</resources>
|
|
@ -4,7 +4,6 @@
|
|||
<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>
|
||||
|
|
Loading…
Reference in New Issue