added core validators, worked on look and feel

This commit is contained in:
Sebastian 2021-10-26 12:08:03 -03:00
parent 835ac85a28
commit 21b60c8f6f
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
44 changed files with 1056 additions and 144 deletions

View File

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

View File

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

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

View File

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

View File

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

View 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>
;
}

View File

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

View File

@ -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 &amp; 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' : ''}>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

@ -41,6 +41,10 @@
}
.home .datePicker div {
margin-top: 0px;
margin-bottom: 0px;
}
.datePicker {
text-align: left;
background: var(--primary-card-color);

View File

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

View File

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