summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-07-31 18:35:02 -0600
committerIván Ávalos <avalos@disroot.org>2023-11-11 13:20:09 -0600
commitde66e6697a006d85df54c79e740798ecd56bb683 (patch)
treed0d38bae034866b31ceec2a86175142997c3abac
parentfb80fc4d9636c957ba4f17a5d57aee3fccd494a1 (diff)
downloadtaler-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>
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/MainActivity.kt8
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt38
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt13
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt77
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/shared/FieldStatus.kt26
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/shared/Utils.kt (renamed from anastasis/src/main/java/net/taler/anastasis/Utils.kt)17
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt21
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt21
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt90
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt7
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/dialogs/ErrorDialog.kt44
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt42
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt12
-rw-r--r--anastasis/src/main/res/values/strings.xml9
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/TalerErrorCode.kt10
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) {