taler-android

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

commit 01f015da6114757ce7b98b8c57ef8f9ff29f314f
parent e3ae33d3f0f0b3450a580729345d07c0f6a994af
Author: Iván Ávalos <avalos@disroot.org>
Date:   Sun,  1 Mar 2026 01:28:52 +0100

[wallet] fix #11143 (new ToS screen)

Diffstat:
Mwallet/build.gradle | 7+++----
Mwallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt | 1-
Mwallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt | 13+++++++++++--
Mwallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt | 342+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/TosAdapter.kt | 94-------------------------------------------------------------------------------
Dwallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt | 91-------------------------------------------------------------------------------
Dwallet/src/main/res/layout/fragment_review_exchange_tos.xml | 95-------------------------------------------------------------------------------
Mwallet/src/main/res/values/strings.xml | 1+
8 files changed, 203 insertions(+), 441 deletions(-)

diff --git a/wallet/build.gradle b/wallet/build.gradle @@ -162,10 +162,9 @@ dependencies { implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' // Markdown rendering - final def markwon_version = '4.6.2' - implementation "io.noties.markwon:core:$markwon_version" - implementation "io.noties.markwon:ext-tables:$markwon_version" - implementation "io.noties.markwon:recycler:$markwon_version" + final def markdown_version = "0.39.2" + implementation("com.mikepenz:multiplatform-markdown-renderer:$markdown_version") + implementation("com.mikepenz:multiplatform-markdown-renderer-m3:$markdown_version") // Java Native Access (must always match JNA in qtart) implementation "net.java.dev.jna:jna:5.17.0@aar" diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -36,7 +36,6 @@ import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.balances.GetCurrencySpecificationResponse import net.taler.wallet.balances.ScopeInfo -import net.taler.wallet.withdraw.TosResponse import org.json.JSONObject @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt @@ -57,4 +57,13 @@ enum class ExchangeTosStatus { MissingTos; fun isAccepted() = this in listOf(Accepted, MissingTos) -} -\ No newline at end of file +} + +@Serializable +data class TosResponse( + val status: ExchangeTosStatus = ExchangeTosStatus.Unknown, + val content: String, + val currentEtag: String, + val contentLanguage: String? = null, + val tosAvailableLanguages: List<String> = emptyList(), +) +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt @@ -1,6 +1,6 @@ /* * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. + * (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 @@ -18,185 +18,218 @@ package net.taler.wallet.withdraw import android.os.Bundle import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup -import android.view.ViewGroup.MarginLayoutParams -import android.widget.AdapterView -import android.widget.ArrayAdapter -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.marginBottom -import androidx.core.view.marginLeft -import androidx.core.view.marginRight -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +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 androidx.navigation.fragment.findNavController -import io.noties.markwon.Markwon +import androidx.navigation.findNavController +import com.mikepenz.markdown.m3.Markdown +import com.mikepenz.markdown.m3.markdownTypography +import com.mikepenz.markdown.model.markdownPadding import kotlinx.coroutines.launch -import net.taler.common.fadeIn -import net.taler.common.fadeOut -import net.taler.wallet.main.MainViewModel import net.taler.wallet.R -import net.taler.wallet.databinding.FragmentReviewExchangeTosBinding +import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.compose.BottomButtonBox +import net.taler.wallet.compose.EmptyComposable +import net.taler.wallet.compose.ErrorComposable +import net.taler.wallet.compose.LoadingScreen +import net.taler.wallet.compose.TalerSurface import net.taler.wallet.exchanges.ExchangeTosStatus -import java.text.ParseException +import net.taler.wallet.exchanges.TosResponse +import net.taler.wallet.main.MainViewModel +import net.taler.wallet.systemBarsPaddingBottom import java.util.Locale -class ReviewExchangeTosFragment : Fragment(), AdapterView.OnItemSelectedListener { - +class ReviewExchangeTosFragment : Fragment() { private val model: MainViewModel by activityViewModels() private val exchangeManager by lazy { model.exchangeManager } - private lateinit var ui: FragmentReviewExchangeTosBinding - private val markwon by lazy { Markwon.builder(requireContext()).build() } - private val adapter by lazy { TosAdapter(markwon) } - - private var tos: TosResponse? = null - private var exchangeBaseUrl: String? = null - private var langAdapter: ArrayAdapter<String>? = null - private var selectedLang: String? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - ui = FragmentReviewExchangeTosBinding.inflate(inflater, container, false) - return ui.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupInsets() - - exchangeBaseUrl = arguments?.getString("exchangeBaseUrl") - ?: error("no exchangeBaseUrl passed") - val readOnly = arguments?.getBoolean("readOnly") ?: false - - langAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item) - langAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - ui.langSpinner.adapter = langAdapter - ui.langSpinner.onItemSelectedListener = this - - ui.buttonCard.visibility = if (readOnly) GONE else VISIBLE - ui.acceptTosCheckBox.isChecked = false - ui.acceptTosCheckBox.setOnCheckedChangeListener { _, _ -> - tos?.let { - viewLifecycleOwner.lifecycleScope.launch { - if (exchangeManager.acceptCurrentTos( - exchangeBaseUrl = exchangeBaseUrl!!, - currentEtag = it.currentEtag, - )) { - findNavController().navigateUp() - } - } + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + setContent { + val exchangeBaseUrl = arguments + ?.getString("exchangeBaseUrl") + ?: error("no exchangeBaseUrl passed") + val readOnly = arguments + ?.getBoolean("readOnly") + ?: false + + var tos: TosResponse? by remember { mutableStateOf(null) } + var selectedLang by remember { mutableStateOf(Locale.getDefault().language) } + + LaunchedEffect(selectedLang) { + tos = null + tos = model.exchangeManager.getExchangeTos(exchangeBaseUrl, selectedLang) } - } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - renderTos(exchangeBaseUrl!!, selectedLang) + TalerSurface { + tos?.let { tos -> + ReviewExchangeTosComposable(tos, + readOnly = readOnly, + onSelectLang = { selectedLang = it }, + onAcceptTos = { + viewLifecycleOwner.lifecycleScope.launch { + if (exchangeManager.acceptCurrentTos( + exchangeBaseUrl = exchangeBaseUrl, + currentEtag = tos.currentEtag, + )) { + findNavController().navigateUp() + } + } + }, + ) + } ?: LoadingScreen() } } } +} - private suspend fun renderTos( - exchangeBaseUrl: String, - language: String? = null, - ) { - val lc = Locale.getDefault().language - selectedLang = language ?: lc - tos = exchangeManager.getExchangeTos(exchangeBaseUrl, selectedLang) - - val tos = tos - if (tos == null || tos.status == ExchangeTosStatus.MissingTos) { - onTosError(getString(R.string.exchange_tos_missing)) - return - } - - // Setup language adapter - val languages = tos.tosAvailableLanguages - langAdapter?.clear() - langAdapter?.addAll(languages.map { lang -> - Locale(lang).displayLanguage - }) - langAdapter?.notifyDataSetChanged() - - // Setup language spinner - if (languages.size > 1) { - ui.langSpinner.visibility = VISIBLE - val i = languages.indexOf(selectedLang) - if (i >= 0) { - ui.langSpinner.setSelection(i) - } - } else { - ui.langSpinner.visibility = GONE - } - - val sections = try { - parseTos(markwon, tos.content) - } catch (e: ParseException) { - onTosError(e.message ?: "Unknown Error") - return - } - - adapter.setSections(sections) - ui.tosList.adapter = adapter - ui.tosList.fadeIn() - - ui.acceptTosCheckBox.fadeIn() - ui.progressBar.fadeOut() +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReviewExchangeTosComposable( + tos: TosResponse, + readOnly: Boolean, + onSelectLang: (String) -> Unit, + onAcceptTos: () -> Unit, +) { + if (tos.status == ExchangeTosStatus.MissingTos) { + EmptyComposable(stringResource(R.string.exchange_tos_missing)) + return } - private fun setupInsets() { - ViewCompat.setOnApplyWindowInsetsListener(ui.tosList) { v, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.updatePadding( - left = insets.left, - right = insets.right, - bottom = insets.bottom, - ) - windowInsets - } - - val checkboxMarginLeft = ui.acceptTosCheckBox.marginLeft - val checkboxMarginRight = ui.acceptTosCheckBox.marginRight - val checkboxMarginBottom = ui.acceptTosCheckBox.marginBottom - ViewCompat.setOnApplyWindowInsetsListener(ui.acceptTosCheckBox) { v, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.updateLayoutParams<MarginLayoutParams> { - leftMargin = checkboxMarginLeft + insets.left - rightMargin = checkboxMarginRight + insets.right - bottomMargin = checkboxMarginBottom + insets.bottom + var expanded by remember { mutableStateOf(false) } + + Scaffold( + bottomBar = { + if (!readOnly) BottomButtonBox { + Button( + modifier = Modifier + .systemBarsPaddingBottom(), + onClick = onAcceptTos, + ) { + Text(stringResource(R.string.exchange_tos_accept)) + } + } + }, + contentWindowInsets = WindowInsets.systemBars.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom + ) + ) { innerPadding -> + LazyColumn(Modifier.padding(innerPadding)) { + if (tos.tosAvailableLanguages.size > 1) item { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + OutlinedTextField( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .clickable { expanded = true }, + label = { Text(stringResource(R.string.language)) }, + value = tos.contentLanguage?.let { + Locale(it).displayLanguage + } ?: "", + onValueChange = {}, + readOnly = true, + enabled = false, + singleLine = true, + textStyle = LocalTextStyle.current.copy( // show text as if not disabled + color = MaterialTheme.colorScheme.onSurface, + ), + ) + + ExposedDropdownMenu ( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + tos.tosAvailableLanguages.forEach { + DropdownMenuItem( + { Text("${Locale(it).displayLanguage}") }, + onClick = { + onSelectLang(it) + expanded = false + } + ) + } + } + } } - windowInsets - } - } - - private fun onTosError(msg: String) { - ui.tosList.fadeIn() - ui.progressBar.fadeOut() - ui.acceptTosCheckBox.fadeIn() - // ui.buttonCard.fadeOut() - ui.errorView.text = getString(R.string.exchange_tos_error, "\n\n$msg") - ui.errorView.fadeIn() - } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - tos?.tosAvailableLanguages?.get(position)?.let { lang -> - viewLifecycleOwner.lifecycleScope.launch { - renderTos(exchangeBaseUrl!!, lang) + item { + Markdown( + content = tos.content.trimIndent(), + modifier = Modifier.padding(16.dp), + typography = markdownTypography( + h1 = MaterialTheme.typography.headlineLarge, + h2 = MaterialTheme.typography.headlineMedium, + h3 = MaterialTheme.typography.headlineSmall, + h4 = MaterialTheme.typography.titleLarge, + h5 = MaterialTheme.typography.titleMedium, + h6 = MaterialTheme.typography.titleSmall, + text = MaterialTheme.typography.bodyMedium, + paragraph = MaterialTheme.typography.bodyMedium, + ), + padding = markdownPadding( + block = 5.dp, + ), + error = { modifier -> + ErrorComposable( + TalerErrorInfo.makeCustomError( + stringResource(R.string.exchange_tos_error, "")), + modifier = modifier, + devMode = false, + ) + }, + ) } } } - - override fun onNothingSelected(parent: AdapterView<*>?) {} - } + +@Preview +@Composable +fun ReviewExchangeTosComposablePreview() { + TalerSurface { + val tos = TosResponse( + status = ExchangeTosStatus.Proposed, + content = "# Terms of service\nThis is a terms of service, obviously.\n## H2\n### H3\n#### H4\n##### H5\n###### H6", + currentEtag = "1.2.0", + contentLanguage = "en", + tosAvailableLanguages = listOf("en", "en_US"), + ) + + ReviewExchangeTosComposable(tos, false, {}, {}) + } +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TosAdapter.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TosAdapter.kt @@ -1,94 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 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.withdraw - -import android.transition.ChangeBounds -import android.transition.TransitionManager.beginDelayedTransition -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import io.noties.markwon.Markwon -import net.taler.wallet.R - -class TosAdapter( - private val markwon: Markwon -) : RecyclerView.Adapter<TosAdapter.TosSectionViewHolder>() { - - private val items = ArrayList<TosSection>() - - init { - setHasStableIds(true) - } - - override fun getItemCount() = items.size - - override fun getItemId(position: Int): Long { - return items[position].node.hashCode().toLong() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TosSectionViewHolder { - val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_tos, parent, false) - return TosSectionViewHolder(v) - } - - override fun onBindViewHolder(holder: TosSectionViewHolder, position: Int) { - holder.bind(items[position]) - } - - fun setSections(sections: List<TosSection>) { - items.clear() - items.addAll(sections) - notifyDataSetChanged() - } - - inner class TosSectionViewHolder(private val v: View) : RecyclerView.ViewHolder(v) { - private val sectionTitle: TextView = v.findViewById(R.id.sectionTitle) - private val expandButton: ImageView = v.findViewById(R.id.expandButton) - private val sectionText: TextView = v.findViewById(R.id.sectionText) - - fun bind(item: TosSection) { - sectionTitle.text = item.title - ?: v.context.getString(R.string.exchange_tos) - showSection(item, item.expanded) - val onClickListener = View.OnClickListener { - val transition = ChangeBounds() - transition.duration = 200L - if (!item.expanded) beginDelayedTransition(v as ViewGroup, transition) - item.expanded = !item.expanded - showSection(item, item.expanded) - } - sectionTitle.setOnClickListener(onClickListener) - } - - private fun showSection(item: TosSection, show: Boolean) { - if (show) { - expandButton.setImageResource(R.drawable.ic_keyboard_arrow_up) - markwon.setParsedMarkdown(sectionText, markwon.render(item.node)) - sectionText.visibility = VISIBLE - } else { - expandButton.setImageResource(R.drawable.ic_keyboard_arrow_down) - sectionText.visibility = GONE - } - } - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TosSection.kt @@ -1,91 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 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.withdraw - -import io.noties.markwon.Markwon -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import net.taler.wallet.exchanges.ExchangeTosStatus -import org.commonmark.node.Code -import org.commonmark.node.Document -import org.commonmark.node.Heading -import org.commonmark.node.Node -import org.commonmark.node.Text -import java.text.ParseException - -data class TosSection( - val title: String?, - val node: Node, - var expanded: Boolean = false -) - -@Throws(ParseException::class) -internal fun parseTos(markwon: Markwon, text: String): List<TosSection> { - val rootNode: Node = markwon.parse(text) - var node: Node? = rootNode.firstChild - ?: throw ParseException("Invalid markdown", 0) - var lastHeading: String? = null - var section = Document() - val sections = ArrayList<TosSection>() - while (node != null) { - val next: Node? = node.next - // TODO: better sectioning logic! level 1+2 is a hack - if (node is Heading && (node.level == 1 || node.level == 2)) { - // if lastHeading exists, close previous section - if (lastHeading != null) { - sections.add(TosSection(lastHeading, section)) - section = Document() - } - // start new section with new heading (stripped of markup) - lastHeading = getNodeText(node) - if (lastHeading.isBlank()) { - return listOf(TosSection(null, rootNode, true)) - } - } else if (lastHeading == null) { - return listOf(TosSection(null, rootNode, true)) - } else { - section.appendChild(node) - } - node = next - } - check(lastHeading != null) - sections.add(TosSection(lastHeading, section)) - return sections -} - -private fun getNodeText(rootNode: Node): String { - var node: Node? = rootNode.firstChild - var text = "" - while (node != null) { - text += when (node) { - is Text -> node.literal - is Code -> node.literal - else -> getNodeText(node) - } - node = node.next - } - return text -} - -@Serializable -data class TosResponse( - val status: ExchangeTosStatus = ExchangeTosStatus.Unknown, - val content: String, - val currentEtag: String, - val contentLanguage: String? = null, - val tosAvailableLanguages: List<String> = emptyList(), -) diff --git a/wallet/src/main/res/layout/fragment_review_exchange_tos.xml b/wallet/src/main/res/layout/fragment_review_exchange_tos.xml @@ -1,95 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ This file is part of GNU Taler - ~ (C) 2020 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" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".withdraw.ReviewExchangeTosFragment"> - - <Spinner - android:id="@+id/langSpinner" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="10dp" - app:layout_constraintHorizontal_bias="1" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - android:visibility="gone" - tools:visibility="visible"/> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/tosList" - android:layout_width="0dp" - android:layout_height="0dp" - app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toTopOf="@+id/buttonCard" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/langSpinner" - android:clipToPadding="false" - tools:listitem="@layout/list_item_tos" /> - - <ProgressBar - android:id="@+id/progressBar" - style="?android:attr/progressBarStyleLarge" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:layout_constraintBottom_toBottomOf="@+id/tosList" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/errorView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:gravity="center" - android:textColor="?colorError" - android:textSize="16sp" - android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:text="@string/exchange_tos_error" - tools:visibility="visible" /> - - <com.google.android.material.card.MaterialCardView - android:id="@+id/buttonCard" - style="@style/BottomCard" - android:layout_width="0dp" - android:layout_height="wrap_content" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent"> - - <CheckBox - android:id="@+id/acceptTosCheckBox" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:text="@string/exchange_tos_accept" - android:visibility="invisible" - tools:visibility="visible" /> - - </com.google.android.material.card.MaterialCardView> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -67,6 +67,7 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish <string name="error">Error</string> <string name="error_export">Export error diagnostics</string> <string name="import_db">Import</string> + <string name="language">Language</string> <string name="loading">Loading</string> <string name="menu">Menu</string> <string name="millisecond">%1$d ms</string>