/*
* 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.payment
import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import net.taler.common.Amount
import net.taler.common.ContractTerms
import net.taler.wallet.TAG
import net.taler.wallet.backend.WalletBackendApi
import org.json.JSONObject
import java.net.MalformedURLException
val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$")
class PaymentManager(
private val walletBackendApi: WalletBackendApi,
private val mapper: ObjectMapper
) {
private val mPayStatus = MutableLiveData(PayStatus.None)
internal val payStatus: LiveData = mPayStatus
private val mDetailsShown = MutableLiveData()
internal val detailsShown: LiveData = mDetailsShown
private var currentPayRequestId = 0
@UiThread
fun preparePay(url: String) {
mPayStatus.value = PayStatus.Loading
mDetailsShown.value = false
val args = JSONObject(mapOf("url" to url))
currentPayRequestId += 1
val payRequestId = currentPayRequestId
walletBackendApi.sendRequest("preparePay", args) { isError, result ->
when {
isError -> {
Log.v(TAG, "got preparePay error result")
mPayStatus.value = PayStatus.Error(result.toString())
}
payRequestId != this.currentPayRequestId -> {
Log.v(TAG, "preparePay result was for old request")
}
else -> {
val status = result.getString("status")
try {
mPayStatus.postValue(getPayStatusUpdate(status, result))
} catch (e: Exception) {
Log.e(TAG, "Error getting PayStatusUpdate", e)
mPayStatus.postValue(PayStatus.Error(e.message ?: "unknown error"))
}
}
}
}
}
private fun getPayStatusUpdate(status: String, json: JSONObject) = when (status) {
"payment-possible" -> PayStatus.Prepared(
contractTerms = getContractTerms(json),
proposalId = json.getString("proposalId"),
totalFees = Amount.fromJsonObject(json.getJSONObject("totalFees"))
)
"paid" -> PayStatus.AlreadyPaid(getContractTerms(json))
"insufficient-balance" -> PayStatus.InsufficientBalance(getContractTerms(json))
"error" -> PayStatus.Error("got some error")
else -> PayStatus.Error("unknown status")
}
private fun getContractTerms(json: JSONObject): ContractTerms {
val terms: ContractTerms = mapper.readValue(json.getString("contractTermsRaw"))
// validate product images
terms.products.forEach { product ->
product.image?.let { image ->
if (REGEX_PRODUCT_IMAGE.matchEntire(image) == null) {
throw MalformedURLException("Invalid image data URL for ${product.description}")
}
}
}
return terms
}
@UiThread
fun toggleDetailsShown() {
val oldValue = mDetailsShown.value ?: false
mDetailsShown.value = !oldValue
}
fun confirmPay(proposalId: String) {
val args = JSONObject(mapOf("proposalId" to proposalId))
walletBackendApi.sendRequest("confirmPay", args) { _, _ ->
mPayStatus.postValue(PayStatus.Success)
}
}
@UiThread
fun abortPay() {
val ps = payStatus.value
if (ps is PayStatus.Prepared) {
abortProposal(ps.proposalId)
}
resetPayStatus()
}
internal fun abortProposal(proposalId: String) {
val args = JSONObject(mapOf("proposalId" to proposalId))
Log.i(TAG, "aborting proposal")
walletBackendApi.sendRequest("abortProposal", args) { isError, _ ->
if (isError) {
Log.e(TAG, "received error response to abortProposal")
return@sendRequest
}
mPayStatus.postValue(PayStatus.None)
}
}
@UiThread
fun resetPayStatus() {
mPayStatus.value = PayStatus.None
}
}
sealed class PayStatus {
object None : PayStatus()
object Loading : PayStatus()
data class Prepared(
val contractTerms: ContractTerms,
val proposalId: String,
val totalFees: Amount
) : PayStatus()
data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus()
data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus()
data class Error(val error: String) : PayStatus()
object Success : PayStatus()
}