2021-11-12 04:09:30 -08:00
|
|
|
package cash.z.ecc.ui.screen.home.viewmodel
|
|
|
|
|
|
|
|
import android.app.Application
|
|
|
|
import androidx.lifecycle.AndroidViewModel
|
|
|
|
import androidx.lifecycle.viewModelScope
|
2021-11-17 12:19:49 -08:00
|
|
|
import cash.z.ecc.android.sdk.Synchronizer
|
2021-11-12 04:09:30 -08:00
|
|
|
import cash.z.ecc.sdk.SynchronizerCompanion
|
|
|
|
import cash.z.ecc.sdk.model.PersistableWallet
|
|
|
|
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
|
2021-11-17 12:19:49 -08:00
|
|
|
import cash.z.ecc.ui.preference.StandardPreferenceKeys
|
|
|
|
import cash.z.ecc.ui.preference.StandardPreferenceSingleton
|
2021-11-12 04:09:30 -08:00
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
2021-11-17 12:19:49 -08:00
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
import kotlinx.coroutines.flow.combine
|
2021-11-12 04:09:30 -08:00
|
|
|
import kotlinx.coroutines.flow.emitAll
|
|
|
|
import kotlinx.coroutines.flow.flow
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
import kotlinx.coroutines.launch
|
2021-11-17 12:19:49 -08:00
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
2021-11-12 04:09:30 -08:00
|
|
|
|
|
|
|
// To make this more multiplatform compatible, we need to remove the dependency on Context
|
|
|
|
// for loading the preferences.
|
|
|
|
class WalletViewModel(application: Application) : AndroidViewModel(application) {
|
2021-11-17 12:19:49 -08:00
|
|
|
/*
|
|
|
|
* Using the Mutex may be overkill, but it ensures that if multiple calls are accidentally made
|
|
|
|
* that they have a consistent ordering.
|
|
|
|
*/
|
|
|
|
private val persistWalletMutex = Mutex()
|
|
|
|
|
2021-11-12 04:09:30 -08:00
|
|
|
/**
|
|
|
|
* A flow of the user's stored wallet. Null indicates that no wallet has been stored.
|
|
|
|
*/
|
2021-11-17 12:19:49 -08:00
|
|
|
private val persistableWalletFlow = flow {
|
2021-11-12 04:09:30 -08:00
|
|
|
// EncryptedPreferenceSingleton.getInstance() is a suspending function, which is why we need
|
|
|
|
// the flow builder to provide a coroutine context.
|
|
|
|
val encryptedPreferenceProvider = EncryptedPreferenceSingleton.getInstance(application)
|
|
|
|
|
|
|
|
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
|
2021-11-17 12:19:49 -08:00
|
|
|
}
|
2021-11-12 04:09:30 -08:00
|
|
|
|
|
|
|
/**
|
2021-11-17 12:19:49 -08:00
|
|
|
* A flow of whether a backup of the user's wallet has been performed.
|
2021-11-12 04:09:30 -08:00
|
|
|
*/
|
2021-11-17 12:19:49 -08:00
|
|
|
private val isBackupCompleteFlow = flow {
|
|
|
|
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
|
|
|
|
emitAll(StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.observe(preferenceProvider))
|
|
|
|
}
|
|
|
|
|
|
|
|
val state: StateFlow<WalletState> = persistableWalletFlow
|
|
|
|
.combine(isBackupCompleteFlow) { persistableWallet: PersistableWallet?, isBackupComplete: Boolean ->
|
|
|
|
if (null == persistableWallet) {
|
|
|
|
WalletState.NoWallet
|
|
|
|
} else if (!isBackupComplete) {
|
|
|
|
WalletState.NeedsBackup(persistableWallet)
|
|
|
|
} else {
|
|
|
|
WalletState.Ready(persistableWallet, SynchronizerCompanion.load(application, persistableWallet))
|
|
|
|
}
|
|
|
|
}.stateIn(
|
|
|
|
viewModelScope,
|
|
|
|
SharingStarted.WhileSubscribed(stopTimeoutMillis = ANDROID_STATE_FLOW_TIMEOUT_MILLIS),
|
|
|
|
WalletState.Loading
|
|
|
|
)
|
2021-11-12 04:09:30 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Persists a wallet asynchronously. Clients observe either [persistableWallet] or [synchronizer]
|
2021-11-17 12:19:49 -08:00
|
|
|
* to see the side effects. This would be used for a user restoring a wallet from a backup.
|
2021-11-12 04:09:30 -08:00
|
|
|
*/
|
|
|
|
fun persistWallet(persistableWallet: PersistableWallet) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
val preferenceProvider = EncryptedPreferenceSingleton.getInstance(getApplication())
|
2021-11-17 12:19:49 -08:00
|
|
|
persistWalletMutex.withLock {
|
|
|
|
EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(preferenceProvider, persistableWallet)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a wallet asynchronously and then persists it. Clients observe
|
|
|
|
* [state] to see the side effects. This would be used for a user creating a new wallet.
|
|
|
|
*/
|
|
|
|
/*
|
|
|
|
* Although waiting for the wallet to be written and then read back is slower, it is probably
|
|
|
|
* safer because it 1. guarantees the wallet is written to disk and 2. has a single source of truth.
|
|
|
|
*/
|
|
|
|
fun createAndPersistWallet() {
|
|
|
|
val application = getApplication<Application>()
|
|
|
|
|
|
|
|
viewModelScope.launch {
|
|
|
|
val newWallet = PersistableWallet.new(application)
|
|
|
|
persistWallet(newWallet)
|
2021-11-12 04:09:30 -08:00
|
|
|
}
|
|
|
|
}
|
2021-11-17 12:19:49 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Asynchronously notes that the user has completed the backup steps, which means the wallet
|
|
|
|
* is ready to use. Clients observe [state] to see the side effects. This would be used
|
|
|
|
* for a user creating a new wallet.
|
|
|
|
*/
|
|
|
|
fun persistBackupComplete() {
|
|
|
|
val application = getApplication<Application>()
|
|
|
|
|
|
|
|
viewModelScope.launch {
|
|
|
|
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
|
|
|
|
StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.putValue(preferenceProvider, true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents the state of the wallet
|
|
|
|
*/
|
|
|
|
sealed class WalletState {
|
|
|
|
object Loading : WalletState()
|
|
|
|
object NoWallet : WalletState()
|
|
|
|
class NeedsBackup(val persistableWallet: PersistableWallet) : WalletState()
|
|
|
|
class Ready(val persistableWallet: PersistableWallet, val synchronizer: Synchronizer) : WalletState()
|
2021-11-12 04:09:30 -08:00
|
|
|
}
|