summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-07-28 23:20:08 -0600
committerIván Ávalos <avalos@disroot.org>2023-11-11 13:20:09 -0600
commita31ca9fa74e07bc06af156c83592471ac3964121 (patch)
treea35381294ade8ebe081eeec958e2c156f9939b14
parent22da595621eb544c35629324a99973df1ac3b3de (diff)
downloadtaler-android-a31ca9fa74e07bc06af156c83592471ac3964121.tar.gz
taler-android-a31ca9fa74e07bc06af156c83592471ac3964121.tar.bz2
taler-android-a31ca9fa74e07bc06af156c83592471ac3964121.zip
Initial challenges screen + multiple fixes
Signed-off-by: Iván Ávalos <avalos@disroot.org>
-rw-r--r--anastasis/build.gradle.kts2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/MainActivity.kt4
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/Routes.kt3
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/Utils.kt30
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt26
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt74
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt36
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt78
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt114
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt254
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt19
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt15
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt14
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt10
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt6
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Form.kt88
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt23
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt6
-rw-r--r--anastasis/src/main/res/values/strings.xml22
20 files changed, 734 insertions, 92 deletions
diff --git a/anastasis/build.gradle.kts b/anastasis/build.gradle.kts
index 7aceb34..eedf4f2 100644
--- a/anastasis/build.gradle.kts
+++ b/anastasis/build.gradle.kts
@@ -66,6 +66,7 @@ dependencies {
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material:material")
implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.compose.material:material-icons-extended:1.4.3")
@@ -73,6 +74,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation("com.google.dagger:hilt-android:2.44")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
+ implementation("io.matthewnelson.encoding:base32:2.0.0")
kapt("com.google.dagger:hilt-android-compiler:2.44")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
diff --git a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
index 4868357..18c83cd 100644
--- a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
@@ -13,6 +13,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.SelectAuthMethodsScreen
import net.taler.anastasis.ui.common.SelectContinentScreen
import net.taler.anastasis.ui.common.SelectCountryScreen
import net.taler.anastasis.ui.common.SelectUserAttributesScreen
@@ -55,6 +56,9 @@ fun MainNavHost(
Routes.SelectUserAttributes.route -> {
SelectUserAttributesScreen()
}
+ Routes.SelectAuthMethods.route -> {
+ SelectAuthMethodsScreen()
+ }
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 1a0accc..6d00cad 100644
--- a/anastasis/src/main/java/net/taler/anastasis/Routes.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/Routes.kt
@@ -11,6 +11,9 @@ sealed class Routes(
object SelectCountry: Routes("select_country")
object SelectUserAttributes: Routes("select_user_attributes")
+ // Backup
+ object SelectAuthMethods: Routes("select_auth_methods")
+
// 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 0ddd54d..8673a05 100644
--- a/anastasis/src/main/java/net/taler/anastasis/Utils.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/Utils.kt
@@ -17,11 +17,21 @@
package net.taler.anastasis
import android.os.Build
+import io.matthewnelson.encoding.base32.Base32
+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.LocalDate
import kotlinx.datetime.toJavaLocalDate
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import org.json.JSONObject
+import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.util.Locale
+import kotlin.time.Duration
object Utils {
fun formatDate(date: LocalDate): String {
@@ -34,4 +44,24 @@ object Utils {
return formatter.format(date)
}
}
+
+ inline fun <reified T> Json.encodeToNativeJson(value: T): JSONObject =
+ JSONObject(encodeToString(value))
+
+ fun encodeBase32 (input: String) = input
+ .toByteArray(Charset.defaultCharset())
+ .encodeToString(Base32.Crockford)
+
+ fun decodeBase32 (input: String) = input
+ .decodeToByteArray(Base32.Crockford)
+ .toString(Charset.defaultCharset())
+
+ // Source: https://stackoverflow.com/questions/54827455/how-to-implement-timer-with-kotlin-coroutines/54828055#54828055
+ fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow {
+ delay(initialDelay)
+ while (true) {
+ emit(Unit)
+ delay(period)
+ }
+ }
} \ No newline at end of file
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 42ce566..c179d8f 100644
--- a/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt
@@ -22,6 +22,7 @@ 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.models.ReducerState
import net.taler.common.ApiResponse
import net.taler.common.ApiResponse.*
@@ -83,4 +84,29 @@ class AnastasisReducerApi() {
WalletResponse.Error(info)
}
}
+
+ suspend inline fun <reified T> reduceAction(
+ state: ReducerState,
+ action: String,
+ args: T? = null,
+ ): WalletResponse<ReducerState> = withContext(Dispatchers.Default) {
+ val json = BackendManager.json
+ val body = JSONObject().apply {
+ put("state", json.encodeToNativeJson(state))
+ put("action", action)
+ if (args != null) put("args", json.encodeToNativeJson(args))
+ }
+ try {
+ when (val response = sendRequest("anastasisReduce", body)) {
+ is Response -> {
+ val t = json.decodeFromJsonElement<ReducerState>(response.result)
+ WalletResponse.Success(t)
+ }
+ is Error -> error("invalid reducer response")
+ }
+ } catch (e: Exception) {
+ val info = TalerErrorInfo(NONE, "", e.toString())
+ WalletResponse.Error(info)
+ }
+ }
}
diff --git a/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt b/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt
new file mode 100644
index 0000000..c87e76d
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/models/ReducerArgs.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.models
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+sealed class ReducerArgs {
+
+ @Serializable
+ data class EnterUserAttributes(
+ @SerialName("identity_attributes")
+ val identityAttributes: Map<String, String>,
+ )
+
+ // TODO: ActionArgsAddProvider
+
+ // TODO: ActionArgsDeleteProvider
+
+ @Serializable
+ data class AddAuthentication(
+ @SerialName("authentication_method")
+ val authenticationMethod: AuthMethod,
+ )
+
+ // TODO: ActionArgsDeleteAuthentication
+
+ // TODO: ActionArgsDeletePolicy
+
+ // TODO: ActionArgsEnterSecretName
+
+ // TODO: ActionArgsEnterSecret
+
+ @Serializable
+ data class SelectContinent(
+ val continent: String,
+ )
+
+ @Serializable
+ data class SelectCountry(
+ @SerialName("country_code")
+ val countryCode: String,
+ )
+
+ // TODO: ActionArgsSelectChallenge
+
+ // TODO: ActionArgsSolveChallengeRequest
+
+ // TODO: ActionArgsAddPolicy
+
+ // TODO: ActionArgsUpdateExpiration
+
+ // TODO: ActionArgsUpdateExpiration
+
+ // TODO: ActionArgsChangeVersion
+
+ // TODO: ActionArgsUpdatePolicy
+
+} \ No newline at end of file
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 37f380f..83afc88 100644
--- a/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt
@@ -305,7 +305,7 @@ sealed class AuthenticationProviderStatus {
val annualFee: String,
@SerialName("business_name")
val businessName: String,
- val currency: String,
+ val currency: String? = null,
@SerialName("http_status")
val httpStatus: Int,
@SerialName("liability_limit")
@@ -335,30 +335,6 @@ sealed class AuthenticationProviderStatus {
// TODO: ReducerStateBackupUserAttributesCollecting
-// TODO: ActionArgsEnterUserAttributes
-
-// TODO: ActionArgsAddProvider
-
-// TODO: ActionArgsDeleteProvider
-
-// TODO: ActionArgsAddAuthentication
-
-// TODO: ActionArgsDeleteAuthentication
-
-// TODO: ActionArgsDeletePolicy
-
-// TODO: ActionArgsEnterSecretName
-
-// TODO: ActionArgsEnterSecret
-
-// TODO: ActionArgsSelectContinent
-
-// TODO: ActionArgsSelectCountry
-
-// TODO: ActionArgsSelectChallenge
-
-// TODO: ActionArgsSolveChallengeRequest
-
// TODO: SolveChallengeAnswerRequest
// TODO: SolveChallengePinRequest
@@ -367,12 +343,6 @@ sealed class AuthenticationProviderStatus {
// TODO: PolicyMember
-// TODO: ActionArgsAddPolicy
-
-// TODO: ActionArgsUpdateExpiration
-
-// TODO: ActionArgsUpdateExpiration
-
@Serializable
data class SelectedVersionInfoProviders(
val url: String,
@@ -385,10 +355,6 @@ data class SelectedVersionInfo(
val providers: SelectedVersionInfoProviders,
)
-// TODO: ActionArgsChangeVersion
-
-// TODO: ActionArgsUpdatePolicy
-
// TODO: DiscoveryCursor
// TODO: PolicyMetaInfo
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 90a5158..0aed98f 100644
--- a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
@@ -16,21 +16,35 @@
package net.taler.anastasis.reducers
+import android.util.Log
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
+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 kotlinx.serialization.json.encodeToJsonElement
+import net.taler.anastasis.Utils
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.ReducerArgs
import net.taler.anastasis.models.ReducerState
+import org.json.JSONObject
+import kotlin.time.Duration.Companion.seconds
class ReducerManager(
private val state: MutableStateFlow<ReducerState?>,
private val api: AnastasisReducerApi,
private val scope: CoroutineScope,
) {
+ private companion object {
+ const val PROVIDER_SYNC_PERIOD = 20
+ }
+
+ private var providerSyncingJob: Job? = null
+
// TODO: error handling!
fun startBackup() = scope.launch {
@@ -75,7 +89,65 @@ class ReducerManager(
fun enterUserAttributes(userAttributes: Map<String, String>) = scope.launch {
state.value?.let { initialState ->
api.reduceAction(initialState, "enter_user_attributes") {
- put("identity_attributes", Json.encodeToJsonElement(userAttributes))
+ put("identity_attributes", JSONObject(userAttributes))
+ }.onSuccess { newState ->
+ state.value = newState
+ }
+ }
+ }
+
+ fun startSyncingProviders() {
+ if (providerSyncingJob != null) return
+ providerSyncingJob = Utils.tickerFlow(PROVIDER_SYNC_PERIOD.seconds)
+ .onEach {
+ state.value?.let { initialState ->
+ // Only run sync when not all providers are synced
+ if (initialState is ReducerState.Backup) {
+ initialState.authenticationProviders?.flatMap {
+ listOf(it.value)
+ }?.fold(false) { a, b ->
+ a || (b !is AuthenticationProviderStatus.Ok)
+ }?.let { sync ->
+ if (!sync) {
+ Log.d("ReducerManager", "All providers are synced")
+ return@onEach
+ }
+ }
+ }
+ Log.d("ReducerManager", "Syncing providers...")
+ api.reduceAction(initialState, "sync_providers")
+ .onSuccess { newState ->
+ state.value = newState
+ }
+ .onError {
+ Log.d("ReducerManager", "Sync error: $it")
+ }
+ }
+ }
+ .catch {
+ Log.d("ReducerManager", "Could not sync providers")
+ }
+ .launchIn(scope)
+ }
+
+ fun stopSyncingProviders() {
+ providerSyncingJob?.cancel()
+ providerSyncingJob = null
+ }
+
+ fun addAuthentication(args: ReducerArgs.AddAuthentication) = scope.launch {
+ state.value?.let { initialState ->
+ api.reduceAction(initialState, "add_authentication", args)
+ .onSuccess { newState ->
+ state.value = newState
+ }
+ }
+ }
+
+ fun deleteAuthentication(index: Int) = scope.launch {
+ state.value?.let { initialState ->
+ api.reduceAction(initialState, "delete_authentication") {
+ put("index", index)
}.onSuccess { newState ->
state.value = newState
}
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt
new file mode 100644
index 0000000..6d30443
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditMethodDialog.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.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
+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
+
+@Composable
+fun EditMethodDialog(
+ type: String? = null,
+ method: AuthMethod? = null,
+ onMethodEdited: (method: AuthMethod) -> Unit,
+ onCancel: () -> Unit,
+) {
+ var localMethod by remember { mutableStateOf(method?.copy(
+ challenge = Utils.decodeBase32(method.challenge),
+ )) }
+ AlertDialog(
+ onDismissRequest = onCancel,
+ title = { Text(stringResource(R.string.add_challenge)) },
+ text = {
+ when(type ?: method?.type) {
+ "question" -> EditQuestionForm(
+ method = localMethod,
+ onMethodEdited = {
+ localMethod = it
+ },
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ onCancel()
+ }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ localMethod?.let { onMethodEdited(
+ it.copy(
+ challenge = Utils.encodeBase32(it.challenge)
+ )
+ ) }
+ }) {
+ Text(stringResource(R.string.add))
+ }
+ }
+ )
+}
+
+@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/backup/SelectAuthMethodsScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt
new file mode 100644
index 0000000..3bdd129
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt
@@ -0,0 +1,254 @@
+/*
+ * 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 android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+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.padding
+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
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+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.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import net.taler.anastasis.R
+import net.taler.anastasis.Utils
+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.reusable.components.ActionCard
+import net.taler.anastasis.ui.reusable.pages.WizardPage
+import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.ReducerViewModel
+
+@Composable
+fun SelectAuthMethodsScreen(
+ viewModel: ReducerViewModel = hiltViewModel(),
+) {
+ val reducerState by viewModel.reducerState.collectAsState()
+
+ val authProviders = when (val state = reducerState) {
+ is ReducerState.Backup -> state.authenticationProviders
+ else -> error("invalid reducer state type")
+ } ?: emptyMap()
+
+ // Get only 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 }
+ } 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 method by remember { mutableStateOf<AuthMethod?>(null) }
+
+ val reset = {
+ showEditDialog = false
+ methodType = null
+ method = null
+ }
+
+ if (showEditDialog) {
+ EditMethodDialog(
+ type = methodType,
+ method = method,
+ onCancel = {
+ reset()
+ },
+ onMethodEdited = {
+ reset()
+ Log.d("onMethodEdited", it.challenge)
+ viewModel.reducerManager.addAuthentication(
+ ReducerArgs.AddAuthentication(it)
+ )
+ }
+ )
+ }
+
+ WizardPage(
+ title = stringResource(R.string.select_auth_methods_title),
+ onBackClicked = {
+ viewModel.goHome()
+ },
+ onPrevClicked = {
+ viewModel.reducerManager.back()
+ },
+ ) {
+ AuthMethods(
+ availableMethods = availableMethods,
+ selectedMethods = selectedMethods,
+ onAddMethod = {
+ methodType = it
+ showEditDialog = true
+ },
+ onDeleteMethod = {
+ viewModel.reducerManager.deleteAuthentication(it)
+ },
+ )
+ }
+}
+
+@Composable
+private fun AuthMethods(
+ availableMethods: List<String>,
+ selectedMethods: List<AuthMethod>,
+ onAddMethod: (type: String) -> Unit,
+ onDeleteMethod: (index: Int) -> Unit,
+) {
+ LazyColumn(
+ modifier = Modifier
+ .padding(LocalSpacing.current.medium)
+ .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 -> {}
+ }
+ }
+
+ item {
+ Divider(Modifier.padding(bottom = LocalSpacing.current.small))
+ }
+
+ items(count = selectedMethods.size) { i ->
+ ChallengeCard(
+ modifier = Modifier
+ .padding(bottom = LocalSpacing.current.small)
+ .fillMaxWidth(),
+ authMethod = selectedMethods[i],
+ onDelete = { onDeleteMethod(i) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun ChallengeCard(
+ modifier: Modifier = Modifier,
+ authMethod: AuthMethod,
+ onDelete: () -> Unit,
+) {
+ ElevatedCard(
+ modifier = modifier,
+ ) {
+ 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)
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Text(authMethod.instructions, style = MaterialTheme.typography.titleMedium)
+ Text(
+ text = Utils.decodeBase32(authMethod.challenge),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(top = 5.dp)
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ IconButton(onClick = onDelete) {
+ Icon(
+ Icons.Default.Delete,
+ contentDescription = stringResource(R.string.delete),
+ )
+ }
+ }
+ }
+ }
+} \ No newline at end of file
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 b8ec266..3fabd1a 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
@@ -20,10 +20,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -49,21 +45,18 @@ fun SelectContinentScreen(
val continents = when (val state = reducerState) {
is ReducerState.Backup -> state.continents
is ReducerState.Recovery -> state.continents
- else -> null
+ else -> error("invalid reducer state type")
} ?: emptyList()
var selectedContinent by remember { mutableStateOf<ContinentInfo?>(null) }
WizardPage(
- title = stringResource(R.string.select_country_title),
- navigationIcon = {
- IconButton(onClick = {
- viewModel.goHome()
- }) {
- Icon(Icons.Default.ArrowBack, "back")
- }
- },
+ title = stringResource(R.string.select_continent_title),
showPrev = false,
+ enableNext = selectedContinent != null,
+ onBackClicked = {
+ viewModel.goHome()
+ },
onNextClicked = {
selectedContinent?.let {
viewModel.reducerManager.selectContinent(it)
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 1c4fa49..639519f 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
@@ -20,10 +20,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -49,19 +45,16 @@ fun SelectCountryScreen(
val countries = when (val state = reducerState) {
is ReducerState.Backup -> state.countries
is ReducerState.Recovery -> state.countries
- else -> null
+ else -> error("invalid reducer state type")
} ?: emptyList()
var selectedCountry by remember { mutableStateOf<CountryInfo?>(null) }
WizardPage(
title = stringResource(R.string.select_country_title),
- navigationIcon = {
- IconButton(onClick = {
- viewModel.goHome()
- }) {
- Icon(Icons.Default.ArrowBack, "back")
- }
+ enableNext = selectedCountry != null,
+ onBackClicked = {
+ viewModel.goHome()
},
onPrevClicked = {
viewModel.reducerManager.back()
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 6fa9f97..51d971f 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
@@ -24,11 +24,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -58,19 +54,15 @@ fun SelectUserAttributesScreen(
val userAttributes = when (val state = reducerState) {
is ReducerState.Backup -> state.requiredAttributes
is ReducerState.Recovery -> state.requiredAttributes
- else -> null
+ else -> error("invalid reducer state type")
} ?: emptyList()
val values = remember { mutableStateMapOf<String, String>() }
WizardPage(
title = stringResource(R.string.select_user_attributes_title),
- navigationIcon = {
- IconButton(onClick = {
- viewModel.goHome()
- }) {
- Icon(Icons.Default.ArrowBack, "back")
- }
+ onBackClicked = {
+ viewModel.goHome()
},
onPrevClicked = {
viewModel.reducerManager.back()
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt
index f79c3a2..cf24c48 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt
@@ -16,10 +16,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ui.reusable.components.ActionCard
+import net.taler.anastasis.ui.theme.LocalSpacing
import net.taler.anastasis.viewmodels.ReducerViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -39,14 +39,14 @@ fun HomeScreen(
Column(
modifier = Modifier
.padding(it)
- .padding(16.dp),
+ .padding(LocalSpacing.current.medium),
verticalArrangement = Arrangement.SpaceEvenly,
) {
// Backup
ActionCard(
modifier = Modifier
.weight(1f)
- .padding(bottom = 8.dp)
+ .padding(bottom = LocalSpacing.current.small)
.fillMaxWidth(),
icon = { Icon(Icons.Outlined.Upload, null) },
headline = stringResource(R.string.backup_secret),
@@ -59,7 +59,7 @@ fun HomeScreen(
ActionCard(
modifier = Modifier
.weight(1f)
- .padding(bottom = 8.dp)
+ .padding(bottom = LocalSpacing.current.small)
.fillMaxWidth(),
icon = { Icon(Icons.Outlined.Download, null) },
headline = stringResource(R.string.recover_secret),
@@ -72,7 +72,7 @@ fun HomeScreen(
ActionCard(
modifier = Modifier
.weight(1f)
- .padding(bottom = 8.dp)
+ .padding(bottom = LocalSpacing.current.small)
.fillMaxWidth(),
icon = { Icon(Icons.Outlined.Restore, null) },
headline = stringResource(R.string.restore_session),
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt
index 72d3cbb..6eea2ce 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt
@@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import net.taler.anastasis.ui.theme.LocalSpacing
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -32,15 +33,14 @@ fun ActionCard(
ElevatedCard(
modifier = modifier,
onClick = onClick,
-
) {
- Column(modifier = Modifier.padding(16.dp)) {
+ Column(modifier = Modifier.padding(LocalSpacing.current.medium)) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (icon != null) {
icon()
Spacer(modifier = Modifier.width(12.dp))
}
- Text(headline, style = MaterialTheme.typography.titleLarge)
+ Text(headline, style = MaterialTheme.typography.titleMedium)
}
subhead?.let {
Text(
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Form.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Form.kt
new file mode 100644
index 0000000..acf3fa4
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Form.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.reusable.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+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.material3.Button
+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
+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 androidx.compose.ui.tooling.preview.Preview
+import net.taler.anastasis.R
+import net.taler.anastasis.ui.theme.LocalSpacing
+
+@Composable
+fun Form(
+ modifier: Modifier = Modifier,
+ button: @Composable () -> Unit,
+ onCancelClicked: () -> Unit,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Column(modifier = modifier) {
+ Column (content = content)
+ Spacer(Modifier.height(LocalSpacing.current.medium))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceAround,
+ ) {
+ TextButton(
+ onClick = onCancelClicked,
+ ) {
+ Text(stringResource(R.string.cancel))
+ Spacer(Modifier.weight(1f))
+ button()
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+fun FormPreview() {
+ Form(
+ modifier = Modifier.fillMaxWidth(),
+ button = {
+ Button(onClick = {}) {
+ Text("Create")
+ }
+ },
+ onCancelClicked = {}
+ ) {
+ var email by remember { mutableStateOf("") }
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = email,
+ onValueChange = { email = it },
+ label = { Text(stringResource(R.string.question)) },
+ )
+ }
+} \ 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 4c649a6..cd8e798 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
@@ -49,7 +49,7 @@ fun Picker(
onOptionChanged: (String) -> Unit,
) {
var filteredOptions by remember { mutableStateOf(options.toList()) }
- var inputValue by remember { mutableStateOf(options.first()) }
+ var inputValue by remember { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current
Column(
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 77b467a..e6a24d6 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
@@ -26,17 +26,21 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+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)
@Composable
fun WizardPage(
modifier: Modifier = Modifier,
title: String,
- navigationIcon: @Composable () -> Unit = {},
+ enableNext: Boolean = true,
+ enablePrev: Boolean = true,
showNext: Boolean = true,
showPrev: Boolean = true,
+ onBackClicked: () -> Unit = {},
onNextClicked: () -> Unit = {},
onPrevClicked: () -> Unit = {},
content: @Composable () -> Unit,
@@ -45,7 +49,11 @@ fun WizardPage(
topBar = {
LargeTopAppBar(
title = { Text(title) },
- navigationIcon = navigationIcon,
+ navigationIcon = {
+ IconButton(onClick = onBackClicked) {
+ Icon(Icons.Default.ArrowBack, stringResource(R.string.back))
+ }
+ }
)
},
) {
@@ -64,6 +72,7 @@ fun WizardPage(
) {
if (showPrev) {
TextButton(
+ enabled = enablePrev,
onClick = onPrevClicked,
) {
Icon(
@@ -72,7 +81,7 @@ fun WizardPage(
modifier = Modifier.size(ButtonDefaults.IconSize),
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("Previous")
+ Text(stringResource(R.string.previous))
}
}
@@ -80,9 +89,10 @@ fun WizardPage(
if (showNext) {
Button(
+ enabled = enableNext,
onClick = onNextClicked,
) {
- Text("Next")
+ Text(stringResource(R.string.next))
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Icon(
Icons.Default.NavigateNext,
@@ -101,11 +111,6 @@ fun WizardPage(
fun WizardPagePreview() {
WizardPage(
title = "Title",
- navigationIcon = {
- IconButton(onClick = {}) {
- Icon(Icons.Default.ArrowBack, null)
- }
- },
) {
Box (
modifier = Modifier.fillMaxSize(),
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 3f5671b..050e0f5 100644
--- a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt
@@ -46,12 +46,16 @@ class ReducerViewModel @Inject constructor(): ViewModel() {
viewModelScope.launch {
_reducerState.collect {
Log.d("ReducerViewModel", it?.toString() ?: "nothing")
+ reducerManager.stopSyncingProviders()
_navRoute.value = when (it) {
is ReducerState.Backup -> when (it.backupState) {
BackupStates.ContinentSelecting -> Routes.SelectContinent.route
BackupStates.CountrySelecting -> Routes.SelectCountry.route
BackupStates.UserAttributesCollecting -> Routes.SelectUserAttributes.route
- BackupStates.AuthenticationsEditing -> TODO()
+ BackupStates.AuthenticationsEditing -> {
+ reducerManager.startSyncingProviders()
+ Routes.SelectAuthMethods.route
+ }
BackupStates.PoliciesReviewing -> TODO()
BackupStates.SecretEditing -> TODO()
BackupStates.TruthsPaying -> TODO()
diff --git a/anastasis/src/main/res/values/strings.xml b/anastasis/src/main/res/values/strings.xml
index de788ce..627fc50 100644
--- a/anastasis/src/main/res/values/strings.xml
+++ b/anastasis/src/main/res/values/strings.xml
@@ -8,11 +8,33 @@
<string name="restore_session">Restore session</string>
<!-- Shared -->
+ <string name="back">Go back</string>
+ <string name="next">Next</string>
+ <string name="previous">Previous</string>
+ <string name="add">Add</string>
+ <string name="edit">Edit</string>
+ <string name="delete">Delete</string>
+ <string name="cancel">Cancel</string>
+ <string name="menu">Menu</string>
<string name="country">Country</string>
<string name="continent">Continent</string>
+ <string name="question">Security question</string>
+ <string name="answer">Answer</string>
<!-- Common -->
+ <string name="select_continent_title">Where do you live?</string>
<string name="select_country_title">Where do you live?</string>
<string name="select_user_attributes_title">Who are you?</string>
+
+ <!-- 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="backup_providers">Manage backup providers</string>
+ <string name="add_challenge">Add challenge</string>
+ <string name="edit_challenge">Edit challenge</string>
</resources> \ No newline at end of file