diff options
Diffstat (limited to 'anastasis/src/main/java')
16 files changed, 898 insertions, 426 deletions
diff --git a/anastasis/src/main/java/net/taler/anastasis/AnastasisApp.kt b/anastasis/src/main/java/net/taler/anastasis/AnastasisApp.kt new file mode 100644 index 0000000..8fc38fa --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/AnastasisApp.kt @@ -0,0 +1,7 @@ +package net.taler.anastasis + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class AnastasisApp: Application()
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt index 24f604a..4868357 100644 --- a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt +++ b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt @@ -8,19 +8,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import net.taler.anastasis.models.ContinentInfo -import net.taler.anastasis.models.CountryInfo -import net.taler.anastasis.models.UserAttributeSpec -import net.taler.anastasis.ui.backup.BackupContinentScreen -import net.taler.anastasis.ui.backup.BackupCountryScreen -import net.taler.anastasis.ui.backup.BackupUserAttributesScreen +import androidx.hilt.navigation.compose.hiltViewModel +import dagger.hilt.android.AndroidEntryPoint +import net.taler.anastasis.ui.common.SelectContinentScreen +import net.taler.anastasis.ui.common.SelectCountryScreen +import net.taler.anastasis.ui.common.SelectUserAttributesScreen import net.taler.anastasis.ui.home.HomeScreen import net.taler.anastasis.ui.theme.AnastasisTheme +import net.taler.anastasis.viewmodels.ReducerViewModel +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,66 +38,24 @@ class MainActivity : ComponentActivity() { } @Composable -fun MainNavHost() { - val navController = rememberNavController() - NavHost( - navController = navController, - startDestination = Routes.Home.route, - ) { - composable(Routes.Home.route) { - HomeScreen(navController = navController) +fun MainNavHost( + viewModel: ReducerViewModel = hiltViewModel(), +) { + val navRoute by viewModel.navRoute.collectAsState() + when (navRoute) { + Routes.Home.route -> { + HomeScreen() } - composable(Routes.BackupContinent.route) { - BackupContinentScreen( - navController = navController, - continents = listOf( - ContinentInfo("Europe"), - ContinentInfo("India"), - ContinentInfo("Asia"), - ContinentInfo("North America") - ), - onSelectContinent = {}, - ) + Routes.SelectContinent.route -> { + SelectContinentScreen() } - composable(Routes.BackupCountry.route) { - BackupCountryScreen( - navController = navController, - countries = listOf( - CountryInfo("ch", "Switzerland", "Europe"), - CountryInfo("de", "Germany", "Europe"), - ), - onSelectCountry = {}, - ) + Routes.SelectCountry.route -> { + SelectCountryScreen() } - composable(Routes.BackupUserAttributes.route) { - BackupUserAttributesScreen( - navController = navController, - userAttributes = listOf( - UserAttributeSpec( - type = "string", - name = "full_name", - label = "Full name", - widget = "anastasis_gtk_ia_full_name", - uuid = "9e8f463f-575f-42cb-85f3-759559997331", - validationLogic = null, - validationRegex = null, - ), - UserAttributeSpec( - type = "date", - name = "birthdate", - label = "Birthdate", - uuid = "83d655c7-bdb6-484d-904e-80c1058c8854", - widget = "anastasis_gtk_ia_birthdate", - validationLogic = null, - validationRegex = null, - ), - ), - ) + Routes.SelectUserAttributes.route -> { + SelectUserAttributesScreen() } - composable(Routes.RecoveryCountry.route) { - Text("This is the recover screen!") - } - composable(Routes.RestoreInit.route) { + 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 18db321..1a0accc 100644 --- a/anastasis/src/main/java/net/taler/anastasis/Routes.kt +++ b/anastasis/src/main/java/net/taler/anastasis/Routes.kt @@ -6,13 +6,10 @@ sealed class Routes( ) { object Home: Routes("home") - // Backup - object BackupContinent: Routes("backup_continent") - object BackupCountry: Routes("backup_country") - object BackupUserAttributes: Routes("backup_user_attributes") - - // Recovery - object RecoveryCountry: Routes("recovery_country") + // Common + object SelectContinent: Routes("select_continent") + object SelectCountry: Routes("select_country") + object SelectUserAttributes: Routes("select_user_attributes") // Restore object RestoreInit: Routes("restore") diff --git a/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt b/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt new file mode 100644 index 0000000..42ce566 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/backend/AnastasisReducerApi.kt @@ -0,0 +1,86 @@ +/* + * This file is part of GNU Taler + * (C) 2020 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.backend + +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.decodeFromJsonElement +import net.taler.anastasis.models.ReducerState +import net.taler.common.ApiResponse +import net.taler.common.ApiResponse.* +import net.taler.common.TalerErrorCode.NONE +import org.json.JSONObject + +@OptIn(DelicateCoroutinesApi::class) +class AnastasisReducerApi() { + + private val backendManager = BackendManager() + + init { + GlobalScope.launch(Dispatchers.IO) { + backendManager.run() + } + } + + suspend fun sendRequest(operation: String, args: JSONObject? = null): ApiResponse { + return backendManager.send(operation, args) + } + + suspend inline fun startBackup(): ReducerState = withContext(Dispatchers.Default) { + val json = BackendManager.json + when (val response = sendRequest("anastasisStartBackup")) { + is Response -> json.decodeFromJsonElement(response.result) + is Error -> error("invalid reducer response") + } + } + + suspend inline fun startRecovery(): ReducerState = withContext(Dispatchers.Default) { + val json = BackendManager.json + when (val response = sendRequest("anastasisStartRecovery")) { + is Response -> json.decodeFromJsonElement(response.result) + is Error -> error("invalid reducer response") + } + } + + suspend inline fun reduceAction( + state: ReducerState, + action: String, + noinline args: (JSONObject.() -> JSONObject)? = null, + ): WalletResponse<ReducerState> = withContext(Dispatchers.Default) { + val json = BackendManager.json + val body = JSONObject().apply { + put("state", JSONObject(json.encodeToString(ReducerState.serializer(), state))) + put("action", action) + if (args != null) put("args", args.invoke(JSONObject())) + } + 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/backend/BackendManager.kt b/anastasis/src/main/java/net/taler/anastasis/backend/BackendManager.kt new file mode 100644 index 0000000..8ba6025 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/backend/BackendManager.kt @@ -0,0 +1,81 @@ +/* + * This file is part of GNU Taler + * (C) 2022 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.backend + +import android.util.Log +import kotlinx.serialization.json.Json +import net.taler.common.ApiMessage +import net.taler.common.ApiResponse +import net.taler.qtart.TalerWalletCore +import net.taler.anastasis.BuildConfig +import org.json.JSONObject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class BackendManager { + + companion object { + private const val TAG = "BackendManager" + private const val TAG_CORE = "taler-wallet-embedded" + val json = Json { + ignoreUnknownKeys = true + } + } + + private val walletCore = TalerWalletCore() + private val requestManager = RequestManager() + + init { + walletCore.setMessageHandler { onMessageReceived(it) } + if (BuildConfig.DEBUG) walletCore.setStdoutHandler { + Log.d(TAG_CORE, it) + } + } + + fun run() { + walletCore.run() + } + + suspend fun send(operation: String, args: JSONObject? = null): ApiResponse = + suspendCoroutine { cont -> + requestManager.addRequest(cont) { id -> + val request = JSONObject().apply { + put("id", id) + put("operation", operation) + if (args != null) put("args", args) + } + Log.d(TAG, "sending message:\n${request.toString(2)}") + walletCore.sendRequest(request.toString()) + } + } + + private fun onMessageReceived(msg: String) { + Log.d(TAG, "message received: $msg") + when (val message = json.decodeFromString<ApiMessage>(msg)) { + is ApiMessage.Notification -> {} + is ApiResponse -> { + val id = message.id + val cont = requestManager.getAndRemoveContinuation(id) + if (cont == null) { + Log.e(TAG, "wallet returned unknown request ID ($id)") + } else { + cont.resume(message) + } + } + } + } +} diff --git a/anastasis/src/main/java/net/taler/anastasis/backend/RequestManager.kt b/anastasis/src/main/java/net/taler/anastasis/backend/RequestManager.kt new file mode 100644 index 0000000..70f81b3 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/backend/RequestManager.kt @@ -0,0 +1,45 @@ +/* + * 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.backend + +import androidx.annotation.GuardedBy +import net.taler.common.ApiResponse +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.Continuation + +class RequestManager { + + @GuardedBy("this") + private val contMap = ConcurrentHashMap<Int, Continuation<ApiResponse>>() + + @Volatile + @GuardedBy("this") + private var currentId = 0 + + @Synchronized + fun addRequest(cont: Continuation<ApiResponse>, block: (Int) -> Unit) { + val id = currentId++ + contMap[id] = cont + block(id) + } + + @Synchronized + fun getAndRemoveContinuation(id: Int): Continuation<ApiResponse>? { + return contMap.remove(id) + } + +} diff --git a/anastasis/src/main/java/net/taler/anastasis/backend/WalletResponse.kt b/anastasis/src/main/java/net/taler/anastasis/backend/WalletResponse.kt new file mode 100644 index 0000000..29ca6b2 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/backend/WalletResponse.kt @@ -0,0 +1,134 @@ +/* + * This file is part of GNU Taler + * (C) 2020 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.backend + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import net.taler.common.TalerErrorCode + +@Serializable +sealed class WalletResponse<T> { + @Serializable + @SerialName("response") + data class Success<T>( + val result: T, + ) : WalletResponse<T>() + + @Serializable + @SerialName("error") + data class Error<T>( + val error: TalerErrorInfo, + ) : WalletResponse<T>() + + fun onSuccess(block: (result: T) -> Unit): WalletResponse<T> { + if (this is Success) block(this.result) + return this + } + + fun onError(block: (result: TalerErrorInfo) -> Unit): WalletResponse<T> { + if (this is Error) block(this.error) + return this + } +} + +@Serializable(with = TalerErrorInfoSerializer::class) +data class TalerErrorInfo( + // Numeric error code defined defined in the + // GANA gnu-taler-error-codes registry. + val code: TalerErrorCode, + + // English description of the error code. + val hint: String? = null, + + // English diagnostic message that can give details + // for the instance of the error. + val message: String? = null, + + // Error extra details + val extra: Map<String, JsonElement> = mapOf(), +) { + val userFacingMsg: String + get() { + return StringBuilder().apply { + // If there's a hint in errorResponse, use it. + extra["errorResponse"] + ?.jsonObject + ?.get("hint") + ?.let { + append(it.jsonPrimitive.content) + } ?: { + // Otherwise, use the standard ones. + hint?.let { append(it) } + message?.let { append(" ").append(it) } + } + }.toString() + } + + fun getStringExtra(key: String): String? = + extra[key]?.jsonPrimitive?.content +} + +class TalerErrorInfoSerializer : KSerializer<TalerErrorInfo> { + private val stringToJsonElementSerializer = MapSerializer(String.serializer(), JsonElement.serializer()) + + override val descriptor: SerialDescriptor + get() = stringToJsonElementSerializer.descriptor + + override fun deserialize(decoder: Decoder): TalerErrorInfo { + // Decoder -> JsonInput + require(decoder is JsonDecoder) + val json = decoder.json + val filtersMap = decoder.decodeSerializableValue(stringToJsonElementSerializer) + + val code = filtersMap["code"]?.let { + json.decodeFromJsonElement(TalerErrorCode.serializer(), it) + } ?: TalerErrorCode.UNKNOWN + val hint = filtersMap["hint"]?.let { + json.decodeFromJsonElement(String.serializer(), it) + } + val message = filtersMap["message"]?.let { + json.decodeFromJsonElement(String.serializer(), it) + } + + val knownKeys = setOf("code", "hint", "message") + val unknownFilters = filtersMap.filter { (key, _) -> !knownKeys.contains(key) } + + return TalerErrorInfo(code, hint, message, unknownFilters) + } + + override fun serialize(encoder: Encoder, value: TalerErrorInfo) { + encoder.encodeSerializableValue(JsonObject.serializer(), buildJsonObject { + put("code", JsonPrimitive(value.code.code)) + put("hint", JsonPrimitive(value.hint)) + put("message", JsonPrimitive(value.message)) + value.extra.forEach { (key, value) -> put(key, value) } + }) + } +} 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 85494f0..37f380f 100644 --- a/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt +++ b/anastasis/src/main/java/net/taler/anastasis/models/ReducerState.kt @@ -7,10 +7,110 @@ import kotlinx.serialization.json.JsonClassDiscriminator import net.taler.common.Amount import net.taler.common.Timestamp -@OptIn(ExperimentalSerializationApi::class) @Serializable +@OptIn(ExperimentalSerializationApi::class) @JsonClassDiscriminator("reducer_type") -abstract class ReducerState +sealed class ReducerState { + @Serializable + @SerialName("backup") + data class Backup( + @SerialName("backup_state") + val backupState: BackupStates, + val continents: List<ContinentInfo>? = null, + val countries: List<CountryInfo>? = null, + @SerialName("identity_attributes") + val identityAttributes: Map<String, String>? = null, + @SerialName("authentication_providers") + val authenticationProviders: Map<String, AuthenticationProviderStatus>? = null, + @SerialName("authentication_methods") + val authenticationMethods: List<AuthMethod>? = null, + @SerialName("required_attributes") + val requiredAttributes: List<UserAttributeSpec>? = null, + @SerialName("selected_continent") + val selectedContinent: String? = null, + @SerialName("selected_country") + val selectedCountry: String? = null, + @SerialName("secret_name") + val secretName: String? = null, + val policies: List<Policy>? = null, + @SerialName("recovery_data") + val recoveryData: RecoveryData? = null, + @SerialName("policy_providers") + val policyProviders: List<PolicyProvider>? = null, + @SerialName("success_details") + val successDetails: Map<String, SuccessDetail>? = null, + val payments: List<String>? = null, + @SerialName("policy_payment_requests") + val policyPaymentRequests: List<PolicyPaymentRequest>? = null, + @SerialName("core_secret") + val coreSecret: CoreSecret? = null, + val expiration: Timestamp? = null, + @SerialName("upload_fees") + val uploadFees: List<UploadFee>? = null, + @SerialName("truth_upload_payment_secrets") + val truthUploadPaymentSecrets: Map<String, String>? = null, + ): ReducerState() { + @Serializable + data class RecoveryData( + @SerialName("truth_metadata") + val truthMetadata: Map<String, TruthMetaData>, + @SerialName("recovery_document") + val recoveryDocument: RecoveryDocument, + ) + + @Serializable + data class PolicyPaymentRequest( + val payto: String, + val provider: String, + ) + + @Serializable + data class UploadFee( + val fee: Amount, + ) + } + + @Serializable + @SerialName("recovery") + data class Recovery( + @SerialName("recovery_state") + val recoveryState: RecoveryStates, + @SerialName("identity_attributes") + val identityAttributes: Map<String, String>? = null, + val continents: List<ContinentInfo>? = null, + val countries: List<CountryInfo>? = null, + @SerialName("selected_continent") + val selectedContinent: String? = null, + @SerialName("selected_country") + val selectedCountry: String? = null, + @SerialName("required_attributes") + val requiredAttributes: List<UserAttributeSpec>? = null, + @SerialName("recovery_information") + val recoveryInformation: RecoveryInformation? = null, + @SerialName("recovery_document") + val recoveryDocument: RecoveryInternalData? = null, + @SerialName("verbatim_recovery_document") + val verbatimRecoveryDocument: RecoveryDocument? = null, + @SerialName("selected_challenge_uuid") + val selectedChallengeUuid: String? = null, + @SerialName("selected_version") + val selectedVersion: SelectedVersionInfo? = null, + @SerialName("challenge_feedback") + val challengeFeedback: Map<String, ChallengeFeedback>? = null, + // TODO: recovered_key_shares + val coreSecret: CoreSecret? = null, + @SerialName("authentication_providers") + val authenticationProviders: Map<String, AuthenticationProviderStatus>? = null, + ): ReducerState() + + @Serializable + @SerialName("error") + data class Error( + val code: Int, + val hint: String? = null, + val detail: String? = null, + ): ReducerState() +} @Serializable data class ContinentInfo( @@ -58,65 +158,6 @@ data class CoreSecret( ) @Serializable -@SerialName("backup") -data class ReducerStateBackup( - @SerialName("backup_state") - val backupState: BackupStates, - val continents: List<ContinentInfo>? = null, - val countries: List<CountryInfo>? = null, - @SerialName("identity_attributes") - val identityAttributes: Map<String, String>? = null, - @SerialName("authentication_providers") - val authenticationProviders: Map<String, AuthenticationProviderStatus>? = null, - @SerialName("authentication_methods") - val authenticationMethods: List<AuthMethod>? = null, - @SerialName("required_attributes") - val requiredAttributes: List<UserAttributeSpec>? = null, - @SerialName("selected_continent") - val selectedContinent: String? = null, - @SerialName("selected_country") - val selectedCountry: String? = null, - @SerialName("secret_name") - val secretName: String? = null, - val policies: List<Policy>? = null, - @SerialName("recovery_data") - val recoveryData: RecoveryData? = null, - @SerialName("policy_providers") - val policyProviders: List<PolicyProvider>? = null, - @SerialName("success_details") - val successDetails: Map<String, SuccessDetail>? = null, - val payments: List<String>? = null, - @SerialName("policy_payment_requests") - val policyPaymentRequests: List<PolicyPaymentRequest>? = null, - @SerialName("core_secret") - val coreSecret: CoreSecret? = null, - val expiration: Timestamp? = null, - @SerialName("upload_fees") - val uploadFees: List<UploadFee>? = null, - @SerialName("truth_upload_payment_secrets") - val truthUploadPaymentSecrets: Map<String, String>? = null, -): ReducerState() { - @Serializable - data class RecoveryData( - @SerialName("truth_metadata") - val truthMetadata: Map<String, TruthMetaData>, - @SerialName("recovery_document") - val recoveryDocument: RecoveryDocument, - ) - - @Serializable - data class PolicyPaymentRequest( - val payto: String, - val provider: String, - ) - - @Serializable - data class UploadFee( - val fee: Amount, - ) -} - -@Serializable data class AuthMethod( val type: String, val instructions: String, @@ -141,9 +182,9 @@ data class UserAttributeSpec( val widget: String, val optional: Boolean? = null, @SerialName("validation-regex") - val validationRegex: String?, + val validationRegex: String? = null, @SerialName("validation-logic") - val validationLogic: String?, + val validationLogic: String? = null, val autocomplete: String? = null, ) @@ -168,38 +209,6 @@ data class RecoveryInformation( } @Serializable -@SerialName("recovery") -data class ReducerStateRecovery( - val recoveryState: RecoveryStates, - @SerialName("identity_attributes") - val identityAttributes: Map<String, String>? = null, - val continents: List<ContinentInfo>? = null, - val countries: List<CountryInfo>? = null, - @SerialName("selected_continent") - val selectedContinent: String? = null, - @SerialName("selected_country") - val selectedCountry: String? = null, - @SerialName("required_attributes") - val requiredAttributes: List<UserAttributeSpec>? = null, - @SerialName("recovery_information") - val recoveryInformation: RecoveryInformation? = null, - @SerialName("recovery_document") - val recoveryDocument: RecoveryInternalData? = null, - @SerialName("verbatim_recovery_document") - val verbatimRecoveryDocument: RecoveryDocument? = null, - @SerialName("selected_challenge_uuid") - val selectedChallengeUuid: String? = null, - @SerialName("selected_version") - val selectedVersion: SelectedVersionInfo? = null, - @SerialName("challenge_feedback") - val challengeFeedback: Map<String, ChallengeFeedback>? = null, - // TODO: recovered_key_shares - val coreSecret: CoreSecret? = null, - @SerialName("authentication_providers") - val authenticationProviders: Map<String, AuthenticationProviderStatus>, -): ReducerState() - -@Serializable data class TruthMetaData( val uuid: String, @SerialName("key_share") @@ -216,14 +225,6 @@ data class TruthMetaData( ) @Serializable -@SerialName("error") -data class ReducerStateError( - val code: Int, - val hint: String? = null, - val detail: String? = null, -): ReducerState() - -@Serializable enum class BackupStates { @SerialName("CONTINENT_SELECTING") ContinentSelecting, @@ -287,48 +288,50 @@ data class MethodSpec( val usageFee: String, ) -@Serializable -@SerialName("not-contacted") -class AuthenticationProviderStatusNotContacted: AuthenticationProviderStatus() - -@Serializable -@SerialName("ok") -data class AuthenticationProviderStatusOk( - @SerialName("annual_fee") - val annualFee: String, - @SerialName("business_name") - val businessName: String, - val currency: String, - @SerialName("http_status") - val httpStatus: Int, - @SerialName("liability_limit") - val liabilityLimit: String, - @SerialName("provider_salt") - val providerSalt: String, - @SerialName("storage_limit_in_megabytes") - val storageLimitInMegabytes: Int, - @SerialName("truth_upload_fee") - val truthUploadFee: String, - val methods: List<MethodSpec>, -): AuthenticationProviderStatus() - -@Serializable -@SerialName("disabled") -class AuthenticationProviderStatusDisabled: AuthenticationProviderStatus() -@Serializable -@SerialName("error") -data class AuthenticationProviderStatusError( - @SerialName("http_status") - val httpStatus: Int? = null, - val code: Int, - val hint: String? = null, -): AuthenticationProviderStatus() @OptIn(ExperimentalSerializationApi::class) @Serializable @JsonClassDiscriminator("status") -abstract class AuthenticationProviderStatus() +sealed class AuthenticationProviderStatus { + @Serializable + @SerialName("not-contacted") + object NotContacted: AuthenticationProviderStatus() + + @Serializable + @SerialName("ok") + data class Ok( + @SerialName("annual_fee") + val annualFee: String, + @SerialName("business_name") + val businessName: String, + val currency: String, + @SerialName("http_status") + val httpStatus: Int, + @SerialName("liability_limit") + val liabilityLimit: String, + @SerialName("provider_salt") + val providerSalt: String, + @SerialName("storage_limit_in_megabytes") + val storageLimitInMegabytes: Int, + @SerialName("truth_upload_fee") + val truthUploadFee: String, + val methods: List<MethodSpec>, + ): AuthenticationProviderStatus() + + @Serializable + @SerialName("disabled") + object Disabled : AuthenticationProviderStatus() + + @Serializable + @SerialName("error") + data class Error( + @SerialName("http_status") + val httpStatus: Int? = null, + val code: Int, + val hint: String? = null, + ): AuthenticationProviderStatus() +} // TODO: ReducerStateBackupUserAttributesCollecting diff --git a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt new file mode 100644 index 0000000..90a5158 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt @@ -0,0 +1,84 @@ +/* + * 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.reducers + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import net.taler.anastasis.backend.AnastasisReducerApi +import net.taler.anastasis.models.ContinentInfo +import net.taler.anastasis.models.CountryInfo +import net.taler.anastasis.models.ReducerState + +class ReducerManager( + private val state: MutableStateFlow<ReducerState?>, + private val api: AnastasisReducerApi, + private val scope: CoroutineScope, +) { + // TODO: error handling! + + fun startBackup() = scope.launch { + state.value = api.startBackup() + } + + fun startRecovery() = scope.launch { + state.value = api.startRecovery() + } + + fun back() = scope.launch { + state.value?.let { initialState -> + api.reduceAction(initialState, "back") + .onSuccess { newState -> + state.value = newState + } + } + } + + fun selectContinent(continent: ContinentInfo) = scope.launch { + state.value?.let { initialState -> + api.reduceAction(initialState, "select_continent") { + put("continent", continent.name) + }.onSuccess { newState -> + state.value = newState + } + } + } + + fun selectCountry(country: CountryInfo) = scope.launch { + state.value?.let { initialState -> + api.reduceAction(initialState, "select_country") { + put("country_code", country.code) + // TODO: stop hardcoding currency! + put("currency", "EUR") + }.onSuccess { newState -> + state.value = newState + } + } + } + + fun enterUserAttributes(userAttributes: Map<String, String>) = scope.launch { + state.value?.let { initialState -> + api.reduceAction(initialState, "enter_user_attributes") { + put("identity_attributes", Json.encodeToJsonElement(userAttributes)) + }.onSuccess { newState -> + state.value = newState + } + } + } +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupContinentScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupContinentScreen.kt deleted file mode 100644 index 9638ba2..0000000 --- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupContinentScreen.kt +++ /dev/null @@ -1,76 +0,0 @@ -package net.taler.anastasis.ui.backup - -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.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import net.taler.anastasis.R -import net.taler.anastasis.Routes -import net.taler.anastasis.models.ContinentInfo -import net.taler.anastasis.ui.reusable.components.Picker -import net.taler.anastasis.ui.reusable.pages.WizardPage -import net.taler.anastasis.ui.theme.LocalSpacing - -@Composable -fun BackupContinentScreen( - navController: NavController, - continents: List<ContinentInfo>, - onSelectContinent: (continent: ContinentInfo) -> Unit, -) { - WizardPage( - title = stringResource(R.string.backup_country_title), - navigationIcon = { - IconButton(onClick = { - navController.navigate(Routes.Home.route) - }) { - Icon(Icons.Default.ArrowBack, "back") - } - }, - showPrev = false, - onNextClicked = { - navController.navigate(Routes.BackupCountry.route) - }, - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(LocalSpacing.current.medium), - verticalArrangement = Arrangement.Top, - ) { - Picker( - label = stringResource(R.string.continent), - options = continents.map { it.name }.toSet(), - onOptionChanged = { option -> - continents.find { it.name == option }?.let { continent -> - onSelectContinent(continent) - } - }, - ) - } - } -} - -@Composable -@Preview -fun BackupContinentScreenPreview() { - val navController = rememberNavController() - BackupContinentScreen( - navController = navController, - continents = listOf( - ContinentInfo("Europe"), - ContinentInfo("India"), - ContinentInfo("Asia"), - ContinentInfo("North America")), - onSelectContinent = {}, - ) -}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt deleted file mode 100644 index dfb974f..0000000 --- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.taler.anastasis.ui.backup - -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.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import net.taler.anastasis.R -import net.taler.anastasis.Routes -import net.taler.anastasis.models.CountryInfo -import net.taler.anastasis.ui.reusable.components.Picker -import net.taler.anastasis.ui.reusable.pages.WizardPage -import net.taler.anastasis.ui.theme.LocalSpacing - -@Composable -fun BackupCountryScreen( - navController: NavController, - countries: List<CountryInfo>, - onSelectCountry: (country: CountryInfo) -> Unit, -) { - WizardPage( - title = stringResource(R.string.backup_country_title), - navigationIcon = { - IconButton(onClick = { - navController.navigate(Routes.Home.route) - }) { - Icon(Icons.Default.ArrowBack, "back") - } - }, - onPrevClicked = { - navController.navigate(Routes.BackupContinent.route) - }, - onNextClicked = { - navController.navigate(Routes.BackupUserAttributes.route) - }, - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(LocalSpacing.current.medium), - verticalArrangement = Arrangement.Top, - ) { - Picker( - label = stringResource(R.string.country), - options = countries.map { it.name }.toSet(), - onOptionChanged = { option -> - countries.find { it.name == option }?.let { country -> - onSelectCountry(country) - } - }, - ) - } - } -} - -@Composable -@Preview -fun BackupCountryScreenPreview() { - val navController = rememberNavController() - BackupCountryScreen( - navController = navController, - countries = listOf( - CountryInfo("ch", "Switzerland", "Europe"), - CountryInfo("de", "Germany", "Europe"), - ), - onSelectCountry = {}, - ) -}
\ 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 new file mode 100644 index 0000000..b8ec266 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt @@ -0,0 +1,90 @@ +/* + * 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.common + +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 +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.hilt.navigation.compose.hiltViewModel +import net.taler.anastasis.R +import net.taler.anastasis.models.ContinentInfo +import net.taler.anastasis.models.ReducerState +import net.taler.anastasis.ui.reusable.components.Picker +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.ReducerViewModel + +@Composable +fun SelectContinentScreen( + viewModel: ReducerViewModel = hiltViewModel(), +) { + val reducerState by viewModel.reducerState.collectAsState() + val continents = when (val state = reducerState) { + is ReducerState.Backup -> state.continents + is ReducerState.Recovery -> state.continents + else -> null + } ?: 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") + } + }, + showPrev = false, + onNextClicked = { + selectedContinent?.let { + viewModel.reducerManager.selectContinent(it) + } + }, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(LocalSpacing.current.medium), + verticalArrangement = Arrangement.Top, + ) { + Picker( + label = stringResource(R.string.continent), + options = continents.map { it.name }.toSet(), + onOptionChanged = { option -> + continents.find { it.name == option }?.let { continent -> + selectedContinent = continent + } + }, + ) + } + } +}
\ 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 new file mode 100644 index 0000000..1c4fa49 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt @@ -0,0 +1,92 @@ +/* + * 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.common + +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 +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.hilt.navigation.compose.hiltViewModel +import net.taler.anastasis.R +import net.taler.anastasis.models.CountryInfo +import net.taler.anastasis.models.ReducerState +import net.taler.anastasis.ui.reusable.components.Picker +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.ReducerViewModel + +@Composable +fun SelectCountryScreen( + viewModel: ReducerViewModel = hiltViewModel(), +) { + val reducerState by viewModel.reducerState.collectAsState() + val countries = when (val state = reducerState) { + is ReducerState.Backup -> state.countries + is ReducerState.Recovery -> state.countries + else -> null + } ?: 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") + } + }, + onPrevClicked = { + viewModel.reducerManager.back() + }, + onNextClicked = { + selectedCountry?.let { + viewModel.reducerManager.selectCountry(it) + } + }, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(LocalSpacing.current.medium), + verticalArrangement = Arrangement.Top, + ) { + Picker( + label = stringResource(R.string.country), + options = countries.map { it.name }.toSet(), + onOptionChanged = { option -> + countries.find { it.name == option }?.let { country -> + selectedCountry = country + } + }, + ) + } + } +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupUserAttributesScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt index bbd0147..6fa9f97 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupUserAttributesScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt @@ -14,7 +14,7 @@ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -package net.taler.anastasis.ui.backup +package net.taler.anastasis.ui.common import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Spacer @@ -32,6 +32,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf @@ -39,38 +40,44 @@ 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.navigation.NavController -import androidx.navigation.compose.rememberNavController +import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R -import net.taler.anastasis.Routes -import net.taler.anastasis.models.UserAttributeSpec +import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.reusable.components.DatePickerField import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.ReducerViewModel import java.util.Calendar @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BackupUserAttributesScreen( - navController: NavController, - userAttributes: List<UserAttributeSpec>, +fun SelectUserAttributesScreen( + viewModel: ReducerViewModel = hiltViewModel(), ) { + val reducerState by viewModel.reducerState.collectAsState() + val userAttributes = when (val state = reducerState) { + is ReducerState.Backup -> state.requiredAttributes + is ReducerState.Recovery -> state.requiredAttributes + else -> null + } ?: emptyList() + val values = remember { mutableStateMapOf<String, String>() } WizardPage( - title = stringResource(R.string.backup_user_attributes_title), + title = stringResource(R.string.select_user_attributes_title), navigationIcon = { IconButton(onClick = { - navController.navigate(Routes.Home.route) + viewModel.goHome() }) { Icon(Icons.Default.ArrowBack, "back") } }, onPrevClicked = { - navController.navigate(Routes.BackupCountry.route) + viewModel.reducerManager.back() + }, + onNextClicked = { + viewModel.reducerManager.enterUserAttributes(values) }, - onNextClicked = {}, ) { LazyColumn( modifier = Modifier @@ -82,8 +89,8 @@ fun BackupUserAttributesScreen( when (attr.type) { "string" -> OutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = values[attr.uuid] ?: "", - onValueChange = { values[attr.uuid] = it }, + value = values[attr.name] ?: "", + onValueChange = { values[attr.name] = it }, label = { Text(attr.label) }, ) "date" -> @Composable { @@ -109,33 +116,4 @@ fun BackupUserAttributesScreen( } } } -} - -@Preview -@Composable -fun BackupUserAttributesScreenPreview() { - val navController = rememberNavController() - BackupUserAttributesScreen( - navController = navController, - userAttributes = listOf( - UserAttributeSpec( - type = "string", - name = "full_name", - label = "Full name", - widget = "anastasis_gtk_ia_full_name", - uuid = "9e8f463f-575f-42cb-85f3-759559997331", - validationLogic = null, - validationRegex = null, - ), - UserAttributeSpec( - type = "date", - name = "birthdate", - label = "Birthdate", - uuid = "83d655c7-bdb6-484d-904e-80c1058c8854", - widget = "anastasis_gtk_ia_birthdate", - validationLogic = null, - validationRegex = null, - ), - ), - ) }
\ 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 98a70b4..f79c3a2 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,18 +16,16 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController +import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R -import net.taler.anastasis.Routes import net.taler.anastasis.ui.reusable.components.ActionCard +import net.taler.anastasis.viewmodels.ReducerViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( - navController: NavController, + viewModel: ReducerViewModel = hiltViewModel(), ) { Scaffold( topBar = { @@ -53,7 +51,7 @@ fun HomeScreen( icon = { Icon(Icons.Outlined.Upload, null) }, headline = stringResource(R.string.backup_secret), onClick = { - navController.navigate(Routes.BackupContinent.route) + viewModel.reducerManager.startBackup() }, ) @@ -66,7 +64,7 @@ fun HomeScreen( icon = { Icon(Icons.Outlined.Download, null) }, headline = stringResource(R.string.recover_secret), onClick = { - navController.navigate(Routes.RecoveryCountry.route) + viewModel.reducerManager.startRecovery() }, ) @@ -78,17 +76,8 @@ fun HomeScreen( .fillMaxWidth(), icon = { Icon(Icons.Outlined.Restore, null) }, headline = stringResource(R.string.restore_session), - onClick = { - navController.navigate(Routes.RestoreInit.route) - }, + onClick = {}, ) } } -} - -@Composable -@Preview -fun HomeScreenPreview() { - val navController = rememberNavController() - HomeScreen(navController = navController) }
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt new file mode 100644 index 0000000..3f5671b --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt @@ -0,0 +1,81 @@ +/* + * 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.viewmodels + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import net.taler.anastasis.Routes +import net.taler.anastasis.backend.AnastasisReducerApi +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.RecoveryStates +import net.taler.anastasis.models.ReducerState +import net.taler.anastasis.reducers.ReducerManager +import javax.inject.Inject + +@HiltViewModel +class ReducerViewModel @Inject constructor(): ViewModel() { + private val api = AnastasisReducerApi() + val reducerManager: ReducerManager + + private val _reducerState = MutableStateFlow<ReducerState?>(null) + val reducerState = _reducerState.asStateFlow() + private val _navRoute = MutableStateFlow(Routes.Home.route) + val navRoute = _navRoute.asStateFlow() + + init { + reducerManager = ReducerManager(_reducerState, api, viewModelScope) + viewModelScope.launch { + _reducerState.collect { + Log.d("ReducerViewModel", it?.toString() ?: "nothing") + _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.PoliciesReviewing -> TODO() + BackupStates.SecretEditing -> TODO() + BackupStates.TruthsPaying -> TODO() + BackupStates.PoliciesPaying -> TODO() + BackupStates.BackupFinished -> TODO() + } + is ReducerState.Recovery -> when (it.recoveryState) { + RecoveryStates.ContinentSelecting -> Routes.SelectContinent.route + RecoveryStates.CountrySelecting -> Routes.SelectCountry.route + RecoveryStates.UserAttributesCollecting -> Routes.SelectUserAttributes.route + RecoveryStates.SecretSelecting -> TODO() + RecoveryStates.ChallengeSelecting -> TODO() + RecoveryStates.ChallengePaying -> TODO() + RecoveryStates.ChallengeSolving -> TODO() + RecoveryStates.RecoveryFinished -> TODO() + } + is ReducerState.Error -> TODO() + else -> Routes.Home.route + } + } + } + } + + fun goHome() { + _reducerState.value = null + } +}
\ No newline at end of file |