taler-android

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

MainViewModel.kt (12075B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2025 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
     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 androidx.core.net.toUri
     59 import net.taler.wallet.donau.DonauManager
     60 import net.taler.wallet.main.ViewMode
     61 
     62 const val TAG = "taler-wallet"
     63 const val OBSERVABILITY_LIMIT = 100
     64 
     65 private val transactionNotifications = listOf(
     66     "transaction-state-transition",
     67 )
     68 
     69 private val observabilityNotifications = listOf(
     70     "task-observability-event",
     71     "request-observability-event",
     72 )
     73 
     74 private val sendUriActions = listOf(
     75     "pay",
     76     "tip",
     77     "pay-pull",
     78     "pay-template",
     79 )
     80 
     81 private val receiveUriActions = listOf(
     82     "withdraw",
     83     "refund",
     84     "pay-push",
     85 )
     86 
     87 class MainViewModel(
     88     app: Application,
     89 ) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
     90 
     91     private val mDevMode = MutableLiveData(BuildConfig.DEBUG)
     92     val devMode: LiveData<Boolean> = mDevMode
     93 
     94     val showProgressBar = MutableLiveData<Boolean>()
     95     var walletVersion: String? = null
     96         private set
     97     var walletVersionHash: String? = null
     98         private set
     99     var exchangeVersion: String? = null
    100         private set
    101     var merchantVersion: String? = null
    102         private set
    103 
    104     @set:Synchronized
    105     private var walletConfig = WalletRunConfig(
    106         testing = Testing(
    107             emitObservabilityEvents = true,
    108             devModeActive = devMode.value == true,
    109         ),
    110         features = Features(
    111             enableV1Contracts = true,
    112         ),
    113         logLevel = if (devMode.value == true) "TRACE" else "INFO",
    114     )
    115 
    116     private val api = WalletBackendApi(app, walletConfig, this, this)
    117 
    118     val networkManager = NetworkManager(app.applicationContext)
    119     val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope)
    120     val balanceManager = BalanceManager(api, viewModelScope, exchangeManager)
    121     val paymentManager = PaymentManager(api, viewModelScope, exchangeManager)
    122     val transactionManager: TransactionManager = TransactionManager(api, viewModelScope)
    123     val refundManager = RefundManager(api, viewModelScope)
    124     val withdrawManager = WithdrawManager(api, viewModelScope, exchangeManager, transactionManager)
    125     val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope)
    126     val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope, balanceManager)
    127     val accountManager: AccountManager = AccountManager(api, viewModelScope)
    128     val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager)
    129     val donauManager: DonauManager = DonauManager(api, viewModelScope, exchangeManager)
    130 
    131     private val mAuthenticated = MutableStateFlow(false)
    132     val authenticated: StateFlow<Boolean> = mAuthenticated
    133 
    134     private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
    135     val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
    136 
    137     private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList())
    138     val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog
    139 
    140     private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
    141     val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
    142 
    143     private val mViewMode = MutableStateFlow<ViewMode>(ViewMode.Assets)
    144     val viewMode: StateFlow<ViewMode> = mViewMode
    145 
    146     @set:Synchronized
    147     private var scanQrContext = ScanQrContext.Unknown
    148 
    149     fun startWallet() {
    150         api.startWallet()
    151     }
    152 
    153     fun stopWallet() {
    154         api.stopWallet()
    155     }
    156 
    157     override fun onVersionReceived(versionInfo: WalletCoreVersion) {
    158         walletVersion = versionInfo.implementationSemver
    159         walletVersionHash = versionInfo.implementationGitHash
    160         exchangeVersion = versionInfo.exchange
    161         merchantVersion = versionInfo.merchant
    162     }
    163 
    164     override fun onNotificationReceived(payload: NotificationPayload) {
    165         if (payload.type == "waiting-for-retry") return // ignore ping)
    166 
    167         val str = BackendManager.json.encodeToString(payload)
    168         Log.i(TAG, "Received notification from wallet-core: $str")
    169 
    170         // Only update balances when we're told they changed
    171         if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) {
    172             balanceManager.loadAssets()
    173         }
    174 
    175         if (payload.type in observabilityNotifications && payload.event != null) {
    176             mObservabilityLog.getAndUpdate { logs ->
    177                 logs.takeLast(OBSERVABILITY_LIMIT)
    178                     .toMutableList().apply {
    179                         add(payload.event)
    180                     }
    181             }
    182         }
    183 
    184         if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) {
    185             payload.transactionId?.let { id ->
    186                 // update currently selected transaction
    187                 transactionManager.updateTransactionIfSelected(id)
    188                 // update currently selected transaction list
    189                 if (payload.type == "transaction-state-transition") {
    190                     transactionManager.getTransactionById(id)?.let { tx ->
    191                         val v = viewMode.value
    192                         if (v is ViewMode.Transactions && v.selectedScope in tx.scopes) {
    193                             transactionManager.loadTransactions(v.selectedScope)
    194                         }
    195                     }
    196                 }
    197             }
    198         }
    199     }
    200 
    201     @UiThread
    202     fun lockWallet() {
    203         mAuthenticated.value = false
    204     }
    205 
    206     @UiThread
    207     fun unlockWallet() {
    208         mAuthenticated.value = true
    209     }
    210 
    211     fun setViewMode(v: ViewMode?) = viewModelScope.launch {
    212         mViewMode.value = when(v) {
    213             null -> ViewMode.Assets
    214             is ViewMode.Transactions -> v.copy(
    215                 // fill-in currency spec from DB
    216                 selectedSpec = exchangeManager.getCurrencySpecification(v.selectedScope),
    217             )
    218             else -> v
    219         }
    220     }
    221 
    222     fun selectScope(scopeInfo: ScopeInfo?) {
    223         if (scopeInfo != null) {
    224             setViewMode(ViewMode.Transactions(scopeInfo))
    225         } else {
    226             setViewMode(ViewMode.Assets)
    227         }
    228     }
    229 
    230     fun showAssets() {
    231         if (viewMode.value != ViewMode.Assets) {
    232             selectScope(null)
    233         }
    234     }
    235 
    236     /**
    237      * Navigates to the given scope info's transaction list, when [MainFragment] is shown.
    238      */
    239     @UiThread
    240     fun showTransactions(scopeInfo: ScopeInfo, stateFilter: TransactionStateFilter? = null) {
    241         mViewMode.value = ViewMode.Transactions(scopeInfo, stateFilter = stateFilter)
    242     }
    243 
    244     @UiThread
    245     fun createAmount(amountText: String, currency: String, incoming: Boolean = false): AmountResult {
    246         val amount = try {
    247             Amount.fromString(currency, amountText)
    248         } catch (e: AmountParserException) {
    249             return AmountResult.InvalidAmount
    250         }
    251         if (incoming || balanceManager.hasSufficientBalance(amount)) return AmountResult.Success(amount)
    252         return AmountResult.InsufficientBalance(amount)
    253     }
    254 
    255     @UiThread
    256     fun dangerouslyReset() {
    257         withdrawManager.resetTestWithdrawal()
    258         balanceManager.resetBalances()
    259     }
    260 
    261     @UiThread
    262     fun scanCode(context: ScanQrContext = ScanQrContext.Unknown) {
    263         scanQrContext = context
    264         mScanCodeEvent.value = true.toEvent()
    265     }
    266 
    267     fun getScanQrContext() = scanQrContext
    268 
    269     fun checkScanQrContext(uri: String): Boolean {
    270         val parsed = uri.toUri()
    271         val action = parsed.host
    272         return when (scanQrContext) {
    273             ScanQrContext.Send -> action in sendUriActions
    274             ScanQrContext.Receive -> action in receiveUriActions
    275             else -> true
    276         }
    277     }
    278 
    279     fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) {
    280         mDevMode.postValue(enabled)
    281         viewModelScope.launch {
    282             val config = walletConfig.copy(
    283                 testing = walletConfig.testing?.copy(
    284                     devModeActive = enabled,
    285                 ) ?: Testing(
    286                     devModeActive = enabled,
    287                 ),
    288                 logLevel = if (enabled) "TRACE" else "INFO",
    289             )
    290 
    291             api.setWalletConfig(config)
    292                 .onSuccess {
    293                     walletConfig = config
    294                 }.onError(onError)
    295         }
    296     }
    297 
    298     fun hintNetworkAvailability(isAvailable: Boolean) {
    299         viewModelScope.launch {
    300             api.request<Unit>("hintNetworkAvailability") {
    301                 put("isNetworkAvailable", isAvailable)
    302             }
    303         }
    304     }
    305 
    306     fun runIntegrationTest(onError: (error: TalerErrorInfo) -> Unit) {
    307         viewModelScope.launch {
    308             api.request<Unit>("runIntegrationTestV2") {
    309                 put("amountToWithdraw", "KUDOS:42")
    310                 put("amountToSpend", "KUDOS:23")
    311                 put("corebankApiBaseUrl", "https://bank.demo.taler.net/")
    312                 put("exchangeBaseUrl", "https://exchange.demo.taler.net/")
    313                 put("merchantBaseUrl", "https://backend.demo.taler.net/instances/sandbox/")
    314                 put("merchantAuthToken", "secret-token:sandbox")
    315             }.onError(onError)
    316         }
    317     }
    318 
    319     fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) {
    320         viewModelScope.launch {
    321             api.request<Unit>("applyDevExperiment") {
    322                 put("devExperimentUri", uri)
    323             }.onError(onError)
    324         }
    325     }
    326 }
    327 
    328 enum class ScanQrContext {
    329     Send,
    330     Receive,
    331     Unknown,
    332 }
    333 
    334 sealed class AmountResult {
    335     data class Success(val amount: Amount) : AmountResult()
    336     data class InsufficientBalance(val amount: Amount) : AmountResult()
    337     data object InvalidAmount : AmountResult()
    338 }