[#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_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
||||||
ZCASH_BIP39_VERSION=1.0.2
|
ZCASH_BIP39_VERSION=1.0.2
|
||||||
ZCASH_SDK_VERSION=1.3.0-beta19
|
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
|
# 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.
|
# 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.Assert.assertFalse
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class WalletAddressesTest {
|
class WalletAddressesTest {
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun security() {
|
fun security() = runTest {
|
||||||
val walletAddresses = WalletAddressesFixture.new()
|
val walletAddresses = WalletAddressesFixture.new()
|
||||||
val actual = WalletAddressesFixture.new().toString()
|
val actual = WalletAddressesFixture.new().toString()
|
||||||
assertFalse(actual.contains(walletAddresses.shieldedOrchard))
|
assertFalse(actual.contains(walletAddresses.shieldedSapling.address))
|
||||||
assertFalse(actual.contains(walletAddresses.shieldedSapling))
|
assertFalse(actual.contains(walletAddresses.transparent.address))
|
||||||
assertFalse(actual.contains(walletAddresses.transparent))
|
assertFalse(actual.contains(walletAddresses.unified.address))
|
||||||
assertFalse(actual.contains(walletAddresses.unified))
|
|
||||||
assertFalse(actual.contains(walletAddresses.viewingKey))
|
assertFalse(actual.contains(walletAddresses.viewingKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun new() = runTest {
|
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
|
package cash.z.ecc.sdk.fixture
|
||||||
|
|
||||||
|
import cash.z.ecc.sdk.model.WalletAddress
|
||||||
import cash.z.ecc.sdk.model.WalletAddresses
|
import cash.z.ecc.sdk.model.WalletAddresses
|
||||||
|
|
||||||
object WalletAddressesFixture {
|
object WalletAddressesFixture {
|
||||||
// These fixture values are derived from the secret defined in PersistableWalletFixture
|
// 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"
|
const val VIEWING_KEY = "03feaa290589a20f795f302ba03847b0a6c9c2b571d75d80bc4ebb02382d0549da"
|
||||||
|
|
||||||
fun new(
|
suspend fun new(
|
||||||
unified: String = UNIFIED,
|
unified: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING,
|
||||||
shieldedOrchard: String = SHIELDED_ORCHARD,
|
shieldedSapling: String = WalletAddressFixture.SHIELDED_SAPLING_ADDRESS_STRING,
|
||||||
shieldedSapling: String = SHIELDED_SAPLING,
|
transparent: String = WalletAddressFixture.TRANSPARENT_ADDRESS_STRING,
|
||||||
transparent: String = TRANSPARENT,
|
|
||||||
viewingKey: String = VIEWING_KEY
|
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
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
data class WalletAddresses(
|
data class WalletAddresses(
|
||||||
val unified: String,
|
val unified: WalletAddress.Unified,
|
||||||
val shieldedOrchard: String,
|
val shieldedSapling: WalletAddress.ShieldedSapling,
|
||||||
val shieldedSapling: String,
|
val transparent: WalletAddress.Transparent,
|
||||||
val transparent: String,
|
|
||||||
val viewingKey: String
|
val viewingKey: String
|
||||||
) {
|
) {
|
||||||
// Override to prevent leaking details in logs
|
// Override to prevent leaking details in logs
|
||||||
|
@ -32,16 +31,19 @@ data class WalletAddresses(
|
||||||
|
|
||||||
val shieldedSaplingAddress = withContext(Dispatchers.IO) {
|
val shieldedSaplingAddress = withContext(Dispatchers.IO) {
|
||||||
DerivationTool.deriveShieldedAddress(bip39Seed, persistableWallet.network)
|
DerivationTool.deriveShieldedAddress(bip39Seed, persistableWallet.network)
|
||||||
|
}.let {
|
||||||
|
WalletAddress.ShieldedSapling.new(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val transparentAddress = withContext(Dispatchers.IO) {
|
val transparentAddress = withContext(Dispatchers.IO) {
|
||||||
DerivationTool.deriveTransparentAddress(bip39Seed, persistableWallet.network)
|
DerivationTool.deriveTransparentAddress(bip39Seed, persistableWallet.network)
|
||||||
|
}.let {
|
||||||
|
WalletAddress.Transparent.new(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO [#161]: Pending SDK support, fix providing correct values for the unified
|
// TODO [#161]: Pending SDK support, fix providing correct values for the unified
|
||||||
return WalletAddresses(
|
return WalletAddresses(
|
||||||
unified = "Unified GitHub Issue #161",
|
unified = WalletAddress.Unified.new("Unified GitHub Issue #161"),
|
||||||
shieldedOrchard = "Shielded Orchard GitHub Issue #161",
|
|
||||||
shieldedSapling = shieldedSaplingAddress,
|
shieldedSapling = shieldedSaplingAddress,
|
||||||
transparent = transparentAddress,
|
transparent = transparentAddress,
|
||||||
viewingKey = viewingKey
|
viewingKey = viewingKey
|
||||||
|
|
|
@ -71,6 +71,7 @@ dependencyResolutionManagement {
|
||||||
val kotlinxCoroutinesVersion = extra["KOTLINX_COROUTINES_VERSION"].toString()
|
val kotlinxCoroutinesVersion = extra["KOTLINX_COROUTINES_VERSION"].toString()
|
||||||
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
|
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
|
||||||
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
|
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
|
||||||
|
val zxingVersion = extra["ZXING_VERSION"].toString()
|
||||||
|
|
||||||
// Standalone versions
|
// Standalone versions
|
||||||
version("jacoco", jacocoVersion)
|
version("jacoco", jacocoVersion)
|
||||||
|
@ -103,6 +104,7 @@ dependencyResolutionManagement {
|
||||||
alias("zcash-sdk").to("cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
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-bip39").to("cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
|
||||||
alias("zcash-walletplgns").to("cash.z.ecc.android:zcash-android-wallet-plugins:$zcashBip39Version")
|
alias("zcash-walletplgns").to("cash.z.ecc.android:zcash-android-wallet-plugins:$zcashBip39Version")
|
||||||
|
alias("zxing").to("com.google.zxing:core:$zxingVersion")
|
||||||
// Test libraries
|
// Test libraries
|
||||||
alias("androidx-compose-test-junit").to("androidx.compose.ui:ui-test-junit4:$androidxComposeVersion")
|
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")
|
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/common",
|
||||||
"src/main/res/ui/home",
|
"src/main/res/ui/home",
|
||||||
"src/main/res/ui/onboarding",
|
"src/main/res/ui/onboarding",
|
||||||
|
"src/main/res/ui/profile",
|
||||||
"src/main/res/ui/restore",
|
"src/main/res/ui/restore",
|
||||||
"src/main/res/ui/wallet_address"
|
"src/main/res/ui/wallet_address"
|
||||||
)
|
)
|
||||||
|
@ -52,6 +53,7 @@ dependencies {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.zcash.sdk)
|
implementation(libs.zcash.sdk)
|
||||||
implementation(libs.zcash.bip39)
|
implementation(libs.zcash.bip39)
|
||||||
|
implementation(libs.zxing)
|
||||||
|
|
||||||
implementation(projects.preferenceApiLib)
|
implementation(projects.preferenceApiLib)
|
||||||
implementation(projects.preferenceImplAndroidLib)
|
implementation(projects.preferenceImplAndroidLib)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class MainActivityTest {
|
class MainActivityCompanionTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@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.R
|
||||||
import cash.z.ecc.ui.test.getStringResource
|
import cash.z.ecc.ui.test.getStringResource
|
||||||
import cash.z.ecc.ui.theme.ZcashTheme
|
import cash.z.ecc.ui.theme.ZcashTheme
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class WalletAddressViewTest {
|
class WalletAddressViewTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun initial_screen_setup() {
|
fun initial_screen_setup() = runTest {
|
||||||
val walletAddresses = WalletAddressesFixture.new()
|
val walletAddresses = WalletAddressesFixture.new()
|
||||||
newTestSetup(walletAddresses)
|
newTestSetup(walletAddresses)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
|
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_orchard)).also {
|
|
||||||
it.assertExists()
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_sapling)).also {
|
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_sapling)).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
@ -41,17 +41,14 @@ class WalletAddressViewTest {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.shieldedOrchard).also {
|
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
|
||||||
it.assertDoesNotExist()
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
|
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
|
||||||
|
@ -61,11 +58,11 @@ class WalletAddressViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun unified_collapses() {
|
fun unified_collapses() = runTest {
|
||||||
val walletAddresses = WalletAddressesFixture.new()
|
val walletAddresses = WalletAddressesFixture.new()
|
||||||
newTestSetup(walletAddresses)
|
newTestSetup(walletAddresses)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,38 +71,18 @@ class WalletAddressViewTest {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.unified).also {
|
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun shielded_orchard_expands() {
|
fun shielded_sapling_expands() = runTest {
|
||||||
val walletAddresses = WalletAddressesFixture.new()
|
val walletAddresses = WalletAddressesFixture.new()
|
||||||
newTestSetup(walletAddresses)
|
newTestSetup(walletAddresses)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.shieldedOrchard).also {
|
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).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()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,18 +91,18 @@ class WalletAddressViewTest {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling).also {
|
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun transparent_expands() {
|
fun transparent_expands() = runTest {
|
||||||
val walletAddresses = WalletAddressesFixture.new()
|
val walletAddresses = WalletAddressesFixture.new()
|
||||||
newTestSetup(walletAddresses)
|
newTestSetup(walletAddresses)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,14 +111,14 @@ class WalletAddressViewTest {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(walletAddresses.transparent).also {
|
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun viewing_expands() {
|
fun viewing_expands() = runTest {
|
||||||
val walletAddresses = WalletAddressesFixture.new()
|
val walletAddresses = WalletAddressesFixture.new()
|
||||||
newTestSetup(walletAddresses)
|
newTestSetup(walletAddresses)
|
||||||
|
|
||||||
|
@ -161,8 +138,8 @@ class WalletAddressViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun back() {
|
fun back() = runTest {
|
||||||
val testSetup = newTestSetup()
|
val testSetup = newTestSetup(WalletAddressesFixture.new())
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
|
|
||||||
|
@ -173,7 +150,7 @@ class WalletAddressViewTest {
|
||||||
assertEquals(1, testSetup.getOnBackCount())
|
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) {
|
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.home.viewmodel.WalletViewModel
|
||||||
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
|
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
|
||||||
import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
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.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
|
||||||
|
@ -203,11 +204,21 @@ class MainActivity : ComponentActivity() {
|
||||||
composable("home") {
|
composable("home") {
|
||||||
WrapHome(
|
WrapHome(
|
||||||
goScan = {},
|
goScan = {},
|
||||||
goProfile = { navController.navigate("wallet_address_details") },
|
goProfile = { navController.navigate("profile") },
|
||||||
goSend = {},
|
goSend = {},
|
||||||
goRequest = {}
|
goRequest = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable("profile") {
|
||||||
|
WrapProfile(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onAddressDetails = { navController.navigate("wallet_address_details") },
|
||||||
|
onAddressBook = { },
|
||||||
|
onSettings = { },
|
||||||
|
onCoinholderVote = { }
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
composable("wallet_address_details") {
|
composable("wallet_address_details") {
|
||||||
WrapWalletAddresses(
|
WrapWalletAddresses(
|
||||||
goBack = {
|
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
|
@Composable
|
||||||
private fun WrapWalletAddresses(
|
private fun WrapWalletAddresses(
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package cash.z.ecc.ui.screen.common
|
package cash.z.ecc.ui.screen.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
|
@ -9,9 +10,25 @@ import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cash.z.ecc.ui.theme.ZcashTheme
|
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
|
@Composable
|
||||||
fun PrimaryButton(
|
fun PrimaryButton(
|
||||||
onClick: () -> Unit,
|
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.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.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.Icon
|
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.screen.common.ListItem
|
||||||
import cash.z.ecc.ui.theme.MINIMAL_WEIGHT
|
import cash.z.ecc.ui.theme.MINIMAL_WEIGHT
|
||||||
import cash.z.ecc.ui.theme.ZcashTheme
|
import cash.z.ecc.ui.theme.ZcashTheme
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -54,7 +54,7 @@ fun ComposablePreview() {
|
||||||
ZcashTheme(darkTheme = true) {
|
ZcashTheme(darkTheme = true) {
|
||||||
GradientSurface {
|
GradientSurface {
|
||||||
WalletAddresses(
|
WalletAddresses(
|
||||||
WalletAddressesFixture.new(),
|
runBlocking { WalletAddressesFixture.new() },
|
||||||
onBack = {}
|
onBack = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,6 @@ private fun WalletDetailTopAppBar(onBack: () -> Unit) {
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
onClick = onBack
|
onClick = onBack
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -114,7 +113,7 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
ExpandableRow(
|
ExpandableRow(
|
||||||
title = stringResource(R.string.wallet_address_unified),
|
title = stringResource(R.string.wallet_address_unified),
|
||||||
content = walletAddresses.unified,
|
content = walletAddresses.unified.address,
|
||||||
isInitiallyExpanded = true
|
isInitiallyExpanded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,9 +122,8 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
||||||
ListHeader(text = stringResource(R.string.wallet_address_header_includes))
|
ListHeader(text = stringResource(R.string.wallet_address_header_includes))
|
||||||
}
|
}
|
||||||
|
|
||||||
OrchardAddress(walletAddresses.shieldedOrchard)
|
SaplingAddress(walletAddresses.shieldedSapling.address)
|
||||||
SaplingAddress(walletAddresses.shieldedSapling)
|
TransparentAddress(walletAddresses.transparent.address)
|
||||||
TransparentAddress(walletAddresses.transparent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,22 +137,6 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
|
||||||
// Refactoring that is being held off until issue #160 is fixed, since knowledge
|
// Refactoring that is being held off until issue #160 is fixed, since knowledge
|
||||||
// of row position will be needed.
|
// 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
|
@Composable
|
||||||
private fun SaplingAddress(saplingAddress: String) {
|
private fun SaplingAddress(saplingAddress: String) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
@ -44,7 +44,6 @@ object Dark {
|
||||||
|
|
||||||
val addressHighlightBorder = Color(0xFF525252)
|
val addressHighlightBorder = Color(0xFF525252)
|
||||||
val addressHighlightUnified = Color(0xFFFFD800)
|
val addressHighlightUnified = Color(0xFFFFD800)
|
||||||
val addressHighlightOrchard = Color(0xFFFFD800)
|
|
||||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||||
val addressHighlightTransparent = Color(0xFF97999A)
|
val addressHighlightTransparent = Color(0xFF97999A)
|
||||||
val addressHighlightViewing = Color(0xFF504062)
|
val addressHighlightViewing = Color(0xFF504062)
|
||||||
|
@ -91,7 +90,6 @@ object Light {
|
||||||
// [TODO #159]: The colors are wrong for light theme
|
// [TODO #159]: The colors are wrong for light theme
|
||||||
val addressHighlightBorder = Color(0xFF525252)
|
val addressHighlightBorder = Color(0xFF525252)
|
||||||
val addressHighlightUnified = Color(0xFFFFD800)
|
val addressHighlightUnified = Color(0xFFFFD800)
|
||||||
val addressHighlightOrchard = Color(0xFFFFD800)
|
|
||||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||||
val addressHighlightTransparent = Color(0xFF97999A)
|
val addressHighlightTransparent = Color(0xFF97999A)
|
||||||
val addressHighlightViewing = Color(0xFF504062)
|
val addressHighlightViewing = Color(0xFF504062)
|
||||||
|
|
|
@ -49,7 +49,6 @@ data class ExtendedColors(
|
||||||
val highlight: Color,
|
val highlight: Color,
|
||||||
val addressHighlightBorder: Color,
|
val addressHighlightBorder: Color,
|
||||||
val addressHighlightUnified: Color,
|
val addressHighlightUnified: Color,
|
||||||
val addressHighlightOrchard: Color,
|
|
||||||
val addressHighlightSapling: Color,
|
val addressHighlightSapling: Color,
|
||||||
val addressHighlightTransparent: Color,
|
val addressHighlightTransparent: Color,
|
||||||
val addressHighlightViewing: Color
|
val addressHighlightViewing: Color
|
||||||
|
@ -78,7 +77,6 @@ val DarkExtendedColorPalette = ExtendedColors(
|
||||||
highlight = Dark.highlight,
|
highlight = Dark.highlight,
|
||||||
addressHighlightBorder = Dark.addressHighlightBorder,
|
addressHighlightBorder = Dark.addressHighlightBorder,
|
||||||
addressHighlightUnified = Dark.addressHighlightUnified,
|
addressHighlightUnified = Dark.addressHighlightUnified,
|
||||||
addressHighlightOrchard = Dark.addressHighlightOrchard,
|
|
||||||
addressHighlightSapling = Dark.addressHighlightSapling,
|
addressHighlightSapling = Dark.addressHighlightSapling,
|
||||||
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
||||||
addressHighlightViewing = Dark.addressHighlightViewing
|
addressHighlightViewing = Dark.addressHighlightViewing
|
||||||
|
@ -99,7 +97,6 @@ val LightExtendedColorPalette = ExtendedColors(
|
||||||
highlight = Light.highlight,
|
highlight = Light.highlight,
|
||||||
addressHighlightBorder = Light.addressHighlightBorder,
|
addressHighlightBorder = Light.addressHighlightBorder,
|
||||||
addressHighlightUnified = Light.addressHighlightUnified,
|
addressHighlightUnified = Light.addressHighlightUnified,
|
||||||
addressHighlightOrchard = Light.addressHighlightOrchard,
|
|
||||||
addressHighlightSapling = Light.addressHighlightSapling,
|
addressHighlightSapling = Light.addressHighlightSapling,
|
||||||
addressHighlightTransparent = Light.addressHighlightTransparent,
|
addressHighlightTransparent = Light.addressHighlightTransparent,
|
||||||
addressHighlightViewing = Light.addressHighlightViewing
|
addressHighlightViewing = Light.addressHighlightViewing
|
||||||
|
@ -121,7 +118,6 @@ val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
highlight = Color.Unspecified,
|
highlight = Color.Unspecified,
|
||||||
addressHighlightBorder = Color.Unspecified,
|
addressHighlightBorder = Color.Unspecified,
|
||||||
addressHighlightUnified = Color.Unspecified,
|
addressHighlightUnified = Color.Unspecified,
|
||||||
addressHighlightOrchard = Color.Unspecified,
|
|
||||||
addressHighlightSapling = Color.Unspecified,
|
addressHighlightSapling = Color.Unspecified,
|
||||||
addressHighlightTransparent = Color.Unspecified,
|
addressHighlightTransparent = Color.Unspecified,
|
||||||
addressHighlightViewing = 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_back_content_description">Back</string>
|
||||||
<string name="wallet_address_unified">Your Unified Address</string>
|
<string name="wallet_address_unified">Your Unified Address</string>
|
||||||
<string name="wallet_address_header_includes">which includes</string>
|
<string name="wallet_address_header_includes">which includes</string>
|
||||||
<string name="wallet_address_shielded_orchard">Shielded Orchard (NU5)</string>
|
|
||||||
<string name="wallet_address_shielded_sapling">Shielded Sapling (NU1)</string>
|
<string name="wallet_address_shielded_sapling">Shielded Sapling (NU1)</string>
|
||||||
<string name="wallet_address_transparent">Transparent</string>
|
<string name="wallet_address_transparent">Transparent</string>
|
||||||
<string name="wallet_address_viewing_key">Viewing Key Only (Sapling)</string>
|
<string name="wallet_address_viewing_key">Viewing Key Only (Sapling)</string>
|
||||||
|
|
Loading…
Reference in New Issue