diff options
Diffstat (limited to 'wallet/src')
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"> |