taler-android

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

ReviewExchangeTosFragment.kt (9361B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2026 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.wallet.withdraw
     18 
     19 import android.os.Bundle
     20 import android.view.LayoutInflater
     21 import android.view.ViewGroup
     22 import androidx.compose.foundation.clickable
     23 import androidx.compose.foundation.layout.WindowInsets
     24 import androidx.compose.foundation.layout.WindowInsetsSides
     25 import androidx.compose.foundation.layout.fillMaxWidth
     26 import androidx.compose.foundation.layout.only
     27 import androidx.compose.foundation.layout.padding
     28 import androidx.compose.foundation.layout.systemBars
     29 import androidx.compose.foundation.lazy.LazyColumn
     30 import androidx.compose.material3.Button
     31 import androidx.compose.material3.DropdownMenuItem
     32 import androidx.compose.material3.ExperimentalMaterial3Api
     33 import androidx.compose.material3.ExposedDropdownMenuBox
     34 import androidx.compose.material3.LocalTextStyle
     35 import androidx.compose.material3.MaterialTheme
     36 import androidx.compose.material3.OutlinedTextField
     37 import androidx.compose.material3.Scaffold
     38 import androidx.compose.material3.Text
     39 import androidx.compose.runtime.Composable
     40 import androidx.compose.runtime.LaunchedEffect
     41 import androidx.compose.runtime.getValue
     42 import androidx.compose.runtime.mutableStateOf
     43 import androidx.compose.runtime.remember
     44 import androidx.compose.runtime.setValue
     45 import androidx.compose.ui.Modifier
     46 import androidx.compose.ui.platform.ComposeView
     47 import androidx.compose.ui.res.stringResource
     48 import androidx.compose.ui.tooling.preview.Preview
     49 import androidx.compose.ui.unit.dp
     50 import androidx.fragment.app.Fragment
     51 import androidx.fragment.app.activityViewModels
     52 import androidx.lifecycle.lifecycleScope
     53 import androidx.navigation.findNavController
     54 import com.mikepenz.markdown.m3.Markdown
     55 import com.mikepenz.markdown.m3.markdownTypography
     56 import com.mikepenz.markdown.model.markdownPadding
     57 import kotlinx.coroutines.launch
     58 import net.taler.wallet.R
     59 import net.taler.wallet.backend.TalerErrorInfo
     60 import net.taler.wallet.compose.BottomButtonBox
     61 import net.taler.wallet.compose.EmptyComposable
     62 import net.taler.wallet.compose.ErrorComposable
     63 import net.taler.wallet.compose.LoadingScreen
     64 import net.taler.wallet.compose.TalerSurface
     65 import net.taler.wallet.exchanges.ExchangeTosStatus
     66 import net.taler.wallet.exchanges.TosResponse
     67 import net.taler.wallet.main.MainViewModel
     68 import net.taler.wallet.systemBarsPaddingBottom
     69 import java.util.Locale
     70 
     71 class ReviewExchangeTosFragment : Fragment() {
     72     private val model: MainViewModel by activityViewModels()
     73     private val exchangeManager by lazy { model.exchangeManager }
     74 
     75     override fun onCreateView(
     76         inflater: LayoutInflater,
     77         container: ViewGroup?,
     78         savedInstanceState: Bundle?
     79     ) = ComposeView(requireContext()).apply {
     80         setContent {
     81             val exchangeBaseUrl = arguments
     82                 ?.getString("exchangeBaseUrl")
     83                 ?: error("no exchangeBaseUrl passed")
     84             val readOnly = arguments
     85                 ?.getBoolean("readOnly")
     86                 ?: false
     87 
     88             var tos: TosResponse? by remember { mutableStateOf(null) }
     89             var selectedLang by remember { mutableStateOf(Locale.getDefault().language) }
     90 
     91             LaunchedEffect(selectedLang) {
     92                 tos = null
     93                 tos = model.exchangeManager.getExchangeTos(exchangeBaseUrl, selectedLang)
     94             }
     95 
     96             TalerSurface {
     97                 tos?.let { tos ->
     98                     ReviewExchangeTosComposable(tos,
     99                         readOnly = readOnly,
    100                         onSelectLang = { selectedLang = it },
    101                         onAcceptTos = {
    102                             viewLifecycleOwner.lifecycleScope.launch {
    103                                 if (exchangeManager.acceptCurrentTos(
    104                                         exchangeBaseUrl = exchangeBaseUrl,
    105                                         currentEtag = tos.currentEtag,
    106                                     )) {
    107                                     findNavController().navigateUp()
    108                                 }
    109                             }
    110                         },
    111                     )
    112                 } ?: LoadingScreen()
    113             }
    114         }
    115     }
    116 }
    117 
    118 @OptIn(ExperimentalMaterial3Api::class)
    119 @Composable
    120 fun ReviewExchangeTosComposable(
    121     tos: TosResponse,
    122     readOnly: Boolean,
    123     onSelectLang: (String) -> Unit,
    124     onAcceptTos: () -> Unit,
    125 ) {
    126     if (tos.status == ExchangeTosStatus.MissingTos) {
    127         EmptyComposable(stringResource(R.string.exchange_tos_missing))
    128         return
    129     }
    130 
    131     var expanded by remember { mutableStateOf(false) }
    132 
    133     Scaffold(
    134         bottomBar = {
    135             if (!readOnly) BottomButtonBox {
    136                 Button(
    137                     modifier = Modifier
    138                         .systemBarsPaddingBottom(),
    139                     onClick = onAcceptTos,
    140                 ) {
    141                     Text(stringResource(R.string.exchange_tos_accept))
    142                 }
    143             }
    144         },
    145         contentWindowInsets = WindowInsets.systemBars.only(
    146             WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
    147         )
    148     ) { innerPadding ->
    149         LazyColumn(Modifier.padding(innerPadding)) {
    150             if (tos.tosAvailableLanguages.size > 1) item {
    151                 ExposedDropdownMenuBox(
    152                     expanded = expanded,
    153                     onExpandedChange = { expanded = it },
    154                 ) {
    155                     OutlinedTextField(
    156                         modifier = Modifier
    157                             .padding(horizontal = 16.dp)
    158                             .fillMaxWidth()
    159                             .clickable { expanded = true },
    160                         label = { Text(stringResource(R.string.language)) },
    161                         value = tos.contentLanguage?.let {
    162                             Locale(it).displayLanguage
    163                         } ?: "",
    164                         onValueChange = {},
    165                         readOnly = true,
    166                         enabled = false,
    167                         singleLine = true,
    168                         textStyle = LocalTextStyle.current.copy( // show text as if not disabled
    169                             color = MaterialTheme.colorScheme.onSurface,
    170                         ),
    171                     )
    172 
    173                     ExposedDropdownMenu (
    174                         expanded = expanded,
    175                         onDismissRequest = { expanded = false }
    176                     ) {
    177                         tos.tosAvailableLanguages.forEach {
    178                             DropdownMenuItem(
    179                                 { Text("${Locale(it).displayLanguage}") },
    180                                 onClick = {
    181                                     onSelectLang(it)
    182                                     expanded = false
    183                                 }
    184                             )
    185                         }
    186                     }
    187                 }
    188             }
    189 
    190             item {
    191                 Markdown(
    192                     content = tos.content.trimIndent(),
    193                     modifier = Modifier.padding(16.dp),
    194                     typography = markdownTypography(
    195                         h1 = MaterialTheme.typography.headlineLarge,
    196                         h2 = MaterialTheme.typography.headlineMedium,
    197                         h3 = MaterialTheme.typography.headlineSmall,
    198                         h4 = MaterialTheme.typography.titleLarge,
    199                         h5 = MaterialTheme.typography.titleMedium,
    200                         h6 = MaterialTheme.typography.titleSmall,
    201                         text = MaterialTheme.typography.bodyMedium,
    202                         paragraph = MaterialTheme.typography.bodyMedium,
    203                     ),
    204                     padding = markdownPadding(
    205                         block = 5.dp,
    206                     ),
    207                     error = { modifier ->
    208                         ErrorComposable(
    209                             TalerErrorInfo.makeCustomError(
    210                                 stringResource(R.string.exchange_tos_error, "")),
    211                             modifier = modifier,
    212                             devMode = false,
    213                         )
    214                     },
    215                 )
    216             }
    217         }
    218     }
    219 }
    220 
    221 @Preview
    222 @Composable
    223 fun ReviewExchangeTosComposablePreview() {
    224     TalerSurface {
    225         val tos = TosResponse(
    226             status = ExchangeTosStatus.Proposed,
    227             content = "# Terms of service\nThis is a terms of service, obviously.\n## H2\n### H3\n#### H4\n##### H5\n###### H6",
    228             currentEtag = "1.2.0",
    229             contentLanguage = "en",
    230             tosAvailableLanguages = listOf("en", "en_US"),
    231         )
    232 
    233         ReviewExchangeTosComposable(tos, false, {}, {})
    234     }
    235 }