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 }