gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: reducer WIP, user error bound


From: gnunet
Subject: [taler-wallet-core] branch master updated: reducer WIP, user error boundaries in UI
Date: Thu, 21 Oct 2021 13:11:39 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 0ee669f5 reducer WIP, user error boundaries in UI
0ee669f5 is described below

commit 0ee669f52341a8331394a1e9892264c0ef0bb7d7
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Oct 21 13:11:17 2021 +0200

    reducer WIP, user error boundaries in UI
---
 .vscode/settings.json                              |   3 +-
 packages/anastasis-core/src/crypto.test.ts         |   5 +
 packages/anastasis-core/src/crypto.ts              |  50 +++-
 packages/anastasis-core/src/index.ts               | 329 ++++++++++++++++-----
 .../anastasis-core/src/recovery-document-types.ts  |  66 +++++
 packages/anastasis-core/src/reducer-types.ts       |  51 ++--
 .../src/hooks/use-anastasis-reducer.ts             |   4 +-
 .../src/pages/home/SecretSelectionScreen.tsx       |   2 +-
 packages/anastasis-webui/src/pages/home/index.tsx  | 142 ++++++---
 packages/anastasis-webui/src/scss/main.scss        |   6 +
 10 files changed, 521 insertions(+), 137 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 52b26670..d8e61693 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -39,7 +39,8 @@
     "search.exclude": {
         "dist": true,
         "prebuilt": true,
-        "src/i18n/*.po": true
+        "src/i18n/*.po": true,
+        "vendor": true
     },
     "search.collapseResults": "auto",
     "files.associations": {
diff --git a/packages/anastasis-core/src/crypto.test.ts 
b/packages/anastasis-core/src/crypto.test.ts
index 1c255014..c0f5e41c 100644
--- a/packages/anastasis-core/src/crypto.test.ts
+++ b/packages/anastasis-core/src/crypto.test.ts
@@ -1,6 +1,7 @@
 import test from "ava";
 import {
   accountKeypairDerive,
+  decryptTruth,
   encryptKeyshare,
   encryptTruth,
   policyKeyDerive,
@@ -94,4 +95,8 @@ test("truth encryption", async (t) => {
     tv.input_truth,
   );
   t.is(enc, tv.output_encrypted_truth);
+
+  const dec = await decryptTruth(tv.input_truth_enc_key, enc);
+
+  t.is(dec, tv.input_truth);
 });
diff --git a/packages/anastasis-core/src/crypto.ts 
b/packages/anastasis-core/src/crypto.ts
index 63de795b..8df893f4 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -9,6 +9,7 @@ import {
   secretbox,
   crypto_sign_keyPair_fromSeed,
   stringToBytes,
+  secretbox_open,
 } from "@gnu-taler/taler-util";
 import { gzipSync } from "fflate";
 import { argon2id } from "hash-wasm";
@@ -87,7 +88,7 @@ export function accountKeypairDerive(userId: UserIdentifier): 
AccountKeyPair {
 
 /**
  * Encrypt the recovery document.
- * 
+ *
  * The caller should first compress the recovery doc.
  */
 export async function encryptRecoveryDocument(
@@ -95,12 +96,19 @@ export async function encryptRecoveryDocument(
   recoveryDocData: OpaqueData,
 ): Promise<OpaqueData> {
   const nonce = encodeCrock(getRandomBytes(nonceSize));
-  return anastasisEncrypt(
-    nonce,
-    asOpaque(userId),
-    recoveryDocData,
-    "erd",
-  );
+  return anastasisEncrypt(nonce, asOpaque(userId), recoveryDocData, "erd");
+}
+
+/**
+ * Encrypt the recovery document.
+ *
+ * The caller should first compress the recovery doc.
+ */
+export async function decryptRecoveryDocument(
+  userId: UserIdentifier,
+  recoveryDocData: OpaqueData,
+): Promise<OpaqueData> {
+  return anastasisDecrypt(asOpaque(userId), recoveryDocData, "erd");
 }
 
 export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
@@ -158,6 +166,22 @@ async function anastasisEncrypt(
   return encodeCrock(typedArrayConcat([nonceBuf, cipherText]));
 }
 
+async function anastasisDecrypt(
+  keySeed: OpaqueData,
+  ciphertext: OpaqueData,
+  salt: string,
+): Promise<OpaqueData> {
+  const ctBuf = decodeCrock(ciphertext);
+  const nonceBuf = ctBuf.slice(0, nonceSize);
+  const enc = ctBuf.slice(nonceSize);
+  const key = await deriveKey(keySeed, encodeCrock(nonceBuf), salt);
+  const cipherText = secretbox_open(enc, nonceBuf, key);
+  if (!cipherText) {
+    throw Error("could not decrypt");
+  }
+  return encodeCrock(cipherText);
+}
+
 export const asOpaque = (x: string): OpaqueData => x;
 const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
 const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
@@ -185,6 +209,18 @@ export async function encryptTruth(
   );
 }
 
+export async function decryptTruth(
+  truthEncKey: TruthKey,
+  truthEnc: EncryptedTruth,
+): Promise<OpaqueData> {
+  const salt = "ect";
+  return await anastasisDecrypt(
+    asOpaque(truthEncKey),
+    asOpaque(truthEnc),
+    salt,
+  );
+}
+
 export interface CoreSecretEncResult {
   encCoreSecret: EncryptedCoreSecret;
   encMasterKeys: EncryptedMasterKey[];
diff --git a/packages/anastasis-core/src/index.ts 
b/packages/anastasis-core/src/index.ts
index c99bd5b4..b8fedf00 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -2,6 +2,8 @@ import {
   AmountString,
   buildSigPS,
   bytesToString,
+  Codec,
+  codecForAny,
   decodeCrock,
   eddsaSign,
   encodeCrock,
@@ -24,6 +26,7 @@ import {
   ActionArgEnterSecret,
   ActionArgEnterSecretName,
   ActionArgEnterUserAttributes,
+  ActionArgSelectChallenge,
   AuthenticationProviderStatus,
   AuthenticationProviderStatusOk,
   AuthMethod,
@@ -33,6 +36,8 @@ import {
   MethodSpec,
   Policy,
   PolicyProvider,
+  RecoveryInformation,
+  RecoveryInternalData,
   RecoveryStates,
   ReducerState,
   ReducerStateBackup,
@@ -60,78 +65,15 @@ import {
   UserIdentifier,
   userIdentifierDerive,
   typedArrayConcat,
+  decryptRecoveryDocument,
 } from "./crypto.js";
-import { zlibSync } from "fflate";
+import { unzlibSync, zlibSync } from "fflate";
+import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
 
 const { fetch, Request, Response, Headers } = fetchPonyfill({});
 
 export * from "./reducer-types.js";
 
-interface RecoveryDocument {
-  // Human-readable name of the secret
-  secret_name?: string;
-
-  // Encrypted core secret.
-  encrypted_core_secret: string; // bytearray of undefined length
-
-  // List of escrow providers and selected authentication method.
-  escrow_methods: EscrowMethod[];
-
-  // List of possible decryption policies.
-  policies: DecryptionPolicy[];
-}
-
-interface DecryptionPolicy {
-  // Salt included to encrypt master key share when
-  // using this decryption policy.
-  salt: string;
-
-  /**
-   * Master key, AES-encrypted with key derived from
-   * salt and keyshares revealed by the following list of
-   * escrow methods identified by UUID.
-   */
-  master_key: string;
-
-  /**
-   * List of escrow methods identified by their UUID.
-   */
-  uuids: string[];
-}
-
-interface EscrowMethod {
-  /**
-   * URL of the escrow provider (including possibly this Anastasis server).
-   */
-  url: string;
-
-  /**
-   * Type of the escrow method (e.g. security question, SMS etc.).
-   */
-  escrow_type: string;
-
-  // UUID of the escrow method.
-  // 16 bytes base32-crock encoded.
-  uuid: TruthUuid;
-
-  // Key used to encrypt the Truth this EscrowMethod is related to.
-  // Client has to provide this key to the server when using /truth/.
-  truth_key: TruthKey;
-
-  /**
-   * Salt to hash the security question answer if applicable.
-   */
-  truth_salt: TruthSalt;
-
-  // Salt from the provider to derive the user ID
-  // at this provider.
-  provider_salt: string;
-
-  // The instructions to give to the user (i.e. the security question
-  // if this is challenge-response).
-  instructions: string;
-}
-
 function getContinents(): ContinentInfo[] {
   const continentSet = new Set<string>();
   const continents: ContinentInfo[] = [];
@@ -203,6 +145,41 @@ async function backupSelectCountry(
   };
 }
 
+async function recoverySelectCountry(
+  state: ReducerStateRecovery,
+  countryCode: string,
+  currencies: string[],
+): Promise<ReducerStateError | ReducerStateRecovery> {
+  const country = anastasisData.countriesList.countries.find(
+    (x) => x.code === countryCode,
+  );
+  if (!country) {
+    return {
+      code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+      hint: "invalid country selected",
+    };
+  }
+
+  const providers: { [x: string]: {} } = {};
+  for (const prov of anastasisData.providersList.anastasis_provider) {
+    if (currencies.includes(prov.currency)) {
+      providers[prov.url] = {};
+    }
+  }
+
+  const ra = (anastasisData.countryDetails as any)[countryCode]
+    .required_attributes;
+
+  return {
+    ...state,
+    recovery_state: RecoveryStates.UserAttributesCollecting,
+    selected_country: countryCode,
+    currencies,
+    required_attributes: ra,
+    authentication_providers: providers,
+  };
+}
+
 async function getProviderInfo(
   providerBaseUrl: string,
 ): Promise<AuthenticationProviderStatus> {
@@ -436,6 +413,13 @@ async function compressRecoveryDoc(rd: any): 
Promise<Uint8Array> {
   return typedArrayConcat([new Uint8Array(sizeHeaderBuf), zippedDoc]);
 }
 
+async function uncompressRecoveryDoc(zippedRd: Uint8Array): Promise<any> {
+  const header = zippedRd.slice(0, 4);
+  const data = zippedRd.slice(4);
+  const res = unzlibSync(data);
+  return JSON.parse(bytesToString(res));
+}
+
 async function uploadSecret(
   state: ReducerStateBackup,
 ): Promise<ReducerStateBackup | ReducerStateError> {
@@ -632,6 +616,97 @@ async function uploadSecret(
   };
 }
 
+/**
+ * Download policy based on current user attributes and selected
+ * version in the state.
+ */
+async function downloadPolicy(
+  state: ReducerStateRecovery,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+  const providerUrls = Object.keys(state.authentication_providers ?? {});
+  let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
+  let recoveryDoc: RecoveryDocument | undefined = undefined;
+  const newProviderStatus: { [url: string]: AuthenticationProviderStatus } = 
{};
+  const userAttributes = state.identity_attributes!;
+  for (const url of providerUrls) {
+    const pi = await getProviderInfo(url);
+    if ("error_code" in pi || !("http_status" in pi)) {
+      // Could not even get /config of the provider
+      continue;
+    }
+    newProviderStatus[url] = pi;
+    const userId = await userIdentifierDerive(userAttributes, pi.salt);
+    const acctKeypair = accountKeypairDerive(userId);
+    const resp = await fetch(new URL(`policy/${acctKeypair.pub}`, url).href);
+    if (resp.status !== 200) {
+      continue;
+    }
+    const body = await resp.arrayBuffer();
+    const bodyDecrypted = await decryptRecoveryDocument(
+      userId,
+      encodeCrock(body),
+    );
+    const rd: RecoveryDocument = await uncompressRecoveryDoc(
+      decodeCrock(bodyDecrypted),
+    );
+    console.log("rd", rd);
+    let policyVersion = 0;
+    try {
+      policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
+    } catch (e) {}
+    foundRecoveryInfo = {
+      provider_url: url,
+      secret_name: rd.secret_name ?? "<unknown>",
+      version: policyVersion,
+    };
+    recoveryDoc = rd;
+    break;
+  }
+  if (!foundRecoveryInfo || !recoveryDoc) {
+    return {
+      code: TalerErrorCode.ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED,
+      hint: "No backups found at any provider for your identity information.",
+    };
+  }
+  const recoveryInfo: RecoveryInformation = {
+    challenges: recoveryDoc.escrow_methods.map((x) => {
+      console.log("providers", state.authentication_providers);
+      const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
+      return {
+        cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
+        instructions: x.instructions,
+        type: x.escrow_type,
+        uuid: x.uuid,
+      };
+    }),
+    policies: recoveryDoc.policies.map((x) => {
+      return x.uuids.map((m) => {
+        return {
+          uuid: m,
+        };
+      });
+    }),
+  };
+  return {
+    ...state,
+    recovery_state: RecoveryStates.SecretSelecting,
+    recovery_document: foundRecoveryInfo,
+    recovery_information: recoveryInfo,
+  };
+}
+
+async function recoveryEnterUserAttributes(
+  state: ReducerStateRecovery,
+  attributes: Record<string, string>,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+  // FIXME: validate attributes
+  const st: ReducerStateRecovery = {
+    ...state,
+    identity_attributes: attributes,
+  };
+  return downloadPolicy(st);
+}
+
 export async function reduceAction(
   state: ReducerState,
   action: string,
@@ -827,6 +902,128 @@ export async function reduceAction(
       };
     }
   }
+
+  if (state.recovery_state === RecoveryStates.ContinentSelecting) {
+    if (action === "select_continent") {
+      const continent: string = args.continent;
+      if (typeof continent !== "string") {
+        return {
+          code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+          hint: "continent required",
+        };
+      }
+      return {
+        ...state,
+        recovery_state: RecoveryStates.CountrySelecting,
+        countries: getCountries(continent),
+        selected_continent: continent,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
+  if (state.recovery_state === RecoveryStates.CountrySelecting) {
+    if (action === "back") {
+      return {
+        ...state,
+        recovery_state: RecoveryStates.ContinentSelecting,
+        countries: undefined,
+      };
+    } else if (action === "select_country") {
+      const countryCode = args.country_code;
+      if (typeof countryCode !== "string") {
+        return {
+          code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+          hint: "country_code required",
+        };
+      }
+      const currencies = args.currencies;
+      return recoverySelectCountry(state, countryCode, currencies);
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
+  if (state.recovery_state === RecoveryStates.UserAttributesCollecting) {
+    if (action === "back") {
+      return {
+        ...state,
+        recovery_state: RecoveryStates.CountrySelecting,
+      };
+    } else if (action === "enter_user_attributes") {
+      const ta = args as ActionArgEnterUserAttributes;
+      return recoveryEnterUserAttributes(state, ta.identity_attributes);
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
+  if (state.recovery_state === RecoveryStates.SecretSelecting) {
+    if (action === "back") {
+      return {
+        ...state,
+        recovery_state: RecoveryStates.UserAttributesCollecting,
+      };
+    } else if (action === "next") {
+      return {
+        ...state,
+        recovery_state: RecoveryStates.ChallengeSelecting,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
+  if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
+    if (action === "select_challenge") {
+      const ta: ActionArgSelectChallenge = args;
+      return {
+        ...state,
+        recovery_state: RecoveryStates.ChallengeSolving,
+        selected_challenge_uuid: ta.uuid,
+      };
+    } else if (action === "back") {
+      return {
+        ...state,
+        recovery_state: RecoveryStates.SecretSelecting,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
+  if (state.recovery_state === RecoveryStates.ChallengeSolving) {
+    if (action === "back") {
+      const ta: ActionArgSelectChallenge = args;
+      return {
+        ...state,
+        selected_challenge_uuid: undefined,
+        recovery_state: RecoveryStates.ChallengeSelecting,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
   return {
     code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
     hint: "Reducer action invalid",
diff --git a/packages/anastasis-core/src/recovery-document-types.ts 
b/packages/anastasis-core/src/recovery-document-types.ts
new file mode 100644
index 00000000..a1d9a55f
--- /dev/null
+++ b/packages/anastasis-core/src/recovery-document-types.ts
@@ -0,0 +1,66 @@
+import { TruthKey, TruthSalt, TruthUuid } from "./crypto.js";
+
+export interface RecoveryDocument {
+  // Human-readable name of the secret
+  secret_name?: string;
+
+  // Encrypted core secret.
+  encrypted_core_secret: string; // bytearray of undefined length
+
+  // List of escrow providers and selected authentication method.
+  escrow_methods: EscrowMethod[];
+
+  // List of possible decryption policies.
+  policies: DecryptionPolicy[];
+}
+
+export interface DecryptionPolicy {
+  // Salt included to encrypt master key share when
+  // using this decryption policy.
+  salt: string;
+
+  /**
+   * Master key, AES-encrypted with key derived from
+   * salt and keyshares revealed by the following list of
+   * escrow methods identified by UUID.
+   */
+  master_key: string;
+
+  /**
+   * List of escrow methods identified by their UUID.
+   */
+  uuids: string[];
+}
+
+export interface EscrowMethod {
+  /**
+   * URL of the escrow provider (including possibly this Anastasis server).
+   */
+  url: string;
+
+  /**
+   * Type of the escrow method (e.g. security question, SMS etc.).
+   */
+  escrow_type: string;
+
+  // UUID of the escrow method.
+  // 16 bytes base32-crock encoded.
+  uuid: TruthUuid;
+
+  // Key used to encrypt the Truth this EscrowMethod is related to.
+  // Client has to provide this key to the server when using /truth/.
+  truth_key: TruthKey;
+
+  /**
+   * Salt to hash the security question answer if applicable.
+   */
+  truth_salt: TruthSalt;
+
+  // Salt from the provider to derive the user ID
+  // at this provider.
+  provider_salt: string;
+
+  // The instructions to give to the user (i.e. the security question
+  // if this is challenge-response).
+  instructions: string;
+}
diff --git a/packages/anastasis-core/src/reducer-types.ts 
b/packages/anastasis-core/src/reducer-types.ts
index 44761ea0..4c73dfa6 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -93,6 +93,22 @@ export interface UserAttributeSpec {
   widget: string;
 }
 
+export interface RecoveryInternalData {
+  secret_name: string;
+  provider_url: string;
+  version: number;
+}
+
+export interface RecoveryInformation {
+  challenges: ChallengeInfo[];
+  policies: {
+    /**
+     * UUID of the associated challenge.
+     */
+    uuid: string;
+  }[][];
+}
+
 export interface ReducerStateRecovery {
   backup_state?: undefined;
   recovery_state: RecoveryStates;
@@ -102,23 +118,20 @@ export interface ReducerStateRecovery {
 
   continents?: any;
   countries?: any;
+
+  selected_continent?: string;
+  selected_country?: string;
+  currencies?: string[];
+
   required_attributes?: any;
 
-  recovery_information?: {
-    challenges: ChallengeInfo[];
-    policies: {
-      /**
-       * UUID of the associated challenge.
-       */
-      uuid: string;
-    }[][];
-  };
+  /**
+   * Recovery information, used by the UI.
+   */
+  recovery_information?: RecoveryInformation;
 
-  recovery_document?: {
-    secret_name: string;
-    provider_url: string;
-    version: number;
-  };
+  // FIXME: This should really be renamed to recovery_internal_data
+  recovery_document?: RecoveryInternalData;
 
   selected_challenge_uuid?: string;
 
@@ -129,11 +142,7 @@ export interface ReducerStateRecovery {
     value: string;
   };
 
-  authentication_providers?: {
-    [url: string]: {
-      business_name: string;
-    };
-  };
+  authentication_providers?: { [url: string]: AuthenticationProviderStatus };
 
   recovery_error?: any;
 }
@@ -244,3 +253,7 @@ export interface ActionArgEnterSecret {
   };
   expiration: Duration;
 }
+
+export interface ActionArgSelectChallenge {
+  uuid: string;
+}
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts 
b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index 4a242a2e..72594749 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -164,10 +164,12 @@ export function useAnastasisReducer(): 
AnastasisReducerApi {
     } else {
       s = await reduceAction(anastasisState.reducerState!, action, args);
     }
-    console.log("got new state from reducer", s);
+    console.log("got response from reducer", s);
     if (s.code) {
+      console.log("response is an error");
       setAnastasisState({ ...anastasisState, currentError: s });
     } else {
+      console.log("response is a new state");
       setAnastasisState({
         ...anastasisState,
         currentError: undefined,
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx 
b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index bbdcf8c2..7cb7fdf2 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -57,7 +57,7 @@ export function SecretSelectionScreen(props: 
RecoveryReducerProps): VNode {
     <AnastasisClientFrame title="Recovery: Select secret">
       <p>Provider: {recoveryDocument.provider_url}</p>
       <p>Secret version: {recoveryDocument.version}</p>
-      <p>Secret name: {recoveryDocument.version}</p>
+      <p>Secret name: {recoveryDocument.secret_name}</p>
       <button onClick={() => setSelectingVersion(true)}>
         Select different secret
       </button>
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx 
b/packages/anastasis-webui/src/pages/home/index.tsx
index 6e9ea07f..5001d1ee 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -1,17 +1,28 @@
 import {
-  ComponentChildren, createContext,
-  Fragment, FunctionalComponent, h, VNode
+  Component,
+  ComponentChildren,
+  createContext,
+  Fragment,
+  FunctionalComponent,
+  h,
+  VNode,
 } from "preact";
-import { useContext, useLayoutEffect, useRef } from "preact/hooks";
+import {
+  useContext,
+  useErrorBoundary,
+  useLayoutEffect,
+  useRef,
+} from "preact/hooks";
 import { Menu } from "../../components/menu";
 import {
-  BackupStates, RecoveryStates,
+  BackupStates,
+  RecoveryStates,
   ReducerStateBackup,
   ReducerStateRecovery,
 } from "anastasis-core";
 import {
   AnastasisReducerApi,
-  useAnastasisReducer
+  useAnastasisReducer,
 } from "../../hooks/use-anastasis-reducer";
 import { AttributeEntryScreen } from "./AttributeEntryScreen";
 import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
@@ -27,7 +38,7 @@ import { SecretSelectionScreen } from 
"./SecretSelectionScreen";
 import { SolveScreen } from "./SolveScreen";
 import { StartScreen } from "./StartScreen";
 import { TruthsPayingScreen } from "./TruthsPayingScreen";
-import "./../home/style"
+import "./../home/style";
 
 const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
 
@@ -40,7 +51,10 @@ export interface CommonReducerProps {
   reducerState: ReducerStateBackup | ReducerStateRecovery;
 }
 
-export function withProcessLabel(reducer: AnastasisReducerApi, text: string): 
string {
+export function withProcessLabel(
+  reducer: AnastasisReducerApi,
+  text: string,
+): string {
   if (isBackup(reducer)) {
     return `Backup: ${text}`;
   }
@@ -71,6 +85,33 @@ interface AnastasisClientFrameProps {
   hideNext?: boolean;
 }
 
+function ErrorBoundary(props: {
+  reducer: AnastasisReducerApi;
+  children: ComponentChildren;
+}) {
+  const [error, resetError] = useErrorBoundary((error) =>
+    console.log("got error", error),
+  );
+  if (error) {
+    return (
+      <div>
+        <button
+          onClick={() => {
+            props.reducer.reset();
+            resetError();
+          }}
+        >
+          Reset
+        </button>
+        <p>
+          Error: <pre>{error.stack}</pre>
+        </p>
+      </div>
+    );
+  }
+  return <div>{props.children}</div>;
+}
+
 export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
   const reducer = useContext(WithReducer);
   if (!reducer) {
@@ -83,29 +124,30 @@ export function AnastasisClientFrame(props: 
AnastasisClientFrameProps): VNode {
       reducer.transition("next", {});
     }
   };
-  const handleKeyPress = (e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>): 
void => {
+  const handleKeyPress = (
+    e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
+  ): void => {
     console.log("Got key press", e.key);
     // FIXME: By default, "next" action should be executed here
   };
-  return (<Fragment>
-    <Menu title="Anastasis" />
-    <section class="section">
-      <div class="home" onKeyPress={(e) => handleKeyPress(e)}> 
-        <button onClick={() => reducer.reset()}>Reset session</button>
-        <h1>{props.title}</h1>
-        <ErrorBanner reducer={reducer} />
-        {props.children}
-        {!props.hideNav ? (
-          <div>
-            <button onClick={() => reducer.back()}>Back</button>
-            {!props.hideNext ? (
-              <button onClick={next}>Next</button>
-            ) : null}
-          </div>
-        ) : null}
+  return (
+    <Fragment>
+      <Menu title="Anastasis" />
+      <div>
+        <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
+          <button onClick={() => reducer.reset()}>Reset session</button>
+          <h1>{props.title}</h1>
+          <ErrorBanner reducer={reducer} />
+          {props.children}
+          {!props.hideNav ? (
+            <div>
+              <button onClick={() => reducer.back()}>Back</button>
+              {!props.hideNext ? <button onClick={next}>Next</button> : null}
+            </div>
+          ) : null}
+        </div>
       </div>
-    </section>
-  </Fragment>
+    </Fragment>
   );
 }
 
@@ -113,7 +155,9 @@ const AnastasisClient: FunctionalComponent = () => {
   const reducer = useAnastasisReducer();
   return (
     <WithReducer.Provider value={reducer}>
-      <AnastasisClientImpl />
+      <ErrorBoundary reducer={reducer}>
+        <AnastasisClientImpl />
+      </ErrorBoundary>
     </WithReducer.Provider>
   );
 };
@@ -130,27 +174,38 @@ const AnastasisClientImpl: FunctionalComponent = () => {
     reducerState.backup_state === BackupStates.ContinentSelecting ||
     reducerState.recovery_state === RecoveryStates.ContinentSelecting
   ) {
-    return <ContinentSelectionScreen reducer={reducer} 
reducerState={reducerState} />;
+    return (
+      <ContinentSelectionScreen reducer={reducer} reducerState={reducerState} 
/>
+    );
   }
   if (
     reducerState.backup_state === BackupStates.CountrySelecting ||
     reducerState.recovery_state === RecoveryStates.CountrySelecting
   ) {
-    return <CountrySelectionScreen reducer={reducer} 
reducerState={reducerState} />;
+    return (
+      <CountrySelectionScreen reducer={reducer} reducerState={reducerState} />
+    );
   }
   if (
     reducerState.backup_state === BackupStates.UserAttributesCollecting ||
     reducerState.recovery_state === RecoveryStates.UserAttributesCollecting
   ) {
-    return <AttributeEntryScreen reducer={reducer} reducerState={reducerState} 
/>;
+    return (
+      <AttributeEntryScreen reducer={reducer} reducerState={reducerState} />
+    );
   }
   if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
     return (
-      <AuthenticationEditorScreen backupState={reducerState} reducer={reducer} 
/>
+      <AuthenticationEditorScreen
+        backupState={reducerState}
+        reducer={reducer}
+      />
     );
   }
   if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
-    return <ReviewPoliciesScreen reducer={reducer} backupState={reducerState} 
/>;
+    return (
+      <ReviewPoliciesScreen reducer={reducer} backupState={reducerState} />
+    );
   }
   if (reducerState.backup_state === BackupStates.SecretEditing) {
     return <SecretEditorScreen reducer={reducer} backupState={reducerState} />;
@@ -162,29 +217,34 @@ const AnastasisClientImpl: FunctionalComponent = () => {
   }
 
   if (reducerState.backup_state === BackupStates.TruthsPaying) {
-    return <TruthsPayingScreen reducer={reducer} backupState={reducerState} />
-
+    return <TruthsPayingScreen reducer={reducer} backupState={reducerState} />;
   }
 
   if (reducerState.backup_state === BackupStates.PoliciesPaying) {
     const backupState: ReducerStateBackup = reducerState;
-    return <PoliciesPayingScreen reducer={reducer} backupState={backupState} />
+    return <PoliciesPayingScreen reducer={reducer} backupState={backupState} 
/>;
   }
 
   if (reducerState.recovery_state === RecoveryStates.SecretSelecting) {
-    return <SecretSelectionScreen reducer={reducer} 
recoveryState={reducerState} />;
+    return (
+      <SecretSelectionScreen reducer={reducer} recoveryState={reducerState} />
+    );
   }
 
   if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
-    return <ChallengeOverviewScreen reducer={reducer} 
recoveryState={reducerState} />;
+    return (
+      <ChallengeOverviewScreen reducer={reducer} recoveryState={reducerState} 
/>
+    );
   }
 
   if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
-    return <SolveScreen reducer={reducer} recoveryState={reducerState} />
+    return <SolveScreen reducer={reducer} recoveryState={reducerState} />;
   }
 
   if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
-    return <RecoveryFinishedScreen reducer={reducer} 
recoveryState={reducerState} />
+    return (
+      <RecoveryFinishedScreen reducer={reducer} recoveryState={reducerState} />
+    );
   }
 
   console.log("unknown state", reducer.currentReducerState);
@@ -196,7 +256,6 @@ const AnastasisClientImpl: FunctionalComponent = () => {
   );
 };
 
-
 interface LabeledInputProps {
   label: string;
   grabFocus?: boolean;
@@ -223,7 +282,6 @@ export function LabeledInput(props: LabeledInputProps): 
VNode {
   );
 }
 
-
 interface ErrorBannerProps {
   reducer: AnastasisReducerApi;
 }
@@ -235,7 +293,7 @@ function ErrorBanner(props: ErrorBannerProps): VNode | null 
{
   const currentError = props.reducer.currentError;
   if (currentError) {
     return (
-      <div id="error"> 
+      <div id="error">
         <p>Error: {JSON.stringify(currentError)}</p>
         <button onClick={() => props.reducer.dismissError()}>
           Dismiss Error
diff --git a/packages/anastasis-webui/src/scss/main.scss 
b/packages/anastasis-webui/src/scss/main.scss
index b2255761..2e60bf6f 100644
--- a/packages/anastasis-webui/src/scss/main.scss
+++ b/packages/anastasis-webui/src/scss/main.scss
@@ -226,4 +226,10 @@ div[data-tooltip]::before {
 .notfound {
   padding: 0 5%;
   margin: 100px 0;
+}
+
+h1 {
+  font-size: 1.5em;
+  margin-top: 0.8em;
+  margin-bottom: 0.8em;
 }
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]