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/onboarding",
|
||||||
"src/main/res/ui/profile",
|
"src/main/res/ui/profile",
|
||||||
"src/main/res/ui/restore",
|
"src/main/res/ui/restore",
|
||||||
|
"src/main/res/ui/seed",
|
||||||
|
"src/main/res/ui/settings",
|
||||||
"src/main/res/ui/wallet_address"
|
"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.view.RestoreWallet
|
||||||
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
|
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
|
||||||
import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel
|
import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel
|
||||||
|
import cash.z.ecc.ui.screen.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.screen.wallet_address.view.WalletAddresses
|
||||||
import cash.z.ecc.ui.theme.ZcashTheme
|
import cash.z.ecc.ui.theme.ZcashTheme
|
||||||
import cash.z.ecc.ui.util.AndroidApiVersion
|
import cash.z.ecc.ui.util.AndroidApiVersion
|
||||||
|
@ -48,6 +50,7 @@ import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val walletViewModel by viewModels<WalletViewModel>()
|
private val walletViewModel by viewModels<WalletViewModel>()
|
||||||
|
@ -141,12 +144,7 @@ class MainActivity : ComponentActivity() {
|
||||||
BackupWallet(
|
BackupWallet(
|
||||||
persistableWallet, backupViewModel.backupState, backupViewModel.testChoices,
|
persistableWallet, backupViewModel.backupState, backupViewModel.testChoices,
|
||||||
onCopyToClipboard = {
|
onCopyToClipboard = {
|
||||||
val clipboardManager = getSystemService(ClipboardManager::class.java)
|
copyToClipboard(applicationContext, persistableWallet)
|
||||||
val data = ClipData.newPlainText(
|
|
||||||
getString(R.string.new_wallet_clipboard_tag),
|
|
||||||
persistableWallet.seedPhrase.joinToString()
|
|
||||||
)
|
|
||||||
clipboardManager.setPrimaryClip(data)
|
|
||||||
}, onComplete = {
|
}, onComplete = {
|
||||||
walletViewModel.persistBackupComplete()
|
walletViewModel.persistBackupComplete()
|
||||||
}
|
}
|
||||||
|
@ -200,32 +198,55 @@ class MainActivity : ComponentActivity() {
|
||||||
private fun Navigation() {
|
private fun Navigation() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "home") {
|
val home = "home"
|
||||||
composable("home") {
|
val profile = "profile"
|
||||||
|
val walletAddressDetails = "wallet_address_details"
|
||||||
|
val settings = "settings"
|
||||||
|
val seed = "seed"
|
||||||
|
|
||||||
|
NavHost(navController = navController, startDestination = home) {
|
||||||
|
composable(home) {
|
||||||
WrapHome(
|
WrapHome(
|
||||||
goScan = {},
|
goScan = {},
|
||||||
goProfile = { navController.navigate("profile") },
|
goProfile = { navController.navigate(profile) },
|
||||||
goSend = {},
|
goSend = {},
|
||||||
goRequest = {}
|
goRequest = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("profile") {
|
composable(profile) {
|
||||||
WrapProfile(
|
WrapProfile(
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onAddressDetails = { navController.navigate("wallet_address_details") },
|
onAddressDetails = { navController.navigate(walletAddressDetails) },
|
||||||
onAddressBook = { },
|
onAddressBook = { },
|
||||||
onSettings = { },
|
onSettings = { navController.navigate(settings) },
|
||||||
onCoinholderVote = { }
|
onCoinholderVote = { },
|
||||||
) {
|
onSupport = {}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
composable("wallet_address_details") {
|
composable(walletAddressDetails) {
|
||||||
WrapWalletAddresses(
|
WrapWalletAddresses(
|
||||||
goBack = {
|
goBack = {
|
||||||
navController.popBackStack()
|
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 {
|
companion object {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal val SPLASH_SCREEN_DELAY = 0.seconds
|
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.
|
* 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)
|
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 android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import cash.z.ecc.android.sdk.Initializer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
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.isMined
|
||||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||||
|
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||||
import cash.z.ecc.sdk.SynchronizerCompanion
|
import cash.z.ecc.sdk.SynchronizerCompanion
|
||||||
import cash.z.ecc.sdk.model.PersistableWallet
|
import cash.z.ecc.sdk.model.PersistableWallet
|
||||||
import cash.z.ecc.sdk.model.WalletAddresses
|
import cash.z.ecc.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.common.ANDROID_STATE_FLOW_TIMEOUT_MILLIS
|
||||||
import cash.z.ecc.ui.preference.EncryptedPreferenceKeys
|
import cash.z.ecc.ui.preference.EncryptedPreferenceKeys
|
||||||
import cash.z.ecc.ui.preference.EncryptedPreferenceSingleton
|
import cash.z.ecc.ui.preference.EncryptedPreferenceSingleton
|
||||||
|
@ -46,6 +49,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
* that they have a consistent ordering.
|
* that they have a consistent ordering.
|
||||||
*/
|
*/
|
||||||
private val persistWalletMutex = Mutex()
|
private val persistWalletMutex = Mutex()
|
||||||
|
private val synchronizerMutex = Mutex()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flow of the user's stored wallet. Null indicates that no wallet has been stored.
|
* 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>()
|
.filterIsInstance<SecretState.Ready>()
|
||||||
.flatMapConcat {
|
.flatMapConcat {
|
||||||
callbackFlow {
|
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)
|
trySend(synchronizer)
|
||||||
awaitClose {
|
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()
|
mutableState.value = current.value.getPrevious()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun goToBeginning() {
|
||||||
|
mutableState.value = OnboardingStage.values().first()
|
||||||
|
}
|
||||||
|
|
||||||
fun goToEnd() {
|
fun goToEnd() {
|
||||||
mutableState.value = OnboardingStage.values().last()
|
mutableState.value = OnboardingStage.values().last()
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,6 @@ fun PreviewRestoreComplete() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RestoreWallet(
|
fun RestoreWallet(
|
||||||
completeWordList: Set<String>,
|
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 addressHighlightSapling = Color(0xFF1BBFF6)
|
||||||
val addressHighlightTransparent = Color(0xFF97999A)
|
val addressHighlightTransparent = Color(0xFF97999A)
|
||||||
val addressHighlightViewing = Color(0xFF504062)
|
val addressHighlightViewing = Color(0xFF504062)
|
||||||
|
|
||||||
|
val dangerous = Color(0xFFEC0008)
|
||||||
|
val onDangerous = Color(0xFFFFFFFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Light {
|
object Light {
|
||||||
|
@ -93,4 +96,7 @@ object Light {
|
||||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||||
val addressHighlightTransparent = Color(0xFF97999A)
|
val addressHighlightTransparent = Color(0xFF97999A)
|
||||||
val addressHighlightViewing = Color(0xFF504062)
|
val addressHighlightViewing = Color(0xFF504062)
|
||||||
|
|
||||||
|
val dangerous = Color(0xFFEC0008)
|
||||||
|
val onDangerous = Color(0xFFFFFFFF)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ private val DarkColorPalette = darkColors(
|
||||||
surface = Dark.backgroundStart,
|
surface = Dark.backgroundStart,
|
||||||
onSurface = Dark.textBodyOnBackground,
|
onSurface = Dark.textBodyOnBackground,
|
||||||
background = Dark.backgroundStart,
|
background = Dark.backgroundStart,
|
||||||
onBackground = Dark.textBodyOnBackground
|
onBackground = Dark.textBodyOnBackground,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
|
@ -51,7 +51,9 @@ data class ExtendedColors(
|
||||||
val addressHighlightUnified: Color,
|
val addressHighlightUnified: Color,
|
||||||
val addressHighlightSapling: Color,
|
val addressHighlightSapling: Color,
|
||||||
val addressHighlightTransparent: Color,
|
val addressHighlightTransparent: Color,
|
||||||
val addressHighlightViewing: Color
|
val addressHighlightViewing: Color,
|
||||||
|
val dangerous: Color,
|
||||||
|
val onDangerous: Color
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun surfaceGradient() = Brush.verticalGradient(
|
fun surfaceGradient() = Brush.verticalGradient(
|
||||||
|
@ -79,7 +81,9 @@ val DarkExtendedColorPalette = ExtendedColors(
|
||||||
addressHighlightUnified = Dark.addressHighlightUnified,
|
addressHighlightUnified = Dark.addressHighlightUnified,
|
||||||
addressHighlightSapling = Dark.addressHighlightSapling,
|
addressHighlightSapling = Dark.addressHighlightSapling,
|
||||||
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
addressHighlightTransparent = Dark.addressHighlightTransparent,
|
||||||
addressHighlightViewing = Dark.addressHighlightViewing
|
addressHighlightViewing = Dark.addressHighlightViewing,
|
||||||
|
dangerous = Dark.dangerous,
|
||||||
|
onDangerous = Dark.onDangerous
|
||||||
)
|
)
|
||||||
|
|
||||||
val LightExtendedColorPalette = ExtendedColors(
|
val LightExtendedColorPalette = ExtendedColors(
|
||||||
|
@ -99,7 +103,9 @@ val LightExtendedColorPalette = ExtendedColors(
|
||||||
addressHighlightUnified = Light.addressHighlightUnified,
|
addressHighlightUnified = Light.addressHighlightUnified,
|
||||||
addressHighlightSapling = Light.addressHighlightSapling,
|
addressHighlightSapling = Light.addressHighlightSapling,
|
||||||
addressHighlightTransparent = Light.addressHighlightTransparent,
|
addressHighlightTransparent = Light.addressHighlightTransparent,
|
||||||
addressHighlightViewing = Light.addressHighlightViewing
|
addressHighlightViewing = Light.addressHighlightViewing,
|
||||||
|
dangerous = Light.dangerous,
|
||||||
|
onDangerous = Light.onDangerous
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalExtendedColors = staticCompositionLocalOf {
|
val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
|
@ -120,7 +126,9 @@ val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
addressHighlightUnified = Color.Unspecified,
|
addressHighlightUnified = Color.Unspecified,
|
||||||
addressHighlightSapling = Color.Unspecified,
|
addressHighlightSapling = Color.Unspecified,
|
||||||
addressHighlightTransparent = 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