summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/MainViewModel.kt')
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt245
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()
+}