gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: common anstasis frame


From: gnunet
Subject: [taler-wallet-core] branch master updated: common anstasis frame
Date: Wed, 13 Oct 2021 19:32:29 +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 3aad5e77 common anstasis frame
3aad5e77 is described below

commit 3aad5e774dc44fc0d757f6c22152fcbb2b54ca1b
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Oct 13 19:32:14 2021 +0200

    common anstasis frame
---
 packages/anastasis-webui/src/components/app.tsx    |  24 +-
 .../src/components/header/index.tsx                |  24 -
 .../src/components/header/style.css                |  48 --
 .../src/hooks/use-anastasis-reducer.ts             |  52 ++
 packages/anastasis-webui/src/routes/home/index.tsx | 831 +++++++++++++--------
 packages/anastasis-webui/src/routes/home/style.css |   2 +-
 6 files changed, 598 insertions(+), 383 deletions(-)

diff --git a/packages/anastasis-webui/src/components/app.tsx 
b/packages/anastasis-webui/src/components/app.tsx
index 5abb12a3..45c9035f 100644
--- a/packages/anastasis-webui/src/components/app.tsx
+++ b/packages/anastasis-webui/src/components/app.tsx
@@ -1,23 +1,13 @@
-import { FunctionalComponent, h } from 'preact';
-import { Route, Router } from 'preact-router';
+import { FunctionalComponent, h } from "preact";
 
-import Home from '../routes/home';
-import Profile from '../routes/profile';
-import NotFoundPage from '../routes/notfound';
-import Header from './header';
+import AnastasisClient from "../routes/home";
 
 const App: FunctionalComponent = () => {
-    return (
-        <div id="preact_root">
-            <Header />
-            <Router>
-                <Route path="/" component={Home} />
-                <Route path="/profile/" component={Profile} user="me" />
-                <Route path="/profile/:user" component={Profile} />
-                <NotFoundPage default />
-            </Router>
-        </div>
-    );
+  return (
+    <div id="preact_root">
+      <AnastasisClient />
+    </div>
+  );
 };
 
 export default App;
