diff options
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>; |