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 }