AddAccountComposable.kt (12921B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2023 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.accounts 18 19 import androidx.compose.foundation.layout.Spacer 20 import androidx.compose.foundation.layout.fillMaxSize 21 import androidx.compose.foundation.layout.fillMaxWidth 22 import androidx.compose.foundation.layout.imePadding 23 import androidx.compose.foundation.layout.padding 24 import androidx.compose.foundation.layout.size 25 import androidx.compose.foundation.lazy.LazyColumn 26 import androidx.compose.foundation.text.KeyboardOptions 27 import androidx.compose.material.icons.Icons 28 import androidx.compose.material.icons.filled.Add 29 import androidx.compose.material.icons.filled.Edit 30 import androidx.compose.material3.Button 31 import androidx.compose.material3.ButtonDefaults 32 import androidx.compose.material3.Icon 33 import androidx.compose.material3.OutlinedTextField 34 import androidx.compose.material3.ScrollableTabRow 35 import androidx.compose.material3.Surface 36 import androidx.compose.material3.Tab 37 import androidx.compose.material3.Text 38 import androidx.compose.runtime.Composable 39 import androidx.compose.runtime.getValue 40 import androidx.compose.runtime.mutableStateOf 41 import androidx.compose.runtime.remember 42 import androidx.compose.runtime.saveable.rememberSaveable 43 import androidx.compose.runtime.setValue 44 import androidx.compose.ui.Alignment.Companion.CenterHorizontally 45 import androidx.compose.ui.Modifier 46 import androidx.compose.ui.platform.LocalFocusManager 47 import androidx.compose.ui.res.stringResource 48 import androidx.compose.ui.text.input.ImeAction 49 import androidx.compose.ui.tooling.preview.Preview 50 import androidx.compose.ui.unit.dp 51 import net.taler.wallet.BottomInsetsSpacer 52 import net.taler.wallet.R 53 import net.taler.wallet.backend.TalerErrorInfo 54 import net.taler.wallet.compose.ErrorComposable 55 import net.taler.wallet.compose.WarningLabel 56 import net.taler.wallet.deposit.AddAccountBitcoin 57 import net.taler.wallet.deposit.AddAccountIBAN 58 import net.taler.wallet.deposit.AddAccountTaler 59 import net.taler.wallet.deposit.GetDepositWireTypesResponse 60 import net.taler.wallet.deposit.WireType 61 import net.taler.wallet.deposit.WireTypeDetails 62 import net.taler.wallet.deposit.getBitcoinPayto 63 import net.taler.wallet.deposit.getIbanPayto 64 import net.taler.wallet.deposit.getTalerPayto 65 import net.taler.wallet.useDebounce 66 67 @Composable 68 fun AddAccountComposable( 69 modifier: Modifier = Modifier, 70 presetAccount: KnownBankAccountInfo? = null, 71 depositWireTypes: GetDepositWireTypesResponse, 72 validateIban: suspend (iban: String) -> Boolean, 73 onSubmit: (paytoUri: String, label: String) -> Unit, 74 onClose: () -> Unit, 75 ) { 76 val focusManager = LocalFocusManager.current 77 val supportedWireTypes = remember(depositWireTypes) { depositWireTypes.wireTypes } 78 val talerBankHostnames = remember(depositWireTypes) { depositWireTypes.hostNames } 79 val presetPaytoUri = remember(presetAccount) { presetAccount?.let { PaytoUri.parse(it.paytoUri) } } 80 81 if (supportedWireTypes.isEmpty()) { 82 return ErrorComposable( 83 error = TalerErrorInfo.makeCustomError( 84 stringResource(R.string.send_deposit_no_methods_error)), 85 modifier = Modifier.fillMaxSize(), 86 devMode = false, 87 onClose = onClose, 88 ) 89 } 90 91 var selectedWireType by remember(supportedWireTypes, presetAccount) { 92 if (presetAccount == null) { 93 return@remember mutableStateOf(supportedWireTypes.firstOrNull()) 94 } 95 96 val parsed = PaytoUri.parse(presetAccount.paytoUri) 97 mutableStateOf(when (parsed) { 98 is PaytoUriIban -> WireType.IBAN 99 is PaytoUriTalerBank -> WireType.TalerBank 100 is PaytoUriBitcoin -> WireType.Bitcoin 101 else -> supportedWireTypes.firstOrNull() 102 }) 103 } 104 105 // payto:// stuff 106 var formError by rememberSaveable { mutableStateOf(false) } // TODO: do an initial validation! 107 var ibanError by rememberSaveable(presetPaytoUri) { mutableStateOf(presetPaytoUri == null) } 108 var formAlias by rememberSaveable(presetAccount) { mutableStateOf(presetAccount?.label ?: "") } 109 var ibanName by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.receiverName ?: "") } 110 var ibanTown by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.receiverTown) } 111 var ibanZip by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.receiverPostalCode) } 112 var ibanIban by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriIban)?.iban ?: "") } 113 var talerName by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriTalerBank)?.receiverName ?: "") } 114 var talerHost by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriTalerBank)?.host ?: talerBankHostnames.firstOrNull() ?: "") } 115 var talerAccount by rememberSaveable(presetPaytoUri) { mutableStateOf((presetPaytoUri as? PaytoUriTalerBank)?.account ?: "") } 116 var bitcoinAddress by rememberSaveable(presetPaytoUri) { mutableStateOf("") } // TODO: fill-in bitcoin address 117 118 val paytoUri = when(selectedWireType) { 119 WireType.IBAN -> getIbanPayto( 120 ibanName.trim(), 121 ibanZip?.trim(), 122 ibanTown?.trim(), 123 ibanIban.trim() 124 ) 125 WireType.TalerBank -> getTalerPayto(talerName.trim(), talerHost.trim(), talerAccount.trim()) 126 WireType.Bitcoin -> getBitcoinPayto(bitcoinAddress.trim()) 127 else -> null 128 } 129 130 // form validation 131 paytoUri.useDebounce { 132 formError = when (selectedWireType) { 133 WireType.IBAN -> { 134 val valid = validateIban(ibanIban) 135 ibanError = !valid || ibanIban.isBlank() 136 !valid || ibanName.isBlank() 137 } 138 139 WireType.TalerBank -> { 140 talerName.isBlank() 141 || talerHost.isBlank() 142 || talerAccount.isBlank() 143 } 144 145 WireType.Bitcoin -> { 146 bitcoinAddress.isBlank() 147 } 148 149 else -> true 150 } 151 } 152 153 LazyColumn( 154 modifier = modifier 155 .fillMaxSize() 156 .imePadding(), 157 horizontalAlignment = CenterHorizontally, 158 ) { 159 // Do not show chooser when editing account 160 if (presetAccount == null 161 && selectedWireType != null 162 && supportedWireTypes.size > 1) { 163 item { 164 MakeDepositWireTypeChooser( 165 supportedWireTypes = supportedWireTypes, 166 selectedWireType = selectedWireType!!, 167 onSelectWireType = { 168 selectedWireType = it 169 } 170 ) 171 } 172 } 173 174 item { 175 WarningLabel( 176 modifier = Modifier.padding(16.dp), 177 label = stringResource(R.string.send_deposit_account_warning), 178 ) 179 } 180 181 when(selectedWireType) { 182 WireType.IBAN -> item { 183 AddAccountIBAN( 184 name = ibanName, 185 town = ibanTown, 186 zip = ibanZip, 187 iban = ibanIban, 188 ibanError = ibanError, 189 onFormEdited = { name, town, zip, iban -> 190 ibanName = name 191 ibanTown = town 192 ibanZip = zip 193 ibanIban = iban 194 } 195 ) 196 } 197 198 WireType.TalerBank -> item { 199 AddAccountTaler( 200 name = talerName, 201 host = talerHost, 202 account = talerAccount, 203 supportedHosts = talerBankHostnames, 204 onFormEdited = { name, host, account -> 205 talerName = name 206 talerHost = host 207 talerAccount = account 208 } 209 ) 210 } 211 212 WireType.Bitcoin -> item { 213 AddAccountBitcoin( 214 bitcoinAddress = bitcoinAddress, 215 onFormEdited = { address -> 216 bitcoinAddress = address 217 } 218 ) 219 } 220 221 else -> {} 222 } 223 224 item { 225 OutlinedTextField( 226 modifier = Modifier 227 .padding(horizontal = 16.dp) 228 .fillMaxWidth(), 229 value = formAlias, 230 onValueChange = { 231 formAlias = it 232 }, 233 label = { 234 Text(stringResource(R.string.send_deposit_account_note)) 235 }, 236 singleLine = true, 237 isError = formAlias.isBlank(), 238 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), 239 ) 240 } 241 242 item { 243 Button( 244 modifier = Modifier.padding(16.dp), 245 enabled = !formError && formAlias.isNotBlank(), 246 onClick = { 247 focusManager.clearFocus() 248 if (paytoUri != null && formAlias.isNotEmpty()) { 249 onSubmit(paytoUri, formAlias) 250 } 251 }, 252 ) { 253 Icon( 254 if (presetAccount == null) { 255 Icons.Default.Add 256 } else { 257 Icons.Default.Edit 258 }, 259 contentDescription = null, 260 modifier = Modifier.size(ButtonDefaults.IconSize), 261 ) 262 263 Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 264 265 if (presetAccount == null) { 266 Text(stringResource(R.string.send_deposit_account_add)) 267 } else { 268 Text(stringResource(R.string.send_deposit_account_edit)) 269 } 270 } 271 } 272 273 item { 274 BottomInsetsSpacer() 275 } 276 } 277 } 278 279 @Composable 280 fun MakeDepositWireTypeChooser( 281 modifier: Modifier = Modifier, 282 supportedWireTypes: List<WireType>, 283 selectedWireType: WireType, 284 onSelectWireType: (wireType: WireType) -> Unit, 285 ) { 286 val selectedIndex = supportedWireTypes.indexOfFirst { 287 it == selectedWireType 288 } 289 290 ScrollableTabRow( 291 selectedTabIndex = selectedIndex, 292 modifier = modifier, 293 edgePadding = 8.dp, 294 ) { 295 supportedWireTypes.forEach { wireType -> 296 if (wireType != WireType.Unknown) { 297 Tab( 298 selected = selectedWireType == wireType, 299 onClick = { onSelectWireType(wireType) }, 300 text = { 301 Text(when(wireType) { 302 WireType.IBAN -> stringResource(R.string.send_deposit_iban) 303 WireType.TalerBank -> stringResource(R.string.send_deposit_taler) 304 WireType.Bitcoin -> stringResource(R.string.send_deposit_bitcoin) 305 else -> error("unknown method") 306 }) 307 } 308 ) 309 } 310 } 311 } 312 } 313 314 @Preview 315 @Composable 316 fun PreviewAddAccountComposable() { 317 Surface { 318 AddAccountComposable( 319 modifier = Modifier, 320 depositWireTypes = GetDepositWireTypesResponse( 321 wireTypeDetails = listOf( 322 WireTypeDetails( 323 paymentTargetType = WireType.IBAN, 324 talerBankHostnames = listOf("bank.test.taler.net") 325 ), 326 WireTypeDetails( 327 paymentTargetType = WireType.TalerBank, 328 talerBankHostnames = listOf("bank.test.taler.net") 329 ), 330 WireTypeDetails( 331 paymentTargetType = WireType.Bitcoin, 332 talerBankHostnames = emptyList(), 333 ) 334 ), 335 ), 336 validateIban = { true }, 337 onSubmit = { _, _ -> }, 338 onClose = {}, 339 ) 340 } 341 }