diff options
Diffstat (limited to 'packages/anastasis-webui/src')
23 files changed, 655 insertions, 219 deletions
diff --git a/packages/anastasis-webui/src/components/AsyncButton.tsx b/packages/anastasis-webui/src/components/AsyncButton.tsx new file mode 100644 index 000000000..5602715e4 --- /dev/null +++ b/packages/anastasis-webui/src/components/AsyncButton.tsx @@ -0,0 +1,51 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ComponentChildren, h, VNode } from "preact"; +// import { LoadingModal } from "../modal"; +import { useAsync } from "../hooks/async"; +// import { Translate } from "../../i18n"; + +type Props = { + children: ComponentChildren; + disabled: boolean; + onClick?: () => Promise<void>; + [rest: string]: any; +}; + +export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode { + const { isLoading, request } = useAsync(onClick); + + // if (isSlow) { + // return <LoadingModal onCancel={cancel} />; + // } + console.log(isLoading) + if (isLoading) { + + return <button class="button">Loading...</button>; + } + + return <span {...rest}> + <button class="button is-info" onClick={request} disabled={disabled}> + {children} + </button> + </span>; +} diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx index 69a05fcf3..c406b85d1 100644 --- a/packages/anastasis-webui/src/components/fields/DateInput.tsx +++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx @@ -1,4 +1,4 @@ -import { format } from "date-fns"; +import { format, isAfter, parse, sub, subYears } from "date-fns"; import { h, VNode } from "preact"; import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { DatePicker } from "../picker/DatePicker"; @@ -19,16 +19,14 @@ export function DateInput(props: DateInputProps): VNode { inputRef.current?.focus(); } }, [props.grabFocus]); - const [opened, setOpened2] = useState(false) - function setOpened(v: boolean): void { - console.log('dale', v) - setOpened2(v) - } + const [opened, setOpened] = useState(false) const value = props.bind[0] || ""; const [dirty, setDirty] = useState(false) const showError = dirty && props.error + const calendar = subYears(new Date(), 30) + return <div class="field"> <label class="label"> {props.label} @@ -36,27 +34,37 @@ export function DateInput(props: DateInputProps): VNode { <i class="mdi mdi-information" /> </span>} </label> - <div class="control has-icons-right"> - <input - type="text" - class={showError ? 'input is-danger' : 'input'} - readonly - onFocus={() => { setOpened(true) } } - value={value} - ref={inputRef} /> - - <span class="control icon is-right"> - <span class="icon"><i class="mdi mdi-calendar" /></span> - </span> + <div class="control"> + <div class="field has-addons"> + <p class="control"> + <input + type="text" + class={showError ? 'input is-danger' : 'input'} + value={value} + onChange={(e) => { + const text = e.currentTarget.value + setDirty(true) + props.bind[1](text); + }} + ref={inputRef} /> + </p> + <p class="control"> + <a class="button" onClick={() => { setOpened(true) }}> + <span class="icon"><i class="mdi mdi-calendar" /></span> + </a> + </p> + </div> </div> + <p class="help">Using the format yyyy-mm-dd</p> {showError && <p class="help is-danger">{props.error}</p>} <DatePicker opened={opened} + initialDate={calendar} years={props.years} closeFunction={() => setOpened(false)} dateReceiver={(d) => { setDirty(true) - const v = format(d, 'yyyy/MM/dd') + const v = format(d, 'yyyy-MM-dd') props.bind[1](v); }} /> diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx index e1bb4c7c0..935951ab9 100644 --- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx +++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx @@ -49,7 +49,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode { </a> <div class="navbar-end"> <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> - <LangSelector /> + {/* <LangSelector /> */} </div> </div> </div> diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx index 35720e0f1..72655662f 100644 --- a/packages/anastasis-webui/src/components/menu/SideBar.tsx +++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx @@ -39,9 +39,9 @@ export function Sidebar({ mobile }: Props): VNode { return ( <aside class="aside is-placed-left is-expanded"> - {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}> + {/* {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}> <LangSelector /> - </div>} + </div>} */} <div class="aside-tools"> <div class="aside-tools-label"> <div><b>Anastasis</b> Reducer</div> @@ -68,7 +68,7 @@ export function Sidebar({ mobile }: Props): VNode { <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> + <span class="menu-item-label"><Translate>Location</Translate></span> </div> </li> <li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}> @@ -85,7 +85,7 @@ export function Sidebar({ mobile }: Props): VNode { <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}> <div class="ml-4"> - <span class="menu-item-label"><Translate>Policies reviewing</Translate></span> + <span class="menu-item-label"><Translate>Policies</Translate></span> </div> </li> <li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}> @@ -94,12 +94,12 @@ export function Sidebar({ mobile }: Props): VNode { <span class="menu-item-label"><Translate>Secret input</Translate></span> </div> </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"> <span class="menu-item-label"><Translate>Payment (optional)</Translate></span> </div> - </li> + </li> */} <li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}> <div class="ml-4"> @@ -116,7 +116,7 @@ export function Sidebar({ mobile }: Props): VNode { <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>Location & Currency</Translate></span> + <span class="menu-item-label"><Translate>Location</Translate></span> </div> </li> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}> diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx index 5b33fa8be..a94b3708e 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; + initialDate?: Date; years?: Array<number>; opened?: boolean; } @@ -213,8 +214,8 @@ export class DatePicker extends Component<Props, State> { // } } - constructor() { - super(); + constructor(props) { + super(props); this.closeDatePicker = this.closeDatePicker.bind(this); this.dayClicked = this.dayClicked.bind(this); @@ -226,11 +227,12 @@ export class DatePicker extends Component<Props, State> { this.toggleYearSelector = this.toggleYearSelector.bind(this); this.displaySelectedMonth = this.displaySelectedMonth.bind(this); + const initial = props.initialDate || now; this.state = { - currentDate: now, - displayedMonth: now.getMonth(), - displayedYear: now.getFullYear(), + currentDate: initial, + displayedMonth: initial.getMonth(), + displayedYear: initial.getFullYear(), selectYearMode: false } } diff --git a/packages/anastasis-webui/src/hooks/async.ts b/packages/anastasis-webui/src/hooks/async.ts new file mode 100644 index 000000000..f142a5dc5 --- /dev/null +++ b/packages/anastasis-webui/src/hooks/async.ts @@ -0,0 +1,77 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { useState } from "preact/hooks"; +// import { cancelPendingRequest } from "./backend"; + +export interface Options { + slowTolerance: number; +} + +export interface AsyncOperationApi<T> { + request: (...a: any) => void; + cancel: () => void; + data: T | undefined; + isSlow: boolean; + isLoading: boolean; + error: string | undefined; +} + +export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> { + const [data, setData] = useState<T | undefined>(undefined); + const [isLoading, setLoading] = useState<boolean>(false); + const [error, setError] = useState<any>(undefined); + const [isSlow, setSlow] = useState(false) + + const request = async (...args: any) => { + if (!fn) return; + setLoading(true); + console.log("loading true") + const handler = setTimeout(() => { + setSlow(true) + }, tooLong) + + try { + const result = await fn(...args); + console.log(result) + setData(result); + } catch (error) { + setError(error); + } + setLoading(false); + setSlow(false) + clearTimeout(handler) + }; + + function cancel() { + // cancelPendingRequest() + setLoading(false); + setSlow(false) + } + + return { + request, + cancel, + data, + isSlow, + isLoading, + error + }; +} diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx index 32d7817e3..549686616 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx @@ -52,8 +52,8 @@ export const Backup = createExample(TestedComponent, { uuid: 'asdasdsa2', widget: 'wid', }, { - name: 'date', - label: 'third', + name: 'birthdate', + label: 'birthdate', type: 'date', uuid: 'asdasdsa3', widget: 'calendar', diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx index 2c7f54c5b..52046b216 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx @@ -7,6 +7,7 @@ import { AnastasisClientFrame, withProcessLabel } from "./index"; import { TextInput } from "../../components/fields/TextInput"; import { DateInput } from "../../components/fields/DateInput"; import { NumberInput } from "../../components/fields/NumberInput"; +import { isAfter, parse } from "date-fns"; export function AttributeEntryScreen(): VNode { const reducer = useAnastasisContext() @@ -46,15 +47,14 @@ export function AttributeEntryScreen(): VNode { identity_attributes: attrs, })} > - <div class="columns"> - <div class="column is-half"> + <div class="columns" style={{ maxWidth: 'unset' }}> + <div class="column is-one-third"> {fieldList} </div> - <div class="column is-half" > + <div class="column is-two-third" > <p>This personal information will help to locate your secret.</p> - <h1><b>This stay private</b></h1> - <p>The information you have entered here: - </p> + <h1 class="title">This stays private</h1> + <p>The information you have entered here:</p> <ul> <li> <span class="icon is-right"> @@ -111,15 +111,17 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode { bind={[props.value, props.setValue]} /> } - <span> + <div class="block"> + This stays private <span class="icon is-right"> <i class="mdi mdi-eye-off" /> </span> - This stay private - </span> + </div> </div> ); } +const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/ + function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined { const pattern = spec['validation-regex'] @@ -136,5 +138,22 @@ function checkIfValid(value: string, spec: UserAttributeSpec): string | undefine if (!optional && !value) { return 'This value is required' } + if ("date" === spec.type) { + if (!YEAR_REGEX.test(value)) { + return "The date doesn't follow the format" + } + + try { + const v = parse(value, 'yyyy-MM-dd', new Date()); + if (Number.isNaN(v.getTime())) { + return "Some numeric values seems out of range for a date" + } + if ("birthdate" === spec.name && isAfter(v, new Date())) { + return "A birthdate cannot be in the future" + } + } catch (e) { + return "Could not parse the date" + } + } return undefined } diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx index 4e7819a77..ab482044f 100644 --- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx @@ -142,6 +142,10 @@ export function AuthenticationEditorScreen(): VNode { </div> <div class="column is-half"> When recovering your wallet, you will be asked to verify your identity via the methods you configure here. + + <b>Explain the exclamation marks</b> + + <a>Explain how to add providers</a> </div> </div> </AnastasisClientFrame> diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx index 70ac8157d..7938baca4 100644 --- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx @@ -23,7 +23,7 @@ export function BackupFinishedScreen(): VNode { </p>} {details && <div class="block"> - <p>The backup is stored by the following providers:</p> + <p>The backup is stored by the following providers:</p> {Object.keys(details).map((x, i) => { const sd = details[x]; return ( @@ -31,11 +31,14 @@ export function BackupFinishedScreen(): VNode { {x} <p> version {sd.policy_version} - {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd/MM/yyyy')}` : ' without expiration date'} + {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'} </p> </div> ); })} </div>} + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={() => reducer.back()}>Back</button> + </div> </AnastasisClientFrame>); } diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx index 2186eb42d..6bdb3515d 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/camelcase */ /* This file is part of GNU Taler (C) 2021 Taler Systems S.A. @@ -19,12 +20,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { ReducerState } from 'anastasis-core'; import { createExample, reducerStatesExample } from '../../utils'; import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen'; export default { - title: 'Pages/ContinentSelectionScreen', + title: 'Pages/Location', component: TestedComponent, args: { order: 2, @@ -35,6 +37,16 @@ export default { }, }; -export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); +export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); -export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); +export const BackupSelectCountry = createExample(TestedComponent, { + ...reducerStatesExample.backupSelectContinent, + selected_continent: 'Testcontinent', +} as ReducerState); + +export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); + +export const RecoverySelectCountry = createExample(TestedComponent, { + ...reducerStatesExample.recoverySelectContinent, + selected_continent: 'Testcontinent', +} as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx index 713655625..4ab0e6a9b 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx @@ -36,20 +36,21 @@ export function ContinentSelectionScreen(): VNode { }) } - const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || - reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting; + // const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || + // reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting; const errors = !theCountry ? "Select a country" : undefined return ( - <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Select location")} onNext={selectCountryAction}> - <div class="columns"> - <div class="column is-half"> + <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}> + + <div class="columns" > + <div class="column is-one-third"> <div class="field"> <label class="label">Continent</label> - <div class="control has-icons-left"> - <div class="select " > - <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} disabled={!step1}> + <div class="control is-expanded has-icons-left"> + <div class="select is-fullwidth" > + <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} > <option key="none" disabled selected value=""> Choose a continent </option> {continentList.map(prov => ( <option key={prov.name} value={prov.name}> @@ -61,18 +62,13 @@ export function ContinentSelectionScreen(): VNode { <i class="mdi mdi-earth" /> </div> </div> - {!step1 && <span class="control"> - <a class="button is-danger" onClick={() => reducer.back()}> - X - </a> - </span>} </div> </div> <div class="field"> <label class="label">Country</label> - <div class="control has-icons-left"> - <div class="select" > + <div class="control is-expanded has-icons-left"> + <div class="select is-fullwidth" > <select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}> <option key="none" disabled selected value=""> Choose a country </option> {countryList.map(prov => ( @@ -88,17 +84,17 @@ export function ContinentSelectionScreen(): VNode { </div> </div> - {theCountry && <div class="field"> + {/* {theCountry && <div class="field"> <label class="label">Available currencies:</label> <div class="control"> <input class="input is-small" type="text" readonly value={theCountry.currency} /> </div> - </div>} + </div>} */} </div> - <div class="column is-half"> + <div class="column is-two-third"> <p> - A location will help to define a common information that will be use to locate your secret and a currency - for payments if needed. + Your location will help us to determine which personal information + ask you for the next step. </p> </div> </div> diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx deleted file mode 100644 index 3a642748a..000000000 --- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { createExample, reducerStatesExample } from '../../utils'; -import { CountrySelectionScreen as TestedComponent } from './CountrySelectionScreen'; - - -export default { - title: 'Pages/CountrySelectionScreen', - component: TestedComponent, - args: { - order: 3, - }, - argTypes: { - onUpdate: { action: 'onUpdate' }, - onBack: { action: 'onBack' }, - }, -}; - -export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry); -export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry); diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx deleted file mode 100644 index b64e1a096..000000000 --- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { h, VNode } from "preact"; -import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame, withProcessLabel } from "./index"; - -export function CountrySelectionScreen(): VNode { - const reducer = useAnastasisContext() - if (!reducer) { - return <div>no reducer in context</div> - } - if (!reducer.currentReducerState || !("countries" in reducer.currentReducerState)) { - return <div>invalid state</div> - } - const sel = (x: any): void => reducer.transition("select_country", { - country_code: x.code, - currencies: [x.currency], - }); - return ( - <AnastasisClientFrame hideNext={"FIXME"} title={withProcessLabel(reducer, "Select Country")} > - <div style={{ display: 'flex', flexDirection: 'column' }}> - {reducer.currentReducerState.countries!.map((x: any) => ( - <div key={x.name}> - <button class="button" onClick={() => sel(x)} > - {x.name} ({x.currency}) - </button> - </div> - ))} - </div> - </AnastasisClientFrame> - ); -} diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx new file mode 100644 index 000000000..fc339e48e --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/camelcase */ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ReducerState } from 'anastasis-core'; +import { createExample, reducerStatesExample } from '../../utils'; +import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen'; + + +export default { + title: 'Pages/backup/ReviewPoliciesScreen/EditPoliciesScreen', + args: { + order: 6, + }, + component: TestedComponent, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +export const EditingAPolicy = createExample(TestedComponent, { + ...reducerStatesExample.policyReview, + policies: [{ + methods: [{ + authentication_method: 1, + provider: 'https://anastasis.demo.taler.net/' + }, { + authentication_method: 2, + provider: 'http://localhost:8086/' + }] + }, { + methods: [{ + authentication_method: 1, + provider: 'http://localhost:8086/' + }] + }], + authentication_methods: [{ + type: "email", + instructions: "Email to qwe@asd.com", + challenge: "E5VPA" + }, { + type: "totp", + instructions: "Response code for 'Anastasis'", + challenge: "E5VPA" + }, { + type: "sms", + instructions: "SMS to 6666-6666", + challenge: "" + }, { + type: "question", + instructions: "How did the chicken cross the road?", + challenge: "C5SP8" + }] +} as ReducerState, { index : 0}); + +export const CreatingAPolicy = createExample(TestedComponent, { + ...reducerStatesExample.policyReview, + policies: [{ + methods: [{ + authentication_method: 1, + provider: 'https://anastasis.demo.taler.net/' + }, { + authentication_method: 2, + provider: 'http://localhost:8086/' + }] + }, { + methods: [{ + authentication_method: 1, + provider: 'http://localhost:8086/' + }] + }], + authentication_methods: [{ + type: "email", + instructions: "Email to qwe@asd.com", + challenge: "E5VPA" + }, { + type: "totp", + instructions: "Response code for 'Anastasis'", + challenge: "E5VPA" + }, { + type: "sms", + instructions: "SMS to 6666-6666", + challenge: "" + }, { + type: "question", + instructions: "How did the chicken cross the road?", + challenge: "C5SP8" + }] +} as ReducerState, { index : 3}); + diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx new file mode 100644 index 000000000..85cc96c46 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { AuthMethod, Policy } from "anastasis-core"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useAnastasisContext } from "../../context/anastasis"; +import { authMethods, KnownAuthMethods } from "./authMethod"; +import { AnastasisClientFrame } from "./index"; + +export interface ProviderInfo { + url: string; + cost: string; + isFree: boolean; +} + +export type ProviderInfoByType = { + [type in KnownAuthMethods]?: ProviderInfo[]; +}; + +interface Props { + index: number; + cancel: () => void; + confirm: (changes: MethodProvider[]) => void; + +} + +export interface MethodProvider { + authentication_method: number; + provider: string; +} + +export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode { + const [changedProvider, setChangedProvider] = useState<Array<string>>([]) + + const reducer = useAnastasisContext() + if (!reducer) { + return <div>no reducer in context</div> + } + if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { + return <div>invalid state</div> + } + + const selectableProviders: ProviderInfoByType = {} + const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {}) + for (let index = 0; index < allProviders.length; index++) { + const [url, status] = allProviders[index] + if ("methods" in status) { + status.methods.map(m => { + const type: KnownAuthMethods = m.type as KnownAuthMethods + const values = selectableProviders[type] || [] + const isFree = !m.usage_fee || m.usage_fee.endsWith(":0") + values.push({ url, cost: m.usage_fee, isFree }) + selectableProviders[type] = values + }) + } + } + + const allAuthMethods = reducer.currentReducerState.authentication_methods ?? []; + const policies = reducer.currentReducerState.policies ?? []; + const policy = policies[policy_index] + + for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) { + policy?.methods.find(m => m.authentication_method === method_index)?.provider + } + + function sendChanges(): void { + const newMethods: MethodProvider[] = [] + allAuthMethods.forEach((method, index) => { + const oldValue = policy?.methods.find(m => m.authentication_method === index) + if (changedProvider[index] === undefined && oldValue !== undefined) { + newMethods.push(oldValue) + } + if (changedProvider[index] !== undefined && changedProvider[index] !== "") { + newMethods.push({ + authentication_method: index, + provider: changedProvider[index] + }) + } + }) + confirm(newMethods) + } + + return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}> + <section class="section"> + {!policy ? <p> + Creating a new policy #{policy_index} + </p> : <p> + Editing policy #{policy_index} + </p>} + {allAuthMethods.map((method, index) => { + //take the url from the updated change or from the policy + const providerURL = changedProvider[index] === undefined ? + policy?.methods.find(m => m.authentication_method === index)?.provider : + changedProvider[index]; + + const type: KnownAuthMethods = method.type as KnownAuthMethods + function changeProviderTo(url: string): void { + const copy = [...changedProvider] + copy[index] = url + setChangedProvider(copy) + } + return ( + <div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}> + <span class="icon"> + {authMethods[type]?.icon} + </span> + <span> + {method.instructions} + </span> + <span> + <span class="select " > + <select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}> + <option key="none" value=""> << off >> </option> + {selectableProviders[type]?.map(prov => ( + <option key={prov.url} value={prov.url}> + {prov.url} + </option> + ))} + </select> + </span> + </span> + </div> + ); + })} + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={cancel}>Cancel</button> + <span class="buttons"> + <button class="button" onClick={() => setChangedProvider([])}>Reset</button> + <button class="button is-info" onClick={sendChanges}>Confirm</button> + </span> + </div> + </section> + </AnastasisClientFrame> +} diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx index 5ba0c937d..9f7e26c16 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx @@ -233,16 +233,16 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, { instructions: "Does P equal NP?", challenge: "C5SP8" },{ - type: "email", - instructions: "Email to qwe@asd.com", + type: "totp", + instructions: "Response code for 'Anastasis'", challenge: "E5VPA" }, { type: "sms", - instructions: "SMS to 555-555", + instructions: "SMS to 6666-6666", challenge: "" }, { type: "question", - instructions: "Does P equal NP?", + instructions: "How did the chicken cross the road?", challenge: "C5SP8" }] } as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx index 673f215e2..b8beb7b47 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { AuthMethod } from "anastasis-core"; import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame } from "./index"; import { authMethods, KnownAuthMethods } from "./authMethod"; +import { EditPoliciesScreen } from "./EditPoliciesScreen"; +import { AnastasisClientFrame } from "./index"; export function ReviewPoliciesScreen(): VNode { + const [editingPolicy, setEditingPolicy] = useState<number | undefined>() const reducer = useAnastasisContext() if (!reducer) { return <div>no reducer in context</div> @@ -12,20 +16,44 @@ export function ReviewPoliciesScreen(): VNode { if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { return <div>invalid state</div> } + const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? []; const policies = reducer.currentReducerState.policies ?? []; + if (editingPolicy !== undefined) { + return ( + <EditPoliciesScreen + index={editingPolicy} + cancel={() => setEditingPolicy(undefined)} + confirm={(newMethods) => { + reducer.runTransaction(async (tx) => { + await tx.transition("delete_policy", { + policy_index: editingPolicy + }); + await tx.transition("add_policy", { + policy: newMethods + }); + }); + setEditingPolicy(undefined) + }} + /> + ) + } + const errors = policies.length < 1 ? 'Need more policies' : undefined return ( <AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies"> {policies.length > 0 && <p class="block"> Based on your configured authentication method you have created, some policies - have been configured. In order to recover your secret you have to solve all the + have been configured. In order to recover your secret you have to solve all the challenges of at least one policy. - </p> } + </p>} {policies.length < 1 && <p class="block"> No policies had been created. Go back and add more authentication methods. - </p> } + </p>} + <div class="block" onClick={() => setEditingPolicy(policies.length + 1)}> + <button class="button is-success">Add new policy</button> + </div> {policies.map((p, policy_index) => { const methods = p.methods .map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider })) @@ -44,18 +72,21 @@ export function ReviewPoliciesScreen(): VNode { </p>} {methods.map((m, i) => { return ( - <p key={i} class="block" style={{display:'flex', alignItems:'center'}}> - <span class="icon"> - {authMethods[m.type as KnownAuthMethods]?.icon} - </span> - <span> - {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a> - </span> - </p> + <p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}> + <span class="icon"> + {authMethods[m.type as KnownAuthMethods]?.icon} + </span> + <span> + {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a> + </span> + </p> ); })} </div> - <div style={{ marginTop: 'auto', marginBottom: 'auto' }}><button class="button is-danger" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button></div> + <div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}> + <button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button> + <button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button> + </div> </div> ); })} diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.tsx index c751ad9e4..6e97eb586 100644 --- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx @@ -10,33 +10,29 @@ export function StartScreen(): VNode { } return ( <AnastasisClientFrame hideNav title="Home"> - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> - <div class="buttons"> - <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}> - <div class="icon"><i class="mdi mdi-arrow-up" /></div> - <span>Backup a secret</span> - </button> + <div class="buttons"> + <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}> + <div class="icon"><i class="mdi mdi-arrow-up" /></div> + <span>Backup a secret</span> + </button> - <button class="button is-info" onClick={() => reducer.startRecover()}> - <div class="icon"><i class="mdi mdi-arrow-down" /></div> - <span>Recover a secret</span> - </button> + <button class="button is-info" onClick={() => reducer.startRecover()}> + <div class="icon"><i class="mdi mdi-arrow-down" /></div> + <span>Recover a secret</span> + </button> - <button class="button"> - <div class="icon"><i class="mdi mdi-file" /></div> - <span>Restore a session</span> - </button> - </div> - - </div> - <div class="column" /> + <button class="button"> + <div class="icon"><i class="mdi mdi-file" /></div> + <span>Restore a session</span> + </button> </div> - </section> + + </div> + <div class="column" /> </div> </AnastasisClientFrame> ); diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx index eab800e35..04fa00d59 100644 --- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx @@ -27,7 +27,7 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A <AnastasisClientFrame hideNav title="Add Security Question"> <div> <p> - For security question authentication, you need to provide a question + For2 security question authentication, you need to provide a question and its answer. When recovering your secret, you will be shown the question and you will need to type the answer exactly as you typed it here. @@ -47,6 +47,13 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A /> </div> + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={cancel}>Cancel</button> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button> + </span> + </div> + {configured.length > 0 && <section class="section"> <div class="block"> Your security questions: @@ -58,12 +65,6 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A </div> })} </div></section>} - <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> - <button class="button" onClick={cancel}>Cancel</button> - <span data-tooltip={errors}> - <button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button> - </span> - </div> </div> </AnastasisClientFrame > ); diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx index fefaa184c..415cf6e98 100644 --- a/packages/anastasis-webui/src/pages/home/index.tsx +++ b/packages/anastasis-webui/src/pages/home/index.tsx @@ -13,6 +13,7 @@ import { import { useErrorBoundary } from "preact/hooks"; +import { AsyncButton } from "../../components/AsyncButton"; import { Menu } from "../../components/menu"; import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis"; import { @@ -25,7 +26,6 @@ import { BackupFinishedScreen } from "./BackupFinishedScreen"; import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen"; import { ChallengePayingScreen } from "./ChallengePayingScreen"; import { ContinentSelectionScreen } from "./ContinentSelectionScreen"; -import { CountrySelectionScreen } from "./CountrySelectionScreen"; import { PoliciesPayingScreen } from "./PoliciesPayingScreen"; import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen"; import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen"; @@ -95,12 +95,19 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode { if (!reducer) { return <p>Fatal: Reducer must be in context.</p>; } - const next = (): void => { - if (props.onNext) { - props.onNext(); - } else { - reducer.transition("next", {}); - } + const next = async (): Promise<void> => { + return new Promise((res, rej) => { + try { + if (props.onNext) { + props.onNext(); + } else { + reducer.transition("next", {}); + } + res() + } catch { + rej() + } + }) }; const handleKeyPress = ( e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>, @@ -111,20 +118,18 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode { return ( <Fragment> <Menu title="Anastasis" /> - <div> - <div class="home" onKeyPress={(e) => handleKeyPress(e)}> - <h1 class="title">{props.title}</h1> + <div class="home" onKeyPress={(e) => handleKeyPress(e)}> + <h1 class="title">{props.title}</h1> + <section class="section is-main-section"> <ErrorBanner /> {props.children} {!props.hideNav ? ( <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <button class="button" onClick={() => reducer.back()}>Back</button> - <span data-tooltip={props.hideNext}> - <button class="button is-info" onClick={next} disabled={props.hideNext !== undefined}>Next</button> - </span> + <AsyncButton data-tooltip={props.hideNext} onClick={next} disabled={props.hideNext !== undefined}>Next</AsyncButton> </div> ) : null} - </div> + </section> </div> </Fragment> ); diff --git a/packages/anastasis-webui/src/scss/main.scss b/packages/anastasis-webui/src/scss/main.scss index 1e0d3fded..b5335073f 100644 --- a/packages/anastasis-webui/src/scss/main.scss +++ b/packages/anastasis-webui/src/scss/main.scss @@ -195,7 +195,7 @@ div[data-tooltip]::before { padding: 1em 1em; min-height: 100%; width: 100%; - max-width: 40em; + // max-width: 40em; } // .home div { diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx index 48ac47544..244be8af8 100644 --- a/packages/anastasis-webui/src/utils/index.tsx +++ b/packages/anastasis-webui/src/utils/index.tsx @@ -86,10 +86,10 @@ const base = { { type: "question", usage_fee: "COL:0" - },{ + }, { type: "sms", usage_fee: "COL:0" - },{ + }, { type: "email", usage_fee: "COL:0" }, @@ -98,6 +98,48 @@ const base = { storage_limit_in_megabytes: 16, truth_upload_fee: "COL:0" }, + "https://kudos.demo.anastasis.lu/": { + http_status: 200, + annual_fee: "COL:0", + business_name: "ana", + currency: "COL", + liability_limit: "COL:10", + methods: [ + { + type: "question", + usage_fee: "COL:0" + }, { + type: "email", + usage_fee: "COL:0" + }, + ], + salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", + storage_limit_in_megabytes: 16, + truth_upload_fee: "COL:0" + }, + "https://anastasis.demo.taler.net/": { + http_status: 200, + annual_fee: "COL:0", + business_name: "ana", + currency: "COL", + liability_limit: "COL:10", + methods: [ + { + type: "question", + usage_fee: "COL:0" + }, { + type: "sms", + usage_fee: "COL:0" + }, { + type: "totp", + usage_fee: "COL:0" + }, + ], + salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", + storage_limit_in_megabytes: 16, + truth_upload_fee: "COL:0" + }, + "http://localhost:8087/": { code: 8414, hint: "request to provider failed" @@ -118,55 +160,72 @@ const base = { export const reducerStatesExample = { initial: undefined, - recoverySelectCountry: {...base, + recoverySelectCountry: { + ...base, recovery_state: RecoveryStates.CountrySelecting } as ReducerState, - recoverySelectContinent: {...base, + recoverySelectContinent: { + ...base, recovery_state: RecoveryStates.ContinentSelecting, } as ReducerState, - secretSelection: {...base, + secretSelection: { + ...base, recovery_state: RecoveryStates.SecretSelecting, } as ReducerState, - recoveryFinished: {...base, + recoveryFinished: { + ...base, recovery_state: RecoveryStates.RecoveryFinished, } as ReducerState, - challengeSelecting: {...base, + challengeSelecting: { + ...base, recovery_state: RecoveryStates.ChallengeSelecting, } as ReducerState, - challengeSolving: {...base, + challengeSolving: { + ...base, recovery_state: RecoveryStates.ChallengeSolving, } as ReducerState, - challengePaying: {...base, + challengePaying: { + ...base, recovery_state: RecoveryStates.ChallengePaying, } as ReducerState, - recoveryAttributeEditing: {...base, + recoveryAttributeEditing: { + ...base, recovery_state: RecoveryStates.UserAttributesCollecting } as ReducerState, - backupSelectCountry: {...base, + backupSelectCountry: { + ...base, backup_state: BackupStates.CountrySelecting } as ReducerState, - backupSelectContinent: {...base, + backupSelectContinent: { + ...base, backup_state: BackupStates.ContinentSelecting, } as ReducerState, - secretEdition: {...base, + secretEdition: { + ...base, backup_state: BackupStates.SecretEditing, } as ReducerState, - policyReview: {...base, + policyReview: { + ...base, backup_state: BackupStates.PoliciesReviewing, } as ReducerState, - policyPay: {...base, + policyPay: { + ...base, backup_state: BackupStates.PoliciesPaying, } as ReducerState, - backupFinished: {...base, + backupFinished: { + ...base, backup_state: BackupStates.BackupFinished, } as ReducerState, - authEditing: {...base, + authEditing: { + ...base, backup_state: BackupStates.AuthenticationsEditing } as ReducerState, - backupAttributeEditing: {...base, + backupAttributeEditing: { + ...base, backup_state: BackupStates.UserAttributesCollecting } as ReducerState, - truthsPaying: {...base, + truthsPaying: { + ...base, backup_state: BackupStates.TruthsPaying } as ReducerState, |