From 9fb6536fbc91adaf7a8a80860fcef5e1f80bfb3b Mon Sep 17 00:00:00 2001
From: Florian Dold <florian@dold.me>
Date: Wed, 3 Nov 2021 18:56:19 +0100
Subject: anastasis-webui: show feedback in solution screen

---
 .../anastasis-webui/src/pages/home/SolveScreen.tsx | 177 ++++++++++++++++-----
 1 file changed, 133 insertions(+), 44 deletions(-)

(limited to 'packages/anastasis-webui/src/pages/home/SolveScreen.tsx')

diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
index b0cfa9bb0..d4d9271b4 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
@@ -1,38 +1,89 @@
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { AnastasisClientFrame } from ".";
-import { ChallengeFeedback, ChallengeInfo } from "../../../../anastasis-core/lib";
+import {
+  ChallengeFeedback,
+  ChallengeFeedbackStatus,
+  ChallengeInfo,
+} from "../../../../anastasis-core/lib";
 import { TextInput } from "../../components/fields/TextInput";
 import { useAnastasisContext } from "../../context/anastasis";
 
+function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
+  const { feedback } = props;
+  if (!feedback) {
+    return null;
+  }
+  switch (feedback.state) {
+    case ChallengeFeedbackStatus.Message:
+      return (
+        <div>
+          <p>{feedback.message}</p>
+        </div>
+      );
+    case ChallengeFeedbackStatus.Pending:
+    case ChallengeFeedbackStatus.AuthIban:
+      return null;
+    case ChallengeFeedbackStatus.RateLimitExceeded:
+      return <div>Rate limit exceeded.</div>;
+    case ChallengeFeedbackStatus.Redirect:
+      return <div>Redirect (FIXME: not supported)</div>;
+    case ChallengeFeedbackStatus.Unsupported:
+      return <div>Challenge not supported by client.</div>;
+    case ChallengeFeedbackStatus.TruthUnknown:
+      return <div>Truth unknown</div>;
+    default:
+      return (
+        <div>
+          <pre>{JSON.stringify(feedback)}</pre>
+        </div>
+      );
+  }
+}
+
 export function SolveScreen(): VNode {
-  const reducer = useAnastasisContext()
+  const reducer = useAnastasisContext();
   const [answer, setAnswer] = useState("");
 
   if (!reducer) {
-    return <AnastasisClientFrame hideNav title="Recovery problem">
-      <div>no reducer in context</div>
-    </AnastasisClientFrame>
+    return (
+      <AnastasisClientFrame hideNav title="Recovery problem">
+        <div>no reducer in context</div>
+      </AnastasisClientFrame>
+    );
   }
-  if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
-    return <AnastasisClientFrame hideNav title="Recovery problem">
-      <div>invalid state</div>
-    </AnastasisClientFrame>
+  if (
+    !reducer.currentReducerState ||
+    reducer.currentReducerState.recovery_state === undefined
+  ) {
+    return (
+      <AnastasisClientFrame hideNav title="Recovery problem">
+        <div>invalid state</div>
+      </AnastasisClientFrame>
+    );
   }
 
   if (!reducer.currentReducerState.recovery_information) {
-    return <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery problem">
-      <div>no recovery information found</div>
-    </AnastasisClientFrame>
+    return (
+      <AnastasisClientFrame
+        hideNext="Recovery document not found"
+        title="Recovery problem"
+      >
+        <div>no recovery information found</div>
+      </AnastasisClientFrame>
+    );
   }
   if (!reducer.currentReducerState.selected_challenge_uuid) {
-    return <AnastasisClientFrame hideNav title="Recovery problem">
-      <div>invalid state</div>
-    </AnastasisClientFrame>
+    return (
+      <AnastasisClientFrame hideNav title="Recovery problem">
+        <div>invalid state</div>
+      </AnastasisClientFrame>
+    );
   }
 
   const chArr = reducer.currentReducerState.recovery_information.challenges;
-  const challengeFeedback = reducer.currentReducerState.challenge_feedback ?? {};
+  const challengeFeedback =
+    reducer.currentReducerState.challenge_feedback ?? {};
   const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
   const challenges: {
     [uuid: string]: ChallengeInfo;
@@ -47,31 +98,44 @@ export function SolveScreen(): VNode {
     email: SolveEmailEntry,
     post: SolvePostEntry,
   };
-  const SolveDialog = selectedChallenge === undefined ? SolveUndefinedEntry : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
+  const SolveDialog =
+    selectedChallenge === undefined
+      ? SolveUndefinedEntry
+      : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
 
   function onNext(): void {
-    reducer?.transition("solve_challenge", { answer })
+    reducer?.transition("solve_challenge", { answer });
   }
   function onCancel(): void {
-    reducer?.back()
+    reducer?.back();
   }
 
-
   return (
-    <AnastasisClientFrame
-      hideNav
-      title="Recovery: Solve challenge"
-    >
+    <AnastasisClientFrame hideNav title="Recovery: Solve challenge">
+      <SolveOverviewFeedbackDisplay
+        feedback={challengeFeedback[selectedUuid]}
+      />
       <SolveDialog
         id={selectedUuid}
         answer={answer}
         setAnswer={setAnswer}
         challenge={selectedChallenge}
-        feedback={challengeFeedback[selectedUuid]} />
+        feedback={challengeFeedback[selectedUuid]}
+      />
 
-      <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
-        <button class="button" onClick={onCancel}>Cancel</button>
-        <button class="button is-info" onClick={onNext} >Confirm</button>
+      <div
+        style={{
+          marginTop: "2em",
+          display: "flex",
+          justifyContent: "space-between",
+        }}
+      >
+        <button class="button" onClick={onCancel}>
+          Cancel
+        </button>
+        <button class="button is-info" onClick={onNext}>
+          Confirm
+        </button>
       </div>
     </AnastasisClientFrame>
   );
@@ -85,38 +149,61 @@ export interface SolveEntryProps {
   setAnswer: (s: string) => void;
 }
 
-function SolveSmsEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode {
-  return (<Fragment>
-    <p>An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below</p>
-    <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
-  </Fragment>
+function SolveSmsEntry({
+  challenge,
+  answer,
+  setAnswer,
+}: SolveEntryProps): VNode {
+  return (
+    <Fragment>
+      <p>
+        An sms has been sent to "<b>{challenge.instructions}</b>". Type the code
+        below
+      </p>
+      <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+    </Fragment>
   );
 }
-function SolveQuestionEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode {
+function SolveQuestionEntry({
+  challenge,
+  answer,
+  setAnswer,
+}: SolveEntryProps): VNode {
   return (
     <Fragment>
       <p>Type the answer to the following question:</p>
-      <pre>
-        {challenge.instructions}
-      </pre>
+      <pre>{challenge.instructions}</pre>
       <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
     </Fragment>
   );
 }
 
-function SolvePostEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode {
+function SolvePostEntry({
+  challenge,
+  answer,
+  setAnswer,
+}: SolveEntryProps): VNode {
   return (
     <Fragment>
-      <p>instruction for post type challenge "<b>{challenge.instructions}</b>"</p>
+      <p>
+        instruction for post type challenge "<b>{challenge.instructions}</b>"
+      </p>
       <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
     </Fragment>
   );
 }
 
-function SolveEmailEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode {
+function SolveEmailEntry({
+  challenge,
+  answer,
+  setAnswer,
+}: SolveEntryProps): VNode {
   return (
     <Fragment>
-      <p>An email has been sent to "<b>{challenge.instructions}</b>". Type the code below</p>
+      <p>
+        An email has been sent to "<b>{challenge.instructions}</b>". Type the
+        code below
+      </p>
       <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
     </Fragment>
   );
@@ -126,7 +213,8 @@ function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
   return (
     <Fragment>
       <p>
-        The challenge selected is not supported for this UI. Please update this version or try using another policy.
+        The challenge selected is not supported for this UI. Please update this
+        version or try using another policy.
       </p>
       <p>
         <b>Challenge type:</b> {props.challenge.type}
@@ -136,9 +224,10 @@ function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
 }
 function SolveUndefinedEntry(props: SolveEntryProps): VNode {
   return (
-    <Fragment >
+    <Fragment>
       <p>
-        There is no challenge information for id <b>"{props.id}"</b>. Try resetting the recovery session.
+        There is no challenge information for id <b>"{props.id}"</b>. Try
+        resetting the recovery session.
       </p>
     </Fragment>
   );
-- 
cgit v1.2.3

