gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: anastasis-webui: finish backu


From: gnunet
Subject: [taler-wallet-core] branch master updated: anastasis-webui: finish backup flow
Date: Wed, 13 Oct 2021 10:51:19 +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 0f1ef7ec anastasis-webui: finish backup flow
0f1ef7ec is described below

commit 0f1ef7eca1f1ab3c5a1787b19a6caec13fb30dec
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Oct 13 10:48:25 2021 +0200

    anastasis-webui: finish backup flow
---
 packages/anastasis-webui/package.json              |   1 +
 .../src/hooks/use-anastasis-reducer.ts             | 185 +++++++-
 packages/anastasis-webui/src/routes/home/index.tsx | 518 +++++++++++++++++++--
 packages/anastasis-webui/src/routes/home/style.css |  25 +-
 pnpm-lock.yaml                                     |  51 +-
 5 files changed, 692 insertions(+), 88 deletions(-)

diff --git a/packages/anastasis-webui/package.json 
b/packages/anastasis-webui/package.json
index ddbd9ef2..fe332be0 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -21,6 +21,7 @@
     ]
   },
   "dependencies": {
+    "@gnu-taler/taler-util": "workspace:^0.8.3",
     "preact": "^10.3.1",
     "preact-render-to-string": "^5.1.4",
     "preact-router": "^3.2.1"
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts 
b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index 30bab96d..d578d141 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<ReducerState>;
+}
+
 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>): 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<AnastasisState>({
-    reducerState: undefined,
-    currentError: undefined,
-  });
+  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);
+  };
 
   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<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;
+  }
+}
diff --git a/packages/anastasis-webui/src/routes/home/index.tsx 
b/packages/anastasis-webui/src/routes/home/index.tsx
index ee339950..f6189768 100644
--- a/packages/anastasis-webui/src/routes/home/index.tsx
+++ b/packages/anastasis-webui/src/routes/home/index.tsx
@@ -1,80 +1,290 @@
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
 import { FunctionalComponent, h } from "preact";
 import { useState } from "preact/hooks";
 import {
   AnastasisReducerApi,
+  AuthMethod,
+  BackupStates,
+  ReducerStateBackup,
+  ReducerStateRecovery,
   useAnastasisReducer,
 } from "../../hooks/use-anastasis-reducer";
 import style from "./style.css";
 
