diff options
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/MainViewModel.kt')
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/MainViewModel.kt | 245 |
1 files changed, 177 insertions, 68 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 7bb6ad9..82eb8d7 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -22,131 +22,240 @@ import androidx.annotation.UiThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Job +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import net.taler.common.Amount +import net.taler.common.AmountParserException import net.taler.common.Event -import net.taler.common.assertUiThread import net.taler.common.toEvent +import net.taler.wallet.accounts.AccountManager +import net.taler.wallet.backend.BackendManager +import net.taler.wallet.backend.NotificationPayload +import net.taler.wallet.backend.NotificationReceiver +import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.backend.VersionReceiver import net.taler.wallet.backend.WalletBackendApi -import net.taler.wallet.balances.BalanceItem -import net.taler.wallet.balances.BalanceResponse +import net.taler.wallet.backend.WalletCoreVersion +import net.taler.wallet.backend.WalletRunConfig +import net.taler.wallet.backend.WalletRunConfig.Testing +import net.taler.wallet.balances.BalanceManager +import net.taler.wallet.balances.ScopeInfo +import net.taler.wallet.deposit.DepositManager +import net.taler.wallet.events.ObservabilityEvent import net.taler.wallet.exchanges.ExchangeManager import net.taler.wallet.payment.PaymentManager -import net.taler.wallet.pending.PendingOperationsManager +import net.taler.wallet.peer.PeerManager import net.taler.wallet.refund.RefundManager +import net.taler.wallet.settings.SettingsManager import net.taler.wallet.transactions.TransactionManager import net.taler.wallet.withdraw.WithdrawManager import org.json.JSONObject -import java.util.concurrent.TimeUnit.DAYS -import java.util.concurrent.TimeUnit.MINUTES -import kotlin.random.Random const val TAG = "taler-wallet" +const val OBSERVABILITY_LIMIT = 100 private val transactionNotifications = listOf( - "proposal-accepted", - "refresh-revealed", - "withdraw-group-finished" + "transaction-state-transition", ) -class MainViewModel(val app: Application) : AndroidViewModel(app) { +private val observabilityNotifications = listOf( + "task-observability-event", + "request-observability-event", +) + +class MainViewModel( + app: Application, +) : AndroidViewModel(app), VersionReceiver, NotificationReceiver { - private val mBalances = MutableLiveData<List<BalanceItem>>() - val balances: LiveData<List<BalanceItem>> = mBalances.distinctUntilChanged() + private val mDevMode = MutableLiveData(BuildConfig.DEBUG) + val devMode: LiveData<Boolean> = mDevMode - val devMode = MutableLiveData(BuildConfig.DEBUG) val showProgressBar = MutableLiveData<Boolean>() + var walletVersion: String? = null + private set + var walletVersionHash: String? = null + private set var exchangeVersion: String? = null private set var merchantVersion: String? = null private set - private val api = WalletBackendApi(app) { payload -> - if (payload.optString("operation") == "init") { - val result = payload.getJSONObject("result") - val versions = result.getJSONObject("supported_protocol_versions") - exchangeVersion = versions.getString("exchange") - merchantVersion = versions.getString("merchant") - } else if (payload.getString("type") != "waiting-for-retry") { // ignore ping - Log.i(TAG, "Received notification from wallet-core: ${payload.toString(2)}") - loadBalances() - if (payload.optString("type") in transactionNotifications) { - assertUiThread() - // TODO notification API should give us a currency to update - // update currently selected transaction list - transactionManager.loadTransactions() - } - // refresh pending ops and history with each notification - if (devMode.value == true) { - pendingOperationsManager.getPending() - } - } - } + @set:Synchronized + private var walletConfig = WalletRunConfig( + testing = Testing( + emitObservabilityEvents = true, + devModeActive = devMode.value ?: false, + ) + ) + + private val api = WalletBackendApi(app, walletConfig, this, this) + val networkManager = NetworkManager(app.applicationContext) val withdrawManager = WithdrawManager(api, viewModelScope) val paymentManager = PaymentManager(api, viewModelScope) - val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api) val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) val refundManager = RefundManager(api, viewModelScope) + val balanceManager = BalanceManager(api, viewModelScope) val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) + val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope) + val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope) + val accountManager: AccountManager = AccountManager(api, viewModelScope) + val depositManager: DepositManager = DepositManager(api, viewModelScope) - private val mTransactionsEvent = MutableLiveData<Event<String>>() - val transactionsEvent: LiveData<Event<String>> = mTransactionsEvent + private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>() + val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent - private val mLastBackup = MutableLiveData( - // fake backup time until we actually do backup - System.currentTimeMillis() - - Random.nextLong(MINUTES.toMillis(5), DAYS.toMillis(2)) - ) - val lastBackup: LiveData<Long> = mLastBackup + private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList()) + val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog + + private val mScanCodeEvent = MutableLiveData<Event<Boolean>>() + val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent - override fun onCleared() { - api.destroy() - super.onCleared() + override fun onVersionReceived(versionInfo: WalletCoreVersion) { + walletVersion = versionInfo.implementationSemver + walletVersionHash = versionInfo.implementationGitHash + exchangeVersion = versionInfo.exchange + merchantVersion = versionInfo.merchant } - @UiThread - fun loadBalances(): Job = viewModelScope.launch { - showProgressBar.value = true - val response = api.request("getBalances", BalanceResponse.serializer()) - showProgressBar.value = false - response.onError { - // TODO expose in UI - Log.e(TAG, "Error retrieving balances: $it") + override fun onNotificationReceived(payload: NotificationPayload) { + if (payload.type == "waiting-for-retry") return // ignore ping) + + val str = BackendManager.json.encodeToString(payload) + Log.i(TAG, "Received notification from wallet-core: $str") + + // Only update balances when we're told they changed + if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) { + balanceManager.loadBalances() + } + + if (payload.type in observabilityNotifications && payload.event != null) { + mObservabilityLog.getAndUpdate { logs -> + logs.takeLast(OBSERVABILITY_LIMIT) + .toMutableList().apply { + add(payload.event) + } + } } - response.onSuccess { - mBalances.value = it.balances + + if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) { + // TODO notification API should give us a currency to update + // update currently selected transaction list + transactionManager.loadTransactions() } } /** - * Navigates to the given currency's transaction list, when [MainFragment] is shown. + * Navigates to the given scope info's transaction list, when [MainFragment] is shown. */ @UiThread - fun showTransactions(currency: String) { - mTransactionsEvent.value = currency.toEvent() + fun showTransactions(scopeInfo: ScopeInfo) { + mTransactionsEvent.value = scopeInfo.toEvent() + } + + @UiThread + fun getCurrencies() = balanceManager.balances.value?.map { balanceItem -> + balanceItem.currency + } ?: emptyList() + + @UiThread + fun createAmount(amountText: String, currency: String): AmountResult { + val amount = try { + Amount.fromString(currency, amountText) + } catch (e: AmountParserException) { + return AmountResult.InvalidAmount + } + if (hasSufficientBalance(amount)) return AmountResult.Success(amount) + return AmountResult.InsufficientBalance + } + + @UiThread + fun hasSufficientBalance(amount: Amount): Boolean { + balanceManager.balances.value?.forEach { balanceItem -> + if (balanceItem.currency == amount.currency) { + return balanceItem.available >= amount + } + } + return false } @UiThread fun dangerouslyReset() { - api.sendRequest("reset") withdrawManager.testWithdrawalStatus.value = null - mBalances.value = emptyList() + balanceManager.resetBalances() } fun startTunnel() { - api.sendRequest("startTunnel") + viewModelScope.launch { + api.sendRequest("startTunnel") + } } fun stopTunnel() { - api.sendRequest("stopTunnel") + viewModelScope.launch { + api.sendRequest("stopTunnel") + } } fun tunnelResponse(resp: String) { - val respJson = JSONObject(resp) - api.sendRequest("tunnelResponse", respJson) + viewModelScope.launch { + api.sendRequest("tunnelResponse", JSONObject(resp)) + } + } + + @UiThread + fun scanCode() { + mScanCodeEvent.value = true.toEvent() + } + + fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) { + mDevMode.postValue(enabled) + viewModelScope.launch { + val config = walletConfig.copy( + testing = walletConfig.testing?.copy( + devModeActive = enabled, + ) ?: Testing( + devModeActive = enabled, + ), + ) + + api.setWalletConfig(config) + .onSuccess { + walletConfig = config + }.onError(onError) + } + } + + fun runIntegrationTest() { + viewModelScope.launch { + api.request<Unit>("runIntegrationTestV2") { + put("amountToWithdraw", "KUDOS:42") + put("amountToSpend", "KUDOS:23") + put("corebankApiBaseUrl", "https://bank.demo.taler.net/") + put("exchangeBaseUrl", "https://exchange.demo.taler.net/") + put("merchantBaseUrl", "https://backend.demo.taler.net/") + put("merchantAuthToken", "secret-token:sandbox") + } + } + } + + fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) { + viewModelScope.launch { + api.request<Unit>("applyDevExperiment") { + put("devExperimentUri", uri) + }.onError(onError) + } } } + +sealed class AmountResult { + class Success(val amount: Amount) : AmountResult() + object InsufficientBalance : AmountResult() + object InvalidAmount : AmountResult() +} |