/*
* 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.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.switchMap
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.history.History
import net.taler.wallet.history.HistoryEvent
import net.taler.wallet.payment.PaymentManager
import org.json.JSONObject
const val TAG = "taler-wallet"
data class BalanceEntry(val available: Amount, val pendingIncoming: Amount)
data class WalletBalances(val initialized: Boolean, val byCurrency: List)
open class WithdrawStatus {
class None : WithdrawStatus()
data class Loading(val talerWithdrawUri: String) : WithdrawStatus()
data class TermsOfServiceReviewRequired(
val talerWithdrawUri: String,
val exchangeBaseUrl: String,
val tosText: String,
val tosEtag: String
) : WithdrawStatus()
class Success : WithdrawStatus()
data class ReceivedDetails(
val talerWithdrawUri: String,
val amount: Amount,
val suggestedExchange: String
) : WithdrawStatus()
data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus()
}
open class PendingOperationInfo(
val type: String,
val detail: JSONObject
)
open class PendingOperations(
val pending: List
)
@Suppress("EXPERIMENTAL_API_USAGE")
class WalletViewModel(val app: Application) : AndroidViewModel(app) {
val testWithdrawalInProgress = MutableLiveData().apply {
value = false
}
val balances = MutableLiveData().apply {
value = WalletBalances(false, listOf())
}
val withdrawStatus = MutableLiveData().apply {
value = WithdrawStatus.None()
}
val pendingOperations = MutableLiveData().apply {
value = PendingOperations(listOf())
}
private val mHistoryProgress = MutableLiveData()
val historyProgress: LiveData = mHistoryProgress
val historyShowAll = MutableLiveData()
val history: LiveData = historyShowAll.switchMap { showAll ->
loadHistory(showAll)
.onStart { mHistoryProgress.postValue(true) }
.onCompletion { mHistoryProgress.postValue(false) }
.asLiveData(Dispatchers.IO)
}
val showProgressBar = MutableLiveData()
private var activeGetBalance = 0
private var activeGetPending = 0
private var currentWithdrawRequestId = 0
private val walletBackendApi = WalletBackendApi(app)
private val mapper = ObjectMapper()
.registerModule(KotlinModule())
.configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
val paymentManager = PaymentManager(walletBackendApi, mapper)
init {
getBalances()
getPending()
walletBackendApi.notificationHandler = {
Log.i(TAG, "got notification from wallet")
getBalances()
getPending()
}
walletBackendApi.connectedHandler = {
activeGetBalance = 0
activeGetPending = 0
getBalances()
getPending()
}
}
fun getBalances() {
if (activeGetBalance > 0) {
return
}
activeGetBalance++
walletBackendApi.sendRequest("getBalances", null) { isError, result ->
activeGetBalance--
if (isError) {
return@sendRequest
}
val balanceList = mutableListOf()
val byCurrency = result.getJSONObject("byCurrency")
val currencyList = byCurrency.keys().asSequence().toList().sorted()
for (currency in currencyList) {
val jsonAmount = byCurrency.getJSONObject(currency)
.getJSONObject("available")
val amount = Amount.fromJson(jsonAmount)
val jsonAmountIncoming = byCurrency.getJSONObject(currency)
.getJSONObject("pendingIncoming")
val amountIncoming = Amount.fromJson(jsonAmountIncoming)
balanceList.add(BalanceEntry(amount, amountIncoming))
}
balances.postValue(WalletBalances(true, balanceList))
}
}
private fun getPending() {
if (activeGetPending > 0) {
return
}
activeGetPending++
walletBackendApi.sendRequest("getPendingOperations", null) { isError, result ->
activeGetPending--
if (isError) {
Log.i(TAG, "got getPending error result")
return@sendRequest
}
Log.i(TAG, "got getPending result")
val pendingList = mutableListOf()
val pendingJson = result.getJSONArray("pendingOperations")
for (i in 0 until pendingJson.length()) {
val p = pendingJson.getJSONObject(i)
val type = p.getString("type")
pendingList.add(PendingOperationInfo(type, p))
}
Log.i(TAG, "Got ${pendingList.size} pending operations")
pendingOperations.postValue(PendingOperations((pendingList)))
}
}
private fun loadHistory(showAll: Boolean) = callbackFlow {
mHistoryProgress.postValue(true)
walletBackendApi.sendRequest("getHistory", null) { isError, result ->
if (isError) {
// TODO show error message in [WalletHistory] fragment
close()
return@sendRequest
}
val history = History()
val json = result.getJSONArray("history")
for (i in 0 until json.length()) {
val event: HistoryEvent = mapper.readValue(json.getString(i))
event.json = json.getJSONObject(i)
history.add(event)
}
history.reverse() // show latest first
mHistoryProgress.postValue(false)
offer(if (showAll) history else history.filter { it.showToUser } as History)
close()
}
awaitClose()
}
fun withdrawTestkudos() {
testWithdrawalInProgress.value = true
walletBackendApi.sendRequest("withdrawTestkudos", null) { _, _ ->
testWithdrawalInProgress.postValue(false)
}
}
fun dangerouslyReset() {
walletBackendApi.sendRequest("reset", null)
testWithdrawalInProgress.value = false
balances.value = WalletBalances(false, listOf())
}
fun startTunnel() {
walletBackendApi.sendRequest("startTunnel", null)
}
fun stopTunnel() {
walletBackendApi.sendRequest("stopTunnel", null)
}
fun tunnelResponse(resp: String) {
val respJson = JSONObject(resp)
walletBackendApi.sendRequest("tunnelResponse", respJson)
}
fun getWithdrawalInfo(talerWithdrawUri: String) {
val args = JSONObject()
args.put("talerWithdrawUri", talerWithdrawUri)
withdrawStatus.value = WithdrawStatus.Loading(talerWithdrawUri)
this.currentWithdrawRequestId++
val myWithdrawRequestId = this.currentWithdrawRequestId
walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result ->
if (isError) {
Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}")
return@sendRequest
}
if (myWithdrawRequestId != this.currentWithdrawRequestId) {
val mismatch = "$myWithdrawRequestId != ${this.currentWithdrawRequestId}"
Log.w(TAG, "Got withdraw result for different request id $mismatch")
return@sendRequest
}
Log.v(TAG, "got getWithdrawDetailsForUri result")
val status = withdrawStatus.value
if (status !is WithdrawStatus.Loading) {
Log.v(TAG, "ignoring withdrawal info result, not loading.")
return@sendRequest
}
val wi = result.getJSONObject("bankWithdrawDetails")
val suggestedExchange = wi.getString("suggestedExchange")
// We just use the suggested exchange, in the future there will be
// a selection dialog.
getWithdrawalInfoWithExchange(talerWithdrawUri, suggestedExchange)
}
}
private fun getWithdrawalInfoWithExchange(talerWithdrawUri: String, selectedExchange: String) {
val args = JSONObject()
args.put("talerWithdrawUri", talerWithdrawUri)
args.put("selectedExchange", selectedExchange)
this.currentWithdrawRequestId++
val myWithdrawRequestId = this.currentWithdrawRequestId
walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result ->
if (isError) {
Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}")
return@sendRequest
}
if (myWithdrawRequestId != this.currentWithdrawRequestId) {
val mismatch = "$myWithdrawRequestId != ${this.currentWithdrawRequestId}"
Log.w(TAG, "Got withdraw result for different request id $mismatch")
return@sendRequest
}
Log.v(TAG, "got getWithdrawDetailsForUri result (with exchange details)")
val status = withdrawStatus.value
if (status !is WithdrawStatus.Loading) {
Log.v(TAG, "ignoring withdrawal info result, not loading.")
return@sendRequest
}
val ei = result.getJSONObject("exchangeWithdrawDetails")
val termsOfServiceAccepted = ei.getBoolean("termsOfServiceAccepted")
if (!termsOfServiceAccepted) {
val exchange = ei.getJSONObject("exchangeInfo")
val tosText = exchange.getString("termsOfServiceText")
val tosEtag = exchange.optString("termsOfServiceLastEtag", "undefined")
withdrawStatus.postValue(
WithdrawStatus.TermsOfServiceReviewRequired(
status.talerWithdrawUri,
selectedExchange,
tosText,
tosEtag
)
)
} else {
val wi = result.getJSONObject("bankWithdrawDetails")
val suggestedExchange = wi.getString("suggestedExchange")
val amount = Amount.fromJson(wi.getJSONObject("amount"))
withdrawStatus.postValue(
WithdrawStatus.ReceivedDetails(
status.talerWithdrawUri,
amount,
suggestedExchange
)
)
}
}
}
fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String) {
val args = JSONObject()
args.put("talerWithdrawUri", talerWithdrawUri)
args.put("selectedExchange", selectedExchange)
withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri)
walletBackendApi.sendRequest("acceptWithdrawal", args) { isError, _ ->
if (isError) {
Log.v(TAG, "got acceptWithdrawal error result")
return@sendRequest
}
Log.v(TAG, "got acceptWithdrawal result")
val status = withdrawStatus.value
if (status !is WithdrawStatus.Withdrawing) {
Log.v(TAG, "ignoring acceptWithdrawal result, invalid state")
}
withdrawStatus.postValue(WithdrawStatus.Success())
}
}
fun retryPendingNow() {
walletBackendApi.sendRequest("retryPendingNow", null)
}
override fun onCleared() {
walletBackendApi.destroy()
super.onCleared()
}
/**
* Accept the currently displayed terms of service.
*/
fun acceptCurrentTermsOfService() {
when (val s = withdrawStatus.value) {
is WithdrawStatus.TermsOfServiceReviewRequired -> {
val args = JSONObject()
args.put("exchangeBaseUrl", s.exchangeBaseUrl)
args.put("etag", s.tosEtag)
walletBackendApi.sendRequest("acceptExchangeTermsOfService", args) { isError, _ ->
if (isError) {
return@sendRequest
}
// Try withdrawing again with accepted ToS
getWithdrawalInfo(s.talerWithdrawUri)
}
}
}
}
fun cancelCurrentWithdraw() {
currentWithdrawRequestId++
withdrawStatus.value = WithdrawStatus.None()
}
}