working version with improved ui

This commit is contained in:
Sebastian 2021-10-27 15:13:35 -03:00
parent 21b60c8f6f
commit 32318a80f4
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
23 changed files with 521 additions and 280 deletions

View File

@ -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)

View File

@ -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>
);
}

View File

@ -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) {

View File

@ -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 &amp; 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 &amp; 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>
<span class="menu-item-label"><Translate>Secret selection</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ? '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>ChallengeSelecting</Translate></span>
</div>
</li>
<li class={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>)}

View File

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

View File

@ -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
}))

View File

@ -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}

View File

@ -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]} />

View File

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

View File

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

View File

@ -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);

View File

@ -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>
);
})}

View File

@ -36,4 +36,5 @@ export default {
};
export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);

View File

@ -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);

View File

@ -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]}
/>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>;