summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-09-11 23:35:27 -0600
committerIván Ávalos <avalos@disroot.org>2023-11-11 13:20:09 -0600
commit8c9f0fb8380e9d70438812dfbc9322d08da5eac3 (patch)
tree5a30b2cbeee8d48b0fb9b94d7609093c8bd103c4
parent46bde811b276660de4e1ea119a00df7e98a888be (diff)
downloadtaler-android-8c9f0fb8380e9d70438812dfbc9322d08da5eac3.tar.gz
taler-android-8c9f0fb8380e9d70438812dfbc9322d08da5eac3.tar.bz2
taler-android-8c9f0fb8380e9d70438812dfbc9322d08da5eac3.zip
[anastasis] Add expiration support and refactor edit secret screen
-rw-r--r--anastasis/build.gradle.kts2
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/backend/Tasks.kt3
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt37
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt127
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt71
-rw-r--r--anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt55
-rw-r--r--anastasis/src/main/res/values/strings.xml2
7 files changed, 236 insertions, 61 deletions
diff --git a/anastasis/build.gradle.kts b/anastasis/build.gradle.kts
index 52a9ec5..ef4b581 100644
--- a/anastasis/build.gradle.kts
+++ b/anastasis/build.gradle.kts
@@ -6,7 +6,7 @@ plugins {
id("com.google.dagger.hilt.android")
}
-val qtartVersion = "0.9.3-dev.14"
+val qtartVersion = "0.9.3-dev.15"
@Suppress("UnstableApiUsage")
android {
diff --git a/anastasis/src/main/java/net/taler/anastasis/backend/Tasks.kt b/anastasis/src/main/java/net/taler/anastasis/backend/Tasks.kt
index d401704..84ac668 100644
--- a/anastasis/src/main/java/net/taler/anastasis/backend/Tasks.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/backend/Tasks.kt
@@ -20,6 +20,7 @@ data class Tasks(
val foreground: Int = 0,
) {
enum class Type {
+ None,
Background,
Foreground,
}
@@ -28,11 +29,13 @@ data class Tasks(
val isForegroundLoading: Boolean get() = foreground > 0
fun addTask(type: Type): Tasks = when (type) {
+ Type.None -> copy()
Type.Background -> copy(background = background + 1)
Type.Foreground -> copy(foreground = foreground + 1)
}
fun removeTask(type: Type): Tasks = when (type) {
+ Type.None -> copy()
Type.Background -> copy(background = if (background == 0) 0 else background - 1)
Type.Foreground -> copy(foreground = if(foreground == 0) 0 else foreground - 1)
}
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 214b351..75eaf0c 100644
--- a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt
@@ -33,12 +33,14 @@ import net.taler.anastasis.models.AggregatedPolicyMetaInfo
import net.taler.anastasis.models.AuthMethod
import net.taler.anastasis.models.AuthenticationProviderStatus
import net.taler.anastasis.models.ContinentInfo
+import net.taler.anastasis.models.CoreSecret
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 net.taler.anastasis.shared.Utils
import net.taler.anastasis.shared.Utils.encodeToNativeJson
+import net.taler.common.Timestamp
import org.json.JSONObject
import kotlin.time.Duration.Companion.seconds
@@ -253,22 +255,29 @@ class ReducerManager(
}
}
- fun backupSecret(
- name: String,
- args: ReducerArgs.EnterSecret,
+ fun enterSecretName(secretName: String) = scope.launch {
+ state.value?.let { initialState ->
+ addTask(Tasks.Type.None)
+ api.reduceAction(initialState, "enter_secret_name") {
+ put("name", secretName)
+ }
+ .onSuccess { onSuccess(it) }
+ .onError { onError(it) }
+ }
+ }
+
+ fun enterSecret(
+ secret: CoreSecret,
+ expiration: Timestamp,
) = scope.launch {
state.value?.let { initialState ->
- addTask()
- api.reduceAction(initialState, "enter_secret", args).onSuccess { newState ->
- scope.launch {
- api.reduceAction(newState, "enter_secret_name") {
- put("name", name)
- }.onSuccess { newNewState ->
- this@ReducerManager.onSuccess(newNewState)
- this@ReducerManager.next()
- }.onError { onError(it) }
- }
- }.onError { onError(it) }
+ addTask(Tasks.Type.None)
+ api.reduceAction(initialState, "enter_secret", ReducerArgs.EnterSecret(
+ secret = ReducerArgs.EnterSecret.Secret(secret.value, secret.mime),
+ expiration = expiration,
+ ))
+ .onSuccess { onSuccess(it) }
+ .onError { onError(it) }
}
}
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
index 799ebf2..0e0bdea 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt
@@ -17,6 +17,12 @@
package net.taler.anastasis.ui.backup
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+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
@@ -24,19 +30,28 @@ 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.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.atTime
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
import net.taler.anastasis.R
import net.taler.anastasis.models.BackupStates
-import net.taler.anastasis.models.ReducerArgs
+import net.taler.anastasis.models.CoreSecret
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.ui.theme.LocalSpacing
import net.taler.anastasis.viewmodels.FakeBackupViewModel
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(
@@ -45,44 +60,106 @@ fun EditSecretScreen(
val state by viewModel.reducerState.collectAsState()
val reducerState = state as? ReducerState.Backup
?: error("invalid reducer state type")
+ val coreSecret = reducerState.coreSecret
- var secretName by remember {
- mutableStateOf(reducerState.secretName ?: "")
- }
- var secretValue by remember {
- mutableStateOf(reducerState.coreSecret?.value?.let {
+ var secretName by remember { mutableStateOf(reducerState.secretName ?: "") }
+ var secretValue by remember { mutableStateOf(
+ coreSecret?.value?.let {
CryptoUtils.decodeCrock(it).toString(Charsets.UTF_8)
- } ?: "")
+ } ?: "",
+ ) }
+
+ val tz = TimeZone.currentSystemDefault()
+ val secretExpirationDate = remember(reducerState.expiration) {
+ Instant.fromEpochMilliseconds(
+ reducerState.expiration?.ms ?: System.currentTimeMillis()
+ ).toLocalDateTime(tz)
}
+ val uploadFees = reducerState.uploadFees ?: emptyList()
WizardPage(
title = stringResource(R.string.edit_secret_title),
onBackClicked = { viewModel.goHome() },
+ enableNext = secretName.isNotEmpty() && coreSecret != null,
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,
- )
- )
+ viewModel.reducerManager?.next()
},
- ) {
- EditSecretForm(
- modifier = Modifier.fillMaxSize(),
- name = secretName,
- value = secretValue,
- ) { name, value, _ ->
- secretName = name
- secretValue = value
+ ) { scroll ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .nestedScroll(scroll),
+ ) {
+ item {
+ EditSecretForm(
+ modifier = Modifier.fillMaxSize(),
+ name = secretName,
+ value = secretValue,
+ expirationDate = secretExpirationDate.date,
+ onSecretNameEdited = { name ->
+ secretName = name
+ viewModel.reducerManager?.enterSecretName(name)
+ },
+ onSecretEdited = { value, date ->
+ secretValue = value
+ viewModel.reducerManager?.enterSecret(
+ secret = CoreSecret(
+ value = CryptoUtils.encodeCrock(value.toByteArray(Charsets.UTF_8)),
+ mime = "text/plain",
+ ),
+ expiration = Timestamp.fromMillis(
+ date.atTime(secretExpirationDate.time).toInstant(tz)
+ .toEpochMilliseconds()
+ ),
+ )
+ },
+ )
+ }
+
+ item {
+ if (uploadFees.isNotEmpty()) {
+ Text(
+ stringResource(R.string.secret_backup_fees),
+ modifier = Modifier.padding(
+ start = LocalSpacing.current.medium,
+ top = LocalSpacing.current.small,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ ),
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+
+ items(items = uploadFees) { fee ->
+ FeeCard(
+ modifier = Modifier
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+ ).fillMaxSize(),
+ fee = fee.fee,
+ )
+ }
}
}
}
+@Composable
+fun FeeCard(
+ modifier: Modifier = Modifier,
+ fee: Amount,
+) {
+ ElevatedCard(modifier) {
+ Text(
+ fee.toString(),
+ modifier = Modifier.padding(LocalSpacing.current.medium)
+ )
+ }
+}
+
@Preview
@Composable
fun EditSecretScreenPreview() {
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
index 2c97e92..5ec5269 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt
@@ -23,17 +23,27 @@ 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.Surface
import androidx.compose.material3.Text
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.tooling.preview.Preview
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
import net.taler.anastasis.R
+import net.taler.anastasis.ui.reusable.components.DatePickerField
+import net.taler.anastasis.ui.theme.AnastasisTheme
import net.taler.anastasis.ui.theme.LocalSpacing
-import net.taler.common.Timestamp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -41,14 +51,16 @@ fun EditSecretForm(
modifier: Modifier = Modifier,
name: String,
value: String,
- expiration: Timestamp? = null,
+ expirationDate: LocalDate,
+ onSecretNameEdited: (name: String) -> Unit,
onSecretEdited: (
- name: String,
value: String,
- expiration: Timestamp?,
+ expirationDate: LocalDate,
) -> Unit,
) {
val focusRequester2 = remember { FocusRequester() }
+ val tz = TimeZone.currentSystemDefault()
+ val currentDate = remember { Clock.System.now().toLocalDateTime(tz).date }
Column(
modifier = modifier,
@@ -59,12 +71,13 @@ fun EditSecretForm(
start = LocalSpacing.current.medium,
end = LocalSpacing.current.medium,
bottom = LocalSpacing.current.small,
- ).fillMaxWidth(),
+ )
+ .fillMaxWidth(),
value = name,
maxLines = 1,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusRequester2.requestFocus() }),
- onValueChange = { onSecretEdited(it, value, expiration) },
+ onValueChange = { onSecretNameEdited(it) },
label = { Text(stringResource(R.string.secret_name)) },
supportingText = { Text(stringResource(R.string.secret_unique)) },
)
@@ -76,10 +89,52 @@ fun EditSecretForm(
start = LocalSpacing.current.medium,
end = LocalSpacing.current.medium,
bottom = LocalSpacing.current.small,
- ).fillMaxWidth(),
+ )
+ .fillMaxWidth(),
value = value,
- onValueChange = { onSecretEdited(name, it, expiration) },
+ onValueChange = { onSecretEdited(it, expirationDate) },
label = { Text(stringResource(R.string.secret_text)) },
)
+
+ DatePickerField(
+ modifier = Modifier
+ .padding(
+ start = LocalSpacing.current.medium,
+ end = LocalSpacing.current.medium,
+ bottom = LocalSpacing.current.small,
+
+ )
+ .fillMaxWidth(),
+ label = stringResource(R.string.secret_expiration),
+ date = expirationDate,
+ onDateSelected = { onSecretEdited(value, it) },
+ minDate = currentDate,
+ )
+ }
+}
+
+@Preview
+@Composable
+fun EditSecretFormPreview() {
+ var name by remember { mutableStateOf("") }
+ var value by remember { mutableStateOf("") }
+ var expirationDate by remember { mutableStateOf<LocalDate>(
+ Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
+ ) }
+ AnastasisTheme {
+ Surface {
+ EditSecretForm(
+ name = name,
+ value = value,
+ expirationDate = expirationDate,
+ onSecretNameEdited = { n ->
+ name = n
+ },
+ onSecretEdited = { v, d ->
+ value = v
+ expirationDate = d
+ },
+ )
+ }
}
} \ No newline at end of file
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt
index 584ee80..db4bf65 100644
--- a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt
+++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt
@@ -17,6 +17,7 @@
package net.taler.anastasis.ui.reusable.components
import android.app.DatePickerDialog
+import android.content.Context
import android.widget.DatePicker
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -40,6 +41,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.atStartOfDayIn
import net.taler.anastasis.shared.Utils
import net.taler.anastasis.ui.theme.LocalSpacing
@@ -52,20 +55,15 @@ fun DatePickerField(
supportingText: (@Composable () -> Unit)? = null,
date: LocalDate?,
onDateSelected: (date: LocalDate) -> Unit,
+ minDate: LocalDate? = null,
+ maxDate: LocalDate? = null,
) {
- val now = Utils.currentDate
- val dialog = DatePickerDialog(
- LocalContext.current,
- { _: DatePicker, y: Int, m: Int, d: Int ->
- onDateSelected(LocalDate(
- year = y,
- monthNumber = m + 1,
- dayOfMonth = d,
- ))
- },
- date?.year ?: now.year,
- (date?.monthNumber ?: now.monthNumber) - 1,
- date?.dayOfMonth ?: now.dayOfMonth,
+ val dialog = getPickerDialog(
+ context = LocalContext.current,
+ initialDate = date,
+ minDate = minDate,
+ maxDate = maxDate,
+ onDateSelected = { onDateSelected(it) },
)
OutlinedTextField(
@@ -95,6 +93,37 @@ fun DatePickerField(
)
}
+private fun getPickerDialog(
+ context: Context,
+ initialDate: LocalDate?,
+ minDate: LocalDate?,
+ maxDate: LocalDate?,
+ onDateSelected: (date: LocalDate) -> Unit,
+): DatePickerDialog {
+ val now = Utils.currentDate
+ val tz = TimeZone.currentSystemDefault()
+ val dialog = DatePickerDialog(
+ context,
+ { _: DatePicker, y: Int, m: Int, d: Int ->
+ onDateSelected(LocalDate(
+ year = y,
+ monthNumber = m + 1,
+ dayOfMonth = d,
+ ))
+ },
+ initialDate?.year ?: now.year,
+ (initialDate?.monthNumber ?: now.monthNumber) - 1,
+ initialDate?.dayOfMonth ?: now.dayOfMonth,
+ )
+ if (minDate != null) {
+ dialog.datePicker.minDate = minDate.atStartOfDayIn(tz).toEpochMilliseconds()
+ }
+ if (maxDate != null) {
+ dialog.datePicker.maxDate = maxDate.atStartOfDayIn(tz).toEpochMilliseconds()
+ }
+ return dialog
+}
+
@Preview
@Composable
fun DatePickerFieldPreview() {
diff --git a/anastasis/src/main/res/values/strings.xml b/anastasis/src/main/res/values/strings.xml
index 0bbf397..f1eba43 100644
--- a/anastasis/src/main/res/values/strings.xml
+++ b/anastasis/src/main/res/values/strings.xml
@@ -74,6 +74,8 @@
<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="secret_expiration">Expiration date</string>
+ <string name="secret_backup_fees">Backup fees</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>