+interface ContinentSelectionProps {
+  reducer: AnastasisReducerApi;
+  reducerState: ReducerStateBackup | ReducerStateRecovery;
+}
+
+function isBackup(reducer: AnastasisReducerApi) {
+  return !!reducer.currentReducerState?.backup_state;
+}
+
+function ContinentSelection(props: ContinentSelectionProps) {
+  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>
+  );
+}
+
+interface CountrySelectionProps {
+  reducer: AnastasisReducerApi;
+  reducerState: ReducerStateBackup | ReducerStateRecovery;
+}
+
+function CountrySelection(props: CountrySelectionProps) {
+  const { reducer, reducerState } = props;
+  return (
+    <div class={style.home}>
+      <h1>Backup: Select Country</h1>
+      <ErrorBanner reducer={reducer} />
+      <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>
+          );
+        })}
+      </div>
+      <div>
+        <button onClick={() => reducer.back()}>Back</button>
+      </div>
+    </div>
+  );
+}
+
 const Home: FunctionalComponent = () => {
   const reducer = useAnastasisReducer();
-  if (!reducer.currentReducerState) {
+  const reducerState = reducer.currentReducerState;
+  if (!reducerState) {
     return (
       <div class={style.home}>
         <h1>Home</h1>
         <p>
-          <button onClick={() => reducer.startBackup()}>Backup</button>
-          <button>Recover</button>
+          <button autoFocus onClick={() => reducer.startBackup()}>
+            Backup
+          </button>
+          <button onClick={() => reducer.startRecover()}>Recover</button>
         </p>
       </div>
     );
   }
   console.log("state", reducer.currentReducerState);
-  if (reducer.currentReducerState.backup_state === "CONTINENT_SELECTING") {
+
+  if (reducerState.backup_state === BackupStates.ContinentSelecting) {
+    return <ContinentSelection reducer={reducer} reducerState={reducerState} 
/>;
+  }
+  if (reducerState.backup_state === BackupStates.CountrySelecting) {
+    return <CountrySelection reducer={reducer} reducerState={reducerState} />;
+  }
+  if (reducerState.backup_state === BackupStates.UserAttributesCollecting) {
+    return <AttributeEntry reducer={reducer} backupState={reducerState} />;
+  }
+  if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
+    return (
+      <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: Select Continent</h1>
+        <h1>Backup: Review Recovery Policies</h1>
         <ErrorBanner reducer={reducer} />
         <div>
-          {reducer.currentReducerState.continents.map((x: any) => {
-            const sel = (x: string) =>
-              reducer.transition("select_continent", { continent: x });
+          {backupState.policies?.map((p, i) => {
+            const policyName = p.methods
+              .map((x) => authMethods[x.authentication_method].type)
+              .join(" + ");
             return (
-              <button onClick={() => sel(x.name)} key={x.name}>
-                {x.name}
-              </button>
+              <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>
     );
   }
-  if (reducer.currentReducerState.backup_state === "COUNTRY_SELECTING") {
+
+  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: Select Continent</h1>
+        <h1>Backup: Provide secret</h1>
         <ErrorBanner reducer={reducer} />
         <div>
-          {reducer.currentReducerState.countries.map((x: any) => {
-            const sel = (x: any) =>
-              reducer.transition("select_country", {
-                country_code: x.code,
-                currencies: [x.currency],
-              });
+          <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>
+    );
+  }
+
+  if (reducerState.backup_state === BackupStates.BackupFinished) {
+    const backupState: ReducerStateBackup = reducerState;
+    return (
+      <div class={style.home}>
+        <h1>Backup finished</h1>
+        <p>
+          Your backup of secret "{backupState.secret_name ?? "??"}" was
+          successful.
+        </p>
+        <p>The backup is stored by the following providers:</p>
+        <ul>
+          {Object.keys(backupState.success_details).map((x, i) => {
+            const sd = backupState.success_details[x];
             return (
-              <button onClick={() => sel(x)} key={x.name}>
-                {x.name} ({x.currency})
-              </button>
+              <li>
+                {x} (Policy version {sd.policy_version})
+              </li>
             );
           })}
-        </div>
+        </ul>
+        <button onClick={() => reducer.reset()}>
+          Start a new backup/recovery
+        </button>
+      </div>
+    );
+  }
+
+  if (reducerState.backup_state === BackupStates.TruthsPaying) {
+    const backupState: ReducerStateBackup = reducerState;
+    const payments = backupState.payments ?? [];
+    return (
+      <div class={style.home}>
+        <h1>Backup: Authentication Storage Payments</h1>
+        <p>
+          Some of the providers require a payment to store the encrypted
+          authentication information.
+        </p>
+        <ul>
+          {payments.map((x) => {
+            return <li>{x}</li>;
+          })}
+        </ul>
         <div>
           <button onClick={() => reducer.back()}>Back</button>
+          <button onClick={() => reducer.transition("pay", {})}>
+            Check payment(s)
+          </button>
         </div>
       </div>
     );
   }
-  if (
-    reducer.currentReducerState.backup_state === "USER_ATTRIBUTES_COLLECTING"
-  ) {
-    return <AttributeEntry reducer={reducer} />;
-  }
 
-  if (reducer.currentReducerState.backup_state === "AUTHENTICATIONS_EDITING") {
-    return <AuthenticationEditor reducer={reducer} />;
+  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>
+        <p>
+          Some of the providers require a payment to store the encrypted
+          recovery document.
+        </p>
+        <ul>
+          {payments.map((x) => {
+            return (
+              <li>
+                {x.provider}: {x.payto}
+              </li>
+            );
+          })}
+        </ul>
+        <div>
+          <button onClick={() => reducer.back()}>Back</button>
+          <button onClick={() => reducer.transition("pay", {})}>
+            Check payment(s)
+          </button>
+        </div>
+      </div>
+    );
   }
 
   console.log("unknown state", reducer.currentReducerState);
@@ -82,31 +292,232 @@ const Home: FunctionalComponent = () => {
     <div class={style.home}>
       <h1>Home</h1>
       <p>Bug: Unknown state.</p>
+      <button onClick={() => reducer.reset()}>Reset</button>
     </div>
   );
 };
 
+interface AuthMethodSetupProps {
+  method: string;
+  addAuthMethod: (x: any) => void;
+  cancel: () => void;
+}
+
+function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
+  const [mobileNumber, setMobileNumber] = useState("");
+  return (
+    <div class={style.home}>
+      <h1>Add {props.method} authentication</h1>
+      <div>
+        <p>
+          For SMS authentication, you need to provide a mobile number. When
+          recovering your secret, you will be asked to enter the code you
+          receive via SMS.
+        </p>
+        <label>
+          Mobile number{" "}
+          <input
+            value={mobileNumber}
+            autoFocus
+            onChange={(e) => setMobileNumber((e.target as any).value)}
+            type="text"
+          />
+        </label>
+        <div>
+          <button onClick={() => props.cancel()}>Cancel</button>
+          <button
+            onClick={() =>
+              props.addAuthMethod({
+                authentication_method: {
+                  type: "sms",
+                  instructions: `SMS to ${mobileNumber}`,
+                  challenge: "E1QPPS8A",
+                },
+              })
+            }
+          >
+            Add
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
+  const [questionText, setQuestionText] = useState("");
+  const [answerText, setAnswerText] = useState("");
+  return (
+    <div class={style.home}>
+      <h1>Add {props.method} authentication</h1>
+      <div>
+        <p>
+          For security question authentication, you need to provide a question
+          and its answer. When recovering your secret, you will be shown the
+          question and you will need to type the answer exactly as you typed it
+          here.
+        </p>
+        <div>
+          <label>
+            Security question
+            <input
+              value={questionText}
+              autoFocus
+              onChange={(e) => setQuestionText((e.target as any).value)}
+              type="text"
+            />
+          </label>
+        </div>
+        <div>
+          <label>
+            Answer
+            <input
+              value={answerText}
+              autoFocus
+              onChange={(e) => setAnswerText((e.target as any).value)}
+              type="text"
+            />
+          </label>
+        </div>
+        <div>
+          <button onClick={() => props.cancel()}>Cancel</button>
+          <button
+            onClick={() =>
+              props.addAuthMethod({
+                authentication_method: {
+                  type: "question",
+                  instructions: questionText,
+                  challenge: encodeCrock(stringToBytes(answerText)),
+                },
+              })
+            }
+          >
+            Add
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+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>
+  );
+}
+
 export interface AuthenticationEditorProps {
   reducer: AnastasisReducerApi;
+  backupState: ReducerStateBackup;
 }
 
 function AuthenticationEditor(props: AuthenticationEditorProps) {
-  const { reducer } = props;
-  const providers = reducer.currentReducerState.authentication_providers;
-  const authAvailable = new Set<string>();
+  const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
+    undefined,
+  );
+  const { reducer, backupState } = props;
+  const providers = backupState.authentication_providers;
+  const authAvailableSet = new Set<string>();
   for (const provKey of Object.keys(providers)) {
     const p = providers[provKey];
     for (const meth of p.methods) {
-      authAvailable.add(meth.type);
+      authAvailableSet.add(meth.type);
+    }
+  }
+  if (selectedMethod) {
+    const cancel = () => setSelectedMethod(undefined);
+    const addMethod = (args: any) => {
+      reducer.transition("add_authentication", args);
+      setSelectedMethod(undefined);
+    };
+    switch (selectedMethod) {
+      case "sms":
+        return (
+          <AuthMethodSmsSetup
+            cancel={cancel}
+            addAuthMethod={addMethod}
+            method="sms"
+          />
+        );
+      case "question":
+        return (
+          <AuthMethodQuestionSetup
+            cancel={cancel}
+            addAuthMethod={addMethod}
+            method="sms"
+          />
+        );
+      default:
+        return (
+          <AuthMethodNotImplemented
+            cancel={cancel}
+            addAuthMethod={addMethod}
+            method={selectedMethod}
+          />
+        );
     }
   }
+  function MethodButton(props: { method: string; label: String }) {
+    return (
+      <button
+        disabled={!authAvailableSet.has(props.method)}
+        onClick={() => {
+          setSelectedMethod(props.method);
+          reducer.dismissError();
+        }}
+      >
+        {props.label}
+      </button>
+    );
+  }
+  const configuredAuthMethods: AuthMethod[] =
+    backupState.authentication_methods ?? [];
+  const haveMethodsConfigured = configuredAuthMethods.length;
   return (
     <div class={style.home}>
       <h1>Backup: Configure Authentication Methods</h1>
-      <p>Auths available: {JSON.stringify(Array.from(authAvailable))}</p>
-      <button>Next</button>
+      <ErrorBanner reducer={reducer} />
+      <h2>Add authentication method</h2>
+      <div>
+        <MethodButton method="sms" label="SMS" />
+        <MethodButton method="email" label="Email" />
+        <MethodButton method="question" label="Question" />
+        <MethodButton method="post" label="Physical Mail" />
+        <MethodButton method="totp" label="TOTP" />
+        <MethodButton method="iban" label="IBAN" />
+      </div>
+      <h2>Configured authentication methods</h2>
+      {haveMethodsConfigured ? (
+        configuredAuthMethods.map((x, i) => {
+          return (
+            <p>
+              {x.type} ({x.instructions}){" "}
+              <button
+                onClick={() =>
+                  reducer.transition("delete_authentication", {
+                    authentication_method: i,
+                  })
+                }
+              >
+                Delete
+              </button>
+            </p>
+          );
+        })
+      ) : (
+        <p>No authentication methods configured yet.</p>
+      )}
       <div>
         <button onClick={() => reducer.back()}>Back</button>
+        <button onClick={() => reducer.transition("next", {})}>Next</button>
       </div>
     </div>
   );
@@ -114,19 +525,21 @@ function AuthenticationEditor(props: 
AuthenticationEditorProps) {
 
 export interface AttributeEntryProps {
   reducer: AnastasisReducerApi;
+  backupState: ReducerStateBackup;
 }
 
 function AttributeEntry(props: AttributeEntryProps) {
-  const reducer = props.reducer;
+  const { reducer, backupState } = props;
   const [attrs, setAttrs] = useState<Record<string, string>>({});
   return (
     <div class={style.home}>
       <h1>Backup: Enter Basic User Attributes</h1>
       <ErrorBanner reducer={reducer} />
       <div>
-        {reducer.currentReducerState.required_attributes.map((x: any) => {
+        {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]}
@@ -134,23 +547,24 @@ function AttributeEntry(props: AttributeEntryProps) {
           );
         })}
       </div>
-      <button
-        onClick={() =>
-          reducer.transition("enter_user_attributes", {
-            identity_attributes: attrs,
-          })
-        }
-      >
-        Next
-      </button>
       <div>
         <button onClick={() => reducer.back()}>Back</button>
+        <button
+          onClick={() =>
+            reducer.transition("enter_user_attributes", {
+              identity_attributes: attrs,
+            })
+          }
+        >
+          Next
+        </button>
       </div>
     </div>
   );
 }
 
 export interface AttributeEntryFieldProps {
+  isFirst: boolean;
   value: string;
   setValue: (newValue: string) => void;
   spec: any;
@@ -161,6 +575,7 @@ function AttributeEntryField(props: 
AttributeEntryFieldProps) {
     <div>
       <label>{props.spec.label}</label>
       <input
+        autoFocus={props.isFirst}
         type="text"
         value={props.value}
         onChange={(e) => props.setValue((e as any).target.value)}
@@ -179,7 +594,14 @@ interface ErrorBannerProps {
 function ErrorBanner(props: ErrorBannerProps) {
   const currentError = props.reducer.currentError;
   if (currentError) {
-    return <div>Error: {JSON.stringify(currentError)}</div>;
+    return (
+      <div id={style.error}>
+        <p>Error: {JSON.stringify(currentError)}</p>
+        <button onClick={() => props.reducer.dismissError()}>
+          Dismiss Error
+        </button>
+      </div>
+    );
   }
   return null;
 }
diff --git a/packages/anastasis-webui/src/routes/home/style.css 
b/packages/anastasis-webui/src/routes/home/style.css
index f052d254..c9f34e6c 100644
--- a/packages/anastasis-webui/src/routes/home/style.css
+++ b/packages/anastasis-webui/src/routes/home/style.css
@@ -1,5 +1,24 @@
 .home {
-       padding: 56px 20px;
-       min-height: 100%;
-       width: 100%;
+  padding: 56px 20px;
+  min-height: 100%;
+  width: 100%;
+}
+
+.home div {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+
+.policy {
+  padding: 0.5em;
+  border: 1px solid black;
+  border-radius: 0.5em;
+  border-radius: 0.5em;
+}
+
+.home > #error {
+  padding: 0.5em;
+  border: 1px solid black;
+  background-color: rgb(228, 189, 197);
+  border-radius: 0.5em;
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b8f1fd54..fbd3c7e9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,6 +27,7 @@ importers:
 
   packages/anastasis-webui:
     specifiers:
+      '@gnu-taler/taler-util': workspace:^0.8.3
       '@types/enzyme': ^3.10.5
       '@types/jest': ^26.0.8
       '@typescript-eslint/eslint-plugin': ^2.25.0
@@ -44,6 +45,7 @@ importers:
       sirv-cli: ^1.0.0-next.3
       typescript: ^3.7.5
     dependencies:
+      '@gnu-taler/taler-util': link:../taler-util
       preact: 10.5.14
       preact-render-to-string: 5.1.19_preact@10.5.14
       preact-router: 3.2.1_preact@10.5.14
@@ -4599,7 +4601,7 @@ packages:
     dependencies:
       '@types/estree': 0.0.39
       estree-walker: 1.0.1
-      picomatch: 2.2.2
+      picomatch: 2.3.0
       rollup: 2.56.2
     dev: true
 
@@ -7681,7 +7683,7 @@ packages:
   /axios/0.21.1:
     resolution: {integrity: 
sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==}
     dependencies:
-      follow-redirects: 1.14.2
+      follow-redirects: 1.14.2_debug@4.3.2
     transitivePeerDependencies:
       - debug
 
@@ -10771,18 +10773,18 @@ packages:
     peerDependencies:
       eslint: ^3 || ^4 || ^5 || ^6 || ^7
     dependencies:
-      array-includes: 3.1.2
+      array-includes: 3.1.3
       array.prototype.flatmap: 1.2.4
       doctrine: 2.1.0
       eslint: 6.8.0
       has: 1.0.3
       jsx-ast-utils: 3.2.0
-      object.entries: 1.1.3
-      object.fromentries: 2.0.3
-      object.values: 1.1.2
+      object.entries: 1.1.4
+      object.fromentries: 2.0.4
+      object.values: 1.1.4
       prop-types: 15.7.2
-      resolve: 1.19.0
-      string.prototype.matchall: 4.0.3
+      resolve: 1.20.0
+      string.prototype.matchall: 4.0.5
     dev: true
 
   /eslint-plugin-react/7.22.0_eslint@7.18.0:
@@ -11444,7 +11446,7 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
-  /follow-redirects/1.14.2:
+  /follow-redirects/1.14.2_debug@4.3.2:
     resolution: {integrity: 
sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==}
     engines: {node: '>=4.0'}
     peerDependencies:
@@ -11452,6 +11454,8 @@ packages:
     peerDependenciesMeta:
       debug:
         optional: true
+    dependencies:
+      debug: 4.3.2_supports-color@6.1.0
 
   /for-each/0.3.3:
     resolution: {integrity: 
sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -12485,7 +12489,7 @@ packages:
     engines: {node: '>=8.0.0'}
     dependencies:
       eventemitter3: 4.0.7
-      follow-redirects: 1.14.2
+      follow-redirects: 1.14.2_debug@4.3.2
       requires-port: 1.0.0
     transitivePeerDependencies:
       - debug
@@ -14131,7 +14135,7 @@ packages:
     resolution: {integrity: 
sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==}
     engines: {node: '>=4.0'}
     dependencies:
-      array-includes: 3.1.2
+      array-includes: 3.1.3
       object.assign: 4.1.2
     dev: true
 
@@ -15962,11 +15966,11 @@ packages:
       - typescript
     dev: true
 
-  /pnp-webpack-plugin/1.7.0_typescript@4.3.5:
+  /pnp-webpack-plugin/1.7.0_typescript@4.4.3:
     resolution: {integrity: 
sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==}
     engines: {node: '>=6'}
     dependencies:
-      ts-pnp: 1.2.0_typescript@4.3.5
+      ts-pnp: 1.2.0_typescript@4.4.3
     transitivePeerDependencies:
       - typescript
     dev: true
@@ -16770,7 +16774,7 @@ packages:
       native-url: 0.3.4
       optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
       ora: 5.4.1
-      pnp-webpack-plugin: 1.7.0_typescript@4.3.5
+      pnp-webpack-plugin: 1.7.0_typescript@4.4.3
       postcss: 8.3.6
       postcss-load-config: 3.1.0
       postcss-loader: 4.3.0_postcss@8.3.6+webpack@4.46.0
@@ -16788,7 +16792,7 @@ packages:
       stack-trace: 0.0.10
       style-loader: 2.0.0_webpack@4.46.0
       terser-webpack-plugin: 4.2.3_webpack@4.46.0
-      typescript: 4.3.5
+      typescript: 4.4.3
       update-notifier: 5.1.0
       url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0
       validate-npm-package-name: 3.0.0
@@ -18039,11 +18043,11 @@ packages:
     peerDependencies:
       rollup: ^2.0.0
     dependencies:
-      '@babel/code-frame': 7.12.13
+      '@babel/code-frame': 7.14.5
       jest-worker: 26.6.2
       rollup: 2.56.2
       serialize-javascript: 4.0.0
-      terser: 5.4.0
+      terser: 5.7.1
     dev: true
 
   /rollup/2.37.1:
@@ -19167,6 +19171,7 @@ packages:
   /svgo/1.3.2:
     resolution: {integrity: 
sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==}
     engines: {node: '>=4.0.0'}
+    deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x.
     hasBin: true
     dependencies:
       chalk: 2.4.2
@@ -19588,6 +19593,18 @@ packages:
       typescript: 4.3.5
     dev: true
 
+  /ts-pnp/1.2.0_typescript@4.4.3:
+    resolution: {integrity: 
sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
+    engines: {node: '>=6'}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+    dependencies:
+      typescript: 4.4.3
+    dev: true
+
   /tsconfig-paths/3.9.0:
     resolution: {integrity: 
sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==}
     dependencies:

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