diff options
44 files changed, 1048 insertions, 136 deletions
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index b4e911ffb..c9e2bcf36 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -77,6 +77,7 @@ import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js"; const { fetch, Request, Response, Headers } = fetchPonyfill({}); export * from "./reducer-types.js"; +export * as validators from './validators.js'; function getContinents(): ContinentInfo[] { const continentSet = new Set<string>(); diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index 1a443bf9b..2e1154fca 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -93,6 +93,9 @@ export interface UserAttributeSpec { type: string; uuid: string; widget: string; + optional?: boolean; + 'validation-regex': string | undefined; + 'validation-logic': string | undefined; } export interface RecoveryInternalData { diff --git a/packages/anastasis-core/src/validators.ts b/packages/anastasis-core/src/validators.ts new file mode 100644 index 000000000..1c04bfdb3 --- /dev/null +++ b/packages/anastasis-core/src/validators.ts @@ -0,0 +1,28 @@ +function isPrime(num: number): boolean { + for (let i = 2, s = Math.sqrt(num); i <= s; i++) + if (num % i === 0) return false; + return num > 1; +} + +export function AL_NID_check(s: string): boolean { return true } +export function BE_NRN_check(s: string): boolean { return true } +export function CH_AHV_check(s: string): boolean { return true } +export function CZ_BN_check(s: string): boolean { return true } +export function DE_TIN_check(s: string): boolean { return true } +export function DE_SVN_check(s: string): boolean { return true } +export function ES_DNI_check(s: string): boolean { return true } +export function IN_AADHAR_check(s: string): boolean { return true } +export function IT_CF_check(s: string): boolean { + return true +} + +export function XX_SQUARE_check(s: string): boolean { + const n = parseInt(s, 10) + const r = Math.sqrt(n) + return n === r * r; +} +export function XY_PRIME_check(s: string): boolean { + const n = parseInt(s, 10) + return isPrime(n) +} + diff --git a/packages/anastasis-webui/.storybook/preview.js b/packages/anastasis-webui/.storybook/preview.js index 7cb9405ba..9ab4d9404 100644 --- a/packages/anastasis-webui/.storybook/preview.js +++ b/packages/anastasis-webui/.storybook/preview.js @@ -21,6 +21,12 @@ import { h } from 'preact'; export const parameters = { controls: { expanded: true }, + options: { + storySort: (a, b) => { + return (a[1].args.order ?? 0) - (b[1].args.order ?? 0) + // return a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }) + } + }, } export const globalTypes = { diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index 57cfdd8d4..4cdb00243 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -25,6 +25,7 @@ "dependencies": { "@gnu-taler/taler-util": "workspace:^0.8.3", "anastasis-core": "workspace:^0.0.1", + "date-fns": "2.22.1", "jed": "1.1.1", "preact": "^10.3.1", "preact-render-to-string": "^5.1.4", diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx new file mode 100644 index 000000000..c45acc6d2 --- /dev/null +++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx @@ -0,0 +1,63 @@ +import { format } from "date-fns"; +import { h, VNode } from "preact"; +import { useLayoutEffect, useRef, useState } from "preact/hooks"; +import { DatePicker } from "../picker/DatePicker"; + +export interface DateInputProps { + label: string; + grabFocus?: boolean; + tooltip?: string; + error?: string; + bind: [string, (x: string) => void]; +} + +export function DateInput(props: DateInputProps): VNode { + const inputRef = useRef<HTMLInputElement>(null); + useLayoutEffect(() => { + if (props.grabFocus) { + inputRef.current?.focus(); + } + }, [props.grabFocus]); + const [opened, setOpened2] = useState(false) + function setOpened(v: boolean) { + console.log('dale', v) + setOpened2(v) + } + + 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 + type="text" + class={showError ? 'input is-danger' : 'input'} + onClick={() => { setOpened(true) }} + value={value} + ref={inputRef} /> + + <span class="control icon is-right"> + <span class="icon"><i class="mdi mdi-calendar" /></span> + </span> + </div> + {showError && <p class="help is-danger">{props.error}</p>} + <DatePicker + opened={opened} + closeFunction={() => setOpened(false)} + dateReceiver={(d) => { + setDirty(true) + const v = format(d, 'yyyy/MM/dd') + props.bind[1](v); + }} + /> + </div> + ; + +} diff --git a/packages/anastasis-webui/src/components/fields/LabeledInput.tsx b/packages/anastasis-webui/src/components/fields/LabeledInput.tsx new file mode 100644 index 000000000..96d634a4f --- /dev/null +++ b/packages/anastasis-webui/src/components/fields/LabeledInput.tsx @@ -0,0 +1,40 @@ +import { h, VNode } from "preact"; +import { useLayoutEffect, useRef, useState } from "preact/hooks"; + +export interface LabeledInputProps { + label: string; + grabFocus?: boolean; + error?: string; + tooltip?: string; + bind: [string, (x: string) => void]; +} + +export function LabeledInput(props: LabeledInputProps): 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} + class={showError ? 'input is-danger' : 'input'} + onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}} + ref={inputRef} + style={{ display: "block" }} /> + </div> + {showError && <p class="help is-danger">{props.error}</p>} + </div> + ); +} diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx index df582a5d0..12223d473 100644 --- a/packages/anastasis-webui/src/components/menu/SideBar.tsx +++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx @@ -59,25 +59,21 @@ export function Sidebar({ mobile }: Props): VNode { {!reducer.currentReducerState && <li> <div class="ml-4"> - <span class="menu-item-label"><Translate>Start one options</Translate></span> + <span class="menu-item-label"><Translate>Select one option</Translate></span> </div> </li> } {reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment> - <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ? 'is-active' : ''}> + <li class={ + reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || + reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}> <div class="ml-4"> - <span class="menu-item-label"><Translate>Continent selection</Translate></span> - </div> - </li> - <li class={reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}> - <div class="ml-4"> - <span class="menu-item-label"><Translate>Country selection</Translate></span> + <span class="menu-item-label"><Translate>Location & Currency</Translate></span> </div> </li> <li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}> <div class="ml-4"> - - <span class="menu-item-label"><Translate>User attributes</Translate></span> + <span class="menu-item-label"><Translate>Personal information</Translate></span> </div> </li> <li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}> @@ -119,7 +115,7 @@ export function Sidebar({ mobile }: Props): VNode { </Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ? 'is-active' : ''}> <div class="ml-4"> - <span class="menu-item-label"><Translate>TruthsPaying</Translate></span> + <span class="menu-item-label"><Translate>ContinentSelecting</Translate></span> </div> </li> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}> diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx new file mode 100644 index 000000000..e51b3db68 --- /dev/null +++ b/packages/anastasis-webui/src/components/picker/DatePicker.tsx @@ -0,0 +1,324 @@ +/* + 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 { h, Component } from "preact"; + +interface Props { + closeFunction?: () => void; + dateReceiver?: (d: Date) => void; + opened?: boolean; +} +interface State { + displayedMonth: number; + displayedYear: number; + selectYearMode: boolean; + currentDate: Date; +} +const now = new Date() + +const monthArrShortFull = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +] + +const monthArrShort = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' +] + +const dayArr = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat' +] + +const yearArr: number[] = [] + + +// inspired by https://codepen.io/m4r1vs/pen/MOOxyE +export class DatePicker extends Component<Props, State> { + + closeDatePicker() { + this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent + } + + /** + * Gets fired when a day gets clicked. + * @param {object} e The event thrown by the <span /> element clicked + */ + dayClicked(e: any) { + + const element = e.target; // the actual element clicked + + if (element.innerHTML === '') return false; // don't continue if <span /> empty + + // get date from clicked element (gets attached when rendered) + const date = new Date(element.getAttribute('data-value')); + + // update the state + this.setState({ currentDate: date }); + this.passDateToParent(date) + } + + /** + * returns days in month as array + * @param {number} month the month to display + * @param {number} year the year to display + */ + getDaysByMonth(month: number, year: number) { + + const calendar = []; + + const date = new Date(year, month, 1); // month to display + + const firstDay = new Date(year, month, 1).getDay(); // first weekday of month + const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month + + let day: number | null = 0; + + // the calendar is 7*6 fields big, so 42 loops + for (let i = 0; i < 42; i++) { + + if (i >= firstDay && day !== null) day = day + 1; + if (day !== null && day > lastDate) day = null; + + // append the calendar Array + calendar.push({ + day: (day === 0 || day === null) ? null : day, // null or number + date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date() + today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean + }); + } + + return calendar; + } + + /** + * Display previous month by updating state + */ + displayPrevMonth() { + if (this.state.displayedMonth <= 0) { + this.setState({ + displayedMonth: 11, + displayedYear: this.state.displayedYear - 1 + }); + } + else { + this.setState({ + displayedMonth: this.state.displayedMonth - 1 + }); + } + } + + /** + * Display next month by updating state + */ + displayNextMonth() { + if (this.state.displayedMonth >= 11) { + this.setState({ + displayedMonth: 0, + displayedYear: this.state.displayedYear + 1 + }); + } + else { + this.setState({ + displayedMonth: this.state.displayedMonth + 1 + }); + } + } + + /** + * Display the selected month (gets fired when clicking on the date string) + */ + displaySelectedMonth() { + if (this.state.selectYearMode) { + this.toggleYearSelector(); + } + else { + if (!this.state.currentDate) return false; + this.setState({ + displayedMonth: this.state.currentDate.getMonth(), + displayedYear: this.state.currentDate.getFullYear() + }); + } + } + + toggleYearSelector() { + this.setState({ selectYearMode: !this.state.selectYearMode }); + } + + changeDisplayedYear(e: any) { + const element = e.target; + this.toggleYearSelector(); + this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 }); + } + + /** + * Pass the selected date to parent when 'OK' is clicked + */ + passSavedDateDateToParent() { + this.passDateToParent(this.state.currentDate) + } + passDateToParent(date: Date) { + if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date); + this.closeDatePicker(); + } + + componentDidUpdate() { + if (this.state.selectYearMode) { + document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it + } + } + + constructor() { + super(); + + this.closeDatePicker = this.closeDatePicker.bind(this); + this.dayClicked = this.dayClicked.bind(this); + this.displayNextMonth = this.displayNextMonth.bind(this); + this.displayPrevMonth = this.displayPrevMonth.bind(this); + this.getDaysByMonth = this.getDaysByMonth.bind(this); + this.changeDisplayedYear = this.changeDisplayedYear.bind(this); + this.passDateToParent = this.passDateToParent.bind(this); + this.toggleYearSelector = this.toggleYearSelector.bind(this); + this.displaySelectedMonth = this.displaySelectedMonth.bind(this); + + + this.state = { + currentDate: now, + displayedMonth: now.getMonth(), + displayedYear: now.getFullYear(), + selectYearMode: false + } + } + + render() { + + const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state; + + return ( + <div> + <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}> + + <div class="datePicker--titles"> + <h3 style={{ + color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)' + }} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3> + <h2 style={{ + color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)' + }} onClick={this.displaySelectedMonth}> + {dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} + </h2> + </div> + + {!selectYearMode && <nav> + <span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span> + <h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4> + <span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span> + </nav>} + + <div class="datePicker--scroll"> + + {!selectYearMode && <div class="datePicker--calendar" > + + <div class="datePicker--dayNames"> + {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)} + </div> + + <div onClick={this.dayClicked} class="datePicker--days"> + + {/* + Loop through the calendar object returned by getDaysByMonth(). + */} + + {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear) + .map( + day => { + let selected = false; + + if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString()); + + return (<span key={day.day} + class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')} + disabled={!day.date} + data-value={day.date} + > + {day.day} + </span>) + } + ) + } + + </div> + + </div>} + + {selectYearMode && <div class="datePicker--selectYear"> + + {yearArr.map(year => ( + <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}> + {year} + </span> + ))} + + </div>} + + </div> + </div> + + <div class="datePicker--background" onClick={this.closeDatePicker} style={{ + display: this.props.opened ? 'block' : 'none', + }} + /> + + </div> + ) + } +} + + +for (let i = 2010; i <= now.getFullYear() + 10; i++) { + yearArr.push(i); +} diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx new file mode 100644 index 000000000..275c80fa6 --- /dev/null +++ b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx @@ -0,0 +1,50 @@ +/* + 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 { h, FunctionalComponent } from 'preact'; +import { useState } from 'preact/hooks'; +import { DurationPicker as TestedComponent } from './DurationPicker'; + + +export default { + title: 'Components/Picker/Duration', + component: TestedComponent, + argTypes: { + onCreate: { action: 'onCreate' }, + goBack: { action: 'goBack' }, + } +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const Example = createExample(TestedComponent, { + days: true, minutes: true, hours: true, seconds: true, + value: 10000000 +}); + +export const WithState = () => { + const [v,s] = useState<number>(1000000) + return <TestedComponent value={v} onChange={s} days minutes hours seconds /> +} diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx new file mode 100644 index 000000000..235a63e2d --- /dev/null +++ b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx @@ -0,0 +1,154 @@ +/* + 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 { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useTranslator } from "../../i18n"; +import "../../scss/DurationPicker.scss"; + +export interface Props { + hours?: boolean; + minutes?: boolean; + seconds?: boolean; + days?: boolean; + onChange: (value: number) => void; + value: number +} + +// inspiration taken from https://github.com/flurmbo/react-duration-picker +export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode { + const ss = 1000 + const ms = ss * 60 + const hs = ms * 60 + const ds = hs * 24 + const i18n = useTranslator() + + return <div class="rdp-picker"> + {days && <DurationColumn unit={i18n`days`} max={99} + value={Math.floor(value / ds)} + onDecrease={value >= ds ? () => onChange(value - ds) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined} + onChange={diff => onChange(value + diff * ds)} + />} + {hours && <DurationColumn unit={i18n`hours`} max={23} min={1} + value={Math.floor(value / hs) % 24} + onDecrease={value >= hs ? () => onChange(value - hs) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined} + onChange={diff => onChange(value + diff * hs)} + />} + {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1} + value={Math.floor(value / ms) % 60} + onDecrease={value >= ms ? () => onChange(value - ms) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined} + onChange={diff => onChange(value + diff * ms)} + />} + {seconds && <DurationColumn unit={i18n`seconds`} max={59} + value={Math.floor(value / ss) % 60} + onDecrease={value >= ss ? () => onChange(value - ss) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined} + onChange={diff => onChange(value + diff * ss)} + />} + </div> +} + +interface ColProps { + unit: string, + min?: number, + max: number, + value: number, + onIncrease?: () => void; + onDecrease?: () => void; + onChange?: (diff: number) => void; +} + +function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) { + const [value, handler] = useState<{v:string}>({ + v: toTwoDigitString(initial) + }) + + return <input + value={value.v} + onBlur={(e) => onChange(parseInt(value.v, 10))} + onInput={(e) => { + e.preventDefault() + const n = Number.parseInt(e.currentTarget.value, 10); + if (isNaN(n)) return handler({v:toTwoDigitString(initial)}) + return handler({v:toTwoDigitString(n)}) + }} + style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} /> +} + +function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode { + + const cellHeight = 35 + return ( + <div class="rdp-column-container"> + <div class="rdp-masked-div"> + <hr class="rdp-reticule" style={{ top: cellHeight * 2 - 1 }} /> + <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} /> + + <div class="rdp-column" style={{ top: 0 }}> + + <div class="rdp-cell" key={value - 2}> + {onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }} + onClick={onDecrease}> + <span class="icon"> + <i class="mdi mdi-chevron-up" /> + </span> + </button>} + </div> + <div class="rdp-cell" key={value - 1}> + {value > min ? toTwoDigitString(value - 1) : ''} + </div> + <div class="rdp-cell rdp-center" key={value}> + {onChange ? + <InputNumber initial={value} onChange={(n) => onChange(n - value)} /> : + toTwoDigitString(value) + } + <div>{unit}</div> + </div> + + <div class="rdp-cell" key={value + 1}> + {value < max ? toTwoDigitString(value + 1) : ''} + </div> + + <div class="rdp-cell" key={value + 2}> + {onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }} + onClick={onIncrease}> + <span class="icon"> + <i class="mdi mdi-chevron-down" /> + </span> + </button>} + </div> + + </div> + </div> + </div> + ); +} + + +function toTwoDigitString(n: number) { + if (n < 10) { + return `0${n}`; + } + return `${n}`; +}
\ No newline at end of file diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx index d28a6df43..d9be48fb4 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx @@ -28,14 +28,40 @@ import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen' export default { title: 'Pages/AttributeEntryScreen', component: TestedComponent, + args: { + order: 4, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, }, }; -export const WithSomeAttributes = createExample(TestedComponent, { - ...reducerStatesExample.attributeEditing, +export const Backup = createExample(TestedComponent, { + ...reducerStatesExample.backupAttributeEditing, + required_attributes: [{ + name: 'first', + label: 'first', + type: 'type', + uuid: 'asdasdsa1', + widget: 'wid', + }, { + name: 'pepe', + label: 'second', + type: 'type', + uuid: 'asdasdsa2', + widget: 'wid', + }, { + name: 'pepe2', + label: 'third', + type: 'type', + uuid: 'asdasdsa3', + widget: 'calendar', + }] +} as ReducerState); + +export const Recovery = createExample(TestedComponent, { + ...reducerStatesExample.recoveryAttributeEditing, required_attributes: [{ name: 'first', label: 'first', @@ -57,7 +83,40 @@ export const WithSomeAttributes = createExample(TestedComponent, { }] } as ReducerState); -export const Empty = createExample(TestedComponent, { - ...reducerStatesExample.attributeEditing, +export const WithNoRequiredAttribute = createExample(TestedComponent, { + ...reducerStatesExample.backupAttributeEditing, required_attributes: undefined } as ReducerState); + +const allWidgets = [ + "anastasis_gtk_ia_aadhar_in", + "anastasis_gtk_ia_ahv", + "anastasis_gtk_ia_birthdate", + "anastasis_gtk_ia_birthnumber_cz", + "anastasis_gtk_ia_birthnumber_sk", + "anastasis_gtk_ia_birthplace", + "anastasis_gtk_ia_cf_it", + "anastasis_gtk_ia_cpr_dk", + "anastasis_gtk_ia_es_dni", + "anastasis_gtk_ia_es_ssn", + "anastasis_gtk_ia_full_name", + "anastasis_gtk_ia_my_jp", + "anastasis_gtk_ia_nid_al", + "anastasis_gtk_ia_nid_be", + "anastasis_gtk_ia_ssn_de", + "anastasis_gtk_ia_ssn_us", + "anastasis_gtk_ia_tax_de", + "anastasis_gtk_xx_prime", + "anastasis_gtk_xx_square", +] + +export const WithAllPosibleWidget = createExample(TestedComponent, { + ...reducerStatesExample.backupAttributeEditing, + required_attributes: allWidgets.map(w => ({ + name: w, + label: `widget: ${w}`, + type: 'type', + uuid: `uuid-${w}`, + widget: w + })) +} as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx index 2f804f940..3b39cf9c4 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { UserAttributeSpec, validators } from "anastasis-core"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { ReducerStateRecovery, ReducerStateBackup, UserAttributeSpec } from "anastasis-core/lib"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer"; -import { AnastasisClientFrame, withProcessLabel, LabeledInput } from "./index"; +import { AnastasisClientFrame, withProcessLabel } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; +import { DateInput } from "../../components/fields/DateInput"; export function AttributeEntryScreen(): VNode { const reducer = useAnastasisContext() @@ -18,48 +19,105 @@ export function AttributeEntryScreen(): VNode { if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) { return <div>invalid state</div> } - + + return ( <AnastasisClientFrame - title={withProcessLabel(reducer, "Select Country")} + title={withProcessLabel(reducer, "Who are you?")} onNext={() => reducer.transition("enter_user_attributes", { identity_attributes: attrs, })} > - {reducer.currentReducerState.required_attributes?.map((x, i: number) => { - return ( - <AttributeEntryField - key={i} - isFirst={i == 0} - setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })} - spec={x} - value={attrs[x.name]} /> - ); - })} + <div class="columns"> + <div class="column is-half"> + + {reducer.currentReducerState.required_attributes?.map((x, i: number) => { + const value = attrs[x.name] + function checkIfValid(): string | undefined { + const pattern = x['validation-regex'] + if (pattern) { + const re = new RegExp(pattern) + if (!re.test(value)) return 'The value is invalid' + } + const logic = x['validation-logic'] + if (logic) { + const func = (validators as any)[logic]; + if (func && typeof func === 'function' && !func(value)) return 'Please check the value' + } + const optional = x.optional + console.log('optiona', optional) + if (!optional && !value) { + return 'This value is required' + } + return undefined + } + + return ( + <AttributeEntryField + key={i} + isFirst={i == 0} + setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })} + spec={x} + isValid={checkIfValid} + value={value} /> + ); + })} + + </div> + <div class="column is-half" > + <h1><b>This stay private</b></h1> + <p>The information you have entered here: + </p> + <ul> + <li> + <span class="icon is-right"> + <i class="mdi mdi-circle-small" /> + </span> + Will be hashed, and therefore unreadable + </li> + <li><span class="icon is-right"> + <i class="mdi mdi-circle-small" /> + </span>The non-hashed version is not shared</li> + </ul> + </div> + </div> </AnastasisClientFrame> ); } -interface AttributeEntryProps { - reducer: AnastasisReducerApi; - reducerState: ReducerStateRecovery | ReducerStateBackup; -} - -export interface AttributeEntryFieldProps { +interface AttributeEntryFieldProps { isFirst: boolean; value: string; setValue: (newValue: string) => void; spec: UserAttributeSpec; + isValid: () => string | undefined; } -export function AttributeEntryField(props: AttributeEntryFieldProps): VNode { +function AttributeEntryField(props: AttributeEntryFieldProps): VNode { + const errorMessage = props.isValid() + return ( <div> - <LabeledInput - grabFocus={props.isFirst} - label={props.spec.label} - bind={[props.value, props.setValue]} - /> + {props.spec.type === 'date' ? + <DateInput + grabFocus={props.isFirst} + label={props.spec.label} + error={errorMessage} + bind={[props.value, props.setValue]} + /> : + <LabeledInput + grabFocus={props.isFirst} + label={props.spec.label} + error={errorMessage} + bind={[props.value, props.setValue]} + /> + } + <span> + <span class="icon is-right"> + <i class="mdi mdi-eye-off" /> + </span> + This stay private + </span> </div> ); } diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx index 9567e0ef7..5243c5259 100644 --- a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx @@ -6,7 +6,8 @@ import { import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { AnastasisClientFrame, LabeledInput } from "./index"; +import { AnastasisClientFrame } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode { const [email, setEmail] = useState(""); diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx index 55e37a968..1c2a9a92e 100644 --- a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx @@ -6,7 +6,7 @@ import { import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { LabeledInput } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode { const [fullName, setFullName] = useState(""); diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx index 7699cdf34..c2bd24ef9 100644 --- a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx @@ -6,7 +6,8 @@ import { import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { AnastasisClientFrame, LabeledInput } from "./index"; +import { AnastasisClientFrame } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode { const [questionText, setQuestionText] = useState(""); diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx index 44d3795b2..8f86831a9 100644 --- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx @@ -24,8 +24,11 @@ import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationE export default { - title: 'Pages/AuthenticationEditorScreen', + title: 'Pages/backup/AuthenticationEditorScreen', component: TestedComponent, + args: { + order: 5, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx index 65a2b7e13..0c9d007bc 100644 --- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx @@ -26,8 +26,11 @@ import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen' export default { - title: 'Pages/BackupFinishedScreen', + title: 'Pages/backup/FinishedScreen', component: TestedComponent, + args: { + order: 9, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx index 4f186c031..def44c5a6 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx @@ -20,23 +20,27 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ReducerState } from 'anastasis-core'; +import { RecoveryStates, ReducerState } from 'anastasis-core'; import { createExample, reducerStatesExample } from '../../utils'; import { ChallengeOverviewScreen as TestedComponent } from './ChallengeOverviewScreen'; export default { - title: 'Pages/ChallengeOverviewScreen', + title: 'Pages/recovery/ChallengeOverviewScreen', component: TestedComponent, + args: { + order: 5, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, }, }; -export const OneChallenge = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting, +export const OneChallenge = createExample(TestedComponent, { + ...reducerStatesExample.challengeSelecting, recovery_information: { - policies: [[{uuid:'1'}]], + policies: [[{ uuid: '1' }]], challenges: [{ cost: 'USD:1', instructions: 'just go for it', @@ -46,37 +50,44 @@ export const OneChallenge = createExample(TestedComponent, {...reducerStatesExam }, } as ReducerState); -export const MoreChallenges = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting, +export const MoreChallenges = createExample(TestedComponent, { + ...reducerStatesExample.challengeSelecting, recovery_information: { - policies: [[{uuid:'1'}, {uuid:'2'}],[{uuid:'3'}]], + policies: [[{ uuid: '1' }, { uuid: '2' }], [{ uuid: 'uuid-3' }]], challenges: [{ cost: 'USD:1', instructions: 'just go for it', type: 'question', uuid: '1', - },{ + }, { cost: 'USD:1', instructions: 'just go for it', type: 'question', uuid: '2', - },{ + }, { cost: 'USD:1', instructions: 'just go for it', type: 'question', - uuid: '3', + uuid: 'uuid-3', }] }, + challenge_feedback: { + 'uuid-3': { + state: 'solved' + } + } } as ReducerState); -export const OneBadConfiguredPolicy = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting, +export const OneBadConfiguredPolicy = createExample(TestedComponent, { + ...reducerStatesExample.challengeSelecting, recovery_information: { - policies: [[{uuid:'2'}]], + policies: [[{ uuid: '2' }]], challenges: [{ cost: 'USD:1', instructions: 'just go for it', type: 'sasd', uuid: '1', - }] + }], }, } as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx new file mode 100644 index 000000000..e5fe09e99 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx @@ -0,0 +1,38 @@ +/* + 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 { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen'; + + +export default { + title: 'Pages/recovery/__ChallengePayingScreen', + component: TestedComponent, + args: { + order: 10, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying); diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx new file mode 100644 index 000000000..d87afdf46 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx @@ -0,0 +1,33 @@ +import { h, VNode } from "preact"; +import { useAnastasisContext } from "../../context/anastasis"; +import { AnastasisClientFrame } from "./index"; + +export function ChallengePayingScreen(): VNode { + const reducer = useAnastasisContext() + if (!reducer) { + return <div>no reducer in context</div> + } + if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { + return <div>invalid state</div> + } + const payments = ['']; //reducer.currentReducerState.payments ?? + return ( + <AnastasisClientFrame + hideNext + title="Recovery: Challenge Paying" + > + <p> + Some of the providers require a payment to store the encrypted + authentication information. + </p> + <ul> + {payments.map((x, i) => { + return <li key={i}>{x}</li>; + })} + </ul> + <button onClick={() => reducer.transition("pay", {})}> + Check payment status now + </button> + </AnastasisClientFrame> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx index aad37cd7f..8744a2b79 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx @@ -26,11 +26,14 @@ import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectio export default { title: 'Pages/ContinentSelectionScreen', component: TestedComponent, + args: { + order: 2, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, }, }; -export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry); -export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry); +export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); +export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx index ad529a4a7..94c0409da 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx @@ -7,11 +7,11 @@ export function ContinentSelectionScreen(): VNode { if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) { return <div /> } - const sel = (x: string): void => reducer.transition("select_continent", { continent: x }); + const select = (continent: string) => (): void => reducer.transition("select_continent", { continent }); return ( <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Continent")}> {reducer.currentReducerState.continents.map((x: any) => ( - <button onClick={() => sel(x.name)} key={x.name}> + <button class="button" onClick={select(x.name)} key={x.name}> {x.name} </button> ))} diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx index adf36980f..3a642748a 100644 --- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx @@ -26,6 +26,9 @@ import { CountrySelectionScreen as TestedComponent } from './CountrySelectionScr export default { title: 'Pages/CountrySelectionScreen', component: TestedComponent, + args: { + order: 3, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx index 555622c1d..417c08633 100644 --- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx @@ -17,11 +17,15 @@ export function CountrySelectionScreen(): VNode { }); return ( <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} > - {reducer.currentReducerState.countries.map((x: any) => ( - <button onClick={() => sel(x)} key={x.name}> - {x.name} ({x.currency}) - </button> - ))} + <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/PoliciesPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx index 1a9462b88..e952ab28d 100644 --- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx @@ -26,8 +26,11 @@ import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen' export default { - title: 'Pages/PoliciesPayingScreen', + title: 'Pages/backup/PoliciesPayingScreen', component: TestedComponent, + args: { + order: 8, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx index 0c1842420..b5933db17 100644 --- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx @@ -26,7 +26,10 @@ import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScr export default { - title: 'Pages/RecoveryFinishedScreen', + title: 'Pages/recovery/FinishedScreen', + args: { + order: 7, + }, component: TestedComponent, argTypes: { onUpdate: { action: 'onUpdate' }, diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx index b52699e7b..91855b023 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx @@ -26,7 +26,10 @@ import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen' export default { - title: 'Pages/ReviewPoliciesScreen', + title: 'Pages/backup/ReviewPoliciesScreen', + args: { + order: 6, + }, component: TestedComponent, argTypes: { onUpdate: { action: 'onUpdate' }, diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx index 18560356a..49dd8fca8 100644 --- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx @@ -26,8 +26,11 @@ import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen'; export default { - title: 'Pages/SecretEditorScreen', + title: 'Pages/backup/SecretEditorScreen', component: TestedComponent, + args: { + order: 7, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx index a5235d66c..f5fd7c0d1 100644 --- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx @@ -4,9 +4,8 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; import { - AnastasisClientFrame, - LabeledInput -} from "./index"; + AnastasisClientFrame} from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; export function SecretEditorScreen(): VNode { const reducer = useAnastasisContext() diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx index e9c597023..f56939090 100644 --- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx @@ -26,8 +26,11 @@ import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScree export default { - title: 'Pages/SecretSelectionScreen', + title: 'Pages/recovery/SecretSelectionScreen', component: TestedComponent, + args: { + order: 4, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx index 2c27895c2..0d70405e5 100644 --- a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx @@ -1,7 +1,8 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame, LabeledInput } from "./index"; +import { AnastasisClientFrame } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; import { SolveEntryProps } from "./SolveScreen"; export function SolveEmailEntry({ challenge, feedback }: SolveEntryProps): VNode { diff --git a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx index 1a824acb8..22b8d470b 100644 --- a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx +++ b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx @@ -1,7 +1,8 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame, LabeledInput } from "./index"; +import { AnastasisClientFrame } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; import { SolveEntryProps } from "./SolveScreen"; export function SolvePostEntry({ challenge, feedback }: SolveEntryProps): VNode { diff --git a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx index 72dadbe89..319289381 100644 --- a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx @@ -1,7 +1,8 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame, LabeledInput } from "./index"; +import { AnastasisClientFrame } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; import { SolveEntryProps } from "./SolveScreen"; export function SolveQuestionEntry({ challenge, feedback }: SolveEntryProps): VNode { diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx index 69af9be42..c05c36b07 100644 --- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx @@ -26,8 +26,11 @@ import { SolveScreen as TestedComponent } from './SolveScreen'; export default { - title: 'Pages/SolveScreen', + title: 'Pages/recovery/SolveScreen', component: TestedComponent, + args: { + order: 6, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx index 163e0d1f3..c4cf3a680 100644 --- a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx @@ -1,7 +1,8 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisClientFrame, LabeledInput } from "./index"; +import { AnastasisClientFrame } from "./index"; +import { LabeledInput } from "../../components/fields/LabeledInput"; import { SolveEntryProps } from "./SolveScreen"; export function SolveSmsEntry({ challenge, feedback }: SolveEntryProps): VNode { diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx index ad84cd8f2..657a2dd74 100644 --- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx @@ -26,6 +26,9 @@ import { StartScreen as TestedComponent } from './StartScreen'; export default { title: 'Pages/StartScreen', component: TestedComponent, + args: { + order: 1, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.tsx index 6625ec5b8..c751ad9e4 100644 --- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx @@ -16,12 +16,21 @@ export function StartScreen(): VNode { <div class="column" /> <div class="column is-four-fifths"> - <div class="buttons is-right"> + <div class="buttons"> <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}> - Backup + <div class="icon"><i class="mdi mdi-arrow-up" /></div> + <span>Backup a secret</span> </button> - <button class="button is-info" onClick={() => reducer.startRecover()}>Recover</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> diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx index e2f3d521e..7568ccd69 100644 --- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx @@ -25,8 +25,11 @@ import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen'; export default { - title: 'Pages/TruthsPayingScreen', + title: 'Pages/backup/__TruthsPayingScreen', component: TestedComponent, + args: { + order: 10, + }, argTypes: { onUpdate: { action: 'onUpdate' }, onBack: { action: 'onBack' }, diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx index 319f590a0..098a8ba8d 100644 --- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx @@ -14,7 +14,7 @@ export function TruthsPayingScreen(): VNode { return ( <AnastasisClientFrame hideNext - title="Backup: Authentication Storage Payments" + title="Backup: Truths Paying" > <p> Some of the providers require a payment to store the encrypted diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx index 4cec47ec8..5cef4ee9c 100644 --- a/packages/anastasis-webui/src/pages/home/index.tsx +++ b/packages/anastasis-webui/src/pages/home/index.tsx @@ -11,10 +11,7 @@ import { VNode } from "preact"; import { - useErrorBoundary, - useLayoutEffect, - useRef -} from "preact/hooks"; + useErrorBoundary} from "preact/hooks"; import { Menu } from "../../components/menu"; import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis"; import { @@ -25,6 +22,7 @@ import { AttributeEntryScreen } from "./AttributeEntryScreen"; import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen"; import { BackupFinishedScreen } from "./BackupFinishedScreen"; import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen"; +import { ChallengePayingScreen } from "./ChallengePayingScreen"; import { ContinentSelectionScreen } from "./ContinentSelectionScreen"; import { CountrySelectionScreen } from "./CountrySelectionScreen"; import { PoliciesPayingScreen } from "./PoliciesPayingScreen"; @@ -118,9 +116,9 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode { <ErrorBanner /> {props.children} {!props.hideNav ? ( - <div> - <button onClick={() => reducer.back()}>Back</button> - {!props.hideNext ? <button onClick={next}>Next</button> : null} + <div style={{marginTop: '2em', display:'flex', justifyContent:'space-between'}}> + <button class="button" onClick={() => reducer.back()}>Back</button> + {!props.hideNext ? <button class="button is-info"onClick={next}>Next</button> : null} </div> ) : null} </div> @@ -222,7 +220,9 @@ const AnastasisClientImpl: FunctionalComponent = () => { <RecoveryFinishedScreen /> ); } - + if (state.recovery_state === RecoveryStates.ChallengePaying) { + return <ChallengePayingScreen />; + } console.log("unknown state", reducer.currentReducerState); return ( <AnastasisClientFrame hideNav title="Bug"> @@ -234,32 +234,6 @@ const AnastasisClientImpl: FunctionalComponent = () => { ); }; -interface LabeledInputProps { - label: string; - grabFocus?: boolean; - bind: [string, (x: string) => void]; -} - -export function LabeledInput(props: LabeledInputProps): VNode { - const inputRef = useRef<HTMLInputElement>(null); - useLayoutEffect(() => { - if (props.grabFocus) { - inputRef.current?.focus(); - } - }, [props.grabFocus]); - return ( - <label> - {props.label} - <input - value={props.bind[0]} - onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)} - ref={inputRef} - style={{ display: "block" }} - /> - </label> - ); -} - /** * Show a dismissible error banner if there is a current error. */ diff --git a/packages/anastasis-webui/src/scss/_custom-calendar.scss b/packages/anastasis-webui/src/scss/_custom-calendar.scss index 9ac877ce0..bff68cf79 100644 --- a/packages/anastasis-webui/src/scss/_custom-calendar.scss +++ b/packages/anastasis-webui/src/scss/_custom-calendar.scss @@ -41,6 +41,10 @@ } +.home .datePicker div { + margin-top: 0px; + margin-bottom: 0px; +} .datePicker { text-align: left; background: var(--primary-card-color); diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx index d1d861469..670e229cd 100644 --- a/packages/anastasis-webui/src/utils/index.tsx +++ b/packages/anastasis-webui/src/utils/index.tsx @@ -115,15 +115,9 @@ export const reducerStatesExample = { recoverySelectCountry: {...base, recovery_state: RecoveryStates.CountrySelecting } as ReducerState, - backupSelectCountry: {...base, - backup_state: BackupStates.CountrySelecting - } as ReducerState, recoverySelectContinent: {...base, recovery_state: RecoveryStates.ContinentSelecting, } as ReducerState, - backupSelectContinent: {...base, - backup_state: BackupStates.ContinentSelecting, - } as ReducerState, secretSelection: {...base, recovery_state: RecoveryStates.SecretSelecting, } as ReducerState, @@ -136,6 +130,18 @@ export const reducerStatesExample = { challengeSolving: {...base, recovery_state: RecoveryStates.ChallengeSolving, } as ReducerState, + challengePaying: {...base, + recovery_state: RecoveryStates.ChallengePaying, + } as ReducerState, + recoveryAttributeEditing: {...base, + recovery_state: RecoveryStates.UserAttributesCollecting + } as ReducerState, + backupSelectCountry: {...base, + backup_state: BackupStates.CountrySelecting + } as ReducerState, + backupSelectContinent: {...base, + backup_state: BackupStates.ContinentSelecting, + } as ReducerState, secretEdition: {...base, backup_state: BackupStates.SecretEditing, } as ReducerState, @@ -151,7 +157,7 @@ export const reducerStatesExample = { authEditing: {...base, backup_state: BackupStates.AuthenticationsEditing } as ReducerState, - attributeEditing: {...base, + backupAttributeEditing: {...base, backup_state: BackupStates.UserAttributesCollecting } as ReducerState, truthsPaying: {...base, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd6d7ae58..283dafaf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,7 @@ importers: bulma: ^0.9.3 bulma-checkbox: ^1.1.1 bulma-radio: ^1.1.1 + date-fns: 2.22.1 enzyme: ^3.11.0 enzyme-adapter-preact-pure: ^3.1.0 eslint: ^6.8.0 @@ -67,6 +68,7 @@ importers: dependencies: '@gnu-taler/taler-util': link:../taler-util anastasis-core: link:../anastasis-core + date-fns: 2.22.1 jed: 1.1.1 preact: 10.5.14 preact-render-to-string: 5.1.19_preact@10.5.14 @@ -8821,7 +8823,7 @@ packages: /axios/0.21.1: resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==} dependencies: - follow-redirects: 1.14.2 + follow-redirects: 1.14.2_debug@4.3.2 transitivePeerDependencies: - debug @@ -10877,6 +10879,11 @@ packages: whatwg-url: 8.7.0 dev: true + /date-fns/2.22.1: + resolution: {integrity: sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==} + engines: {node: '>=0.11'} + dev: false + /date-fns/2.23.0: resolution: {integrity: sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==} engines: {node: '>=0.11'} @@ -12618,7 +12625,7 @@ packages: readable-stream: 2.3.7 dev: true - /follow-redirects/1.14.2: + /follow-redirects/1.14.2_debug@4.3.2: resolution: {integrity: sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==} engines: {node: '>=4.0'} peerDependencies: @@ -12626,6 +12633,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.2_supports-color@6.1.0 /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -13660,7 +13669,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.14.2 + follow-redirects: 1.14.2_debug@4.3.2 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -17139,7 +17148,7 @@ packages: resolution: {integrity: sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==} engines: {node: '>=6'} dependencies: - ts-pnp: 1.2.0_typescript@3.9.10 + ts-pnp: 1.2.0_typescript@4.4.3 transitivePeerDependencies: - typescript dev: true @@ -17157,7 +17166,7 @@ packages: resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'} dependencies: - ts-pnp: 1.2.0_typescript@4.3.5 + ts-pnp: 1.2.0_typescript@4.4.3 transitivePeerDependencies: - typescript dev: true @@ -20910,7 +20919,7 @@ packages: resolution: {integrity: sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==} dev: true - /ts-pnp/1.2.0_typescript@3.9.10: + /ts-pnp/1.2.0_typescript@4.3.5: resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} engines: {node: '>=6'} peerDependencies: @@ -20919,10 +20928,10 @@ packages: typescript: optional: true dependencies: - typescript: 3.9.10 + typescript: 4.3.5 dev: true - /ts-pnp/1.2.0_typescript@4.3.5: + /ts-pnp/1.2.0_typescript@4.4.3: resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} engines: {node: '>=6'} peerDependencies: @@ -20931,7 +20940,7 @@ packages: typescript: optional: true dependencies: - typescript: 4.3.5 + typescript: 4.4.3 dev: true /tsconfig-paths/3.9.0: |