From e19ba096d57353db6b1f141da4bf170ef2d2d534 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 29 Jul 2020 14:12:50 -0300 Subject: [wallet] update to new wallet-core with v8 exchange API (except payments which are still buggy) --- wallet/build.gradle | 4 +- .../src/main/java/net/taler/wallet/MainActivity.kt | 5 +- .../src/main/java/net/taler/wallet/MainFragment.kt | 2 +- .../main/java/net/taler/wallet/MainViewModel.kt | 39 +--- .../net/taler/wallet/backend/WalletBackendApi.kt | 4 +- .../taler/wallet/backend/WalletBackendService.kt | 9 +- .../net/taler/wallet/balances/BalanceAdapter.kt | 11 +- .../net/taler/wallet/balances/BalancesFragment.kt | 2 +- .../main/java/net/taler/wallet/crypto/Encoding.kt | 134 ------------ .../net/taler/wallet/exchanges/ExchangeFees.kt | 95 +++++++++ .../net/taler/wallet/exchanges/ExchangeManager.kt | 3 +- .../wallet/exchanges/SelectExchangeFragment.kt | 144 +++++++++++++ .../net/taler/wallet/history/DevHistoryAdapter.kt | 110 ---------- .../net/taler/wallet/history/DevHistoryFragment.kt | 87 -------- .../net/taler/wallet/history/DevHistoryManager.kt | 75 ------- .../java/net/taler/wallet/history/HistoryEvent.kt | 199 ------------------ .../net/taler/wallet/history/JsonDialogFragment.kt | 57 ------ .../net/taler/wallet/payment/PaymentManager.kt | 105 +++++----- .../net/taler/wallet/payment/PaymentResponses.kt | 62 ++++++ .../wallet/pending/PendingOperationsManager.kt | 4 +- .../wallet/transactions/TransactionManager.kt | 8 - .../wallet/transactions/TransactionsFragment.kt | 2 +- .../java/net/taler/wallet/withdraw/ExchangeFees.kt | 95 --------- .../wallet/withdraw/ManualWithdrawFragment.kt | 2 +- .../wallet/withdraw/PromptWithdrawFragment.kt | 26 ++- .../wallet/withdraw/ReviewExchangeTosFragment.kt | 2 +- .../wallet/withdraw/SelectExchangeFragment.kt | 142 ------------- .../net/taler/wallet/withdraw/WithdrawManager.kt | 227 ++++++++++----------- wallet/src/main/res/drawable/ic_directions.xml | 25 --- wallet/src/main/res/drawable/ic_edit.xml | 10 + wallet/src/main/res/drawable/ic_history.xml | 9 - .../res/drawable/transaction_payment_aborted.xml | 25 --- .../main/res/drawable/transaction_tip_declined.xml | 25 --- wallet/src/main/res/layout/fragment_json.xml | 40 ---- .../main/res/layout/fragment_prompt_withdraw.xml | 6 +- .../src/main/res/layout/fragment_transactions.xml | 2 +- wallet/src/main/res/layout/list_item_history.xml | 75 ------- wallet/src/main/res/menu/activity_main_drawer.xml | 4 - wallet/src/main/res/navigation/nav_graph.xml | 12 +- wallet/src/main/res/values/strings.xml | 1 - wallet/src/main/res/xml/settings_backup.xml | 2 +- .../net/taler/wallet/crypto/Base32CrockfordTest.kt | 36 ---- 42 files changed, 526 insertions(+), 1401 deletions(-) delete mode 100644 wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt create mode 100644 wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt create mode 100644 wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt delete mode 100644 wallet/src/main/res/drawable/ic_directions.xml create mode 100644 wallet/src/main/res/drawable/ic_edit.xml delete mode 100644 wallet/src/main/res/drawable/ic_history.xml delete mode 100644 wallet/src/main/res/drawable/transaction_payment_aborted.xml delete mode 100644 wallet/src/main/res/drawable/transaction_tip_declined.xml delete mode 100644 wallet/src/main/res/layout/fragment_json.xml delete mode 100644 wallet/src/main/res/layout/list_item_history.xml delete mode 100644 wallet/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt diff --git a/wallet/build.gradle b/wallet/build.gradle index 5b28c6c..8cca8dc 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -23,7 +23,7 @@ plugins { id "de.undercouch.download" } -def walletCoreVersion = "v0.7.1-dev.10" +def walletCoreVersion = "v0.7.1-dev.14" static def versionCodeEpoch() { return (new Date().getTime() / 1000).toInteger() @@ -47,7 +47,7 @@ android { minSdkVersion 24 targetSdkVersion 29 versionCode 6 - versionName "0.7.1.dev.10" + versionName "0.7.1.dev.14" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "WALLET_CORE_VERSION", "\"$walletCoreVersion\"" } diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index fdb8cf8..c7c31ca 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, setSupportActionBar(toolbar) val appBarConfiguration = AppBarConfiguration( - setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations, R.id.nav_history), + setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations), drawer_layout ) toolbar.setupWithNavController(nav, appBarConfiguration) @@ -122,7 +122,6 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, R.id.nav_home -> nav.navigate(R.id.nav_main) R.id.nav_settings -> nav.navigate(R.id.nav_settings) R.id.nav_pending_operations -> nav.navigate(R.id.nav_pending_operations) - R.id.nav_history -> nav.navigate(R.id.nav_history) } drawer_layout.closeDrawer(START) return true @@ -160,7 +159,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Log.v(TAG, "navigating!") // there's more than one entry point, so use global action nav.navigate(R.id.action_global_promptWithdraw) - model.withdrawManager.getWithdrawalInfo(url) + model.withdrawManager.getWithdrawalDetails(url) } url.toLowerCase(ROOT).startsWith("taler://refund/") -> { model.showProgressBar.value = true diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt index a735987..d5bd3fc 100644 --- a/wallet/src/main/java/net/taler/wallet/MainFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/MainFragment.kt @@ -49,7 +49,7 @@ class MainFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { model.balances.observe(viewLifecycleOwner, Observer { - onBalancesChanged(it.values.toList()) + onBalancesChanged(it) }) model.transactionsEvent.observe(viewLifecycleOwner, EventObserver { currency -> // we only need to navigate to a dedicated list, when in multi-currency mode diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 46f5021..3d725d0 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -27,14 +27,13 @@ import androidx.lifecycle.viewModelScope import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import net.taler.common.Amount +import com.fasterxml.jackson.module.kotlin.readValue import net.taler.common.Event import net.taler.common.assertUiThread import net.taler.common.toEvent import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.balances.BalanceItem import net.taler.wallet.exchanges.ExchangeManager -import net.taler.wallet.history.DevHistoryManager import net.taler.wallet.payment.PaymentManager import net.taler.wallet.pending.PendingOperationsManager import net.taler.wallet.refund.RefundManager @@ -55,8 +54,8 @@ private val transactionNotifications = listOf( class MainViewModel(val app: Application) : AndroidViewModel(app) { - private val mBalances = MutableLiveData>() - val balances: LiveData> = mBalances.distinctUntilChanged() + private val mBalances = MutableLiveData>() + val balances: LiveData> = mBalances.distinctUntilChanged() val devMode = MutableLiveData(BuildConfig.DEBUG) val showProgressBar = MutableLiveData() @@ -85,7 +84,6 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { // refresh pending ops and history with each notification if (devMode.value == true) { pendingOperationsManager.getPending() - historyManager.loadHistory() } } } @@ -94,12 +92,10 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { .registerModule(KotlinModule()) .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) - val withdrawManager = WithdrawManager(walletBackendApi) + val withdrawManager = WithdrawManager(walletBackendApi, mapper) val paymentManager = PaymentManager(walletBackendApi, mapper) val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(walletBackendApi) - val historyManager: DevHistoryManager = - DevHistoryManager(walletBackendApi, viewModelScope, mapper) val transactionManager: TransactionManager = TransactionManager(walletBackendApi, viewModelScope, mapper) val refundManager = RefundManager(walletBackendApi) @@ -123,26 +119,13 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { @UiThread fun loadBalances() { showProgressBar.value = true - walletBackendApi.sendRequest("getBalances", null) { isError, result -> + walletBackendApi.sendRequest("getBalances") { isError, result -> if (isError) { Log.e(TAG, "Error retrieving balances: ${result.toString(2)}") return@sendRequest } - val balanceMap = HashMap() - 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.fromJsonObject(jsonAmount) - val jsonAmountIncoming = byCurrency.getJSONObject(currency) - .getJSONObject("pendingIncoming") - val amountIncoming = Amount.fromJsonObject(jsonAmountIncoming) - val hasPending = transactionManager.hasPending(currency) - balanceMap[currency] = BalanceItem(amount, amountIncoming, hasPending) - } - mBalances.postValue(balanceMap) - showProgressBar.postValue(false) + mBalances.value = mapper.readValue(result.getString("balances")) + showProgressBar.value = false } } @@ -156,17 +139,17 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { @UiThread fun dangerouslyReset() { - walletBackendApi.sendRequest("reset", null) + walletBackendApi.sendRequest("reset") withdrawManager.testWithdrawalInProgress.value = false - mBalances.value = emptyMap() + mBalances.value = emptyList() } fun startTunnel() { - walletBackendApi.sendRequest("startTunnel", null) + walletBackendApi.sendRequest("startTunnel") } fun stopTunnel() { - walletBackendApi.sendRequest("stopTunnel", null) + walletBackendApi.sendRequest("stopTunnel") } fun tunnelResponse(resp: String) { diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt index 3ffcd7b..51b3419 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt @@ -30,7 +30,7 @@ import android.util.Log import android.util.SparseArray import org.json.JSONObject import java.lang.ref.WeakReference -import java.util.* +import java.util.LinkedList class WalletBackendApi( private val app: Application, @@ -115,7 +115,7 @@ class WalletBackendApi( fun sendRequest( operation: String, - args: JSONObject?, + args: JSONObject? = null, onResponse: (isError: Boolean, message: JSONObject) -> Unit = { _, _ -> } ) { val requestID = nextRequestID++ diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt index c810054..f39a3e7 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt @@ -30,7 +30,7 @@ import net.taler.wallet.BuildConfig.WALLET_CORE_VERSION import net.taler.wallet.HostCardEmulatorService import org.json.JSONObject import java.lang.ref.WeakReference -import java.util.* +import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap import kotlin.system.exitProcess @@ -56,9 +56,10 @@ class WalletBackendService : Service() { private val subscribers = LinkedList() override fun onCreate() { - val talerWalletAndroidCode = assets.open("taler-wallet-android-$WALLET_CORE_VERSION.js").use { - it.readBytes().toString(Charsets.UTF_8) - } + val talerWalletAndroidCode = + assets.open("taler-wallet-android-$WALLET_CORE_VERSION.js").use { + it.readBytes().toString(Charsets.UTF_8) + } Log.i(TAG, "onCreate in wallet backend service") diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt index be50364..c090e75 100644 --- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt +++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt @@ -28,7 +28,14 @@ import net.taler.common.Amount import net.taler.wallet.R import net.taler.wallet.balances.BalanceAdapter.BalanceViewHolder -data class BalanceItem(val available: Amount, val pendingIncoming: Amount, val hasPending: Boolean) +data class BalanceItem( + val available: Amount, + val pendingIncoming: Amount, + val pendingOutgoing: Amount +) { + val currency: String get() = available.currency + val hasPending: Boolean get() = !pendingIncoming.isZero() || !pendingOutgoing.isZero() +} class BalanceAdapter(private val listener: BalanceClickListener) : Adapter() { @@ -65,7 +72,7 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter - */ - -package net.taler.wallet.crypto - -import java.io.ByteArrayOutputStream - -class EncodingException : Exception("Invalid encoding") - - -object Base32Crockford { - - private fun ByteArray.getIntAt(index: Int): Int { - val x = this[index].toInt() - return if (x >= 0) x else (x + 256) - } - - private var encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" - - fun encode(data: ByteArray): String { - val sb = StringBuilder() - val size = data.size - var bitBuf = 0 - var numBits = 0 - var pos = 0 - while (pos < size || numBits > 0) { - if (pos < size && numBits < 5) { - val d = data.getIntAt(pos++) - bitBuf = (bitBuf shl 8) or d - numBits += 8 - } - if (numBits < 5) { - // zero-padding - bitBuf = bitBuf shl (5 - numBits) - numBits = 5 - } - val v = bitBuf.ushr(numBits - 5) and 31 - sb.append(encTable[v]) - numBits -= 5 - } - return sb.toString() - } - - fun decode(encoded: String, out: ByteArrayOutputStream) { - val size = encoded.length - var bitpos = 0 - var bitbuf = 0 - var readPosition = 0 - - while (readPosition < size || bitpos > 0) { - //println("at position $readPosition with bitpos $bitpos") - if (readPosition < size) { - val v = getValue(encoded[readPosition++]) - bitbuf = (bitbuf shl 5) or v - bitpos += 5 - } - while (bitpos >= 8) { - val d = (bitbuf ushr (bitpos - 8)) and 0xFF - out.write(d) - bitpos -= 8 - } - if (readPosition == size && bitpos > 0) { - bitbuf = (bitbuf shl (8 - bitpos)) and 0xFF - bitpos = if (bitbuf == 0) 0 else 8 - } - } - } - - fun decode(encoded: String): ByteArray { - val out = ByteArrayOutputStream() - decode(encoded, out) - return out.toByteArray() - } - - private fun getValue(chr: Char): Int { - var a = chr - when (a) { - 'O', 'o' -> a = '0' - 'i', 'I', 'l', 'L' -> a = '1' - 'u', 'U' -> a = 'V' - } - if (a in '0'..'9') - return a - '0' - if (a in 'a'..'z') - a = Character.toUpperCase(a) - var dec = 0 - if (a in 'A'..'Z') { - if ('I' < a) dec++ - if ('L' < a) dec++ - if ('O' < a) dec++ - if ('U' < a) dec++ - return a - 'A' + 10 - dec - } - throw EncodingException() - } - - /** - * Compute the length of the resulting string when encoding data of the given size - * in bytes. - * - * @param dataSize size of the data to encode in bytes - * @return size of the string that would result from encoding - */ - @Suppress("unused") - fun calculateEncodedStringLength(dataSize: Int): Int { - return (dataSize * 8 + 4) / 5 - } - - /** - * Compute the length of the resulting data in bytes when decoding a (valid) string of the - * given size. - * - * @param stringSize size of the string to decode - * @return size of the resulting data in bytes - */ - @Suppress("unused") - fun calculateDecodedDataLength(stringSize: Int): Int { - return stringSize * 5 / 8 - } -} - diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt new file mode 100644 index 0000000..ae90b98 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt @@ -0,0 +1,95 @@ +/* + * 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.exchanges + +import net.taler.common.Amount +import net.taler.common.Timestamp +import org.json.JSONObject + +data class CoinFee( + val coin: Amount, + val quantity: Int, + val feeDeposit: Amount, + val feeRefresh: Amount, + val feeRefund: Amount, + val feeWithdraw: Amount +) + +data class WireFee( + val start: Timestamp, + val end: Timestamp, + val wireFee: Amount, + val closingFee: Amount +) + +data class ExchangeFees( + val withdrawFee: Amount, + val overhead: Amount, + val earliestDepositExpiration: Timestamp, + val coinFees: List, + val wireFees: List +) { + companion object { + fun fromExchangeWithdrawDetailsJson(json: JSONObject): ExchangeFees { + val earliestDepositExpiration = + json.getJSONObject("earliestDepositExpiration").getLong("t_ms") + val selectedDenoms = json.getJSONObject("selectedDenoms") + val denoms = selectedDenoms.getJSONArray("selectedDenoms") + val coinFees = ArrayList(denoms.length()) + for (i in 0 until denoms.length()) { + val denom = denoms.getJSONObject(i) + val d = denom.getJSONObject("denom") + val coinFee = CoinFee( + coin = Amount.fromJsonObject(d.getJSONObject("value")), + quantity = denom.getInt("count"), + feeDeposit = Amount.fromJsonObject(d.getJSONObject("feeDeposit")), + feeRefresh = Amount.fromJsonObject(d.getJSONObject("feeRefresh")), + feeRefund = Amount.fromJsonObject(d.getJSONObject("feeRefund")), + feeWithdraw = Amount.fromJsonObject(d.getJSONObject("feeWithdraw")) + ) + coinFees.add(coinFee) + } + + val wireFeesJson = json.getJSONObject("wireFees") + val feesForType = wireFeesJson.getJSONObject("feesForType") + val bankFees = feesForType.getJSONArray("x-taler-bank") + val wireFees = ArrayList(bankFees.length()) + for (i in 0 until bankFees.length()) { + val fee = bankFees.getJSONObject(i) + val startStamp = + fee.getJSONObject("startStamp").getLong("t_ms") + val endStamp = + fee.getJSONObject("endStamp").getLong("t_ms") + val wireFee = WireFee( + start = Timestamp(startStamp), + end = Timestamp(endStamp), + wireFee = Amount.fromJsonObject(fee.getJSONObject("wireFee")), + closingFee = Amount.fromJsonObject(fee.getJSONObject("closingFee")) + ) + wireFees.add(wireFee) + } + + return ExchangeFees( + withdrawFee = Amount.fromJsonObject(json.getJSONObject("withdrawFee")), + overhead = Amount.fromJsonObject(json.getJSONObject("overhead")), + earliestDepositExpiration = Timestamp(earliestDepositExpiration), + coinFees = coinFees, + wireFees = wireFees + ) + } + } +} diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt index 41c8f2c..9d31b5f 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -21,7 +21,6 @@ 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.Event import net.taler.common.toEvent import net.taler.wallet.TAG @@ -46,7 +45,7 @@ class ExchangeManager( private fun list(): LiveData> { mProgress.value = true - walletBackendApi.sendRequest("listExchanges", JSONObject()) { isError, result -> + walletBackendApi.sendRequest("listExchanges") { isError, result -> if (isError) { throw AssertionError("Wallet core failed to return exchanges!") } else { diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt new file mode 100644 index 0000000..ef4894d --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt @@ -0,0 +1,144 @@ +/* + * 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.exchanges + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat.getColor +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import kotlinx.android.synthetic.main.fragment_select_exchange.* +import net.taler.common.Amount +import net.taler.common.toRelativeTime +import net.taler.common.toShortDate +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder +import net.taler.wallet.exchanges.WireFeeAdapter.WireFeeViewHolder + +class SelectExchangeFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val withdrawManager by lazy { model.withdrawManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_select_exchange, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val fees = withdrawManager.exchangeFees ?: throw IllegalStateException() + if (fees.withdrawFee.isZero()) { + withdrawFeeLabel.visibility = GONE + withdrawFeeView.visibility = GONE + } else withdrawFeeView.setAmount(fees.withdrawFee) + if (fees.overhead.isZero()) { + overheadLabel.visibility = GONE + overheadView.visibility = GONE + } else overheadView.setAmount(fees.overhead) + expirationView.text = fees.earliestDepositExpiration.ms.toRelativeTime(requireContext()) + coinFeesList.adapter = + CoinFeeAdapter(fees.coinFees) + wireFeesList.adapter = + WireFeeAdapter(fees.wireFees) + } + + private fun TextView.setAmount(amount: Amount) { + if (amount.isZero()) text = amount.toString() + else { + text = getString(R.string.amount_negative, amount) + setTextColor(getColor(context, R.color.red)) + } + } + +} + +private class CoinFeeAdapter(private val items: List) : Adapter() { + override fun getItemCount() = items.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinFeeViewHolder { + val v = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_coin_fee, parent, false) + return CoinFeeViewHolder(v) + } + + override fun onBindViewHolder(holder: CoinFeeViewHolder, position: Int) { + holder.bind(items[position]) + } + + private class CoinFeeViewHolder(private val v: View) : ViewHolder(v) { + private val res = v.context.resources + private val coinView: TextView = v.findViewById(R.id.coinView) + private val withdrawFeeView: TextView = v.findViewById(R.id.withdrawFeeView) + private val depositFeeView: TextView = v.findViewById(R.id.depositFeeView) + private val refreshFeeView: TextView = v.findViewById(R.id.refreshFeeView) + private val refundFeeView: TextView = v.findViewById(R.id.refundFeeView) + fun bind(item: CoinFee) { + coinView.text = res.getQuantityString( + R.plurals.exchange_fee_coin, + item.quantity, + item.coin, + item.quantity + ) + withdrawFeeView.text = + v.context.getString(R.string.exchange_fee_withdraw_fee, item.feeWithdraw) + depositFeeView.text = + v.context.getString(R.string.exchange_fee_deposit_fee, item.feeDeposit) + refreshFeeView.text = + v.context.getString(R.string.exchange_fee_refresh_fee, item.feeRefresh) + refundFeeView.text = + v.context.getString(R.string.exchange_fee_refund_fee, item.feeRefresh) + } + } +} + +private class WireFeeAdapter(private val items: List) : Adapter() { + override fun getItemCount() = items.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WireFeeViewHolder { + val v = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_wire_fee, parent, false) + return WireFeeViewHolder(v) + } + + override fun onBindViewHolder(holder: WireFeeViewHolder, position: Int) { + holder.bind(items[position]) + } + + private class WireFeeViewHolder(private val v: View) : ViewHolder(v) { + private val validityView: TextView = v.findViewById(R.id.validityView) + private val wireFeeView: TextView = v.findViewById(R.id.wireFeeView) + private val closingFeeView: TextView = v.findViewById(R.id.closingFeeView) + fun bind(item: WireFee) { + validityView.text = v.context.getString( + R.string.exchange_fee_wire_fee_timespan, + item.start.ms.toShortDate(v.context), + item.end.ms.toShortDate(v.context) + ) + wireFeeView.text = + v.context.getString(R.string.exchange_fee_wire_fee_wire_fee, item.wireFee) + closingFeeView.text = + v.context.getString(R.string.exchange_fee_wire_fee_closing_fee, item.closingFee) + } + } +} diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt deleted file mode 100644 index a2684e1..0000000 --- a/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.history - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView.Adapter -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import net.taler.common.exhaustive -import net.taler.common.toRelativeTime -import net.taler.wallet.R -import net.taler.wallet.history.DevHistoryAdapter.HistoryViewHolder -import net.taler.wallet.transactions.AmountType - -internal class DevHistoryAdapter( - private val listener: OnEventClickListener -) : Adapter() { - - private var history: List = ArrayList() - - init { - setHasStableIds(false) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.list_item_history, parent, false) - return HistoryViewHolder(view) - } - - override fun getItemCount(): Int = history.size - - override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { - val transaction = history[position] - holder.bind(transaction) - } - - fun update(updatedHistory: List) { - this.history = updatedHistory - this.notifyDataSetChanged() - } - - internal open inner class HistoryViewHolder(private val v: View) : ViewHolder(v) { - - protected val context: Context = v.context - - private val icon: ImageView = v.findViewById(R.id.icon) - protected val title: TextView = v.findViewById(R.id.title) - private val time: TextView = v.findViewById(R.id.time) - private val amount: TextView = v.findViewById(R.id.amount) - - private val amountColor = amount.currentTextColor - - open fun bind(historyEvent: HistoryEvent) { - v.setOnClickListener { listener.onTransactionClicked(historyEvent) } - icon.setImageResource(historyEvent.icon) - title.text = historyEvent.title - time.text = historyEvent.timestamp.ms.toRelativeTime(context) - bindAmount(historyEvent.displayAmount) - } - - private fun bindAmount(displayAmount: DisplayAmount?) { - if (displayAmount == null) { - amount.visibility = GONE - } else { - amount.visibility = VISIBLE - when (displayAmount.type) { - AmountType.Positive -> { - amount.text = context.getString( - R.string.amount_positive, displayAmount.amount.amountStr - ) - amount.setTextColor(context.getColor(R.color.green)) - } - AmountType.Negative -> { - amount.text = context.getString( - R.string.amount_negative, displayAmount.amount.amountStr - ) - amount.setTextColor(context.getColor(R.color.red)) - } - AmountType.Neutral -> { - amount.text = displayAmount.amount.amountStr - amount.setTextColor(amountColor) - } - }.exhaustive - } - } - - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt deleted file mode 100644 index c3c07a3..0000000 --- a/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.history - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.INVISIBLE -import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL -import kotlinx.android.synthetic.main.fragment_transactions.* -import net.taler.common.fadeIn -import net.taler.common.fadeOut -import net.taler.wallet.MainViewModel -import net.taler.wallet.R - -internal interface OnEventClickListener { - fun onTransactionClicked(historyEvent: HistoryEvent) -} - -class DevHistoryFragment : Fragment(), - OnEventClickListener { - - private val model: MainViewModel by activityViewModels() - private val historyManager by lazy { model.historyManager } - private val historyAdapter by lazy { DevHistoryAdapter(this) } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_transactions, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (savedInstanceState == null) historyManager.loadHistory() - - list.apply { - adapter = historyAdapter - addItemDecoration(DividerItemDecoration(context, VERTICAL)) - } - historyManager.progress.observe(viewLifecycleOwner, Observer { show -> - progressBar.visibility = if (show) VISIBLE else INVISIBLE - }) - historyManager.history.observe(viewLifecycleOwner, Observer { result -> - onHistoryResult(result) - }) - } - - override fun onTransactionClicked(historyEvent: HistoryEvent) { - JsonDialogFragment.new(historyEvent.json.toString(2)) - .show(parentFragmentManager, null) - } - - private fun onHistoryResult(result: HistoryResult) = when (result) { - HistoryResult.Error -> { - list.fadeOut() - emptyState.text = getString(R.string.transactions_error) - emptyState.fadeIn() - } - is HistoryResult.Success -> { - emptyState.visibility = if (result.history.isEmpty()) VISIBLE else INVISIBLE - historyAdapter.update(result.history) - list.fadeIn() - } - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt deleted file mode 100644 index 9052d6e..0000000 --- a/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.history - -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.Dispatchers -import kotlinx.coroutines.launch -import net.taler.wallet.backend.WalletBackendApi -import org.json.JSONObject -import java.util.* - -sealed class HistoryResult { - object Error : HistoryResult() - class Success(val history: List) : HistoryResult() -} - -class DevHistoryManager( - private val walletBackendApi: WalletBackendApi, - private val scope: CoroutineScope, - private val mapper: ObjectMapper -) { - - private val mProgress = MutableLiveData() - val progress: LiveData = mProgress - - private val mHistory = MutableLiveData() - val history: LiveData = mHistory - - @UiThread - internal fun loadHistory() { - mProgress.value = true - walletBackendApi.sendRequest("getHistory", null) { isError, result -> - scope.launch(Dispatchers.Default) { - onEventsLoaded(isError, result) - } - } - } - - private fun onEventsLoaded(isError: Boolean, result: JSONObject) { - if (isError) { - mHistory.postValue(HistoryResult.Error) - return - } - val history = LinkedList() - 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 - mProgress.postValue(false) - mHistory.postValue(HistoryResult.Success(history)) - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt deleted file mode 100644 index 3cbe7d7..0000000 --- a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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.history - -import androidx.annotation.DrawableRes -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonSubTypes.Type -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY -import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME -import com.fasterxml.jackson.annotation.JsonTypeName -import net.taler.common.Amount -import net.taler.common.Timestamp -import net.taler.wallet.R -import net.taler.wallet.transactions.AmountType -import org.json.JSONObject - -class DisplayAmount( - val amount: Amount, - val type: AmountType -) - -@JsonTypeInfo( - use = NAME, - include = PROPERTY, - property = "type", - defaultImpl = UnknownHistoryEvent::class -) -/** missing: -AuditorComplaintSent = "auditor-complained-sent", -AuditorComplaintProcessed = "auditor-complaint-processed", -AuditorTrustAdded = "auditor-trust-added", -AuditorTrustRemoved = "auditor-trust-removed", -ExchangeTermsAccepted = "exchange-terms-accepted", -ExchangePolicyChanged = "exchange-policy-changed", -ExchangeTrustAdded = "exchange-trust-added", -ExchangeTrustRemoved = "exchange-trust-removed", -FundsDepositedToSelf = "funds-deposited-to-self", -FundsRecouped = "funds-recouped", -ReserveCreated = "reserve-created", - */ -@JsonSubTypes( - Type(value = ExchangeAddedEvent::class, name = "exchange-added"), - Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"), - Type(value = ReserveBalanceUpdatedHistoryEvent::class, name = "reserve-balance-updated"), - Type(value = WithdrawHistoryEvent::class, name = "withdrawn"), - Type(value = OrderAcceptedHistoryEvent::class, name = "order-accepted"), - Type(value = OrderRefusedHistoryEvent::class, name = "order-refused"), - Type(value = OrderRedirectedHistoryEvent::class, name = "order-redirected"), - Type(value = PaymentHistoryEvent::class, name = "payment-sent"), - Type(value = PaymentAbortedHistoryEvent::class, name = "payment-aborted"), - Type(value = TipAcceptedHistoryEvent::class, name = "tip-accepted"), - Type(value = TipDeclinedHistoryEvent::class, name = "tip-declined"), - Type(value = RefundHistoryEvent::class, name = "refund"), - Type(value = RefreshHistoryEvent::class, name = "refreshed") -) -abstract class HistoryEvent( - val timestamp: Timestamp, - val eventId: String, - @get:DrawableRes - open val icon: Int = R.drawable.ic_account_balance -) { - val title: String get() = this::class.java.simpleName - open val displayAmount: DisplayAmount? = null - lateinit var json: JSONObject -} - -class UnknownHistoryEvent(timestamp: Timestamp, eventId: String) : HistoryEvent(timestamp, eventId) - -@JsonTypeName("exchange-added") -class ExchangeAddedEvent( - timestamp: Timestamp, - eventId: String -) : HistoryEvent(timestamp, eventId) - -@JsonTypeName("exchange-updated") -class ExchangeUpdatedEvent( - timestamp: Timestamp, - eventId: String -) : HistoryEvent(timestamp, eventId) - -@JsonTypeName("reserve-balance-updated") -class ReserveBalanceUpdatedHistoryEvent( - timestamp: Timestamp, - eventId: String, - val reserveBalance: Amount -) : HistoryEvent(timestamp, eventId) { - override val displayAmount = DisplayAmount(reserveBalance, AmountType.Neutral) -} - -@JsonTypeName("withdrawn") -class WithdrawHistoryEvent( - timestamp: Timestamp, - eventId: String, - val amountWithdrawnEffective: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.transaction_withdrawal - override val displayAmount = DisplayAmount(amountWithdrawnEffective, AmountType.Positive) -} - -@JsonTypeName("order-accepted") -class OrderAcceptedHistoryEvent( - timestamp: Timestamp, - eventId: String -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.ic_add_circle -} - -@JsonTypeName("order-refused") -class OrderRefusedHistoryEvent( - timestamp: Timestamp, - eventId: String -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.ic_cancel -} - -@JsonTypeName("payment-sent") -class PaymentHistoryEvent( - timestamp: Timestamp, - eventId: String, - val amountPaidWithFees: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.ic_cash_usd_outline - override val displayAmount = DisplayAmount(amountPaidWithFees, AmountType.Negative) -} - -@JsonTypeName("payment-aborted") -class PaymentAbortedHistoryEvent( - timestamp: Timestamp, - eventId: String, - amountLost: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.transaction_payment_aborted - override val displayAmount = DisplayAmount(amountLost, AmountType.Negative) -} - -@JsonTypeName("refreshed") -class RefreshHistoryEvent( - timestamp: Timestamp, - eventId: String, - val amountRefreshedEffective: Amount, - val amountRefreshedRaw: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.transaction_refresh - override val displayAmount = - DisplayAmount(amountRefreshedRaw - amountRefreshedEffective, AmountType.Negative) -} - -@JsonTypeName("order-redirected") -class OrderRedirectedHistoryEvent( - timestamp: Timestamp, - eventId: String -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.ic_directions -} - -@JsonTypeName("tip-accepted") -class TipAcceptedHistoryEvent( - timestamp: Timestamp, - eventId: String, - tipRaw: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.transaction_tip_accepted - override val displayAmount = DisplayAmount(tipRaw, AmountType.Positive) -} - -@JsonTypeName("tip-declined") -class TipDeclinedHistoryEvent( - timestamp: Timestamp, - eventId: String, - tipAmount: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.transaction_tip_declined - override val displayAmount = DisplayAmount(tipAmount, AmountType.Neutral) -} - -@JsonTypeName("refund") -class RefundHistoryEvent( - timestamp: Timestamp, - eventId: String, - val amountRefundedEffective: Amount -) : HistoryEvent(timestamp, eventId) { - override val icon = R.drawable.transaction_refund - override val displayAmount = DisplayAmount(amountRefundedEffective, AmountType.Positive) -} diff --git a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt deleted file mode 100644 index 31c2b93..0000000 --- a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.history - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import androidx.fragment.app.DialogFragment -import kotlinx.android.synthetic.main.fragment_json.* -import net.taler.wallet.R - -class JsonDialogFragment : DialogFragment() { - - companion object { - fun new(json: String): JsonDialogFragment { - return JsonDialogFragment().apply { - arguments = Bundle().apply { putString("json", json) } - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_json, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val json = requireArguments().getString("json") - jsonView.text = json - } - - override fun onStart() { - super.onStart() - dialog?.window?.setLayout(MATCH_PARENT, WRAP_CONTENT) - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt index 5c73d6c..c6351ee 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -26,11 +26,29 @@ import net.taler.common.Amount import net.taler.common.ContractTerms import net.taler.wallet.TAG import net.taler.wallet.backend.WalletBackendApi +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 totalFees: Amount + ) : PayStatus() + + data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus() + object AlreadyPaid : PayStatus() + data class Error(val error: String) : PayStatus() + data class Success(val currency: String) : PayStatus() +} + class PaymentManager( private val walletBackendApi: WalletBackendApi, private val mapper: ObjectMapper @@ -42,52 +60,27 @@ class PaymentManager( 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 - + val args = JSONObject(mapOf("talerPayUri" to url)) 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")) - } - } + if (isError) { + handleError("preparePay", result.toString(2)) + return@sendRequest + } + val response: PreparePayResponse = mapper.readValue(result.toString()) + Log.e(TAG, "PreparePayResponse $response") + mPayStatus.value = when (response) { + is PaymentPossibleResponse -> TODO() + is InsufficientBalanceResponse -> TODO() + is AlreadyConfirmedResponse -> TODO() } } } - 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 @@ -101,16 +94,13 @@ class PaymentManager( return terms } - @UiThread - fun toggleDetailsShown() { - val oldValue = mDetailsShown.value ?: false - mDetailsShown.value = !oldValue - } - fun confirmPay(proposalId: String, currency: String) { val args = JSONObject(mapOf("proposalId" to proposalId)) - - walletBackendApi.sendRequest("confirmPay", args) { _, _ -> + walletBackendApi.sendRequest("confirmPay", args) { isError, result -> + if (isError) { + handleError("preparePay", result.toString()) + return@sendRequest + } mPayStatus.postValue(PayStatus.Success(currency)) } } @@ -129,8 +119,9 @@ class PaymentManager( Log.i(TAG, "aborting proposal") - walletBackendApi.sendRequest("abortProposal", args) { isError, _ -> + walletBackendApi.sendRequest("abortProposal", args) { isError, result -> if (isError) { + handleError("abortProposal", result.toString(2)) Log.e(TAG, "received error response to abortProposal") return@sendRequest } @@ -138,24 +129,20 @@ class PaymentManager( } } + @UiThread + fun toggleDetailsShown() { + val oldValue = mDetailsShown.value ?: false + mDetailsShown.value = !oldValue + } + @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() + private fun handleError(operation: String, msg: String) { + Log.e(TAG, "got $operation error result $msg") + mPayStatus.value = PayStatus.Error(msg) + } - data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus() - data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus() - data class Error(val error: String) : PayStatus() - data class Success(val currency: String) : PayStatus() } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt new file mode 100644 index 0000000..4c5b010 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt @@ -0,0 +1,62 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonSubTypes.Type +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME +import com.fasterxml.jackson.annotation.JsonTypeName +import net.taler.common.ContractTerms +import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse +import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse +import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse + +@JsonTypeInfo(use = NAME, include = PROPERTY, property = "status") +@JsonSubTypes( + Type(value = PaymentPossibleResponse::class, name = "payment-possible"), + Type(value = AlreadyConfirmedResponse::class, name = "already-confirmed"), + Type(value = InsufficientBalanceResponse::class, name = "insufficient-balance") +) +sealed class PreparePayResponse(open val proposalId: String) { + @JsonTypeName("payment-possible") + data class PaymentPossibleResponse( + override val proposalId: String, + val contractTerms: ContractTerms + ) : PreparePayResponse(proposalId) + + @JsonTypeName("insufficient-balance") + data class InsufficientBalanceResponse( + override val proposalId: String, + val contractTerms: ContractTerms + ) : PreparePayResponse(proposalId) + + @JsonTypeName("already-confirmed") + data class AlreadyConfirmedResponse( + override val proposalId: String, + /** + * Did the payment succeed? + */ + val paid: Boolean, + + /** + * Redirect URL for the fulfillment page, only given if paid==true. + */ + val nextUrl: String? + ) : PreparePayResponse(proposalId) +} diff --git a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt index 6c58b81..7027687 100644 --- a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt +++ b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt @@ -32,7 +32,7 @@ class PendingOperationsManager(private val walletBackendApi: WalletBackendApi) { val pendingOperations = MutableLiveData>() internal fun getPending() { - walletBackendApi.sendRequest("getPendingOperations", null) { isError, result -> + walletBackendApi.sendRequest("getPendingOperations") { isError, result -> if (isError) { Log.i(TAG, "got getPending error result: $result") return@sendRequest @@ -51,7 +51,7 @@ class PendingOperationsManager(private val walletBackendApi: WalletBackendApi) { } fun retryPendingNow() { - walletBackendApi.sendRequest("retryPendingNow", null) + walletBackendApi.sendRequest("retryPendingNow") } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt index d8204b6..bd37b37 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -102,12 +102,4 @@ class TransactionManager( } } - @UiThread - fun hasPending(currency: String): Boolean { - val result = mTransactions[currency]?.value ?: return false - return if (result is TransactionsResult.Success) { - result.transactions.any { it.pending } - } else false - } - } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt index 526aa94..2ae58c3 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt @@ -112,7 +112,7 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode. override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) model.balances.observe(viewLifecycleOwner, Observer { balances -> - balances[currency]?.available?.let { amount -> + balances.find { it.currency == currency }?.available?.let { amount -> requireActivity().title = getString(R.string.transactions_detail_title_balance, amount) } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt deleted file mode 100644 index 9c815c9..0000000 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.withdraw - -import net.taler.common.Amount -import net.taler.common.Timestamp -import org.json.JSONObject - -data class CoinFee( - val coin: Amount, - val quantity: Int, - val feeDeposit: Amount, - val feeRefresh: Amount, - val feeRefund: Amount, - val feeWithdraw: Amount -) - -data class WireFee( - val start: Timestamp, - val end: Timestamp, - val wireFee: Amount, - val closingFee: Amount -) - -data class ExchangeFees( - val withdrawFee: Amount, - val overhead: Amount, - val earliestDepositExpiration: Timestamp, - val coinFees: List, - val wireFees: List -) { - companion object { - fun fromExchangeWithdrawDetailsJson(json: JSONObject): ExchangeFees { - val earliestDepositExpiration = - json.getJSONObject("earliestDepositExpiration").getLong("t_ms") - val selectedDenoms = json.getJSONObject("selectedDenoms") - val denoms = selectedDenoms.getJSONArray("selectedDenoms") - val coinFees = ArrayList(denoms.length()) - for (i in 0 until denoms.length()) { - val denom = denoms.getJSONObject(i) - val d = denom.getJSONObject("denom") - val coinFee = CoinFee( - coin = Amount.fromJsonObject(d.getJSONObject("value")), - quantity = denom.getInt("count"), - feeDeposit = Amount.fromJsonObject(d.getJSONObject("feeDeposit")), - feeRefresh = Amount.fromJsonObject(d.getJSONObject("feeRefresh")), - feeRefund = Amount.fromJsonObject(d.getJSONObject("feeRefund")), - feeWithdraw = Amount.fromJsonObject(d.getJSONObject("feeWithdraw")) - ) - coinFees.add(coinFee) - } - - val wireFeesJson = json.getJSONObject("wireFees") - val feesForType = wireFeesJson.getJSONObject("feesForType") - val bankFees = feesForType.getJSONArray("x-taler-bank") - val wireFees = ArrayList(bankFees.length()) - for (i in 0 until bankFees.length()) { - val fee = bankFees.getJSONObject(i) - val startStamp = - fee.getJSONObject("startStamp").getLong("t_ms") - val endStamp = - fee.getJSONObject("endStamp").getLong("t_ms") - val wireFee = WireFee( - start = Timestamp(startStamp), - end = Timestamp(endStamp), - wireFee = Amount.fromJsonObject(fee.getJSONObject("wireFee")), - closingFee = Amount.fromJsonObject(fee.getJSONObject("closingFee")) - ) - wireFees.add(wireFee) - } - - return ExchangeFees( - withdrawFee = Amount.fromJsonObject(json.getJSONObject("withdrawFee")), - overhead = Amount.fromJsonObject(json.getJSONObject("overhead")), - earliestDepositExpiration = Timestamp(earliestDepositExpiration), - coinFees = coinFees, - wireFees = wireFees - ) - } - } -} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt index 55f931d..9788d1c 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt @@ -60,7 +60,7 @@ class ManualWithdrawFragment : Fragment() { val amount = Amount(exchangeItem.currency, value, 0) amountView.hideKeyboard() Toast.makeText(view.context, "Not implemented: $amount", LENGTH_SHORT).show() - withdrawManager.getWithdrawalDetails(exchangeItem, amount) + withdrawManager.getWithdrawalDetails(exchangeItem.exchangeBaseUrl, amount) } } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt index 331554b..5a98a89 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -20,6 +20,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer @@ -34,7 +36,7 @@ import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.cleanExchange import net.taler.wallet.withdraw.WithdrawStatus.Loading -import net.taler.wallet.withdraw.WithdrawStatus.TermsOfServiceReviewRequired +import net.taler.wallet.withdraw.WithdrawStatus.TosReviewRequired import net.taler.wallet.withdraw.WithdrawStatus.Withdrawing class PromptWithdrawFragment : Fragment() { @@ -59,17 +61,13 @@ class PromptWithdrawFragment : Fragment() { private fun showWithdrawStatus(status: WithdrawStatus?): Any = when (status) { is WithdrawStatus.ReceivedDetails -> { - showContent(status.amount, status.fee, status.exchange) + showContent(status.amountRaw, status.amountEffective, status.exchangeBaseUrl) confirmWithdrawButton.apply { text = getString(R.string.withdraw_button_confirm) setOnClickListener { it.fadeOut() confirmProgressBar.fadeIn() - withdrawManager.acceptWithdrawal( - status.talerWithdrawUri, - status.exchange, - status.amount.currency - ) + withdrawManager.acceptWithdrawal() } isEnabled = true } @@ -87,8 +85,8 @@ class PromptWithdrawFragment : Fragment() { is Withdrawing -> { model.showProgressBar.value = true } - is TermsOfServiceReviewRequired -> { - showContent(status.amount, status.fee, status.exchange) + is TosReviewRequired -> { + showContent(status.amountRaw, status.amountEffective, status.exchangeBaseUrl) confirmWithdrawButton.apply { text = getString(R.string.withdraw_button_tos) setOnClickListener { @@ -104,20 +102,20 @@ class PromptWithdrawFragment : Fragment() { null -> model.showProgressBar.value = false } - private fun showContent(amount: Amount, fee: Amount, exchange: String) { + private fun showContent(amountRaw: Amount, amountEffective: Amount, exchange: String) { model.showProgressBar.value = false progressBar.fadeOut() introView.fadeIn() - effectiveAmountView.text = (amount - fee).toString() + effectiveAmountView.text = amountEffective.toString() effectiveAmountView.fadeIn() chosenAmountLabel.fadeIn() - chosenAmountView.text = amount.toString() + chosenAmountView.text = amountRaw.toString() chosenAmountView.fadeIn() feeLabel.fadeIn() - feeView.text = getString(R.string.amount_negative, fee.toString()) + feeView.text = getString(R.string.amount_negative, (amountRaw - amountEffective).toString()) feeView.fadeIn() exchangeIntroView.fadeIn() @@ -125,7 +123,7 @@ class PromptWithdrawFragment : Fragment() { withdrawExchangeUrl.fadeIn() selectExchangeButton.fadeIn() selectExchangeButton.setOnClickListener { - findNavController().navigate(R.id.action_promptWithdraw_to_selectExchangeFragment) + Toast.makeText(context, "Not yet implemented", LENGTH_SHORT).show() } withdrawCard.fadeIn() diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt index ffaef5a..db1f326 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt @@ -55,7 +55,7 @@ class ReviewExchangeTosFragment : Fragment() { } withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer { when (it) { - is WithdrawStatus.TermsOfServiceReviewRequired -> { + is WithdrawStatus.TosReviewRequired -> { val sections = try { // TODO remove next line once exchange delivers proper markdown val text = it.tosText.replace("****************", "================") diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt deleted file mode 100644 index 2ade9f2..0000000 --- a/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.withdraw - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.ViewGroup -import android.widget.TextView -import androidx.core.content.ContextCompat.getColor -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.RecyclerView.Adapter -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import kotlinx.android.synthetic.main.fragment_select_exchange.* -import net.taler.common.Amount -import net.taler.common.toRelativeTime -import net.taler.common.toShortDate -import net.taler.wallet.MainViewModel -import net.taler.wallet.R -import net.taler.wallet.withdraw.CoinFeeAdapter.CoinFeeViewHolder -import net.taler.wallet.withdraw.WireFeeAdapter.WireFeeViewHolder - -class SelectExchangeFragment : Fragment() { - - private val model: MainViewModel by activityViewModels() - private val withdrawManager by lazy { model.withdrawManager } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_select_exchange, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val fees = withdrawManager.exchangeFees ?: throw IllegalStateException() - if (fees.withdrawFee.isZero()) { - withdrawFeeLabel.visibility = GONE - withdrawFeeView.visibility = GONE - } else withdrawFeeView.setAmount(fees.withdrawFee) - if (fees.overhead.isZero()) { - overheadLabel.visibility = GONE - overheadView.visibility = GONE - } else overheadView.setAmount(fees.overhead) - expirationView.text = fees.earliestDepositExpiration.ms.toRelativeTime(requireContext()) - coinFeesList.adapter = CoinFeeAdapter(fees.coinFees) - wireFeesList.adapter = WireFeeAdapter(fees.wireFees) - } - - private fun TextView.setAmount(amount: Amount) { - if (amount.isZero()) text = amount.toString() - else { - text = getString(R.string.amount_negative, amount) - setTextColor(getColor(context, R.color.red)) - } - } - -} - -private class CoinFeeAdapter(private val items: List) : Adapter() { - override fun getItemCount() = items.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinFeeViewHolder { - val v = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_coin_fee, parent, false) - return CoinFeeViewHolder(v) - } - - override fun onBindViewHolder(holder: CoinFeeViewHolder, position: Int) { - holder.bind(items[position]) - } - - private class CoinFeeViewHolder(private val v: View) : ViewHolder(v) { - private val res = v.context.resources - private val coinView: TextView = v.findViewById(R.id.coinView) - private val withdrawFeeView: TextView = v.findViewById(R.id.withdrawFeeView) - private val depositFeeView: TextView = v.findViewById(R.id.depositFeeView) - private val refreshFeeView: TextView = v.findViewById(R.id.refreshFeeView) - private val refundFeeView: TextView = v.findViewById(R.id.refundFeeView) - fun bind(item: CoinFee) { - coinView.text = res.getQuantityString( - R.plurals.exchange_fee_coin, - item.quantity, - item.coin, - item.quantity - ) - withdrawFeeView.text = - v.context.getString(R.string.exchange_fee_withdraw_fee, item.feeWithdraw) - depositFeeView.text = - v.context.getString(R.string.exchange_fee_deposit_fee, item.feeDeposit) - refreshFeeView.text = - v.context.getString(R.string.exchange_fee_refresh_fee, item.feeRefresh) - refundFeeView.text = - v.context.getString(R.string.exchange_fee_refund_fee, item.feeRefresh) - } - } -} - -private class WireFeeAdapter(private val items: List) : Adapter() { - override fun getItemCount() = items.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WireFeeViewHolder { - val v = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_wire_fee, parent, false) - return WireFeeViewHolder(v) - } - - override fun onBindViewHolder(holder: WireFeeViewHolder, position: Int) { - holder.bind(items[position]) - } - - private class WireFeeViewHolder(private val v: View) : ViewHolder(v) { - private val validityView: TextView = v.findViewById(R.id.validityView) - private val wireFeeView: TextView = v.findViewById(R.id.wireFeeView) - private val closingFeeView: TextView = v.findViewById(R.id.closingFeeView) - fun bind(item: WireFee) { - validityView.text = v.context.getString( - R.string.exchange_fee_wire_fee_timespan, - item.start.ms.toShortDate(v.context), - item.end.ms.toShortDate(v.context) - ) - wireFeeView.text = - v.context.getString(R.string.exchange_fee_wire_fee_wire_fee, item.wireFee) - closingFeeView.text = - v.context.getString(R.string.exchange_fee_wire_fee_closing_fee, item.closingFee) - } - } -} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt index ea65e7c..e14a747 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -17,38 +17,57 @@ package net.taler.wallet.withdraw import android.util.Log +import androidx.annotation.UiThread 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.wallet.TAG import net.taler.wallet.backend.WalletBackendApi +import net.taler.wallet.exchanges.ExchangeFees import net.taler.wallet.exchanges.ExchangeItem import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails import org.json.JSONObject sealed class WithdrawStatus { - data class Loading(val talerWithdrawUri: String) : WithdrawStatus() - data class TermsOfServiceReviewRequired( - val talerWithdrawUri: String, - val exchange: String, + data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus() + data class TosReviewRequired( + val talerWithdrawUri: String? = null, + val exchangeBaseUrl: String, + val amountRaw: Amount, + val amountEffective: Amount, val tosText: String, - val tosEtag: String, - val amount: Amount, - val fee: Amount + val tosEtag: String ) : WithdrawStatus() data class ReceivedDetails( - val talerWithdrawUri: String, - val exchange: String, - val amount: Amount, - val fee: Amount + val talerWithdrawUri: String? = null, + val exchangeBaseUrl: String, + val amountRaw: Amount, + val amountEffective: Amount ) : WithdrawStatus() - data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus() + object Withdrawing : WithdrawStatus() data class Success(val currency: String) : WithdrawStatus() data class Error(val message: String?) : WithdrawStatus() } -class WithdrawManager(private val walletBackendApi: WalletBackendApi) { +data class WithdrawalDetailsForUri( + val amount: Amount, + val defaultExchangeBaseUrl: String?, + val possibleExchanges: List +) + +data class WithdrawalDetails( + val tosAccepted: Boolean, + val amountRaw: Amount, + val amountEffective: Amount +) + +class WithdrawManager( + private val walletBackendApi: WalletBackendApi, + private val mapper: ObjectMapper +) { val withdrawStatus = MutableLiveData() val testWithdrawalInProgress = MutableLiveData(false) @@ -58,149 +77,127 @@ class WithdrawManager(private val walletBackendApi: WalletBackendApi) { fun withdrawTestkudos() { testWithdrawalInProgress.value = true - - walletBackendApi.sendRequest("withdrawTestkudos", null) { _, _ -> + walletBackendApi.sendRequest("withdrawTestkudos") { _, _ -> testWithdrawalInProgress.postValue(false) } } - fun getWithdrawalDetails(exchangeItem: ExchangeItem, amount: Amount) { + fun getWithdrawalDetails(uri: String) { + withdrawStatus.value = WithdrawStatus.Loading(uri) val args = JSONObject().apply { - put("exchangeBaseUrl", exchangeItem.exchangeBaseUrl) - put("amount", amount.toJSONString()) + put("talerWithdrawUri", uri) } - walletBackendApi.sendRequest("getWithdrawalDetailsForAmount", args) { isError, result -> - // {"rawAmount":"TESTKUDOS:5","effectiveAmount":"TESTKUDOS:4.8","paytoUris":["payto:\/\/x-taler-bank\/bank.test.taler.net\/Exchange"],"tosAccepted":false} + walletBackendApi.sendRequest("getWithdrawalDetailsForUri", args) { isError, result -> if (isError) { - Log.e(TAG, "$result") + handleError("getWithdrawalDetailsForUri", result) + return@sendRequest + } + val details: WithdrawalDetailsForUri = mapper.readValue(result.toString()) + if (details.defaultExchangeBaseUrl == null) { + // TODO go to exchange selection screen instead + val chosenExchange = details.possibleExchanges[0].exchangeBaseUrl + getWithdrawalDetails(chosenExchange, details.amount, uri) } else { - Log.e(TAG, "$result") + getWithdrawalDetails(details.defaultExchangeBaseUrl, details.amount, uri) } } } - fun getWithdrawalInfo(talerWithdrawUri: String) { + fun getWithdrawalDetails(exchangeBaseUrl: String, amount: Amount, uri: String? = null) { + withdrawStatus.value = WithdrawStatus.Loading(uri) val args = JSONObject().apply { - put("talerWithdrawUri", talerWithdrawUri) + put("exchangeBaseUrl", exchangeBaseUrl) + put("amount", amount.toJSONString()) } - withdrawStatus.value = WithdrawStatus.Loading(talerWithdrawUri) - - walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result -> + walletBackendApi.sendRequest("getWithdrawalDetailsForAmount", args) { isError, result -> if (isError) { - Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}") - val message = if (result.has("message")) result.getString("message") else null - withdrawStatus.postValue(WithdrawStatus.Error(message)) + handleError("getWithdrawalDetailsForAmount", result) 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) + val details: WithdrawalDetails = mapper.readValue(result.toString()) + if (details.tosAccepted) + withdrawStatus.value = ReceivedDetails( + talerWithdrawUri = uri, + exchangeBaseUrl = exchangeBaseUrl, + amountRaw = details.amountRaw, + amountEffective = details.amountEffective + ) + else getExchangeTos(exchangeBaseUrl, details, uri) } } - private fun getWithdrawalInfoWithExchange(talerWithdrawUri: String, selectedExchange: String) { + private fun getExchangeTos(exchangeBaseUrl: String, details: WithdrawalDetails, uri: String?) { val args = JSONObject().apply { - put("talerWithdrawUri", talerWithdrawUri) - put("selectedExchange", selectedExchange) + put("exchangeBaseUrl", exchangeBaseUrl) } - - walletBackendApi.sendRequest("getWithdrawDetailsForUri", args) { isError, result -> + walletBackendApi.sendRequest("getExchangeTos", args) { isError, result -> if (isError) { - Log.e(TAG, "Error getWithdrawDetailsForUri ${result.toString(4)}") - val message = if (result.has("message")) result.getString("message") else null - withdrawStatus.postValue(WithdrawStatus.Error(message)) - return@sendRequest - } - Log.v(TAG, "got getWithdrawDetailsForUri result (with exchange details)") - val status = withdrawStatus.value - if (status !is WithdrawStatus.Loading) { - Log.w(TAG, "ignoring withdrawal info result, not loading.") + handleError("getExchangeTos", result) return@sendRequest } - val wi = result.getJSONObject("bankWithdrawDetails") - val amount = Amount.fromJsonObject(wi.getJSONObject("amount")) - - val ei = result.getJSONObject("exchangeWithdrawDetails") - val termsOfServiceAccepted = ei.getBoolean("termsOfServiceAccepted") - - exchangeFees = ExchangeFees.fromExchangeWithdrawDetailsJson(ei) - - val withdrawFee = Amount.fromJsonObject(ei.getJSONObject("withdrawFee")) - val overhead = Amount.fromJsonObject(ei.getJSONObject("overhead")) - val fee = withdrawFee + overhead - - 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, - amount, fee - ) - ) - } else { - withdrawStatus.postValue( - ReceivedDetails( - status.talerWithdrawUri, - selectedExchange, amount, - fee - ) - ) - } + withdrawStatus.value = WithdrawStatus.TosReviewRequired( + talerWithdrawUri = uri, + exchangeBaseUrl = exchangeBaseUrl, + amountRaw = details.amountRaw, + amountEffective = details.amountEffective, + tosText = result.getString("tos"), + tosEtag = result.getString("currentEtag") + ) } } - fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String, currency: String) { - val args = JSONObject() - args.put("talerWithdrawUri", talerWithdrawUri) - args.put("selectedExchange", selectedExchange) - - withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri) - - walletBackendApi.sendRequest("acceptWithdrawal", args) { isError, result -> + /** + * Accept the currently displayed terms of service. + */ + fun acceptCurrentTermsOfService() { + val s = withdrawStatus.value as WithdrawStatus.TosReviewRequired + val args = JSONObject().apply { + put("exchangeBaseUrl", s.exchangeBaseUrl) + put("etag", s.tosEtag) + } + walletBackendApi.sendRequest("setExchangeTosAccepted", args) { isError, result -> if (isError) { - Log.v(TAG, "got acceptWithdrawal error result: ${result.toString(2)}") - return@sendRequest - } - Log.v(TAG, "got acceptWithdrawal result") - val status = withdrawStatus.value - if (status !is WithdrawStatus.Withdrawing) { - Log.w(TAG, "ignoring acceptWithdrawal result, invalid state: $status") + handleError("setExchangeTosAccepted", result) return@sendRequest } - withdrawStatus.postValue(WithdrawStatus.Success(currency)) + withdrawStatus.value = ReceivedDetails( + talerWithdrawUri = s.talerWithdrawUri, + exchangeBaseUrl = s.exchangeBaseUrl, + amountRaw = s.amountRaw, + amountEffective = s.amountEffective + ) } } - /** - * Accept the currently displayed terms of service. - */ - fun acceptCurrentTermsOfService() { - val s = withdrawStatus.value - check(s is WithdrawStatus.TermsOfServiceReviewRequired) + @UiThread + fun acceptWithdrawal() { + val status = withdrawStatus.value as ReceivedDetails + val operation = if (status.talerWithdrawUri == null) + "acceptManualWithdrawal" else "acceptBankIntegratedWithdrawal" val args = JSONObject().apply { - put("exchangeBaseUrl", s.exchange) - put("etag", s.tosEtag) + put("exchangeBaseUrl", status.exchangeBaseUrl) + if (status.talerWithdrawUri == null) { + put("amount", status.amountRaw) + } else { + put("talerWithdrawUri", status.talerWithdrawUri) + } } - walletBackendApi.sendRequest("acceptExchangeTermsOfService", args) { isError, result -> + withdrawStatus.value = WithdrawStatus.Withdrawing + walletBackendApi.sendRequest(operation, args) { isError, result -> if (isError) { - Log.e(TAG, "Error acceptExchangeTermsOfService ${result.toString(4)}") + handleError(operation, result) return@sendRequest } - val status = ReceivedDetails(s.talerWithdrawUri, s.exchange, s.amount, s.fee) - withdrawStatus.postValue(status) + withdrawStatus.value = WithdrawStatus.Success(status.amountRaw.currency) } } + @UiThread + private fun handleError(operation: String, result: JSONObject) { + Log.e(TAG, "Error $operation ${result.toString(2)}") + val message = if (result.has("message")) result.getString("message") else null + withdrawStatus.value = WithdrawStatus.Error(message) + } + } diff --git a/wallet/src/main/res/drawable/ic_directions.xml b/wallet/src/main/res/drawable/ic_directions.xml deleted file mode 100644 index 229d69c..0000000 --- a/wallet/src/main/res/drawable/ic_directions.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/wallet/src/main/res/drawable/ic_edit.xml b/wallet/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..69d2830 --- /dev/null +++ b/wallet/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/wallet/src/main/res/drawable/ic_history.xml b/wallet/src/main/res/drawable/ic_history.xml deleted file mode 100644 index d9f75ea..0000000 --- a/wallet/src/main/res/drawable/ic_history.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/wallet/src/main/res/drawable/transaction_payment_aborted.xml b/wallet/src/main/res/drawable/transaction_payment_aborted.xml deleted file mode 100644 index 8d47c26..0000000 --- a/wallet/src/main/res/drawable/transaction_payment_aborted.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/wallet/src/main/res/drawable/transaction_tip_declined.xml b/wallet/src/main/res/drawable/transaction_tip_declined.xml deleted file mode 100644 index 4bd1633..0000000 --- a/wallet/src/main/res/drawable/transaction_tip_declined.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/wallet/src/main/res/layout/fragment_json.xml b/wallet/src/main/res/layout/fragment_json.xml deleted file mode 100644 index d9bca8f..0000000 --- a/wallet/src/main/res/layout/fragment_json.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - diff --git a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml index c9c9402..6bca4ef 100644 --- a/wallet/src/main/res/layout/fragment_prompt_withdraw.xml +++ b/wallet/src/main/res/layout/fragment_prompt_withdraw.xml @@ -162,12 +162,12 @@ - - - - - - - - - - - - diff --git a/wallet/src/main/res/menu/activity_main_drawer.xml b/wallet/src/main/res/menu/activity_main_drawer.xml index 62abc32..d1cc462 100644 --- a/wallet/src/main/res/menu/activity_main_drawer.xml +++ b/wallet/src/main/res/menu/activity_main_drawer.xml @@ -43,10 +43,6 @@ android:id="@+id/nav_pending_operations" android:icon="@drawable/ic_sync" android:title="@string/pending_operations_title" /> - diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index 93db557..285fac9 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -133,7 +133,7 @@ @@ -143,12 +143,6 @@ android:label="@string/pending_operations_title" tools:layout="@layout/fragment_pending_operations" /> - - - - diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 421d4ab..1e629a6 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -40,7 +40,6 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card Withdraw Digital Cash Exchange\'s Terms of Service Exchange Fees - Event History Error Go Back diff --git a/wallet/src/main/res/xml/settings_backup.xml b/wallet/src/main/res/xml/settings_backup.xml index 52b72ac..f8c5839 100644 --- a/wallet/src/main/res/xml/settings_backup.xml +++ b/wallet/src/main/res/xml/settings_backup.xml @@ -14,7 +14,7 @@ ~ GNU Taler; see the file COPYING. If not, see --> - - */ - -package net.taler.wallet.crypto - -import org.junit.Test - -class Base32CrockfordTest { - @Test - fun testBasic() { - val inputStr = "Hello, World" - val data = inputStr.toByteArray(Charsets.UTF_8) - val enc = Base32Crockford.encode(data) - println(enc) - val dec = Base32Crockford.decode(enc) - val recoveredInputStr = dec.toString(Charsets.UTF_8) - println(recoveredInputStr) - - val foo = - Base32Crockford.decode("51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30H2365338E9G6RT4AH1N6H13EGHR70RK6H1S6X2M4CSP8CSK8E1G88VKJH25610KGCHR8RWM4DJ47123CH9K89334D1S8N24ACJ48CR3EH256MR3AH1R711KCE9N6S134GSN6RW46D1H6CV3CDHJ6D0KEDHR6D24CD248MWKADHJ6WT34D25712KCD2474V46EA18H2M4GHM6WTK2E216S14CD238GSK0G9G692KCDHM6RW34CT16MV3CG9P60S34C1G70SMCHHQ8CVKJG9K6CVK6GHK70R46HJ26CR4AE9M8523ADHS8RR3EE1R74S32CHP6N1K0GT38D1M6C1R84TM2E9N8MSK2C1J71248E9H6H1MCD9J70VK4GSG6124CCHR6RS4ADSH8N0M4H1G84R4CD1G8D24AG9N6RR48DT1712K6GJ26X232DT36N0K4C9M8H236HJ48N2K4G9H8GVM8E1P8GSM6E9K891K4CSN65348C26611M8DHJ8S1M6H9G8H338CHS6GV3CD9K64S3GCHR8H2M6GJ58MT3EHA26S232GSJ6GTMAGA570W44DA2852KEDSR8MTKEGA460T3CCT18MR48CHK6WWKEGJ460WK4EA568VM6GSJ70T32CA461234DJ66RS34DHM6D242CT46MV3JDA584S4ADSM6S1MAE1P6GTKEGA68N1M8E216WRMAGHM6RR4ADSJ8MR3EDJ2690KAD9H6H346D9R88RKECSN8RRKJC1N74W34DSQ60W48DSJ8S1K0DSH8D1M4E1J6H1M2D1S8S33CG9R6RSMCH9K4CMGM81051JJ08SG64R30C1H4CMGM81054520A8A00") - println(foo.toString(Charsets.UTF_8)) - } -} -- cgit v1.2.3