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; grabFocus?: boolean;
tooltip?: string; tooltip?: string;
error?: string; error?: string;
years?: Array<number>;
bind: [string, (x: string) => void]; bind: [string, (x: string) => void];
} }
@ -19,7 +20,7 @@ export function DateInput(props: DateInputProps): VNode {
} }
}, [props.grabFocus]); }, [props.grabFocus]);
const [opened, setOpened2] = useState(false) const [opened, setOpened2] = useState(false)
function setOpened(v: boolean) { function setOpened(v: boolean): void {
console.log('dale', v) console.log('dale', v)
setOpened2(v) setOpened2(v)
} }
@ -50,6 +51,7 @@ export function DateInput(props: DateInputProps): VNode {
{showError && <p class="help is-danger">{props.error}</p>} {showError && <p class="help is-danger">{props.error}</p>}
<DatePicker <DatePicker
opened={opened} opened={opened}
years={props.years}
closeFunction={() => setOpened(false)} closeFunction={() => setOpened(false)}
dateReceiver={(d) => { dateReceiver={(d) => {
setDirty(true) 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 { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { useLayoutEffect, useRef, useState } from "preact/hooks";
export interface LabeledInputProps { export interface TextInputProps {
label: string; label: string;
grabFocus?: boolean; grabFocus?: boolean;
error?: string; error?: string;
@ -9,7 +9,7 @@ export interface LabeledInputProps {
bind: [string, (x: string) => void]; bind: [string, (x: string) => void];
} }
export function LabeledInput(props: LabeledInputProps): VNode { export function TextInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
if (props.grabFocus) { if (props.grabFocus) {

View File

@ -64,8 +64,7 @@ export function Sidebar({ mobile }: Props): VNode {
</li> </li>
} }
{reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment> {reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment>
<li class={ <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}> reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span> <span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span>
@ -79,73 +78,65 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
<div class="ml-4"> <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> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>PoliciesReviewing</Translate></span> <span class="menu-item-label"><Translate>Policies reviewing</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>SecretEditing</Translate></span> <span class="menu-item-label"><Translate>Secret input</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>PoliciesPaying</Translate></span> <span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>BackupFinished</Translate></span> <span class="menu-item-label"><Translate>Backup completed</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>TruthsPaying</Translate></span> <span class="menu-item-label"><Translate>Truth Paying</Translate></span>
</div> </div>
</li> </li>
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment> </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"> <div class="ml-4">
<span class="menu-item-label"><Translate>ContinentSelecting</Translate></span> <span class="menu-item-label"><Translate>Location &amp; Currency</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>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>UserAttributesCollecting</Translate></span> <span class="menu-item-label"><Translate>Personal information</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>SecretSelecting</Translate></span> <span class="menu-item-label"><Translate>Secret selection</Translate></span>
</div> </div>
</li> </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"> <div class="ml-4">
<span class="menu-item-label"><Translate>ChallengeSelecting</Translate></span> <span class="menu-item-label"><Translate>Solve Challenges</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>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>RecoveryFinished</Translate></span> <span class="menu-item-label"><Translate>Secret recovered</Translate></span>
</div> </div>
</li> </li>
</Fragment>)} </Fragment>)}

View File

@ -24,6 +24,7 @@ import { h, Component } from "preact";
interface Props { interface Props {
closeFunction?: () => void; closeFunction?: () => void;
dateReceiver?: (d: Date) => void; dateReceiver?: (d: Date) => void;
years?: Array<number>;
opened?: boolean; opened?: boolean;
} }
interface State { interface State {
@ -207,9 +208,9 @@ export class DatePicker extends Component<Props, State> {
} }
componentDidUpdate() { componentDidUpdate() {
if (this.state.selectYearMode) { // if (this.state.selectYearMode) {
document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it // document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
} // }
} }
constructor() { constructor() {
@ -296,8 +297,7 @@ export class DatePicker extends Component<Props, State> {
</div>} </div>}
{selectYearMode && <div class="datePicker--selectYear"> {selectYearMode && <div class="datePicker--selectYear">
{(this.props.years || yearArr).map(year => (
{yearArr.map(year => (
<span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}> <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
{year} {year}
</span> </span>

View File

@ -40,21 +40,21 @@ export default {
export const Backup = createExample(TestedComponent, { export const Backup = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing, ...reducerStatesExample.backupAttributeEditing,
required_attributes: [{ required_attributes: [{
name: 'first', name: 'first name',
label: 'first', label: 'first',
type: 'type', type: 'string',
uuid: 'asdasdsa1', uuid: 'asdasdsa1',
widget: 'wid', widget: 'wid',
}, { }, {
name: 'pepe', name: 'last name',
label: 'second', label: 'second',
type: 'type', type: 'string',
uuid: 'asdasdsa2', uuid: 'asdasdsa2',
widget: 'wid', widget: 'wid',
}, { }, {
name: 'pepe2', name: 'date',
label: 'third', label: 'third',
type: 'type', type: 'date',
uuid: 'asdasdsa3', uuid: 'asdasdsa3',
widget: 'calendar', widget: 'calendar',
}] }]
@ -65,19 +65,19 @@ export const Recovery = createExample(TestedComponent, {
required_attributes: [{ required_attributes: [{
name: 'first', name: 'first',
label: 'first', label: 'first',
type: 'type', type: 'string',
uuid: 'asdasdsa1', uuid: 'asdasdsa1',
widget: 'wid', widget: 'wid',
}, { }, {
name: 'pepe', name: 'pepe',
label: 'second', label: 'second',
type: 'type', type: 'string',
uuid: 'asdasdsa2', uuid: 'asdasdsa2',
widget: 'wid', widget: 'wid',
}, { }, {
name: 'pepe2', name: 'pepe2',
label: 'third', label: 'third',
type: 'type', type: 'date',
uuid: 'asdasdsa3', uuid: 'asdasdsa3',
widget: 'calendar', widget: 'calendar',
}] }]
@ -110,12 +110,20 @@ const allWidgets = [
"anastasis_gtk_xx_square", "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, { export const WithAllPosibleWidget = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing, ...reducerStatesExample.backupAttributeEditing,
required_attributes: allWidgets.map(w => ({ required_attributes: allWidgets.map(w => ({
name: w, name: w,
label: `widget: ${w}`, label: `widget: ${w}`,
type: 'type', type: typeForWidget(w),
uuid: `uuid-${w}`, uuid: `uuid-${w}`,
widget: w widget: w
})) }))

View File

@ -4,8 +4,9 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index"; import { AnastasisClientFrame, withProcessLabel } from "./index";
import { LabeledInput } from "../../components/fields/LabeledInput"; import { TextInput } from "../../components/fields/TextInput";
import { DateInput } from "../../components/fields/DateInput"; import { DateInput } from "../../components/fields/DateInput";
import { NumberInput } from "../../components/fields/NumberInput";
export function AttributeEntryScreen(): VNode { export function AttributeEntryScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext()
@ -65,6 +66,7 @@ export function AttributeEntryScreen(): VNode {
</div> </div>
<div class="column is-half" > <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> <h1><b>This stay private</b></h1>
<p>The information you have entered here: <p>The information you have entered here:
</p> </p>
@ -92,20 +94,33 @@ interface AttributeEntryFieldProps {
spec: UserAttributeSpec; spec: UserAttributeSpec;
isValid: () => string | undefined; isValid: () => string | undefined;
} }
const possibleBirthdayYear: Array<number> = []
for (let i = 0; i < 100; i++ ) {
possibleBirthdayYear.push(2020 - i)
}
function AttributeEntryField(props: AttributeEntryFieldProps): VNode { function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
const errorMessage = props.isValid() const errorMessage = props.isValid()
return ( return (
<div> <div>
{props.spec.type === 'date' ? {props.spec.type === 'date' &&
<DateInput <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} grabFocus={props.isFirst}
label={props.spec.label} label={props.spec.label}
error={errorMessage} error={errorMessage}
bind={[props.value, props.setValue]} bind={[props.value, props.setValue]}
/> : />
<LabeledInput }
{props.spec.type === 'string' &&
<TextInput
grabFocus={props.isFirst} grabFocus={props.isFirst}
label={props.spec.label} label={props.spec.label}
error={errorMessage} error={errorMessage}

View File

@ -7,7 +7,7 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
import { LabeledInput } from "../../components/fields/LabeledInput"; import { TextInput } from "../../components/fields/TextInput";
export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode { export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
@ -19,7 +19,7 @@ export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
email. email.
</p> </p>
<div> <div>
<LabeledInput <TextInput
label="Email address" label="Email address"
grabFocus grabFocus
bind={[email, setEmail]} /> bind={[email, setEmail]} />

View File

@ -6,7 +6,7 @@ import {
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
import { LabeledInput } from "../../components/fields/LabeledInput"; import { TextInput } from "../../components/fields/TextInput";
export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode { export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {
const [fullName, setFullName] = useState(""); 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. code that you will receive in a letter to that address.
</p> </p>
<div> <div>
<LabeledInput <TextInput
grabFocus grabFocus
label="Full Name" label="Full Name"
bind={[fullName, setFullName]} /> bind={[fullName, setFullName]} />
</div> </div>
<div> <div>
<LabeledInput label="Street" bind={[street, setStreet]} /> <TextInput label="Street" bind={[street, setStreet]} />
</div> </div>
<div> <div>
<LabeledInput label="City" bind={[city, setCity]} /> <TextInput label="City" bind={[city, setCity]} />
</div> </div>
<div> <div>
<LabeledInput label="Postal Code" bind={[postcode, setPostcode]} /> <TextInput label="Postal Code" bind={[postcode, setPostcode]} />
</div> </div>
<div> <div>
<LabeledInput label="Country" bind={[country, setCountry]} /> <TextInput label="Country" bind={[country, setCountry]} />
</div> </div>
<div> <div>
<button onClick={() => props.cancel()}>Cancel</button> <button onClick={() => props.cancel()}>Cancel</button>

View File

@ -7,7 +7,7 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
import { LabeledInput } from "../../components/fields/LabeledInput"; import { TextInput } from "../../components/fields/TextInput";
export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode { export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
const [questionText, setQuestionText] = useState(""); const [questionText, setQuestionText] = useState("");
@ -29,13 +29,13 @@ export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
here. here.
</p> </p>
<div> <div>
<LabeledInput <TextInput
label="Security question" label="Security question"
grabFocus grabFocus
bind={[questionText, setQuestionText]} /> bind={[questionText, setQuestionText]} />
</div> </div>
<div> <div>
<LabeledInput label="Answer" bind={[answerText, setAnswerText]} /> <TextInput label="Answer" bind={[answerText, setAnswerText]} />
</div> </div>
<div> <div>
<button onClick={() => props.cancel()}>Cancel</button> <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, ...reducerStatesExample.challengeSelecting,
recovery_information: { recovery_information: {
policies: [[{ uuid: '1' }]], policies: [[{ uuid: '1' }]],
@ -50,7 +50,7 @@ export const OneChallenge = createExample(TestedComponent, {
}, },
} as ReducerState); } as ReducerState);
export const MoreChallenges = createExample(TestedComponent, { export const SomePoliciesOneSolved = createExample(TestedComponent, {
...reducerStatesExample.challengeSelecting, ...reducerStatesExample.challengeSelecting,
recovery_information: { recovery_information: {
policies: [[{ uuid: '1' }, { uuid: '2' }], [{ uuid: 'uuid-3' }]], policies: [[{ uuid: '1' }, { uuid: '2' }], [{ uuid: 'uuid-3' }]],
@ -75,13 +75,13 @@ export const MoreChallenges = createExample(TestedComponent, {
'uuid-3': { 'uuid-3': {
state: 'solved' state: 'solved'
} }
} },
} as ReducerState); } as ReducerState);
export const OneBadConfiguredPolicy = createExample(TestedComponent, { export const OneBadConfiguredPolicy = createExample(TestedComponent, {
...reducerStatesExample.challengeSelecting, ...reducerStatesExample.challengeSelecting,
recovery_information: { recovery_information: {
policies: [[{ uuid: '2' }]], policies: [[{ uuid: '1' }, { uuid: '2' }]],
challenges: [{ challenges: [{
cost: 'USD:1', cost: 'USD:1',
instructions: 'just go for it', instructions: 'just go for it',
@ -91,4 +91,130 @@ export const OneBadConfiguredPolicy = createExample(TestedComponent, {
}, },
} as ReducerState); } 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); export const NoPolicies = createExample(TestedComponent, reducerStatesExample.challengeSelecting);

View File

@ -1,3 +1,4 @@
import { ChallengeFeedback } from "anastasis-core";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
@ -13,65 +14,94 @@ export function ChallengeOverviewScreen(): VNode {
} }
const policies = reducer.currentReducerState.recovery_information?.policies ?? []; const policies = reducer.currentReducerState.recovery_information?.policies ?? [];
const chArr = reducer.currentReducerState.recovery_information?.challenges ?? []; const knownChallengesArray = reducer.currentReducerState.recovery_information?.challenges ?? [];
const challengeFeedback = reducer.currentReducerState?.challenge_feedback; const challengeFeedback = reducer.currentReducerState?.challenge_feedback ?? {};
const challenges: { const knownChallengesMap: {
[uuid: string]: { [uuid: string]: {
type: string; type: string;
instructions: string; instructions: string;
cost: string; cost: string;
feedback: ChallengeFeedback | undefined;
}; };
} = {}; } = {};
for (const ch of chArr) { for (const ch of knownChallengesArray) {
challenges[ch.uuid] = { knownChallengesMap[ch.uuid] = {
type: ch.type, type: ch.type,
cost: ch.cost, cost: ch.cost,
instructions: ch.instructions, 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 ( return (
<AnastasisClientFrame title="Recovery: Solve challenges"> <AnastasisClientFrame hideNext={!atLeastThereIsOnePolicySolved} title="Recovery: Solve challenges">
<h2>Policies</h2> {!policies.length ? <p>
{!policies.length && <p> No policies found, try with another version of the secret
No policies found </p> : (policies.length === 1 ? <p>
</p>} One policy found for this secret. You need to solve all the challenges in order to recover your secret.
{policies.map((row, i) => { </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 ( return (
<div key={i}> <div key={i}>
<h3>Policy #{i + 1}</h3> <b>Policy #{i + 1}</b>
{row.map(column => { {row.challenges.length === 0 && <p>
const ch = challenges[column.uuid]; This policy doesn't have challenges
if (!ch) return <div> </p>}
There is no challenge for this policy {row.challenges.length === 1 && <p>
</div> This policy just have one challenge to be solved
const feedback = challengeFeedback?.[column.uuid]; </p>}
return ( {row.challenges.length > 1 && <p>
<div key={column.uuid} This policy have {row.challenges.length} challenges
style={{ </p>}
borderLeft: "2px solid gray", <table class="table">
paddingLeft: "0.5em", <thead>
borderRadius: "0.5em", <tr>
marginTop: "0.5em", <td>Challenge type</td>
marginBottom: "0.5em", <td>Description</td>
}} <td>Status</td>
> <td>Cost</td>
<h4> </tr>
{ch.type} ({ch.instructions}) </thead>
</h4> <tbody>
<p>Status: {feedback?.state ?? "unknown"}</p> {tableBody}
{feedback?.state !== "solved" ? ( </tbody>
<button </table>
onClick={() => reducer.transition("select_challenge", {
uuid: column.uuid,
})}
>
Solve
</button>
) : null}
</div>
);
})}
</div> </div>
); );
})} })}

View File

@ -36,4 +36,5 @@ export default {
}; };
export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); 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, ...reducerStatesExample.recoveryFinished,
core_secret: { mime: 'text/plain', value: 'hello' } core_secret: { mime: 'text/plain', value: 'hello' }
} as ReducerState); } as ReducerState);

View File

@ -5,7 +5,7 @@ import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { import {
AnastasisClientFrame} from "./index"; AnastasisClientFrame} from "./index";
import { LabeledInput } from "../../components/fields/LabeledInput"; import { TextInput } from "../../components/fields/TextInput";
export function SecretEditorScreen(): VNode { export function SecretEditorScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext()
@ -47,14 +47,14 @@ export function SecretEditorScreen(): VNode {
onNext={() => secretNext()} onNext={() => secretNext()}
> >
<div> <div>
<LabeledInput <TextInput
label="Secret Name:" label="Secret Name:"
grabFocus grabFocus
bind={[secretName, setSecretName]} bind={[secretName, setSecretName]}
/> />
</div> </div>
<div> <div>
<LabeledInput <TextInput
label="Secret Value:" label="Secret Value:"
bind={[secretValue, setSecretValue]} bind={[secretValue, setSecretValue]}
/> />

View File

@ -29,15 +29,31 @@ export function SecretSelectionScreen(): VNode {
version: n, version: n,
provider_url: p, provider_url: p,
}); });
setSelectingVersion(false);
}); });
setSelectingVersion(false);
} }
const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {})
const recoveryDocument = reducer.currentReducerState.recovery_document const recoveryDocument = reducer.currentReducerState.recovery_document
if (!recoveryDocument) { if (!recoveryDocument) {
return ( return (
<AnastasisClientFrame hideNav title="Recovery: Problem"> <AnastasisClientFrame hideNext title="Recovery: Problem">
<p>No recovery document found</p> <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> </AnastasisClientFrame>
) )
} }
@ -45,43 +61,75 @@ export function SecretSelectionScreen(): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery: Select secret"> <AnastasisClientFrame hideNav title="Recovery: Select secret">
<p>Select a different version of the secret</p> <p>Select a different version of the secret</p>
<table class="table">
<tr>
<td><b>Provider</b></td>
<td>
<select onChange={(e) => setOtherProvider((e.target as any).value)}> <select onChange={(e) => setOtherProvider((e.target as any).value)}>
{Object.keys(reducer.currentReducerState.authentication_providers ?? {}).map( {providerList.map(prov => (
(x, i) => ( <option key={prov} selected={prov === recoveryDocument.provider_url} value={prov}>
<option key={i} selected={x === recoveryDocument.provider_url} value={x}> {prov}
{x}
</option> </option>
) ))}
)}
</select> </select>
<div> </td>
</tr>
<tr>
<td><b>Version</b></td>
<td>
<input <input
value={otherVersion} value={otherVersion}
onChange={(e) => setOtherVersion(Number((e.target as HTMLInputElement).value))} onChange={(e) => setOtherVersion(Number((e.target as HTMLInputElement).value))}
type="number" /> type="number" />
<button onClick={() => selectVersion(otherProvider, otherVersion)}> </td>
Use this version <td>
</button> <a onClick={() => setOtherVersion(0)}>set to latest version</a>
</div> </td>
<div> </tr>
<button onClick={() => selectVersion(otherProvider, 0)}> </table>
Use latest version <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
</button> <button class="button" onClick={() => setSelectingVersion(false)}>Cancel</button>
</div> <button class="button is-info" onClick={() => selectVersion(otherProvider, otherVersion)}>Confirm</button>
<div>
<button onClick={() => setSelectingVersion(false)}>Cancel</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
return ( return (
<AnastasisClientFrame title="Recovery: Select secret"> <AnastasisClientFrame title="Recovery: Select secret">
<p>Provider: {recoveryDocument.provider_url}</p> <p>Secret found, you can select another version or continue to the challenges solving</p>
<p>Secret version: {recoveryDocument.version}</p> <table class="table">
<p>Secret name: {recoveryDocument.secret_name}</p> <tr>
<button onClick={() => setSelectingVersion(true)}> <td>
Select different secret <b>Provider</b>
</button> <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> </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 { ChallengeFeedback, ChallengeInfo } from "../../../../anastasis-core/lib";
import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis"; 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 { export function SolveScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext()
const [answer, setAnswer] = useState("");
if (!reducer) { 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) { 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) { 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) { 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 chArr = reducer.currentReducerState.recovery_information.challenges;
const challengeFeedback = reducer.currentReducerState.challenge_feedback ?? {}; const challengeFeedback = reducer.currentReducerState.challenge_feedback ?? {};
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid; const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
@ -39,16 +47,99 @@ export function SolveScreen(): VNode {
email: SolveEmailEntry, email: SolveEmailEntry,
post: SolvePostEntry, 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 ( return (
<AnastasisClientFrame
hideNav
title="Recovery: Solve challenge"
>
<SolveDialog <SolveDialog
id={selectedUuid}
answer={answer}
setAnswer={setAnswer}
challenge={selectedChallenge} challenge={selectedChallenge}
feedback={challengeFeedback[selectedUuid]} /> feedback={challengeFeedback[selectedUuid]} />
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={onCancel}>Cancel</button>
<button class="button is-info" onClick={onNext} >Confirm</button>
</div>
</AnastasisClientFrame>
); );
} }
export interface SolveEntryProps { export interface SolveEntryProps {
id: string;
challenge: ChallengeInfo; challenge: ChallengeInfo;
feedback?: ChallengeFeedback; 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/> 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 { format } from "date-fns";
import { Fragment, JSX, VNode, h } from "preact"; import { JSX, VNode } from "preact";
import { route } from 'preact-router'; import { route } from 'preact-router';
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi"; import emptyImg from "../../static/img/empty.png";
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 { ErrorMessage } from "../components/ErrorMessage"; import { ErrorMessage } from "../components/ErrorMessage";
import { Part } from "../components/Part"; 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 { export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
const [transaction, setTransaction] = useState< const [transaction, setTransaction] = useState<
@ -42,7 +42,7 @@ export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
} }
}; };
fetchData(); fetchData();
}, []); }, [tid]);
if (!transaction) { if (!transaction) {
return <div><i18n.Translate>Loading ...</i18n.Translate></div>; return <div><i18n.Translate>Loading ...</i18n.Translate></div>;