diff options
| author | Sebastian <sebasjm@gmail.com> | 2021-10-27 15:13:35 -0300 | 
|---|---|---|
| committer | Sebastian <sebasjm@gmail.com> | 2021-10-27 15:13:35 -0300 | 
| commit | 32318a80f48bf52ca7823a0c055164f43bdaf1d6 (patch) | |
| tree | 08ef42e65d1db93f3958dd75d39f4bff1d03d2b6 /packages | |
| parent | 21b60c8f6ff69bf114779a767a3ac3355f69a34f (diff) | |
working version with improved ui
Diffstat (limited to 'packages')
23 files changed, 521 insertions, 280 deletions
| diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx index c45acc6d2..e1c354f7b 100644 --- a/packages/anastasis-webui/src/components/fields/DateInput.tsx +++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx @@ -8,6 +8,7 @@ export interface DateInputProps {    grabFocus?: boolean;    tooltip?: string;    error?: string; +  years?: Array<number>;    bind: [string, (x: string) => void];  } @@ -19,7 +20,7 @@ export function DateInput(props: DateInputProps): VNode {      }    }, [props.grabFocus]);    const [opened, setOpened2] = useState(false) -  function setOpened(v: boolean) { +  function setOpened(v: boolean): void {      console.log('dale', v)      setOpened2(v)    } @@ -50,6 +51,7 @@ export function DateInput(props: DateInputProps): VNode {      {showError && <p class="help is-danger">{props.error}</p>}      <DatePicker        opened={opened} +      years={props.years}        closeFunction={() => setOpened(false)}        dateReceiver={(d) => {          setDirty(true) diff --git a/packages/anastasis-webui/src/components/fields/NumberInput.tsx b/packages/anastasis-webui/src/components/fields/NumberInput.tsx new file mode 100644 index 000000000..af9bbe66b --- /dev/null +++ b/packages/anastasis-webui/src/components/fields/NumberInput.tsx @@ -0,0 +1,41 @@ +import { h, VNode } from "preact"; +import { useLayoutEffect, useRef, useState } from "preact/hooks"; + +export interface TextInputProps { +  label: string; +  grabFocus?: boolean; +  error?: string; +  tooltip?: string; +  bind: [string, (x: string) => void]; +} + +export function NumberInput(props: TextInputProps): VNode { +  const inputRef = useRef<HTMLInputElement>(null); +  useLayoutEffect(() => { +    if (props.grabFocus) { +      inputRef.current?.focus(); +    } +  }, [props.grabFocus]); +  const value = props.bind[0]; +  const [dirty, setDirty] = useState(false) +  const showError = dirty && props.error +  return (<div class="field"> +    <label class="label"> +      {props.label} +      {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}> +        <i class="mdi mdi-information" /> +      </span>} +    </label> +    <div class="control has-icons-right"> +      <input +        value={value} +        type="number" +        class={showError ? 'input is-danger' : 'input'} +        onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}} +        ref={inputRef} +        style={{ display: "block" }} /> +    </div> +    {showError && <p class="help is-danger">{props.error}</p>} +  </div> +  ); +} diff --git a/packages/anastasis-webui/src/components/fields/LabeledInput.tsx b/packages/anastasis-webui/src/components/fields/TextInput.tsx index 96d634a4f..fa6fd9792 100644 --- a/packages/anastasis-webui/src/components/fields/LabeledInput.tsx +++ b/packages/anastasis-webui/src/components/fields/TextInput.tsx @@ -1,7 +1,7 @@  import { h, VNode } from "preact";  import { useLayoutEffect, useRef, useState } from "preact/hooks"; -export interface LabeledInputProps { +export interface TextInputProps {    label: string;    grabFocus?: boolean;    error?: string; @@ -9,7 +9,7 @@ export interface LabeledInputProps {    bind: [string, (x: string) => void];  } -export function LabeledInput(props: LabeledInputProps): VNode { +export function TextInput(props: TextInputProps): VNode {    const inputRef = useRef<HTMLInputElement>(null);    useLayoutEffect(() => {      if (props.grabFocus) { diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx index 12223d473..87e771009 100644 --- a/packages/anastasis-webui/src/components/menu/SideBar.tsx +++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx @@ -64,9 +64,8 @@ export function Sidebar({ mobile }: Props): VNode {              </li>            }            {reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment> -            <li class={ -                reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || -                reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}> +            <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || +              reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>                <div class="ml-4">                  <span class="menu-item-label"><Translate>Location & Currency</Translate></span>                </div> @@ -79,73 +78,65 @@ export function Sidebar({ mobile }: Props): VNode {              <li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>Auth methods</Translate></span> +                <span class="menu-item-label"><Translate>Authorization methods</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>PoliciesReviewing</Translate></span> +                <span class="menu-item-label"><Translate>Policies reviewing</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>SecretEditing</Translate></span> +                <span class="menu-item-label"><Translate>Secret input</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>PoliciesPaying</Translate></span> +                <span class="menu-item-label"><Translate>Payment (optional)</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>BackupFinished</Translate></span> +                <span class="menu-item-label"><Translate>Backup completed</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>TruthsPaying</Translate></span> +                <span class="menu-item-label"><Translate>Truth Paying</Translate></span>                </div>              </li>            </Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment> -            <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ? 'is-active' : ''}> +            <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting || +              reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>ContinentSelecting</Translate></span> -              </div> -            </li> -            <li class={reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}> -              <div class="ml-4"> -                <span class="menu-item-label"><Translate>CountrySelecting</Translate></span> +                <span class="menu-item-label"><Translate>Location & Currency</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>UserAttributesCollecting</Translate></span> +                <span class="menu-item-label"><Translate>Personal information</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>SecretSelecting</Translate></span> -              </div> -            </li> -            <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ? 'is-active' : ''}> -              <div class="ml-4"> -                <span class="menu-item-label"><Translate>ChallengeSelecting</Translate></span> +                <span class="menu-item-label"><Translate>Secret selection</Translate></span>                </div>              </li> -            <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}> +            <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting || +              reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>ChallengeSolving</Translate></span> +                <span class="menu-item-label"><Translate>Solve Challenges</Translate></span>                </div>              </li>              <li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>                <div class="ml-4"> -                <span class="menu-item-label"><Translate>RecoveryFinished</Translate></span> +                <span class="menu-item-label"><Translate>Secret recovered</Translate></span>                </div>              </li>            </Fragment>)} diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx index e51b3db68..5b33fa8be 100644 --- a/packages/anastasis-webui/src/components/picker/DatePicker.tsx +++ b/packages/anastasis-webui/src/components/picker/DatePicker.tsx @@ -24,6 +24,7 @@ import { h, Component } from "preact";  interface Props {    closeFunction?: () => void;    dateReceiver?: (d: Date) => void; +  years?: Array<number>;    opened?: boolean;  }  interface State { @@ -207,9 +208,9 @@ export class DatePicker extends Component<Props, State> {    }    componentDidUpdate() { -    if (this.state.selectYearMode) { -      document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it -    } +    // if (this.state.selectYearMode) { +    //   document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it +    // }    }    constructor() { @@ -296,8 +297,7 @@ export class DatePicker extends Component<Props, State> {              </div>}              {selectYearMode && <div class="datePicker--selectYear"> - -              {yearArr.map(year => ( +              {(this.props.years || yearArr).map(year => (                  <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>                    {year}                  </span> diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx index d9be48fb4..32d7817e3 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx @@ -40,21 +40,21 @@ export default {  export const Backup = createExample(TestedComponent, {    ...reducerStatesExample.backupAttributeEditing,    required_attributes: [{ -    name: 'first', +    name: 'first name',      label: 'first', -    type: 'type', +    type: 'string',      uuid: 'asdasdsa1',      widget: 'wid',    }, { -    name: 'pepe', +    name: 'last name',      label: 'second', -    type: 'type', +    type: 'string',      uuid: 'asdasdsa2',      widget: 'wid',    }, { -    name: 'pepe2', +    name: 'date',      label: 'third', -    type: 'type', +    type: 'date',      uuid: 'asdasdsa3',      widget: 'calendar',    }] @@ -65,19 +65,19 @@ export const Recovery = createExample(TestedComponent, {    required_attributes: [{      name: 'first',      label: 'first', -    type: 'type', +    type: 'string',      uuid: 'asdasdsa1',      widget: 'wid',    }, {      name: 'pepe',      label: 'second', -    type: 'type', +    type: 'string',      uuid: 'asdasdsa2',      widget: 'wid',    }, {      name: 'pepe2',      label: 'third', -    type: 'type', +    type: 'date',      uuid: 'asdasdsa3',      widget: 'calendar',    }] @@ -110,12 +110,20 @@ const allWidgets = [    "anastasis_gtk_xx_square",  ] +function typeForWidget(name: string): string { +  if (["anastasis_gtk_xx_prime", +    "anastasis_gtk_xx_square", +  ].includes(name)) return "number"; +  if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date" +  return "string"; +} +  export const WithAllPosibleWidget = createExample(TestedComponent, {    ...reducerStatesExample.backupAttributeEditing,    required_attributes: allWidgets.map(w => ({      name: w,      label: `widget: ${w}`, -    type: 'type', +    type: typeForWidget(w),      uuid: `uuid-${w}`,      widget: w    })) diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx index 3b39cf9c4..f74dcefba 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx @@ -4,8 +4,9 @@ import { h, VNode } from "preact";  import { useState } from "preact/hooks";  import { useAnastasisContext } from "../../context/anastasis";  import { AnastasisClientFrame, withProcessLabel } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; +import { TextInput } from "../../components/fields/TextInput";  import { DateInput } from "../../components/fields/DateInput"; +import { NumberInput } from "../../components/fields/NumberInput";  export function AttributeEntryScreen(): VNode {    const reducer = useAnastasisContext() @@ -65,6 +66,7 @@ export function AttributeEntryScreen(): VNode {          </div>          <div class="column is-half" > +          <p>This personal information will help to locate your secret in the first place</p>            <h1><b>This stay private</b></h1>            <p>The information you have entered here:            </p> @@ -92,20 +94,33 @@ interface AttributeEntryFieldProps {    spec: UserAttributeSpec;    isValid: () => string | undefined;  } - +const possibleBirthdayYear: Array<number> = [] +for (let i = 0; i < 100; i++ ) { +  possibleBirthdayYear.push(2020 - i) +}  function AttributeEntryField(props: AttributeEntryFieldProps): VNode {    const errorMessage = props.isValid()    return (      <div> -      {props.spec.type === 'date' ? +      {props.spec.type === 'date' &&          <DateInput            grabFocus={props.isFirst}            label={props.spec.label} +          years={possibleBirthdayYear} +          error={errorMessage} +          bind={[props.value, props.setValue]} +        />} +      {props.spec.type === 'number' && +        <NumberInput +          grabFocus={props.isFirst} +          label={props.spec.label}            error={errorMessage}            bind={[props.value, props.setValue]} -        /> : -        <LabeledInput +        /> +      } +      {props.spec.type === 'string' && +        <TextInput            grabFocus={props.isFirst}            label={props.spec.label}            error={errorMessage} diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx index 5243c5259..c3783ea6c 100644 --- a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx @@ -7,7 +7,7 @@ import { h, VNode } from "preact";  import { useState } from "preact/hooks";  import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";  import { AnastasisClientFrame } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; +import { TextInput } from "../../components/fields/TextInput";  export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {    const [email, setEmail] = useState(""); @@ -19,7 +19,7 @@ export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {          email.        </p>        <div> -        <LabeledInput +        <TextInput            label="Email address"            grabFocus            bind={[email, setEmail]} /> diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx index 1c2a9a92e..c4ddeff91 100644 --- a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx @@ -6,7 +6,7 @@ import {  import { h, VNode } from "preact";  import { useState } from "preact/hooks";  import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { LabeledInput } from "../../components/fields/LabeledInput"; +import { TextInput } from "../../components/fields/TextInput";  export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {    const [fullName, setFullName] = useState(""); @@ -42,22 +42,22 @@ export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {            code that you will receive in a letter to that address.          </p>          <div> -          <LabeledInput +          <TextInput              grabFocus              label="Full Name"              bind={[fullName, setFullName]} />          </div>          <div> -          <LabeledInput label="Street" bind={[street, setStreet]} /> +          <TextInput label="Street" bind={[street, setStreet]} />          </div>          <div> -          <LabeledInput label="City" bind={[city, setCity]} /> +          <TextInput label="City" bind={[city, setCity]} />          </div>          <div> -          <LabeledInput label="Postal Code" bind={[postcode, setPostcode]} /> +          <TextInput label="Postal Code" bind={[postcode, setPostcode]} />          </div>          <div> -          <LabeledInput label="Country" bind={[country, setCountry]} /> +          <TextInput label="Country" bind={[country, setCountry]} />          </div>          <div>            <button onClick={() => props.cancel()}>Cancel</button> diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx index c2bd24ef9..f1bab94ab 100644 --- a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx @@ -7,7 +7,7 @@ import { h, VNode } from "preact";  import { useState } from "preact/hooks";  import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";  import { AnastasisClientFrame } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; +import { TextInput } from "../../components/fields/TextInput";  export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {    const [questionText, setQuestionText] = useState(""); @@ -29,13 +29,13 @@ export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {            here.          </p>          <div> -          <LabeledInput +          <TextInput              label="Security question"              grabFocus              bind={[questionText, setQuestionText]} />          </div>          <div> -          <LabeledInput label="Answer" bind={[answerText, setAnswerText]} /> +          <TextInput label="Answer" bind={[answerText, setAnswerText]} />          </div>          <div>            <button onClick={() => props.cancel()}>Cancel</button> diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx index def44c5a6..758963574 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx @@ -37,7 +37,7 @@ export default {    },  }; -export const OneChallenge = createExample(TestedComponent, { +export const OneUnsolvedPolicy = createExample(TestedComponent, {    ...reducerStatesExample.challengeSelecting,    recovery_information: {      policies: [[{ uuid: '1' }]], @@ -50,7 +50,7 @@ export const OneChallenge = createExample(TestedComponent, {    },  } as ReducerState); -export const MoreChallenges = createExample(TestedComponent, { +export const SomePoliciesOneSolved = createExample(TestedComponent, {    ...reducerStatesExample.challengeSelecting,    recovery_information: {      policies: [[{ uuid: '1' }, { uuid: '2' }], [{ uuid: 'uuid-3' }]], @@ -75,13 +75,13 @@ export const MoreChallenges = createExample(TestedComponent, {      'uuid-3': {        state: 'solved'      } -  } +  },  } as ReducerState);  export const OneBadConfiguredPolicy = createExample(TestedComponent, {    ...reducerStatesExample.challengeSelecting,    recovery_information: { -    policies: [[{ uuid: '2' }]], +    policies: [[{ uuid: '1' }, { uuid: '2' }]],      challenges: [{        cost: 'USD:1',        instructions: 'just go for it', @@ -91,4 +91,130 @@ export const OneBadConfiguredPolicy = createExample(TestedComponent, {    },  } as ReducerState); +export const OnePolicyWithAllTheChallenges = createExample(TestedComponent, { +  ...reducerStatesExample.challengeSelecting, +  recovery_information: { +    policies: [[ +      { uuid: '1' }, +      { uuid: '2' }, +      { uuid: '3' }, +      { uuid: '4' }, +      { uuid: '5' }, +      { uuid: '6' }, +    ]], +    challenges: [{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '1', +    },{ +      cost: 'USD:1', +      instructions: 'enter a text received by a sms', +      type: 'sms', +      uuid: '2', +    },{ +      cost: 'USD:1', +      instructions: 'enter a text received by a email', +      type: 'email', +      uuid: '3', +    },{ +      cost: 'USD:1', +      instructions: 'enter a code based on a time-based one-time password', +      type: 'totp', +      uuid: '4', +    },{ +      cost: 'USD:1', +      instructions: 'send a wire transfer to an account', +      type: 'iban', +      uuid: '5', +    },{ +      cost: 'USD:1', +      instructions: 'just go for it', +      type: 'new-type-of-challenge', +      uuid: '6', +    }], +  }, +} as ReducerState); + + +export const OnePolicyWithAllTheChallengesInDifferentState = createExample(TestedComponent, { +  ...reducerStatesExample.challengeSelecting, +  recovery_information: { +    policies: [[ +      { uuid: '1' }, +      { uuid: '2' }, +      { uuid: '3' }, +      { uuid: '4' }, +      { uuid: '5' }, +      { uuid: '6' }, +      { uuid: '7' }, +      { uuid: '8' }, +      { uuid: '9' }, +      { uuid: '10' }, +    ]], +    challenges: [{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '1', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '2', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '3', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '4', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '5', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '6', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '7', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '8', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '9', +    },{ +      cost: 'USD:1', +      instructions: 'answer the a question correctly', +      type: 'question', +      uuid: '10', +    }], +  }, +  challenge_feedback: { +    1: { state: 'solved' }, +    2: { state: 'hint' }, +    3: { state: 'details' }, +    4: { state: 'body' }, +    5: { state: 'redirect' }, +    6: { state: 'server-failure' }, +    7: { state: 'truth-unknown' }, +    8: { state: 'rate-limit-exceeded' }, +    9: { state: 'authentication-timeout' }, +    10: { state: 'external-instructions' }, +  } +} as ReducerState);  export const NoPolicies = createExample(TestedComponent, reducerStatesExample.challengeSelecting); diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx index c9b52e91b..3bb3fb837 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx @@ -1,3 +1,4 @@ +import { ChallengeFeedback } from "anastasis-core";  import { h, VNode } from "preact";  import { useAnastasisContext } from "../../context/anastasis";  import { AnastasisClientFrame } from "./index"; @@ -13,65 +14,94 @@ export function ChallengeOverviewScreen(): VNode {    }    const policies = reducer.currentReducerState.recovery_information?.policies ?? []; -  const chArr = reducer.currentReducerState.recovery_information?.challenges ?? []; -  const challengeFeedback = reducer.currentReducerState?.challenge_feedback; +  const knownChallengesArray = reducer.currentReducerState.recovery_information?.challenges ?? []; +  const challengeFeedback = reducer.currentReducerState?.challenge_feedback ?? {}; -  const challenges: { +  const knownChallengesMap: {      [uuid: string]: {        type: string;        instructions: string;        cost: string; +      feedback: ChallengeFeedback | undefined;      };    } = {}; -  for (const ch of chArr) { -    challenges[ch.uuid] = { +  for (const ch of knownChallengesArray) { +    knownChallengesMap[ch.uuid] = {        type: ch.type,        cost: ch.cost,        instructions: ch.instructions, +      feedback: challengeFeedback[ch.uuid]      };    } +  const policiesWithInfo = policies.map(row => { +    let isPolicySolved = true +    const challenges = row.map(({ uuid }) => { +      const info = knownChallengesMap[uuid]; +      const isChallengeSolved = info?.feedback?.state === 'solved' +      isPolicySolved = isPolicySolved && isChallengeSolved +      return { info, uuid, isChallengeSolved } +    }).filter(ch => ch.info !== undefined) + +    return { isPolicySolved, challenges } +  }) + +  const atLeastThereIsOnePolicySolved = policiesWithInfo.find(p => p.isPolicySolved) !== undefined +    return ( -    <AnastasisClientFrame title="Recovery: Solve challenges"> -      <h2>Policies</h2> -      {!policies.length && <p> -        No policies found -      </p>} -      {policies.map((row, i) => { +    <AnastasisClientFrame hideNext={!atLeastThereIsOnePolicySolved} title="Recovery: Solve challenges"> +      {!policies.length ? <p> +        No policies found, try with another version of the secret +      </p> : (policies.length === 1 ? <p> +        One policy found for this secret. You need to solve all the challenges in order to recover your secret. +      </p> : <p> +        We have found {policies.length} polices. You need to solve all the challenges from one policy in order +        to recover your secret. +      </p>)} +      {policiesWithInfo.map((row, i) => { +        const tableBody = row.challenges.map(({ info, uuid }) => { +          return ( +            <tr key={uuid}> +              <td>{info.type}</td> +              <td> +                {info.instructions} +              </td> +              <td>{info.feedback?.state ?? "unknown"}</td> +              <td>{info.cost}</td> +              <td> +                {info.feedback?.state !== "solved" ? ( +                  <a onClick={() => reducer.transition("select_challenge", { uuid })}> +                    Solve +                  </a> +                ) : null} +              </td> +            </tr> +          ); +        })          return (            <div key={i}> -            <h3>Policy #{i + 1}</h3> -            {row.map(column => { -              const ch = challenges[column.uuid]; -              if (!ch) return <div> -                There is no challenge for this policy -              </div> -              const feedback = challengeFeedback?.[column.uuid]; -              return ( -                <div key={column.uuid} -                  style={{ -                    borderLeft: "2px solid gray", -                    paddingLeft: "0.5em", -                    borderRadius: "0.5em", -                    marginTop: "0.5em", -                    marginBottom: "0.5em", -                  }} -                > -                  <h4> -                    {ch.type} ({ch.instructions}) -                  </h4> -                  <p>Status: {feedback?.state ?? "unknown"}</p> -                  {feedback?.state !== "solved" ? ( -                    <button -                      onClick={() => reducer.transition("select_challenge", { -                        uuid: column.uuid, -                      })} -                    > -                      Solve -                    </button> -                  ) : null} -                </div> -              ); -            })} +            <b>Policy #{i + 1}</b> +            {row.challenges.length === 0 && <p> +              This policy doesn't have challenges +            </p>} +            {row.challenges.length === 1 && <p> +              This policy just have one challenge to be solved +            </p>} +            {row.challenges.length > 1 && <p> +              This policy have {row.challenges.length} challenges +            </p>} +            <table class="table"> +              <thead> +                <tr> +                  <td>Challenge type</td> +                  <td>Description</td> +                  <td>Status</td> +                  <td>Cost</td> +                </tr> +              </thead> +              <tbody> +                {tableBody} +              </tbody> +            </table>            </div>          );        })} diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx index 8744a2b79..2186eb42d 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx @@ -36,4 +36,5 @@ export default {  };  export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); +  export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx index b5933db17..0d2ebb778 100644 --- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx @@ -37,7 +37,7 @@ export default {    },  }; -export const NormalEnding = createExample(TestedComponent, { +export const GoodEnding = createExample(TestedComponent, {    ...reducerStatesExample.recoveryFinished,    core_secret: { mime: 'text/plain', value: 'hello' }  } as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx index f5fd7c0d1..79a46761c 100644 --- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx @@ -5,7 +5,7 @@ import { useState } from "preact/hooks";  import { useAnastasisContext } from "../../context/anastasis";  import {    AnastasisClientFrame} from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; +import { TextInput } from "../../components/fields/TextInput";  export function SecretEditorScreen(): VNode {    const reducer = useAnastasisContext() @@ -47,14 +47,14 @@ export function SecretEditorScreen(): VNode {        onNext={() => secretNext()}      >        <div> -        <LabeledInput +        <TextInput            label="Secret Name:"            grabFocus            bind={[secretName, setSecretName]}          />        </div>        <div> -        <LabeledInput +        <TextInput            label="Secret Value:"            bind={[secretValue, setSecretValue]}          /> diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx index 903f57868..5d67ee472 100644 --- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx @@ -29,15 +29,31 @@ export function SecretSelectionScreen(): VNode {          version: n,          provider_url: p,        }); -      setSelectingVersion(false);      }); +    setSelectingVersion(false);    } +  const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {})    const recoveryDocument = reducer.currentReducerState.recovery_document    if (!recoveryDocument) {      return ( -      <AnastasisClientFrame hideNav title="Recovery: Problem"> -        <p>No recovery document found</p> +      <AnastasisClientFrame hideNext title="Recovery: Problem"> +        <p>No recovery document found, try with another provider</p> +        <table class="table"> +          <tr> +            <td><b>Provider</b></td> +            <td> +              <select onChange={(e) => setOtherProvider((e.target as any).value)}> +                <option key="none" disabled selected > Choose another provider </option> +                {providerList.map(prov => ( +                  <option key={prov} value={prov}> +                    {prov} +                  </option> +                ))} +              </select> +            </td> +          </tr> +        </table>        </AnastasisClientFrame>      )    } @@ -45,43 +61,75 @@ export function SecretSelectionScreen(): VNode {      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(reducer.currentReducerState.authentication_providers ?? {}).map( -            (x, i) => ( -              <option key={i} selected={x === recoveryDocument.provider_url} 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)}> -            Use this version -          </button> -        </div> -        <div> -          <button onClick={() => selectVersion(otherProvider, 0)}> -            Use latest version -          </button> -        </div> -        <div> -          <button onClick={() => setSelectingVersion(false)}>Cancel</button> +        <table class="table"> +          <tr> +            <td><b>Provider</b></td> +            <td> +              <select onChange={(e) => setOtherProvider((e.target as any).value)}> +                {providerList.map(prov => ( +                  <option key={prov} selected={prov === recoveryDocument.provider_url} value={prov}> +                    {prov} +                  </option> +                ))} +              </select> +            </td> +          </tr> +          <tr> +            <td><b>Version</b></td> +            <td> +              <input +                value={otherVersion} +                onChange={(e) => setOtherVersion(Number((e.target as HTMLInputElement).value))} +                type="number" /> +            </td> +            <td> +              <a onClick={() => setOtherVersion(0)}>set to latest version</a> +            </td> +          </tr> +        </table> +        <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> +          <button class="button" onClick={() => setSelectingVersion(false)}>Cancel</button> +          <button class="button is-info" onClick={() => selectVersion(otherProvider, otherVersion)}>Confirm</button>          </div> +        </AnastasisClientFrame>      );    }    return (      <AnastasisClientFrame title="Recovery: Select secret"> -      <p>Provider: {recoveryDocument.provider_url}</p> -      <p>Secret version: {recoveryDocument.version}</p> -      <p>Secret name: {recoveryDocument.secret_name}</p> -      <button onClick={() => setSelectingVersion(true)}> -        Select different secret -      </button> +      <p>Secret found, you can select another version or continue to the challenges solving</p> +      <table class="table"> +        <tr> +          <td> +            <b>Provider</b> +            <span class="icon has-tooltip-right" data-tooltip="Service provider backing up your secret"> +              <i class="mdi mdi-information" /> +            </span> +          </td> +          <td>{recoveryDocument.provider_url}</td> +          <td><a onClick={() => setSelectingVersion(true)}>use another provider</a></td> +        </tr> +        <tr> +          <td> +            <b>Secret version</b> +            <span class="icon has-tooltip-right" data-tooltip="Secret version to be recovered"> +              <i class="mdi mdi-information" /> +            </span> +          </td> +          <td>{recoveryDocument.version}</td> +          <td><a onClick={() => setSelectingVersion(true)}>use another version</a></td> +        </tr> +        <tr> +          <td> +            <b>Secret name</b> +            <span class="icon has-tooltip-right" data-tooltip="Secret identifier"> +              <i class="mdi mdi-information" /> +            </span> +          </td> +          <td>{recoveryDocument.secret_name}</td> +          <td> </td> +        </tr> +      </table>      </AnastasisClientFrame>    );  } diff --git a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx deleted file mode 100644 index 0d70405e5..000000000 --- a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; -import { SolveEntryProps } from "./SolveScreen"; - -export function SolveEmailEntry({ challenge, feedback }: SolveEntryProps): VNode { -  const [answer, setAnswer] = useState(""); -  const reducer = useAnastasisContext() -  const next = (): void => { -    if (reducer) reducer.transition("solve_challenge", { -      answer, -    }) -  }; -  return ( -    <AnastasisClientFrame -      title="Recovery: Solve challenge" -      onNext={() => next()} -    > -      <p>Feedback: {JSON.stringify(feedback)}</p> -      <p>{challenge.instructions}</p> -      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} /> -    </AnastasisClientFrame> -  ); -} diff --git a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx deleted file mode 100644 index 22b8d470b..000000000 --- a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; -import { SolveEntryProps } from "./SolveScreen"; - -export function SolvePostEntry({ challenge, feedback }: SolveEntryProps): VNode { -  const [answer, setAnswer] = useState(""); -  const reducer = useAnastasisContext() -  const next = (): void => { -    if (reducer) reducer.transition("solve_challenge", { answer }) -  }; -  return ( -    <AnastasisClientFrame -      title="Recovery: Solve challenge" -      onNext={() => next()} -    > -      <p>Feedback: {JSON.stringify(feedback)}</p> -      <p>{challenge.instructions}</p> -      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} /> -    </AnastasisClientFrame> -  ); -} diff --git a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx deleted file mode 100644 index 319289381..000000000 --- a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; -import { SolveEntryProps } from "./SolveScreen"; - -export function SolveQuestionEntry({ challenge, feedback }: SolveEntryProps): VNode { -  const [answer, setAnswer] = useState(""); -  const reducer = useAnastasisContext() -  const next = (): void => { -    if (reducer) reducer.transition("solve_challenge", { answer }) -  }; -  return ( -    <AnastasisClientFrame -      title="Recovery: Solve challenge" -      onNext={() => next()} -    > -      <p>Feedback: {JSON.stringify(feedback)}</p> -      <p>Question: {challenge.instructions}</p> -      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} /> -    </AnastasisClientFrame> -  ); -} diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx index 05ae50b48..077726e02 100644 --- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx @@ -1,28 +1,36 @@ -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AnastasisClientFrame } from ".";  import { ChallengeFeedback, ChallengeInfo } from "../../../../anastasis-core/lib"; +import { TextInput } from "../../components/fields/TextInput";  import { useAnastasisContext } from "../../context/anastasis"; -import { SolveEmailEntry } from "./SolveEmailEntry"; -import { SolvePostEntry } from "./SolvePostEntry"; -import { SolveQuestionEntry } from "./SolveQuestionEntry"; -import { SolveSmsEntry } from "./SolveSmsEntry"; -import { SolveUnsupportedEntry } from "./SolveUnsupportedEntry";  export function SolveScreen(): VNode {    const reducer = useAnastasisContext() - +  const [answer, setAnswer] = useState(""); +      if (!reducer) { -    return <div>no reducer in context</div> +    return <AnastasisClientFrame hideNext title="Recovery problem"> +      <div>no reducer in context</div> +    </AnastasisClientFrame>    }    if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { -    return <div>invalid state</div> +    return <AnastasisClientFrame hideNext title="Recovery problem"> +      <div>invalid state</div> +    </AnastasisClientFrame>    }    if (!reducer.currentReducerState.recovery_information) { -    return <div>no recovery information found</div> +    return <AnastasisClientFrame hideNext title="Recovery problem"> +      <div>no recovery information found</div> +    </AnastasisClientFrame>    }    if (!reducer.currentReducerState.selected_challenge_uuid) { -    return <div>no selected uuid</div> +    return <AnastasisClientFrame hideNext title="Recovery problem"> +      <div>no selected uuid</div> +    </AnastasisClientFrame>    } +    const chArr = reducer.currentReducerState.recovery_information.challenges;    const challengeFeedback = reducer.currentReducerState.challenge_feedback ?? {};    const selectedUuid = reducer.currentReducerState.selected_challenge_uuid; @@ -39,16 +47,99 @@ export function SolveScreen(): VNode {      email: SolveEmailEntry,      post: SolvePostEntry,    }; -  const SolveDialog = dialogMap[selectedChallenge?.type] ?? SolveUnsupportedEntry; +  const SolveDialog = selectedChallenge === undefined ? SolveUndefinedEntry : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry; + +  function onNext(): void { +    reducer?.transition("solve_challenge", { answer }) +  } +  function onCancel(): void { +    reducer?.back() +  } +   +    return ( -    <SolveDialog -      challenge={selectedChallenge} -      feedback={challengeFeedback[selectedUuid]} /> +    <AnastasisClientFrame +      hideNav +      title="Recovery: Solve challenge" +    > +      <SolveDialog +        id={selectedUuid} +        answer={answer} +        setAnswer={setAnswer} +        challenge={selectedChallenge} +        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> +    </AnastasisClientFrame>    );  }  export interface SolveEntryProps { +  id: string;    challenge: ChallengeInfo;    feedback?: ChallengeFeedback; +  answer: string; +  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 SolveQuestionEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { +  return ( +    <Fragment> +      <p>Type the answer to the following question:</p> +      <pre> +        {challenge.instructions} +      </pre> +      <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> +    </Fragment> +  ); +} + +function SolvePostEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { +  return ( +    <Fragment> +      <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 { +  return ( +    <Fragment> +      <p>An email has been sent to "<b>{challenge.instructions}</b>". Type the code below</p> +      <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> +    </Fragment> +  ); +} + +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. +      </p> +      <p> +        <b>Challenge type:</b> {props.challenge.type} +      </p> +    </Fragment> +  ); +} +function SolveUndefinedEntry(props: SolveEntryProps): VNode { +  return ( +    <Fragment > +      <p> +        There is no challenge information for id <b>"{props.id}"</b>. Try resetting the recovery session. +      </p> +    </Fragment> +  ); +} diff --git a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx deleted file mode 100644 index c4cf3a680..000000000 --- a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame } from "./index"; -import { LabeledInput } from "../../components/fields/LabeledInput"; -import { SolveEntryProps } from "./SolveScreen"; - -export function SolveSmsEntry({ challenge, feedback }: SolveEntryProps): VNode { -  const [answer, setAnswer] = useState(""); -  const reducer = useAnastasisContext() -  const next = (): void => { -    if (reducer) reducer.transition("solve_challenge", { -      answer, -    }) -  }; -  return ( -    <AnastasisClientFrame -      title="Recovery: Solve challenge" -      onNext={() => next()} -    > -      <p>Feedback: {JSON.stringify(feedback)}</p> -      <p>{challenge.instructions}</p> -      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} /> -    </AnastasisClientFrame> -  ); -} diff --git a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx deleted file mode 100644 index 7f538d249..000000000 --- a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { h, VNode } from "preact"; -import { AnastasisClientFrame } from "./index"; -import { SolveEntryProps } from "./SolveScreen"; - -export function SolveUnsupportedEntry(props: SolveEntryProps): VNode { -  return ( -    <AnastasisClientFrame hideNext title="Recovery: Solve challenge"> -      <p>{JSON.stringify(props.challenge)}</p> -      <p>Challenge not supported.</p> -    </AnastasisClientFrame> -  ); -} diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 8a97ad50c..cf41efb59 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -14,17 +14,17 @@   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -import { AmountJson, AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util"; +import { AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util";  import { format } from "date-fns"; -import { Fragment, JSX, VNode, h } from "preact"; +import { JSX, VNode } from "preact";  import { route } from 'preact-router';  import { useEffect, useState } from "preact/hooks"; -import * as wxApi from "../wxApi"; -import { Pages } from "../NavigationBar"; -import emptyImg from "../../static/img/empty.png" -import { Button, ButtonBox, ButtonBoxDestructive, ButtonDestructive, ButtonPrimary, ExtraLargeText, FontIcon, LargeText, ListOfProducts, PopupBox, Row, RowBorderGray, SmallLightText, WalletBox, WarningBox } from "../components/styled"; +import emptyImg from "../../static/img/empty.png";  import { ErrorMessage } from "../components/ErrorMessage";  import { Part } from "../components/Part"; +import { ButtonBox, ButtonBoxDestructive, ButtonPrimary, FontIcon, ListOfProducts, RowBorderGray, SmallLightText, WalletBox, WarningBox } from "../components/styled"; +import { Pages } from "../NavigationBar"; +import * as wxApi from "../wxApi";  export function TransactionPage({ tid }: { tid: string; }): JSX.Element {    const [transaction, setTransaction] = useState< @@ -42,7 +42,7 @@ export function TransactionPage({ tid }: { tid: string; }): JSX.Element {        }      };      fetchData(); -  }, []); +  }, [tid]);    if (!transaction) {      return <div><i18n.Translate>Loading ...</i18n.Translate></div>; | 