diff --git a/packages/anastasis-webui/src/components/header/index.tsx 
b/packages/anastasis-webui/src/components/header/index.tsx
deleted file mode 100644
index f2b6fe8a..00000000
--- a/packages/anastasis-webui/src/components/header/index.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { FunctionalComponent, h } from 'preact';
-import { Link } from 'preact-router/match';
-import style from './style.css';
-
-const Header: FunctionalComponent = () => {
-    return (
-        <header class={style.header}>
-            <h1>Preact App</h1>
-            <nav>
-                <Link activeClassName={style.active} href="/">
-                    Home
-                </Link>
-                <Link activeClassName={style.active} href="/profile">
-                    Me
-                </Link>
-                <Link activeClassName={style.active} href="/profile/john">
-                    John
-                </Link>
-            </nav>
-        </header>
-    );
-};
-
-export default Header;
diff --git a/packages/anastasis-webui/src/components/header/style.css 
b/packages/anastasis-webui/src/components/header/style.css
deleted file mode 100644
index f08fda70..00000000
--- a/packages/anastasis-webui/src/components/header/style.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.header {
-       position: fixed;
-       left: 0;
-       top: 0;
-       width: 100%;
-       height: 56px;
-       padding: 0;
-       background: #673AB7;
-       box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
-       z-index: 50;
-}
-
-.header h1 {
-       float: left;
-       margin: 0;
-       padding: 0 15px;
-       font-size: 24px;
-       line-height: 56px;
-       font-weight: 400;
-       color: #FFF;
-}
-
-.header nav {
-       float: right;
-       font-size: 100%;
-}
-
-.header nav a {
-       display: inline-block;
-       height: 56px;
-       line-height: 56px;
-       padding: 0 15px;
-       min-width: 50px;
-       text-align: center;
-       background: rgba(255,255,255,0);
-       text-decoration: none;
-       color: #FFF;
-       will-change: background-color;
-}
-
-.header nav a:hover,
-.header nav a:active {
-       background: rgba(0,0,0,0.2);
-}
-
-.header nav a.active {
-       background: rgba(0,0,0,0.4);
-}
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts 
b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index 6ca0ccfa..efa0592d 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -11,6 +11,7 @@ export interface ReducerStateBackup {
   code: undefined;
   continents: any;
   countries: any;
+  identity_attributes?: { [n: string]: string };
   authentication_providers: any;
   authentication_methods?: AuthMethod[];
   required_attributes: any;
@@ -39,14 +40,60 @@ export interface AuthMethod {
   challenge: string;
 }
 
+export interface ChallengeInfo {
+  cost: string;
+  instructions: string;
+  type: string;
+  uuid: string;
+}
+
 export interface ReducerStateRecovery {
   backup_state: undefined;
   recovery_state: RecoveryStates;
   code: undefined;
 
+  identity_attributes?: { [n: string]: string };
+
   continents: any;
   countries: any;
   required_attributes: any;
+
+  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;
 }
 
 export interface ReducerStateError {
@@ -76,6 +123,11 @@ export enum RecoveryStates {
   ContinentSelecting = "CONTINENT_SELECTING",
   CountrySelecting = "COUNTRY_SELECTING",
   UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
+  SecretSelecting = "SECRET_SELECTING",
+  ChallengeSelecting = "CHALLENGE_SELECTING",
+  ChallengePaying = "CHALLENGE_PAYING",
+  ChallengeSolving = "CHALLENGE_SOLVING",
+  RecoveryFinished = "RECOVERY_FINISHED",
 }
 
 const reducerBaseUrl = "http://localhost:5000/";;
diff --git a/packages/anastasis-webui/src/routes/home/index.tsx 
b/packages/anastasis-webui/src/routes/home/index.tsx
index f0b63085..99f8febb 100644
--- a/packages/anastasis-webui/src/routes/home/index.tsx
+++ b/packages/anastasis-webui/src/routes/home/index.tsx
@@ -1,14 +1,23 @@
 import {
+  bytesToString,
   canonicalJson,
+  decodeCrock,
   encodeCrock,
   stringToBytes,
 } from "@gnu-taler/taler-util";
-import { FunctionalComponent, h } from "preact";
-import { useState } from "preact/hooks";
+import {
+  FunctionalComponent,
+  ComponentChildren,
+  h,
+  createContext,
+} from "preact";
+import { useState, useContext, useRef, useLayoutEffect } from "preact/hooks";
 import {
   AnastasisReducerApi,
   AuthMethod,
   BackupStates,
+  ChallengeFeedback,
+  ChallengeInfo,
   RecoveryStates,
   ReducerStateBackup,
   ReducerStateRecovery,
@@ -16,85 +25,340 @@ import {
 } from "../../hooks/use-anastasis-reducer";
 import style from "./style.css";
 
-interface ContinentSelectionProps {
+const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
+
+function isBackup(reducer: AnastasisReducerApi) {
+  return !!reducer.currentReducerState?.backup_state;
+}
+
+interface CommonReducerProps {
   reducer: AnastasisReducerApi;
   reducerState: ReducerStateBackup | ReducerStateRecovery;
 }
 
-function isBackup(reducer: AnastasisReducerApi) {
-  return !!reducer.currentReducerState?.backup_state;
+function withProcessLabel(reducer: AnastasisReducerApi, text: string): string {
+  if (isBackup(reducer)) {
+    return "Backup: " + text;
+  }
+  return "Recovery: " + text;
 }
 
-function ContinentSelection(props: ContinentSelectionProps) {
+function ContinentSelection(props: CommonReducerProps) {
   const { reducer, reducerState } = props;
   return (
-    <div class={style.home}>
-      <h1>{isBackup(reducer) ? "Backup" : "Recovery"}: Select Continent</h1>
-      <ErrorBanner reducer={reducer} />
-      <div>
-        {reducerState.continents.map((x: any) => {
-          const sel = (x: string) =>
-            reducer.transition("select_continent", { continent: x });
-          return (
-            <button onClick={() => sel(x.name)} key={x.name}>
-              {x.name}
-            </button>
-          );
-        })}
-      </div>
-      <div>
-        <button onClick={() => reducer.back()}>Back</button>
-      </div>
-    </div>
+    <AnastasisClientFrame
+      hideNext
+      title={withProcessLabel(reducer, "Select Continent")}
+    >
+      {reducerState.continents.map((x: any) => {
+        const sel = (x: string) =>
+          reducer.transition("select_continent", { continent: x });
+        return (
+          <button onClick={() => sel(x.name)} key={x.name}>
+            {x.name}
+          </button>
+        );
+      })}
+    </AnastasisClientFrame>
+  );
+}
+
+function CountrySelection(props: CommonReducerProps) {
+  const { reducer, reducerState } = props;
+  return (
+    <AnastasisClientFrame
+      hideNext
+      title={withProcessLabel(reducer, "Select Country")}
+    >
+      {reducerState.countries.map((x: any) => {
+        const sel = (x: any) =>
+          reducer.transition("select_country", {
+            country_code: x.code,
+            currencies: [x.currency],
+          });
+        return (
+          <button onClick={() => sel(x)} key={x.name}>
+            {x.name} ({x.currency})
+          </button>
+        );
+      })}
+    </AnastasisClientFrame>
   );
 }
 
-interface CountrySelectionProps {
+interface SolveEntryProps {
   reducer: AnastasisReducerApi;
-  reducerState: ReducerStateBackup | ReducerStateRecovery;
+  challenge: ChallengeInfo;
+  feedback?: ChallengeFeedback;
 }
 
-function CountrySelection(props: CountrySelectionProps) {
-  const { reducer, reducerState } = props;
+function SolveQuestionEntry(props: SolveEntryProps) {
+  const [answer, setAnswer] = useState("");
+  const { reducer, challenge, feedback } = props;
+  const next = () =>
+    reducer.transition("solve_challenge", {
+      answer,
+    });
   return (
-    <div class={style.home}>
-      <h1>Backup: Select Country</h1>
-      <ErrorBanner reducer={reducer} />
+    <AnastasisClientFrame
+      title="Recovery: Solve challenge"
+      onNext={() => next()}
+    >
+      <p>Feedback: {JSON.stringify(feedback)}</p>
+      <p>Question: {challenge.instructions}</p>
+      <label>
+        <input
+          value={answer}
+          onChange={(e) => setAnswer((e.target as HTMLInputElement).value)}
+          type="test"
+        />
+      </label>
+    </AnastasisClientFrame>
+  );
+}
+
+function SecretEditor(props: BackupReducerProps) {
+  const { reducer } = props;
+  const [secretName, setSecretName] = useState("");
+  const [secretValue, setSecretValue] = useState("");
+  const secretNext = () => {
+    reducer.runTransaction(async (tx) => {
+      await tx.transition("enter_secret_name", {
+        name: secretName,
+      });
+      await tx.transition("enter_secret", {
+        secret: {
+          value: encodeCrock(stringToBytes(secretValue)),
+          mime: "text/plain",
+        },
+        expiration: {
+          t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
+        },
+      });
+      await tx.transition("next", {});
+    });
+  };
+  return (
+    <AnastasisClientFrame
+      title="Backup: Provide secret"
+      onNext={() => secretNext()}
+    >
       <div>
-        {reducerState.countries.map((x: any) => {
-          const sel = (x: any) =>
-            reducer.transition("select_country", {
-              country_code: x.code,
-              currencies: [x.currency],
-            });
-          return (
-            <button onClick={() => sel(x)} key={x.name}>
-              {x.name} ({x.currency})
-            </button>
-          );
-        })}
+        <label>
+          Secret name:{" "}
+          <input
+            value={secretName}
+            onChange={(e) =>
+              setSecretName((e.target as HTMLInputElement).value)
+            }
+            type="text"
+          />
+        </label>
+      </div>
+      <div>
+        <label>
+          Secret value:{" "}
+          <input
+            value={secretValue}
+            onChange={(e) =>
+              setSecretValue((e.target as HTMLInputElement).value)
+            }
+            type="text"
+          />
+        </label>
       </div>
+      or:
       <div>
-        <button onClick={() => reducer.back()}>Back</button>
+        <label>
+          File Upload: <input type="file" />
+        </label>
       </div>
-    </div>
+    </AnastasisClientFrame>
   );
 }
 
-const Home: FunctionalComponent = () => {
+export interface BackupReducerProps {
+  reducer: AnastasisReducerApi;
+  backupState: ReducerStateBackup;
+}
+
+function ReviewPolicies(props: BackupReducerProps) {
+  const { reducer, backupState } = props;
+  const authMethods = backupState.authentication_methods!;
+  return (
+    <AnastasisClientFrame title="Backup: Review Recovery Policies">
+      {backupState.policies?.map((p, i) => {
+        const policyName = p.methods
+          .map((x) => authMethods[x.authentication_method].type)
+          .join(" + ");
+        return (
+          <div class={style.policy}>
+            <h3>
+              Policy #{i + 1}: {policyName}
+            </h3>
+            Required Authentications:
+            <ul>
+              {p.methods.map((x) => {
+                const m = authMethods[x.authentication_method];
+                return (
+                  <li>
+                    {m.type} ({m.instructions}) at provider {x.provider}
+                  </li>
+                );
+              })}
+            </ul>
+            <div>
+              <button
+                onClick={() =>
+                  reducer.transition("delete_policy", { policy_index: i })
+                }
+              >
+                Delete Policy
+              </button>
+            </div>
+          </div>
+        );
+      })}
+    </AnastasisClientFrame>
+  );
+}
+
+export interface RecoveryReducerProps {
+  reducer: AnastasisReducerApi;
+  recoveryState: ReducerStateRecovery;
+}
+
+function SecretSelection(props: RecoveryReducerProps) {
+  const { reducer, recoveryState } = props;
+  const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
+  const [otherVersion, setOtherVersion] = useState<number>(
+    recoveryState.recovery_document?.version ?? 0,
+  );
+  const [otherProvider, setOtherProvider] = useState<string>("");
+  function selectVersion(p: string, n: number) {
+    reducer.runTransaction(async (tx) => {
+      await tx.transition("change_version", {
+        version: n,
+        provider_url: p,
+      });
+      setSelectingVersion(false);
+    });
+  }
+  if (selectingVersion) {
+    return (
+      <AnastasisClientFrame hideNav title="Recovery: Select secret">
+        <p>Select a different version of the secret</p>
+        <select onChange={(e) => setOtherProvider((e.target as any).value)}>
+          {Object.keys(recoveryState.authentication_providers ?? {}).map(
+            (x) => {
+              return <option value={x}>{x}</option>;
+            },
+          )}
+        </select>
+        <div>
+          <input
+            value={otherVersion}
+            onChange={(e) =>
+              setOtherVersion(Number((e.target as HTMLInputElement).value))
+            }
+            type="number"
+          />
+          <button onClick={() => selectVersion(otherProvider, otherVersion)}>
+            Select
+          </button>
+        </div>
+        <div>
+          <button onClick={() => selectVersion(otherProvider, 0)}>
+            Use latest version
+          </button>
+        </div>
+        <div>
+          <button onClick={() => setSelectingVersion(false)}>Cancel</button>
+        </div>
+      </AnastasisClientFrame>
+    );
+  }
+  return (
+    <AnastasisClientFrame title="Recovery: Select secret">
+      <p>Provider: {recoveryState.recovery_document!.provider_url}</p>
+      <p>Secret version: {recoveryState.recovery_document!.version}</p>
+      <p>Secret name: {recoveryState.recovery_document!.version}</p>
+      <button onClick={() => setSelectingVersion(true)}>
+        Select different secret
+      </button>
+    </AnastasisClientFrame>
+  );
+}
+
+interface AnastasisClientFrameProps {
+  onNext?(): void;
+  title: string;
+  children: ComponentChildren;
+  /**
+   * Should back/next buttons be provided?
+   */
+  hideNav?: boolean;
+  /**
+   * Hide only the "next" button.
+   */
+  hideNext?: boolean;
+}
+
+function AnastasisClientFrame(props: AnastasisClientFrameProps) {
+  return (
+    <WithReducer.Consumer>
+      {(reducer) => {
+        if (!reducer) {
+          return <p>Fatal: Reducer must be in context.</p>;
+        }
+        const next = () => {
+          if (props.onNext) {
+            props.onNext();
+          } else {
+            reducer.transition("next", {});
+          }
+        };
+        return (
+          <div class={style.home}>
+            <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>
+        );
+      }}
+    </WithReducer.Consumer>
+  );
+}
+
+const AnastasisClient: FunctionalComponent = () => {
   const reducer = useAnastasisReducer();
+  return (
+    <WithReducer.Provider value={reducer}>
+      <AnastasisClientImpl />
+    </WithReducer.Provider>
+  );
+};
+
+const AnastasisClientImpl: FunctionalComponent = () => {
+  const reducer = useContext(WithReducer)!;
   const reducerState = reducer.currentReducerState;
   if (!reducerState) {
     return (
-      <div class={style.home}>
-        <h1>Home</h1>
-        <p>
-          <button autoFocus onClick={() => reducer.startBackup()}>
-            Backup
-          </button>
-          <button onClick={() => reducer.startRecover()}>Recover</button>
-        </p>
-      </div>
+      <AnastasisClientFrame hideNav title="Home">
+        <button autoFocus onClick={() => reducer.startBackup()}>
+          Backup
+        </button>
+        <button onClick={() => reducer.startRecover()}>Recover</button>
+      </AnastasisClientFrame>
     );
   }
   console.log("state", reducer.currentReducerState);
@@ -122,109 +386,17 @@ const Home: FunctionalComponent = () => {
       <AuthenticationEditor backupState={reducerState} reducer={reducer} />
     );
   }
-
   if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
-    const backupState: ReducerStateBackup = reducerState;
-    const authMethods = backupState.authentication_methods!;
-    return (
-      <div class={style.home}>
-        <h1>Backup: Review Recovery Policies</h1>
-        <ErrorBanner reducer={reducer} />
-        <div>
-          {backupState.policies?.map((p, i) => {
-            const policyName = p.methods
-              .map((x) => authMethods[x.authentication_method].type)
-              .join(" + ");
-            return (
-              <div class={style.policy}>
-                <h3>
-                  Policy #{i + 1}: {policyName}
-                </h3>
-                Required Authentications:
-                <ul>
-                  {p.methods.map((x) => {
-                    const m = authMethods[x.authentication_method];
-                    return (
-                      <li>
-                        {m.type} ({m.instructions}) at provider {x.provider}
-                      </li>
-                    );
-                  })}
-                </ul>
-                <div>
-                  <button
-                    onClick={() =>
-                      reducer.transition("delete_policy", { policy_index: i })
-                    }
-                  >
-                    Delete Policy
-                  </button>
-                </div>
-              </div>
-            );
-          })}
-        </div>
-        <div>
-          <button onClick={() => reducer.back()}>Back</button>
-          <button onClick={() => reducer.transition("next", {})}>Next</button>
-        </div>
-      </div>
-    );
+    return <ReviewPolicies reducer={reducer} backupState={reducerState} />;
   }
-
   if (reducerState.backup_state === BackupStates.SecretEditing) {
-    const [secretName, setSecretName] = useState("");
-    const [secretValue, setSecretValue] = useState("");
-    const secretNext = () => {
-      reducer.runTransaction(async (tx) => {
-        await tx.transition("enter_secret_name", {
-          name: secretName,
-        });
-        await tx.transition("enter_secret", {
-          secret: {
-            value: "EDJP6WK5EG50",
-            mime: "text/plain",
-          },
-          expiration: {
-            t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
-          },
-        });
-        await tx.transition("next", {});
-      });
-    };
-    return (
-      <div class={style.home}>
-        <h1>Backup: Provide secret</h1>
-        <ErrorBanner reducer={reducer} />
-        <div>
-          <label>
-            Secret name: <input type="text" />
-          </label>
-        </div>
-        <div>
-          <label>
-            Secret value: <input type="text" />
-          </label>
-        </div>
-        or:
-        <div>
-          <label>
-            File Upload: <input type="file" />
-          </label>
-        </div>
-        <div>
-          <button onClick={() => reducer.back()}>Back</button>
-          <button onClick={() => secretNext()}>Next</button>
-        </div>
-      </div>
-    );
+    return <SecretEditor reducer={reducer} backupState={reducerState} />;
   }
 
   if (reducerState.backup_state === BackupStates.BackupFinished) {
     const backupState: ReducerStateBackup = reducerState;
     return (
-      <div class={style.home}>
-        <h1>Backup finished</h1>
+      <AnastasisClientFrame hideNext title="Backup finished">
         <p>
           Your backup of secret "{backupState.secret_name ?? "??"}" was
           successful.
@@ -240,10 +412,8 @@ const Home: FunctionalComponent = () => {
             );
           })}
         </ul>
-        <button onClick={() => reducer.reset()}>
-          Start a new backup/recovery
-        </button>
-      </div>
+        <button onClick={() => reducer.reset()}>Back to start</button>
+      </AnastasisClientFrame>
     );
   }
 
@@ -251,8 +421,10 @@ const Home: FunctionalComponent = () => {
     const backupState: ReducerStateBackup = reducerState;
     const payments = backupState.payments ?? [];
     return (
-      <div class={style.home}>
-        <h1>Backup: Authentication Storage Payments</h1>
+      <AnastasisClientFrame
+        hideNext
+        title="Backup: Authentication Storage Payments"
+      >
         <p>
           Some of the providers require a payment to store the encrypted
           authentication information.
@@ -262,22 +434,19 @@ const Home: FunctionalComponent = () => {
             return <li>{x}</li>;
           })}
         </ul>
-        <div>
-          <button onClick={() => reducer.back()}>Back</button>
-          <button onClick={() => reducer.transition("pay", {})}>
-            Check payment(s)
-          </button>
-        </div>
-      </div>
+        <button onClick={() => reducer.transition("pay", {})}>
+          Check payment status now
+        </button>
+      </AnastasisClientFrame>
     );
   }
 
   if (reducerState.backup_state === BackupStates.PoliciesPaying) {
     const backupState: ReducerStateBackup = reducerState;
     const payments = backupState.policy_payment_requests ?? [];
+
     return (
-      <div class={style.home}>
-        <h1>Backup: Recovery Document Payments</h1>
+      <AnastasisClientFrame hideNext title="Backup: Recovery Document 
Payments">
         <p>
           Some of the providers require a payment to store the encrypted
           recovery document.
@@ -291,23 +460,111 @@ const Home: FunctionalComponent = () => {
             );
           })}
         </ul>
-        <div>
-          <button onClick={() => reducer.back()}>Back</button>
-          <button onClick={() => reducer.transition("pay", {})}>
-            Check payment(s)
-          </button>
-        </div>
-      </div>
+        <button onClick={() => reducer.transition("pay", {})}>
+          Check payment status now
+        </button>
+      </AnastasisClientFrame>
+    );
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.SecretSelecting) {
+    return <SecretSelection reducer={reducer} recoveryState={reducerState} />;
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
+    const policies = reducerState.recovery_information!.policies;
+    const chArr = reducerState.recovery_information!.challenges;
+    const challenges: {
+      [uuid: string]: {
+        type: string;
+        instructions: string;
+        cost: string;
+      };
+    } = {};
+    for (const ch of chArr) {
+      challenges[ch.uuid] = {
+        type: ch.type,
+        cost: ch.cost,
+        instructions: ch.instructions,
+      };
+    }
+    return (
+      <AnastasisClientFrame title="Recovery: Solve challenges">
+        <h2>Policies</h2>
+        {policies.map((x, i) => {
+          return (
+            <div>
+              <h3>Policy #{i + 1}</h3>
+              {x.map((x) => {
+                const ch = challenges[x.uuid];
+                return (
+                  <div>
+                    {ch.type} ({ch.instructions})
+                    <button
+                      onClick={() =>
+                        reducer.transition("select_challenge", {
+                          uuid: x.uuid,
+                        })
+                      }
+                    >
+                      Solve
+                    </button>
+                  </div>
+                );
+              })}
+            </div>
+          );
+        })}
+      </AnastasisClientFrame>
+    );
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
+    const chArr = reducerState.recovery_information!.challenges;
+    const challengeFeedback = reducerState.challenge_feedback ?? {};
+    const selectedUuid = reducerState.selected_challenge_uuid!;
+    const challenges: {
+      [uuid: string]: ChallengeInfo;
+    } = {};
+    for (const ch of chArr) {
+      challenges[ch.uuid] = ch;
+    }
+    const selectedChallenge = challenges[selectedUuid];
+    if (selectedChallenge.type === "question") {
+      return (
+        <SolveQuestionEntry
+          challenge={selectedChallenge}
+          reducer={reducer}
+          feedback={challengeFeedback[selectedUuid]}
+        />
+      );
+    } else {
+      return (
+        <AnastasisClientFrame hideNext title="Recovery: Solve challenge">
+          <p>{JSON.stringify(selectedChallenge)}</p>
+          <p>Challenge not supported.</p>
+        </AnastasisClientFrame>
+      );
+    }
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
+    return (
+      <AnastasisClientFrame title="Recovery Finished" hideNext>
+        <h1>Recovery Finished</h1>
+        <p>
+          Secret: 
{bytesToString(decodeCrock(reducerState.core_secret?.value!))}
+        </p>
+      </AnastasisClientFrame>
     );
   }
 
   console.log("unknown state", reducer.currentReducerState);
   return (
-    <div class={style.home}>
-      <h1>Home</h1>
+    <AnastasisClientFrame hideNav title="Bug">
       <p>Bug: Unknown state.</p>
       <button onClick={() => reducer.reset()}>Reset</button>
-    </div>
+    </AnastasisClientFrame>
   );
 };
 
