/* * 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import net.taler.common.Amount import net.taler.common.ContractTerms import net.taler.wallet.TAG import net.taler.wallet.accounts.PaytoUriIban import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi 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 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 mPayStatus = MutableLiveData(PayStatus.None) internal val payStatus: LiveData = mPayStatus private val mDepositState = MutableStateFlow(DepositState.Start) internal val depositState = mDepositState.asStateFlow() @UiThread fun preparePay(url: String) = scope.launch { mPayStatus.value = PayStatus.Loading api.request("preparePayForUri", PreparePayResponse.serializer()) { put("talerPayUri", url) }.onError { handleError("preparePayForUri", it) }.onSuccess { response -> mPayStatus.value = when (response) { is PaymentPossibleResponse -> response.toPayStatusPrepared() is InsufficientBalanceResponse -> InsufficientBalance( response.contractTerms, response.amountRaw ) is AlreadyConfirmedResponse -> AlreadyPaid } } } 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") { put("proposalId", proposalId) }.onError { Log.e(TAG, "received error response to abortProposal") handleError("abortProposal", it) }.onSuccess { mPayStatus.postValue(PayStatus.None) } } @UiThread fun resetPayStatus() { mPayStatus.value = PayStatus.None } private fun handleError(operation: String, error: TalerErrorInfo) { Log.e(TAG, "got $operation error result $error") mPayStatus.value = PayStatus.Error(error.userFacingMsg) } /* Deposits */ @UiThread fun onDepositButtonClicked(amount: Amount, receiverName: String, iban: String, bic: String) { val paytoUri: String = PaytoUriIban( iban = iban, bic = bic, targetPath = "", params = mapOf("receiver-name" to receiverName), ).paytoUri if (depositState.value.showFees) { val effectiveDepositAmount = depositState.value.effectiveDepositAmount ?: Amount.zero(amount.currency) makeDeposit(paytoUri, amount, effectiveDepositAmount) } else { prepareDeposit(paytoUri, amount) } } private fun prepareDeposit(paytoUri: String, amount: Amount) { mDepositState.value = DepositState.CheckingFees scope.launch { api.request("prepareDeposit", PrepareDepositResponse.serializer()) { put("depositPaytoUri", paytoUri) put("amount", amount.toJSONString()) }.onError { Log.e(TAG, "Error prepareDeposit $it") mDepositState.value = DepositState.Error(it.userFacingMsg) }.onSuccess { mDepositState.value = DepositState.FeesChecked( effectiveDepositAmount = it.effectiveDepositAmount, ) } } } private fun makeDeposit( paytoUri: String, amount: Amount, effectiveDepositAmount: Amount, ) { mDepositState.value = DepositState.MakingDeposit(effectiveDepositAmount) scope.launch { api.request("createDepositGroup", CreateDepositGroupResponse.serializer()) { put("depositPaytoUri", paytoUri) put("amount", amount.toJSONString()) }.onError { Log.e(TAG, "Error createDepositGroup $it") mDepositState.value = DepositState.Error(it.userFacingMsg) }.onSuccess { mDepositState.value = DepositState.Success } } } @UiThread fun resetDepositState() { mDepositState.value = DepositState.Start } } @Serializable data class PrepareDepositResponse( val totalDepositCost: Amount, val effectiveDepositAmount: Amount, ) @Serializable data class CreateDepositGroupResponse( val depositGroupId: String, val transactionId: String, )