/*
* This file is part of GNU Taler
* (C) 2020 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3, or (at your option) any later version.
*
* GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see
*/
package net.taler.wallet
import android.app.Application
import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
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.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.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.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
const val TAG = "taler-wallet"
const val OBSERVABILITY_LIMIT = 100
private val transactionNotifications = listOf(
"transaction-state-transition",
)
private val observabilityNotifications = listOf(
"task-observability-event",
"request-observability-event",
)
class MainViewModel(
app: Application,
) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
private val mDevMode = MutableLiveData(BuildConfig.DEBUG)
val devMode: LiveData = mDevMode
val showProgressBar = MutableLiveData()
var walletVersion: String? = null
private set
var walletVersionHash: String? = null
private set
var exchangeVersion: String? = null
private set
var merchantVersion: String? = null
private set
@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 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>()
val transactionsEvent: LiveData> = mTransactionsEvent
private val mObservabilityLog = MutableStateFlow>(emptyList())
val observabilityLog: StateFlow> = mObservabilityLog
private val mScanCodeEvent = MutableLiveData>()
val scanCodeEvent: LiveData> = mScanCodeEvent
override fun onVersionReceived(versionInfo: WalletCoreVersion) {
walletVersion = versionInfo.implementationSemver
walletVersionHash = versionInfo.implementationGitHash
exchangeVersion = versionInfo.exchange
merchantVersion = versionInfo.merchant
}
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)
}
}
}
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 scope info's transaction list, when [MainFragment] is shown.
*/
@UiThread
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() {
withdrawManager.testWithdrawalStatus.value = null
balanceManager.resetBalances()
}
fun startTunnel() {
viewModelScope.launch {
api.sendRequest("startTunnel")
}
}
fun stopTunnel() {
viewModelScope.launch {
api.sendRequest("stopTunnel")
}
}
fun tunnelResponse(resp: String) {
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("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("applyDevExperiment") {
put("devExperimentUri", uri)
}.onError(onError)
}
}
}
sealed class AmountResult {
class Success(val amount: Amount) : AmountResult()
object InsufficientBalance : AmountResult()
object InvalidAmount : AmountResult()
}