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.Synchronizer 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 import cash.z.ecc.ui.preference.StandardPreferenceKeys import cash.z.ecc.ui.preference.StandardPreferenceSingleton import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock // To make this more multiplatform compatible, we need to remove the dependency on Context // for loading the preferences. class WalletViewModel(application: Application) : AndroidViewModel(application) { /* * 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() /** * A flow of the user's stored wallet. Null indicates that no wallet has been stored. */ private val persistableWalletFlow = flow { // 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)) } /** * A flow of whether a backup of the user's wallet has been performed. */ private val isBackupCompleteFlow = flow { val preferenceProvider = StandardPreferenceSingleton.getInstance(application) emitAll(StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.observe(preferenceProvider)) } val state: StateFlow = 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 ) /** * 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 persistNewWallet() { val application = getApplication() viewModelScope.launch { val newWallet = PersistableWallet.new(application) persistExistingWallet(newWallet) } } /** * Persists a wallet asynchronously. Clients observe [state] * to see the side effects. This would be used for a user restoring a wallet from a backup. */ fun persistExistingWallet(persistableWallet: PersistableWallet) { val application = getApplication() viewModelScope.launch { val preferenceProvider = EncryptedPreferenceSingleton.getInstance(application) persistWalletMutex.withLock { EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(preferenceProvider, persistableWallet) } } } /** * 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() viewModelScope.launch { val preferenceProvider = StandardPreferenceSingleton.getInstance(application) // Use the Mutex here to avoid timing issues. During wallet restore, persistBackupComplete() // is called prior to persistExistingWallet(). Although persistBackupComplete() should // complete quickly, it isn't guaranteed to complete before persistExistingWallet() // unless a mutex is used here. persistWalletMutex.withLock { 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() }