taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit 3d3eee87ff1218102f52d0cef7e8e7428792b1e2
parent 52e06a4e001731b9852d42d53223a530fca547f3
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 10 Apr 2025 18:16:12 +0200

[wallet] allow retrying QR when offline

bug 0009692

Diffstat:
Mwallet/src/main/java/net/taler/wallet/HandleUriFragment.kt | 48+++++++++++++++++++++++++++++++++++++++---------
Awallet/src/main/java/net/taler/wallet/compose/RetryScreen.kt | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/res/values/strings.xml | 1+
3 files changed, 118 insertions(+), 9 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt b/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt @@ -23,6 +23,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast.LENGTH_LONG +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView import androidx.core.os.bundleOf import androidx.fragment.app.Fragment @@ -38,12 +40,14 @@ import kotlinx.coroutines.launch import net.taler.common.isOnline import net.taler.common.showError import net.taler.wallet.compose.LoadingScreen +import net.taler.wallet.compose.RetryScreen 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 +import androidx.core.net.toUri class HandleUriFragment: Fragment() { private val model: MainViewModel by activityViewModels() @@ -51,10 +55,12 @@ class HandleUriFragment: Fragment() { lateinit var uri: String lateinit var from: String + private var processing = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { uri = arguments?.getString("uri") ?: error("no uri passed") from = arguments?.getString("from") ?: error("no from passed") @@ -62,7 +68,14 @@ class HandleUriFragment: Fragment() { return ComposeView(requireContext()).apply { setContent { TalerSurface { - LoadingScreen() + val networkStatus by model.networkManager.networkStatus.observeAsState() + if (networkStatus == true) { + LoadingScreen() + } else { + RetryScreen { + processTalerUri() + } + } } } } @@ -70,8 +83,25 @@ class HandleUriFragment: Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + model.networkManager.networkStatus.observe(viewLifecycleOwner) { status -> + if (status) { + processTalerUri() + } + } + } + + override fun onStart() { + super.onStart() + processing = false + } + + private fun processTalerUri() { + // FIXME: pressing `retry` is basically a fake action when offline, + // may be useful in the future if Taler action errors properly allow retrying. + if (processing || model.networkManager.networkStatus.value == false) return + processing = true - val uri = Uri.parse(uri) + val uri = uri.toUri() if (uri.fragment != null && !requireContext().isOnline()) { connectToWifi(requireContext(), uri.fragment!!) } @@ -110,12 +140,12 @@ class HandleUriFragment: Fragment() { } else u when { - action.startsWith("pay/", ignoreCase = true) -> { + action.startsWith("pay/", ignoreCase = true) -> run { Log.v(TAG, "navigating!") findNavController().navigate(R.id.action_handleUri_to_promptPayment) model.paymentManager.preparePay(u2) } - action.startsWith("withdraw/", ignoreCase = true) -> { + action.startsWith("withdraw/", ignoreCase = true) -> run { Log.v(TAG, "navigating!") // there's more than one entry point, so use global action val args = bundleOf( @@ -126,7 +156,7 @@ class HandleUriFragment: Fragment() { findNavController().navigate(R.id.action_handleUri_to_promptWithdraw, args) } - action.startsWith("withdraw-exchange/", ignoreCase = true) -> { + action.startsWith("withdraw-exchange/", ignoreCase = true) -> run { Log.v(TAG, "navigating!") val args = bundleOf( "withdrawExchangeUri" to u2, @@ -136,15 +166,15 @@ class HandleUriFragment: Fragment() { findNavController().navigate(R.id.action_handleUri_to_promptWithdraw, args) } - action.startsWith("refund/", ignoreCase = true) -> { + action.startsWith("refund/", ignoreCase = true) -> run { model.showProgressBar.value = true model.refundManager.refund(u2).observe(viewLifecycleOwner, Observer(::onRefundResponse)) } - action.startsWith("pay-pull/", ignoreCase = true) -> { + action.startsWith("pay-pull/", ignoreCase = true) -> run { findNavController().navigate(R.id.action_handleUri_to_promptPullPayment) model.peerManager.preparePeerPullDebit(u2) } - action.startsWith("pay-push/", ignoreCase = true) -> { + action.startsWith("pay-push/", ignoreCase = true) -> run { findNavController().navigate(R.id.action_handleUri_to_promptPushPayment) model.peerManager.preparePeerPushCredit(u2) } diff --git a/wallet/src/main/java/net/taler/wallet/compose/RetryScreen.kt b/wallet/src/main/java/net/taler/wallet/compose/RetryScreen.kt @@ -0,0 +1,77 @@ +/* + * This file is part of GNU Taler + * (C) 2025 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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.taler.wallet.R +import net.taler.wallet.systemBarsPaddingBottom + +@Composable +fun RetryScreen(onRetry: () -> Unit) { + Column ( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .systemBarsPaddingBottom(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + stringResource(R.string.qr_scan_offline), + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(16.dp)) + + Button(onRetry) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.transactions_retry)) + } + } +} + +@Composable +@Preview +fun RetryScreenPreview() { + TalerSurface { + RetryScreen {} + } +} +\ No newline at end of file diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -76,6 +76,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="qr_scan_context_title">Possibly unintended action</string> <string name="qr_scan_context_receive_message">Seems like you were intending to receive money, but the QR code that you scanned corresponds to a different action. Do you wish to continue?</string> <string name="qr_scan_context_send_message">Seems like you were intending to send money, but the QR code that you scanned corresponds to a different action. Do you wish to continue?</string> + <string name="qr_scan_offline">This action requires internet connection, in order to retry when internet is available, please stay on this screen and wait, or press the button below.</string> <!-- Errors -->