wallet-core/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts

336 lines
8.1 KiB
TypeScript
Raw Normal View History

2021-10-11 10:58:55 +02:00
import { useState } from "preact/hooks";
2021-10-13 10:48:25 +02:00
export type ReducerState =
| ReducerStateBackup
| ReducerStateRecovery
| ReducerStateError;
export interface ReducerStateBackup {
recovery_state: undefined;
backup_state: BackupStates;
code: undefined;
continents: any;
countries: any;
2021-10-13 19:32:14 +02:00
identity_attributes?: { [n: string]: string };
2021-10-13 10:48:25 +02:00
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;
}[];
core_secret?: {
mime: string;
value: string;
};
2021-10-13 10:48:25 +02:00
}
export interface AuthMethod {
type: string;
instructions: string;
challenge: string;
}
2021-10-13 19:32:14 +02:00
export interface ChallengeInfo {
cost: string;
instructions: string;
type: string;
uuid: string;
}
2021-10-13 10:48:25 +02:00
export interface ReducerStateRecovery {
backup_state: undefined;
recovery_state: RecoveryStates;
code: undefined;
2021-10-13 19:32:14 +02:00
identity_attributes?: { [n: string]: string };
2021-10-13 10:48:25 +02:00
continents: any;
countries: any;
2021-10-13 11:35:24 +02:00
required_attributes: any;
2021-10-13 19:32:14 +02:00
recovery_information?: {
challenges: ChallengeInfo[];
policies: {
/**
* UUID of the associated challenge.
*/
uuid: string;
}[][];
};
recovery_document?: {
secret_name: string;
provider_url: string;
version: number;
};
selected_challenge_uuid?: string;
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
core_secret?: {
mime: string;
value: string;
};
authentication_providers?: {
[url: string]: {
business_name: string;
};
};
recovery_error: any;
}
export interface ChallengeFeedback {
state: string;
2021-10-13 10:48:25 +02:00
}
export interface ReducerStateError {
backup_state: undefined;
recovery_state: undefined;
code: number;
}
2021-10-11 10:58:55 +02:00
interface AnastasisState {
reducerState: ReducerState | undefined;
currentError: any;
}
export enum BackupStates {
ContinentSelecting = "CONTINENT_SELECTING",
CountrySelecting = "COUNTRY_SELECTING",
2021-10-13 10:48:25 +02:00
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
AuthenticationsEditing = "AUTHENTICATIONS_EDITING",
PoliciesReviewing = "POLICIES_REVIEWING",
SecretEditing = "SECRET_EDITING",
TruthsPaying = "TRUTHS_PAYING",
PoliciesPaying = "POLICIES_PAYING",
BackupFinished = "BACKUP_FINISHED",
2021-10-11 10:58:55 +02:00
}
export enum RecoveryStates {
ContinentSelecting = "CONTINENT_SELECTING",
CountrySelecting = "COUNTRY_SELECTING",
2021-10-13 11:35:24 +02:00
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
2021-10-13 19:32:14 +02:00
SecretSelecting = "SECRET_SELECTING",
ChallengeSelecting = "CHALLENGE_SELECTING",
ChallengePaying = "CHALLENGE_PAYING",
ChallengeSolving = "CHALLENGE_SOLVING",
RecoveryFinished = "RECOVERY_FINISHED",
2021-10-11 10:58:55 +02:00
}
const reducerBaseUrl = "http://localhost:5000/";
async function getBackupStartState(): Promise<ReducerState> {
const resp = await fetch(new URL("start-backup", reducerBaseUrl).href);
return await resp.json();
}
async function getRecoveryStartState(): Promise<ReducerState> {
const resp = await fetch(new URL("start-recovery", reducerBaseUrl).href);
return await resp.json();
}
async function reduceState(
state: any,
action: string,
args: any,
): Promise<ReducerState> {
const resp = await fetch(new URL("action", reducerBaseUrl).href, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
state,
action,
arguments: args,
}),
});
return resp.json();
}
2021-10-13 10:48:25 +02:00
export interface ReducerTransactionHandle {
transactionState: ReducerState;
transition(action: string, args: any): Promise<ReducerState>;
}
2021-10-11 10:58:55 +02:00
export interface AnastasisReducerApi {
2021-10-13 10:48:25 +02:00
currentReducerState: ReducerState | undefined;
2021-10-11 10:58:55 +02:00
currentError: any;
2021-10-13 10:48:25 +02:00
dismissError: () => void;
2021-10-11 10:58:55 +02:00
startBackup: () => void;
startRecover: () => void;
2021-10-13 10:48:25 +02:00
reset: () => void;
2021-10-11 10:58:55 +02:00
back: () => void;
transition(action: string, args: any): void;
2021-10-13 10:48:25 +02:00
/**
* Run multiple reducer steps in a transaction without
* affecting the UI-visible transition state in-between.
*/
runTransaction(f: (h: ReducerTransactionHandle) => Promise<void>): 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;
2021-10-11 10:58:55 +02:00
}
export function useAnastasisReducer(): AnastasisReducerApi {
2021-10-13 10:48:25 +02:00
const [anastasisState, setAnastasisStateInternal] = useState<AnastasisState>(
() => ({
reducerState: restoreState(),
currentError: undefined,
}),
);
const setAnastasisState = (newState: AnastasisState) => {
try {
localStorage.setItem(
"anastasisReducerState",
JSON.stringify(newState.reducerState),
);
} catch (e) {
console.log(e);
}
setAnastasisStateInternal(newState);
};
2021-10-11 10:58:55 +02:00
async function doTransition(action: string, args: any) {
console.log("reducing with", action, args);
const s = await reduceState(anastasisState.reducerState, action, args);
console.log("got new state from reducer", s);
if (s.code) {
setAnastasisState({ ...anastasisState, currentError: s });
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
}
return {
currentReducerState: anastasisState.reducerState,
currentError: anastasisState.currentError,
async startBackup() {
const s = await getBackupStartState();
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
},
async startRecover() {
const s = await getRecoveryStartState();
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
},
transition(action: string, args: any) {
doTransition(action, args);
},
back() {
2021-10-13 10:48:25 +02:00
const reducerState = anastasisState.reducerState;
if (!reducerState) {
return;
}
2021-10-11 10:58:55 +02:00
if (
2021-10-13 10:48:25 +02:00
reducerState.backup_state === BackupStates.ContinentSelecting ||
reducerState.recovery_state === RecoveryStates.ContinentSelecting
2021-10-11 10:58:55 +02:00
) {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: undefined,
});
} else {
doTransition("back", {});
}
},
2021-10-13 10:48:25 +02:00
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();
},
2021-10-11 10:58:55 +02:00
};
}
2021-10-13 10:48:25 +02:00
class ReducerTxImpl implements ReducerTransactionHandle {
constructor(public transactionState: ReducerState) {}
async transition(action: string, args: any): Promise<ReducerState> {
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;
}
}