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