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:
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 -->