summaryrefslogtreecommitdiff
path: root/wallet/src
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src')
-rw-r--r--wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt272
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt225
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt71
-rw-r--r--wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt9
-rw-r--r--wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt12
-rw-r--r--wallet/src/main/java/net/taler/wallet/Utils.kt6
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt3
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt43
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt10
-rw-r--r--wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt17
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt34
-rw-r--r--wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt7
-rw-r--r--wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt163
-rw-r--r--wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt66
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt11
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt19
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt6
-rw-r--r--wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt15
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt6
-rw-r--r--wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt6
-rw-r--r--wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt3
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt163
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt81
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt80
-rw-r--r--wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt16
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt13
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt34
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt24
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt22
-rw-r--r--wallet/src/main/res/drawable/ic_funds_receive.xml5
-rw-r--r--wallet/src/main/res/drawable/ic_funds_send.xml5
-rw-r--r--wallet/src/main/res/drawable/transaction_loss.xml26
-rw-r--r--wallet/src/main/res/layout/balance_actions.xml117
-rw-r--r--wallet/src/main/res/layout/fragment_transactions.xml109
-rw-r--r--wallet/src/main/res/layout/fragment_uri_input.xml2
-rw-r--r--wallet/src/main/res/layout/list_item_balance.xml51
-rw-r--r--wallet/src/main/res/layout/list_item_transaction.xml10
-rw-r--r--wallet/src/main/res/menu/exchange.xml3
-rw-r--r--wallet/src/main/res/menu/global_dev.xml24
-rw-r--r--wallet/src/main/res/navigation/nav_graph.xml102
-rw-r--r--wallet/src/main/res/values-de/strings.xml4
-rw-r--r--wallet/src/main/res/values-it/strings.xml2
-rw-r--r--wallet/src/main/res/values/strings.xml81
-rw-r--r--wallet/src/main/res/values/styles.xml14
48 files changed, 1434 insertions, 572 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt b/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt
new file mode 100644
index 0000000..6b8db78
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt
@@ -0,0 +1,272 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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
+
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast.LENGTH_LONG
+import androidx.compose.ui.platform.ComposeView
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.taler.common.isOnline
+import net.taler.common.showError
+import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.refund.RefundStatus
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.Locale
+
+class HandleUriFragment: Fragment() {
+ private val model: MainViewModel by activityViewModels()
+
+ lateinit var uri: String
+ lateinit var from: String
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ uri = arguments?.getString("uri") ?: error("no uri passed")
+ from = arguments?.getString("from") ?: error("no from passed")
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ LoadingScreen()
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val uri = Uri.parse(uri)
+ if (uri.fragment != null && !requireContext().isOnline()) {
+ connectToWifi(requireContext(), uri.fragment!!)
+ }
+
+ // TODO: fix this bad async programming, make it only async when needed.
+ getTalerAction(uri, 3, MutableLiveData<String>()).observe(viewLifecycleOwner) { u ->
+ Log.v(TAG, "found action $u")
+
+ if (u.startsWith("payto://", ignoreCase = true)) {
+ Log.v(TAG, "navigating with paytoUri!")
+ val bundle = bundleOf("uri" to u)
+ findNavController().navigate(R.id.action_handleUri_to_nav_payto_uri, bundle)
+ return@observe
+ }
+
+ val normalizedURL = u.lowercase(Locale.ROOT)
+ var ext = false
+ val action = normalizedURL.substring(
+ if (normalizedURL.startsWith("taler://", ignoreCase = true)) {
+ "taler://".length
+ } else if (normalizedURL.startsWith("ext+taler://", ignoreCase = true)) {
+ ext = true
+ "ext+taler://".length
+ } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) &&
+ model.devMode.value == true
+ ) {
+ "taler+http://".length
+ } else {
+ normalizedURL.length
+ }
+ )
+
+ // Remove ext+ scheme prefix if present
+ val u2 = if (ext) {
+ "taler://" + u.substring("ext+taler://".length)
+ } else u
+
+ when {
+ action.startsWith("pay/", ignoreCase = true) -> {
+ Log.v(TAG, "navigating!")
+ findNavController().navigate(R.id.action_handleUri_to_promptPayment)
+ model.paymentManager.preparePay(u2)
+ }
+ action.startsWith("withdraw/", ignoreCase = true) -> {
+ Log.v(TAG, "navigating!")
+ // there's more than one entry point, so use global action
+ findNavController().navigate(R.id.action_handleUri_to_promptWithdraw)
+ model.withdrawManager.getWithdrawalDetails(u2)
+ }
+
+ action.startsWith("withdraw-exchange/", ignoreCase = true) -> {
+ prepareManualWithdrawal(u2)
+ }
+
+ action.startsWith("refund/", ignoreCase = true) -> {
+ model.showProgressBar.value = true
+ model.refundManager.refund(u2).observe(viewLifecycleOwner, Observer(::onRefundResponse))
+ }
+ action.startsWith("pay-pull/", ignoreCase = true) -> {
+ findNavController().navigate(R.id.action_handleUri_to_promptPullPayment)
+ model.peerManager.preparePeerPullDebit(u2)
+ }
+ action.startsWith("pay-push/", ignoreCase = true) -> {
+ findNavController().navigate(R.id.action_handleUri_to_promptPushPayment)
+ model.peerManager.preparePeerPushCredit(u2)
+ }
+ action.startsWith("pay-template/", ignoreCase = true) -> {
+ val bundle = bundleOf("uri" to u2)
+ findNavController().navigate(R.id.action_handleUri_to_promptPayTemplate, bundle)
+ }
+ action.startsWith("dev-experiment/", ignoreCase = true) -> {
+ model.applyDevExperiment(u2) { error ->
+ showError(error)
+ }
+ findNavController().navigate(R.id.nav_main)
+ }
+ else -> {
+ showError(R.string.error_unsupported_uri, "From: $from\nURI: $u2")
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+
+ private fun getTalerAction(
+ uri: Uri,
+ maxRedirects: Int,
+ actionFound: MutableLiveData<String>,
+ ): MutableLiveData<String> {
+ val scheme = uri.scheme ?: return actionFound
+
+ if (scheme == "http" || scheme == "https") {
+ model.viewModelScope.launch(Dispatchers.IO) {
+ val conn = URL(uri.toString()).openConnection() as HttpURLConnection
+ Log.v(TAG, "prepare query: $uri")
+ conn.setRequestProperty("Accept", "text/html")
+ conn.connectTimeout = 5000
+ conn.requestMethod = "HEAD"
+ try {
+ conn.connect()
+ } catch (e: IOException) {
+ Log.e(TAG, "Error connecting to $uri ", e)
+ showError(R.string.error_broken_uri, "$uri")
+ return@launch
+ }
+ val status = conn.responseCode
+
+ if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_PAYMENT_REQUIRED) {
+ val talerHeader = conn.headerFields["Taler"]
+ if (talerHeader != null && talerHeader[0] != null) {
+ Log.v(TAG, "taler header: ${talerHeader[0]}")
+ val talerHeaderUri = Uri.parse(talerHeader[0])
+ getTalerAction(talerHeaderUri, 0, actionFound)
+ }
+ } else if (status == HttpURLConnection.HTTP_MOVED_TEMP
+ || status == HttpURLConnection.HTTP_MOVED_PERM
+ || status == HttpURLConnection.HTTP_SEE_OTHER
+ ) {
+ val location = conn.headerFields["Location"]
+ if (location != null && location[0] != null) {
+ Log.v(TAG, "location redirect: ${location[0]}")
+ val locUri = Uri.parse(location[0])
+ getTalerAction(locUri, maxRedirects - 1, actionFound)
+ }
+ } else {
+ showError(R.string.error_broken_uri, "$uri")
+ findNavController().popBackStack()
+ }
+ }
+ } else {
+ actionFound.postValue(uri.toString())
+ }
+
+ return actionFound
+ }
+
+ private fun prepareManualWithdrawal(uri: String) {
+ model.showProgressBar.value = true
+ lifecycleScope.launch(Dispatchers.IO) {
+ val response = model.withdrawManager.prepareManualWithdrawal(uri)
+ if (response == null) withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ findNavController().navigate(R.id.errorFragment)
+ } else {
+ val exchange =
+ model.exchangeManager.findExchangeByUrl(response.exchangeBaseUrl)
+ if (exchange == null) withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ showError(R.string.exchange_add_error)
+ findNavController().navigateUp()
+ } else {
+ model.exchangeManager.withdrawalExchange = exchange
+ withContext(Dispatchers.Main) {
+ model.showProgressBar.value = false
+ val args = Bundle().apply {
+ if (response.amount != null) {
+ putString("amount", response.amount.toJSONString())
+ }
+ }
+
+ findNavController().navigate(R.id.action_handleUri_to_manualWithdrawal, args)
+ }
+ }
+ }
+ }
+ }
+
+ private fun onRefundResponse(status: RefundStatus) {
+ model.showProgressBar.value = false
+ when (status) {
+ is RefundStatus.Error -> {
+ if (model.devMode.value == true) {
+ showError(status.error)
+ } else {
+ showError(R.string.refund_error, status.error.userFacingMsg)
+ }
+
+ findNavController().navigateUp()
+ }
+ is RefundStatus.Success -> {
+ lifecycleScope.launch {
+ val transactionId = status.response.transactionId
+ val transaction = model.transactionManager.getTransactionById(transactionId)
+ if (transaction != null) {
+ // TODO: currency what? scopes are the cool thing now
+ // val currency = transaction.amountRaw.currency
+ // model.showTransactions(currency)
+ Snackbar.make(requireView(), getString(R.string.refund_success), LENGTH_LONG).show()
+ }
+
+ findNavController().navigateUp()
+ }
+ }
+
+ }
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index 5dfd920..00fd2d3 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -22,9 +22,9 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.content.IntentFilter
-import android.net.Uri
import android.os.Bundle
import android.util.Log
+import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE
import android.view.View.INVISIBLE
@@ -34,10 +34,6 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat.START
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Observer
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
@@ -46,19 +42,12 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
-import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
-import com.google.android.material.snackbar.Snackbar
import com.google.zxing.client.android.Intents.Scan.MIXED_SCAN
import com.google.zxing.client.android.Intents.Scan.SCAN_TYPE
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.journeyapps.barcodescanner.ScanOptions.QR_CODE
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import net.taler.common.EventObserver
-import net.taler.common.isOnline
-import net.taler.common.showError
import net.taler.wallet.BuildConfig.VERSION_CODE
import net.taler.wallet.BuildConfig.VERSION_NAME
import net.taler.wallet.HostCardEmulatorService.Companion.HTTP_TUNNEL_RESPONSE
@@ -66,11 +55,7 @@ import net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_CONNECTED
import net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_DISCONNECTED
import net.taler.wallet.HostCardEmulatorService.Companion.TRIGGER_PAYMENT_ACTION
import net.taler.wallet.databinding.ActivityMainBinding
-import net.taler.wallet.refund.RefundStatus
-import java.io.IOException
-import java.net.HttpURLConnection
-import java.net.URL
-import java.util.Locale.ROOT
+import net.taler.wallet.events.ObservabilityDialog
class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
OnPreferenceStartFragmentCallback {
@@ -144,6 +129,10 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
model.networkManager.networkStatus.observe(this) { online ->
ui.content.offlineBanner.visibility = if (online) GONE else VISIBLE
}
+
+ model.devMode.observe(this) {
+ invalidateMenu()
+ }
}
@Deprecated("Deprecated in Java")
@@ -159,6 +148,14 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
}
}
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ if (model.devMode.value == true) {
+ menuInflater.inflate(R.menu.global_dev, menu)
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_home -> nav.navigate(R.id.nav_main)
@@ -168,192 +165,26 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
return true
}
- override fun onDestroy() {
- unregisterReceiver(triggerPaymentReceiver)
- unregisterReceiver(nfcConnectedReceiver)
- unregisterReceiver(nfcDisconnectedReceiver)
- unregisterReceiver(tunnelResponseReceiver)
- super.onDestroy()
- }
-
- private fun getTalerAction(
- uri: Uri,
- maxRedirects: Int,
- actionFound: MutableLiveData<String>,
- ): MutableLiveData<String> {
- val scheme = uri.scheme ?: return actionFound
-
- if (scheme == "http" || scheme == "https") {
- model.viewModelScope.launch(Dispatchers.IO) {
- val conn = URL(uri.toString()).openConnection() as HttpURLConnection
- Log.v(TAG, "prepare query: $uri")
- conn.setRequestProperty("Accept", "text/html")
- conn.connectTimeout = 5000
- conn.requestMethod = "HEAD"
- try {
- conn.connect()
- } catch (e: IOException) {
- Log.e(TAG, "Error connecting to $uri ", e)
- showError(R.string.error_broken_uri, "$uri")
- return@launch
- }
- val status = conn.responseCode
-
- if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_PAYMENT_REQUIRED) {
- val talerHeader = conn.headerFields["Taler"]
- if (talerHeader != null && talerHeader[0] != null) {
- Log.v(TAG, "taler header: ${talerHeader[0]}")
- val talerHeaderUri = Uri.parse(talerHeader[0])
- getTalerAction(talerHeaderUri, 0, actionFound)
- }
- } else if (status == HttpURLConnection.HTTP_MOVED_TEMP
- || status == HttpURLConnection.HTTP_MOVED_PERM
- || status == HttpURLConnection.HTTP_SEE_OTHER
- ) {
- val location = conn.headerFields["Location"]
- if (location != null && location[0] != null) {
- Log.v(TAG, "location redirect: ${location[0]}")
- val locUri = Uri.parse(location[0])
- getTalerAction(locUri, maxRedirects - 1, actionFound)
- }
- } else {
- showError(R.string.error_broken_uri, "$uri")
- }
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_show_logs -> {
+ ObservabilityDialog().show(supportFragmentManager, "OBSERVABILITY")
}
- } else {
- actionFound.postValue(uri.toString())
}
-
- return actionFound
+ return super.onOptionsItemSelected(item)
}
- private fun handleTalerUri(url: String, from: String) {
- val uri = Uri.parse(url)
- if (uri.fragment != null && !isOnline()) {
- connectToWifi(this, uri.fragment!!)
- }
-
- getTalerAction(uri, 3, MutableLiveData<String>()).observe(this) { u ->
- Log.v(TAG, "found action $u")
-
- if (u.startsWith("payto://", ignoreCase = true)) {
- Log.v(TAG, "navigating with paytoUri!")
- val bundle = bundleOf("uri" to u)
- nav.navigate(R.id.action_nav_payto_uri, bundle)
- return@observe
- }
-
- val normalizedURL = u.lowercase(ROOT)
- var ext = false
- val action = normalizedURL.substring(
- if (normalizedURL.startsWith("taler://", ignoreCase = true)) {
- "taler://".length
- } else if (normalizedURL.startsWith("ext+taler://", ignoreCase = true)) {
- ext = true
- "ext+taler://".length
- } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) &&
- model.devMode.value == true
- ) {
- "taler+http://".length
- } else {
- normalizedURL.length
- }
- )
-
- // Remove ext+ scheme prefix if present
- val u2 = if (ext) {
- "taler://" + u.substring("ext+taler://".length)
- } else u
-
- when {
- action.startsWith("pay/", ignoreCase = true) -> {
- Log.v(TAG, "navigating!")
- nav.navigate(R.id.action_global_promptPayment)
- model.paymentManager.preparePay(u2)
- }
- action.startsWith("withdraw/", ignoreCase = true) -> {
- Log.v(TAG, "navigating!")
- // there's more than one entry point, so use global action
- nav.navigate(R.id.action_global_promptWithdraw)
- model.withdrawManager.getWithdrawalDetails(u2)
- }
-
- action.startsWith("withdraw-exchange/", ignoreCase = true) -> {
- model.showProgressBar.value = true
- lifecycleScope.launch(Dispatchers.IO) {
- val response = model.withdrawManager.prepareManualWithdrawal(u2)
- if (response == null) withContext(Dispatchers.Main) {
- model.showProgressBar.value = false
- nav.navigate(R.id.errorFragment)
- } else {
- val exchange =
- model.exchangeManager.findExchangeByUrl(response.exchangeBaseUrl)
- if (exchange == null) withContext(Dispatchers.Main) {
- model.showProgressBar.value = false
- showError(R.string.exchange_add_error)
- } else {
- model.exchangeManager.withdrawalExchange = exchange
- withContext(Dispatchers.Main) {
- model.showProgressBar.value = false
- val args = Bundle().apply {
- if (response.amount != null) {
- putString("amount", response.amount.toJSONString())
- }
- }
- // there's more than one entry point, so use global action
- nav.navigate(R.id.action_global_manual_withdrawal, args)
- }
- }
- }
- }
- }
-
- action.startsWith("refund/", ignoreCase = true) -> {
- model.showProgressBar.value = true
- model.refundManager.refund(u2).observe(this, Observer(::onRefundResponse))
- }
- action.startsWith("pay-pull/", ignoreCase = true) -> {
- nav.navigate(R.id.action_global_prompt_pull_payment)
- model.peerManager.preparePeerPullDebit(u2)
- }
- action.startsWith("pay-push/", ignoreCase = true) -> {
- nav.navigate(R.id.action_global_prompt_push_payment)
- model.peerManager.preparePeerPushCredit(u2)
- }
- action.startsWith("pay-template/", ignoreCase = true) -> {
- val bundle = bundleOf("uri" to u2)
- nav.navigate(R.id.action_global_prompt_pay_template, bundle)
- }
- else -> {
- showError(R.string.error_unsupported_uri, "From: $from\nURI: $u2")
- }
- }
- }
+ private fun handleTalerUri(uri: String, from: String) {
+ val args = bundleOf("uri" to uri, "from" to from)
+ nav.navigate(R.id.action_global_handle_uri, args)
}
- private fun onRefundResponse(status: RefundStatus) {
- model.showProgressBar.value = false
- when (status) {
- is RefundStatus.Error -> {
- if (model.devMode.value == true) {
- showError(status.error)
- } else {
- showError(R.string.refund_error, status.error.userFacingMsg)
- }
- }
- is RefundStatus.Success -> {
- lifecycleScope.launch {
- val transactionId = status.response.transactionId
- val transaction = model.transactionManager.getTransactionById(transactionId)
- if (transaction != null) {
- // TODO: currency what? scopes are the cool thing now
- // val currency = transaction.amountRaw.currency
- // model.showTransactions(currency)
- Snackbar.make(ui.navView, getString(R.string.refund_success), LENGTH_LONG).show()
- }
- }
- }
- }
+ override fun onDestroy() {
+ unregisterReceiver(triggerPaymentReceiver)
+ unregisterReceiver(nfcConnectedReceiver)
+ unregisterReceiver(nfcDisconnectedReceiver)
+ unregisterReceiver(tunnelResponseReceiver)
+ super.onDestroy()
}
private val triggerPaymentReceiver = object : BroadcastReceiver() {
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 5903446..82eb8d7 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -24,20 +24,29 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
import net.taler.common.Amount
import net.taler.common.AmountParserException
import net.taler.common.Event
import net.taler.common.toEvent
import net.taler.wallet.accounts.AccountManager
+import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.NotificationPayload
import net.taler.wallet.backend.NotificationReceiver
+import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.VersionReceiver
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.backend.WalletCoreVersion
+import net.taler.wallet.backend.WalletRunConfig
+import net.taler.wallet.backend.WalletRunConfig.Testing
import net.taler.wallet.balances.BalanceManager
import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.deposit.DepositManager
+import net.taler.wallet.events.ObservabilityEvent
import net.taler.wallet.exchanges.ExchangeManager
import net.taler.wallet.payment.PaymentManager
import net.taler.wallet.peer.PeerManager
@@ -48,16 +57,24 @@ import net.taler.wallet.withdraw.WithdrawManager
import org.json.JSONObject
const val TAG = "taler-wallet"
+const val OBSERVABILITY_LIMIT = 100
private val transactionNotifications = listOf(
"transaction-state-transition",
)
+private val observabilityNotifications = listOf(
+ "task-observability-event",
+ "request-observability-event",
+)
+
class MainViewModel(
app: Application,
) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
- val devMode = MutableLiveData(BuildConfig.DEBUG)
+ private val mDevMode = MutableLiveData(BuildConfig.DEBUG)
+ val devMode: LiveData<Boolean> = mDevMode
+
val showProgressBar = MutableLiveData<Boolean>()
var walletVersion: String? = null
private set
@@ -68,7 +85,15 @@ class MainViewModel(
var merchantVersion: String? = null
private set
- private val api = WalletBackendApi(app, this, this)
+ @set:Synchronized
+ private var walletConfig = WalletRunConfig(
+ testing = Testing(
+ emitObservabilityEvents = true,
+ devModeActive = devMode.value ?: false,
+ )
+ )
+
+ private val api = WalletBackendApi(app, walletConfig, this, this)
val networkManager = NetworkManager(app.applicationContext)
val withdrawManager = WithdrawManager(api, viewModelScope)
@@ -85,6 +110,9 @@ class MainViewModel(
private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
+ private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList())
+ val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog
+
private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
@@ -97,13 +125,24 @@ class MainViewModel(
override fun onNotificationReceived(payload: NotificationPayload) {
if (payload.type == "waiting-for-retry") return // ignore ping)
- Log.i(TAG, "Received notification from wallet-core: $payload")
+
+ val str = BackendManager.json.encodeToString(payload)
+ Log.i(TAG, "Received notification from wallet-core: $str")
// Only update balances when we're told they changed
if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) {
balanceManager.loadBalances()
}
+ if (payload.type in observabilityNotifications && payload.event != null) {
+ mObservabilityLog.getAndUpdate { logs ->
+ logs.takeLast(OBSERVABILITY_LIMIT)
+ .toMutableList().apply {
+ add(payload.event)
+ }
+ }
+ }
+
if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) {
// TODO notification API should give us a currency to update
// update currently selected transaction list
@@ -174,6 +213,24 @@ class MainViewModel(
mScanCodeEvent.value = true.toEvent()
}
+ fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) {
+ mDevMode.postValue(enabled)
+ viewModelScope.launch {
+ val config = walletConfig.copy(
+ testing = walletConfig.testing?.copy(
+ devModeActive = enabled,
+ ) ?: Testing(
+ devModeActive = enabled,
+ ),
+ )
+
+ api.setWalletConfig(config)
+ .onSuccess {
+ walletConfig = config
+ }.onError(onError)
+ }
+ }
+
fun runIntegrationTest() {
viewModelScope.launch {
api.request<Unit>("runIntegrationTestV2") {
@@ -187,6 +244,14 @@ class MainViewModel(
}
}
+ fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) {
+ viewModelScope.launch {
+ api.request<Unit>("applyDevExperiment") {
+ put("devExperimentUri", uri)
+ }.onError(onError)
+ }
+ }
+
}
sealed class AmountResult {
diff --git a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
index 85e2340..2accaaf 100644
--- a/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/ReceiveFundsFragment.kt
@@ -50,10 +50,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
-import kotlinx.serialization.encodeToString
import net.taler.common.Amount
import net.taler.common.CurrencySpecification
-import net.taler.wallet.backend.BackendManager
import net.taler.wallet.compose.AmountInputField
import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS
import net.taler.wallet.compose.TalerSurface
@@ -111,10 +109,7 @@ class ReceiveFundsFragment : Fragment() {
}
private fun onPeerPull(amount: Amount) {
- val bundle = bundleOf(
- "amount" to amount.toJSONString(),
- "scopeInfo" to BackendManager.json.encodeToString(scopeInfo),
- )
+ val bundle = bundleOf("amount" to amount.toJSONString())
peerManager.checkPeerPullCredit(amount)
findNavController().navigate(R.id.action_receiveFunds_to_nav_peer_pull, bundle)
}
@@ -176,7 +171,7 @@ private fun ReceiveFundsIntro(
.weight(1f),
onClick = {
val amount = getAmount(currency, text)
- if (amount == null) isError = true
+ if (amount == null || amount.isZero()) isError = true
else onManualWithdraw(amount)
}) {
Text(text = stringResource(R.string.receive_withdraw))
diff --git a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
index a26361b..2581979 100644
--- a/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/SendFundsFragment.kt
@@ -47,10 +47,8 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
-import kotlinx.serialization.encodeToString
import net.taler.common.Amount
import net.taler.common.CurrencySpecification
-import net.taler.wallet.backend.BackendManager
import net.taler.wallet.compose.AmountInputField
import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS
import net.taler.wallet.compose.TalerSurface
@@ -85,18 +83,12 @@ class SendFundsFragment : Fragment() {
}
private fun onDeposit(amount: Amount) {
- val bundle = bundleOf(
- "amount" to amount.toJSONString(),
- "scopeInfo" to BackendManager.json.encodeToString(scopeInfo),
- )
+ val bundle = bundleOf("amount" to amount.toJSONString())
findNavController().navigate(R.id.action_sendFunds_to_nav_deposit, bundle)
}
private fun onPeerPush(amount: Amount) {
- val bundle = bundleOf(
- "amount" to amount.toJSONString(),
- "scopeInfo" to BackendManager.json.encodeToString(scopeInfo),
- )
+ val bundle = bundleOf("amount" to amount.toJSONString())
peerManager.checkPeerPushDebit(amount)
findNavController().navigate(R.id.action_sendFunds_to_nav_peer_push, bundle)
}
diff --git a/wallet/src/main/java/net/taler/wallet/Utils.kt b/wallet/src/main/java/net/taler/wallet/Utils.kt
index 8b34531..5c4fedc 100644
--- a/wallet/src/main/java/net/taler/wallet/Utils.kt
+++ b/wallet/src/main/java/net/taler/wallet/Utils.kt
@@ -139,3 +139,9 @@ fun FragmentActivity.showError(error: TalerErrorInfo) {
val message = json.encodeToString(error)
showError(message)
}
+
+fun Context.getThemeColor(attr: Int): Int {
+ val typedValue = TypedValue()
+ theme.resolveAttribute(attr, typedValue, true)
+ return typedValue.data
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
index 46eb2f0..def4668 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
@@ -19,6 +19,7 @@ package net.taler.wallet.backend
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
+import net.taler.wallet.events.ObservabilityEvent
@Serializable
sealed class ApiMessage {
@@ -35,6 +36,7 @@ sealed class ApiMessage {
data class NotificationPayload(
val type: String,
val id: String? = null,
+ val event: ObservabilityEvent? = null,
)
@Serializable
diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
index b2f1f10..9292ef5 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
@@ -17,7 +17,6 @@
package net.taler.wallet.backend
import android.util.Log
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import net.taler.qtart.TalerWalletCore
import net.taler.wallet.BuildConfig
@@ -40,6 +39,7 @@ class BackendManager(
private const val TAG_CORE = "taler-wallet-embedded"
val json = Json {
ignoreUnknownKeys = true
+ coerceInputValues = true
}
@JvmStatic
private val initialized = AtomicBoolean(false)
@@ -52,6 +52,7 @@ class BackendManager(
// TODO using Dagger/Hilt and @Singleton would be nice as well
if (initialized.getAndSet(true)) error("Already initialized")
walletCore.setMessageHandler { onMessageReceived(it) }
+ walletCore.setCurlHttpClient()
if (BuildConfig.DEBUG) walletCore.setStdoutHandler {
Log.d(TAG_CORE, it)
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
index e9f7fcd..7fe1a6b 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
@@ -17,12 +17,55 @@
package net.taler.wallet.backend
import kotlinx.serialization.Serializable
+import net.taler.wallet.exchanges.BuiltinExchange
@Serializable
data class InitResponse(
val versionInfo: WalletCoreVersion,
)
+@Serializable
+data class WalletRunConfig(
+ val builtin: Builtin? = Builtin(),
+ val testing: Testing? = Testing(),
+ val features: Features? = Features(),
+) {
+ /**
+ * Initialization values useful for a complete startup.
+ *
+ * These are values may be overridden by different wallets
+ */
+ @Serializable
+ data class Builtin(
+ val exchanges: List<BuiltinExchange> = emptyList(),
+ )
+
+ /**
+ * Unsafe options which it should only be used to create
+ * testing environment.
+ */
+ @Serializable
+ data class Testing(
+ /**
+ * Allow withdrawal of denominations even though they are about to expire.
+ */
+ val denomselAllowLate: Boolean = false,
+ val devModeActive: Boolean = false,
+ val insecureTrustExchange: Boolean = false,
+ val preventThrottling: Boolean = false,
+ val skipDefaults: Boolean = false,
+ val emitObservabilityEvents: Boolean? = false,
+ )
+
+ /**
+ * Configurations values that may be safe to show to the user
+ */
+ @Serializable
+ data class Features(
+ val allowHttp: Boolean = false,
+ )
+}
+
fun interface VersionReceiver {
fun onVersionReceived(versionInfo: WalletCoreVersion)
}
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 4e179bb..fba9885 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
+import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import net.taler.wallet.backend.TalerErrorCode.NONE
@@ -34,6 +35,7 @@ private const val WALLET_DB = "talerwalletdb.sqlite3"
@OptIn(DelicateCoroutinesApi::class)
class WalletBackendApi(
private val app: Application,
+ private val initialConfig: WalletRunConfig,
private val versionReceiver: VersionReceiver,
notificationReceiver: NotificationReceiver,
) {
@@ -54,9 +56,11 @@ class WalletBackendApi(
} else {
"${app.filesDir}/${WALLET_DB}"
}
+
request("init", InitResponse.serializer()) {
put("persistentStoragePath", db)
put("logLevel", "INFO")
+ put("config", JSONObject(BackendManager.json.encodeToString(initialConfig)))
}.onSuccess { response ->
versionReceiver.onVersionReceived(response.versionInfo)
}.onError { error ->
@@ -65,6 +69,12 @@ class WalletBackendApi(
}
}
+ suspend fun setWalletConfig(config: WalletRunConfig): WalletResponse<InitResponse> {
+ return request("initWallet", InitResponse.serializer()) {
+ put("config", JSONObject(BackendManager.json.encodeToString(config)))
+ }
+ }
+
suspend fun sendRequest(operation: String, args: JSONObject? = null): ApiResponse {
return backendManager.send(operation, args)
}
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 f40def4..aabef4b 100644
--- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt
@@ -61,8 +61,7 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
private val amountView: TextView = v.findViewById(R.id.balanceAmountView)
private val scopeView: TextView = v.findViewById(R.id.scopeView)
private val balanceInboundAmount: TextView = v.findViewById(R.id.balanceInboundAmount)
- private val balanceInboundLabel: TextView = v.findViewById(R.id.balanceInboundLabel)
- private val pendingView: TextView = v.findViewById(R.id.pendingView)
+ private val balanceOutboundAmount: TextView = v.findViewById(R.id.balanceOutboundAmount)
fun bind(item: BalanceItem) {
v.setOnClickListener { listener.onBalanceClick(item.scopeInfo) }
@@ -71,11 +70,17 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
val amountIncoming = item.pendingIncoming
if (amountIncoming.isZero()) {
balanceInboundAmount.visibility = GONE
- balanceInboundLabel.visibility = GONE
} else {
balanceInboundAmount.visibility = VISIBLE
- balanceInboundLabel.visibility = VISIBLE
- balanceInboundAmount.text = v.context.getString(R.string.amount_positive, amountIncoming.toString(showSymbol = false))
+ balanceInboundAmount.text = v.context.getString(R.string.balances_inbound_amount, amountIncoming.toString(showSymbol = false))
+ }
+
+ val amountOutgoing = item.pendingOutgoing
+ if (amountOutgoing.isZero()) {
+ balanceOutboundAmount.visibility = GONE
+ } else {
+ balanceOutboundAmount.visibility = VISIBLE
+ balanceOutboundAmount.text = v.context.getString(R.string.balances_outbound_amount, amountOutgoing.toString(showSymbol = false))
}
val scopeInfo = item.scopeInfo
@@ -90,8 +95,6 @@ class BalanceAdapter(private val listener: BalanceClickListener) : Adapter<Balan
VISIBLE
}
}
-
- pendingView.visibility = if (item.hasPending) VISIBLE else GONE
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt b/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt
new file mode 100644
index 0000000..6412d63
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt
@@ -0,0 +1,34 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun LoadingScreen() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator()
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
index 846afb9..20acee1 100644
--- a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt
@@ -30,8 +30,6 @@ import net.taler.common.showError
import net.taler.wallet.CURRENCY_BTC
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
-import net.taler.wallet.backend.BackendManager
-import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.showError
@@ -40,6 +38,7 @@ class DepositFragment : Fragment() {
private val model: MainViewModel by activityViewModels()
private val depositManager get() = model.depositManager
private val balanceManager get() = model.balanceManager
+ private val transactionManager get() = model.transactionManager
override fun onCreateView(
inflater: LayoutInflater,
@@ -49,9 +48,7 @@ class DepositFragment : Fragment() {
val amount = arguments?.getString("amount")?.let {
Amount.fromJSONString(it)
} ?: error("no amount passed")
- val scopeInfo: ScopeInfo? = arguments?.getString("scopeInfo")?.let {
- BackendManager.json.decodeFromString(it)
- }
+ val scopeInfo = transactionManager.selectedScope
val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
val receiverName = arguments?.getString("receiverName")
val iban = arguments?.getString("IBAN")
diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
new file mode 100644
index 0000000..0ce5c01
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
@@ -0,0 +1,163 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.events
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.events.ObservabilityDialog.Companion.json
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+class ObservabilityDialog: DialogFragment() {
+ private val model: MainViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ val events by model.observabilityLog.collectAsState()
+ ObservabilityComposable(events.reversed()) {
+ dismiss()
+ }
+ }
+ }
+
+ companion object {
+ @OptIn(ExperimentalSerializationApi::class)
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ }
+}
+
+@Composable
+fun ObservabilityComposable(
+ events: List<ObservabilityEvent>,
+ onDismiss: () -> Unit,
+) {
+ var showJson by remember { mutableStateOf(false) }
+
+ AlertDialog(
+ title = { Text(stringResource(R.string.observability_title)) },
+ text = {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(events) { event ->
+ ObservabilityItem(event, showJson)
+ }
+ }
+ },
+ onDismissRequest = onDismiss,
+ dismissButton = {
+ Button(onClick = { showJson = !showJson }) {
+ Text(if (showJson) {
+ stringResource(R.string.observability_hide_json)
+ } else {
+ stringResource(R.string.observability_show_json)
+ })
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.close))
+ }
+ },
+ )
+}
+
+@Composable
+fun ObservabilityItem(
+ event: ObservabilityEvent,
+ showJson: Boolean,
+) {
+ val body = json.encodeToString(event.body)
+ val timestamp = DateTimeFormatter
+ .ofLocalizedDateTime(FormatStyle.MEDIUM)
+ .format(event.timestamp)
+
+ ListItem(
+ modifier = Modifier.fillMaxWidth(),
+ headlineContent = { Text(event.type) },
+ overlineContent = { Text(timestamp) },
+ supportingContent = if (!showJson) null else { ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ modifier = Modifier.background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ shape = MaterialTheme.shapes.small,
+ )
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(10.dp)
+ .fillMaxWidth(),
+ text = body,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+
+ CopyToClipboardButton(
+ label = "Event",
+ content = body,
+ colors = ButtonDefaults.textButtonColors(),
+ )
+ }
+ },
+ )
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
new file mode 100644
index 0000000..a50cde2
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
@@ -0,0 +1,66 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.events
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import java.time.LocalDateTime
+
+
+@Serializable(with = ObservabilityEventSerializer::class)
+class ObservabilityEvent(
+ val body: JsonObject,
+ val timestamp: LocalDateTime,
+ val type: String,
+)
+
+class ObservabilityEventSerializer: KSerializer<ObservabilityEvent> {
+ private val jsonElementSerializer = JsonElement.serializer()
+
+ override val descriptor: SerialDescriptor
+ get() = jsonElementSerializer.descriptor
+
+ override fun deserialize(decoder: Decoder): ObservabilityEvent {
+ require(decoder is JsonDecoder)
+ val jsonObject = decoder
+ .decodeJsonElement()
+ .jsonObject
+
+ val type = jsonObject["type"]
+ ?.jsonPrimitive
+ ?.content
+ ?: "unknown"
+
+ return ObservabilityEvent(
+ body = jsonObject,
+ timestamp = LocalDateTime.now(),
+ type = type,
+ )
+ }
+
+ override fun serialize(encoder: Encoder, value: ObservabilityEvent) {
+ encoder.encodeSerializableValue(JsonObject.serializer(), value.body)
+ }
+} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
index cb294ac..674632e 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeAdapter.kt
@@ -33,6 +33,7 @@ interface ExchangeClickListener {
fun onExchangeSelected(item: ExchangeItem)
fun onManualWithdraw(item: ExchangeItem)
fun onPeerReceive(item: ExchangeItem)
+ fun onExchangeReload(item: ExchangeItem)
fun onExchangeDelete(item: ExchangeItem)
}
@@ -99,6 +100,10 @@ internal class ExchangeAdapter(
listener.onPeerReceive(item)
true
}
+ R.id.action_reload -> {
+ listener.onExchangeReload(item)
+ true
+ }
R.id.action_delete -> {
listener.onExchangeDelete(item)
true
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
index 5482b5a..8a40bff 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt
@@ -110,6 +110,13 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
showError(error.userFacingMsg)
}
})
+ exchangeManager.reloadError.observe(viewLifecycleOwner, EventObserver { error ->
+ if (model.devMode.value == true) {
+ showError(error)
+ } else {
+ showError(error.userFacingMsg)
+ }
+ })
}
protected open fun onExchangeUpdate(exchanges: List<ExchangeItem>) {
@@ -145,6 +152,10 @@ open class ExchangeListFragment : Fragment(), ExchangeClickListener {
findNavController().navigate(R.id.action_global_receiveFunds)
}
+ override fun onExchangeReload(item: ExchangeItem) {
+ exchangeManager.reload(item.exchangeBaseUrl)
+ }
+
override fun onExchangeDelete(item: ExchangeItem) {
val optionsArray = arrayOf(getString(R.string.exchange_delete_force))
val checkedArray = BooleanArray(1) { false }
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 eb01cab..fa357b5 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
@@ -62,6 +62,9 @@ class ExchangeManager(
private val mDeleteError = MutableLiveData<Event<TalerErrorInfo>>()
val deleteError: LiveData<Event<TalerErrorInfo>> = mDeleteError
+ private val mReloadError = MutableLiveData<Event<TalerErrorInfo>>()
+ val reloadError: LiveData<Event<TalerErrorInfo>> = mReloadError
+
var withdrawalExchange: ExchangeItem? = null
private fun list(): LiveData<List<ExchangeItem>> {
@@ -95,6 +98,22 @@ class ExchangeManager(
}
}
+ fun reload(exchangeUrl: String, force: Boolean = true) = scope.launch {
+ mProgress.value = true
+ api.request<Unit>("updateExchangeEntry") {
+ put("exchangeBaseUrl", exchangeUrl)
+ put("force", force)
+ }.onError {
+ Log.e(TAG, "Error reloading exchange: $it")
+ mProgress.value = false
+ mReloadError.value = it.toEvent()
+ }.onSuccess {
+ mProgress.value = false
+ Log.d(TAG, "Exchange $exchangeUrl reloaded")
+ list()
+ }
+ }
+
fun delete(exchangeUrl: String, purge: Boolean = false) = scope.launch {
mProgress.value = true
api.request<Unit>("deleteExchange") {
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
index ce0bd82..0015e1c 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
@@ -21,6 +21,12 @@ import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.cleanExchange
@Serializable
+data class BuiltinExchange(
+ val exchangeBaseUrl: String,
+ val currencyHint: String? = null,
+)
+
+@Serializable
data class ExchangeItem(
val exchangeBaseUrl: String,
// can be null before exchange info in wallet-core was fully loaded
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
index b6c2fb1..ffa4875 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
@@ -18,7 +18,7 @@ package net.taler.wallet.payment
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -26,10 +26,12 @@ import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import net.taler.common.Amount
import net.taler.common.ContractTerms
import net.taler.wallet.AmountResult
import net.taler.wallet.R
+import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
sealed class AmountFieldStatus {
@@ -86,7 +88,7 @@ fun PayTemplateComposable(
@Composable
fun PayTemplateError(message: String) {
Box(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier.padding(16.dp).fillMaxSize(),
contentAlignment = Center,
) {
Text(
@@ -99,12 +101,7 @@ fun PayTemplateError(message: String) {
@Composable
fun PayTemplateLoading() {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Center,
- ) {
- CircularProgressIndicator()
- }
+ LoadingScreen()
}
@Preview
@@ -149,7 +146,7 @@ fun PayTemplateInsufficientBalancePreview() {
}
}
-@Preview
+@Preview(widthDp = 300)
@Composable
fun PayTemplateAlreadyPaidPreview() {
TalerSurface {
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
index 79ab542..8f2fb96 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
@@ -31,8 +31,6 @@ import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
-import net.taler.wallet.backend.BackendManager
-import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.exchanges.ExchangeItem
@@ -52,9 +50,7 @@ class OutgoingPullFragment : Fragment() {
val amount = arguments?.getString("amount")?.let {
Amount.fromJSONString(it)
} ?: error("no amount passed")
- val scopeInfo: ScopeInfo? = arguments?.getString("scopeInfo")?.let {
- BackendManager.json.decodeFromString(it)
- }
+ val scopeInfo = transactionManager.selectedScope
val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
return ComposeView(requireContext()).apply {
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
index fa3c79a..01fb566 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
@@ -33,8 +33,6 @@ import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
-import net.taler.wallet.backend.BackendManager
-import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.showError
@@ -60,9 +58,7 @@ class OutgoingPushFragment : Fragment() {
val amount = arguments?.getString("amount")?.let {
Amount.fromJSONString(it)
} ?: error("no amount passed")
- val scopeInfo: ScopeInfo? = arguments?.getString("scopeInfo")?.let {
- BackendManager.json.decodeFromString(it)
- }
+ val scopeInfo = transactionManager.selectedScope
val spec = scopeInfo?.let { balanceManager.getSpecForScopeInfo(it) }
requireActivity().onBackPressedDispatcher.addCallback(
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
index 0435665..38eeb9b 100644
--- a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt
@@ -33,6 +33,7 @@ import net.taler.wallet.BuildConfig.VERSION_CODE
import net.taler.wallet.BuildConfig.VERSION_NAME
import net.taler.wallet.MainViewModel
import net.taler.wallet.R
+import net.taler.wallet.showError
import net.taler.wallet.withdraw.WithdrawTestStatus
import java.lang.System.currentTimeMillis
@@ -108,7 +109,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
devPrefs.forEach { it.isVisible = enabled }
}
prefDevMode.setOnPreferenceChangeListener { _, newValue ->
- model.devMode.value = newValue as Boolean
+ model.setDevMode(newValue as Boolean) { error ->
+ showError(error)
+ }
true
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
index 22dcc3f..3b686a6 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt
@@ -36,6 +36,7 @@ import net.taler.common.CurrencySpecification
import net.taler.common.exhaustive
import net.taler.common.toRelativeTime
import net.taler.wallet.R
+import net.taler.wallet.getThemeColor
import net.taler.wallet.transactions.TransactionAdapter.TransactionViewHolder
import net.taler.wallet.transactions.TransactionMajorState.Aborted
import net.taler.wallet.transactions.TransactionMajorState.Failed
@@ -97,7 +98,7 @@ internal class TransactionAdapter(
private val amountColor = amount.currentTextColor
private val extraInfoColor = extraInfoView.currentTextColor
- private val red = getColor(context, R.color.red)
+ private val red = context.getThemeColor(R.attr.colorError)
private val green = getColor(context, R.color.green)
fun bind(transaction: Transaction, selected: Boolean) {
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt
new file mode 100644
index 0000000..9138345
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionLossFragment.kt
@@ -0,0 +1,163 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.taler.common.Amount
+import net.taler.common.CurrencySpecification
+import net.taler.common.Timestamp
+import net.taler.common.toAbsoluteTime
+import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorCode
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.LossEventType.DenomExpired
+import net.taler.wallet.transactions.LossEventType.DenomUnoffered
+import net.taler.wallet.transactions.LossEventType.DenomVanished
+import net.taler.wallet.transactions.TransactionAction.Abort
+import net.taler.wallet.transactions.TransactionAction.Retry
+import net.taler.wallet.transactions.TransactionAction.Suspend
+import net.taler.wallet.transactions.TransactionMajorState.Pending
+
+class TransactionLossFragment: TransactionDetailFragment() {
+ val scope get() = transactionManager.selectedScope
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ val t = transactionManager.selectedTransaction.observeAsState().value
+ val spec = scope?.let { balanceManager.getSpecForScopeInfo(it) }
+
+ TalerSurface {
+ if (t is TransactionDenomLoss) {
+ TransitionLossComposable(t, devMode, spec) {
+ onTransitionButtonClicked(t, it)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TransitionLossComposable(
+ t: TransactionDenomLoss,
+ devMode: Boolean,
+ spec: CurrencySpecification?,
+ onTransition: (t: TransactionAction) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ val context = LocalContext.current
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = t.timestamp.ms.toAbsoluteTime(context).toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.loss_amount),
+ amount = t.amountEffective.withSpec(spec),
+ amountType = AmountType.Negative,
+ )
+
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.loss_reason),
+ info = stringResource(
+ when(t.lossEventType) {
+ DenomExpired -> R.string.loss_reason_expired
+ DenomVanished -> R.string.loss_reason_vanished
+ DenomUnoffered -> R.string.loss_reason_unoffered
+ }
+ )
+ )
+
+ TransitionsComposable(t, devMode, onTransition)
+
+ if (devMode && t.error != null) {
+ ErrorTransactionButton(error = t.error)
+ }
+ }
+}
+
+fun previewLossTransaction(lossEventType: LossEventType) =
+ TransactionDenomLoss(
+ transactionId = "transactionId",
+ timestamp = Timestamp.fromMillis(System.currentTimeMillis() - 360 * 60 * 1000),
+ txState = TransactionState(Pending),
+ txActions = listOf(Retry, Suspend, Abort),
+ amountRaw = Amount.fromString("TESTKUDOS", "0.3"),
+ amountEffective = Amount.fromString("TESTKUDOS", "0.3"),
+ error = TalerErrorInfo(code = TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED),
+ lossEventType = lossEventType,
+ )
+
+@Composable
+@Preview
+fun TransitionLossComposableExpiredPreview() {
+ val t = previewLossTransaction(DenomExpired)
+ Surface {
+ TransitionLossComposable(t, true, null) {}
+ }
+}
+
+@Composable
+@Preview
+fun TransitionLossComposableVanishedPreview() {
+ val t = previewLossTransaction(DenomVanished)
+ Surface {
+ TransitionLossComposable(t, true, null) {}
+ }
+}
+
+@Composable
+@Preview
+fun TransactionLossComposableUnofferedPreview() {
+ val t = previewLossTransaction(DenomUnoffered)
+ Surface {
+ TransitionLossComposable(t, true, null) {}
+ }
+}
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 5399287..d0dec41 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -82,6 +82,8 @@ class TransactionManager(
mProgress.postValue(false)
}.onSuccess { result ->
val transactions = LinkedList(result.transactions)
+ val comparator = compareBy<Transaction> { it.txState.major == Pending }
+ transactions.sortWith(comparator)
transactions.reverse() // show latest first
mProgress.value = false
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
index 7091c90..f89be83 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionState.kt
@@ -79,102 +79,21 @@ enum class TransactionMajorState {
@Serializable
enum class TransactionMinorState {
- @SerialName("unknown")
- Unknown,
-
- @SerialName("deposit")
- Deposit,
-
@SerialName("kyc")
KycRequired,
- @SerialName("aml")
- AmlRequired,
-
- @SerialName("merge-kyc")
- MergeKycRequired,
-
- @SerialName("track")
- Track,
-
- @SerialName("submit-payment")
- SubmitPayment,
-
- @SerialName("rebind-session")
- RebindSession,
-
- @SerialName("refresh")
- Refresh,
-
- @SerialName("pickup")
- Pickup,
-
- @SerialName("auto-refund")
- AutoRefund,
-
- @SerialName("user")
- User,
-
- @SerialName("bank")
- Bank,
-
@SerialName("exchange")
Exchange,
- @SerialName("claim-proposal")
- ClaimProposal,
-
- @SerialName("check-refund")
- CheckRefund,
-
@SerialName("create-purse")
CreatePurse,
- @SerialName("delete-purse")
- DeletePurse,
-
- @SerialName("refresh-expired")
- RefreshExpired,
-
@SerialName("ready")
Ready,
- @SerialName("merge")
- Merge,
-
- @SerialName("repurchase")
- Repurchase,
-
- @SerialName("bank-register-reserve")
- BankRegisterReserve,
-
@SerialName("bank-confirm-transfer")
BankConfirmTransfer,
- @SerialName("withdraw-coins")
- WithdrawCoins,
-
@SerialName("exchange-wait-reserve")
ExchangeWaitReserve,
-
- @SerialName("aborting-bank")
- AbortingBank,
-
- @SerialName("refused")
- Refused,
-
- @SerialName("withdraw")
- Withdraw,
-
- @SerialName("merchant-order-proposed")
- MerchantOrderProposed,
-
- @SerialName("proposed")
- Proposed,
-
- @SerialName("refund-available")
- RefundAvailable,
-
- @SerialName("accept-refund")
- AcceptRefund
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
index be36a13..2bd204c 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
@@ -17,6 +17,7 @@
package net.taler.wallet.transactions
import android.content.Context
+import android.net.Uri
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
@@ -216,6 +217,16 @@ data class WithdrawalExchangeAccountDetails (
val paytoUri: String,
/**
+ * Status that indicates whether the account can be used
+ * by the user to send funds for a withdrawal.
+ *
+ * ok: account should be shown to the user
+ * error: account should not be shown to the user, UIs might render the error (in conversionError),
+ * especially in dev mode.
+ */
+ val status: Status,
+
+ /**
* Transfer amount. Might be in a different currency than the requested
* amount for withdrawal.
*
@@ -235,7 +246,23 @@ data class WithdrawalExchangeAccountDetails (
* exchange.
*/
val creditRestrictions: List<AccountRestriction>? = null,
-)
+
+ /**
+ * Label given to the account or the account's bank by the exchange.
+ */
+ val bankLabel: String? = null,
+
+ val priority: Int? = null,
+) {
+ @Serializable
+ enum class Status {
+ @SerialName("ok")
+ Ok,
+
+ @SerialName("error")
+ Error;
+ }
+}
@Serializable
sealed class AccountRestriction {
@@ -328,10 +355,7 @@ class TransactionRefund(
@Transient
override val amountType = AmountType.Positive
- override fun getTitle(context: Context): String {
- val merchantName = paymentInfo?.merchant?.name ?: "null"
- return context.getString(R.string.transaction_refund_from, merchantName)
- }
+ override fun getTitle(context: Context) = paymentInfo?.merchant?.name ?: context.getString(R.string.transaction_refund)
override val generalTitleRes = R.string.refund_title
}
@@ -378,7 +402,10 @@ class TransactionDeposit(
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_deposit)
+ val uri = Uri.parse(targetPaytoUri)
+ return uri.getQueryParameter("receiver-name")?.let { receiverName ->
+ context.getString(R.string.transaction_deposit_to, receiverName)
+ } ?: context.getString(R.string.transaction_deposit)
}
override val generalTitleRes = R.string.transaction_deposit
@@ -506,6 +533,47 @@ class TransactionPeerPushCredit(
}
/**
+ * A transaction to indicate financial loss due to denominations
+ * that became unusable for deposits.
+ */
+@Serializable
+@SerialName("denom-loss")
+class TransactionDenomLoss(
+ override val transactionId: String,
+ override val timestamp: Timestamp,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ override val error: TalerErrorInfo? = null,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ val lossEventType: LossEventType,
+): Transaction() {
+ override val icon: Int = R.drawable.transaction_loss
+ override val detailPageNav = R.id.nav_transactions_detail_loss
+
+ @Transient
+ override val amountType: AmountType = AmountType.Negative
+
+ override fun getTitle(context: Context): String {
+ return context.getString(R.string.transaction_denom_loss)
+ }
+
+ override val generalTitleRes: Int = R.string.transaction_denom_loss
+}
+
+@Serializable
+enum class LossEventType {
+ @SerialName("denom-expired")
+ DenomExpired,
+
+ @SerialName("denom-vanished")
+ DenomVanished,
+
+ @SerialName("denom-unoffered")
+ DenomUnoffered
+}
+
+/**
* This represents a transaction that we can not parse for some reason.
*/
class DummyTransaction(
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 5243427..d2d0c9c 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt
@@ -26,6 +26,7 @@ import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.fragment.app.Fragment
@@ -44,6 +45,8 @@ import net.taler.wallet.MainViewModel
import net.taler.wallet.R
import net.taler.wallet.TAG
import net.taler.wallet.balances.BalanceState.Success
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.cleanExchange
import net.taler.wallet.databinding.FragmentTransactionsBinding
import net.taler.wallet.showError
@@ -115,7 +118,7 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
if (balances.size == 1) ui.mainFab.visibility = INVISIBLE
balances.find { it.scopeInfo == scopeInfo }?.let { balance ->
- ui.amount.text = balance.available.toString(showSymbol = false)
+ ui.actionsBar.amount.text = balance.available.toString(showSymbol = false)
transactionAdapter.setCurrencySpec(balance.available.spec)
}
}
@@ -125,10 +128,10 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
transactionManager.transactions.observe(viewLifecycleOwner) { result ->
onTransactionsResult(result)
}
- ui.sendButton.setOnClickListener {
+ ui.actionsBar.sendButton.setOnClickListener {
findNavController().navigate(R.id.sendFunds)
}
- ui.receiveButton.setOnClickListener {
+ ui.actionsBar.receiveButton.setOnClickListener {
findNavController().navigate(R.id.action_global_receiveFunds)
}
ui.mainFab.setOnClickListener {
@@ -154,6 +157,8 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
override fun onStart() {
super.onStart()
requireActivity().title = getString(R.string.transactions_detail_title_currency, scopeInfo.currency)
+ (requireActivity() as AppCompatActivity).supportActionBar?.subtitle =
+ (scopeInfo as? ScopeInfo.Exchange)?.url?.let { cleanExchange(it) }
}
private fun setupSearch(item: MenuItem) {
@@ -261,6 +266,11 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.
return true
}
+ override fun onStop() {
+ super.onStop()
+ (requireActivity() as AppCompatActivity).supportActionBar?.subtitle = null
+ }
+
override fun onDestroyActionMode(mode: ActionMode) {
tracker?.clearSelection()
actionMode = null
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
index 5155b5b..6f88614 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt
@@ -81,11 +81,13 @@ fun TransactionWithdrawalComposable(
ActionButton(tx = t, listener = actionListener)
- TransactionAmountComposable(
- label = stringResource(R.string.amount_chosen),
- amount = t.amountRaw.withSpec(spec),
- amountType = AmountType.Neutral,
- )
+ if (t.amountRaw != t.amountEffective) {
+ TransactionAmountComposable(
+ label = stringResource(R.string.amount_chosen),
+ amount = t.amountRaw.withSpec(spec),
+ amountType = AmountType.Neutral,
+ )
+ }
val fee = t.amountRaw - t.amountEffective
if (!fee.isZero()) {
@@ -129,6 +131,7 @@ fun TransactionWithdrawalComposablePreview() {
WithdrawalExchangeAccountDetails(
paytoUri = "payto://IBAN/1231231231",
transferAmount = Amount.fromJSONString("NETZBON:42.23"),
+ status = WithdrawalExchangeAccountDetails.Status.Ok,
currencySpecification = CurrencySpecification(
name = "NETZBON",
numFractionalInputDigits = 2,
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
index 35ff89c..027bc57 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ScreenTransfer.kt
@@ -53,6 +53,7 @@ import net.taler.wallet.compose.copyToClipBoard
import net.taler.wallet.transactions.AmountType
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails.Status.*
import net.taler.wallet.withdraw.TransferData
import net.taler.wallet.withdraw.WithdrawStatus
@@ -66,13 +67,20 @@ fun ScreenTransfer(
// TODO: show some placeholder
if (status.withdrawalTransfers.isEmpty()) return
- val defaultTransfer = status.withdrawalTransfers[0]
+ val transfers = status.withdrawalTransfers.filter {
+ // TODO: in dev mode, show debug info when status is `Error'
+ it.withdrawalAccount.status == Ok
+ }.sortedByDescending {
+ it.withdrawalAccount.priority
+ }
+
+ val defaultTransfer = transfers[0]
var selectedTransfer by remember { mutableStateOf(defaultTransfer) }
Column {
if (status.withdrawalTransfers.size > 1) {
TransferAccountChooser(
- accounts = status.withdrawalTransfers.map { it.withdrawalAccount },
+ accounts = transfers.map { it.withdrawalAccount },
selectedAccount = selectedTransfer.withdrawalAccount,
onSelectAccount = { account ->
status.withdrawalTransfers.find {
@@ -92,8 +100,8 @@ fun ScreenTransfer(
is TransferData.Taler -> TransferTaler(
transfer = transfer,
exchangeBaseUrl = status.exchangeBaseUrl,
- transactionAmountRaw = status.transactionAmountRaw,
- transactionAmountEffective = status.transactionAmountEffective,
+ transactionAmountRaw = status.transactionAmountRaw.withSpec(spec),
+ transactionAmountEffective = status.transactionAmountEffective.withSpec(spec),
)
is TransferData.IBAN -> TransferIBAN(
@@ -205,13 +213,13 @@ fun WithdrawalAmountTransfer(
amount = fee,
amountType = AmountType.Negative,
)
- }
- TransactionAmountComposable(
- label = stringResource(id = R.string.withdraw_total),
- amount = amountEffective,
- amountType = AmountType.Positive,
- )
+ TransactionAmountComposable(
+ label = stringResource(id = R.string.withdraw_total),
+ amount = amountEffective,
+ amountType = AmountType.Positive,
+ )
+ }
}
}
@@ -236,7 +244,9 @@ fun TransferAccountChooser(
selected = selectedAccount.paytoUri == account.paytoUri,
onClick = { onSelectAccount(account) },
text = {
- if (account.currencySpecification?.name != null) {
+ if (!account.bankLabel.isNullOrEmpty()) {
+ Text(account.bankLabel)
+ } else if (account.currencySpecification?.name != null) {
Text(stringResource(
R.string.withdraw_account_currency,
index + 1,
@@ -274,6 +284,7 @@ fun ScreenTransferPreview() {
withdrawalAccount = WithdrawalExchangeAccountDetails(
paytoUri = "https://taler.net/kudos",
transferAmount = Amount("KUDOS", 10, 0),
+ status = Ok,
currencySpecification = CurrencySpecification(
"KUDOS",
numFractionalInputDigits = 2,
@@ -295,6 +306,7 @@ fun ScreenTransferPreview() {
withdrawalAccount = WithdrawalExchangeAccountDetails(
paytoUri = "https://taler.net/btc",
transferAmount = Amount("BTC", 0, 14000000),
+ status = Ok,
currencySpecification = CurrencySpecification(
"Bitcoin",
numFractionalInputDigits = 2,
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
index 6c1b014..1698530 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferIBAN.kt
@@ -42,6 +42,12 @@ fun TransferIBAN(
transactionAmountRaw: Amount,
transactionAmountEffective: Amount,
) {
+ val transferAmount = transfer
+ .withdrawalAccount
+ .transferAmount
+ ?.withSpec(transfer.withdrawalAccount.currencySpecification)
+ ?: transfer.amountRaw
+
Column(
modifier = Modifier.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -49,7 +55,7 @@ fun TransferIBAN(
Text(
text = stringResource(
R.string.withdraw_manual_ready_intro,
- transfer.amountRaw.toString()),
+ transferAmount),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.padding(vertical = 8.dp)
@@ -67,25 +73,21 @@ fun TransferIBAN(
.padding(all = 16.dp)
)
+ DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
transfer.receiverName?.let {
DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it)
}
DetailRow(stringResource(R.string.withdraw_manual_ready_iban), transfer.iban)
- DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
TransactionInfoComposable(
label = stringResource(R.string.withdraw_exchange),
info = cleanExchange(exchangeBaseUrl),
)
- transfer.withdrawalAccount.transferAmount?.let { amount ->
- WithdrawalAmountTransfer(
- amountRaw = transactionAmountRaw,
- amountEffective = transactionAmountEffective,
- conversionAmountRaw = amount.withSpec(
- transfer.withdrawalAccount.currencySpecification,
- ),
- )
- }
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = transferAmount,
+ )
}
} \ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
index cc6597e..089d0de 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/manual/TransferTaler.kt
@@ -42,6 +42,12 @@ fun TransferTaler(
transactionAmountRaw: Amount,
transactionAmountEffective: Amount,
) {
+ val transferAmount = transfer
+ .withdrawalAccount
+ .transferAmount
+ ?.withSpec(transfer.withdrawalAccount.currencySpecification)
+ ?: transfer.amountRaw
+
Column(
modifier = Modifier.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -49,7 +55,7 @@ fun TransferTaler(
Text(
text = stringResource(
R.string.withdraw_manual_ready_intro,
- transfer.amountRaw.toString()),
+ transferAmount),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.padding(vertical = 8.dp)
@@ -67,23 +73,21 @@ fun TransferTaler(
.padding(all = 16.dp)
)
+ DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
transfer.receiverName?.let {
DetailRow(stringResource(R.string.withdraw_manual_ready_receiver), it)
}
DetailRow(stringResource(R.string.withdraw_manual_ready_account), transfer.account)
- DetailRow(stringResource(R.string.withdraw_manual_ready_subject), transfer.subject)
TransactionInfoComposable(
label = stringResource(R.string.withdraw_exchange),
info = cleanExchange(exchangeBaseUrl),
)
- transfer.withdrawalAccount.transferAmount?.let { amount ->
- WithdrawalAmountTransfer(
- amountRaw = transactionAmountRaw,
- amountEffective = transactionAmountEffective,
- conversionAmountRaw = amount,
- )
- }
+ WithdrawalAmountTransfer(
+ amountRaw = transactionAmountRaw,
+ amountEffective = transactionAmountEffective,
+ conversionAmountRaw = transferAmount,
+ )
}
} \ No newline at end of file
diff --git a/wallet/src/main/res/drawable/ic_funds_receive.xml b/wallet/src/main/res/drawable/ic_funds_receive.xml
new file mode 100644
index 0000000..f540e4e
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_funds_receive.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z"/>
+
+</vector>
diff --git a/wallet/src/main/res/drawable/ic_funds_send.xml b/wallet/src/main/res/drawable/ic_funds_send.xml
new file mode 100644
index 0000000..9696eb6
--- /dev/null
+++ b/wallet/src/main/res/drawable/ic_funds_send.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
+
+</vector>
diff --git a/wallet/src/main/res/drawable/transaction_loss.xml b/wallet/src/main/res/drawable/transaction_loss.xml
new file mode 100644
index 0000000..ffc9a2e
--- /dev/null
+++ b/wallet/src/main/res/drawable/transaction_loss.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2024 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/>
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.12,9.88C16.56,9.32 15.8,9 15,9S13.44,9.32 12.88,9.88C12.32,10.44 12,11.2 12,12S12.32,13.56 12.88,14.12 14.2,15 15,15 16.56,14.68 17.12,14.12 18,12.8 18,12 17.68,10.44 17.12,9.88M7,6V18H23V6H7M21,14C20.47,14 19.96,14.21 19.59,14.59C19.21,14.96 19,15.47 19,16H11C11,15.47 10.79,14.96 10.41,14.59C10.04,14.21 9.53,14 9,14V10C9.53,10 10.04,9.79 10.41,9.41C10.79,9.04 11,8.53 11,8H19C19,8.53 19.21,9.04 19.59,9.41C19.96,9.79 20.47,10 21,10V14M5,8H3C2.45,8 2,7.55 2,7C2,6.45 2.45,6 3,6H5V8M5,13H2C1.45,13 1,12.55 1,12C1,11.45 1.45,11 2,11H5V13M5,18H1C0.448,18 0,17.55 0,17C0,16.45 0.448,16 1,16H5V18Z"/>
+</vector>
diff --git a/wallet/src/main/res/layout/balance_actions.xml b/wallet/src/main/res/layout/balance_actions.xml
new file mode 100644
index 0000000..d071a78
--- /dev/null
+++ b/wallet/src/main/res/layout/balance_actions.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2024 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/>
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/sendButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
+ android:layout_marginVertical="10dp"
+ android:paddingHorizontal="18dp"
+ android:text="@string/transactions_send_funds"
+ app:icon="@drawable/ic_funds_send"
+ tools:ignore="MissingConstraints" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/receiveButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="10dp"
+ android:layout_marginVertical="10dp"
+ android:paddingHorizontal="18dp"
+ android:text="@string/transactions_receive_funds"
+ app:icon="@drawable/ic_funds_receive"
+ tools:ignore="MissingConstraints" />
+
+ <androidx.constraintlayout.helper.widget.Flow
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:constraint_referenced_ids="sendButton,receiveButton"
+ android:paddingHorizontal="10dp"
+ app:flow_horizontalGap="10dp"
+ app:flow_horizontalBias="0"
+ app:flow_horizontalAlign="start"
+ app:flow_horizontalStyle="packed"
+ app:flow_wrapMode="chain"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/amountBarrier"
+ app:layout_constraintBottom_toBottomOf="@id/topBarrier"/>
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/amountBarrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/divider"
+ app:barrierDirection="start"/>
+
+ <LinearLayout
+ android:id="@+id/amountLayout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:orientation="vertical"
+ android:gravity="end"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/divider"
+ app:layout_constraintStart_toEndOf="@id/amountBarrier">
+
+ <TextView
+ android:id="@+id/balanceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/transactions_balance"
+ android:textSize="14sp" />
+
+ <TextView
+ android:id="@+id/amount"
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="8dp"
+ android:textStyle="bold"
+ tools:text="23.42"
+ tools:visibility="visible" />
+
+ </LinearLayout>
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/topBarrier"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="sendButton,receiveButton,amountLayout" />
+
+ <com.google.android.material.divider.MaterialDivider
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/topBarrier" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/wallet/src/main/res/layout/fragment_transactions.xml b/wallet/src/main/res/layout/fragment_transactions.xml
index 8fa46f5..c417540 100644
--- a/wallet/src/main/res/layout/fragment_transactions.xml
+++ b/wallet/src/main/res/layout/fragment_transactions.xml
@@ -20,87 +20,40 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <com.google.android.material.button.MaterialButton
- android:id="@+id/sendButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="10dp"
- android:text="@string/transactions_send_funds"
- app:layout_constraintBottom_toTopOf="@+id/divider"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
- <com.google.android.material.button.MaterialButton
- android:id="@+id/receiveButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginHorizontal="10dp"
- android:text="@string/transactions_receive_funds"
- app:layout_constraintBottom_toTopOf="@+id/divider"
- app:layout_constraintEnd_toStartOf="@+id/amount"
- app:layout_constraintHorizontal_chainStyle="spread_inside"
- app:layout_constraintStart_toEndOf="@+id/sendButton"
- app:layout_constraintTop_toTopOf="parent" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
- <TextView
- android:id="@+id/balanceLabel"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- android:layout_marginEnd="16dp"
- android:text="@string/transactions_balance"
- android:textSize="14sp"
- app:layout_constraintBottom_toTopOf="@+id/amount"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="1.0"
- app:layout_constraintStart_toEndOf="@+id/receiveButton"
- app:layout_constraintTop_toTopOf="parent" />
+ <FrameLayout
+ android:id="@+id/actionsFrame"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent">
+ <include
+ android:id="@+id/actionsBar"
+ layout="@layout/balance_actions"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"/>
+ </FrameLayout>
- <TextView
- android:id="@+id/amount"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="8dp"
- android:textSize="24sp"
- android:textStyle="bold"
- app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toTopOf="@+id/divider"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toEndOf="@+id/receiveButton"
- app:layout_constraintTop_toBottomOf="@+id/balanceLabel"
- tools:text="23.42"
- tools:visibility="visible" />
-
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/topBarrier"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:barrierDirection="bottom"
- app:constraint_referenced_ids="sendButton,receiveButton,amount" />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:nestedScrollingEnabled="false"
+ android:scrollbars="vertical"
+ android:visibility="invisible"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:listitem="@layout/list_item_transaction"
+ tools:visibility="visible" />
- <com.google.android.material.divider.MaterialDivider
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/topBarrier" />
+ </LinearLayout>
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/list"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:scrollbars="vertical"
- android:visibility="invisible"
- app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/divider"
- tools:listitem="@layout/list_item_transaction"
- tools:visibility="visible" />
+ </androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/emptyState"
@@ -115,7 +68,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/divider"
+ app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ProgressBar
@@ -128,7 +81,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/divider"
+ app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
diff --git a/wallet/src/main/res/layout/fragment_uri_input.xml b/wallet/src/main/res/layout/fragment_uri_input.xml
index 95c2297..6547625 100644
--- a/wallet/src/main/res/layout/fragment_uri_input.xml
+++ b/wallet/src/main/res/layout/fragment_uri_input.xml
@@ -68,7 +68,7 @@
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:backgroundTint="@color/green"
- android:text="@string/ok"
+ android:text="@string/open"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/pasteButton"
diff --git a/wallet/src/main/res/layout/list_item_balance.xml b/wallet/src/main/res/layout/list_item_balance.xml
index 53e3d89..6e5e440 100644
--- a/wallet/src/main/res/layout/list_item_balance.xml
+++ b/wallet/src/main/res/layout/list_item_balance.xml
@@ -14,9 +14,9 @@
~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-->
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
@@ -26,26 +26,16 @@
android:id="@+id/balanceAmountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
style="?textAppearanceDisplaySmall"
- app:layout_constraintEnd_toStartOf="@+id/pendingView"
- app:layout_constraintHorizontal_bias="0.0"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
tools:text="100.50" />
<TextView
android:id="@+id/scopeView"
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="?textAppearanceBodyMedium"
android:visibility="gone"
- app:layout_constraintTop_toBottomOf="@id/balanceAmountView"
- app:layout_constraintBottom_toTopOf="@id/balanceInboundAmount"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/pendingView"
tools:text="@string/balance_scope_exchange"
tools:visibility="visible"/>
@@ -54,40 +44,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/green"
- android:textSize="20sp"
style="?textAppearanceBodyLarge"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/balanceInboundLabel"
- app:layout_constraintHorizontal_bias="0.0"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/scopeView"
- tools:text="+10 TESTKUDOS"
+ tools:text="+10 TESTKUDOS inbound"
tools:visibility="visible" />
<TextView
- android:id="@+id/balanceInboundLabel"
+ android:id="@+id/balanceOutboundAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:text="@string/balances_inbound_label"
- android:textColor="@color/green"
- style="?textAppearanceBodyMedium"
- app:layout_constraintBottom_toBottomOf="@+id/balanceInboundAmount"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/balanceInboundAmount"
- app:layout_constraintTop_toTopOf="@+id/balanceInboundAmount"
+ android:textColor="?colorError"
+ style="?textAppearanceBodyLarge"
+ tools:text="-10 TESTKUDOS outbound"
tools:visibility="visible" />
- <TextView
- android:id="@+id/pendingView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/badge"
- android:text="@string/transaction_pending"
- android:textColor="?android:textColorPrimaryInverse"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
diff --git a/wallet/src/main/res/layout/list_item_transaction.xml b/wallet/src/main/res/layout/list_item_transaction.xml
index 64d9045..ad792ae 100644
--- a/wallet/src/main/res/layout/list_item_transaction.xml
+++ b/wallet/src/main/res/layout/list_item_transaction.xml
@@ -22,9 +22,9 @@
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
- android:paddingTop="8dp"
+ android:paddingTop="12dp"
android:paddingEnd="16dp"
- android:paddingBottom="8dp">
+ android:paddingBottom="12dp">
<ImageView
android:id="@+id/icon"
@@ -50,11 +50,11 @@
<TextView
android:id="@+id/extraInfoView"
+ style="@style/TransactionSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
- android:textSize="14sp"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="@+id/title"
@@ -64,11 +64,11 @@
<TextView
android:id="@+id/time"
+ style="@style/TransactionTimestamp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
- android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="@+id/title"
@@ -84,9 +84,9 @@
<TextView
android:id="@+id/amount"
+ style="@style/TransactionAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/pendingView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
diff --git a/wallet/src/main/res/menu/exchange.xml b/wallet/src/main/res/menu/exchange.xml
index 1d2c2e5..d99ff00 100644
--- a/wallet/src/main/res/menu/exchange.xml
+++ b/wallet/src/main/res/menu/exchange.xml
@@ -22,6 +22,9 @@
android:id="@+id/action_receive_peer"
android:title="@string/receive_peer" />
<item
+ android:id="@+id/action_reload"
+ android:title="@string/exchange_reload" />
+ <item
android:id="@+id/action_delete"
android:title="@string/transactions_delete" />
</menu>
diff --git a/wallet/src/main/res/menu/global_dev.xml b/wallet/src/main/res/menu/global_dev.xml
new file mode 100644
index 0000000..d6f73b9
--- /dev/null
+++ b/wallet/src/main/res/menu/global_dev.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2024 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/>
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/action_show_logs"
+ android:title="@string/show_logs"
+ android:icon="@drawable/ic_bug_report"
+ app:showAsAction="ifRoom" />
+</menu> \ No newline at end of file
diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml
index c48d93d..f6c90ab 100644
--- a/wallet/src/main/res/navigation/nav_graph.xml
+++ b/wallet/src/main/res/navigation/nav_graph.xml
@@ -34,6 +34,65 @@
</fragment>
<fragment
+ android:id="@+id/handleUri"
+ android:name="net.taler.wallet.HandleUriFragment"
+ android:label="@string/handle_uri_title">
+ <argument
+ android:name="uri"
+ app:argType="string"
+ app:nullable="false" />
+ <argument
+ android:name="from"
+ app:argType="string"
+ app:nullable="false" />
+
+ <action
+ android:id="@+id/action_handleUri_to_receiveFunds"
+ app:destination="@id/receiveFunds"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_sendFunds"
+ app:destination="@id/sendFunds"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptWithdraw"
+ app:destination="@id/promptWithdraw"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_manualWithdrawal"
+ app:destination="@id/nav_exchange_manual_withdrawal"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPayment"
+ app:destination="@id/promptPayment"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPullPayment"
+ app:destination="@id/promptPullPayment"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPushPayment"
+ app:destination="@id/promptPushPayment"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_promptPayTemplate"
+ app:destination="@id/promptPayTemplate"
+ app:popUpTo="@id/nav_main" />
+
+ <action
+ android:id="@+id/action_handleUri_to_nav_payto_uri"
+ app:destination="@id/nav_payto_uri"
+ app:popUpTo="@id/nav_main" />
+ </fragment>
+
+ <fragment
android:id="@+id/receiveFunds"
android:name="net.taler.wallet.ReceiveFundsFragment"
android:label="@string/transactions_receive_funds">
@@ -134,10 +193,6 @@
app:argType="string"
app:nullable="false" />
<argument
- android:name="scopeInfo"
- app:argType="string"
- app:nullable="true" />
- <argument
android:name="IBAN"
android:defaultValue="@null"
app:argType="string"
@@ -162,11 +217,6 @@
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
- <argument
- android:name="scopeInfo"
- android:defaultValue="@null"
- app:argType="string"
- app:nullable="true" />
<action
android:id="@+id/action_nav_peer_pull_to_nav_main"
app:destination="@id/nav_main"
@@ -186,11 +236,6 @@
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
- <argument
- android:name="scopeInfo"
- android:defaultValue="@null"
- app:argType="string"
- app:nullable="true" />
<action
android:id="@+id/action_nav_peer_push_to_nav_main"
app:destination="@id/nav_main"
@@ -279,6 +324,11 @@
android:label="@string/transactions_detail_title" />
<fragment
+ android:id="@+id/nav_transactions_detail_loss"
+ android:name="net.taler.wallet.transactions.TransactionLossFragment"
+ android:label="@string/transactions_detail_title" />
+
+ <fragment
android:id="@+id/nav_transactions_detail_dummy"
android:name="net.taler.wallet.transactions.TransactionDummyFragment"
android:label="@string/transactions_detail_title" />
@@ -333,6 +383,10 @@
tools:layout="@layout/fragment_error" />
<action
+ android:id="@+id/action_global_handle_uri"
+ app:destination="@id/handleUri" />
+
+ <action
android:id="@+id/action_global_receiveFunds"
app:destination="@id/receiveFunds" />
@@ -341,30 +395,10 @@
app:destination="@id/sendFunds" />
<action
- android:id="@+id/action_global_promptWithdraw"
- app:destination="@id/promptWithdraw" />
-
- <action
- android:id="@+id/action_global_manual_withdrawal"
- app:destination="@id/nav_exchange_manual_withdrawal" />
-
- <action
android:id="@+id/action_global_promptPayment"
app:destination="@id/promptPayment" />
<action
- android:id="@+id/action_global_prompt_pull_payment"
- app:destination="@id/promptPullPayment" />
-
- <action
- android:id="@+id/action_global_prompt_push_payment"
- app:destination="@id/promptPushPayment" />
-
- <action
- android:id="@+id/action_global_prompt_pay_template"
- app:destination="@id/promptPayTemplate" />
-
- <action
android:id="@+id/action_nav_transactions_detail_withdrawal"
app:destination="@id/nav_transactions_detail_withdrawal" />
diff --git a/wallet/src/main/res/values-de/strings.xml b/wallet/src/main/res/values-de/strings.xml
index 5ea98e9..f4e3fed 100644
--- a/wallet/src/main/res/values-de/strings.xml
+++ b/wallet/src/main/res/values-de/strings.xml
@@ -120,7 +120,7 @@
<string name="paste_invalid">Die Zwischenablage enthält einen ungültigen Datentyp</string>
<string name="uri_invalid">Keine gültige Taler-URI</string>
<string name="ok">Bestätigen</string>
- <string name="cancel">Abbrechen</string>
+ <string name="cancel">Zurück</string>
<string name="search">Suche</string>
<string name="menu">Menü</string>
<string name="nav_error">Fehler</string>
@@ -161,7 +161,7 @@
<string name="transactions_delete_dialog_message">Sind Sie sicher, dass Sie diese Transaktion aus Ihrem Wallet entfernen möchten?</string>
<string name="transactions_delete_dialog_title">Transaktion löschen</string>
<string name="receive_peer_payment_intro">Möchten Sie diese Zahlung erhalten?</string>
- <string name="transactions_abort">Abbrechen</string>
+ <string name="transactions_abort">Abbruch ausführen</string>
<string name="payment_pay_template_title">Passen Sie Ihre Bestellung an</string>
<string name="send_intro">Wählen Sie aus, wohin Sie Geld senden möchten:</string>
<string name="send_deposit_title">Einzahlung auf ein Bankkonto</string>
diff --git a/wallet/src/main/res/values-it/strings.xml b/wallet/src/main/res/values-it/strings.xml
index fdc4594..61bb306 100644
--- a/wallet/src/main/res/values-it/strings.xml
+++ b/wallet/src/main/res/values-it/strings.xml
@@ -226,7 +226,7 @@
<string name="settings_db_export_error">Errore nell\'esportazione della banca dati</string>
<string name="transactions_abort">Annulla</string>
<string name="transactions_fail">Arresta</string>
- <string name="transactions_abort_dialog_title">Annulla la Transazione</string>
+ <string name="transactions_abort_dialog_title">Annulla la transazione</string>
<string name="transactions_fail_dialog_title">Annulla la Transazione</string>
<string name="transactions_fail_dialog_message">Sei sicuro di voler annullare questa transazione? I fondi ancora in transito ANDRANNO PERSI.</string>
<string name="transactions_abort_dialog_message">Sei sicuro di voler annullare questa transazione? I fondi ancora in transito potrebbero andare persi.</string>
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 2ec3d40..b0fa772 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -37,21 +37,22 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="nav_header_subtitle">Wallet</string>
<string name="nav_prompt_withdraw">Withdraw Digital Cash</string>
- <string name="nav_exchange_tos">Exchange\'s Terms of Service</string>
- <string name="nav_exchange_select">Select Exchange</string>
- <string name="nav_exchange_fees">Exchange Fees</string>
+ <string name="nav_exchange_tos">PSP\'s Terms of Service</string>
+ <string name="nav_exchange_select">Select provider</string>
+ <string name="nav_exchange_fees">Provider fees</string>
<string name="nav_error">Error</string>
<string name="button_back">Go Back</string>
<string name="button_scan_qr_code">Scan Taler QR Code</string>
<string name="button_scan_qr_code_label">Scan QR code</string>
- <string name="enter_uri">Enter Taler URI</string>
+ <string name="enter_uri">Enter taler:// URI</string>
<string name="copy" tools:override="true">Copy</string>
<string name="copy_uri">Copy Taler URI</string>
<string name="paste">Paste</string>
<string name="paste_invalid">Clipboard contains an invalid data type</string>
<string name="uri_invalid">Not a valid Taler URI</string>
<string name="ok">OK</string>
+ <string name="open">Open</string>
<string name="cancel">Cancel</string>
<string name="search">Search</string>
<string name="menu">Menu</string>
@@ -73,28 +74,31 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="host_apdu_service_desc">Taler NFC Payments</string>
+ <string name="handle_uri_title">Loading action</string>
+
<string name="balances_title">Balances</string>
<string name="amount_positive">+%s</string>
<string name="amount_negative">-%s</string>
<string name="amount_chosen">Chosen Amount</string>
<string name="amount_sent">Amount sent</string>
<string name="amount_received">Amount received</string>
- <string name="balances_inbound_label">inbound</string>
+ <string name="balances_inbound_amount">+%1$s inbound</string>
+ <string name="balances_outbound_amount">-%1$s outbound</string>
<string name="balances_empty_state">There is no digital cash in your wallet.\n\nYou can get test money from the demo bank:\n\nhttps://bank.demo.taler.net</string>
- <string name="balance_scope_exchange">Exchange: %1$s</string>
+ <string name="balance_scope_exchange">From %1$s</string>
<string name="balance_scope_auditor">Auditor: %1$s</string>
<string name="transactions_title">Transactions</string>
<string name="transactions_balance">Balance</string>
- <string name="transactions_send_funds">Send\nFunds</string>
+ <string name="transactions_send_funds">Send</string>
<string name="transactions_send_funds_title">Send %1$s</string>
- <string name="transactions_receive_funds">Receive\nFunds</string>
+ <string name="transactions_receive_funds">Receive</string>
<string name="transactions_receive_funds_title">Receive %1$s</string>
<string name="transactions_empty">You don\'t have any transactions</string>
<string name="transactions_empty_search">No transactions found. Try a different search.</string>
<string name="transactions_error">Could not load transactions\n\n%s</string>
<string name="transactions_detail_title">Transaction</string>
- <string name="transactions_detail_title_currency">%s Transactions</string>
+ <string name="transactions_detail_title_currency">%s transactions</string>
<string name="transactions_delete">Delete</string>
<string name="transactions_retry">Retry</string>
<string name="transactions_abort">Abort</string>
@@ -115,17 +119,18 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="transaction_paid">Paid</string>
<string name="transaction_order_total">Total</string>
<string name="transaction_order">Purchase</string>
- <string name="transaction_order_id">Receipt #%1$s</string>
+ <string name="transaction_order_id">Order #%1$s</string>
<string name="transaction_refund">Refund</string>
- <string name="transaction_refund_from">Refund from %s</string>
<string name="transaction_pending">PENDING</string>
<string name="transaction_refresh">Coin expiry change fee</string>
<string name="transaction_deposit">Deposit</string>
+ <string name="transaction_deposit_to">Deposit to %1$s</string>
<string name="transaction_peer_push_debit">Push payment</string>
<string name="transaction_peer_pull_credit">Invoice</string>
<string name="transaction_peer_pull_debit">Invoice paid</string>
<string name="transaction_peer_push_credit">Push payment</string>
<string name="transaction_action_kyc">Complete KYC</string>
+ <string name="transaction_denom_loss">Loss of funds</string>
<string name="transaction_dummy_title">Unknown Transaction</string>
<string name="payment_title">Payment</string>
@@ -180,7 +185,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="send_peer_payment_instruction">Let the payee scan this QR code to receive:</string>
<string name="send_peer_expiration_period">Expires in</string>
<string name="send_peer_expiration_1d">1 day</string>
- <string name="send_peer_expiration_7d">7 days</string>
+ <string name="send_peer_expiration_7d">1 week</string>
<string name="send_peer_expiration_30d">30 days</string>
<string name="send_peer_expiration_custom">Custom</string>
<string name="send_peer_expiration_days">Days</string>
@@ -199,21 +204,21 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="withdraw_fees">Fee</string>
<string name="withdraw_restrict_age">Restrict Usage to Age</string>
<string name="withdraw_restrict_age_unrestricted">Unrestricted</string>
- <string name="withdraw_exchange">Exchange</string>
+ <string name="withdraw_exchange">Provider</string>
<string name="withdraw_bank">Bank</string>
<string name="withdraw_button_confirm">Confirm Withdraw</string>
<string name="withdraw_button_confirm_bank">Confirm with bank</string>
<string name="withdraw_button_tos">Review Terms</string>
<string name="withdraw_waiting_confirm">Waiting for confirmation</string>
- <string name="withdraw_manual_title">Make a manual transfer to the exchange</string>
+ <string name="withdraw_manual_title">Make a manual transfer to the provider</string>
<string name="withdraw_amount">How much to withdraw?</string>
<string name="withdraw_amount_error">Enter valid amount</string>
<string name="withdraw_manual_payment_options">Payment options supported by %1$s:\n\n%2$s</string>
<string name="withdraw_manual_check_fees">Check fees</string>
- <string name="withdraw_manual_ready_title">Exchange is ready for withdrawal!</string>
- <string name="withdraw_manual_ready_intro">To complete the process you need to wire %s to the exchange bank account</string>
+ <string name="withdraw_manual_ready_title">Provider is ready for withdrawal!</string>
+ <string name="withdraw_manual_ready_intro">To complete the process you need to wire %s to the provider\'s bank account</string>
<string name="withdraw_manual_ready_details_intro">Bank transfer details</string>
- <string name="withdraw_manual_bitcoin_title">Bitcoin exchange ready for withdrawal</string>
+ <string name="withdraw_manual_bitcoin_title">Bitcoin provider ready for withdrawal</string>
<string name="withdraw_manual_bitcoin_intro">Now make a split transaction with the following three outputs.</string>
<string name="withdraw_manual_ready_iban">IBAN</string>
<string name="withdraw_manual_ready_account">Account</string>
@@ -229,23 +234,24 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="withdraw_account_currency">Account #%1$d (%2$s)</string>
<string name="withdraw_transfer">Transfer</string>
<string name="withdraw_conversion">Conversion</string>
- <string name="withdraw_conversion_support">This exchange supports currency conversion</string>
+ <string name="withdraw_conversion_support">This provider supports currency conversion</string>
- <string name="exchange_settings_title">Exchanges</string>
- <string name="exchange_settings_summary">Manage list of exchanges known to this wallet</string>
- <string name="exchange_list_title">Exchanges</string>
- <string name="exchange_list_empty">No exchanges known\n\nAdd one manually or withdraw digital cash!</string>
+ <string name="exchange_settings_title">Providers</string>
+ <string name="exchange_settings_summary">Manage list of providers known to this wallet</string>
+ <string name="exchange_list_title">Providers</string>
+ <string name="exchange_list_empty">No providers known\n\nAdd one manually or withdraw digital cash!</string>
<string name="exchange_list_currency">Currency: %s</string>
- <string name="exchange_list_add">Add exchange</string>
- <string name="exchange_list_select">Select exchange</string>
- <string name="exchange_delete">Delete exchange</string>
+ <string name="exchange_list_add">Add provider</string>
+ <string name="exchange_list_select">Select provider</string>
+ <string name="exchange_delete">Delete provider</string>
<string name="exchange_delete_force">Force deletion (purge)</string>
- <string name="exchange_dialog_delete_message">Are you sure you want to delete this exchange? Forcing this operation will result in a loss of funds.</string>
- <string name="exchange_not_contacted">Exchange not contacted</string>
- <string name="exchange_add_url">Enter address of exchange</string>
- <string name="exchange_add_error">Could not add exchange</string>
- <string name="exchange_list_error">Could not list exchanges</string>
- <string name="exchange_list_add_dev">Add development exchanges</string>
+ <string name="exchange_dialog_delete_message">Are you sure you want to delete this provider? Forcing this operation will result in a loss of funds.</string>
+ <string name="exchange_reload">Reload information</string>
+ <string name="exchange_not_contacted">Provider not contacted</string>
+ <string name="exchange_add_url">Enter address of provider</string>
+ <string name="exchange_add_error">Could not add provider</string>
+ <string name="exchange_list_error">Could not list providers</string>
+ <string name="exchange_list_add_dev">Add development providers</string>
<string name="exchange_menu_manual_withdraw">Withdraw</string>
<string name="exchange_fee_withdrawal_fee_label">Withdrawal Fee:</string>
@@ -267,10 +273,23 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="exchange_tos_accept">Accept Terms of Service</string>
<string name="exchange_tos_error">Error showing Terms of Service: %s</string>
+ <string name="loss_amount">Amount lost</string>
+ <string name="loss_reason">Reason</string>
+ <string name="loss_reason_expired">Funds were not renewed, because the wallet was not opened for a long time</string>
+ <string name="loss_reason_vanished">The payment provider lost the record of the funds</string>
+ <string name="loss_reason_unoffered">The payment provider stopped offering the denomination backing the funds</string>
+
+
<string name="pending_operations_title">Pending Operations</string>
<string name="pending_operations_refuse">Refuse Proposal</string>
<string name="pending_operations_no_action">(no action)</string>
+ <!-- Observability -->
+ <string name="show_logs">Show logs</string>
+ <string name="observability_title">Internal event log</string>
+ <string name="observability_show_json">Show JSON</string>
+ <string name="observability_hide_json">Hide JSON</string>
+
<string name="settings_dev_mode">Developer Mode</string>
<string name="settings_dev_mode_summary">Shows more information intended for debugging</string>
<string name="settings_withdraw_testkudos">Withdraw TESTKUDOS</string>
diff --git a/wallet/src/main/res/values/styles.xml b/wallet/src/main/res/values/styles.xml
index d7d939f..961c8da 100644
--- a/wallet/src/main/res/values/styles.xml
+++ b/wallet/src/main/res/values/styles.xml
@@ -98,7 +98,19 @@
<style name="DialogTheme" parent="Theme.Material3.DayNight.Dialog.Alert" />
<style name="TransactionTitle">
- <item name="android:textSize">16sp</item>
+ <item name="android:textAppearance">@style/TextAppearance.Material3.TitleMedium</item>
+ </style>
+
+ <style name="TransactionSubtitle">
+ <item name="android:textAppearance">@style/TextAppearance.Material3.BodyMedium</item>
+ </style>
+
+ <style name="TransactionTimestamp">
+ <item name="android:textAppearance">@style/TextAppearance.Material3.LabelMedium</item>
+ </style>
+
+ <style name="TransactionAmount">
+ <item name="android:textAppearance">@style/TextAppearance.Material3.TitleLarge</item>
</style>
<style name="TransactionLabel">