commit 3fcb7e0a28bb96f97026cb33e542bb0be3321b6b
parent cfd3af80964fb2a4a76df2a1c0dfd7453ba74cfd
Author: Iván Ávalos <avalos@disroot.org>
Date: Tue, 27 Jan 2026 19:14:25 +0100
[wallet] fix #10674 (shopping URLs)
Diffstat:
7 files changed, 517 insertions(+), 51 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/Balances.kt b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
@@ -29,6 +29,7 @@ data class BalanceItem(
val pendingIncoming: Amount,
val pendingOutgoing: Amount,
val disablePeerPayments: Boolean,
+ val shoppingUrls: List<String> = emptyList(),
) {
val currency: String get() = available.currency
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/NewMenuComponents.kt b/wallet/src/main/java/net/taler/wallet/compose/NewMenuComponents.kt
@@ -0,0 +1,188 @@
+/**
+ * Metrolist Project (C) 2026
+ * Licensed under GPL-3.0 | See git history for contributors
+ */
+
+package net.taler.wallet.compose
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+// Enhanced Menu Item - Material 3 Expressive Design
+@Composable
+fun NewMenuItem(
+ modifier: Modifier = Modifier,
+ headlineContent: @Composable () -> Unit,
+ leadingContent: @Composable (() -> Unit)? = null,
+ trailingContent: @Composable (() -> Unit)? = null,
+ supportingContent: @Composable (() -> Unit)? = null,
+ onClick: (() -> Unit)? = null,
+ enabled: Boolean = true,
+) {
+ ListItem(
+ headlineContent = headlineContent,
+ leadingContent = leadingContent,
+ trailingContent = trailingContent,
+ supportingContent = supportingContent,
+ modifier = modifier
+ .clickable(enabled = enabled) { onClick?.invoke() }
+ .padding(horizontal = 4.dp),
+ tonalElevation = 0.dp
+ )
+}
+
+// Enhanced Menu Section Header - Material 3 Expressive Design
+@Composable
+fun NewMenuSectionHeader(
+ modifier: Modifier = Modifier,
+ text: String,
+) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.primary,
+ modifier = modifier.padding(horizontal = 20.dp, vertical = 12.dp)
+ )
+}
+
+// Enhanced Menu Content - Material 3 Expressive Design
+@Composable
+fun NewMenuContent(
+ modifier: Modifier = Modifier,
+ headerContent: @Composable (() -> Unit)? = null,
+ actionGrid: @Composable (() -> Unit)? = null,
+ menuItems: @Composable (() -> Unit)? = null,
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ // Header
+ headerContent?.invoke()
+
+ // Action Grid
+ actionGrid?.invoke()
+
+ // Divider if both header and actions exist
+ if (headerContent != null && actionGrid != null) {
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 16.dp),
+ color = MaterialTheme.colorScheme.outlineVariant
+ )
+ }
+
+ // Menu Items
+ menuItems?.invoke()
+ }
+}
+
+@Composable
+fun Material3MenuGroup(
+ items: List<Material3MenuItemData>,
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ items.forEachIndexed { index, item ->
+ val shape = when {
+ items.size == 1 -> RoundedCornerShape(24.dp)
+ index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 6.dp, bottomEnd = 6.dp)
+ index == items.size - 1 -> RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 24.dp, bottomEnd = 24.dp)
+ else -> RoundedCornerShape(6.dp)
+ }
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .animateContentSize(),
+ shape = shape,
+ colors = item.cardColors ?: CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Material3MenuItemRow(item = item)
+ }
+ }
+ }
+}
+
+@Composable
+private fun Material3MenuItemRow(
+ item: Material3MenuItemData,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ enabled = item.onClick != null,
+ onClick = { item.onClick?.invoke() }
+ )
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ item.icon?.let { icon ->
+ icon()
+ Spacer(modifier = Modifier.width(16.dp))
+ }
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ ProvideTextStyle(MaterialTheme.typography.titleMedium) {
+ item.title()
+ }
+
+ item.description?.let { desc ->
+ Spacer(modifier = Modifier.height(2.dp))
+ ProvideTextStyle(
+ MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ ) {
+ desc()
+ }
+ }
+ }
+ item.trailingContent?.let { trailing ->
+ Spacer(modifier = Modifier.width(8.dp))
+ trailing()
+ }
+ }
+}
+
+data class Material3MenuItemData(
+ val icon: (@Composable () -> Unit)? = null,
+ val title: @Composable () -> Unit,
+ val description: (@Composable () -> Unit)? = null,
+ val onClick: (() -> Unit)? = null,
+ val cardColors: CardColors? = null,
+ val trailingContent: (@Composable () -> Unit)? = null,
+)
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeShoppingFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeShoppingFragment.kt
@@ -0,0 +1,194 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2026 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.exchanges
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Link
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.launch
+import net.taler.wallet.BottomInsetsSpacer
+import net.taler.wallet.R
+import net.taler.wallet.balances.BalanceItem
+import net.taler.wallet.balances.BalanceState
+import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.compose.EmptyComposable
+import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.Material3MenuGroup
+import net.taler.wallet.compose.Material3MenuItemData
+import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.compose.collectAsStateLifecycleAware
+import net.taler.wallet.getAttrColor
+import net.taler.wallet.launchInAppBrowser
+import net.taler.wallet.main.MainViewModel
+import net.taler.wallet.main.ViewMode
+
+class ExchangeShoppingFragment : Fragment() {
+ private val model: MainViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ TalerSurface {
+ val viewMode by model.viewMode.collectAsStateLifecycleAware()
+ val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope
+ val balanceState by model.balanceManager.state.observeAsState(BalanceState.None)
+
+ val selectedBalance = remember(balanceState, selectedScope) {
+ val balances = (balanceState as? BalanceState.Success)?.balances
+ selectedScope?.let {
+ balances?.find { it.scopeInfo == selectedScope }
+ }
+ }
+
+ if (selectedScope == null) {
+ EmptyComposable(stringResource(R.string.exchange_unselected))
+ return@TalerSurface
+ }
+
+ if (selectedBalance == null) {
+ LoadingScreen()
+ return@TalerSurface
+ }
+
+ ExchangeShoppingComposable(
+ currency = selectedBalance.currency,
+ shoppingUrls = selectedBalance.shoppingUrls,
+ )
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ model.viewMode.collect { viewMode ->
+ val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope
+ selectedScope?.let { scope ->
+ (requireActivity() as AppCompatActivity).apply {
+ supportActionBar?.title =
+ getString(R.string.exchange_shopping_label, scope.currency)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ExchangeShoppingComposable(
+ currency: String,
+ shoppingUrls: List<String>,
+) {
+ val context = LocalContext.current
+ val linkColor = Color(context.getAttrColor(android.R.attr.textColorLink))
+
+ Column (
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = stringResource(
+ R.string.exchange_shopping_message,
+ currency,
+ ),
+ )
+
+ Box(Modifier.padding(horizontal = 16.dp)) {
+ Material3MenuGroup(items = buildList {
+ shoppingUrls.forEach { url ->
+ add(
+ Material3MenuItemData(
+ icon = {
+ Icon(
+ Icons.Default.Link,
+ contentDescription = null
+ )
+ },
+ title = {
+ Text(
+ modifier = Modifier.basicMarquee(),
+ text = url,
+ color = linkColor,
+ )
+ },
+ onClick = { launchInAppBrowser(context, url) },
+ )
+ )
+ }
+ })
+ }
+
+ BottomInsetsSpacer()
+ }
+}
+
+@Preview
+@Composable
+fun ExchangeShoppingComposablePreview() {
+ TalerSurface {
+ ExchangeShoppingComposable(
+ currency = "CHF",
+ shoppingUrls = listOf(
+ "https://shopping.taler.net/",
+ "https://shopping.taler.ar/",
+ "https://shopping.taler-ops.ch/",
+ )
+ )
+ }
+}
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/main/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/main/MainFragment.kt
@@ -203,28 +203,30 @@ class MainFragment: Fragment() {
!online || (balanceState as? BalanceState.Success)?.balances?.isEmpty() ?: true
}
- val disablePeer = remember(balanceState, viewMode) {
- val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope
+ val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope
+
+ val selectedBalance = remember(balanceState, selectedScope) {
val balances = (balanceState as? BalanceState.Success)?.balances
- ?.filter { !it.disablePeerPayments }
- ?: emptyList()
selectedScope?.let {
- balances.find { it.scopeInfo == selectedScope } == null
- } ?: balances.isEmpty()
+ balances?.find { it.scopeInfo == selectedScope }
+ }
}
TalerActionsModal(
showSheet = showSheet,
sheetState = sheetState,
+ selectedCurrency = selectedBalance?.currency,
+ showShopping = selectedBalance?.shoppingUrls?.isNotEmpty() == true,
onDismiss = { showSheet = false },
disableActions = disableActions,
- disablePeer = disablePeer,
+ disablePeer = selectedBalance?.disablePeerPayments == true,
onSend = this@MainFragment::onSend,
onReceive = this@MainFragment::onReceive,
onScanQr = this@MainFragment::onScanQr,
onDeposit = this@MainFragment::onDeposit,
onWithdraw = this@MainFragment::onWithdraw,
onEnterUri = this@MainFragment::onEnterUri,
+ onShoppingDiscovery = this@MainFragment::onShoppingDiscovery,
)
}
}
@@ -303,6 +305,11 @@ class MainFragment: Fragment() {
model.settingsManager.saveActionButtonUsed(requireContext())
findNavController().navigate(R.id.nav_uri_input)
}
+
+ private fun onShoppingDiscovery() {
+ model.settingsManager.saveActionButtonUsed(requireContext())
+ findNavController().navigate(R.id.nav_shopping)
+ }
}
@Composable
diff --git a/wallet/src/main/java/net/taler/wallet/main/TalerActionButton.kt b/wallet/src/main/java/net/taler/wallet/main/TalerActionButton.kt
@@ -20,6 +20,8 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@@ -29,15 +31,20 @@ import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeFloatingActionButton
+import androidx.compose.material3.ListItem
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -48,6 +55,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.runBlocking
@@ -55,6 +63,9 @@ import net.taler.wallet.R
import net.taler.wallet.compose.DemandAttention
import net.taler.wallet.compose.GridMenu
import net.taler.wallet.compose.GridMenuItem
+import net.taler.wallet.compose.Material3MenuGroup
+import net.taler.wallet.compose.Material3MenuItemData
+import net.taler.wallet.compose.TalerSurface
import kotlin.math.roundToInt
@Composable
@@ -126,6 +137,8 @@ fun TalerActionButton(
fun TalerActionsModal(
showSheet: Boolean,
sheetState: SheetState,
+ selectedCurrency: String? = null,
+ showShopping: Boolean,
disableActions: Boolean,
disablePeer: Boolean,
onDismiss: () -> Unit,
@@ -135,62 +148,108 @@ fun TalerActionsModal(
onDeposit: () -> Unit,
onWithdraw: () -> Unit,
onEnterUri: () -> Unit,
+ onShoppingDiscovery: () -> Unit,
) {
if (showSheet) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
- GridMenu(
- contentPadding = PaddingValues(
- start = 8.dp,
- end = 8.dp,
- bottom = 16.dp + WindowInsets
- .systemBars
- .asPaddingValues()
- .calculateBottomPadding(),
- ),
- ) {
- GridMenuItem(
- icon = R.drawable.ic_link,
- title = R.string.enter_uri,
- onClick = { onEnterUri(); onDismiss() },
- )
+ Column {
+ if (showShopping && selectedCurrency != null) {
+ Box(Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 9.dp)) {
+ Material3MenuGroup(items = buildList {
+ add(
+ Material3MenuItemData(
+ title = { Text(stringResource(R.string.exchange_shopping_label, selectedCurrency)) },
+ icon = { Icon(
+ Icons.Default.LocationOn,
+ contentDescription = null
+ ) },
+ onClick = onShoppingDiscovery,
+ )
+ )
+ })
+ }
+ }
+
+ GridMenu(
+ contentPadding = PaddingValues(
+ start = 8.dp,
+ end = 8.dp,
+ bottom = 16.dp + WindowInsets
+ .systemBars
+ .asPaddingValues()
+ .calculateBottomPadding(),
+ ),
+ ) {
+ GridMenuItem(
+ icon = R.drawable.ic_link,
+ title = R.string.enter_uri,
+ onClick = { onEnterUri(); onDismiss() },
+ )
- GridMenuItem(
- icon = R.drawable.transaction_deposit,
- title = R.string.send_deposit_button_label,
- onClick = { onDeposit(); onDismiss() },
- enabled = !disableActions
- )
+ GridMenuItem(
+ icon = R.drawable.transaction_deposit,
+ title = R.string.send_deposit_button_label,
+ onClick = { onDeposit(); onDismiss() },
+ enabled = !disableActions
+ )
- GridMenuItem(
- icon = R.drawable.ic_scan_qr,
- title = R.string.button_scan_qr_code_label,
- onClick = { onScanQr(); onDismiss() },
- )
+ GridMenuItem(
+ icon = R.drawable.ic_scan_qr,
+ title = R.string.button_scan_qr_code_label,
+ onClick = { onScanQr(); onDismiss() },
+ )
- GridMenuItem(
- icon = R.drawable.transaction_p2p_incoming,
- title = R.string.transactions_receive_funds,
- onClick = { onReceive(); onDismiss() },
- enabled = !disableActions && !disablePeer,
- )
+ GridMenuItem(
+ icon = R.drawable.transaction_p2p_incoming,
+ title = R.string.transactions_receive_funds,
+ onClick = { onReceive(); onDismiss() },
+ enabled = !disableActions && !disablePeer,
+ )
- GridMenuItem(
- icon = R.drawable.transaction_withdrawal,
- title = R.string.withdraw_button_label,
- onClick = { onWithdraw(); onDismiss() },
- enabled = !disableActions,
- )
+ GridMenuItem(
+ icon = R.drawable.transaction_withdrawal,
+ title = R.string.withdraw_button_label,
+ onClick = { onWithdraw(); onDismiss() },
+ enabled = !disableActions,
+ )
- GridMenuItem(
- icon = R.drawable.transaction_p2p_outgoing,
- title = R.string.transactions_send_funds,
- onClick = { onSend(); onDismiss() },
- enabled = !disableActions && !disablePeer,
- )
+ GridMenuItem(
+ icon = R.drawable.transaction_p2p_outgoing,
+ title = R.string.transactions_send_funds,
+ onClick = { onSend(); onDismiss() },
+ enabled = !disableActions && !disablePeer,
+ )
+ }
}
}
}
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+fun TalerActionsModalPreview() {
+ TalerSurface {
+ TalerActionsModal(
+ showSheet = true,
+ sheetState = rememberModalBottomSheetState(),
+ selectedCurrency = "CHF",
+ showShopping = true,
+ disableActions = false,
+ disablePeer = false,
+ onDismiss = {},
+ onSend = {},
+ onReceive = {},
+ onScanQr = {},
+ onDeposit = {},
+ onWithdraw = {},
+ onEnterUri = {},
+ onShoppingDiscovery = {},
+ )
+ }
}
\ 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
@@ -347,6 +347,11 @@
app:argType="string" />
</fragment>
+ <fragment
+ android:id="@+id/exchangeShopping"
+ android:name="net.taler.wallet.exchanges.ExchangeShoppingFragment"
+ android:label="@string/exchange_shopping_title" />
+
<action
android:id="@+id/action_global_handle_uri"
app:destination="@id/handleUri" />
@@ -388,4 +393,8 @@
android:id="@+id/action_global_reviewExchangeTos"
app:destination="@id/reviewExchangeTOS" />
+ <action
+ android:id="@+id/nav_shopping"
+ app:destination="@id/exchangeShopping" />
+
</navigation>
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -426,6 +426,12 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<string name="exchange_tos_missing">Terms of service were not provided by payment service</string>
<string name="exchange_tos_view">Review terms of service</string>
<string name="exchange_tos_error">Error showing terms of service: %1$s</string>
+ <string name="exchange_unselected">No payment provider is selected</string>
+
+ <!-- Exchange shopping discovery -->
+ <string name="exchange_shopping_title">Where to pay</string>
+ <string name="exchange_shopping_label">Where to pay with %1$s</string>
+ <string name="exchange_shopping_message">Your payment service provider offers the following website(s) with information about where you can pay in %1$s using your Taler wallet:</string>
<!-- Losses -->