diff options
Diffstat (limited to 'wallet/src/main/java')
25 files changed, 277 insertions, 912 deletions
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<Map<String, BalanceItem>>() - val balances: LiveData<Map<String, BalanceItem>> = mBalances.distinctUntilChanged() + private val mBalances = MutableLiveData<List<BalanceItem>>() + val balances: LiveData<List<BalanceItem>> = mBalances.distinctUntilChanged() val devMode = MutableLiveData(BuildConfig.DEBUG) val showProgressBar = MutableLiveData<Boolean>() @@ -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<String, BalanceItem>() - 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<Messenger>() 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<BalanceViewHolder>() { @@ -65,7 +72,7 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan fun bind(item: BalanceItem) { v.setOnClickListener { listener.onBalanceClick(item.available.currency) } - currencyView.text = item.available.currency + currencyView.text = item.currency amountView.text = item.available.amountStr val amountIncoming = item.pendingIncoming diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt index 22dd992..2b4d032 100644 --- a/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/balances/BalancesFragment.kt @@ -60,7 +60,7 @@ class BalancesFragment : Fragment(), } model.balances.observe(viewLifecycleOwner, Observer { - onBalancesChanged(it.values.toList()) + onBalancesChanged(it) }) } diff --git a/wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt b/wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt deleted file mode 100644 index 25a59be..0000000 --- a/wallet/src/main/java/net/taler/wallet/crypto/Encoding.kt +++ /dev/null @@ -1,134 +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 <http://www.gnu.org/licenses/> - */ - -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/withdraw/ExchangeFees.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt index 9c815c9..ae90b98 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ExchangeFees.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt @@ -14,7 +14,7 @@ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -package net.taler.wallet.withdraw +package net.taler.wallet.exchanges import net.taler.common.Amount import net.taler.common.Timestamp 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<List<ExchangeItem>> { 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/withdraw/SelectExchangeFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt index 2ade9f2..ef4894d 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/SelectExchangeFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/SelectExchangeFragment.kt @@ -14,7 +14,7 @@ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -package net.taler.wallet.withdraw +package net.taler.wallet.exchanges import android.os.Bundle import android.view.LayoutInflater @@ -33,8 +33,8 @@ 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 +import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder +import net.taler.wallet.exchanges.WireFeeAdapter.WireFeeViewHolder class SelectExchangeFragment : Fragment() { @@ -59,8 +59,10 @@ class SelectExchangeFragment : Fragment() { 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) + coinFeesList.adapter = + CoinFeeAdapter(fees.coinFees) + wireFeesList.adapter = + WireFeeAdapter(fees.wireFees) } private fun TextView.setAmount(amount: Amount) { 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 <http://www.gnu.org/licenses/> - */ - -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<HistoryViewHolder>() { - - private var history: List<HistoryEvent> = 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<HistoryEvent>) { - 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 <http://www.gnu.org/licenses/> - */ - -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 <http://www.gnu.org/licenses/> - */ - -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<HistoryEvent>) : HistoryResult() -} - -class DevHistoryManager( - private val walletBackendApi: WalletBackendApi, - private val scope: CoroutineScope, - private val mapper: ObjectMapper -) { - - private val mProgress = MutableLiveData<Boolean>() - val progress: LiveData<Boolean> = mProgress - - private val mHistory = MutableLiveData<HistoryResult>() - val history: LiveData<HistoryResult> = 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<HistoryEvent>() - 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 <http://www.gnu.org/licenses/> - */ - -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 <http://www.gnu.org/licenses/> - */ - -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<Boolean>() internal val detailsShown: LiveData<Boolean> = 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 } @@ -139,23 +130,19 @@ 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 <http://www.gnu.org/licenses/> + */ + +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<List<PendingOperationInfo>>() 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/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/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<ExchangeItem> +) + +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<WithdrawStatus>() 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) + } + } |