/* * 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import net.taler.lib.common.Amount import net.taler.common.ContractTerms import net.taler.wallet.TAG import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.backend.WalletErrorInfo import net.taler.wallet.payment.PayStatus.AlreadyPaid import net.taler.wallet.payment.PayStatus.InsufficientBalance import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse import org.json.JSONObject import java.net.MalformedURLException val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$") sealed class PayStatus { object None : PayStatus() object Loading : PayStatus() data class Prepared( val contractTerms: ContractTerms, val proposalId: String, val amountRaw: Amount, val amountEffective: Amount ) : PayStatus() data class InsufficientBalance( val contractTerms: ContractTerms, val amountRaw: Amount ) : PayStatus() // TODO bring user to fulfilment URI object AlreadyPaid : PayStatus() data class Error(val error: String) : PayStatus() data class Success(val currency: String) : PayStatus() } class PaymentManager( private val api: WalletBackendApi, private val scope: CoroutineScope, private val mapper: ObjectMapper ) { private val mPayStatus = MutableLiveData(PayStatus.None) internal val payStatus: LiveData = mPayStatus private val mDetailsShown = MutableLiveData() internal val detailsShown: LiveData = mDetailsShown @UiThread fun preparePay(url: String) = scope.launch { mPayStatus.value = PayStatus.Loading mDetailsShown.value = false api.request("preparePay", mapper) { put("talerPayUri", url) }.onError { handleError("preparePay", it) }.onSuccess { response -> Log.e(TAG, "PreparePayResponse $response") // TODO remove mPayStatus.value = when (response) { is PaymentPossibleResponse -> response.toPayStatusPrepared() is InsufficientBalanceResponse -> InsufficientBalance( response.contractTerms, response.amountRaw ) is AlreadyConfirmedResponse -> AlreadyPaid } } } // TODO validate product images (or leave to wallet-core?) 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 } fun confirmPay(proposalId: String, currency: String) = scope.launch { api.request("confirmPay", ConfirmPayResult.serializer()) { put("proposalId", proposalId) }.onError { handleError("confirmPay", it) }.onSuccess { mPayStatus.postValue(PayStatus.Success(currency)) } } @UiThread fun abortPay() { val ps = payStatus.value if (ps is PayStatus.Prepared) { abortProposal(ps.proposalId) } resetPayStatus() } internal fun abortProposal(proposalId: String) = scope.launch { Log.i(TAG, "aborting proposal") api.request("abortProposal", mapper) { put("proposalId", proposalId) }.onError { Log.e(TAG, "received error response to abortProposal") handleError("abortProposal", it) }.onSuccess { mPayStatus.postValue(PayStatus.None) } } @UiThread fun toggleDetailsShown() { val oldValue = mDetailsShown.value ?: false mDetailsShown.value = !oldValue } @UiThread fun resetPayStatus() { mPayStatus.value = PayStatus.None } private fun handleError(operation: String, error: WalletErrorInfo) { Log.e(TAG, "got $operation error result $error") mPayStatus.value = PayStatus.Error(error.userFacingMsg) } }