added core validators, worked on look and feel
This commit is contained in:
parent
835ac85a28
commit
21b60c8f6f
@ -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>();
|
||||
|
@ -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 {
|
||||
|
28
packages/anastasis-core/src/validators.ts
Normal file
28
packages/anastasis-core/src/validators.ts
Normal file
@ -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)
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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",
|
||||
|
63
packages/anastasis-webui/src/components/fields/DateInput.tsx
Normal file
63
packages/anastasis-webui/src/components/fields/DateInput.tsx
Normal file
@ -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>
|
||||
;
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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' : ''}>
|
||||
|
324
packages/anastasis-webui/src/components/picker/DatePicker.tsx
Normal file
324
packages/anastasis-webui/src/components/picker/DatePicker.tsx
Normal file
@ -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);
|
||||
}
|
@ -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 />
|
||||
}
|
@ -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}`;
|
||||
}
|
@ -28,14 +28,17 @@ 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',
|
||||
@ -57,7 +60,63 @@ export const WithSomeAttributes = createExample(TestedComponent, {
|
||||
}]
|
||||
} as ReducerState);
|
||||
|
||||
export const Empty = createExample(TestedComponent, {
|
||||
...reducerStatesExample.attributeEditing,
|
||||
export const Recovery = createExample(TestedComponent, {
|
||||
...reducerStatesExample.recoveryAttributeEditing,
|
||||
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 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);
|
||||
|
@ -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()
|
||||
@ -19,47 +20,104 @@ export function AttributeEntryScreen(): VNode {
|
||||
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,
|
||||
})}
|
||||
>
|
||||
<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}
|
||||
value={attrs[x.name]} />
|
||||
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>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
@ -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("");
|
||||
|
@ -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("");
|
||||
|
@ -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("");
|
||||
|
@ -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' },
|
||||
|
@ -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' },
|
||||
|
@ -20,21 +20,25 @@
|
||||
* @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' }]],
|
||||
challenges: [{
|
||||
@ -46,9 +50,10 @@ 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',
|
||||
@ -63,12 +68,18 @@ export const MoreChallenges = createExample(TestedComponent, {...reducerStatesEx
|
||||
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' }]],
|
||||
challenges: [{
|
||||
@ -76,7 +87,7 @@ export const OneBadConfiguredPolicy = createExample(TestedComponent, {...reducer
|
||||
instructions: 'just go for it',
|
||||
type: 'sasd',
|
||||
uuid: '1',
|
||||
}]
|
||||
}],
|
||||
},
|
||||
} as ReducerState);
|
||||
|
||||
|
@ -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);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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' },
|
||||
|
@ -17,11 +17,15 @@ export function CountrySelectionScreen(): VNode {
|
||||
});
|
||||
return (
|
||||
<AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} >
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{reducer.currentReducerState.countries.map((x: any) => (
|
||||
<button onClick={() => sel(x)} key={x.name}>
|
||||
<div key={x.name}>
|
||||
<button class="button" onClick={() => sel(x)} >
|
||||
{x.name} ({x.currency})
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
@ -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' },
|
||||
|
@ -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' },
|
||||
|
@ -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' },
|
||||
|
@ -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' },
|
||||
|
@ -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()
|
||||
|
@ -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' },
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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' },
|
||||
|
@ -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 {
|
||||
|
@ -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' },
|
||||
|
@ -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>
|
||||
|
@ -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' },
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -41,6 +41,10 @@
|
||||
}
|
||||
|
||||
|
||||
.home .datePicker div {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.datePicker {
|
||||
text-align: left;
|
||||
background: var(--primary-card-color);
|
||||
|
@ -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,
|
||||
|
@ -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,18 +20919,6 @@ packages:
|
||||
resolution: {integrity: sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==}
|
||||
dev: true
|
||||
|
||||
/ts-pnp/1.2.0_typescript@3.9.10:
|
||||
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
|
||||
engines: {node: '>=6'}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
typescript: 3.9.10
|
||||
dev: true
|
||||
|
||||
/ts-pnp/1.2.0_typescript@4.3.5:
|
||||
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
|
||||
engines: {node: '>=6'}
|
||||
@ -20934,6 +20931,18 @@ packages:
|
||||
typescript: 4.3.5
|
||||
dev: true
|
||||
|
||||
/ts-pnp/1.2.0_typescript@4.4.3:
|
||||
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
|
||||
engines: {node: '>=6'}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
typescript: 4.4.3
|
||||
dev: true
|
||||
|
||||
/tsconfig-paths/3.9.0:
|
||||
resolution: {integrity: sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==}
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user