[#483] Fix crash when wiping wallet
This commit is contained in:
parent
981d70727b
commit
3d5ed7b10b
|
@ -13,6 +13,7 @@ import co.electriccoin.zcash.ui.test.getStringResource
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -66,6 +67,7 @@ class SettingsViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Wipe has been disabled in Settings and is now a debug-only option")
|
||||
fun wipe() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
|
|
|
@ -5,14 +5,18 @@ import cash.z.ecc.android.bip39.Mnemonics
|
|||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.spackle.LazyWithArgument
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -20,17 +24,26 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
class WalletCoordinator(context: Context) {
|
||||
companion object {
|
||||
|
@ -61,29 +74,57 @@ class WalletCoordinator(context: Context) {
|
|||
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
|
||||
}
|
||||
|
||||
private val lockoutMutex = Mutex()
|
||||
private val synchronizerLockoutId = MutableStateFlow<UUID?>(null)
|
||||
|
||||
private sealed class InternalSynchronizerStatus {
|
||||
object NoWallet : InternalSynchronizerStatus()
|
||||
class Available(val synchronizer: cash.z.ecc.android.sdk.Synchronizer) : InternalSynchronizerStatus()
|
||||
class Lockout(val id: UUID) : InternalSynchronizerStatus()
|
||||
}
|
||||
|
||||
private val synchronizerOrLockoutId: Flow<Flow<InternalSynchronizerStatus>> = persistableWallet
|
||||
.combine(synchronizerLockoutId) { persistableWallet: PersistableWallet?, lockoutId: UUID? ->
|
||||
if (null != lockoutId) { // this one needs to come first
|
||||
flowOf(InternalSynchronizerStatus.Lockout(lockoutId))
|
||||
} else if (null == persistableWallet) {
|
||||
flowOf(InternalSynchronizerStatus.NoWallet)
|
||||
} else {
|
||||
callbackFlow<InternalSynchronizerStatus.Available> {
|
||||
val initializer = Initializer.new(context, persistableWallet.toConfig())
|
||||
val synchronizer = synchronizerMutex.withLock {
|
||||
val synchronizer = Synchronizer.new(initializer)
|
||||
synchronizer.start(walletScope)
|
||||
}
|
||||
|
||||
trySend(InternalSynchronizerStatus.Available(synchronizer))
|
||||
awaitClose {
|
||||
Twig.info { "Closing flow and stopping synchronizer" }
|
||||
synchronizer.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizer for the Zcash SDK. Emits null until a wallet secret is persisted.
|
||||
*
|
||||
* Note that this synchronizer is closed as soon as it stops being collected. For UI use
|
||||
* cases, see [WalletViewModel].
|
||||
*/
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
val synchronizer: StateFlow<Synchronizer?> = persistableWallet
|
||||
.filterNotNull()
|
||||
.flatMapConcat {
|
||||
callbackFlow {
|
||||
val initializer = Initializer.new(context, it.toConfig())
|
||||
val synchronizer = synchronizerMutex.withLock {
|
||||
val synchronizer = Synchronizer.new(initializer)
|
||||
synchronizer.start(walletScope)
|
||||
}
|
||||
|
||||
trySend(synchronizer)
|
||||
awaitClose {
|
||||
synchronizer.stop()
|
||||
}
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
val synchronizer: StateFlow<Synchronizer?> = synchronizerOrLockoutId
|
||||
.flatMapLatest {
|
||||
it
|
||||
}
|
||||
.map {
|
||||
when (it) {
|
||||
is InternalSynchronizerStatus.Available -> it.synchronizer
|
||||
is InternalSynchronizerStatus.Lockout -> null
|
||||
InternalSynchronizerStatus.NoWallet -> null
|
||||
}
|
||||
}.stateIn(
|
||||
}
|
||||
.stateIn(
|
||||
walletScope, SharingStarted.WhileSubscribed(),
|
||||
null
|
||||
)
|
||||
|
@ -108,54 +149,73 @@ class WalletCoordinator(context: Context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wipes the wallet.
|
||||
*
|
||||
* In order for a wipe to occur, the synchronizer must be loaded already
|
||||
* which would happen if the UI is collecting it.
|
||||
*
|
||||
* @return True if the wipe was performed and false if the wipe was not performed.
|
||||
* Resets persisted data in the SDK, but preserves the wallet secret. This will cause the
|
||||
* synchronizer to emit a new instance.
|
||||
*/
|
||||
suspend fun wipeWallet(): Boolean {
|
||||
/*
|
||||
* 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 WalletCoordinator 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.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
fun resetSdk() {
|
||||
walletScope.launch {
|
||||
lockoutMutex.withLock {
|
||||
val lockoutId = UUID.randomUUID()
|
||||
synchronizerLockoutId.value = lockoutId
|
||||
|
||||
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. By checking for referential equality at
|
||||
// the end, we can reduce that timing gap.
|
||||
val wasStarted = it.isStarted
|
||||
if (wasStarted) {
|
||||
it.stop()
|
||||
}
|
||||
synchronizerOrLockoutId
|
||||
.flatMapConcat { it }
|
||||
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
|
||||
.filter { it.id == lockoutId }
|
||||
.onFirst {
|
||||
synchronizerMutex.withLock {
|
||||
val didDelete = Initializer.erase(
|
||||
applicationContext,
|
||||
ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
|
||||
Initializer.erase(
|
||||
applicationContext,
|
||||
ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
Twig.info { "SDK erase result: $didDelete" }
|
||||
}
|
||||
}
|
||||
|
||||
if (wasStarted && synchronizer.value === it) {
|
||||
it.start(walletScope)
|
||||
}
|
||||
|
||||
return true
|
||||
synchronizerLockoutId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
/**
|
||||
* Wipes the wallet. Will cause the app-wide synchronizer to be reset with a new instance.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
fun wipeEntireWallet() {
|
||||
walletScope.launch {
|
||||
lockoutMutex.withLock {
|
||||
val lockoutId = UUID.randomUUID()
|
||||
synchronizerLockoutId.value = lockoutId
|
||||
|
||||
synchronizerOrLockoutId
|
||||
.flatMapConcat { it }
|
||||
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
|
||||
.filter { it.id == lockoutId }
|
||||
.onFirst {
|
||||
// Note that clearing the data here is non-atomic since multiple files must be modified
|
||||
|
||||
EncryptedPreferenceSingleton.getInstance(applicationContext).also { provider ->
|
||||
EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(provider, null)
|
||||
}
|
||||
|
||||
StandardPreferenceSingleton.getInstance(applicationContext).also { provider ->
|
||||
StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.putValue(provider, false)
|
||||
}
|
||||
|
||||
synchronizerMutex.withLock {
|
||||
val didDelete = Initializer.erase(
|
||||
applicationContext,
|
||||
ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
Twig.info { "SDK erase result: $didDelete" }
|
||||
}
|
||||
|
||||
synchronizerLockoutId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavHostController
|
||||
|
@ -24,8 +23,6 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.compose.rememberNavController
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.send
|
||||
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.ui.design.compat.FontCompat
|
||||
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
|
@ -34,8 +31,8 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|||
import co.electriccoin.zcash.ui.screen.about.WrapAbout
|
||||
import co.electriccoin.zcash.ui.screen.backup.WrapBackup
|
||||
import co.electriccoin.zcash.ui.screen.backup.copyToClipboard
|
||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||
import co.electriccoin.zcash.ui.screen.home.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
|
@ -45,7 +42,7 @@ import co.electriccoin.zcash.ui.screen.request.view.Request
|
|||
import co.electriccoin.zcash.ui.screen.scan.WrapScan
|
||||
import co.electriccoin.zcash.ui.screen.seed.view.Seed
|
||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||
import co.electriccoin.zcash.ui.screen.settings.view.Settings
|
||||
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
|
||||
import co.electriccoin.zcash.ui.screen.support.WrapSupport
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
|
||||
import co.electriccoin.zcash.ui.screen.update.WrapUpdate
|
||||
|
@ -169,6 +166,8 @@ class MainActivity : ComponentActivity() {
|
|||
goSend = { navController.navigate(NAV_SEND) },
|
||||
goRequest = { navController.navigate(NAV_REQUEST) }
|
||||
)
|
||||
|
||||
WrapCheckForUpdate()
|
||||
}
|
||||
composable(NAV_PROFILE) {
|
||||
WrapProfile(
|
||||
|
@ -257,41 +256,6 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapHome(
|
||||
goScan: () -> Unit,
|
||||
goProfile: () -> Unit,
|
||||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit
|
||||
) {
|
||||
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
|
||||
if (null == walletSnapshot) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
|
||||
// We might eventually want to check the debuggable property of the manifest instead
|
||||
// of relying on BuildConfig.
|
||||
val isDebugMenuEnabled = BuildConfig.DEBUG &&
|
||||
!FirebaseTestLabUtil.isFirebaseTestLab(context) &&
|
||||
!EmulatorWtfUtil.isEmulatorWtf(context)
|
||||
|
||||
Home(
|
||||
walletSnapshot,
|
||||
walletViewModel.transactionSnapshot.collectAsState().value,
|
||||
goScan = goScan,
|
||||
goRequest = goRequest,
|
||||
goSend = goSend,
|
||||
goProfile = goProfile,
|
||||
isDebugMenuEnabled = isDebugMenuEnabled
|
||||
)
|
||||
|
||||
reportFullyDrawn()
|
||||
|
||||
WrapCheckForUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapCheckForUpdate() {
|
||||
val updateInfo = checkUpdateViewModel.updateInfo.collectAsState().value
|
||||
|
@ -324,33 +288,6 @@ 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
|
||||
// occurring during same session as onboarding)
|
||||
// onboardingViewModel.onboardingState.goToBeginning()
|
||||
// onboardingViewModel.isImporting.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapSeed(
|
||||
goBack: () -> Unit
|
||||
|
|
|
@ -25,6 +25,10 @@ class BackupState(initialState: BackupStage = BackupStage.values().first()) {
|
|||
mutableState.value = current.value.getPrevious()
|
||||
}
|
||||
|
||||
fun goToBeginning() {
|
||||
mutableState.value = BackupStage.values().first()
|
||||
}
|
||||
|
||||
fun goToSeed() {
|
||||
mutableState.value = BackupStage.Seed
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.ui.BuildConfig
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
|
||||
@Composable
|
||||
internal fun MainActivity.WrapHome(
|
||||
goScan: () -> Unit,
|
||||
goProfile: () -> Unit,
|
||||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit
|
||||
) {
|
||||
WrapHome(
|
||||
this,
|
||||
goScan = goScan,
|
||||
goProfile = goProfile,
|
||||
goSend = goSend,
|
||||
goRequest = goRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun WrapHome(
|
||||
activity: ComponentActivity,
|
||||
goScan: () -> Unit,
|
||||
goProfile: () -> Unit,
|
||||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit
|
||||
) {
|
||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||
|
||||
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
|
||||
if (null == walletSnapshot) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
|
||||
// We might eventually want to check the debuggable property of the manifest instead
|
||||
// of relying on BuildConfig.
|
||||
val isDebugMenuEnabled = BuildConfig.DEBUG &&
|
||||
!FirebaseTestLabUtil.isFirebaseTestLab(context) &&
|
||||
!EmulatorWtfUtil.isEmulatorWtf(context)
|
||||
|
||||
Home(
|
||||
walletSnapshot,
|
||||
walletViewModel.transactionSnapshot.collectAsState().value,
|
||||
goScan = goScan,
|
||||
goRequest = goRequest,
|
||||
goSend = goSend,
|
||||
goProfile = goProfile,
|
||||
isDebugMenuEnabled = isDebugMenuEnabled,
|
||||
resetSdk = {
|
||||
walletViewModel.resetSdk()
|
||||
},
|
||||
wipeEntireWallet = {
|
||||
// Although this is debug only, it still might be nice to show a warning dialog
|
||||
// before performing this action
|
||||
walletViewModel.wipeEntireWallet()
|
||||
|
||||
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
|
||||
onboardingViewModel.onboardingState.goToBeginning()
|
||||
onboardingViewModel.isImporting.value = false
|
||||
|
||||
val backupViewModel by activity.viewModels<BackupViewModel>()
|
||||
backupViewModel.backupState.goToBeginning()
|
||||
}
|
||||
)
|
||||
|
||||
activity.reportFullyDrawn()
|
||||
}
|
||||
}
|
|
@ -57,7 +57,9 @@ fun ComposablePreview() {
|
|||
goProfile = {},
|
||||
goSend = {},
|
||||
goRequest = {},
|
||||
isDebugMenuEnabled = false
|
||||
isDebugMenuEnabled = false,
|
||||
resetSdk = {},
|
||||
wipeEntireWallet = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -73,10 +75,12 @@ fun Home(
|
|||
goProfile: () -> Unit,
|
||||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit,
|
||||
resetSdk: () -> Unit,
|
||||
wipeEntireWallet: () -> Unit,
|
||||
isDebugMenuEnabled: Boolean
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
HomeTopAppBar(isDebugMenuEnabled)
|
||||
HomeTopAppBar(isDebugMenuEnabled, resetSdk, wipeEntireWallet)
|
||||
}) { paddingValues ->
|
||||
HomeMainContent(
|
||||
paddingValues,
|
||||
|
@ -91,19 +95,23 @@ fun Home(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun HomeTopAppBar(isDebugMenuEnabled: Boolean) {
|
||||
private fun HomeTopAppBar(
|
||||
isDebugMenuEnabled: Boolean,
|
||||
resetSdk: () -> Unit,
|
||||
wipeEntireWallet: () -> Unit
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.app_name)) },
|
||||
actions = {
|
||||
if (isDebugMenuEnabled) {
|
||||
DebugMenu()
|
||||
DebugMenu(resetSdk, wipeEntireWallet)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugMenu() {
|
||||
private fun DebugMenu(resetSdk: () -> Unit, wipeEntireWallet: () -> Unit) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||
|
@ -130,6 +138,20 @@ private fun DebugMenu() {
|
|||
expanded = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Reset SDK") },
|
||||
onClick = {
|
||||
resetSdk()
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Wipe entire wallet") },
|
||||
onClick = {
|
||||
wipeEntireWallet()
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,9 +31,9 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -101,10 +101,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
null
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
val walletSnapshot: StateFlow<WalletSnapshot?> = synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapConcat { it.toWalletSnapshot() }
|
||||
.flatMapLatest {
|
||||
if (null == it) {
|
||||
flowOf(null)
|
||||
} else {
|
||||
it.toWalletSnapshot()
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
|
@ -112,10 +117,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
)
|
||||
|
||||
// This is not the right API, because the transaction list could be very long and might need UI filtering
|
||||
@OptIn(FlowPreview::class)
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
val transactionSnapshot: StateFlow<List<Transaction>> = synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapConcat { it.toTransactions() }
|
||||
.flatMapLatest {
|
||||
if (null == it) {
|
||||
flowOf(emptyList())
|
||||
} else {
|
||||
it.toTransactions()
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
emptyList()
|
||||
|
@ -195,14 +205,26 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
|
||||
/**
|
||||
* This asynchronously wipes the wallet state.
|
||||
* This asynchronously resets the SDK state. This is non-destructive, as SDK state can be rederived.
|
||||
*
|
||||
* This method only has an effect if the synchronizer currently is loaded.
|
||||
* This could be used as a troubleshooting step in debugging.
|
||||
*/
|
||||
fun wipeWallet() {
|
||||
viewModelScope.launch {
|
||||
walletCoordinator.wipeWallet()
|
||||
}
|
||||
fun resetSdk() {
|
||||
walletCoordinator.resetSdk()
|
||||
}
|
||||
|
||||
/**
|
||||
* This asynchronously wipes the entire wallet state.
|
||||
*
|
||||
* This is destructive, as the seed phrase is deleted along with the SDK state.
|
||||
*
|
||||
* This could be used as part of testing, to quickly reset the app state.
|
||||
*
|
||||
* A more complete reset of app state can be performed in Android Settings, as this will not
|
||||
* clear application state beyond the SDK and wallet secret.
|
||||
*/
|
||||
fun wipeEntireWallet() {
|
||||
walletCoordinator.wipeEntireWallet()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package co.electriccoin.zcash.ui.screen.settings
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.settings.view.Settings
|
||||
|
||||
@Composable
|
||||
internal fun MainActivity.WrapSettings(
|
||||
goBack: () -> Unit,
|
||||
goWalletBackup: () -> Unit
|
||||
) {
|
||||
WrapSettings(
|
||||
activity = this,
|
||||
goBack = goBack,
|
||||
goWalletBackup = goWalletBackup
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapSettings(
|
||||
activity: ComponentActivity,
|
||||
goBack: () -> Unit,
|
||||
goWalletBackup: () -> Unit
|
||||
) {
|
||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsState().value
|
||||
if (null == synchronizer) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
Settings(
|
||||
onBack = goBack,
|
||||
onBackupWallet = goWalletBackup,
|
||||
onRescanWallet = {
|
||||
walletViewModel.rescanBlockchain()
|
||||
}, onWipeWallet = {
|
||||
walletViewModel.wipeEntireWallet()
|
||||
|
||||
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
|
||||
onboardingViewModel.onboardingState.goToBeginning()
|
||||
onboardingViewModel.isImporting.value = false
|
||||
|
||||
val backupViewModel by activity.viewModels<BackupViewModel>()
|
||||
backupViewModel.backupState.goToBeginning()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.DangerousButton
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||
|
@ -78,7 +77,7 @@ private fun SettingsTopAppBar(onBack: () -> Unit) {
|
|||
private fun SettingsMainContent(
|
||||
paddingValues: PaddingValues,
|
||||
onBackupWallet: () -> Unit,
|
||||
onWipeWallet: () -> Unit,
|
||||
@Suppress("UNUSED_PARAMETER") onWipeWallet: () -> Unit,
|
||||
onRescanWallet: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
|
@ -86,7 +85,8 @@ private fun SettingsMainContent(
|
|||
.padding(top = paddingValues.calculateTopPadding())
|
||||
) {
|
||||
PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup))
|
||||
DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe))
|
||||
// We have decided to not include this in settings; see overflow debug menu instead
|
||||
// DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe))
|
||||
TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue