From 0f1ef7eca1f1ab3c5a1787b19a6caec13fb30dec Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 13 Oct 2021 10:48:25 +0200 Subject: anastasis-webui: finish backup flow --- .../src/hooks/use-anastasis-reducer.ts | 185 ++++++++++++++++++--- 1 file changed, 165 insertions(+), 20 deletions(-) (limited to 'packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts') diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index 30bab96d1..d578d1418 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -1,6 +1,58 @@ import { useState } from "preact/hooks"; -type ReducerState = any; +export type ReducerState = + | ReducerStateBackup + | ReducerStateRecovery + | ReducerStateError; + +export interface ReducerStateBackup { + recovery_state: undefined; + backup_state: BackupStates; + code: undefined; + continents: any; + countries: any; + authentication_providers: any; + authentication_methods?: AuthMethod[]; + required_attributes: any; + secret_name?: string; + policies?: { + methods: { + authentication_method: number; + provider: string; + }[]; + }[]; + success_details: { + [provider_url: string]: { + policy_version: number; + }; + }; + payments?: string[]; + policy_payment_requests?: { + payto: string; + provider: string; + }[]; +} + +export interface AuthMethod { + type: string; + instructions: string; + challenge: string; +} + +export interface ReducerStateRecovery { + backup_state: undefined; + recovery_state: RecoveryStates; + code: undefined; + + continents: any; + countries: any; +} + +export interface ReducerStateError { + backup_state: undefined; + recovery_state: undefined; + code: number; +} interface AnastasisState { reducerState: ReducerState | undefined; @@ -10,6 +62,13 @@ interface AnastasisState { export enum BackupStates { ContinentSelecting = "CONTINENT_SELECTING", CountrySelecting = "COUNTRY_SELECTING", + UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING", + AuthenticationsEditing = "AUTHENTICATIONS_EDITING", + PoliciesReviewing = "POLICIES_REVIEWING", + SecretEditing = "SECRET_EDITING", + TruthsPaying = "TRUTHS_PAYING", + PoliciesPaying = "POLICIES_PAYING", + BackupFinished = "BACKUP_FINISHED", } export enum RecoveryStates { @@ -49,20 +108,62 @@ async function reduceState( return resp.json(); } +export interface ReducerTransactionHandle { + transactionState: ReducerState; + transition(action: string, args: any): Promise; +} + export interface AnastasisReducerApi { - currentReducerState: ReducerState; + currentReducerState: ReducerState | undefined; currentError: any; + dismissError: () => void; startBackup: () => void; startRecover: () => void; + reset: () => void; back: () => void; transition(action: string, args: any): void; + /** + * Run multiple reducer steps in a transaction without + * affecting the UI-visible transition state in-between. + */ + runTransaction(f: (h: ReducerTransactionHandle) => Promise): void; +} + +function restoreState(): any { + let state: any; + try { + let s = localStorage.getItem("anastasisReducerState"); + if (s === "undefined") { + state = undefined; + } else if (s) { + console.log("restoring state from", s); + state = JSON.parse(s); + } + } catch (e) { + console.log(e); + } + return state ?? undefined; } export function useAnastasisReducer(): AnastasisReducerApi { - const [anastasisState, setAnastasisState] = useState({ - reducerState: undefined, - currentError: undefined, - }); + const [anastasisState, setAnastasisStateInternal] = useState( + () => ({ + reducerState: restoreState(), + currentError: undefined, + }), + ); + + const setAnastasisState = (newState: AnastasisState) => { + try { + localStorage.setItem( + "anastasisReducerState", + JSON.stringify(newState.reducerState), + ); + } catch (e) { + console.log(e); + } + setAnastasisStateInternal(newState); + }; async function doTransition(action: string, args: any) { console.log("reducing with", action, args); @@ -102,30 +203,74 @@ export function useAnastasisReducer(): AnastasisReducerApi { doTransition(action, args); }, back() { + const reducerState = anastasisState.reducerState; + if (!reducerState) { + return; + } if ( - anastasisState.reducerState.backup_state === - BackupStates.ContinentSelecting || - anastasisState.reducerState.recovery_state === - RecoveryStates.ContinentSelecting + reducerState.backup_state === BackupStates.ContinentSelecting || + reducerState.recovery_state === RecoveryStates.ContinentSelecting ) { setAnastasisState({ ...anastasisState, currentError: undefined, reducerState: undefined, }); - } else if ( - anastasisState.reducerState.backup_state === - BackupStates.CountrySelecting - ) { - doTransition("unselect_continent", {}); - } else if ( - anastasisState.reducerState.recovery_state === - RecoveryStates.CountrySelecting - ) { - doTransition("unselect_continent", {}); } else { doTransition("back", {}); } }, + dismissError() { + setAnastasisState({ ...anastasisState, currentError: undefined }); + }, + reset() { + setAnastasisState({ + ...anastasisState, + currentError: undefined, + reducerState: undefined, + }); + }, + runTransaction(f) { + async function run() { + const txHandle = new ReducerTxImpl(anastasisState.reducerState!); + try { + await f(txHandle); + } catch (e) { + console.log("exception during reducer transaction", e); + } + const s = txHandle.transactionState; + console.log("transaction finished, new state", s); + if (s.code !== undefined) { + setAnastasisState({ + ...anastasisState, + currentError: txHandle.transactionState, + }); + } else { + setAnastasisState({ + ...anastasisState, + reducerState: txHandle.transactionState, + currentError: undefined, + }); + } + } + run(); + }, }; } + +class ReducerTxImpl implements ReducerTransactionHandle { + constructor(public transactionState: ReducerState) {} + async transition(action: string, args: any): Promise { + console.log("making transition in transaction", action); + this.transactionState = await reduceState( + this.transactionState, + action, + args, + ); + // Abort transaction as soon as we transition into an error state. + if (this.transactionState.code !== undefined) { + throw Error("transition resulted in error"); + } + return this.transactionState; + } +} -- cgit v1.2.3