diff options
author | Iván Ávalos <avalos@disroot.org> | 2023-07-31 18:35:02 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2023-11-11 13:20:09 -0600 |
commit | de66e6697a006d85df54c79e740798ecd56bb683 (patch) | |
tree | d0d38bae034866b31ceec2a86175142997c3abac | |
parent | fb80fc4d9636c957ba4f17a5d57aee3fccd494a1 (diff) | |
download | taler-android-de66e6697a006d85df54c79e740798ecd56bb683.tar.gz taler-android-de66e6697a006d85df54c79e740798ecd56bb683.tar.bz2 taler-android-de66e6697a006d85df54c79e740798ecd56bb683.zip |
Attributes validation + basic error handling + improvements!
Signed-off-by: Iván Ávalos <avalos@disroot.org>
17 files changed, 333 insertions, 106 deletions
diff --git a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt index 8d6a8ad..653d8a5 100644 --- a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt +++ b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt @@ -20,6 +20,7 @@ import net.taler.anastasis.ui.backup.SelectAuthMethodsScreen import net.taler.anastasis.ui.common.SelectContinentScreen import net.taler.anastasis.ui.common.SelectCountryScreen import net.taler.anastasis.ui.common.SelectUserAttributesScreen +import net.taler.anastasis.ui.dialogs.ErrorDialog import net.taler.anastasis.ui.home.HomeScreen import net.taler.anastasis.ui.theme.AnastasisTheme import net.taler.anastasis.viewmodels.ReducerViewModel @@ -54,6 +55,13 @@ class MainActivity : ComponentActivity() { fun MainNavHost( viewModel: ReducerViewModel = hiltViewModel(), ) { + val error by viewModel.reducerError.collectAsState() + error?.let { + ErrorDialog(error = it) { + viewModel.cleanError() + } + } + val navRoute by viewModel.navRoute.collectAsState() when (navRoute) { Routes.Home.route -> { diff --git a/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt b/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt index c179d8f..9070490 100644 --- a/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt +++ b/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt @@ -22,10 +22,11 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.decodeFromJsonElement -import net.taler.anastasis.Utils.encodeToNativeJson +import net.taler.anastasis.shared.Utils.encodeToNativeJson import net.taler.anastasis.models.ReducerState import net.taler.common.ApiResponse import net.taler.common.ApiResponse.* +import net.taler.common.TalerErrorCode import net.taler.common.TalerErrorCode.NONE import org.json.JSONObject @@ -74,10 +75,24 @@ class AnastasisReducerApi() { try { when (val response = sendRequest("anastasisReduce", body)) { is Response -> { - val t = json.decodeFromJsonElement<ReducerState>(response.result) - WalletResponse.Success(t) + when (val t = json.decodeFromJsonElement<ReducerState>(response.result)) { + is ReducerState.Recovery, is ReducerState.Backup -> { + WalletResponse.Success(t) + } + is ReducerState.Error -> { + WalletResponse.Error( + TalerErrorInfo( + code = TalerErrorCode.fromInt(t.code), + hint = t.hint, + ) + ) + } + } + } + is Error -> { + val error: TalerErrorInfo = json.decodeFromJsonElement(response.error) + WalletResponse.Error(error) } - is Error -> error("invalid reducer response") } } catch (e: Exception) { val info = TalerErrorInfo(NONE, "", e.toString()) @@ -99,8 +114,19 @@ class AnastasisReducerApi() { try { when (val response = sendRequest("anastasisReduce", body)) { is Response -> { - val t = json.decodeFromJsonElement<ReducerState>(response.result) - WalletResponse.Success(t) + when (val t = json.decodeFromJsonElement<ReducerState>(response.result)) { + is ReducerState.Recovery, is ReducerState.Backup -> { + WalletResponse.Success(t) + } + is ReducerState.Error -> { + WalletResponse.Error( + TalerErrorInfo( + code = TalerErrorCode.fromInt(t.code), + hint = t.hint, + ) + ) + } + } } is Error -> error("invalid reducer response") } diff --git a/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt b/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt index c87e76d..58e0223 100644 --- a/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt +++ b/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt @@ -18,6 +18,7 @@ package net.taler.anastasis.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.taler.common.Timestamp @Serializable sealed class ReducerArgs { @@ -44,7 +45,17 @@ sealed class ReducerArgs { // TODO: ActionArgsEnterSecretName - // TODO: ActionArgsEnterSecret + @Serializable + data class EnterSecret( + val secret: Secret, + val expiration: Timestamp? = null, + ) { + @Serializable + data class Secret( + val value: String, + val mime: String? = null, + ) + } @Serializable data class SelectContinent( diff --git a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt index fd527f6..a074044 100644 --- a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt +++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt @@ -25,9 +25,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -import net.taler.anastasis.Utils -import net.taler.anastasis.Utils.encodeToNativeJson +import net.taler.anastasis.shared.Utils +import net.taler.anastasis.shared.Utils.encodeToNativeJson import net.taler.anastasis.backend.AnastasisReducerApi +import net.taler.anastasis.backend.TalerErrorInfo import net.taler.anastasis.models.AuthenticationProviderStatus import net.taler.anastasis.models.ContinentInfo import net.taler.anastasis.models.CountryInfo @@ -39,6 +40,7 @@ import kotlin.time.Duration.Companion.seconds class ReducerManager( private val state: MutableStateFlow<ReducerState?>, + private val error: MutableStateFlow<TalerErrorInfo?>, private val api: AnastasisReducerApi, private val scope: CoroutineScope, ) { @@ -50,6 +52,14 @@ class ReducerManager( // TODO: error handling! + private fun onSuccess(newState: ReducerState) { + state.value = newState + } + + private fun onError(info: TalerErrorInfo) { + error.value = info + } + fun startBackup() = scope.launch { state.value = api.startBackup() } @@ -61,18 +71,16 @@ class ReducerManager( fun back() = scope.launch { state.value?.let { initialState -> api.reduceAction(initialState, "back") - .onSuccess { newState -> - state.value = newState - } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } fun next() = scope.launch { state.value?.let { initialState -> api.reduceAction(initialState, "next") - .onSuccess { newState -> - state.value = newState - } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -80,9 +88,9 @@ class ReducerManager( state.value?.let { initialState -> api.reduceAction(initialState, "select_continent") { put("continent", continent.name) - }.onSuccess { newState -> - state.value = newState } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -92,9 +100,9 @@ class ReducerManager( put("country_code", country.code) // TODO: stop hardcoding currency! put("currency", "EUR") - }.onSuccess { newState -> - state.value = newState } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -102,9 +110,9 @@ class ReducerManager( state.value?.let { initialState -> api.reduceAction(initialState, "enter_user_attributes") { put("identity_attributes", JSONObject(userAttributes)) - }.onSuccess { newState -> - state.value = newState } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -150,9 +158,8 @@ class ReducerManager( fun addAuthentication(args: ReducerArgs.AddAuthentication) = scope.launch { state.value?.let { initialState -> api.reduceAction(initialState, "add_authentication", args) - .onSuccess { newState -> - state.value = newState - } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -160,9 +167,9 @@ class ReducerManager( state.value?.let { initialState -> api.reduceAction(initialState, "delete_authentication") { put("authentication_method", index) - }.onSuccess { newState -> - state.value = newState } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -170,9 +177,9 @@ class ReducerManager( state.value?.let { initialState -> api.reduceAction(initialState, "add_policy") { put("policy", Json.encodeToNativeJson(policy.methods)) - }.onSuccess { newState -> - state.value = newState - }.onError { Log.d("ReducerManager", "$it") } + } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -181,9 +188,9 @@ class ReducerManager( api.reduceAction(initialState, "update_policy") { put("policy_index", index) put("policy", Json.encodeToNativeJson(policy.methods)) - }.onSuccess { newState -> - state.value = newState } + .onSuccess { onSuccess(it) } + .onError { onError(it) } } } @@ -191,8 +198,26 @@ class ReducerManager( state.value?.let { initialState -> api.reduceAction(initialState, "delete_policy") { put("policy_index", index) - }.onSuccess { newState -> - state.value = newState + } + .onSuccess { onSuccess(it) } + .onError { onError(it) } + } + } + + fun backupSecret( + name: String, + args: ReducerArgs.EnterSecret, + ) = scope.launch { + state.value?.let { initialState -> + api.reduceAction(initialState, "enter_secret", args).onSuccess { newState -> + scope.launch { + api.reduceAction(newState, "enter_secret_name") { + put("name", name) + }.onSuccess { newNewState -> + this@ReducerManager.onSuccess(newNewState) + this@ReducerManager.next() + }.onError { onError(it) } + } } } } diff --git a/anastasis/src/main/java/net/taler/anastasis/shared/FieldStatus.kt b/anastasis/src/main/java/net/taler/anastasis/shared/FieldStatus.kt new file mode 100644 index 0000000..a471e20 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/shared/FieldStatus.kt @@ -0,0 +1,26 @@ +/* + * This file is part of GNU Taler + * (C) 2023 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.anastasis.shared + +import net.taler.anastasis.R + +enum class FieldStatus(val msgRes: Int?, val error: Boolean) { + Null(msgRes = null, error = false), + Blank(msgRes = R.string.field_empty, error = true), + Invalid(msgRes = R.string.field_invalid, error = true), + Valid(msgRes = null, error = false); +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/Utils.kt b/anastasis/src/main/java/net/taler/anastasis/shared/Utils.kt index 58e6b89..916dd36 100644 --- a/anastasis/src/main/java/net/taler/anastasis/Utils.kt +++ b/anastasis/src/main/java/net/taler/anastasis/shared/Utils.kt @@ -14,7 +14,7 @@ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -package net.taler.anastasis +package net.taler.anastasis.shared import android.os.Build import io.matthewnelson.encoding.base32.Base32 @@ -22,8 +22,11 @@ import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow +import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.json.JSONArray @@ -37,15 +40,18 @@ import kotlin.time.Duration object Utils { fun formatDate(date: LocalDate): String { val javaDate = date.toJavaLocalDate() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - return javaDate.format(formatter) + javaDate.format(formatter) } else { val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - return formatter.format(date) + formatter.format(date) } } + val currentDate: LocalDate + get() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + inline fun <reified T> Json.encodeToNativeJson(value: T): JSONObject = JSONObject(encodeToString(value)) @@ -56,6 +62,9 @@ object Utils { .toByteArray(Charset.defaultCharset()) .encodeToString(Base32.Crockford) + fun encodeBase32 (input: ByteArray) = input + .encodeToString(Base32.Crockford) + fun decodeBase32 (input: String) = input .decodeToByteArray(Base32.Crockford) .toString(Charset.defaultCharset()) diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt index 6d8f0ff..9234f99 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R -import net.taler.anastasis.Utils +import net.taler.anastasis.shared.Utils import net.taler.anastasis.models.AuthMethod import net.taler.anastasis.models.AuthenticationProviderStatus import net.taler.anastasis.models.ReducerArgs diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt index 303b82e..eacbea4 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R -import net.taler.anastasis.models.ContinentInfo import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.reusable.components.Picker import net.taler.anastasis.ui.reusable.pages.WizardPage @@ -48,15 +47,25 @@ fun SelectContinentScreen( else -> error("invalid reducer state type") } ?: emptyList() - var selectedContinent by remember { mutableStateOf<ContinentInfo?>(null) } + val selectedContinent = when (val state = reducerState) { + is ReducerState.Backup -> state.selectedContinent + is ReducerState.Recovery -> state.selectedContinent + else -> error("invalid reducer state type") + } + + var localContinent by remember { + mutableStateOf(selectedContinent?.let { selected -> + continents.find { it.name == selected } + }) + } WizardPage( title = stringResource(R.string.select_continent_title), showPrev = false, - enableNext = selectedContinent != null, + enableNext = localContinent != null, onBackClicked = { viewModel.goHome() }, onNextClicked = { - selectedContinent?.let { + localContinent?.let { viewModel.reducerManager.selectContinent(it) } }, @@ -69,11 +78,11 @@ fun SelectContinentScreen( ) { Picker( label = stringResource(R.string.continent), - initialOption = selectedContinent?.name, + initialOption = localContinent?.name, options = continents.map { it.name }.toSet(), onOptionChanged = { option -> continents.find { it.name == option }?.let { continent -> - selectedContinent = continent + localContinent = continent } }, ) diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt index 4f5d753..4c8a32e 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R -import net.taler.anastasis.models.CountryInfo import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.reusable.components.Picker import net.taler.anastasis.ui.reusable.pages.WizardPage @@ -48,15 +47,25 @@ fun SelectCountryScreen( else -> error("invalid reducer state type") } ?: emptyList() - var selectedCountry by remember { mutableStateOf<CountryInfo?>(null) } + val selectedCountry = when (val state = reducerState) { + is ReducerState.Backup -> state.selectedCountry + is ReducerState.Recovery -> state.selectedCountry + else -> error("invalid reducer state type") + } + + var localCountry by remember { + mutableStateOf(selectedCountry?.let { selected -> + countries.find { it.code == selected } + }) + } WizardPage( title = stringResource(R.string.select_country_title), - enableNext = selectedCountry != null, + enableNext = localCountry != null, onBackClicked = { viewModel.goHome() }, onPrevClicked = { viewModel.goBack() }, onNextClicked = { - selectedCountry?.let { + localCountry?.let { viewModel.reducerManager.selectCountry(it) } }, @@ -69,11 +78,11 @@ fun SelectCountryScreen( ) { Picker( label = stringResource(R.string.country), - initialOption = selectedCountry?.name, + initialOption = localCountry?.name, options = countries.map { it.name }.toSet(), onOptionChanged = { option -> countries.find { it.name == option }?.let { country -> - selectedCountry = country + localCountry = country } }, ) diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt index 6de45b5..e0badcb 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt @@ -31,20 +31,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.datetime.toLocalDate import net.taler.anastasis.R +import net.taler.anastasis.shared.Utils import net.taler.anastasis.models.ReducerState +import net.taler.anastasis.models.UserAttributeSpec +import net.taler.anastasis.shared.FieldStatus import net.taler.anastasis.ui.reusable.components.DatePickerField import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing import net.taler.anastasis.viewmodels.ReducerViewModel -import java.util.Calendar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -58,7 +59,15 @@ fun SelectUserAttributesScreen( else -> error("invalid reducer state type") } ?: emptyList() - val values = remember { mutableStateMapOf<String, String>() } + val identityAttributes = when(val state = reducerState) { + is ReducerState.Backup -> state.identityAttributes + is ReducerState.Recovery -> state.identityAttributes + else -> error("invalid reducer state type") + } ?: emptyMap() + + val values = remember { mutableStateMapOf( + *identityAttributes.toList().toTypedArray() + ) } WizardPage( title = stringResource(R.string.select_user_attributes_title), @@ -67,43 +76,72 @@ fun SelectUserAttributesScreen( onNextClicked = { viewModel.reducerManager.enterUserAttributes(values) }, - ) { + enableNext = userAttributes.fold(true) { a, b -> + a && (fieldStatus(b, values[b.name]) == FieldStatus.Valid) + } + ) { scrollConnection -> LazyColumn( modifier = Modifier .fillMaxSize() - .nestedScroll(it) - .padding(LocalSpacing.current.medium), + .nestedScroll(scrollConnection), verticalArrangement = Arrangement.Top, ) { items(items = userAttributes) { attr -> + val status = fieldStatus(attr, values[attr.name]) + val supportingText: @Composable () -> Unit = @Composable { + status.msgRes?.let { + Text(stringResource(it)) + } ?: if (attr.optional == true) { + Text(stringResource(R.string.field_optional)) + } else null + } when (attr.type) { "string" -> OutlinedTextField( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding( + start = LocalSpacing.current.medium, + end = LocalSpacing.current.medium, + ) + .fillMaxWidth(), value = values[attr.name] ?: "", onValueChange = { values[attr.name] = it }, + isError = status.error, + supportingText = supportingText, label = { Text(attr.label) }, ) - "date" -> @Composable { - val cal = Calendar.getInstance() - var yy by remember { mutableStateOf(cal.get(Calendar.YEAR)) } - var mm by remember { mutableStateOf(cal.get(Calendar.MONTH)) } - var dd by remember { mutableStateOf(cal.get(Calendar.DAY_OF_MONTH)) } - DatePickerField( - modifier = Modifier.fillMaxWidth(), - label = attr.label, - yy = yy, - mm = mm, - dd = dd, - onDateSelected = { y, m, d -> - yy = y - mm = m - dd = d - }, - ) - } + "date" -> DatePickerField( + modifier = Modifier + .padding( + start = LocalSpacing.current.medium, + end = LocalSpacing.current.medium, + ) + .fillMaxWidth(), + label = attr.label, + isError = status.error, + supportingText = supportingText, + date = values[attr.name]?.toLocalDate(), + onDateSelected = { date -> + values[attr.name] = Utils.formatDate(date) + }, + ) } Spacer(Modifier.height(LocalSpacing.current.small)) } } } +} + +fun fieldStatus( + field: UserAttributeSpec, + value: String? = null, +): FieldStatus = if (value == null) { + FieldStatus.Null +} else { + if (value.isNotBlank()) { + field.validationRegex?.toRegex()?.let { + if (value.matches(it)) + FieldStatus.Valid else FieldStatus.Invalid + } ?: FieldStatus.Valid + } else if (field.optional == true) + FieldStatus.Valid else FieldStatus.Blank }
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt index f8b580e..2b2f776 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import net.taler.anastasis.R -import net.taler.anastasis.Utils +import net.taler.anastasis.shared.Utils import net.taler.anastasis.models.AuthMethod import net.taler.anastasis.ui.forms.EditQuestionForm diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt index 8ec3662..09cd564 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt @@ -17,6 +17,8 @@ package net.taler.anastasis.ui.dialogs import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -42,13 +44,16 @@ fun EditPolicyDialog( onCancel: () -> Unit, ) { var localPolicy by remember { mutableStateOf(policy) } + val scrollState = rememberScrollState() AlertDialog( onDismissRequest = onCancel, title = { Text(stringResource(R.string.add_policy)) }, text = { EditPolicyForm( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState), policy = localPolicy, methods = methods, providers = providers, diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/ErrorDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/ErrorDialog.kt new file mode 100644 index 0000000..40e4534 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/ErrorDialog.kt @@ -0,0 +1,44 @@ +/* + * This file is part of GNU Taler + * (C) 2023 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.anastasis.ui.dialogs + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import net.taler.anastasis.R +import net.taler.anastasis.backend.TalerErrorInfo + +@Composable +fun ErrorDialog( + error: TalerErrorInfo, + onCancel: () -> Unit, +) { + AlertDialog( + onDismissRequest = onCancel, + title = { Text(stringResource(R.string.error)) }, + text = { Text(error.hint ?: error.code.name) }, + confirmButton = { + TextButton(onClick = { + onCancel() + }) { + Text(stringResource(R.string.close)) + } + }, + ) +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt index 545d5b1..584ee80 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt @@ -40,32 +40,41 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import kotlinx.datetime.LocalDate -import net.taler.anastasis.Utils +import net.taler.anastasis.shared.Utils import net.taler.anastasis.ui.theme.LocalSpacing -import java.util.Calendar @OptIn(ExperimentalMaterial3Api::class) @Composable fun DatePickerField( modifier: Modifier = Modifier, label: String, - yy: Int, - mm: Int, - dd: Int, - onDateSelected: (yy: Int, mm: Int, dd: Int) -> Unit, + isError: Boolean = false, + supportingText: (@Composable () -> Unit)? = null, + date: LocalDate?, + onDateSelected: (date: LocalDate) -> Unit, ) { + val now = Utils.currentDate val dialog = DatePickerDialog( LocalContext.current, { _: DatePicker, y: Int, m: Int, d: Int -> - onDateSelected(y, m, d) - }, yy, mm, dd, + onDateSelected(LocalDate( + year = y, + monthNumber = m + 1, + dayOfMonth = d, + )) + }, + date?.year ?: now.year, + (date?.monthNumber ?: now.monthNumber) - 1, + date?.dayOfMonth ?: now.dayOfMonth, ) OutlinedTextField( modifier = modifier, - value = Utils.formatDate(LocalDate(yy, mm, dd)), + value = date?.let { Utils.formatDate(it) } ?: "", label = { Text(label) }, onValueChange = { }, + isError = isError, + supportingText = supportingText, trailingIcon = { Button( modifier = Modifier.padding(end = LocalSpacing.current.medium), @@ -89,21 +98,12 @@ fun DatePickerField( @Preview @Composable fun DatePickerFieldPreview() { - val cal = Calendar.getInstance() - var yy by remember { mutableStateOf(cal.get(Calendar.YEAR)) } - var mm by remember { mutableStateOf(cal.get(Calendar.MONTH)) } - var dd by remember { mutableStateOf(cal.get(Calendar.DAY_OF_MONTH)) } + var date by remember { mutableStateOf(Utils.currentDate) } Surface { DatePickerField( label = "Birthdate", - yy = yy, - mm = mm, - dd = dd, - onDateSelected = { y, m, d -> - yy = y - mm = m - dd = d - }, + date = date, + onDateSelected = { date = it }, ) } }
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt index d157707..46be209 100644 --- a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt +++ b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt @@ -16,7 +16,6 @@ package net.taler.anastasis.viewmodels -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import net.taler.anastasis.Routes import net.taler.anastasis.backend.AnastasisReducerApi +import net.taler.anastasis.backend.TalerErrorInfo import net.taler.anastasis.models.BackupStates import net.taler.anastasis.models.RecoveryStates import net.taler.anastasis.models.ReducerState @@ -38,14 +38,15 @@ class ReducerViewModel @Inject constructor(): ViewModel() { private val _reducerState = MutableStateFlow<ReducerState?>(null) val reducerState = _reducerState.asStateFlow() + private val _reducerError = MutableStateFlow<TalerErrorInfo?>(null) + val reducerError = _reducerError.asStateFlow() private val _navRoute = MutableStateFlow(Routes.Home.route) val navRoute = _navRoute.asStateFlow() init { - reducerManager = ReducerManager(_reducerState, api, viewModelScope) + reducerManager = ReducerManager(_reducerState, _reducerError, api, viewModelScope) viewModelScope.launch { _reducerState.collect { - Log.d("ReducerViewModel", it?.toString() ?: "nothing") reducerManager.stopSyncingProviders() _navRoute.value = when (it) { is ReducerState.Backup -> when (it.backupState) { @@ -72,7 +73,6 @@ class ReducerViewModel @Inject constructor(): ViewModel() { RecoveryStates.ChallengeSolving -> TODO() RecoveryStates.RecoveryFinished -> TODO() } - is ReducerState.Error -> TODO() else -> Routes.Home.route } } @@ -110,4 +110,8 @@ class ReducerViewModel @Inject constructor(): ViewModel() { fun goHome() { _reducerState.value = null } + + fun cleanError() { + _reducerError.value = null + } }
\ No newline at end of file diff --git a/anastasis/src/main/res/values/strings.xml b/anastasis/src/main/res/values/strings.xml index cf7054d..b8776c4 100644 --- a/anastasis/src/main/res/values/strings.xml +++ b/anastasis/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ <string name="edit">Edit</string> <string name="delete">Delete</string> <string name="cancel">Cancel</string> + <string name="close">Close</string> <string name="menu">Menu</string> <string name="country">Country</string> <string name="continent">Continent</string> @@ -24,6 +25,10 @@ <string name="provided_by">Provided by %1$s</string> <string name="disabled">Disabled</string> <string name="unknown">Unknown</string> + <string name="error">Error</string> + <string name="field_empty">This field is required</string> + <string name="field_invalid">This field is invalid</string> + <string name="field_optional">This field is optional</string> <!-- Common --> <string name="select_continent_title">Where do you live?</string> @@ -33,6 +38,7 @@ <!-- Backup --> <string name="select_auth_methods_title">Authentication methods</string> <string name="review_policies_title">Recovery policies</string> + <string name="edit_secret_title">Provide secret to backup</string> <string name="add_auth_method_question">Add a question challenge</string> <string name="add_auth_method_sms">Add a SMS challenge</string> <string name="add_auth_method_email">Add an e-mail challenge</string> @@ -49,4 +55,7 @@ <string name="add_challenge">Add challenge</string> <string name="add_policy">Add policy</string> <string name="edit_policy">Edit policy</string> + <string name="secret_name">Secret name</string> + <string name="secret_unique">This should be unique</string> + <string name="secret_text">Secret text</string> </resources>
\ No newline at end of file diff --git a/taler-kotlin-android/src/main/java/net/taler/common/TalerErrorCode.kt b/taler-kotlin-android/src/main/java/net/taler/common/TalerErrorCode.kt index dbe59a4..e9968a9 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/TalerErrorCode.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/TalerErrorCode.kt @@ -3868,6 +3868,12 @@ enum class TalerErrorCode(val code: Int) { * (A value of 0 indicates that the error is generated client-side). */ END(9999); + + companion object { + fun fromInt(code: Int) = enumValues<TalerErrorCode>().firstOrNull { + code == it.code + } ?: UNKNOWN + } } @OptIn(ExperimentalSerializationApi::class) @@ -3879,9 +3885,7 @@ object TalerErrorCodeSerializer : KSerializer<TalerErrorCode> { override fun deserialize(decoder: Decoder): TalerErrorCode { val code = decoder.decodeInt() - return enumValues<TalerErrorCode>().firstOrNull { - code == it.code - } ?: TalerErrorCode.UNKNOWN + return TalerErrorCode.fromInt(code) } override fun serialize(encoder: Encoder, value: TalerErrorCode) { |