taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

MainViewModel.kt (11101B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2026 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.wallet.main
     18 
     19 import android.app.Application
     20 import android.util.Log
     21 import androidx.annotation.UiThread
     22 import androidx.lifecycle.AndroidViewModel
     23 import androidx.lifecycle.LiveData
     24 import androidx.lifecycle.MutableLiveData
     25 import androidx.lifecycle.viewModelScope
     26 import kotlinx.coroutines.Dispatchers
     27 import kotlinx.coroutines.flow.MutableStateFlow
     28 import kotlinx.coroutines.flow.StateFlow
     29 import kotlinx.coroutines.flow.getAndUpdate
     30 import kotlinx.coroutines.launch
     31 import net.taler.common.Amount
     32 import net.taler.common.AmountParserException
     33 import net.taler.common.Event
     34 import net.taler.common.toEvent
     35 import net.taler.wallet.accounts.AccountManager
     36 import net.taler.wallet.backend.BackendManager
     37 import net.taler.wallet.backend.NotificationPayload
     38 import net.taler.wallet.backend.NotificationReceiver
     39 import net.taler.wallet.backend.TalerErrorInfo
     40 import net.taler.wallet.backend.VersionReceiver
     41 import net.taler.wallet.backend.WalletBackendApi
     42 import net.taler.wallet.backend.WalletCoreVersion
     43 import net.taler.wallet.backend.WalletRunConfig
     44 import net.taler.wallet.backend.WalletRunConfig.Features
     45 import net.taler.wallet.backend.WalletRunConfig.Testing
     46 import net.taler.wallet.balances.BalanceManager
     47 import net.taler.wallet.balances.ScopeInfo
     48 import net.taler.wallet.deposit.DepositManager
     49 import net.taler.wallet.events.ObservabilityEvent
     50 import net.taler.wallet.exchanges.ExchangeManager
     51 import net.taler.wallet.payment.PaymentManager
     52 import net.taler.wallet.peer.PeerManager
     53 import net.taler.wallet.refund.RefundManager
     54 import net.taler.wallet.settings.SettingsManager
     55 import net.taler.wallet.transactions.TransactionManager
     56 import net.taler.wallet.transactions.TransactionStateFilter
     57 import net.taler.wallet.withdraw.WithdrawManager
     58 import net.taler.wallet.BuildConfig
     59 import net.taler.wallet.NetworkManager
     60 import net.taler.wallet.donau.DonauManager
     61 import net.taler.wallet.tokens.TokenManager
     62 
     63 const val TAG = "taler-wallet"
     64 const val OBSERVABILITY_LIMIT = 100
     65 
     66 private val transactionNotifications = listOf(
     67     "transaction-state-transition",
     68 )
     69 
     70 private val observabilityNotifications = listOf(
     71     "task-observability-event",
     72     "request-observability-event",
     73 )
     74 
     75 class MainViewModel(
     76     app: Application,
     77 ) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
     78 
     79     private val mDevMode = MutableLiveData(BuildConfig.DEBUG)
     80     val devMode: LiveData<Boolean> = mDevMode
     81 
     82     val showProgressBar = MutableLiveData<Boolean>()
     83     var walletVersion: String? = null
     84         private set
     85     var walletVersionHash: String? = null
     86         private set
     87     var exchangeVersion: String? = null
     88         private set
     89     var merchantVersion: String? = null
     90         private set
     91 
     92     @set:Synchronized
     93     private var walletConfig = WalletRunConfig(
     94         testing = Testing(
     95             emitObservabilityEvents = true,
     96             devModeActive = devMode.value == true,
     97         ),
     98         features = Features(
     99             enableV1Contracts = true,
    100         ),
    101         logLevel = if (devMode.value == true) "TRACE" else "INFO",
    102     )
    103 
    104     private val api = WalletBackendApi(app, walletConfig, this, this)
    105 
    106     val networkManager = NetworkManager(app.applicationContext)
    107     val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope)
    108     val balanceManager = BalanceManager(api, viewModelScope, exchangeManager)
    109     val paymentManager = PaymentManager(api, viewModelScope, exchangeManager)
    110     val transactionManager: TransactionManager = TransactionManager(api, viewModelScope)
    111     val refundManager = RefundManager(api, viewModelScope)
    112     val withdrawManager = WithdrawManager(api, viewModelScope, exchangeManager, transactionManager)
    113     val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope)
    114     val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope, balanceManager)
    115     val accountManager: AccountManager = AccountManager(api, viewModelScope)
    116     val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager)
    117     val tokenManager: TokenManager = TokenManager(api)
    118     val donauManager: DonauManager = DonauManager(api, viewModelScope, exchangeManager)
    119 
    120     private val mAuthenticated = MutableStateFlow(false)
    121     val authenticated: StateFlow<Boolean> = mAuthenticated
    122 
    123     private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
    124     val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
    125 
    126     private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList())
    127     val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog
    128 
    129     private val mShowObservabilityLog = MutableStateFlow(false)
    130     val showObservabilityLog: StateFlow<Boolean> = mShowObservabilityLog
    131 
    132     private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
    133     val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
    134 
    135     private val mViewMode = MutableStateFlow<ViewMode>(ViewMode.Assets)
    136     val viewMode: StateFlow<ViewMode> = mViewMode
    137 
    138     fun startWallet() {
    139         api.startWallet()
    140     }
    141 
    142     fun stopWallet() {
    143         api.stopWallet()
    144     }
    145 
    146     override fun onVersionReceived(versionInfo: WalletCoreVersion) {
    147         walletVersion = versionInfo.implementationSemver
    148         walletVersionHash = versionInfo.implementationGitHash
    149         exchangeVersion = versionInfo.exchange
    150         merchantVersion = versionInfo.merchant
    151     }
    152 
    153     override fun onNotificationReceived(payload: NotificationPayload) {
    154         if (payload.type == "waiting-for-retry") return // ignore ping)
    155 
    156         val str = BackendManager.json.encodeToString(payload)
    157         Log.i(TAG, "Received notification from wallet-core: $str")
    158 
    159         // Only update balances when we're told they changed
    160         if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) {
    161             balanceManager.loadAssets()
    162         }
    163 
    164         if (payload.type in observabilityNotifications && payload.event != null) {
    165             mObservabilityLog.getAndUpdate { logs ->
    166                 logs.takeLast(OBSERVABILITY_LIMIT)
    167                     .toMutableList().apply {
    168                         add(payload.event)
    169                     }
    170             }
    171         }
    172 
    173         if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) {
    174             payload.transactionId?.let { id ->
    175                 // update currently selected transaction
    176                 transactionManager.updateTransactionIfSelected(id)
    177                 // update currently selected transaction list
    178                 if (payload.type == "transaction-state-transition") {
    179                     transactionManager.getTransactionById(id)?.let { tx ->
    180                         val v = viewMode.value
    181                         if (v is ViewMode.Transactions && v.selectedScope in tx.scopes) {
    182                             transactionManager.loadTransactions(v.selectedScope)
    183                         }
    184                     }
    185                 }
    186             }
    187         }
    188     }
    189 
    190     @UiThread
    191     fun lockWallet() {
    192         mAuthenticated.value = false
    193     }
    194 
    195     @UiThread
    196     fun unlockWallet() {
    197         mAuthenticated.value = true
    198     }
    199 
    200     fun setViewMode(v: ViewMode?) = viewModelScope.launch {
    201         mViewMode.value = when(v) {
    202             null -> ViewMode.Assets
    203             is ViewMode.Transactions -> v.copy(
    204                 // fill-in currency spec from DB
    205                 selectedSpec = exchangeManager.getCurrencySpecification(v.selectedScope),
    206             )
    207             else -> v
    208         }
    209     }
    210 
    211     fun selectScope(scopeInfo: ScopeInfo?) {
    212         if (scopeInfo != null) {
    213             setViewMode(ViewMode.Transactions(scopeInfo))
    214         } else {
    215             setViewMode(ViewMode.Assets)
    216         }
    217     }
    218 
    219     fun showAssets() {
    220         if (viewMode.value != ViewMode.Assets) {
    221             selectScope(null)
    222         }
    223     }
    224 
    225     /**
    226      * Navigates to the given scope info's transaction list, when [MainScreen] is shown.
    227      */
    228     @UiThread
    229     fun showTransactions(scopeInfo: ScopeInfo, stateFilter: TransactionStateFilter? = null) {
    230         mViewMode.value = ViewMode.Transactions(scopeInfo, stateFilter = stateFilter)
    231     }
    232 
    233     @UiThread
    234     fun createAmount(amountText: String, currency: String, incoming: Boolean = false): AmountResult {
    235         val amount = try {
    236             Amount.fromString(currency, amountText)
    237         } catch (e: AmountParserException) {
    238             return AmountResult.InvalidAmount
    239         }
    240         if (incoming || balanceManager.hasSufficientBalance(amount)) return AmountResult.Success(amount)
    241         return AmountResult.InsufficientBalance(amount)
    242     }
    243 
    244     @UiThread
    245     fun dangerouslyReset() {
    246         withdrawManager.resetTestWithdrawal()
    247         balanceManager.resetBalances()
    248     }
    249 
    250     @UiThread
    251     fun scanCode() {
    252         mScanCodeEvent.value = true.toEvent()
    253     }
    254 
    255     fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) {
    256         mDevMode.postValue(enabled)
    257         viewModelScope.launch {
    258             val config = walletConfig.copy(
    259                 testing = walletConfig.testing?.copy(
    260                     devModeActive = enabled,
    261                 ) ?: Testing(
    262                     devModeActive = enabled,
    263                 ),
    264                 logLevel = if (enabled) "TRACE" else "INFO",
    265             )
    266 
    267             api.setWalletConfig(config)
    268                 .onSuccess {
    269                     walletConfig = config
    270                 }.onError(onError)
    271         }
    272     }
    273 
    274     fun showObservabilityLog() {
    275         mShowObservabilityLog.value = true
    276     }
    277 
    278     fun hideObservabilityLog() {
    279         mShowObservabilityLog.value = false
    280     }
    281 
    282     fun hintNetworkAvailability(isAvailable: Boolean) {
    283         viewModelScope.launch {
    284             api.request<Unit>("hintNetworkAvailability") {
    285                 put("isNetworkAvailable", isAvailable)
    286             }
    287         }
    288     }
    289 
    290     fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) {
    291         viewModelScope.launch {
    292             api.request<Unit>("applyDevExperiment") {
    293                 put("devExperimentUri", uri)
    294             }.onError(onError)
    295         }
    296     }
    297 }
    298 
    299 sealed class AmountResult {
    300     data class Success(val amount: Amount) : AmountResult()
    301     data class InsufficientBalance(val amount: Amount) : AmountResult()
    302     data object InvalidAmount : AmountResult()
    303 }