Settings which are not yet implemented have not been included to not introduce non-functional buttons into the UI. Followup issue #38 covers one of the key settings, which is how we'll implement authorization in the app
This commit is contained in:
parent
52b6382d47
commit
012839841d
|
@ -32,6 +32,8 @@ android {
|
|||
"src/main/res/ui/onboarding",
|
||||
"src/main/res/ui/profile",
|
||||
"src/main/res/ui/restore",
|
||||
"src/main/res/ui/seed",
|
||||
"src/main/res/ui/settings",
|
||||
"src/main/res/ui/wallet_address"
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package cash.z.ecc.ui.screen.seed.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.PersistableWalletFixture
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SeedViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.seed_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun copyToClipboard() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getCopyToClipboardCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.seed_copy)).also {
|
||||
it.performScrollTo()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getCopyToClipboardCount())
|
||||
}
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private var onBackCount = AtomicInteger(0)
|
||||
private var onCopyToClipboardCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
}
|
||||
|
||||
fun getCopyToClipboardCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCopyToClipboardCount.get()
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
Seed(
|
||||
PersistableWalletFixture.new(),
|
||||
onBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onCopyToClipboard = {
|
||||
onCopyToClipboardCount.incrementAndGet()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package cash.z.ecc.ui.screen.settings.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.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
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SettingsViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.settings_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun backup() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getBackupCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.settings_backup)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getBackupCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun rescan() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getBackupCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getRescanCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun wipe() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getBackupCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.settings_wipe)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getWipeCount())
|
||||
}
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private var onBackCount = AtomicInteger(0)
|
||||
private var onBackupCount = AtomicInteger(0)
|
||||
private var onRescanCount = AtomicInteger(0)
|
||||
private var onWipeCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
}
|
||||
|
||||
fun getBackupCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackupCount.get()
|
||||
}
|
||||
|
||||
fun getRescanCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onRescanCount.get()
|
||||
}
|
||||
|
||||
fun getWipeCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onWipeCount.get()
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
Settings(
|
||||
onBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onBackupWallet = {
|
||||
onBackupCount.incrementAndGet()
|
||||
},
|
||||
onRescanWallet = {
|
||||
onRescanCount.incrementAndGet()
|
||||
},
|
||||
onWipeWallet = {
|
||||
onWipeCount.incrementAndGet()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,6 +38,8 @@ 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
|
||||
import cash.z.ecc.ui.screen.seed.view.Seed
|
||||
import cash.z.ecc.ui.screen.settings.view.Settings
|
||||
import cash.z.ecc.ui.screen.wallet_address.view.WalletAddresses
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import cash.z.ecc.ui.util.AndroidApiVersion
|
||||
|
@ -48,6 +50,7 @@ import kotlin.time.Duration
|
|||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val walletViewModel by viewModels<WalletViewModel>()
|
||||
|
@ -141,12 +144,7 @@ class MainActivity : ComponentActivity() {
|
|||
BackupWallet(
|
||||
persistableWallet, backupViewModel.backupState, backupViewModel.testChoices,
|
||||
onCopyToClipboard = {
|
||||
val clipboardManager = getSystemService(ClipboardManager::class.java)
|
||||
val data = ClipData.newPlainText(
|
||||
getString(R.string.new_wallet_clipboard_tag),
|
||||
persistableWallet.seedPhrase.joinToString()
|
||||
)
|
||||
clipboardManager.setPrimaryClip(data)
|
||||
copyToClipboard(applicationContext, persistableWallet)
|
||||
}, onComplete = {
|
||||
walletViewModel.persistBackupComplete()
|
||||
}
|
||||
|
@ -200,32 +198,55 @@ class MainActivity : ComponentActivity() {
|
|||
private fun Navigation() {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = "home") {
|
||||
composable("home") {
|
||||
val home = "home"
|
||||
val profile = "profile"
|
||||
val walletAddressDetails = "wallet_address_details"
|
||||
val settings = "settings"
|
||||
val seed = "seed"
|
||||
|
||||
NavHost(navController = navController, startDestination = home) {
|
||||
composable(home) {
|
||||
WrapHome(
|
||||
goScan = {},
|
||||
goProfile = { navController.navigate("profile") },
|
||||
goProfile = { navController.navigate(profile) },
|
||||
goSend = {},
|
||||
goRequest = {}
|
||||
)
|
||||
}
|
||||
composable("profile") {
|
||||
composable(profile) {
|
||||
WrapProfile(
|
||||
onBack = { navController.popBackStack() },
|
||||
onAddressDetails = { navController.navigate("wallet_address_details") },
|
||||
onAddressDetails = { navController.navigate(walletAddressDetails) },
|
||||
onAddressBook = { },
|
||||
onSettings = { },
|
||||
onCoinholderVote = { }
|
||||
) {
|
||||
}
|
||||
onSettings = { navController.navigate(settings) },
|
||||
onCoinholderVote = { },
|
||||
onSupport = {}
|
||||
)
|
||||
}
|
||||
composable("wallet_address_details") {
|
||||
composable(walletAddressDetails) {
|
||||
WrapWalletAddresses(
|
||||
goBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(settings) {
|
||||
WrapSettings(
|
||||
goBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
goWalletBackup = {
|
||||
navController.navigate(seed)
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(seed) {
|
||||
WrapSeed(
|
||||
goBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,12 +313,74 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapSettings(
|
||||
goBack: () -> Unit,
|
||||
goWalletBackup: () -> Unit
|
||||
) {
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsState().value
|
||||
if (null == synchronizer) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
Settings(
|
||||
onBack = goBack,
|
||||
onBackupWallet = goWalletBackup,
|
||||
onRescanWallet = {
|
||||
walletViewModel.rescanBlockchain()
|
||||
}, onWipeWallet = {
|
||||
walletViewModel.wipeWallet()
|
||||
|
||||
// If wipe ever becomes an operation to also delete the seed, then we'll also need
|
||||
// to do the following to clear any retained state from onboarding (only happens if
|
||||
// occuring during same session as onboarding)
|
||||
// onboardingViewModel.onboardingState.goToBeginning()
|
||||
// onboardingViewModel.isImporting.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapSeed(
|
||||
goBack: () -> Unit
|
||||
) {
|
||||
val persistableWallet = run {
|
||||
val secretState = walletViewModel.secretState.collectAsState().value
|
||||
if (secretState is SecretState.Ready) {
|
||||
secretState.persistableWallet
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsState().value
|
||||
if (null == synchronizer || null == persistableWallet) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
Seed(
|
||||
persistableWallet = persistableWallet,
|
||||
onBack = goBack,
|
||||
onCopyToClipboard = {
|
||||
copyToClipboard(applicationContext, persistableWallet)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
internal val SPLASH_SCREEN_DELAY = 0.seconds
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(context: Context, persistableWallet: PersistableWallet) {
|
||||
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
|
||||
val data = ClipData.newPlainText(
|
||||
context.getString(R.string.new_wallet_clipboard_tag),
|
||||
persistableWallet.seedPhrase.joinToString()
|
||||
)
|
||||
clipboardManager.setPrimaryClip(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fetches fonts on Android N (API 25) and below.
|
||||
*/
|
||||
|
|
|
@ -104,3 +104,22 @@ fun TertiaryButton(
|
|||
Text(style = MaterialTheme.typography.button, text = text, color = ZcashTheme.colors.onTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DangerousButton(
|
||||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.then(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
),
|
||||
colors = buttonColors(backgroundColor = ZcashTheme.colors.dangerous)
|
||||
) {
|
||||
Text(style = MaterialTheme.typography.button, text = text, color = ZcashTheme.colors.onDangerous)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.ui.screen.home.viewmodel
|
|||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||
|
@ -10,9 +11,11 @@ import cash.z.ecc.android.sdk.db.entity.Transaction
|
|||
import cash.z.ecc.android.sdk.db.entity.isMined
|
||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.sdk.SynchronizerCompanion
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import cash.z.ecc.ui.common.ANDROID_STATE_FLOW_TIMEOUT_MILLIS
|
||||
import cash.z.ecc.ui.preference.EncryptedPreferenceKeys
|
||||
import cash.z.ecc.ui.preference.EncryptedPreferenceSingleton
|
||||
|
@ -46,6 +49,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
* that they have a consistent ordering.
|
||||
*/
|
||||
private val persistWalletMutex = Mutex()
|
||||
private val synchronizerMutex = Mutex()
|
||||
|
||||
/**
|
||||
* A flow of the user's stored wallet. Null indicates that no wallet has been stored.
|
||||
|
@ -88,9 +92,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
.filterIsInstance<SecretState.Ready>()
|
||||
.flatMapConcat {
|
||||
callbackFlow {
|
||||
val synchronizer = SynchronizerCompanion.load(application, it.persistableWallet)
|
||||
val synchronizer = synchronizerMutex.withLock {
|
||||
val synchronizer = SynchronizerCompanion.load(application, it.persistableWallet)
|
||||
|
||||
synchronizer.start(viewModelScope)
|
||||
synchronizer.start(viewModelScope)
|
||||
}
|
||||
|
||||
trySend(synchronizer)
|
||||
awaitClose {
|
||||
|
@ -183,6 +189,67 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method only has an effect if the synchronizer currently is loaded.
|
||||
*/
|
||||
fun rescanBlockchain() {
|
||||
viewModelScope.launch {
|
||||
synchronizerMutex.withLock {
|
||||
synchronizer.value?.let {
|
||||
it.rewindToNearestHeight(it.latestBirthdayHeight, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This asynchronously wipes the wallet state.
|
||||
*
|
||||
* This method only has an effect if the synchronizer currently is loaded.
|
||||
*/
|
||||
fun wipeWallet() {
|
||||
/*
|
||||
* This implementation could perhaps be a little brittle due to needing to stop and start the
|
||||
* synchronizer. If another client is interacting with the synchronizer at the same time,
|
||||
* it isn't well defined exactly what the behavior should be.
|
||||
*
|
||||
* Possible enhancements to improve this:
|
||||
* - Hide the synchronizer from clients; prefer to add additional APIs to WalletViewModel
|
||||
* which delegate to the synchronizer
|
||||
* - Add a private StateFlow to WalletViewModel to signal internal operations which should
|
||||
* cancel the synchronizer for other observers. Modify synchronizer flow to use a combine
|
||||
* operator to check the private stateflow. When initiating a wipe, set that private
|
||||
* StateFlow to cancel other observers of the synchronizer.
|
||||
*/
|
||||
|
||||
viewModelScope.launch {
|
||||
synchronizerMutex.withLock {
|
||||
synchronizer.value?.let {
|
||||
// There is a minor race condition here. With the right timing, it is possible
|
||||
// that the collection of the Synchronizer flow is canceled during an erase.
|
||||
// In such a situation, the Synchronizer would be restarted at the end of
|
||||
// this method even though it shouldn't. Overall it shouldn't be too harmful,
|
||||
// since the viewModelScope would still eventually be canceled.
|
||||
// By at least checking for referential equality at the end, we can reduce that
|
||||
// timing gap.
|
||||
val wasStarted = it.isStarted
|
||||
if (wasStarted) {
|
||||
it.stop()
|
||||
}
|
||||
|
||||
Initializer.erase(
|
||||
getApplication(),
|
||||
ZcashNetwork.fromResources(getApplication())
|
||||
)
|
||||
|
||||
if (wasStarted && synchronizer.value === it) {
|
||||
it.start(viewModelScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,6 +23,10 @@ class OnboardingState(initialState: OnboardingStage = OnboardingStage.values().f
|
|||
mutableState.value = current.value.getPrevious()
|
||||
}
|
||||
|
||||
fun goToBeginning() {
|
||||
mutableState.value = OnboardingStage.values().first()
|
||||
}
|
||||
|
||||
fun goToEnd() {
|
||||
mutableState.value = OnboardingStage.values().last()
|
||||
}
|
||||
|
|
|
@ -98,7 +98,6 @@ fun PreviewRestoreComplete() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
@Composable
|
||||
fun RestoreWallet(
|
||||
completeWordList: Set<String>,
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package cash.z.ecc.ui.screen.seed.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.screen.common.Body
|
||||
import cash.z.ecc.ui.screen.common.ChipGrid
|
||||
import cash.z.ecc.ui.screen.common.GradientSurface
|
||||
import cash.z.ecc.ui.screen.common.Header
|
||||
import cash.z.ecc.ui.screen.common.TertiaryButton
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
|
||||
@Preview("Seed")
|
||||
@Composable
|
||||
fun PreviewSeed() {
|
||||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
Seed(
|
||||
persistableWallet = PersistableWalletFixture.new(),
|
||||
onBack = {},
|
||||
onCopyToClipboard = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Note we have some things to determine regarding locking of the secrets for persistableWallet
|
||||
* (e.g. seed phrase and spending keys) which should require additional authorization to view.
|
||||
*/
|
||||
@Composable
|
||||
fun Seed(
|
||||
persistableWallet: PersistableWallet,
|
||||
onBack: () -> Unit,
|
||||
onCopyToClipboard: () -> Unit
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
SeedTopAppBar(onBack = onBack)
|
||||
}) {
|
||||
SeedMainContent(persistableWallet = persistableWallet, onCopyToClipboard = onCopyToClipboard)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeedTopAppBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.seed_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.seed_back_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeedMainContent(
|
||||
persistableWallet: PersistableWallet,
|
||||
onCopyToClipboard: () -> Unit
|
||||
) {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Header(stringResource(R.string.seed_header))
|
||||
Body(stringResource(R.string.seed_body))
|
||||
|
||||
ChipGrid(persistableWallet.seedPhrase.split)
|
||||
|
||||
TertiaryButton(onClick = onCopyToClipboard, text = stringResource(R.string.seed_copy))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package cash.z.ecc.ui.screen.settings.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.screen.common.DangerousButton
|
||||
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.theme.ZcashTheme
|
||||
|
||||
@Preview("Settings")
|
||||
@Composable
|
||||
fun PreviewSettings() {
|
||||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
Settings(
|
||||
onBack = {},
|
||||
onBackupWallet = {},
|
||||
onWipeWallet = {},
|
||||
onRescanWallet = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Settings(
|
||||
onBack: () -> Unit,
|
||||
onBackupWallet: () -> Unit,
|
||||
onWipeWallet: () -> Unit,
|
||||
onRescanWallet: () -> Unit,
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
SettingsTopAppBar(onBack = onBack)
|
||||
}) {
|
||||
SettingsMainContent(
|
||||
onBackupWallet = onBackupWallet,
|
||||
onWipeWallet = onWipeWallet,
|
||||
onRescanWallet = onRescanWallet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsTopAppBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.settings_header)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.settings_back_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsMainContent(
|
||||
onBackupWallet: () -> Unit,
|
||||
onWipeWallet: () -> Unit,
|
||||
onRescanWallet: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup))
|
||||
DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe))
|
||||
TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan))
|
||||
}
|
||||
}
|
|
@ -47,6 +47,9 @@ object Dark {
|
|||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||
val addressHighlightTransparent = Color(0xFF97999A)
|
||||
val addressHighlightViewing = Color(0xFF504062)
|
||||
|
||||
val dangerous = Color(0xFFEC0008)
|
||||
val onDangerous = Color(0xFFFFFFFF)
|
||||
}
|
||||
|
||||
object Light {
|
||||
|
@ -93,4 +96,7 @@ object Light {
|
|||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||
val addressHighlightTransparent = Color(0xFF97999A)
|
||||
val addressHighlightViewing = Color(0xFF504062)
|
||||
|
||||
val dangerous = Color(0xFFEC0008)
|
||||
val onDangerous = Color(0xFFFFFFFF)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ private val DarkColorPalette = darkColors(
|
|||
surface = Dark.backgroundStart,
|
||||
onSurface = Dark.textBodyOnBackground,
|
||||
background = Dark.backgroundStart,
|
||||
onBackground = Dark.textBodyOnBackground
|
||||
onBackground = Dark.textBodyOnBackground,
|
||||
)
|
||||
|
||||
private val LightColorPalette = lightColors(
|
||||
|
@ -51,7 +51,9 @@ data class ExtendedColors(
|
|||
val addressHighlightUnified: Color,
|
||||
val addressHighlightSapling: Color,
|
||||
val addressHighlightTransparent: Color,
|
||||
val addressHighlightViewing: Color
|
||||
val addressHighlightViewing: Color,
|
||||
val dangerous: Color,
|
||||
val onDangerous: Color
|
||||
) {
|
||||
@Composable
|
||||
fun surfaceGradient() = Brush.verticalGradient(
|
||||
|
@ -79,7 +81,9 @@ val DarkExtendedColorPalette = ExtendedColors(
|
|||
addressHighlightUnified = Dark.addressHighlightUnified,
|
||||
addressHighlightSapling = Dark.addressHighlightSapling,
|
||||
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
||||
addressHighlightViewing = Dark.addressHighlightViewing
|
||||
addressHighlightViewing = Dark.addressHighlightViewing,
|
||||
dangerous = Dark.dangerous,
|
||||
onDangerous = Dark.onDangerous
|
||||
)
|
||||
|
||||
val LightExtendedColorPalette = ExtendedColors(
|
||||
|
@ -99,7 +103,9 @@ val LightExtendedColorPalette = ExtendedColors(
|
|||
addressHighlightUnified = Light.addressHighlightUnified,
|
||||
addressHighlightSapling = Light.addressHighlightSapling,
|
||||
addressHighlightTransparent = Light.addressHighlightTransparent,
|
||||
addressHighlightViewing = Light.addressHighlightViewing
|
||||
addressHighlightViewing = Light.addressHighlightViewing,
|
||||
dangerous = Light.dangerous,
|
||||
onDangerous = Light.onDangerous
|
||||
)
|
||||
|
||||
val LocalExtendedColors = staticCompositionLocalOf {
|
||||
|
@ -120,7 +126,9 @@ val LocalExtendedColors = staticCompositionLocalOf {
|
|||
addressHighlightUnified = Color.Unspecified,
|
||||
addressHighlightSapling = Color.Unspecified,
|
||||
addressHighlightTransparent = Color.Unspecified,
|
||||
addressHighlightViewing = Color.Unspecified
|
||||
addressHighlightViewing = Color.Unspecified,
|
||||
dangerous = Color.Unspecified,
|
||||
onDangerous = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<resources>
|
||||
<string name="seed_title">Backup Wallet</string>
|
||||
<string name="seed_back_content_description">Back</string>
|
||||
|
||||
<string name="seed_header">Your Secret Recovery Phrase</string>
|
||||
<string name="seed_body">These words represent your funds and the security used to protect them.</string>
|
||||
<string name="seed_copy">Copy to buffer</string>
|
||||
|
||||
</resources>
|
|
@ -0,0 +1,10 @@
|
|||
<resources>
|
||||
<string name="settings_header">Settings</string>
|
||||
<string name="settings_back_content_description">Back</string>
|
||||
|
||||
<string name="settings_backup">Backup Wallet</string>
|
||||
<string name="settings_wipe">Wipe Wallet Data</string>
|
||||
|
||||
<string name="settings_rescan">Rescan Blockchain</string>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue