summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-07-30 01:13:56 -0600
committerIván Ávalos <avalos@disroot.org>2023-11-11 13:20:09 -0600
commitc4daf6ba593a57fb25e1f4705b303350bf8a3fa1 (patch)
tree880a995eca7b8f34e22b594c0fdda75531e13a0e
parenta31ca9fa74e07bc06af156c83592471ac3964121 (diff)
downloadtaler-android-c4daf6ba593a57fb25e1f4705b303350bf8a3fa1.tar.gz
taler-android-c4daf6ba593a57fb25e1f4705b303350bf8a3fa1.tar.bz2
taler-android-c4daf6ba593a57fb25e1f4705b303350bf8a3fa1.zip
Policy editing + too many things to list
Signed-off-by: Iván Ávalos <avalos@disroot.org>
-rw-r--r--anastasis/src/main/AndroidManifest.xml2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/MainActivity.kt15
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/Routes.kt1
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/Utils.kt4
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt42
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt45
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt258
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt144
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt5
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt9
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt10
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt (renamed from anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt)48
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt78
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/forms/EditPolicyForm.kt117
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/forms/EditQuestionForm.kt89
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt41
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt5
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt17
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt30
-rw-r--r--anastasis/src/main/res/values/strings.xml25
-rw-r--r--anastasis/src/main/res/values/themes.xml5
21 files changed, 804 insertions, 186 deletions
diff --git a/anastasis/src/main/AndroidManifest.xml b/anastasis/src/main/AndroidManifest.xml
index ce3333f..ff44d2a 100644
--- a/anastasis/src/main/AndroidManifest.xml
+++ b/anastasis/src/main/AndroidManifest.xml
@@ -9,6 +9,8 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:enableOnBackInvokedCallback="true"
+ android:windowSoftInputMode="adjustResize"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Anastasis"
diff --git a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
index 18c83cd..8d6a8ad 100644
--- a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
@@ -2,7 +2,9 @@ package net.taler.anastasis
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
+import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -13,6 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import dagger.hilt.android.AndroidEntryPoint
+import net.taler.anastasis.ui.backup.ReviewPoliciesScreen
import net.taler.anastasis.ui.backup.SelectAuthMethodsScreen
import net.taler.anastasis.ui.common.SelectContinentScreen
import net.taler.anastasis.ui.common.SelectCountryScreen
@@ -23,8 +26,17 @@ import net.taler.anastasis.viewmodels.ReducerViewModel
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+ val viewModel: ReducerViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (viewModel.goBack()) finish()
+ }
+ })
+
setContent {
AnastasisTheme {
Surface(
@@ -59,6 +71,9 @@ fun MainNavHost(
Routes.SelectAuthMethods.route -> {
SelectAuthMethodsScreen()
}
+ Routes.ReviewPoliciesScreen.route -> {
+ ReviewPoliciesScreen()
+ }
Routes.RestoreInit.route -> {
Text("This is the restore session screen!")
}
diff --git a/anastasis/src/main/java/net/taler/anastasis/Routes.kt b/anastasis/src/main/java/net/taler/anastasis/Routes.kt
index 6d00cad..5258e7f 100644
--- a/anastasis/src/main/java/net/taler/anastasis/Routes.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/Routes.kt
@@ -13,6 +13,7 @@ sealed class Routes(
// Backup
object SelectAuthMethods: Routes("select_auth_methods")
+ object ReviewPoliciesScreen: Routes("review_policies")
// Restore
object RestoreInit: Routes("restore")
diff --git a/anastasis/src/main/java/net/taler/anastasis/Utils.kt b/anastasis/src/main/java/net/taler/anastasis/Utils.kt
index 8673a05..58e6b89 100644
--- a/anastasis/src/main/java/net/taler/anastasis/Utils.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/Utils.kt
@@ -26,6 +26,7 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.toJavaLocalDate
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import org.json.JSONArray
import org.json.JSONObject
import java.nio.charset.Charset
import java.text.SimpleDateFormat
@@ -48,6 +49,9 @@ object Utils {
inline fun <reified T> Json.encodeToNativeJson(value: T): JSONObject =
JSONObject(encodeToString(value))
+ inline fun <reified T> Json.encodeToNativeJson(value: Collection<T>): JSONArray =
+ JSONArray(encodeToString(value))
+
fun encodeBase32 (input: String) = input
.toByteArray(Charset.defaultCharset())
.encodeToString(Base32.Crockford)
diff --git a/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt b/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt
index 83afc88..036d075 100644
--- a/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt
@@ -1,9 +1,19 @@
package net.taler.anastasis.models
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountBalance
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Mail
+import androidx.compose.material.icons.filled.QuestionAnswer
+import androidx.compose.material.icons.filled.QuestionMark
+import androidx.compose.material.icons.filled.Sms
+import androidx.compose.material.icons.filled.Token
+import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
+import net.taler.anastasis.R
import net.taler.common.Amount
import net.taler.common.Timestamp
@@ -159,12 +169,38 @@ data class CoreSecret(
@Serializable
data class AuthMethod(
- val type: String,
+ val type: Type,
val instructions: String,
val challenge: String,
@SerialName("mime_type")
val mimeType: String? = null,
-)
+) {
+ @Serializable
+ enum class Type(
+ val icon: ImageVector,
+ val nameRes: Int,
+ ) {
+ @SerialName("question")
+ Question(Icons.Default.QuestionAnswer, R.string.auth_method_question),
+
+ @SerialName("sms")
+ Sms(Icons.Default.Sms, R.string.auth_method_sms),
+
+ @SerialName("email")
+ Email(Icons.Default.Email, R.string.auth_method_email),
+
+ @SerialName("iban")
+ Iban(Icons.Default.AccountBalance, R.string.auth_method_iban),
+
+ @SerialName("mail")
+ Mail(Icons.Default.Mail, R.string.auth_method_mail),
+
+ @SerialName("totp")
+ Totp(Icons.Default.Token, R.string.auth_method_totp),
+
+ Unknown(Icons.Default.QuestionMark, R.string.unknown),
+ }
+}
@Serializable
data class ChallengeInfo(
@@ -283,7 +319,7 @@ enum class RecoveryStates {
@Serializable
data class MethodSpec(
- val type: String,
+ val type: AuthMethod.Type,
@SerialName("usage_fee")
val usageFee: String,
)
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 0aed98f..fd527f6 100644
--- a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
@@ -24,11 +24,14 @@ import kotlinx.coroutines.flow.catch
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.backend.AnastasisReducerApi
import net.taler.anastasis.models.AuthenticationProviderStatus
import net.taler.anastasis.models.ContinentInfo
import net.taler.anastasis.models.CountryInfo
+import net.taler.anastasis.models.Policy
import net.taler.anastasis.models.ReducerArgs
import net.taler.anastasis.models.ReducerState
import org.json.JSONObject
@@ -64,6 +67,15 @@ class ReducerManager(
}
}
+ fun next() = scope.launch {
+ state.value?.let { initialState ->
+ api.reduceAction(initialState, "next")
+ .onSuccess { newState ->
+ state.value = newState
+ }
+ }
+ }
+
fun selectContinent(continent: ContinentInfo) = scope.launch {
state.value?.let { initialState ->
api.reduceAction(initialState, "select_continent") {
@@ -147,10 +159,41 @@ class ReducerManager(
fun deleteAuthentication(index: Int) = scope.launch {
state.value?.let { initialState ->
api.reduceAction(initialState, "delete_authentication") {
- put("index", index)
+ put("authentication_method", index)
+ }.onSuccess { newState ->
+ state.value = newState
+ }
+ }
+ }
+
+ fun addPolicy(policy: Policy) = scope.launch {
+ 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") }
+ }
+ }
+
+ fun updatePolicy(index: Int, policy: Policy) = scope.launch {
+ state.value?.let { initialState ->
+ api.reduceAction(initialState, "update_policy") {
+ put("policy_index", index)
+ put("policy", Json.encodeToNativeJson(policy.methods))
}.onSuccess { newState ->
state.value = newState
}
}
}
+
+ fun deletePolicy(index: Int) = scope.launch {
+ state.value?.let { initialState ->
+ api.reduceAction(initialState, "delete_policy") {
+ put("policy_index", index)
+ }.onSuccess { newState ->
+ state.value = newState
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt
new file mode 100644
index 0000000..38853c7
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt
@@ -0,0 +1,258 @@
+/*
+ * 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.backup
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.IconButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.hilt.navigation.compose.hiltViewModel
+import net.taler.anastasis.R
+import net.taler.anastasis.models.AuthMethod
+import net.taler.anastasis.models.AuthenticationProviderStatus
+import net.taler.anastasis.models.Policy
+import net.taler.anastasis.models.ReducerState
+import net.taler.anastasis.ui.dialogs.EditPolicyDialog
+import net.taler.anastasis.ui.reusable.pages.WizardPage
+import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.ReducerViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ReviewPoliciesScreen(
+ viewModel: ReducerViewModel = hiltViewModel(),
+) {
+ val state by viewModel.reducerState.collectAsState()
+ val reducerState = state as? ReducerState.Backup
+ ?: error("invalid reducer type")
+
+ val policies = reducerState.policies ?: emptyList()
+ // Get only providers with "ok" status
+ val providers = reducerState.authenticationProviders?.filter {
+ it.value is AuthenticationProviderStatus.Ok
+ }?.mapValues { it.value as AuthenticationProviderStatus.Ok } ?: emptyMap()
+ val methods = reducerState.authenticationMethods ?: emptyList()
+
+ var showEditDialog by remember { mutableStateOf(false) }
+ var editingPolicy by remember { mutableStateOf<Policy?>(null) }
+ var editingPolicyIndex by remember { mutableStateOf<Int?>(null) }
+ val reset = {
+ showEditDialog = false
+ editingPolicy = null
+ editingPolicyIndex = null
+ }
+
+ if (showEditDialog) {
+ EditPolicyDialog(
+ policy = editingPolicy,
+ methods = methods,
+ providers = providers,
+ onCancel = { reset() },
+ onPolicyEdited = {
+ editingPolicyIndex?.let { index ->
+ viewModel.reducerManager.updatePolicy(index, it)
+ } ?: run {
+ viewModel.reducerManager.addPolicy(it)
+ }
+ reset()
+ }
+ )
+ }
+
+ WizardPage(
+ title = stringResource(R.string.review_policies_title),
+ onBackClicked = { viewModel.goHome() },
+ onPrevClicked = { viewModel.goBack() },
+ onNextClicked = {
+ viewModel.reducerManager.next()
+ }
+ ) {
+ Scaffold(
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = { showEditDialog = true },
+ ) {
+ Icon(
+ Icons.Default.Add,
+ contentDescription = stringResource(R.string.add),
+ )
+ }
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxWidth()
+ ) {
+ items(count = policies.size) { index ->
+ val policy = policies[index]
+ PolicyCard(
+ modifier = Modifier.padding(
+ start = LocalSpacing.current.small,
+ end = LocalSpacing.current.small,
+ bottom = LocalSpacing.current.small,
+ ),
+ policy = policy,
+ methods = methods,
+ providers = providers,
+ index = index,
+ onEdit = {
+ editingPolicy = policy
+ editingPolicyIndex = index
+ showEditDialog = true
+ },
+ ) {
+ viewModel.reducerManager.deletePolicy(index)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun PolicyCard(
+ modifier: Modifier = Modifier,
+ methods: List<AuthMethod>,
+ providers: Map<String, AuthenticationProviderStatus>,
+ policy: Policy,
+ index: Int,
+ onEdit: () -> Unit,
+ onDelete: () -> Unit,
+) {
+ ElevatedCard(
+ modifier = modifier,
+ ) {
+ Column(modifier = Modifier.padding(LocalSpacing.current.medium)) {
+ var expanded by remember { mutableStateOf(false) }
+ Row(
+ horizontalArrangement = Arrangement.SpaceAround,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ stringResource(R.string.policy_n, index + 1),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Spacer(Modifier.weight(1f))
+ Box {
+ IconButton(onClick = { expanded = true }) {
+ Icon(
+ Icons.Default.MoreVert,
+ contentDescription = stringResource(R.string.menu),
+ tint = MaterialTheme.colorScheme.onBackground,
+ )
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ DropdownMenuItem(onClick = {
+ onEdit()
+ expanded = false
+ }) {
+ Text(stringResource(R.string.edit))
+ }
+ DropdownMenuItem(onClick = onDelete) {
+ Text(stringResource(R.string.delete))
+ }
+ }
+ }
+ }
+ }
+ Column {
+ policy.methods.forEach { m ->
+ val method = methods[m.authenticationMethod]
+ val provider = providers[m.provider] as? AuthenticationProviderStatus.Ok
+ if (provider != null) {
+ PolicyMethodCard(
+ modifier = Modifier
+ .padding(top = LocalSpacing.current.small)
+ .fillMaxWidth(),
+ method = method,
+ provider = provider,
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun PolicyMethodCard(
+ modifier: Modifier = Modifier,
+ method: AuthMethod,
+ provider: AuthenticationProviderStatus.Ok,
+) {
+ OutlinedCard(
+ modifier = modifier,
+ ) {
+ Row(
+ modifier = Modifier.padding(LocalSpacing.current.medium),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ method.type.icon,
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = stringResource(method.type.nameRes),
+ )
+ Spacer(Modifier.width(LocalSpacing.current.medium))
+ Column {
+ Text(method.instructions, style = MaterialTheme.typography.labelLarge)
+ Spacer(Modifier.height(LocalSpacing.current.small))
+ Text(
+ stringResource(R.string.provider),
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Bold,
+ )
+ Text(
+ provider.businessName,
+ style = MaterialTheme.typography.labelMedium,
+ )
+ }
+ }
+ }
+} \ No newline at end of file
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 3bdd129..6d8f0ff 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
@@ -27,13 +27,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountBalance
import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.filled.Email
-import androidx.compose.material.icons.filled.Mail
-import androidx.compose.material.icons.filled.QuestionMark
-import androidx.compose.material.icons.filled.Sms
-import androidx.compose.material.icons.filled.Token
import androidx.compose.material3.Divider
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@@ -48,6 +42,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -57,6 +53,7 @@ import net.taler.anastasis.models.AuthMethod
import net.taler.anastasis.models.AuthenticationProviderStatus
import net.taler.anastasis.models.ReducerArgs
import net.taler.anastasis.models.ReducerState
+import net.taler.anastasis.ui.dialogs.EditMethodDialog
import net.taler.anastasis.ui.reusable.components.ActionCard
import net.taler.anastasis.ui.reusable.pages.WizardPage
import net.taler.anastasis.ui.theme.LocalSpacing
@@ -66,27 +63,23 @@ import net.taler.anastasis.viewmodels.ReducerViewModel
fun SelectAuthMethodsScreen(
viewModel: ReducerViewModel = hiltViewModel(),
) {
- val reducerState by viewModel.reducerState.collectAsState()
+ val state by viewModel.reducerState.collectAsState()
+ val reducerState = state as? ReducerState.Backup
+ ?: error("invalid reducer state type")
- val authProviders = when (val state = reducerState) {
- is ReducerState.Backup -> state.authenticationProviders
- else -> error("invalid reducer state type")
- } ?: emptyMap()
+ val authProviders = reducerState.authenticationProviders ?: emptyMap()
+ val selectedMethods = reducerState.authenticationMethods ?: emptyList()
- // Get only methods of providers with "ok" status
+ // Get only known methods of providers with "ok" status
val availableMethods = authProviders.flatMap { entry ->
if (entry.value is AuthenticationProviderStatus.Ok) {
(entry.value as AuthenticationProviderStatus.Ok).methods.map { it.type }
+ .filter { it != AuthMethod.Type.Unknown }
} else emptyList()
}.distinct()
- val selectedMethods = when (val state = reducerState) {
- is ReducerState.Backup -> state.authenticationMethods
- else -> error("invalid reducer state type")
- } ?: emptyList()
-
var showEditDialog by remember { mutableStateOf(false) }
- var methodType by remember { mutableStateOf<String?>(null) }
+ var methodType by remember { mutableStateOf<AuthMethod.Type?>(null) }
var method by remember { mutableStateOf<AuthMethod?>(null) }
val reset = {
@@ -114,14 +107,14 @@ fun SelectAuthMethodsScreen(
WizardPage(
title = stringResource(R.string.select_auth_methods_title),
- onBackClicked = {
- viewModel.goHome()
- },
- onPrevClicked = {
- viewModel.reducerManager.back()
- },
- ) {
+ onBackClicked = { viewModel.goHome() },
+ onPrevClicked = { viewModel.goBack() },
+ onNextClicked = {
+ viewModel.reducerManager.next()
+ }
+ ) { scroll ->
AuthMethods(
+ nestedScrollConnection = scroll,
availableMethods = availableMethods,
selectedMethods = selectedMethods,
onAddMethod = {
@@ -137,63 +130,30 @@ fun SelectAuthMethodsScreen(
@Composable
private fun AuthMethods(
- availableMethods: List<String>,
+ nestedScrollConnection: NestedScrollConnection,
+ availableMethods: List<AuthMethod.Type>,
selectedMethods: List<AuthMethod>,
- onAddMethod: (type: String) -> Unit,
+ onAddMethod: (type: AuthMethod.Type) -> Unit,
onDeleteMethod: (index: Int) -> Unit,
) {
LazyColumn(
modifier = Modifier
- .padding(LocalSpacing.current.medium)
+ .nestedScroll(nestedScrollConnection)
.fillMaxWidth(),
verticalArrangement = Arrangement.Top,
) {
items(items = availableMethods) { method ->
- when (method) {
- "question" -> ActionCard(
- modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
- .fillMaxWidth(),
- icon = { Icon(Icons.Default.QuestionMark, contentDescription = null) },
- headline = stringResource(R.string.auth_method_question)
- ) { onAddMethod("question") }
- "sms" -> ActionCard(
- modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
- .fillMaxWidth(),
- icon = { Icon(Icons.Default.Sms, contentDescription = null) },
- headline = stringResource(R.string.auth_method_sms)
- ) { onAddMethod("sms") }
- "email" -> ActionCard(
- modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
- .fillMaxWidth(),
- icon = { Icon(Icons.Default.Email, contentDescription = null) },
- headline = stringResource(R.string.auth_method_email)
- ) { onAddMethod("email") }
- "iban" -> ActionCard(
- modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
- .fillMaxWidth(),
- icon = { Icon(Icons.Default.AccountBalance, contentDescription = null) },
- headline = stringResource(R.string.auth_method_question)
- ) { onAddMethod("iban") }
- "mail" -> ActionCard(
- modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
- .fillMaxWidth(),
- icon = { Icon(Icons.Default.Mail, contentDescription = null) },
- headline = stringResource(R.string.auth_method_mail)
- ) { onAddMethod("mail") }
- "totp" -> ActionCard(
- modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
- .fillMaxWidth(),
- icon = { Icon(Icons.Default.Token, contentDescription = null) },
- headline = stringResource(R.string.auth_method_totp)
- ) { onAddMethod("totp") }
- else -> {}
- }
+ AddMethodCard(
+ modifier = Modifier
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ )
+ .fillMaxWidth(),
+ type = method,
+ onClick = { onAddMethod(method) },
+ )
}
item {
@@ -203,7 +163,11 @@ private fun AuthMethods(
items(count = selectedMethods.size) { i ->
ChallengeCard(
modifier = Modifier
- .padding(bottom = LocalSpacing.current.small)
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ )
.fillMaxWidth(),
authMethod = selectedMethods[i],
onDelete = { onDeleteMethod(i) },
@@ -213,6 +177,28 @@ private fun AuthMethods(
}
@Composable
+private fun AddMethodCard(
+ modifier: Modifier = Modifier,
+ type: AuthMethod.Type,
+ onClick: (type: AuthMethod.Type) -> Unit,
+) {
+ ActionCard(
+ modifier = modifier,
+ icon = { Icon(type.icon, contentDescription = null) },
+ headline = when (type) {
+ AuthMethod.Type.Question -> stringResource(R.string.add_auth_method_question)
+ AuthMethod.Type.Sms -> stringResource(R.string.add_auth_method_sms)
+ AuthMethod.Type.Email -> stringResource(R.string.add_auth_method_email)
+ AuthMethod.Type.Iban -> stringResource(R.string.add_auth_method_iban)
+ AuthMethod.Type.Mail -> stringResource(R.string.add_auth_method_mail)
+ AuthMethod.Type.Totp -> stringResource(R.string.add_auth_method_totp)
+ AuthMethod.Type.Unknown -> error("unknown auth method type")
+ },
+ onClick = { onClick(type) },
+ )
+}
+
+@Composable
private fun ChallengeCard(
modifier: Modifier = Modifier,
authMethod: AuthMethod,
@@ -223,15 +209,7 @@ private fun ChallengeCard(
) {
Column(modifier = Modifier.padding(LocalSpacing.current.medium)) {
Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(when (authMethod.type) {
- "question" -> Icons.Default.QuestionMark
- "sms" -> Icons.Default.Sms
- "email" -> Icons.Default.Email
- "iban" -> Icons.Default.AccountBalance
- "mail" -> Icons.Default.Mail
- "totp" -> Icons.Default.Token
- else -> error("unknown auth method")
- }, contentDescription = null)
+ Icon(authMethod.type.icon, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(authMethod.instructions, style = MaterialTheme.typography.titleMedium)
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 3fabd1a..303b82e 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
@@ -54,9 +54,7 @@ fun SelectContinentScreen(
title = stringResource(R.string.select_continent_title),
showPrev = false,
enableNext = selectedContinent != null,
- onBackClicked = {
- viewModel.goHome()
- },
+ onBackClicked = { viewModel.goHome() },
onNextClicked = {
selectedContinent?.let {
viewModel.reducerManager.selectContinent(it)
@@ -71,6 +69,7 @@ fun SelectContinentScreen(
) {
Picker(
label = stringResource(R.string.continent),
+ initialOption = selectedContinent?.name,
options = continents.map { it.name }.toSet(),
onOptionChanged = { option ->
continents.find { it.name == option }?.let { 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 639519f..4f5d753 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
@@ -53,12 +53,8 @@ fun SelectCountryScreen(
WizardPage(
title = stringResource(R.string.select_country_title),
enableNext = selectedCountry != null,
- onBackClicked = {
- viewModel.goHome()
- },
- onPrevClicked = {
- viewModel.reducerManager.back()
- },
+ onBackClicked = { viewModel.goHome() },
+ onPrevClicked = { viewModel.goBack() },
onNextClicked = {
selectedCountry?.let {
viewModel.reducerManager.selectCountry(it)
@@ -73,6 +69,7 @@ fun SelectCountryScreen(
) {
Picker(
label = stringResource(R.string.country),
+ initialOption = selectedCountry?.name,
options = countries.map { it.name }.toSet(),
onOptionChanged = { option ->
countries.find { it.name == option }?.let { 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 51d971f..6de45b5 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
@@ -35,6 +35,7 @@ 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 net.taler.anastasis.R
@@ -61,12 +62,8 @@ fun SelectUserAttributesScreen(
WizardPage(
title = stringResource(R.string.select_user_attributes_title),
- onBackClicked = {
- viewModel.goHome()
- },
- onPrevClicked = {
- viewModel.reducerManager.back()
- },
+ onBackClicked = { viewModel.goHome() },
+ onPrevClicked = { viewModel.goBack() },
onNextClicked = {
viewModel.reducerManager.enterUserAttributes(values)
},
@@ -74,6 +71,7 @@ fun SelectUserAttributesScreen(
LazyColumn(
modifier = Modifier
.fillMaxSize()
+ .nestedScroll(it)
.padding(LocalSpacing.current.medium),
verticalArrangement = Arrangement.Top,
) {
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt
index 6d30443..f8b580e 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt
@@ -14,13 +14,9 @@
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-package net.taler.anastasis.ui.backup
+package net.taler.anastasis.ui.dialogs
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -28,15 +24,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import net.taler.anastasis.R
import net.taler.anastasis.Utils
import net.taler.anastasis.models.AuthMethod
+import net.taler.anastasis.ui.forms.EditQuestionForm
@Composable
fun EditMethodDialog(
- type: String? = null,
+ type: AuthMethod.Type? = null,
method: AuthMethod? = null,
onMethodEdited: (method: AuthMethod) -> Unit,
onCancel: () -> Unit,
@@ -49,12 +45,13 @@ fun EditMethodDialog(
title = { Text(stringResource(R.string.add_challenge)) },
text = {
when(type ?: method?.type) {
- "question" -> EditQuestionForm(
+ AuthMethod.Type.Question -> EditQuestionForm(
method = localMethod,
onMethodEdited = {
localMethod = it
},
)
+ else -> {}
}
},
dismissButton = {
@@ -77,38 +74,3 @@ fun EditMethodDialog(
}
)
}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun EditQuestionForm(
- method: AuthMethod? = null,
- onMethodEdited: (method: AuthMethod) -> Unit,
-) {
- val localMethod = method ?: AuthMethod(
- type = "question",
- instructions = "",
- challenge = "",
- mimeType = "plain/text",
- )
-
- Column {
- OutlinedTextField(
- modifier = Modifier.fillMaxWidth(),
- value = localMethod.instructions,
- onValueChange = {
- onMethodEdited(localMethod.copy(instructions = it))
- },
- label = { Text(stringResource(R.string.question)) },
- )
-
- OutlinedTextField(
- modifier = Modifier.fillMaxWidth(),
- value = localMethod.challenge,
- onValueChange = {
- onMethodEdited(localMethod.copy(challenge = it))
- },
- label = { Text(stringResource(R.string.answer)) },
- )
- }
-
-} \ No newline at end of file
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
new file mode 100644
index 0000000..e9f0e2b
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditPolicyDialog.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.foundation.layout.fillMaxWidth
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import net.taler.anastasis.R
+import net.taler.anastasis.models.AuthMethod
+import net.taler.anastasis.models.AuthenticationProviderStatus
+import net.taler.anastasis.models.Policy
+import net.taler.anastasis.ui.forms.EditPolicyForm
+
+@Composable
+fun EditPolicyDialog(
+ policy: Policy? = null,
+ methods: List<AuthMethod>,
+ providers: Map<String, AuthenticationProviderStatus.Ok>,
+ onPolicyEdited: (policy: Policy) -> Unit,
+ onCancel: () -> Unit,
+) {
+ var localPolicy by remember { mutableStateOf(policy) }
+
+ AlertDialog(
+ onDismissRequest = onCancel,
+ title = { Text(stringResource(if (policy != null)
+ R.string.edit_policy else R.string.add_policy)) },
+ text = {
+ EditPolicyForm(
+ modifier = Modifier.fillMaxWidth(),
+ policy = localPolicy,
+ methods = methods,
+ providers = providers,
+ onPolicyEdited = {
+ localPolicy = it
+ }
+ )
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ onCancel()
+ }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ localPolicy?.let { onPolicyEdited(it) }
+ }) {
+ Text(stringResource(if (policy != null)
+ R.string.edit else R.string.add))
+ }
+ }
+ )
+
+}
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditPolicyForm.kt b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditPolicyForm.kt
new file mode 100644
index 0000000..4cc3661
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditPolicyForm.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.forms
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import net.taler.anastasis.models.AuthMethod
+import net.taler.anastasis.models.AuthenticationProviderStatus
+import net.taler.anastasis.models.Policy
+import net.taler.anastasis.ui.reusable.components.DropdownTextField
+import net.taler.anastasis.ui.theme.LocalSpacing
+
+@Composable
+fun EditPolicyForm(
+ modifier: Modifier = Modifier,
+ policy: Policy?,
+ methods: List<AuthMethod>,
+ providers: Map<String, AuthenticationProviderStatus.Ok>,
+ onPolicyEdited: (policy: Policy) -> Unit,
+) {
+ val localPolicy = policy ?: Policy(methods = listOf())
+ val localMethods = localPolicy.methods.associateBy { it.authenticationMethod }
+ val submitLocalMethods = { it: Map<Int, Policy.PolicyMethod> ->
+ onPolicyEdited(
+ localPolicy.copy(
+ methods = it.flatMap { entry ->
+ listOf(entry.value)
+ }
+ )
+ )
+ }
+
+ LazyColumn(
+ modifier = modifier,
+ ) {
+ items(count = methods.size) { index ->
+ val method = methods[index]
+ // Get only the providers that support this method type
+ val methodProviders = providers.filterValues { provider ->
+ method.type in provider.methods.map { it.type }
+ }.keys.toList()
+ val selectedProvider = localMethods[index]?.provider
+ val checked = selectedProvider != null
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(
+ enabled = checked,
+ checked = checked,
+ onCheckedChange = {
+ if (it) selectedProvider?.let { prov ->
+ submitLocalMethods(
+ localMethods.toMutableMap().apply {
+ this[index] = Policy.PolicyMethod(
+ authenticationMethod = index,
+ provider = prov,
+ )
+ }
+ )
+ } else {
+ submitLocalMethods(
+ localMethods.toMutableMap().apply {
+ remove(index)
+ }
+ )
+ }
+ },
+ )
+ DropdownTextField(
+ modifier = Modifier.padding(bottom = LocalSpacing.current.small),
+ label = method.instructions,
+ leadingIcon = {
+ Icon(
+ method.type.icon,
+ contentDescription = stringResource(method.type.nameRes),
+ )
+ },
+ selectedIndex = selectedProvider?.let{ methodProviders.indexOf(it) },
+ options = methodProviders,
+ onOptionSelected = {
+ submitLocalMethods(
+ localMethods.toMutableMap().apply {
+ this[index] = Policy.PolicyMethod(
+ authenticationMethod = index,
+ provider = methodProviders[it],
+ )
+ }
+ )
+ },
+ )
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditQuestionForm.kt b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditQuestionForm.kt
new file mode 100644
index 0000000..38017b9
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditQuestionForm.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.forms
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import net.taler.anastasis.R
+import net.taler.anastasis.models.AuthMethod
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EditQuestionForm(
+ method: AuthMethod? = null,
+ onMethodEdited: (method: AuthMethod) -> Unit,
+) {
+ val localMethod = method ?: AuthMethod(
+ type = AuthMethod.Type.Question,
+ instructions = "",
+ challenge = "",
+ mimeType = "plain/text",
+ )
+
+ val focusRequester1 = remember { FocusRequester() }
+ val focusRequester2 = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
+
+ Column {
+ OutlinedTextField(
+ modifier = Modifier
+ .focusRequester(focusRequester1)
+ .fillMaxWidth(),
+ value = localMethod.instructions,
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+ keyboardActions = KeyboardActions(onNext = { focusRequester2.requestFocus() }),
+ onValueChange = {
+ onMethodEdited(localMethod.copy(instructions = it))
+ },
+ label = { Text(stringResource(R.string.question)) },
+ )
+
+ OutlinedTextField(
+ modifier = Modifier
+ .focusRequester(focusRequester2)
+ .fillMaxWidth(),
+ value = localMethod.challenge,
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChange = {
+ onMethodEdited(localMethod.copy(challenge = it))
+ },
+ label = { Text(stringResource(R.string.answer)) },
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ focusRequester1.requestFocus()
+ }
+
+} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt
index 0fe727a..cbed1e9 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt
@@ -34,45 +34,38 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.toSize
-import androidx.compose.ui.window.PopupProperties
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropdownTextField(
modifier: Modifier = Modifier,
label: String,
- options: Set<String>,
- onOptionChanged: (String) -> Unit,
+ leadingIcon: (@Composable () -> Unit)? = null,
+ selectedIndex: Int? = null,
+ options: List<String>,
+ onOptionSelected: (index: Int) -> Unit,
) {
- var filteredOptions by remember { mutableStateOf(options.toList()) }
- var inputValue by remember { mutableStateOf(options.first()) }
var expanded by remember { mutableStateOf(false) }
var size by remember { mutableStateOf(Size.Zero) }
- val focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
- .wrapContentSize(Alignment.Center)
- .focusRequester(focusRequester),
+ .wrapContentSize(Alignment.Center),
) {
OutlinedTextField(
modifier = modifier
.onGloballyPositioned { coordinates ->
size = coordinates.size.toSize()
},
- value = inputValue,
- onValueChange = { value ->
- inputValue = value
- expanded = true
- filteredOptions = options.filter { it.contains(value) }
- },
+ readOnly = true,
+ leadingIcon = leadingIcon,
+ value = if (selectedIndex != null) options[selectedIndex] else "",
+ onValueChange = {},
singleLine = true,
label = { Text(label) },
trailingIcon = {
@@ -85,29 +78,23 @@ fun DropdownTextField(
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
- if (filteredOptions.isNotEmpty()) {
+ if (options.isNotEmpty()) {
DropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
},
- /*
- * TODO: we should NOT disable focus, but this will be necessary
- * until Google fixes ExposedDropdownMenuBox focus crash.
- */
- properties = PopupProperties(focusable = false),
modifier = Modifier
.width(with(LocalDensity.current) { size.width.toDp() }),
) {
- filteredOptions.forEach { s ->
+ options.forEachIndexed { i, s ->
DropdownMenuItem(
text = {
Text(text = s)
},
onClick = {
- inputValue = s
expanded = false
- onOptionChanged(s)
+ onOptionSelected(i)
}
)
}
@@ -122,8 +109,8 @@ fun DropdownTextFieldComposable() {
Surface {
DropdownTextField(
label = "Continent",
- options = setOf("Europe", "India", "Asia", "North America"),
- onOptionChanged = {},
+ options = listOf("Europe", "India", "Asia", "North America"),
+ onOptionSelected = {},
)
}
} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt
index cd8e798..3285f08 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt
@@ -45,11 +45,13 @@ import net.taler.anastasis.ui.theme.LocalSpacing
fun Picker(
modifier: Modifier = Modifier,
label: String,
+ leadingIcon: (@Composable () -> Unit)? = null,
+ initialOption: String? = null,
options: Set<String>,
onOptionChanged: (String) -> Unit,
) {
var filteredOptions by remember { mutableStateOf(options.toList()) }
- var inputValue by remember { mutableStateOf("") }
+ var inputValue by remember { mutableStateOf(initialOption ?: "") }
val keyboardController = LocalSoftwareKeyboardController.current
Column(
@@ -58,6 +60,7 @@ fun Picker(
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
+ leadingIcon = leadingIcon,
value = inputValue,
onValueChange = { value ->
inputValue = value
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt
index e6a24d6..f0b4556 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt
@@ -3,6 +3,7 @@ package net.taler.anastasis.ui.reusable.pages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -23,15 +24,18 @@ import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.taler.anastasis.R
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun WizardPage(
modifier: Modifier = Modifier,
@@ -43,9 +47,13 @@ fun WizardPage(
onBackClicked: () -> Unit = {},
onNextClicked: () -> Unit = {},
onPrevClicked: () -> Unit = {},
- content: @Composable () -> Unit,
+ content: @Composable (nestedScrollConnection: NestedScrollConnection) -> Unit,
) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+
Scaffold(
+ modifier = Modifier
+ .nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text(title) },
@@ -53,7 +61,8 @@ fun WizardPage(
IconButton(onClick = onBackClicked) {
Icon(Icons.Default.ArrowBack, stringResource(R.string.back))
}
- }
+ },
+ scrollBehavior = scrollBehavior,
)
},
) {
@@ -63,7 +72,7 @@ fun WizardPage(
Box(modifier = Modifier
.weight(1f)
.fillMaxWidth()) {
- content()
+ content(scrollBehavior.nestedScrollConnection)
}
Divider()
Row(
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 050e0f5..d157707 100644
--- a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt
@@ -56,7 +56,7 @@ class ReducerViewModel @Inject constructor(): ViewModel() {
reducerManager.startSyncingProviders()
Routes.SelectAuthMethods.route
}
- BackupStates.PoliciesReviewing -> TODO()
+ BackupStates.PoliciesReviewing -> Routes.ReviewPoliciesScreen.route
BackupStates.SecretEditing -> TODO()
BackupStates.TruthsPaying -> TODO()
BackupStates.PoliciesPaying -> TODO()
@@ -79,6 +79,34 @@ class ReducerViewModel @Inject constructor(): ViewModel() {
}
}
+ fun goBack(): Boolean = when (val state = reducerState.value) {
+ is ReducerState.Backup -> when (state.backupState) {
+ BackupStates.ContinentSelecting -> {
+ goHome()
+ false
+ }
+ else -> {
+ reducerManager.back()
+ false
+ }
+ }
+ is ReducerState.Recovery -> when(state.recoveryState) {
+ RecoveryStates.ContinentSelecting -> {
+ goHome()
+ false
+ }
+ else -> {
+ reducerManager.back()
+ false
+ }
+ }
+ is ReducerState.Error -> {
+ reducerManager.back()
+ false
+ }
+ else -> true
+ }
+
fun goHome() {
_reducerState.value = null
}
diff --git a/anastasis/src/main/res/values/strings.xml b/anastasis/src/main/res/values/strings.xml
index 627fc50..51bcd53 100644
--- a/anastasis/src/main/res/values/strings.xml
+++ b/anastasis/src/main/res/values/strings.xml
@@ -20,6 +20,9 @@
<string name="continent">Continent</string>
<string name="question">Security question</string>
<string name="answer">Answer</string>
+ <string name="policy_n">Policy %1$d</string>
+ <string name="provider">Provider</string>
+ <string name="unknown">Unknown</string>
<!-- Common -->
<string name="select_continent_title">Where do you live?</string>
@@ -28,13 +31,21 @@
<!-- Backup -->
<string name="select_auth_methods_title">Authentication methods</string>
- <string name="auth_method_question">Add a question challenge</string>
- <string name="auth_method_sms">Add a SMS challenge</string>
- <string name="auth_method_email">Add an e-mail challenge</string>
- <string name="auth_method_iban">Add an IBAN provider</string>
- <string name="auth_method_mail">Add a physical mail provider</string>
- <string name="auth_method_totp">Add a TOTP challenge</string>
+ <string name="review_policies_title">Recovery policies</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>
+ <string name="add_auth_method_iban">Add an IBAN provider</string>
+ <string name="add_auth_method_mail">Add a physical mail provider</string>
+ <string name="add_auth_method_totp">Add a TOTP challenge</string>
+ <string name="auth_method_question">Question</string>
+ <string name="auth_method_sms">SMS</string>
+ <string name="auth_method_email">E-mail</string>
+ <string name="auth_method_iban">IBAN</string>
+ <string name="auth_method_mail">Mail</string>
+ <string name="auth_method_totp">TOTP</string>
<string name="backup_providers">Manage backup providers</string>
<string name="add_challenge">Add challenge</string>
- <string name="edit_challenge">Edit challenge</string>
+ <string name="add_policy">Add policy</string>
+ <string name="edit_policy">Edit policy</string>
</resources> \ No newline at end of file
diff --git a/anastasis/src/main/res/values/themes.xml b/anastasis/src/main/res/values/themes.xml
index a5484d9..c413fd9 100644
--- a/anastasis/src/main/res/values/themes.xml
+++ b/anastasis/src/main/res/values/themes.xml
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <style name="Theme.Anastasis" parent="android:Theme.Material.Light.NoActionBar" />
+ <style name="Theme.Anastasis" parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ </style>
</resources> \ No newline at end of file