@@ -328,9 +585,12 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
       },
     });
   };
+  //const inputRef = useRef<HTMLInputElement>(null);
+  // useLayoutEffect(() => {
+  //   inputRef.current?.focus();
+  // }, []);
   return (
-    <div class={style.home}>
-      <h1>Add {props.method} authentication</h1>
+    <AnastasisClientFrame hideNav title="Add SMS authentication">
       <div>
         <p>
           For SMS authentication, you need to provide a mobile number. When
@@ -338,9 +598,11 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
           receive via SMS.
         </p>
         <label>
-          Mobile number{" "}
+          Mobile number:{" "}
           <input
             value={mobileNumber}
+            //ref={inputRef}
+            style={{ display: "block" }}
             autoFocus
             onChange={(e) => setMobileNumber((e.target as any).value)}
             type="text"
@@ -351,7 +613,7 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
           <button onClick={() => addSmsAuth()}>Add</button>
         </div>
       </div>
-    </div>
+    </AnastasisClientFrame>
   );
 }
 
@@ -359,8 +621,7 @@ function AuthMethodQuestionSetup(props: 
AuthMethodSetupProps) {
   const [questionText, setQuestionText] = useState("");
   const [answerText, setAnswerText] = useState("");
   return (
-    <div class={style.home}>
-      <h1>Add {props.method} authentication</h1>
+    <AnastasisClientFrame hideNav title="Add Security Question">
       <div>
         <p>
           For security question authentication, you need to provide a question
@@ -370,9 +631,10 @@ function AuthMethodQuestionSetup(props: 
AuthMethodSetupProps) {
         </p>
         <div>
           <label>
-            Security question
+            Security question:{" "}
             <input
               value={questionText}
+              style={{ display: "block" }}
               autoFocus
               onChange={(e) => setQuestionText((e.target as any).value)}
               type="text"
@@ -381,9 +643,10 @@ function AuthMethodQuestionSetup(props: 
AuthMethodSetupProps) {
         </div>
         <div>
           <label>
-            Answer
+            Answer:{" "}
             <input
               value={answerText}
+              style={{ display: "block" }}
               autoFocus
               onChange={(e) => setAnswerText((e.target as any).value)}
               type="text"
@@ -407,50 +670,48 @@ function AuthMethodQuestionSetup(props: 
AuthMethodSetupProps) {
           </button>
         </div>
       </div>
-    </div>
+    </AnastasisClientFrame>
   );
 }
 
 function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
   const [email, setEmail] = useState("");
   return (
-    <div class={style.home}>
-      <h1>Add {props.method} authentication</h1>
+    <AnastasisClientFrame hideNav title="Add email authentication">
+      <p>
+        For email authentication, you need to provide an email address. When
+        recovering your secret, you will need to enter the code you receive by
+        email.
+      </p>
       <div>
-        <p>
-          For email authentication, you need to provid an email address. When
-          recovering your secret, you need to enter the code you will receive 
by
-          email.
-        </p>
-        <div>
-          <label>
-            Email address
-            <input
-              value={email}
-              autoFocus
-              onChange={(e) => setEmail((e.target as any).value)}
-              type="text"
-            />
-          </label>
-        </div>
-        <div>
-          <button onClick={() => props.cancel()}>Cancel</button>
-          <button
-            onClick={() =>
-              props.addAuthMethod({
-                authentication_method: {
-                  type: "email",
-                  instructions: `Email to ${email}`,
-                  challenge: encodeCrock(stringToBytes(email)),
-                },
-              })
-            }
-          >
-            Add
-          </button>
-        </div>
+        <label>
+          Email address:{" "}
+          <input
+            style={{ display: "block" }}
+            value={email}
+            autoFocus
+            onChange={(e) => setEmail((e.target as any).value)}
+            type="text"
+          />
+        </label>
       </div>
-    </div>
+      <div>
+        <button onClick={() => props.cancel()}>Cancel</button>
+        <button
+          onClick={() =>
+            props.addAuthMethod({
+              authentication_method: {
+                type: "email",
+                instructions: `Email to ${email}`,
+                challenge: encodeCrock(stringToBytes(email)),
+              },
+            })
+          }
+        >
+          Add
+        </button>
+      </div>
+    </AnastasisClientFrame>
   );
 }
 
@@ -460,6 +721,28 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
   const [city, setCity] = useState("");
   const [postcode, setPostcode] = useState("");
   const [country, setCountry] = useState("");
+
+  const addPostAuth = () => {
+    () =>
+      props.addAuthMethod({
+        authentication_method: {
+          type: "email",
+          instructions: `Letter to address in postal code ${postcode}`,
+          challenge: encodeCrock(
+            stringToBytes(
+              canonicalJson({
+                full_name: fullName,
+                street,
+                city,
+                postcode,
+                country,
+              }),
+            ),
+          ),
+        },
+      });
+  };
+
   return (
     <div class={style.home}>
       <h1>Add {props.method} authentication</h1>
@@ -526,29 +809,7 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
         </div>
         <div>
           <button onClick={() => props.cancel()}>Cancel</button>
-          <button
-            onClick={() =>
-              props.addAuthMethod({
-                authentication_method: {
-                  type: "email",
-                  instructions: `Letter to address in postal code ${postcode}`,
-                  challenge: encodeCrock(
-                    stringToBytes(
-                      canonicalJson({
-                        full_name: fullName,
-                        street,
-                        city,
-                        postcode,
-                        country,
-                      }),
-                    ),
-                  ),
-                },
-              })
-            }
-          >
-            Add
-          </button>
+          <button onClick={() => addPostAuth()}>Add</button>
         </div>
       </div>
     </div>
@@ -557,15 +818,10 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) 
{
 
 function AuthMethodNotImplemented(props: AuthMethodSetupProps) {
   return (
-    <div class={style.home}>
-      <h1>Add {props.method} authentication</h1>
-      <div>
-        <p>
-          This auth method is not implemented yet, please choose another one.
-        </p>
-        <button onClick={() => props.cancel()}>Cancel</button>
-      </div>
-    </div>
+    <AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
+      <p>This auth method is not implemented yet, please choose another 
one.</p>
+      <button onClick={() => props.cancel()}>Cancel</button>
+    </AnastasisClientFrame>
   );
 }
 
@@ -583,8 +839,10 @@ function AuthenticationEditor(props: 
AuthenticationEditorProps) {
   const authAvailableSet = new Set<string>();
   for (const provKey of Object.keys(providers)) {
     const p = providers[provKey];
-    for (const meth of p.methods) {
-      authAvailableSet.add(meth.type);
+    if (p.methods) {
+      for (const meth of p.methods) {
+        authAvailableSet.add(meth.type);
+      }
     }
   }
   if (selectedMethod) {
@@ -653,10 +911,7 @@ function AuthenticationEditor(props: 
AuthenticationEditorProps) {
     backupState.authentication_methods ?? [];
   const haveMethodsConfigured = configuredAuthMethods.length;
   return (
-    <div class={style.home}>
-      <h1>Backup: Configure Authentication Methods</h1>
-      <ErrorBanner reducer={reducer} />
-      <h2>Add authentication method</h2>
+    <AnastasisClientFrame title="Backup: Configure Authentication Methods">
       <div>
         <MethodButton method="sms" label="SMS" />
         <MethodButton method="email" label="Email" />
@@ -686,11 +941,7 @@ function AuthenticationEditor(props: 
AuthenticationEditorProps) {
       ) : (
         <p>No authentication methods configured yet.</p>
       )}
-      <div>
-        <button onClick={() => reducer.back()}>Back</button>
-        <button onClick={() => reducer.transition("next", {})}>Next</button>
-      </div>
-    </div>
+    </AnastasisClientFrame>
   );
 }
 
@@ -701,36 +952,29 @@ export interface AttributeEntryProps {
 
 function AttributeEntry(props: AttributeEntryProps) {
   const { reducer, reducerState: backupState } = props;
-  const [attrs, setAttrs] = useState<Record<string, string>>({});
+  const [attrs, setAttrs] = useState<Record<string, string>>(
+    props.reducerState.identity_attributes ?? {},
+  );
   return (
-    <div class={style.home}>
-      <h1>Backup: Enter Basic User Attributes</h1>
-      <ErrorBanner reducer={reducer} />
-      <div>
-        {backupState.required_attributes.map((x: any, i: number) => {
-          return (
-            <AttributeEntryField
-              isFirst={i == 0}
-              setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
-              spec={x}
-              value={attrs[x.name]}
-            />
-          );
-        })}
-      </div>
-      <div>
-        <button onClick={() => reducer.back()}>Back</button>
-        <button
-          onClick={() =>
-            reducer.transition("enter_user_attributes", {
-              identity_attributes: attrs,
-            })
-          }
-        >
-          Next
-        </button>
-      </div>
-    </div>
+    <AnastasisClientFrame
+      title={withProcessLabel(reducer, "Select Country")}
+      onNext={() =>
+        reducer.transition("enter_user_attributes", {
+          identity_attributes: attrs,
+        })
+      }
+    >
+      {backupState.required_attributes.map((x: any, i: number) => {
+        return (
+          <AttributeEntryField
+            isFirst={i == 0}
+            setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
+            spec={x}
+            value={attrs[x.name]}
+          />
+        );
+      })}
+    </AnastasisClientFrame>
   );
 }
 
@@ -744,8 +988,9 @@ export interface AttributeEntryFieldProps {
 function AttributeEntryField(props: AttributeEntryFieldProps) {
   return (
     <div>
-      <label>{props.spec.label}</label>
+      <label>{props.spec.label}:</label>
       <input
+        style={{ display: "block" }}
         autoFocus={props.isFirst}
         type="text"
         value={props.value}
@@ -777,4 +1022,4 @@ function ErrorBanner(props: ErrorBannerProps) {
   return null;
 }
 
-export default Home;
+export default AnastasisClient;
diff --git a/packages/anastasis-webui/src/routes/home/style.css 
b/packages/anastasis-webui/src/routes/home/style.css
index c9f34e6c..b94981f1 100644
--- a/packages/anastasis-webui/src/routes/home/style.css
+++ b/packages/anastasis-webui/src/routes/home/style.css
@@ -1,5 +1,5 @@
 .home {
-  padding: 56px 20px;
+  padding: 1em 1em;
   min-height: 100%;
   width: 100%;
 }

-- 
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]