summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-08-01 00:55:21 -0600
committerIván Ávalos <avalos@disroot.org>2023-11-11 13:20:09 -0600
commit7b7123eb657bc5e5b443f3b54c22a16d042dd874 (patch)
tree4a1fa7086f80a9e1f982be07c5f976609b589d6f
parentf66a4b0dc576e4a6102d1a4612784b4ff0212d86 (diff)
downloadtaler-android-7b7123eb657bc5e5b443f3b54c22a16d042dd874.tar.gz
taler-android-7b7123eb657bc5e5b443f3b54c22a16d042dd874.tar.bz2
taler-android-7b7123eb657bc5e5b443f3b54c22a16d042dd874.zip
Secret editing + backup finished + Compose previews + lots of improvements
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.kt10
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/Routes.kt4
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt196
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt110
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt98
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt69
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt26
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt34
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt60
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt8
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt85
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt4
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt58
-rw-r--r--anastasis/src/main/res/values/strings.xml4
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt2
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt (renamed from taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt)28
18 files changed, 748 insertions, 52 deletions
diff --git a/anastasis/build.gradle.kts b/anastasis/build.gradle.kts
index eedf4f2..52a9ec5 100644
--- a/anastasis/build.gradle.kts
+++ b/anastasis/build.gradle.kts
@@ -27,7 +27,7 @@ android {
buildTypes {
release {
- isMinifyEnabled = false
+ isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
diff --git a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
index 653d8a5..80b74bf 100644
--- a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt
@@ -15,6 +15,8 @@ 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.BackupFinishedScreen
+import net.taler.anastasis.ui.backup.EditSecretScreen
import net.taler.anastasis.ui.backup.ReviewPoliciesScreen
import net.taler.anastasis.ui.backup.SelectAuthMethodsScreen
import net.taler.anastasis.ui.common.SelectContinentScreen
@@ -79,9 +81,15 @@ fun MainNavHost(
Routes.SelectAuthMethods.route -> {
SelectAuthMethodsScreen()
}
- Routes.ReviewPoliciesScreen.route -> {
+ Routes.ReviewPolicies.route -> {
ReviewPoliciesScreen()
}
+ Routes.EditSecret.route -> {
+ EditSecretScreen()
+ }
+ Routes.BackupFinished.route -> {
+ BackupFinishedScreen()
+ }
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 5258e7f..e3242af 100644
--- a/anastasis/src/main/java/net/taler/anastasis/Routes.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/Routes.kt
@@ -13,7 +13,9 @@ sealed class Routes(
// Backup
object SelectAuthMethods: Routes("select_auth_methods")
- object ReviewPoliciesScreen: Routes("review_policies")
+ object ReviewPolicies: Routes("review_policies")
+ object EditSecret: Routes("edit_secret")
+ object BackupFinished: Routes("backup_finished")
// Restore
object RestoreInit: Routes("restore")
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 a074044..b3483d5 100644
--- a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
@@ -218,7 +218,7 @@ class ReducerManager(
this@ReducerManager.next()
}.onError { onError(it) }
}
- }
+ }.onError { onError(it) }
}
}
} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt
new file mode 100644
index 0000000..fc89caa
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CloudDone
+import androidx.compose.material3.ElevatedCard
+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.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import net.taler.anastasis.R
+import net.taler.anastasis.models.AuthenticationProviderStatus
+import net.taler.anastasis.models.BackupStates
+import net.taler.anastasis.models.ReducerState
+import net.taler.anastasis.models.SuccessDetail
+import net.taler.anastasis.shared.Utils
+import net.taler.anastasis.ui.reusable.pages.WizardPage
+import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
+import net.taler.common.Timestamp
+
+@Composable
+fun BackupFinishedScreen(
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
+) {
+ val state by viewModel.reducerState.collectAsState()
+ val reducerState = state as? ReducerState.Backup
+ ?: error("invalid reducer state type")
+
+ // Get only providers with "ok" status
+ val providers = remember(reducerState.authenticationProviders) {
+ reducerState.authenticationProviders?.filter {
+ it.value is AuthenticationProviderStatus.Ok
+ }?.mapValues { it.value as AuthenticationProviderStatus.Ok } ?: emptyMap()
+ }
+
+ val details = reducerState.successDetails ?: emptyMap()
+
+ WizardPage(
+ title = stringResource(R.string.backup_finished_title),
+ onBackClicked = { viewModel.goHome() },
+ showNext = false,
+ showPrev = false,
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .nestedScroll(it),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ item {
+ Box(
+ modifier = Modifier
+ .padding(LocalSpacing.current.large)
+ .background(MaterialTheme.colorScheme.primary, shape = CircleShape)
+ .fillMaxWidth(0.4f)
+ .aspectRatio(1f),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ Icons.Default.CloudDone,
+ modifier = Modifier.fillMaxSize(0.5f),
+ tint = MaterialTheme.colorScheme.onPrimary,
+ contentDescription = stringResource(R.string.success),
+ )
+ }
+ }
+ item {
+ Text(
+ stringResource(R.string.backup_stored_providers),
+ modifier = Modifier.padding(LocalSpacing.current.medium),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ items(items = details.keys.toList()) { url ->
+ val provider = providers[url] ?: return@items
+ val detail = details[url] ?: return@items
+ ProviderCard(
+ modifier = Modifier
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ )
+ .fillMaxWidth(),
+ provider = provider,
+ detail = detail,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ProviderCard(
+ modifier: Modifier,
+ provider: AuthenticationProviderStatus.Ok,
+ detail: SuccessDetail,
+) {
+ ElevatedCard(
+ modifier = modifier,
+ ) {
+ Column(
+ modifier = Modifier.padding(LocalSpacing.current.medium)
+ ) {
+ val date = Utils.formatDate(
+ Instant
+ .fromEpochMilliseconds(detail.policyExpiration.ms)
+ .toLocalDateTime(TimeZone.currentSystemDefault())
+ .date,
+ )
+ Text(
+ provider.businessName,
+ modifier = Modifier.padding(bottom = LocalSpacing.current.small),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ Text(
+ stringResource(
+ R.string.backup_policy_detail,
+ detail.policyVersion,
+ date,
+ )
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun BackupFinishedScreenPreview() {
+ BackupFinishedScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.BackupFinished,
+ authenticationProviders = mapOf(
+ "https://localhost:8088/" to AuthenticationProviderStatus.Ok(
+ httpStatus = 200,
+ methods = listOf(),
+ annualFee = "EUR:0.99",
+ truthUploadFee = "EUR:3.99",
+ liabilityLimit = "EUR:1",
+ currency = "EUR",
+ storageLimitInMegabytes = 1,
+ businessName = "Anastasis 42",
+ providerSalt = "BXAPCKSH9D3MYJTS9536RHJHCX",
+ )
+ ),
+ successDetails = mapOf(
+ "https://localhost:8088/" to SuccessDetail(
+ policyVersion = 1,
+ policyExpiration = Timestamp.now(),
+ ),
+ ),
+ ),
+ ),
+ )
+} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt
new file mode 100644
index 0000000..9706ceb
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.fillMaxSize
+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.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import net.taler.anastasis.R
+import net.taler.anastasis.models.BackupStates
+import net.taler.anastasis.models.CoreSecret
+import net.taler.anastasis.models.ReducerArgs
+import net.taler.anastasis.models.ReducerState
+import net.taler.anastasis.ui.forms.EditSecretForm
+import net.taler.anastasis.ui.reusable.pages.WizardPage
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
+import net.taler.common.Amount
+import net.taler.common.CryptoUtils
+import net.taler.common.Timestamp
+
+@Composable
+fun EditSecretScreen(
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
+) {
+ val state by viewModel.reducerState.collectAsState()
+ val reducerState = state as? ReducerState.Backup
+ ?: error("invalid reducer state type")
+
+ var secretName by remember {
+ mutableStateOf(reducerState.secretName ?: "")
+ }
+ var secretValue by remember {
+ mutableStateOf(reducerState.coreSecret?.value?.let {
+ CryptoUtils.decodeCrock(it).toString(Charsets.UTF_8)
+ } ?: "")
+ }
+
+ WizardPage(
+ title = stringResource(R.string.edit_secret_title),
+ onBackClicked = { viewModel.goHome() },
+ onPrevClicked = { viewModel.goBack() },
+ onNextClicked = {
+ viewModel.reducerManager?.backupSecret(
+ name = secretName,
+ args = ReducerArgs.EnterSecret(
+ secret = ReducerArgs.EnterSecret.Secret(
+ value = CryptoUtils.encodeCrock(secretValue.toByteArray(Charsets.UTF_8)),
+ mime = "text/plain",
+ ),
+ expiration = null,
+ )
+ )
+ },
+ ) {
+ EditSecretForm(
+ modifier = Modifier.fillMaxSize(),
+ name = secretName,
+ value = secretValue,
+ ) { name, value, _ ->
+ secretName = name
+ secretValue = value
+ }
+ }
+}
+
+@Preview
+@Composable
+fun EditSecretScreenPreview() {
+ EditSecretScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.SecretEditing,
+ secretName = "_TALERWALLET_MyPinePhone",
+ coreSecret = CoreSecret(
+ value = "EDJP6WK5EG50",
+ mime = "text/plain",
+ ),
+ expiration = Timestamp.never(),
+ uploadFees = listOf(
+ ReducerState.Backup.UploadFee(
+ fee = Amount("KUDOS", 42L, 0),
+ ),
+ ),
+ ),
+ ),
+ )
+} \ 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
index c42e7bc..edd6ddb 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt
@@ -49,22 +49,27 @@ 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.compose.ui.tooling.preview.Preview
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.BackupStates
+import net.taler.anastasis.models.MethodSpec
import net.taler.anastasis.models.Policy
import net.taler.anastasis.models.ReducerState
import net.taler.anastasis.ui.dialogs.EditPolicyDialog
import net.taler.anastasis.ui.forms.EditPolicyForm
import net.taler.anastasis.ui.reusable.pages.WizardPage
import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReviewPoliciesScreen(
- viewModel: ReducerViewModel = hiltViewModel(),
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
) {
val state by viewModel.reducerState.collectAsState()
val reducerState = state as? ReducerState.Backup
@@ -97,9 +102,9 @@ fun ReviewPoliciesScreen(
onCancel = { reset() },
onPolicyEdited = {
editingPolicyIndex?.let { index ->
- viewModel.reducerManager.updatePolicy(index, it)
+ viewModel.reducerManager?.updatePolicy(index, it)
} ?: run {
- viewModel.reducerManager.addPolicy(it)
+ viewModel.reducerManager?.addPolicy(it)
}
reset()
}
@@ -111,7 +116,7 @@ fun ReviewPoliciesScreen(
onBackClicked = { viewModel.goHome() },
onPrevClicked = { viewModel.goBack() },
onNextClicked = {
- viewModel.reducerManager.next()
+ viewModel.reducerManager?.next()
}
) {
Scaffold(
@@ -143,10 +148,10 @@ fun ReviewPoliciesScreen(
providers = providers,
index = index,
onEdit = {
- viewModel.reducerManager.updatePolicy(index, it)
+ viewModel.reducerManager?.updatePolicy(index, it)
},
) {
- viewModel.reducerManager.deletePolicy(index)
+ viewModel.reducerManager?.deletePolicy(index)
}
}
}
@@ -249,11 +254,84 @@ fun PolicyMethodCard(
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
)
-// Text(
-// provider.businessName,
-// style = MaterialTheme.typography.labelMedium,
-// )
}
}
}
+}
+
+@Preview
+@Composable
+fun ReviewPoliciesScreenPreview() {
+ ReviewPoliciesScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.PoliciesReviewing,
+ authenticationMethods = listOf(
+ AuthMethod(
+ type = AuthMethod.Type.Question,
+ mimeType = "text/plain",
+ challenge = "E1QPPS8A",
+ instructions = "What is your favorite GNU package?",
+ ),
+ AuthMethod(
+ type = AuthMethod.Type.Email,
+ instructions = "E-mail to user@*le.com",
+ challenge = "ENSPAWJ0CNW62VBGDHJJWRVFDM50",
+ )
+ ),
+ authenticationProviders = mapOf(
+ "http://localhost:8088/" to AuthenticationProviderStatus.Ok(
+ httpStatus = 200,
+ methods = listOf(
+ MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"),
+ MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"),
+ ),
+ annualFee = "EUR:0.99",
+ truthUploadFee = "EUR:3.99",
+ liabilityLimit = "EUR:1",
+ currency = "EUR",
+ storageLimitInMegabytes = 1,
+ businessName = "Anastasis 4",
+ providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW",
+ ),
+ "http://localhost:8089/" to AuthenticationProviderStatus.Ok(
+ httpStatus = 200,
+ methods = listOf(
+ MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"),
+ MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"),
+ ),
+ annualFee = "EUR:0.99",
+ truthUploadFee = "EUR:3.99",
+ liabilityLimit = "EUR:1",
+ currency = "EUR",
+ storageLimitInMegabytes = 1,
+ businessName = "Anastasis 2",
+ providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW",
+ ),
+ ),
+ policies = listOf(
+ Policy(
+ methods = listOf(
+ Policy.PolicyMethod(
+ authenticationMethod = 0,
+ provider = "http://localhost:8089/",
+ ),
+ Policy.PolicyMethod(
+ authenticationMethod = 1,
+ provider = "http://localhost:8088/",
+ ),
+ ),
+ ),
+ Policy(
+ methods = listOf(
+ Policy.PolicyMethod(
+ authenticationMethod = 0,
+ provider = "http://localhost:8089/",
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
} \ 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 1ffd9ac..7cbea00 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
@@ -45,23 +45,28 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
import net.taler.anastasis.R
import net.taler.anastasis.shared.Utils
import net.taler.anastasis.models.AuthMethod
import net.taler.anastasis.models.AuthenticationProviderStatus
+import net.taler.anastasis.models.BackupStates
+import net.taler.anastasis.models.MethodSpec
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
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
@Composable
fun SelectAuthMethodsScreen(
- viewModel: ReducerViewModel = hiltViewModel(),
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
) {
val state by viewModel.reducerState.collectAsState()
val reducerState = state as? ReducerState.Backup
@@ -99,7 +104,7 @@ fun SelectAuthMethodsScreen(
onMethodEdited = {
reset()
Log.d("onMethodEdited", it.challenge)
- viewModel.reducerManager.addAuthentication(
+ viewModel.reducerManager?.addAuthentication(
ReducerArgs.AddAuthentication(it)
)
}
@@ -111,7 +116,7 @@ fun SelectAuthMethodsScreen(
onBackClicked = { viewModel.goHome() },
onPrevClicked = { viewModel.goBack() },
onNextClicked = {
- viewModel.reducerManager.next()
+ viewModel.reducerManager?.next()
}
) { scroll ->
AuthMethods(
@@ -123,7 +128,7 @@ fun SelectAuthMethodsScreen(
showEditDialog = true
},
onDeleteMethod = {
- viewModel.reducerManager.deleteAuthentication(it)
+ viewModel.reducerManager?.deleteAuthentication(it)
},
)
}
@@ -230,4 +235,60 @@ private fun ChallengeCard(
}
}
}
+}
+
+@Preview
+@Composable
+fun SelectAuthMethodsScreenPreview() {
+ SelectAuthMethodsScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.AuthenticationsEditing,
+ authenticationMethods = listOf(
+ AuthMethod(
+ type = AuthMethod.Type.Question,
+ mimeType = "text/plain",
+ challenge = "E1QPPS8A",
+ instructions = "What is your favorite GNU package?",
+ ),
+ AuthMethod(
+ type = AuthMethod.Type.Email,
+ instructions = "E-mail to user@*le.com",
+ challenge = "ENSPAWJ0CNW62VBGDHJJWRVFDM50",
+ )
+ ),
+ authenticationProviders = mapOf(
+ "http://localhost:8088/" to AuthenticationProviderStatus.Ok(
+ httpStatus = 200,
+ methods = listOf(
+ MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"),
+ MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"),
+ ),
+ annualFee = "EUR:0.99",
+ truthUploadFee = "EUR:3.99",
+ liabilityLimit = "EUR:1",
+ currency = "EUR",
+ storageLimitInMegabytes = 1,
+ businessName = "Anastasis 4",
+ providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW",
+ ),
+ "http://localhost:8089/" to AuthenticationProviderStatus.Ok(
+ httpStatus = 200,
+ methods = listOf(
+ MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"),
+ MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"),
+ ),
+ annualFee = "EUR:0.99",
+ truthUploadFee = "EUR:3.99",
+ liabilityLimit = "EUR:1",
+ currency = "EUR",
+ storageLimitInMegabytes = 1,
+ businessName = "Anastasis 2",
+ providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW",
+ ),
+ ),
+
+ )
+ )
+ )
} \ 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 eacbea4..10a8978 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
@@ -28,17 +28,22 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
import net.taler.anastasis.R
+import net.taler.anastasis.models.BackupStates
+import net.taler.anastasis.models.ContinentInfo
import net.taler.anastasis.models.ReducerState
import net.taler.anastasis.ui.reusable.components.Picker
import net.taler.anastasis.ui.reusable.pages.WizardPage
import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
@Composable
fun SelectContinentScreen(
- viewModel: ReducerViewModel = hiltViewModel(),
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
) {
val reducerState by viewModel.reducerState.collectAsState()
val continents = when (val state = reducerState) {
@@ -66,7 +71,7 @@ fun SelectContinentScreen(
onBackClicked = { viewModel.goHome() },
onNextClicked = {
localContinent?.let {
- viewModel.reducerManager.selectContinent(it)
+ viewModel.reducerManager?.selectContinent(it)
}
},
) {
@@ -88,4 +93,21 @@ fun SelectContinentScreen(
)
}
}
+}
+
+@Preview
+@Composable
+fun SelectContinentScreenPreview() {
+ SelectContinentScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.ContinentSelecting,
+ selectedContinent = "Europe",
+ continents = listOf(
+ ContinentInfo(name = "Europe"),
+ ContinentInfo(name = "North America"),
+ )
+ )
+ )
+ )
} \ No newline at end of file
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 4c8a32e..f137e5e 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
@@ -28,17 +28,22 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
import net.taler.anastasis.R
+import net.taler.anastasis.models.BackupStates
+import net.taler.anastasis.models.CountryInfo
import net.taler.anastasis.models.ReducerState
import net.taler.anastasis.ui.reusable.components.Picker
import net.taler.anastasis.ui.reusable.pages.WizardPage
import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
@Composable
fun SelectCountryScreen(
- viewModel: ReducerViewModel = hiltViewModel(),
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
) {
val reducerState by viewModel.reducerState.collectAsState()
val countries = when (val state = reducerState) {
@@ -66,7 +71,7 @@ fun SelectCountryScreen(
onPrevClicked = { viewModel.goBack() },
onNextClicked = {
localCountry?.let {
- viewModel.reducerManager.selectCountry(it)
+ viewModel.reducerManager?.selectCountry(it)
}
},
) {
@@ -88,4 +93,29 @@ fun SelectCountryScreen(
)
}
}
+}
+
+@Preview
+@Composable
+fun SelectCountryScreenPreview() {
+ SelectCountryScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.ContinentSelecting,
+ selectedCountry = "ch",
+ countries = listOf(
+ CountryInfo(
+ code = "ch",
+ name = "Switzerland",
+ continent = "Europe",
+ ),
+ CountryInfo(
+ code = "de",
+ name = "Germany",
+ continent = "Europe",
+ )
+ )
+ )
+ )
+ )
} \ No newline at end of file
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 9374798..6c09ae5 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,6 +24,8 @@ 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.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
@@ -34,23 +36,29 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.datetime.toLocalDate
import net.taler.anastasis.R
-import net.taler.anastasis.shared.Utils
+import net.taler.anastasis.models.BackupStates
import net.taler.anastasis.models.ReducerState
import net.taler.anastasis.models.UserAttributeSpec
import net.taler.anastasis.shared.FieldStatus
+import net.taler.anastasis.shared.Utils
import net.taler.anastasis.ui.reusable.components.DatePickerField
import net.taler.anastasis.ui.reusable.pages.WizardPage
import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.anastasis.viewmodels.FakeReducerViewModel
import net.taler.anastasis.viewmodels.ReducerViewModel
+import net.taler.anastasis.viewmodels.ReducerViewModelI
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectUserAttributesScreen(
- viewModel: ReducerViewModel = hiltViewModel(),
+ viewModel: ReducerViewModelI = hiltViewModel<ReducerViewModel>(),
) {
val reducerState by viewModel.reducerState.collectAsState()
val userAttributes = when (val state = reducerState) {
@@ -69,18 +77,18 @@ fun SelectUserAttributesScreen(
*identityAttributes.toList().toTypedArray()
) }
- val enableNext = remember(userAttributes, values) {
- userAttributes.fold(true) { a, b ->
- a && (fieldStatus(b, values[b.name]) == FieldStatus.Valid)
- }
+ val enableNext = userAttributes.fold(true) { a, b ->
+ a && (fieldStatus(b, values[b.name]) == FieldStatus.Valid)
}
+ val focusManager = LocalFocusManager.current
+
WizardPage(
title = stringResource(R.string.select_user_attributes_title),
onBackClicked = { viewModel.goHome() },
onPrevClicked = { viewModel.goBack() },
onNextClicked = {
- viewModel.reducerManager.enterUserAttributes(values)
+ viewModel.reducerManager?.enterUserAttributes(values)
},
enableNext = enableNext,
) { scrollConnection ->
@@ -91,9 +99,7 @@ fun SelectUserAttributesScreen(
verticalArrangement = Arrangement.Top,
) {
items(items = userAttributes) { attr ->
- val status = remember(attr, values) {
- fieldStatus(attr, values[attr.name])
- }
+ val status = fieldStatus(attr, values[attr.name])
val supportingRes = remember(attr, status) {
status.msgRes ?: if (attr.optional == true) {
R.string.field_optional
@@ -108,6 +114,8 @@ fun SelectUserAttributesScreen(
)
.fillMaxWidth(),
value = values[attr.name] ?: "",
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = { values[attr.name] = it },
isError = status.error,
supportingText = {
@@ -152,4 +160,36 @@ private fun fieldStatus(
} ?: FieldStatus.Valid
} else if (field.optional == true)
FieldStatus.Valid else FieldStatus.Blank
+}
+
+@Preview
+@Composable
+fun SelectUserAttributesScreenPreview() {
+ SelectUserAttributesScreen(
+ viewModel = FakeReducerViewModel(
+ state = ReducerState.Backup(
+ backupState = BackupStates.UserAttributesCollecting,
+ identityAttributes = mapOf(
+ "full_name" to "Max Musterman",
+ "birthdate" to "2000-01-01",
+ ),
+ requiredAttributes = listOf(
+ UserAttributeSpec(
+ type = "string",
+ name = "full_name",
+ label = "Full name",
+ widget = "anastasis_gtk_ia_full_name",
+ uuid = "9e8f463f-575f-42cb-85f3-759559997331",
+ ),
+ UserAttributeSpec(
+ type = "date",
+ name = "birthdate",
+ label = "Birthdate",
+ widget = "anastasis_gtk_ia_birthdate",
+ uuid = "83d655c7-bdb6-484d-904e-80c1058c8854",
+ ),
+ ),
+ ),
+ ),
+ )
} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt
index a7c3f62..6437e5a 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt
@@ -26,11 +26,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import net.taler.anastasis.R
-import net.taler.anastasis.shared.Utils
import net.taler.anastasis.models.AuthMethod
import net.taler.anastasis.ui.forms.EditEmailForm
import net.taler.anastasis.ui.forms.EditQuestionForm
import net.taler.anastasis.ui.forms.EditSmsForm
+import net.taler.common.CryptoUtils
@Composable
fun EditMethodDialog(
@@ -40,7 +40,7 @@ fun EditMethodDialog(
onCancel: () -> Unit,
) {
var localMethod by remember { mutableStateOf(method?.copy(
- challenge = Utils.decodeBase32(method.challenge),
+ challenge = CryptoUtils.decodeCrock(method.challenge).toString(Charsets.UTF_8),
)) }
AlertDialog(
onDismissRequest = onCancel,
@@ -73,7 +73,9 @@ fun EditMethodDialog(
TextButton(onClick = {
localMethod?.let { onMethodEdited(
it.copy(
- challenge = Utils.encodeBase32(it.challenge)
+ challenge = CryptoUtils.encodeCrock(
+ it.challenge.toByteArray(Charsets.UTF_8),
+ )
)
) }
}) {
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt
new file mode 100644
index 0000000..2c97e92
--- /dev/null
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.layout.padding
+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.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import net.taler.anastasis.R
+import net.taler.anastasis.ui.theme.LocalSpacing
+import net.taler.common.Timestamp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EditSecretForm(
+ modifier: Modifier = Modifier,
+ name: String,
+ value: String,
+ expiration: Timestamp? = null,
+ onSecretEdited: (
+ name: String,
+ value: String,
+ expiration: Timestamp?,
+ ) -> Unit,
+) {
+ val focusRequester2 = remember { FocusRequester() }
+
+ Column(
+ modifier = modifier,
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ ).fillMaxWidth(),
+ value = name,
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+ keyboardActions = KeyboardActions(onNext = { focusRequester2.requestFocus() }),
+ onValueChange = { onSecretEdited(it, value, expiration) },
+ label = { Text(stringResource(R.string.secret_name)) },
+ supportingText = { Text(stringResource(R.string.secret_unique)) },
+ )
+
+ OutlinedTextField(
+ modifier = Modifier
+ .focusRequester(focusRequester2)
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ ).fillMaxWidth(),
+ value = value,
+ onValueChange = { onSecretEdited(name, it, expiration) },
+ label = { Text(stringResource(R.string.secret_text)) },
+ )
+ }
+} \ No newline at end of file
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 cf24c48..8cee602 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
@@ -51,7 +51,7 @@ fun HomeScreen(
icon = { Icon(Icons.Outlined.Upload, null) },
headline = stringResource(R.string.backup_secret),
onClick = {
- viewModel.reducerManager.startBackup()
+ viewModel.reducerManager?.startBackup()
},
)
@@ -64,7 +64,7 @@ fun HomeScreen(
icon = { Icon(Icons.Outlined.Download, null) },
headline = stringResource(R.string.recover_secret),
onClick = {
- viewModel.reducerManager.startRecovery()
+ viewModel.reducerManager?.startRecovery()
},
)
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 46be209..ad90528 100644
--- a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt
@@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import net.taler.anastasis.Routes
@@ -31,15 +32,25 @@ import net.taler.anastasis.models.ReducerState
import net.taler.anastasis.reducers.ReducerManager
import javax.inject.Inject
+interface ReducerViewModelI {
+ val reducerManager: ReducerManager?
+ val reducerState: StateFlow<ReducerState?>
+ val reducerError: StateFlow<TalerErrorInfo?>
+
+ fun goBack(): Boolean
+ fun goHome()
+ fun cleanError()
+}
+
@HiltViewModel
-class ReducerViewModel @Inject constructor(): ViewModel() {
+class ReducerViewModel @Inject constructor(): ViewModel(), ReducerViewModelI {
private val api = AnastasisReducerApi()
- val reducerManager: ReducerManager
+ override val reducerManager: ReducerManager?
private val _reducerState = MutableStateFlow<ReducerState?>(null)
- val reducerState = _reducerState.asStateFlow()
+ override val reducerState = _reducerState.asStateFlow()
private val _reducerError = MutableStateFlow<TalerErrorInfo?>(null)
- val reducerError = _reducerError.asStateFlow()
+ override val reducerError = _reducerError.asStateFlow()
private val _navRoute = MutableStateFlow(Routes.Home.route)
val navRoute = _navRoute.asStateFlow()
@@ -57,11 +68,11 @@ class ReducerViewModel @Inject constructor(): ViewModel() {
reducerManager.startSyncingProviders()
Routes.SelectAuthMethods.route
}
- BackupStates.PoliciesReviewing -> Routes.ReviewPoliciesScreen.route
- BackupStates.SecretEditing -> TODO()
+ BackupStates.PoliciesReviewing -> Routes.ReviewPolicies.route
+ BackupStates.SecretEditing -> Routes.EditSecret.route
BackupStates.TruthsPaying -> TODO()
BackupStates.PoliciesPaying -> TODO()
- BackupStates.BackupFinished -> TODO()
+ BackupStates.BackupFinished -> Routes.BackupFinished.route
}
is ReducerState.Recovery -> when (it.recoveryState) {
RecoveryStates.ContinentSelecting -> Routes.SelectContinent.route
@@ -79,14 +90,14 @@ class ReducerViewModel @Inject constructor(): ViewModel() {
}
}
- fun goBack(): Boolean = when (val state = reducerState.value) {
+ override fun goBack(): Boolean = when (val state = reducerState.value) {
is ReducerState.Backup -> when (state.backupState) {
BackupStates.ContinentSelecting -> {
goHome()
false
}
else -> {
- reducerManager.back()
+ reducerManager?.back()
false
}
}
@@ -96,22 +107,43 @@ class ReducerViewModel @Inject constructor(): ViewModel() {
false
}
else -> {
- reducerManager.back()
+ reducerManager?.back()
false
}
}
is ReducerState.Error -> {
- reducerManager.back()
+ reducerManager?.back()
false
}
else -> true
}
- fun goHome() {
+ override fun goHome() {
+ _reducerState.value = null
+ }
+
+ override fun cleanError() {
+ _reducerError.value = null
+ }
+}
+
+class FakeReducerViewModel(
+ state: ReducerState,
+ error: TalerErrorInfo? = null,
+): ReducerViewModelI {
+ override val reducerManager = null
+ private val _reducerState = MutableStateFlow<ReducerState?>(state)
+ override val reducerState: StateFlow<ReducerState?> = _reducerState.asStateFlow()
+ private val _reducerError = MutableStateFlow(error)
+ override val reducerError: StateFlow<TalerErrorInfo?> = _reducerError.asStateFlow()
+
+ override fun goBack(): Boolean = false
+
+ override fun goHome() {
_reducerState.value = null
}
- fun cleanError() {
+ override fun cleanError() {
_reducerError.value = null
}
} \ No newline at end of file
diff --git a/anastasis/src/main/res/values/strings.xml b/anastasis/src/main/res/values/strings.xml
index bd903c1..5a6aecf 100644
--- a/anastasis/src/main/res/values/strings.xml
+++ b/anastasis/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
<string name="provided_by">Provided by %1$s</string>
<string name="disabled">Disabled</string>
<string name="unknown">Unknown</string>
+ <string name="success">Success</string>
<string name="error">Error</string>
<string name="field_empty">This field is required</string>
<string name="field_invalid">This field is invalid</string>
@@ -41,6 +42,7 @@
<string name="select_auth_methods_title">Authentication methods</string>
<string name="review_policies_title">Recovery policies</string>
<string name="edit_secret_title">Provide secret to backup</string>
+ <string name="backup_finished_title">Your backup is complete!</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>
@@ -62,4 +64,6 @@
<string name="secret_name">Secret name</string>
<string name="secret_unique">This should be unique</string>
<string name="secret_text">Secret text</string>
+ <string name="backup_stored_providers">Your backup is being stored by the following providers:</string>
+ <string name="backup_policy_detail">Version %1$d expires at %2$s</string>
</resources> \ No newline at end of file
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
index 4b77f85..c0c28fb 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
@@ -105,7 +105,7 @@ class Bech32 {
fun generateFakeSegwitAddress(reservePub: String?, addr: String): List<String> {
if (reservePub == null || reservePub.isEmpty()) return listOf()
- val pub = CyptoUtils.decodeCrock(reservePub)
+ val pub = CryptoUtils.decodeCrock(reservePub)
if (pub.size != 32) return listOf()
val firstRnd = pub.copyOfRange(0, 4)
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt
index c1fbe8c..0dba7aa 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt
@@ -18,7 +18,9 @@ package net.taler.common
import kotlin.math.floor
-object CyptoUtils {
+object CryptoUtils {
+ private const val encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+
internal fun getValue(c: Char): Int {
val a = when (c) {
'o','O' -> '0'
@@ -41,6 +43,30 @@ object CyptoUtils {
throw Error("encoding error")
}
+ fun encodeCrock(bytes: ByteArray): String {
+ var sb = ""
+ val size = bytes.size
+ var bitBuf = 0
+ var numBits = 0
+ var pos = 0
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ val d = bytes[pos++]
+ bitBuf = bitBuf.shl(8).or(d.toInt())
+ numBits += 8
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf.shl(5 - numBits)
+ numBits = 5
+ }
+ val v = bitBuf.ushr(numBits - 5).and(31)
+ sb += encTable[v]
+ numBits -= 5
+ }
+ return sb
+ }
+
fun decodeCrock(e: String): ByteArray {
val size = e.length
var bitpos = 0