This commit is contained in:
Sebastian 2021-11-10 10:20:52 -03:00
parent e03b0d1b9b
commit a62deeef5d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
92 changed files with 4214 additions and 2805 deletions

View File

@ -10,6 +10,7 @@
"devDependencies": { "devDependencies": {
"@linaria/esbuild": "^3.0.0-beta.13", "@linaria/esbuild": "^3.0.0-beta.13",
"@linaria/shaker": "^3.0.0-beta.13", "@linaria/shaker": "^3.0.0-beta.13",
"esbuild": "^0.12.29" "esbuild": "^0.12.29",
"prettier": "^2.2.1"
} }
} }

View File

@ -5,11 +5,14 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "preact build --no-sw --no-esm", "build": "preact build --no-sw --no-esm",
"serve": "sirv build --port 8080 --cors --single", "serve": "sirv build --port ${PORT:=8080} --cors --single",
"dev": "preact watch --no-sw --no-esm", "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"test": "jest ./tests", "test": "jest ./tests",
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"build-single": "preact build --no-sw --no-esm -c preact.single-config.js --dest single && sh remove-link-stylesheet.sh",
"serve-single": "sirv single --port ${PORT:=8080} --cors --single",
"pretty": "prettier --write src",
"storybook": "start-storybook -p 6006" "storybook": "start-storybook -p 6006"
}, },
"eslintConfig": { "eslintConfig": {
@ -25,6 +28,7 @@
"dependencies": { "dependencies": {
"@gnu-taler/taler-util": "workspace:^0.8.3", "@gnu-taler/taler-util": "workspace:^0.8.3",
"anastasis-core": "workspace:^0.0.1", "anastasis-core": "workspace:^0.0.1",
"base64-inline-loader": "1.1.1",
"date-fns": "2.25.0", "date-fns": "2.25.0",
"jed": "1.1.1", "jed": "1.1.1",
"preact": "^10.5.15", "preact": "^10.5.15",

View File

@ -1,5 +1,3 @@
{ {
"presets": [ "presets": ["preact-cli/babel"]
"preact-cli/babel"
]
} }

View File

@ -31,7 +31,12 @@ type Props = {
[rest: string]: any; [rest: string]: any;
}; };
export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode { export function AsyncButton({
onClick,
disabled,
children,
...rest
}: Props): VNode {
const { isLoading, request } = useAsync(onClick); const { isLoading, request } = useAsync(onClick);
// if (isSlow) { // if (isSlow) {
@ -41,9 +46,11 @@ export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VN
return <button class="button">Loading...</button>; return <button class="button">Loading...</button>;
} }
return <span data-tooltip={rest['data-tooltip']} style={{marginLeft: 5}}> return (
<span data-tooltip={rest["data-tooltip"]} style={{ marginLeft: 5 }}>
<button {...rest} onClick={request} disabled={disabled}> <button {...rest} onClick={request} disabled={disabled}>
{children} {children}
</button> </button>
</span>; </span>
);
} }

View File

@ -27,7 +27,7 @@ export interface Notification {
type: MessageType; type: MessageType;
} }
export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS' export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
interface Props { interface Props {
notifications: Notification[]; notifications: Notification[];
@ -36,24 +36,39 @@ interface Props {
function messageStyle(type: MessageType): string { function messageStyle(type: MessageType): string {
switch (type) { switch (type) {
case "INFO": return "message is-info"; case "INFO":
case "WARN": return "message is-warning"; return "message is-info";
case "ERROR": return "message is-danger"; case "WARN":
case "SUCCESS": return "message is-success"; return "message is-warning";
default: return "message" case "ERROR":
return "message is-danger";
case "SUCCESS":
return "message is-success";
default:
return "message";
} }
} }
export function Notifications({ notifications, removeNotification }: Props): VNode { export function Notifications({
return <div class="block"> notifications,
{notifications.map((n, i) => <article key={i} class={messageStyle(n.type)}> removeNotification,
}: Props): VNode {
return (
<div class="block">
{notifications.map((n, i) => (
<article key={i} class={messageStyle(n.type)}>
<div class="message-header"> <div class="message-header">
<p>{n.message}</p> <p>{n.message}</p>
{removeNotification && <button class="delete" onClick={() => removeNotification && removeNotification(n)} />} {removeNotification && (
<button
class="delete"
onClick={() => removeNotification && removeNotification(n)}
/>
)}
</div> </div>
{n.description && <div class="message-body"> {n.description && <div class="message-body">{n.description}</div>}
{n.description} </article>
</div>} ))}
</article>)}
</div> </div>
);
} }

View File

@ -21,15 +21,28 @@ import qrcode from "qrcode-generator";
export function QR({ text }: { text: string }): VNode { export function QR({ text }: { text: string }): VNode {
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const qr = qrcode(0, 'L'); const qr = qrcode(0, "L");
qr.addData(text); qr.addData(text);
qr.make(); qr.make();
if (divRef.current) divRef.current.innerHTML = qr.createSvgTag({ if (divRef.current)
divRef.current.innerHTML = qr.createSvgTag({
scalable: true, scalable: true,
}); });
}); });
return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> return (
<div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} /> <div
</div>; style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div
style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
ref={divRef}
/>
</div>
);
} }

View File

@ -1,6 +1,5 @@
import { FunctionalComponent, h } from "preact"; import { FunctionalComponent, h } from "preact";
import { TranslationProvider } from "../context/translation"; import { TranslationProvider } from "../context/translation";
import AnastasisClient from "../pages/home"; import AnastasisClient from "../pages/home";
const App: FunctionalComponent = () => { const App: FunctionalComponent = () => {

View File

@ -19,38 +19,49 @@ export function DateInput(props: DateInputProps): VNode {
inputRef.current?.focus(); inputRef.current?.focus();
} }
}, [props.grabFocus]); }, [props.grabFocus]);
const [opened, setOpened] = useState(false) const [opened, setOpened] = useState(false);
const value = props.bind[0] || ""; const value = props.bind[0] || "";
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false);
const showError = dirty && props.error const showError = dirty && props.error;
const calendar = subYears(new Date(), 30) const calendar = subYears(new Date(), 30);
return <div class="field"> return (
<div class="field">
<label class="label"> <label class="label">
{props.label} {props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}> {props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" /> <i class="mdi mdi-information" />
</span>} </span>
)}
</label> </label>
<div class="control"> <div class="control">
<div class="field has-addons"> <div class="field has-addons">
<p class="control"> <p class="control">
<input <input
type="text" type="text"
class={showError ? 'input is-danger' : 'input'} class={showError ? "input is-danger" : "input"}
value={value} value={value}
onInput={(e) => { onInput={(e) => {
const text = e.currentTarget.value const text = e.currentTarget.value;
setDirty(true) setDirty(true);
props.bind[1](text); props.bind[1](text);
}} }}
ref={inputRef} /> ref={inputRef}
/>
</p> </p>
<p class="control"> <p class="control">
<a class="button" onClick={() => { setOpened(true) }}> <a
<span class="icon"><i class="mdi mdi-calendar" /></span> class="button"
onClick={() => {
setOpened(true);
}}
>
<span class="icon">
<i class="mdi mdi-calendar" />
</span>
</a> </a>
</p> </p>
</div> </div>
@ -63,12 +74,11 @@ export function DateInput(props: DateInputProps): VNode {
years={props.years} years={props.years}
closeFunction={() => setOpened(false)} closeFunction={() => setOpened(false)}
dateReceiver={(d) => { dateReceiver={(d) => {
setDirty(true) setDirty(true);
const v = format(d, 'yyyy-MM-dd') const v = format(d, "yyyy-MM-dd");
props.bind[1](v); props.bind[1](v);
}} }}
/> />
</div> </div>
; );
} }

View File

@ -18,14 +18,17 @@ export function EmailInput(props: TextInputProps): VNode {
} }
}, [props.grabFocus]); }, [props.grabFocus]);
const value = props.bind[0]; const value = props.bind[0];
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false);
const showError = dirty && props.error const showError = dirty && props.error;
return (<div class="field"> return (
<div class="field">
<label class="label"> <label class="label">
{props.label} {props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}> {props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" /> <i class="mdi mdi-information" />
</span>} </span>
)}
</label> </label>
<div class="control has-icons-right"> <div class="control has-icons-right">
<input <input
@ -33,10 +36,14 @@ export function EmailInput(props: TextInputProps): VNode {
required required
placeholder={props.placeholder} placeholder={props.placeholder}
type="email" type="email"
class={showError ? 'input is-danger' : 'input'} class={showError ? "input is-danger" : "input"}
onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}} onInput={(e) => {
setDirty(true);
props.bind[1]((e.target as HTMLInputElement).value);
}}
ref={inputRef} ref={inputRef}
style={{ display: "block" }} /> style={{ display: "block" }}
/>
</div> </div>
{showError && <p class="help is-danger">{props.error}</p>} {showError && <p class="help is-danger">{props.error}</p>}
</div> </div>

View File

@ -22,7 +22,7 @@ import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { TextInputProps } from "./TextInput"; import { TextInputProps } from "./TextInput";
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024 const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
export function FileInput(props: TextInputProps): VNode { export function FileInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -34,48 +34,54 @@ export function FileInput(props: TextInputProps): VNode {
const value = props.bind[0]; const value = props.bind[0];
// const [dirty, setDirty] = useState(false) // const [dirty, setDirty] = useState(false)
const image = useRef<HTMLInputElement>(null) const image = useRef<HTMLInputElement>(null);
const [sizeError, setSizeError] = useState(false) const [sizeError, setSizeError] = useState(false);
function onChange(v: string): void { function onChange(v: string): void {
// setDirty(true); // setDirty(true);
props.bind[1](v); props.bind[1](v);
} }
return <div class="field"> return (
<div class="field">
<label class="label"> <label class="label">
<a onClick={() => image.current?.click()}> <a onClick={() => image.current?.click()}>{props.label}</a>
{props.label} {props.tooltip && (
</a> <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" /> <i class="mdi mdi-information" />
</span>} </span>
)}
</label> </label>
<div class="control"> <div class="control">
<input <input
ref={image} style={{ display: 'none' }} ref={image}
type="file" name={String(name)} style={{ display: "none" }}
onChange={e => { type="file"
const f: FileList | null = e.currentTarget.files name={String(name)}
onChange={(e) => {
const f: FileList | null = e.currentTarget.files;
if (!f || f.length != 1) { if (!f || f.length != 1) {
return onChange("") return onChange("");
} }
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
setSizeError(true) setSizeError(true);
return onChange("") return onChange("");
} }
setSizeError(false) setSizeError(false);
return f[0].arrayBuffer().then(b => { return f[0].arrayBuffer().then((b) => {
const b64 = btoa( const b64 = btoa(
new Uint8Array(b) new Uint8Array(b).reduce(
.reduce((data, byte) => data + String.fromCharCode(byte), '') (data, byte) => data + String.fromCharCode(byte),
) "",
return onChange(`data:${f[0].type};base64,${b64}` as any) ),
}) );
}} /> return onChange(`data:${f[0].type};base64,${b64}` as any);
});
}}
/>
{props.error && <p class="help is-danger">{props.error}</p>} {props.error && <p class="help is-danger">{props.error}</p>}
{sizeError && <p class="help is-danger"> {sizeError && (
File should be smaller than 1 MB <p class="help is-danger">File should be smaller than 1 MB</p>
</p>} )}
</div> </div>
</div> </div>
);
} }

View File

@ -23,7 +23,7 @@ import { useLayoutEffect, useRef, useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png"; import emptyImage from "../../assets/empty.png";
import { TextInputProps } from "./TextInput"; import { TextInputProps } from "./TextInput";
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024 const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
export function ImageInput(props: TextInputProps): VNode { export function ImageInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -35,47 +35,59 @@ export function ImageInput(props: TextInputProps): VNode {
const value = props.bind[0]; const value = props.bind[0];
// const [dirty, setDirty] = useState(false) // const [dirty, setDirty] = useState(false)
const image = useRef<HTMLInputElement>(null) const image = useRef<HTMLInputElement>(null);
const [sizeError, setSizeError] = useState(false) const [sizeError, setSizeError] = useState(false);
function onChange(v: string): void { function onChange(v: string): void {
// setDirty(true); // setDirty(true);
props.bind[1](v); props.bind[1](v);
} }
return <div class="field"> return (
<div class="field">
<label class="label"> <label class="label">
{props.label} {props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}> {props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" /> <i class="mdi mdi-information" />
</span>} </span>
)}
</label> </label>
<div class="control"> <div class="control">
<img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} /> <img
src={!value ? emptyImage : value}
style={{ width: 200, height: 200 }}
onClick={() => image.current?.click()}
/>
<input <input
ref={image} style={{ display: 'none' }} ref={image}
type="file" name={String(name)} style={{ display: "none" }}
onChange={e => { type="file"
const f: FileList | null = e.currentTarget.files name={String(name)}
onChange={(e) => {
const f: FileList | null = e.currentTarget.files;
if (!f || f.length != 1) { if (!f || f.length != 1) {
return onChange(emptyImage) return onChange(emptyImage);
} }
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
setSizeError(true) setSizeError(true);
return onChange(emptyImage) return onChange(emptyImage);
} }
setSizeError(false) setSizeError(false);
return f[0].arrayBuffer().then(b => { return f[0].arrayBuffer().then((b) => {
const b64 = btoa( const b64 = btoa(
new Uint8Array(b) new Uint8Array(b).reduce(
.reduce((data, byte) => data + String.fromCharCode(byte), '') (data, byte) => data + String.fromCharCode(byte),
) "",
return onChange(`data:${f[0].type};base64,${b64}` as any) ),
}) );
}} /> return onChange(`data:${f[0].type};base64,${b64}` as any);
});
}}
/>
{props.error && <p class="help is-danger">{props.error}</p>} {props.error && <p class="help is-danger">{props.error}</p>}
{sizeError && <p class="help is-danger"> {sizeError && (
Image should be smaller than 1 MB <p class="help is-danger">Image should be smaller than 1 MB</p>
</p>} )}
</div> </div>
</div> </div>
);
} }

View File

@ -18,23 +18,30 @@ export function TextInput(props: TextInputProps): VNode {
} }
}, [props.grabFocus]); }, [props.grabFocus]);
const value = props.bind[0]; const value = props.bind[0];
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false);
const showError = dirty && props.error const showError = dirty && props.error;
return (<div class="field"> return (
<div class="field">
<label class="label"> <label class="label">
{props.label} {props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}> {props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" /> <i class="mdi mdi-information" />
</span>} </span>
)}
</label> </label>
<div class="control has-icons-right"> <div class="control has-icons-right">
<input <input
value={value} value={value}
placeholder={props.placeholder} placeholder={props.placeholder}
class={showError ? 'input is-danger' : 'input'} class={showError ? "input is-danger" : "input"}
onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}} onInput={(e) => {
setDirty(true);
props.bind[1]((e.target as HTMLInputElement).value);
}}
ref={inputRef} ref={inputRef}
style={{ display: "block" }} /> style={{ display: "block" }}
/>
</div> </div>
{showError && <p class="help is-danger">{props.error}</p>} {showError && <p class="help is-danger">{props.error}</p>}
</div> </div>

View File

@ -21,38 +21,42 @@
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import langIcon from '../../assets/icons/languageicon.svg'; import langIcon from "../../assets/icons/languageicon.svg";
import { useTranslationContext } from "../../context/translation"; import { useTranslationContext } from "../../context/translation";
import { strings as messages } from '../../i18n/strings' import { strings as messages } from "../../i18n/strings";
type LangsNames = { type LangsNames = {
[P in keyof typeof messages]: string [P in keyof typeof messages]: string;
} };
const names: LangsNames = { const names: LangsNames = {
es: 'Español [es]', es: "Español [es]",
en: 'English [en]', en: "English [en]",
fr: 'Français [fr]', fr: "Français [fr]",
de: 'Deutsch [de]', de: "Deutsch [de]",
sv: 'Svenska [sv]', sv: "Svenska [sv]",
it: 'Italiano [it]', it: "Italiano [it]",
} };
function getLangName(s: keyof LangsNames | string): string { function getLangName(s: keyof LangsNames | string): string {
if (names[s]) return names[s] if (names[s]) return names[s];
return String(s) return String(s);
} }
export function LangSelector(): VNode { export function LangSelector(): VNode {
const [updatingLang, setUpdatingLang] = useState(false) const [updatingLang, setUpdatingLang] = useState(false);
const { lang, changeLanguage } = useTranslationContext() const { lang, changeLanguage } = useTranslationContext();
return <div class="dropdown is-active "> return (
<div class="dropdown is-active ">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button has-tooltip-left" <button
class="button has-tooltip-left"
data-tooltip="change language selection" data-tooltip="change language selection"
aria-haspopup="true" aria-haspopup="true"
aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}> aria-controls="dropdown-menu"
onClick={() => setUpdatingLang(!updatingLang)}
>
<div class="icon is-small is-left"> <div class="icon is-small is-left">
<img src={langIcon} /> <img src={langIcon} />
</div> </div>
@ -62,12 +66,27 @@ export function LangSelector(): VNode {
</div> </div>
</button> </button>
</div> </div>
{updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu"> {updatingLang && (
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content"> <div class="dropdown-content">
{Object.keys(messages) {Object.keys(messages)
.filter((l) => l !== lang) .filter((l) => l !== lang)
.map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)} .map((l) => (
<a
key={l}
class="dropdown-item"
value={l}
onClick={() => {
changeLanguage(l);
setUpdatingLang(false);
}}
>
{getLangName(l)}
</a>
))}
</div> </div>
</div>}
</div> </div>
)}
</div>
);
} }

View File

@ -19,12 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { Fragment, h, VNode } from "preact";
import { Fragment, h, VNode } from 'preact'; import { BackupStates, RecoveryStates } from "../../../../anastasis-core/lib";
import { BackupStates, RecoveryStates } from '../../../../anastasis-core/lib'; import { useAnastasisContext } from "../../context/anastasis";
import { useAnastasisContext } from '../../context/anastasis'; import { Translate } from "../../i18n";
import { Translate } from '../../i18n'; import { LangSelector } from "./LangSelector";
import { LangSelector } from './LangSelector';
interface Props { interface Props {
mobile?: boolean; mobile?: boolean;
@ -32,10 +31,10 @@ interface Props {
export function Sidebar({ mobile }: Props): VNode { export function Sidebar({ mobile }: Props): VNode {
// const config = useConfigContext(); // const config = useConfigContext();
const config = { version: 'none' } const config = { version: "none" };
// FIXME: add replacement for __VERSION__ with the current version // FIXME: add replacement for __VERSION__ with the current version
const process = { env: { __VERSION__: '0.0.0' } } const process = { env: { __VERSION__: "0.0.0" } };
const reducer = useAnastasisContext()! const reducer = useAnastasisContext()!;
return ( return (
<aside class="aside is-placed-left is-expanded"> <aside class="aside is-placed-left is-expanded">
@ -44,54 +43,106 @@ export function Sidebar({ mobile }: Props): VNode {
</div>} */} </div>} */}
<div class="aside-tools"> <div class="aside-tools">
<div class="aside-tools-label"> <div class="aside-tools-label">
<div><b>Anastasis</b></div> <div>
<div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}> <b>Anastasis</b>
</div>
<div
class="is-size-7 has-text-right"
style={{ lineHeight: 0, marginTop: -10 }}
>
Version {process.env.__VERSION__} ({config.version}) Version {process.env.__VERSION__} ({config.version})
</div> </div>
</div> </div>
</div> </div>
<div class="menu is-menu-main"> <div class="menu is-menu-main">
{!reducer.currentReducerState && {!reducer.currentReducerState && (
<p class="menu-label"> <p class="menu-label">
<Translate>Backup or Recorver</Translate> <Translate>Backup or Recorver</Translate>
</p> </p>
} )}
<ul class="menu-list"> <ul class="menu-list">
{!reducer.currentReducerState && {!reducer.currentReducerState && (
<li> <li>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Select one option</Translate></span> <span class="menu-item-label">
<Translate>Select one option</Translate>
</span>
</div> </div>
</li> </li>
)}
{reducer.currentReducerState &&
reducer.currentReducerState.backup_state ? (
<Fragment>
<li
class={
reducer.currentReducerState.backup_state ===
BackupStates.ContinentSelecting ||
reducer.currentReducerState.backup_state ===
BackupStates.CountrySelecting
? "is-active"
: ""
} }
{reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment> >
<li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Location</Translate></span> <span class="menu-item-label">
<Translate>Location</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.backup_state ===
BackupStates.UserAttributesCollecting
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Personal information</Translate></span> <span class="menu-item-label">
<Translate>Personal information</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.backup_state ===
BackupStates.AuthenticationsEditing
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label">
<span class="menu-item-label"><Translate>Authorization methods</Translate></span> <Translate>Authorization methods</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.backup_state ===
BackupStates.PoliciesReviewing
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label">
<span class="menu-item-label"><Translate>Policies</Translate></span> <Translate>Policies</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.backup_state ===
BackupStates.SecretEditing
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label">
<span class="menu-item-label"><Translate>Secret input</Translate></span> <Translate>Secret input</Translate>
</span>
</div> </div>
</li> </li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}> {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
@ -100,10 +151,18 @@ export function Sidebar({ mobile }: Props): VNode {
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span> <span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div> </div>
</li> */} </li> */}
<li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.backup_state ===
BackupStates.BackupFinished
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label">
<span class="menu-item-label"><Translate>Backup completed</Translate></span> <Translate>Backup completed</Translate>
</span>
</div> </div>
</li> </li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}> {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
@ -112,46 +171,107 @@ export function Sidebar({ mobile }: Props): VNode {
<span class="menu-item-label"><Translate>Truth Paying</Translate></span> <span class="menu-item-label"><Translate>Truth Paying</Translate></span>
</div> </div>
</li> */} </li> */}
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment> </Fragment>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting || ) : (
reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}> reducer.currentReducerState &&
reducer.currentReducerState?.recovery_state && (
<Fragment>
<li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.ContinentSelecting ||
reducer.currentReducerState.recovery_state ===
RecoveryStates.CountrySelecting
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Location</Translate></span> <span class="menu-item-label">
<Translate>Location</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.UserAttributesCollecting
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Personal information</Translate></span> <span class="menu-item-label">
<Translate>Personal information</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.SecretSelecting
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Secret selection</Translate></span> <span class="menu-item-label">
<Translate>Secret selection</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting || <li
reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}> class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.ChallengeSelecting ||
reducer.currentReducerState.recovery_state ===
RecoveryStates.ChallengeSolving
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Solve Challenges</Translate></span> <span class="menu-item-label">
<Translate>Solve Challenges</Translate>
</span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}> <li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.RecoveryFinished
? "is-active"
: ""
}
>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Secret recovered</Translate></span> <span class="menu-item-label">
<Translate>Secret recovered</Translate>
</span>
</div> </div>
</li> </li>
</Fragment>)} </Fragment>
{reducer.currentReducerState && )
)}
{reducer.currentReducerState && (
<li> <li>
<div class="buttons ml-4"> <div class="buttons ml-4">
<button class="button is-danger is-right" onClick={() => reducer.reset()}>Reset session</button> <button
class="button is-danger is-right"
onClick={() => reducer.reset()}
>
Reset session
</button>
</div> </div>
</li> </li>
} )}
{/* <li>
<div class="buttons ml-4">
<button class="button is-info is-right" >Manage providers</button>
</div>
</li> */}
</ul> </ul>
</div> </div>
</aside> </aside>
); );
} }

View File

@ -34,54 +34,44 @@ interface State {
selectYearMode: boolean; selectYearMode: boolean;
currentDate: Date; currentDate: Date;
} }
const now = new Date() const now = new Date();
const monthArrShortFull = [ const monthArrShortFull = [
'January', "January",
'February', "February",
'March', "March",
'April', "April",
'May', "May",
'June', "June",
'July', "July",
'August', "August",
'September', "September",
'October', "October",
'November', "November",
'December' "December",
] ];
const monthArrShort = [ const monthArrShort = [
'Jan', "Jan",
'Feb', "Feb",
'Mar', "Mar",
'Apr', "Apr",
'May', "May",
'Jun', "Jun",
'Jul', "Jul",
'Aug', "Aug",
'Sep', "Sep",
'Oct', "Oct",
'Nov', "Nov",
'Dec' "Dec",
] ];
const dayArr = [ const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat'
]
const yearArr: number[] = []
const yearArr: number[] = [];
// inspired by https://codepen.io/m4r1vs/pen/MOOxyE // inspired by https://codepen.io/m4r1vs/pen/MOOxyE
export class DatePicker extends Component<Props, State> { export class DatePicker extends Component<Props, State> {
closeDatePicker() { closeDatePicker() {
this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
} }
@ -91,17 +81,16 @@ export class DatePicker extends Component<Props, State> {
* @param {object} e The event thrown by the <span /> element clicked * @param {object} e The event thrown by the <span /> element clicked
*/ */
dayClicked(e: any) { dayClicked(e: any) {
const element = e.target; // the actual element clicked const element = e.target; // the actual element clicked
if (element.innerHTML === '') return false; // don't continue if <span /> empty if (element.innerHTML === "") return false; // don't continue if <span /> empty
// get date from clicked element (gets attached when rendered) // get date from clicked element (gets attached when rendered)
const date = new Date(element.getAttribute('data-value')); const date = new Date(element.getAttribute("data-value"));
// update the state // update the state
this.setState({ currentDate: date }); this.setState({ currentDate: date });
this.passDateToParent(date) this.passDateToParent(date);
} }
/** /**
@ -110,7 +99,6 @@ export class DatePicker extends Component<Props, State> {
* @param {number} year the year to display * @param {number} year the year to display
*/ */
getDaysByMonth(month: number, year: number) { getDaysByMonth(month: number, year: number) {
const calendar = []; const calendar = [];
const date = new Date(year, month, 1); // month to display const date = new Date(year, month, 1); // month to display
@ -122,15 +110,17 @@ export class DatePicker extends Component<Props, State> {
// the calendar is 7*6 fields big, so 42 loops // the calendar is 7*6 fields big, so 42 loops
for (let i = 0; i < 42; i++) { for (let i = 0; i < 42; i++) {
if (i >= firstDay && day !== null) day = day + 1; if (i >= firstDay && day !== null) day = day + 1;
if (day !== null && day > lastDate) day = null; if (day !== null && day > lastDate) day = null;
// append the calendar Array // append the calendar Array
calendar.push({ calendar.push({
day: (day === 0 || day === null) ? null : day, // null or number day: day === 0 || day === null ? null : day, // null or number
date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date() 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 today:
day === now.getDate() &&
month === now.getMonth() &&
year === now.getFullYear(), // boolean
}); });
} }
@ -144,12 +134,11 @@ export class DatePicker extends Component<Props, State> {
if (this.state.displayedMonth <= 0) { if (this.state.displayedMonth <= 0) {
this.setState({ this.setState({
displayedMonth: 11, displayedMonth: 11,
displayedYear: this.state.displayedYear - 1 displayedYear: this.state.displayedYear - 1,
}); });
} } else {
else {
this.setState({ this.setState({
displayedMonth: this.state.displayedMonth - 1 displayedMonth: this.state.displayedMonth - 1,
}); });
} }
} }
@ -161,12 +150,11 @@ export class DatePicker extends Component<Props, State> {
if (this.state.displayedMonth >= 11) { if (this.state.displayedMonth >= 11) {
this.setState({ this.setState({
displayedMonth: 0, displayedMonth: 0,
displayedYear: this.state.displayedYear + 1 displayedYear: this.state.displayedYear + 1,
}); });
} } else {
else {
this.setState({ this.setState({
displayedMonth: this.state.displayedMonth + 1 displayedMonth: this.state.displayedMonth + 1,
}); });
} }
} }
@ -177,12 +165,11 @@ export class DatePicker extends Component<Props, State> {
displaySelectedMonth() { displaySelectedMonth() {
if (this.state.selectYearMode) { if (this.state.selectYearMode) {
this.toggleYearSelector(); this.toggleYearSelector();
} } else {
else {
if (!this.state.currentDate) return false; if (!this.state.currentDate) return false;
this.setState({ this.setState({
displayedMonth: this.state.currentDate.getMonth(), displayedMonth: this.state.currentDate.getMonth(),
displayedYear: this.state.currentDate.getFullYear() displayedYear: this.state.currentDate.getFullYear(),
}); });
} }
} }
@ -194,17 +181,21 @@ export class DatePicker extends Component<Props, State> {
changeDisplayedYear(e: any) { changeDisplayedYear(e: any) {
const element = e.target; const element = e.target;
this.toggleYearSelector(); this.toggleYearSelector();
this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 }); this.setState({
displayedYear: parseInt(element.innerHTML, 10),
displayedMonth: 0,
});
} }
/** /**
* Pass the selected date to parent when 'OK' is clicked * Pass the selected date to parent when 'OK' is clicked
*/ */
passSavedDateDateToParent() { passSavedDateDateToParent() {
this.passDateToParent(this.state.currentDate) this.passDateToParent(this.state.currentDate);
} }
passDateToParent(date: Date) { passDateToParent(date: Date) {
if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date); if (typeof this.props.dateReceiver === "function")
this.props.dateReceiver(date);
this.closeDatePicker(); this.closeDatePicker();
} }
@ -233,94 +224,133 @@ export class DatePicker extends Component<Props, State> {
currentDate: initial, currentDate: initial,
displayedMonth: initial.getMonth(), displayedMonth: initial.getMonth(),
displayedYear: initial.getFullYear(), displayedYear: initial.getFullYear(),
selectYearMode: false selectYearMode: false,
} };
} }
render() { render() {
const {
const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state; currentDate,
displayedMonth,
displayedYear,
selectYearMode,
} = this.state;
return ( return (
<div> <div>
<div class={`datePicker ${this.props.opened && "datePicker--opened"}`}> <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}>
<div class="datePicker--titles"> <div class="datePicker--titles">
<h3 style={{ <h3
color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)' style={{
}} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3> color: selectYearMode
<h2 style={{ ? "rgba(255,255,255,.87)"
color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)' : "rgba(255,255,255,.57)",
}} onClick={this.displaySelectedMonth}> }}
{dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} 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> </h2>
</div> </div>
{!selectYearMode && <nav> {!selectYearMode && (
<span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span> <nav>
<h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4> <span onClick={this.displayPrevMonth} class="icon">
<span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span> <i
</nav>} 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"> <div class="datePicker--scroll">
{!selectYearMode && (
{!selectYearMode && <div class="datePicker--calendar" > <div class="datePicker--calendar">
<div class="datePicker--dayNames"> <div class="datePicker--dayNames">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)} {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
<span key={i}>{day}</span>
))}
</div> </div>
<div onClick={this.dayClicked} class="datePicker--days"> <div onClick={this.dayClicked} class="datePicker--days">
{/* {/*
Loop through the calendar object returned by getDaysByMonth(). Loop through the calendar object returned by getDaysByMonth().
*/} */}
{this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear) {this.getDaysByMonth(
.map( this.state.displayedMonth,
day => { this.state.displayedYear,
).map((day) => {
let selected = false; let selected = false;
if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString()); if (currentDate && day.date)
selected =
currentDate.toLocaleDateString() ===
day.date.toLocaleDateString();
return (<span key={day.day} return (
class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')} <span
key={day.day}
class={
(day.today ? "datePicker--today " : "") +
(selected ? "datePicker--selected" : "")
}
disabled={!day.date} disabled={!day.date}
data-value={day.date} data-value={day.date}
> >
{day.day} {day.day}
</span>) </span>
} );
) })}
}
</div> </div>
</div>
)}
</div>} {selectYearMode && (
<div class="datePicker--selectYear">
{selectYearMode && <div class="datePicker--selectYear"> {(this.props.years || yearArr).map((year) => (
{(this.props.years || yearArr).map(year => ( <span
<span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}> key={year}
class={year === displayedYear ? "selected" : ""}
onClick={this.changeDisplayedYear}
>
{year} {year}
</span> </span>
))} ))}
</div>
</div>} )}
</div> </div>
</div> </div>
<div class="datePicker--background" onClick={this.closeDatePicker} style={{ <div
display: this.props.opened ? 'block' : 'none', class="datePicker--background"
onClick={this.closeDatePicker}
style={{
display: this.props.opened ? "block" : "none",
}} }}
/> />
</div> </div>
) );
} }
} }
for (let i = 2010; i <= now.getFullYear() + 10; i++) { for (let i = 2010; i <= now.getFullYear() + 10; i++) {
yearArr.push(i); yearArr.push(i);
} }

View File

@ -19,32 +19,37 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { h, FunctionalComponent } from 'preact'; import { h, FunctionalComponent } from "preact";
import { useState } from 'preact/hooks'; import { useState } from "preact/hooks";
import { DurationPicker as TestedComponent } from './DurationPicker'; import { DurationPicker as TestedComponent } from "./DurationPicker";
export default { export default {
title: 'Components/Picker/Duration', title: "Components/Picker/Duration",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onCreate: { action: 'onCreate' }, onCreate: { action: "onCreate" },
goBack: { action: 'goBack' }, goBack: { action: "goBack" },
} },
}; };
function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { function createExample<Props>(
const r = (args: any) => <Component {...args} /> Component: FunctionalComponent<Props>,
r.args = props props: Partial<Props>,
return r ) {
const r = (args: any) => <Component {...args} />;
r.args = props;
return r;
} }
export const Example = createExample(TestedComponent, { export const Example = createExample(TestedComponent, {
days: true, minutes: true, hours: true, seconds: true, days: true,
value: 10000000 minutes: true,
hours: true,
seconds: true,
value: 10000000,
}); });
export const WithState = () => { export const WithState = () => {
const [v,s] = useState<number>(1000000) const [v, s] = useState<number>(1000000);
return <TestedComponent value={v} onChange={s} days minutes hours seconds /> return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
} };

View File

@ -30,75 +30,123 @@ export interface Props {
seconds?: boolean; seconds?: boolean;
days?: boolean; days?: boolean;
onChange: (value: number) => void; onChange: (value: number) => void;
value: number value: number;
} }
// inspiration taken from https://github.com/flurmbo/react-duration-picker // inspiration taken from https://github.com/flurmbo/react-duration-picker
export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode { export function DurationPicker({
const ss = 1000 days,
const ms = ss * 60 hours,
const hs = ms * 60 minutes,
const ds = hs * 24 seconds,
const i18n = useTranslator() 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"> return (
{days && <DurationColumn unit={i18n`days`} max={99} <div class="rdp-picker">
{days && (
<DurationColumn
unit={i18n`days`}
max={99}
value={Math.floor(value / ds)} value={Math.floor(value / ds)}
onDecrease={value >= ds ? () => onChange(value - ds) : undefined} onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined} onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
onChange={diff => onChange(value + diff * ds)} onChange={(diff) => onChange(value + diff * ds)}
/>} />
{hours && <DurationColumn unit={i18n`hours`} max={23} min={1} )}
{hours && (
<DurationColumn
unit={i18n`hours`}
max={23}
min={1}
value={Math.floor(value / hs) % 24} value={Math.floor(value / hs) % 24}
onDecrease={value >= hs ? () => onChange(value - hs) : undefined} onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined} onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
onChange={diff => onChange(value + diff * hs)} onChange={(diff) => onChange(value + diff * hs)}
/>} />
{minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1} )}
{minutes && (
<DurationColumn
unit={i18n`minutes`}
max={59}
min={1}
value={Math.floor(value / ms) % 60} value={Math.floor(value / ms) % 60}
onDecrease={value >= ms ? () => onChange(value - ms) : undefined} onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined} onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
onChange={diff => onChange(value + diff * ms)} onChange={(diff) => onChange(value + diff * ms)}
/>} />
{seconds && <DurationColumn unit={i18n`seconds`} max={59} )}
{seconds && (
<DurationColumn
unit={i18n`seconds`}
max={59}
value={Math.floor(value / ss) % 60} value={Math.floor(value / ss) % 60}
onDecrease={value >= ss ? () => onChange(value - ss) : undefined} onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined} onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
onChange={diff => onChange(value + diff * ss)} onChange={(diff) => onChange(value + diff * ss)}
/>} />
)}
</div> </div>
);
} }
interface ColProps { interface ColProps {
unit: string, unit: string;
min?: number, min?: number;
max: number, max: number;
value: number, value: number;
onIncrease?: () => void; onIncrease?: () => void;
onDecrease?: () => void; onDecrease?: () => void;
onChange?: (diff: number) => void; onChange?: (diff: number) => void;
} }
function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) { function InputNumber({
initial,
onChange,
}: {
initial: number;
onChange: (n: number) => void;
}) {
const [value, handler] = useState<{ v: string }>({ const [value, handler] = useState<{ v: string }>({
v: toTwoDigitString(initial) v: toTwoDigitString(initial),
}) });
return <input return (
<input
value={value.v} value={value.v}
onBlur={(e) => onChange(parseInt(value.v, 10))} onBlur={(e) => onChange(parseInt(value.v, 10))}
onInput={(e) => { onInput={(e) => {
e.preventDefault() e.preventDefault();
const n = Number.parseInt(e.currentTarget.value, 10); const n = Number.parseInt(e.currentTarget.value, 10);
if (isNaN(n)) return handler({v:toTwoDigitString(initial)}) if (isNaN(n)) return handler({ v: toTwoDigitString(initial) });
return handler({v:toTwoDigitString(n)}) return handler({ v: toTwoDigitString(n) });
}} }}
style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} /> style={{
width: 50,
border: "none",
fontSize: "inherit",
background: "inherit",
}}
/>
);
} }
function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode { function DurationColumn({
unit,
const cellHeight = 35 min = 0,
max,
value,
onIncrease,
onDecrease,
onChange,
}: ColProps): VNode {
const cellHeight = 35;
return ( return (
<div class="rdp-column-container"> <div class="rdp-column-container">
<div class="rdp-masked-div"> <div class="rdp-masked-div">
@ -106,46 +154,55 @@ function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onC
<hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} /> <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
<div class="rdp-column" style={{ top: 0 }}> <div class="rdp-column" style={{ top: 0 }}>
<div class="rdp-cell" key={value - 2}> <div class="rdp-cell" key={value - 2}>
{onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }} {onDecrease && (
onClick={onDecrease}> <button
style={{ width: "100%", textAlign: "center", margin: 5 }}
onClick={onDecrease}
>
<span class="icon"> <span class="icon">
<i class="mdi mdi-chevron-up" /> <i class="mdi mdi-chevron-up" />
</span> </span>
</button>} </button>
)}
</div> </div>
<div class="rdp-cell" key={value - 1}> <div class="rdp-cell" key={value - 1}>
{value > min ? toTwoDigitString(value - 1) : ''} {value > min ? toTwoDigitString(value - 1) : ""}
</div> </div>
<div class="rdp-cell rdp-center" key={value}> <div class="rdp-cell rdp-center" key={value}>
{onChange ? {onChange ? (
<InputNumber initial={value} onChange={(n) => onChange(n - value)} /> : <InputNumber
initial={value}
onChange={(n) => onChange(n - value)}
/>
) : (
toTwoDigitString(value) toTwoDigitString(value)
} )}
<div>{unit}</div> <div>{unit}</div>
</div> </div>
<div class="rdp-cell" key={value + 1}> <div class="rdp-cell" key={value + 1}>
{value < max ? toTwoDigitString(value + 1) : ''} {value < max ? toTwoDigitString(value + 1) : ""}
</div> </div>
<div class="rdp-cell" key={value + 2}> <div class="rdp-cell" key={value + 2}>
{onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }} {onIncrease && (
onClick={onIncrease}> <button
style={{ width: "100%", textAlign: "center", margin: 5 }}
onClick={onIncrease}
>
<span class="icon"> <span class="icon">
<i class="mdi mdi-chevron-down" /> <i class="mdi mdi-chevron-down" />
</span> </span>
</button>} </button>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
function toTwoDigitString(n: number) { function toTwoDigitString(n: number) {
if (n < 10) { if (n < 10) {
return `0${n}`; return `0${n}`;

View File

@ -19,15 +19,15 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createContext, h, VNode } from 'preact'; import { createContext, h, VNode } from "preact";
import { useContext } from 'preact/hooks'; import { useContext } from "preact/hooks";
import { AnastasisReducerApi } from '../hooks/use-anastasis-reducer'; import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
type Type = AnastasisReducerApi | undefined; type Type = AnastasisReducerApi | undefined;
const initial = undefined const initial = undefined;
const Context = createContext<Type>(initial) const Context = createContext<Type>(initial);
interface Props { interface Props {
value: AnastasisReducerApi; value: AnastasisReducerApi;
@ -36,6 +36,6 @@ interface Props {
export const AnastasisProvider = ({ value, children }: Props): VNode => { export const AnastasisProvider = ({ value, children }: Props): VNode => {
return h(Context.Provider, { value, children }); return h(Context.Provider, { value, children });
} };
export const useAnastasisContext = (): Type => useContext(Context); export const useAnastasisContext = (): Type => useContext(Context);

View File

@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createContext, h, VNode } from 'preact' import { createContext, h, VNode } from "preact";
import { useContext, useEffect } from 'preact/hooks' import { useContext, useEffect } from "preact/hooks";
import { useLang } from '../hooks' import { useLang } from "../hooks";
import * as jedLib from "jed"; import * as jedLib from "jed";
import { strings } from "../i18n/strings"; import { strings } from "../i18n/strings";
@ -31,13 +31,13 @@ interface Type {
changeLanguage: (l: string) => void; changeLanguage: (l: string) => void;
} }
const initial = { const initial = {
lang: 'en', lang: "en",
handler: null, handler: null,
changeLanguage: () => { changeLanguage: () => {
// do not change anything // do not change anything
} },
} };
const Context = createContext<Type>(initial) const Context = createContext<Type>(initial);
interface Props { interface Props {
initial?: string; initial?: string;
@ -45,15 +45,22 @@ interface Props {
forceLang?: string; forceLang?: string;
} }
export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => { export const TranslationProvider = ({
const [lang, changeLanguage] = useLang(initial) initial,
children,
forceLang,
}: Props): VNode => {
const [lang, changeLanguage] = useLang(initial);
useEffect(() => { useEffect(() => {
if (forceLang) { if (forceLang) {
changeLanguage(forceLang) changeLanguage(forceLang);
}
})
const handler = new jedLib.Jed(strings[lang] || strings['en']);
return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
} }
});
const handler = new jedLib.Jed(strings[lang] || strings["en"]);
return h(Context.Provider, {
value: { lang, handler, changeLanguage },
children,
});
};
export const useTranslationContext = (): Type => useContext(Context); export const useTranslationContext = (): Type => useContext(Context);

View File

@ -2,19 +2,19 @@ declare module "*.css" {
const mapping: Record<string, string>; const mapping: Record<string, string>;
export default mapping; export default mapping;
} }
declare module '*.svg' { declare module "*.svg" {
const content: any; const content: any;
export default content; export default content;
} }
declare module '*.jpeg' { declare module "*.jpeg" {
const content: any; const content: any;
export default content; export default content;
} }
declare module '*.png' { declare module "*.png" {
const content: any; const content: any;
export default content; export default content;
} }
declare module 'jed' { declare module "jed" {
const x: any; const x: any;
export = x; export = x;
} }

View File

@ -34,36 +34,39 @@ export interface AsyncOperationApi<T> {
error: string | undefined; error: string | undefined;
} }
export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> { export function useAsync<T>(
fn?: (...args: any) => Promise<T>,
{ slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
): AsyncOperationApi<T> {
const [data, setData] = useState<T | undefined>(undefined); const [data, setData] = useState<T | undefined>(undefined);
const [isLoading, setLoading] = useState<boolean>(false); const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(undefined); const [error, setError] = useState<any>(undefined);
const [isSlow, setSlow] = useState(false) const [isSlow, setSlow] = useState(false);
const request = async (...args: any) => { const request = async (...args: any) => {
if (!fn) return; if (!fn) return;
setLoading(true); setLoading(true);
const handler = setTimeout(() => { const handler = setTimeout(() => {
setSlow(true) setSlow(true);
}, tooLong) }, tooLong);
try { try {
console.log("calling async", args) console.log("calling async", args);
const result = await fn(...args); const result = await fn(...args);
console.log("async back", result) console.log("async back", result);
setData(result); setData(result);
} catch (error) { } catch (error) {
setError(error); setError(error);
} }
setLoading(false); setLoading(false);
setSlow(false) setSlow(false);
clearTimeout(handler) clearTimeout(handler);
}; };
function cancel() { function cancel() {
// cancelPendingRequest() // cancelPendingRequest()
setLoading(false); setLoading(false);
setSlow(false) setSlow(false);
} }
return { return {
@ -72,6 +75,6 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
data, data,
isSlow, isSlow,
isLoading, isLoading,
error error,
}; };
} }

View File

@ -20,76 +20,105 @@
*/ */
import { StateUpdater, useState } from "preact/hooks"; import { StateUpdater, useState } from "preact/hooks";
export type ValueOrFunction<T> = T | ((p: T) => T) export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => { const calculateRootPath = () => {
const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/' const rootPath =
return rootPath typeof window !== undefined
} ? window.location.origin + window.location.pathname
: "/";
return rootPath;
};
export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] { export function useBackendURL(
const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath()) url?: string,
const [triedToLog, setTriedToLog] = useLocalStorage('tried-login') ): [string, boolean, StateUpdater<string>, () => void] {
const [value, setter] = useNotNullLocalStorage(
"backend-url",
url || calculateRootPath(),
);
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => { const checkedSetter = (v: ValueOrFunction<string>) => {
setTriedToLog('yes') setTriedToLog("yes");
return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, '')) return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
} };
const resetBackend = () => { const resetBackend = () => {
setTriedToLog(undefined) setTriedToLog(undefined);
} };
return [value, !!triedToLog, checkedSetter, resetBackend] return [value, !!triedToLog, checkedSetter, resetBackend];
} }
export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] { export function useBackendDefaultToken(): [
return useLocalStorage('backend-token') string | undefined,
StateUpdater<string | undefined>,
] {
return useLocalStorage("backend-token");
} }
export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] { export function useBackendInstanceToken(
const [token, setToken] = useLocalStorage(`backend-token-${id}`) id: string,
const [defaultToken, defaultSetToken] = useBackendDefaultToken() ): [string | undefined, StateUpdater<string | undefined>] {
const [token, setToken] = useLocalStorage(`backend-token-${id}`);
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token // instance named 'default' use the default token
if (id === 'default') { if (id === "default") {
return [defaultToken, defaultSetToken] return [defaultToken, defaultSetToken];
} }
return [token, setToken] return [token, setToken];
} }
export function useLang(initial?: string): [string, StateUpdater<string>] { export function useLang(initial?: string): [string, StateUpdater<string>] {
const browserLang = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined; const browserLang =
const defaultLang = (browserLang || initial || 'en').substring(0, 2) typeof window !== "undefined"
return useNotNullLocalStorage('lang-preference', defaultLang) ? navigator.language || (navigator as any).userLanguage
: undefined;
const defaultLang = (browserLang || initial || "en").substring(0, 2);
return useNotNullLocalStorage("lang-preference", defaultLang);
} }
export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] { export function useLocalStorage(
const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => { key: string,
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue; initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>(():
| string
| undefined => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
}); });
const setValue = (value?: string | ((val?: string) => string | undefined)) => { const setValue = (
setStoredValue(p => { value?: string | ((val?: string) => string | undefined),
const toStore = value instanceof Function ? value(p) : value ) => {
setStoredValue((p) => {
const toStore = value instanceof Function ? value(p) : value;
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
if (!toStore) { if (!toStore) {
window.localStorage.removeItem(key) window.localStorage.removeItem(key);
} else { } else {
window.localStorage.setItem(key, toStore); window.localStorage.setItem(key, toStore);
} }
} }
return toStore return toStore;
}) });
}; };
return [storedValue, setValue]; return [storedValue, setValue];
} }
export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] { export function useNotNullLocalStorage(
key: string,
initialValue: string,
): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => { const [storedValue, setStoredValue] = useState<string>((): string => {
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue; return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
}); });
const setValue = (value: string | ((val: string) => string)) => { const setValue = (value: string | ((val: string) => string)) => {
@ -97,7 +126,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
setStoredValue(valueToStore); setStoredValue(valueToStore);
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
if (!valueToStore) { if (!valueToStore) {
window.localStorage.removeItem(key) window.localStorage.removeItem(key);
} else { } else {
window.localStorage.setItem(key, valueToStore); window.localStorage.setItem(key, valueToStore);
} }
@ -106,5 +135,3 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
return [storedValue, setValue]; return [storedValue, setValue];
} }

View File

@ -27,18 +27,20 @@ import { useTranslationContext } from "../context/translation";
export function useTranslator() { export function useTranslator() {
const ctx = useTranslationContext(); const ctx = useTranslationContext();
const jed = ctx.handler const jed = ctx.handler;
return function str(stringSeq: TemplateStringsArray, ...values: any[]): string { return function str(
stringSeq: TemplateStringsArray,
...values: any[]
): string {
const s = toI18nString(stringSeq); const s = toI18nString(stringSeq);
if (!s) return s if (!s) return s;
const tr = jed const tr = jed
.translate(s) .translate(s)
.ifPlural(1, s) .ifPlural(1, s)
.fetch(...values); .fetch(...values);
return tr; return tr;
};
} }
}
/** /**
* Convert template strings to a msgid * Convert template strings to a msgid
@ -54,7 +56,6 @@ export function useTranslator() {
return s; return s;
} }
interface TranslateSwitchProps { interface TranslateSwitchProps {
target: number; target: number;
children: ComponentChildren; children: ComponentChildren;
@ -131,9 +132,9 @@ function getTranslatedChildren(
*/ */
export function Translate({ children }: TranslateProps): VNode { export function Translate({ children }: TranslateProps): VNode {
const s = stringifyChildren(children); const s = stringifyChildren(children);
const ctx = useTranslationContext() const ctx = useTranslationContext();
const translation: string = ctx.handler.ngettext(s, s, 1); const translation: string = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children) const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>; return <Fragment>{result}</Fragment>;
} }
@ -154,14 +155,16 @@ export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
let plural: VNode<TranslationPluralProps> | undefined; let plural: VNode<TranslationPluralProps> | undefined;
// const children = this.props.children; // const children = this.props.children;
if (children) { if (children) {
(children instanceof Array ? children : [children]).forEach((child: any) => { (children instanceof Array ? children : [children]).forEach(
(child: any) => {
if (child.type === TranslatePlural) { if (child.type === TranslatePlural) {
plural = child; plural = child;
} }
if (child.type === TranslateSingular) { if (child.type === TranslateSingular) {
singular = child; singular = child;
} }
}); },
);
} }
if (!singular || !plural) { if (!singular || !plural) {
console.error("translation not found"); console.error("translation not found");
@ -182,9 +185,12 @@ interface TranslationPluralProps {
/** /**
* See [[TranslateSwitch]]. * See [[TranslateSwitch]].
*/ */
export function TranslatePlural({ children, target }: TranslationPluralProps): VNode { export function TranslatePlural({
children,
target,
}: TranslationPluralProps): VNode {
const s = stringifyChildren(children); const s = stringifyChildren(children);
const ctx = useTranslationContext() const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, 1); const translation = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children); const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>; return <Fragment>{result}</Fragment>;
@ -193,11 +199,13 @@ export function TranslatePlural({ children, target }: TranslationPluralProps): V
/** /**
* See [[TranslateSwitch]]. * See [[TranslateSwitch]].
*/ */
export function TranslateSingular({ children, target }: TranslationPluralProps): VNode { export function TranslateSingular({
children,
target,
}: TranslationPluralProps): VNode {
const s = stringifyChildren(children); const s = stringifyChildren(children);
const ctx = useTranslationContext() const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, target); const translation = ctx.handler.ngettext(s, s, target);
const result = getTranslatedChildren(translation, children); const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>; return <Fragment>{result}</Fragment>;
} }

View File

@ -17,28 +17,28 @@
/*eslint quote-props: ["error", "consistent"]*/ /*eslint quote-props: ["error", "consistent"]*/
export const strings: { [s: string]: any } = {}; export const strings: { [s: string]: any } = {};
strings['de'] = { strings["de"] = {
"domain": "messages", domain: "messages",
"locale_data": { locale_data: {
"messages": { messages: {
"": { "": {
"domain": "messages", domain: "messages",
"plural_forms": "nplurals=2; plural=(n != 1);", plural_forms: "nplurals=2; plural=(n != 1);",
"lang": "" lang: "",
},
},
}, },
}
}
}; };
strings['en'] = { strings["en"] = {
"domain": "messages", domain: "messages",
"locale_data": { locale_data: {
"messages": { messages: {
"": { "": {
"domain": "messages", domain: "messages",
"plural_forms": "nplurals=2; plural=(n != 1);", plural_forms: "nplurals=2; plural=(n != 1);",
"lang": "" lang: "",
},
},
}, },
}
}
}; };

View File

@ -1,4 +1,4 @@
import App from './components/app'; import App from "./components/app";
import './scss/main.scss'; import "./scss/main.scss";
export default App; export default App;

View File

@ -19,20 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { AddingProviderScreen as TestedComponent } from './AddingProviderScreen'; import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen";
export default { export default {
title: 'Pages/ManageProvider', title: "Pages/ManageProvider",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 1, order: 1,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
@ -40,20 +39,31 @@ export const NewProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
} as ReducerState); } as ReducerState);
export const NewProviderWithoutProviderList = createExample(TestedComponent, { export const NewProviderWithoutProviderList = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
authentication_providers: {} authentication_providers: {},
} as ReducerState); } as ReducerState);
export const NewVideoProvider = createExample(TestedComponent, { export const NewVideoProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'video'}); } as ReducerState,
{ providerType: "video" },
);
export const NewSmsProvider = createExample(TestedComponent, { export const NewSmsProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'sms'}); } as ReducerState,
{ providerType: "sms" },
);
export const NewIBANProvider = createExample(TestedComponent, { export const NewIBANProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'iban' }); } as ReducerState,
{ providerType: "iban" },
);

View File

@ -11,185 +11,250 @@ interface Props {
onCancel: () => void; onCancel: () => void;
} }
async function testProvider(
async function testProvider(url: string, expectedMethodType?: string): Promise<void> { url: string,
expectedMethodType?: string,
): Promise<void> {
try { try {
const response = await fetch(new URL("config", url).href) const response = await fetch(new URL("config", url).href);
const json = await (response.json().catch(d => ({}))) const json = await response.json().catch((d) => ({}));
if (!("methods" in json) || !Array.isArray(json.methods)) { if (!("methods" in json) || !Array.isArray(json.methods)) {
throw Error("This provider doesn't have authentication method. Check the provider URL") throw Error(
"This provider doesn't have authentication method. Check the provider URL",
);
} }
console.log("expected", expectedMethodType) console.log("expected", expectedMethodType);
if (!expectedMethodType) { if (!expectedMethodType) {
return return;
} }
let found = false let found = false;
for (let i = 0; i < json.methods.length && !found; i++) { for (let i = 0; i < json.methods.length && !found; i++) {
found = json.methods[i].type === expectedMethodType found = json.methods[i].type === expectedMethodType;
} }
if (!found) { if (!found) {
throw Error(`This provider does not support authentication method ${expectedMethodType}`) throw Error(
`This provider does not support authentication method ${expectedMethodType}`,
);
} }
return return;
} catch (e) { } catch (e) {
console.log("error", e) console.log("error", e);
const error = e instanceof Error ? const error =
Error(`There was an error testing this provider, try another one. ${e.message}`) : e instanceof Error
Error(`There was an error testing this provider, try another one.`) ? Error(
throw error `There was an error testing this provider, try another one. ${e.message}`,
)
: Error(`There was an error testing this provider, try another one.`);
throw error;
} }
} }
export function AddingProviderScreen({ providerType, onCancel }: Props): VNode { export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
const [providerURL, setProviderURL] = useState(""); const [providerURL, setProviderURL] = useState("");
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>();
const [testing, setTesting] = useState(false) const [testing, setTesting] = useState(false);
const providerLabel = providerType ? authMethods[providerType].label : undefined const providerLabel = providerType
? authMethods[providerType].label
: undefined;
//FIXME: move this timeout logic into a hook //FIXME: move this timeout logic into a hook
const timeout = useRef<number | undefined>(undefined); const timeout = useRef<number | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (timeout) window.clearTimeout(timeout.current) if (timeout) window.clearTimeout(timeout.current);
timeout.current = window.setTimeout(async () => { timeout.current = window.setTimeout(async () => {
const url = providerURL.endsWith('/') ? providerURL : (providerURL + '/') const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
if (!providerURL || authProviders.includes(url)) return; if (!providerURL || authProviders.includes(url)) return;
try { try {
setTesting(true) setTesting(true);
await testProvider(url, providerType) await testProvider(url, providerType);
// this is use as tested but everything when ok // this is use as tested but everything when ok
// undefined will mean that the field is not dirty // undefined will mean that the field is not dirty
setError("") setError("");
} catch (e) { } catch (e) {
console.log("tuvieja", e) console.log("tuvieja", e);
if (e instanceof Error) setError(e.message) if (e instanceof Error) setError(e.message);
} }
setTesting(false) setTesting(false);
}, 200); }, 200);
}, [providerURL, reducer]) }, [providerURL, reducer]);
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || !("authentication_providers" in reducer.currentReducerState)) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
!("authentication_providers" in reducer.currentReducerState)
) {
return <div>invalid state</div>;
} }
async function addProvider(provider_url: string): Promise<void> { async function addProvider(provider_url: string): Promise<void> {
await reducer?.transition("add_provider", { provider_url }) await reducer?.transition("add_provider", { provider_url });
onCancel() onCancel();
} }
function deleteProvider(provider_url: string): void { function deleteProvider(provider_url: string): void {
reducer?.transition("delete_provider", { provider_url }) reducer?.transition("delete_provider", { provider_url });
} }
const allAuthProviders = reducer.currentReducerState.authentication_providers || {} const allAuthProviders =
const authProviders = Object.keys(allAuthProviders).filter(provUrl => { reducer.currentReducerState.authentication_providers || {};
const authProviders = Object.keys(allAuthProviders).filter((provUrl) => {
const p = allAuthProviders[provUrl]; const p = allAuthProviders[provUrl];
if (!providerLabel) { if (!providerLabel) {
return p && ("currency" in p) return p && "currency" in p;
} else { } else {
return p && ("currency" in p) && p.methods.findIndex(m => m.type === providerType) !== -1 return (
p &&
"currency" in p &&
p.methods.findIndex((m) => m.type === providerType) !== -1
);
} }
}) });
let errors = !providerURL ? 'Add provider URL' : undefined let errors = !providerURL ? "Add provider URL" : undefined;
let url: string | undefined; let url: string | undefined;
try { try {
url = new URL("",providerURL).href url = new URL("", providerURL).href;
} catch { } catch {
errors = 'Check the URL' errors = "Check the URL";
} }
if (!!error && !errors) { if (!!error && !errors) {
errors = error errors = error;
} }
if (!errors && authProviders.includes(url!)) { if (!errors && authProviders.includes(url!)) {
errors = 'That provider is already known' errors = "That provider is already known";
} }
return ( return (
<AnastasisClientFrame hideNav <AnastasisClientFrame
hideNav
title="Backup: Manage providers" title="Backup: Manage providers"
hideNext={errors}> hideNext={errors}
>
<div> <div>
{!providerLabel ? {!providerLabel ? (
<p> <p>Add a provider url</p>
Add a provider url ) : (
</p> : <p>Add a provider url for a {providerLabel} service</p>
<p> )}
Add a provider url for a {providerLabel} service
</p>
}
<div class="container"> <div class="container">
<TextInput <TextInput
label="Provider URL" label="Provider URL"
placeholder="https://provider.com" placeholder="https://provider.com"
grabFocus grabFocus
error={errors} error={errors}
bind={[providerURL, setProviderURL]} /> bind={[providerURL, setProviderURL]}
/>
</div> </div>
<p class="block"> <p class="block">Example: https://kudos.demo.anastasis.lu</p>
Example: https://kudos.demo.anastasis.lu
</p>
{testing && <p class="has-text-info">Testing</p>} {testing && <p class="has-text-info">Testing</p>}
<div class="block" style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={onCancel}>Cancel</button> class="block"
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={onCancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={error !== "" || testing} onClick={() => addProvider(url!)}>Add</button> <button
class="button is-info"
disabled={error !== "" || testing}
onClick={() => addProvider(url!)}
>
Add
</button>
</span> </span>
</div> </div>
{authProviders.length > 0 ? ( {authProviders.length > 0 ? (
!providerLabel ? !providerLabel ? (
<p class="subtitle">Current providers</p>
) : (
<p class="subtitle"> <p class="subtitle">
Current providers
</p> : <p class="subtitle">
Current providers for {providerLabel} service Current providers for {providerLabel} service
</p> </p>
)
) : !providerLabel ? (
<p class="subtitle">No known providers, add one.</p>
) : ( ) : (
!providerLabel ? <p class="subtitle"> <p class="subtitle">No known providers for {providerLabel} service</p>
No known providers, add one.
</p> : <p class="subtitle">
No known providers for {providerLabel} service
</p>
)} )}
{authProviders.map(k => { {authProviders.map((k) => {
const p = allAuthProviders[k] as AuthenticationProviderStatusOk const p = allAuthProviders[k] as AuthenticationProviderStatusOk;
return <TableRow url={k} info={p} onDelete={deleteProvider} /> return <TableRow url={k} info={p} onDelete={deleteProvider} />;
})} })}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
function TableRow({ url, info, onDelete }: { onDelete: (s: string) => void, url: string, info: AuthenticationProviderStatusOk }) { function TableRow({
const [status, setStatus] = useState("checking") url,
info,
onDelete,
}: {
onDelete: (s: string) => void;
url: string;
info: AuthenticationProviderStatusOk;
}) {
const [status, setStatus] = useState("checking");
useEffect(function () { useEffect(function () {
testProvider(url.endsWith('/') ? url.substring(0, url.length - 1) : url) testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url)
.then(function () { setStatus('responding') }) .then(function () {
.catch(function () { setStatus('failed to contact') }) setStatus("responding");
}) })
return <div class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> .catch(function () {
setStatus("failed to contact");
});
});
return (
<div
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<div> <div>
<div class="subtitle">{url}</div> <div class="subtitle">{url}</div>
<dl> <dl>
<dt><b>Business Name</b></dt> <dt>
<b>Business Name</b>
</dt>
<dd>{info.business_name}</dd> <dd>{info.business_name}</dd>
<dt><b>Supported methods</b></dt> <dt>
<dd>{info.methods.map(m => m.type).join(',')}</dd> <b>Supported methods</b>
<dt><b>Maximum storage</b></dt> </dt>
<dd>{info.methods.map((m) => m.type).join(",")}</dd>
<dt>
<b>Maximum storage</b>
</dt>
<dd>{info.storage_limit_in_megabytes} Mb</dd> <dd>{info.storage_limit_in_megabytes} Mb</dd>
<dt><b>Status</b></dt> <dt>
<b>Status</b>
</dt>
<dd>{status}</dd> <dd>{status}</dd>
</dl> </dl>
</div> </div>
<div class="block" style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}> <div
<button class="button is-danger" onClick={() => onDelete(url)}>Remove</button> class="block"
style={{
marginTop: "auto",
marginBottom: "auto",
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
}}
>
<button class="button is-danger" onClick={() => onDelete(url)}>
Remove
</button>
</div> </div>
</div> </div>
);
} }

View File

@ -19,72 +19,79 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen'; import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen";
export default { export default {
title: 'Pages/PersonalInformation', title: "Pages/PersonalInformation",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 3, order: 3,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const Backup = createExample(TestedComponent, { export const Backup = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing, ...reducerStatesExample.backupAttributeEditing,
required_attributes: [{ required_attributes: [
name: 'first name', {
label: 'first', name: "first name",
type: 'string', label: "first",
uuid: 'asdasdsa1', type: "string",
widget: 'wid', uuid: "asdasdsa1",
}, { widget: "wid",
name: 'last name', },
label: 'second', {
type: 'string', name: "last name",
uuid: 'asdasdsa2', label: "second",
widget: 'wid', type: "string",
}, { uuid: "asdasdsa2",
name: 'birthdate', widget: "wid",
label: 'birthdate', },
type: 'date', {
uuid: 'asdasdsa3', name: "birthdate",
widget: 'calendar', label: "birthdate",
}] type: "date",
uuid: "asdasdsa3",
widget: "calendar",
},
],
} as ReducerState); } as ReducerState);
export const Recovery = createExample(TestedComponent, { export const Recovery = createExample(TestedComponent, {
...reducerStatesExample.recoveryAttributeEditing, ...reducerStatesExample.recoveryAttributeEditing,
required_attributes: [{ required_attributes: [
name: 'first', {
label: 'first', name: "first",
type: 'string', label: "first",
uuid: 'asdasdsa1', type: "string",
widget: 'wid', uuid: "asdasdsa1",
}, { widget: "wid",
name: 'pepe', },
label: 'second', {
type: 'string', name: "pepe",
uuid: 'asdasdsa2', label: "second",
widget: 'wid', type: "string",
}, { uuid: "asdasdsa2",
name: 'pepe2', widget: "wid",
label: 'third', },
type: 'date', {
uuid: 'asdasdsa3', name: "pepe2",
widget: 'calendar', label: "third",
}] type: "date",
uuid: "asdasdsa3",
widget: "calendar",
},
],
} as ReducerState); } as ReducerState);
export const WithNoRequiredAttribute = createExample(TestedComponent, { export const WithNoRequiredAttribute = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing, ...reducerStatesExample.backupAttributeEditing,
required_attributes: undefined required_attributes: undefined,
} as ReducerState); } as ReducerState);
const allWidgets = [ const allWidgets = [
@ -107,23 +114,22 @@ const allWidgets = [
"anastasis_gtk_ia_tax_de", "anastasis_gtk_ia_tax_de",
"anastasis_gtk_xx_prime", "anastasis_gtk_xx_prime",
"anastasis_gtk_xx_square", "anastasis_gtk_xx_square",
] ];
function typeForWidget(name: string): string { function typeForWidget(name: string): string {
if (["anastasis_gtk_xx_prime", if (["anastasis_gtk_xx_prime", "anastasis_gtk_xx_square"].includes(name))
"anastasis_gtk_xx_square", return "number";
].includes(name)) return "number"; if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date";
if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date"
return "string"; return "string";
} }
export const WithAllPosibleWidget = createExample(TestedComponent, { export const WithAllPosibleWidget = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing, ...reducerStatesExample.backupAttributeEditing,
required_attributes: allWidgets.map(w => ({ required_attributes: allWidgets.map((w) => ({
name: w, name: w,
label: `widget: ${w}`, label: `widget: ${w}`,
type: typeForWidget(w), type: typeForWidget(w),
uuid: `uuid-${w}`, uuid: `uuid-${w}`,
widget: w widget: w,
})) })),
} as ReducerState); } as ReducerState);

View File

@ -9,24 +9,32 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index"; import { AnastasisClientFrame, withProcessLabel } from "./index";
export function AttributeEntryScreen(): VNode { export function AttributeEntryScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
const state = reducer?.currentReducerState const state = reducer?.currentReducerState;
const currentIdentityAttributes = state && "identity_attributes" in state ? (state.identity_attributes || {}) : {} const currentIdentityAttributes =
const [attrs, setAttrs] = useState<Record<string, string>>(currentIdentityAttributes); state && "identity_attributes" in state
? state.identity_attributes || {}
: {};
const [attrs, setAttrs] = useState<Record<string, string>>(
currentIdentityAttributes,
);
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
!("required_attributes" in reducer.currentReducerState)
) {
return <div>invalid state</div>;
} }
const reqAttr = reducer.currentReducerState.required_attributes || [] const reqAttr = reducer.currentReducerState.required_attributes || [];
let hasErrors = false; let hasErrors = false;
const fieldList: VNode[] = reqAttr.map((spec, i: number) => { const fieldList: VNode[] = reqAttr.map((spec, i: number) => {
const value = attrs[spec.name] const value = attrs[spec.name];
const error = checkIfValid(value, spec) const error = checkIfValid(value, spec);
hasErrors = hasErrors || error !== undefined hasErrors = hasErrors || error !== undefined;
return ( return (
<AttributeEntryField <AttributeEntryField
key={i} key={i}
@ -34,22 +42,23 @@ export function AttributeEntryScreen(): VNode {
setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })} setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
spec={spec} spec={spec}
errorMessage={error} errorMessage={error}
value={value} /> value={value}
/>
); );
}) });
return ( return (
<AnastasisClientFrame <AnastasisClientFrame
title={withProcessLabel(reducer, "Who are you?")} title={withProcessLabel(reducer, "Who are you?")}
hideNext={hasErrors ? "Complete the form." : undefined} hideNext={hasErrors ? "Complete the form." : undefined}
onNext={() => reducer.transition("enter_user_attributes", { onNext={() =>
reducer.transition("enter_user_attributes", {
identity_attributes: attrs, identity_attributes: attrs,
})} })
}
> >
<div class="columns" style={{ maxWidth: 'unset' }}> <div class="columns" style={{ maxWidth: "unset" }}>
<div class="column"> <div class="column">{fieldList}</div>
{fieldList}
</div>
<div class="column"> <div class="column">
<p>This personal information will help to locate your secret.</p> <p>This personal information will help to locate your secret.</p>
<h1 class="title">This stays private</h1> <h1 class="title">This stays private</h1>
@ -61,9 +70,12 @@ export function AttributeEntryScreen(): VNode {
</span> </span>
Will be hashed, and therefore unreadable Will be hashed, and therefore unreadable
</li> </li>
<li><span class="icon is-right"> <li>
<span class="icon is-right">
<i class="mdi mdi-circle-small" /> <i class="mdi mdi-circle-small" />
</span>The non-hashed version is not shared</li> </span>
The non-hashed version is not shared
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -78,22 +90,22 @@ interface AttributeEntryFieldProps {
spec: UserAttributeSpec; spec: UserAttributeSpec;
errorMessage: string | undefined; errorMessage: string | undefined;
} }
const possibleBirthdayYear: Array<number> = [] const possibleBirthdayYear: Array<number> = [];
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
possibleBirthdayYear.push(2020 - i) possibleBirthdayYear.push(2020 - i);
} }
function AttributeEntryField(props: AttributeEntryFieldProps): VNode { function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
return ( return (
<div> <div>
{props.spec.type === 'date' && {props.spec.type === "date" &&
<DateInput <DateInput
grabFocus={props.isFirst} grabFocus={props.isFirst}
label={props.spec.label} label={props.spec.label}
years={possibleBirthdayYear} years={possibleBirthdayYear}
error={props.errorMessage} error={props.errorMessage}
bind={[props.value, props.setValue]} bind={[props.value, props.setValue]}
/>} />
}
{props.spec.type === 'number' && {props.spec.type === 'number' &&
<PhoneNumberInput <PhoneNumberInput
grabFocus={props.isFirst} grabFocus={props.isFirst}
@ -102,14 +114,14 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
bind={[props.value, props.setValue]} bind={[props.value, props.setValue]}
/> />
} }
{props.spec.type === 'string' && {props.spec.type === "string" && (
<TextInput <TextInput
grabFocus={props.isFirst} grabFocus={props.isFirst}
label={props.spec.label} label={props.spec.label}
error={props.errorMessage} error={props.errorMessage}
bind={[props.value, props.setValue]} bind={[props.value, props.setValue]}
/> />
} )}
<div class="block"> <div class="block">
This stays private This stays private
<span class="icon is-right"> <span class="icon is-right">
@ -119,40 +131,43 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
</div> </div>
); );
} }
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/ const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/;
function checkIfValid(
function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined { value: string,
const pattern = spec['validation-regex'] spec: UserAttributeSpec,
): string | undefined {
const pattern = spec["validation-regex"];
if (pattern) { if (pattern) {
const re = new RegExp(pattern) const re = new RegExp(pattern);
if (!re.test(value)) return 'The value is invalid' if (!re.test(value)) return "The value is invalid";
} }
const logic = spec['validation-logic'] const logic = spec["validation-logic"];
if (logic) { if (logic) {
const func = (validators as any)[logic]; const func = (validators as any)[logic];
if (func && typeof func === 'function' && !func(value)) return 'Please check the value' if (func && typeof func === "function" && !func(value))
return "Please check the value";
} }
const optional = spec.optional const optional = spec.optional;
if (!optional && !value) { if (!optional && !value) {
return 'This value is required' return "This value is required";
} }
if ("date" === spec.type) { if ("date" === spec.type) {
if (!YEAR_REGEX.test(value)) { if (!YEAR_REGEX.test(value)) {
return "The date doesn't follow the format" return "The date doesn't follow the format";
} }
try { try {
const v = parse(value, 'yyyy-MM-dd', new Date()); const v = parse(value, "yyyy-MM-dd", new Date());
if (Number.isNaN(v.getTime())) { if (Number.isNaN(v.getTime())) {
return "Some numeric values seems out of range for a date" return "Some numeric values seems out of range for a date";
} }
if ("birthdate" === spec.name && isAfter(v, new Date())) { if ("birthdate" === spec.name && isAfter(v, new Date())) {
return "A birthdate cannot be in the future" return "A birthdate cannot be in the future";
} }
} catch (e) { } catch (e) {
return "Could not parse the date" return "Could not parse the date";
} }
} }
return undefined return undefined;
} }

View File

@ -19,69 +19,80 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen'; import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen";
export default { export default {
title: 'Pages/backup/AuthorizationMethod', title: "Pages/backup/AuthorizationMethod",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 4, order: 4,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const InitialState = createExample(TestedComponent, reducerStatesExample.authEditing); export const InitialState = createExample(
TestedComponent,
reducerStatesExample.authEditing,
);
export const OneAuthMethodConfigured = createExample(TestedComponent, { export const OneAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
authentication_methods: [{ authentication_methods: [
type: 'question', {
instructions: 'what time is it?', type: "question",
challenge: 'asd', instructions: "what time is it?",
}] challenge: "asd",
},
],
} as ReducerState); } as ReducerState);
export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, { export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
authentication_methods: [{ authentication_methods: [
type: 'question', {
instructions: 'what time is it?', type: "question",
challenge: 'asd', instructions: "what time is it?",
},{ challenge: "asd",
type: 'question', },
instructions: 'what time is it?', {
challenge: 'qwe', type: "question",
},{ instructions: "what time is it?",
type: 'sms', challenge: "qwe",
instructions: 'what time is it?', },
challenge: 'asd', {
},{ type: "sms",
type: 'email', instructions: "what time is it?",
instructions: 'what time is it?', challenge: "asd",
challenge: 'asd', },
},{ {
type: 'email', type: "email",
instructions: 'what time is it?', instructions: "what time is it?",
challenge: 'asd', challenge: "asd",
},{ },
type: 'email', {
instructions: 'what time is it?', type: "email",
challenge: 'asd', instructions: "what time is it?",
},{ challenge: "asd",
type: 'email', },
instructions: 'what time is it?', {
challenge: 'asd', type: "email",
}] instructions: "what time is it?",
challenge: "asd",
},
{
type: "email",
instructions: "what time is it?",
challenge: "asd",
},
],
} as ReducerState); } as ReducerState);
export const NoAuthMethodProvided = createExample(TestedComponent, { export const NoAuthMethodProvided = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
authentication_providers: {}, authentication_providers: {},
authentication_methods: [] authentication_methods: [],
} as ReducerState); } as ReducerState);

View File

@ -20,7 +20,9 @@ export function AuthenticationEditorScreen(): VNode {
KnownAuthMethods | undefined KnownAuthMethods | undefined
>(undefined); >(undefined);
const [tooFewAuths, setTooFewAuths] = useState(false); const [tooFewAuths, setTooFewAuths] = useState(false);
const [manageProvider, setManageProvider] = useState<string | undefined>(undefined) const [manageProvider, setManageProvider] = useState<string | undefined>(
undefined,
);
// const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined) // const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
@ -68,11 +70,14 @@ export function AuthenticationEditorScreen(): VNode {
} }
if (manageProvider !== undefined) { if (manageProvider !== undefined) {
return (
return <AddingProviderScreen <AddingProviderScreen
onCancel={() => setManageProvider(undefined)} onCancel={() => setManageProvider(undefined)}
providerType={isKnownAuthMethods(manageProvider) ? manageProvider : undefined} providerType={
isKnownAuthMethods(manageProvider) ? manageProvider : undefined
}
/> />
);
} }
if (selectedMethod) { if (selectedMethod) {
@ -100,7 +105,7 @@ export function AuthenticationEditorScreen(): VNode {
description="No providers founds" description="No providers founds"
label="Add a provider manually" label="Add a provider manually"
onConfirm={() => { onConfirm={() => {
setManageProvider(selectedMethod) setManageProvider(selectedMethod);
}} }}
> >
<p> <p>
@ -193,7 +198,7 @@ export function AuthenticationEditorScreen(): VNode {
description="No providers founds" description="No providers founds"
label="Add a provider manually" label="Add a provider manually"
onConfirm={() => { onConfirm={() => {
setManageProvider("") setManageProvider("");
}} }}
> >
<p> <p>
@ -214,7 +219,10 @@ export function AuthenticationEditorScreen(): VNode {
authentication method is defined by the backup provider list. authentication method is defined by the backup provider list.
</p> </p>
<p class="block"> <p class="block">
<button class="button is-info" onClick={() => setManageProvider("")}> <button
class="button is-info"
onClick={() => setManageProvider("")}
>
Manage backup providers Manage backup providers
</button> </button>
</p> </p>

View File

@ -19,44 +19,47 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen'; import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen";
export default { export default {
title: 'Pages/backup/Finished', title: "Pages/backup/Finished",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 8, order: 8,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished); export const WithoutName = createExample(
TestedComponent,
reducerStatesExample.backupFinished,
);
export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished, export const WithName = createExample(TestedComponent, {
secret_name: 'super_secret', ...reducerStatesExample.backupFinished,
secret_name: "super_secret",
} as ReducerState); } as ReducerState);
export const WithDetails = createExample(TestedComponent, { export const WithDetails = createExample(TestedComponent, {
...reducerStatesExample.backupFinished, ...reducerStatesExample.backupFinished,
secret_name: 'super_secret', secret_name: "super_secret",
success_details: { success_details: {
'http://anastasis.net': { "http://anastasis.net": {
policy_expiration: { policy_expiration: {
t_ms: 'never' t_ms: "never",
}, },
policy_version: 0 policy_version: 0,
}, },
'http://taler.net': { "http://taler.net": {
policy_expiration: { policy_expiration: {
t_ms: new Date().getTime() + 60*60*24*1000 t_ms: new Date().getTime() + 60 * 60 * 24 * 1000,
},
policy_version: 1,
}, },
policy_version: 1
}, },
}
} as ReducerState); } as ReducerState);

View File

@ -4,25 +4,31 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function BackupFinishedScreen(): VNode { export function BackupFinishedScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
} }
const details = reducer.currentReducerState.success_details const details = reducer.currentReducerState.success_details;
return (<AnastasisClientFrame hideNav title="Backup finished"> return (
{reducer.currentReducerState.secret_name ? <p> <AnastasisClientFrame hideNav title="Backup finished">
Your backup of secret <b>"{reducer.currentReducerState.secret_name}"</b> was {reducer.currentReducerState.secret_name ? (
successful.
</p> :
<p> <p>
Your secret was successfully backed up. Your backup of secret{" "}
</p>} <b>"{reducer.currentReducerState.secret_name}"</b> was successful.
</p>
) : (
<p>Your secret was successfully backed up.</p>
)}
{details && <div class="block"> {details && (
<div class="block">
<p>The backup is stored by the following providers:</p> <p>The backup is stored by the following providers:</p>
{Object.keys(details).map((x, i) => { {Object.keys(details).map((x, i) => {
const sd = details[x]; const sd = details[x];
@ -31,14 +37,29 @@ export function BackupFinishedScreen(): VNode {
{x} {x}
<p> <p>
version {sd.policy_version} version {sd.policy_version}
{sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'} {sd.policy_expiration.t_ms !== "never"
? ` expires at: ${format(
sd.policy_expiration.t_ms,
"dd-MM-yyyy",
)}`
: " without expiration date"}
</p> </p>
</div> </div>
); );
})} })}
</div>}
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div> </div>
</AnastasisClientFrame>); )}
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
} }

View File

@ -19,7 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from "anastasis-core"; import {
ChallengeFeedbackStatus,
RecoveryStates,
ReducerState,
} from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils"; import { createExample, reducerStatesExample } from "../../utils";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen"; import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
@ -247,20 +251,20 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
"uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() }, "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
"uuid-2": { "uuid-2": {
state: ChallengeFeedbackStatus.Message.toString(), state: ChallengeFeedbackStatus.Message.toString(),
message: 'Challenge should be solved' message: "Challenge should be solved",
}, },
"uuid-3": { "uuid-3": {
state: ChallengeFeedbackStatus.AuthIban.toString(), state: ChallengeFeedbackStatus.AuthIban.toString(),
challenge_amount: "EUR:1", challenge_amount: "EUR:1",
credit_iban: "DE12345789000", credit_iban: "DE12345789000",
business_name: "Data Loss Incorporated", business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321" wire_transfer_subject: "Anastasis 987654321",
}, },
"uuid-4": { "uuid-4": {
state: ChallengeFeedbackStatus.Payment.toString(), state: ChallengeFeedbackStatus.Payment.toString(),
taler_pay_uri: "taler://pay/...", taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/", provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG" payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
}, },
"uuid-5": { "uuid-5": {
state: ChallengeFeedbackStatus.RateLimitExceeded.toString(), state: ChallengeFeedbackStatus.RateLimitExceeded.toString(),
@ -269,7 +273,7 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
"uuid-6": { "uuid-6": {
state: ChallengeFeedbackStatus.Redirect.toString(), state: ChallengeFeedbackStatus.Redirect.toString(),
redirect_url: "https://videoconf.example.com/", redirect_url: "https://videoconf.example.com/",
http_status: 303 http_status: 303,
}, },
"uuid-7": { "uuid-7": {
state: ChallengeFeedbackStatus.ServerFailure.toString(), state: ChallengeFeedbackStatus.ServerFailure.toString(),

View File

@ -11,23 +11,34 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
} }
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.Message: case ChallengeFeedbackStatus.Message:
return ( return <div class="block has-text-danger">{feedback.message}</div>;
<div class="block has-text-danger">{feedback.message}</div>
);
case ChallengeFeedbackStatus.Solved: case ChallengeFeedbackStatus.Solved:
return <div /> return <div />;
case ChallengeFeedbackStatus.Pending: case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.Solved:
case ChallengeFeedbackStatus.AuthIban: case ChallengeFeedbackStatus.AuthIban:
return null; return null;
case ChallengeFeedbackStatus.ServerFailure: case ChallengeFeedbackStatus.ServerFailure:
return <div class="block has-text-danger">Server error.</div>; return <div class="block has-text-danger">Server error.</div>;
case ChallengeFeedbackStatus.RateLimitExceeded: case ChallengeFeedbackStatus.RateLimitExceeded:
return <div class="block has-text-danger">There were to many failed attempts.</div>; return (
<div class="block has-text-danger">
There were to many failed attempts.
</div>
);
case ChallengeFeedbackStatus.Unsupported: case ChallengeFeedbackStatus.Unsupported:
return <div class="block has-text-danger">This client doesn't support solving this type of challenge. Use another version or contact the provider.</div>; return (
<div class="block has-text-danger">
This client doesn't support solving this type of challenge. Use
another version or contact the provider.
</div>
);
case ChallengeFeedbackStatus.TruthUnknown: case ChallengeFeedbackStatus.TruthUnknown:
return <div class="block has-text-danger">Provider doesn't recognize the challenge of the policy. Contact the provider for further information.</div>; return (
<div class="block has-text-danger">
Provider doesn't recognize the challenge of the policy. Contact the
provider for further information.
</div>
);
case ChallengeFeedbackStatus.Redirect: case ChallengeFeedbackStatus.Redirect:
default: default:
return <div />; return <div />;
@ -70,7 +81,8 @@ export function ChallengeOverviewScreen(): VNode {
feedback: challengeFeedback[ch.uuid], feedback: challengeFeedback[ch.uuid],
}; };
} }
const policiesWithInfo = policies.map((row) => { const policiesWithInfo = policies
.map((row) => {
let isPolicySolved = true; let isPolicySolved = true;
const challenges = row const challenges = row
.map(({ uuid }) => { .map(({ uuid }) => {
@ -81,8 +93,13 @@ export function ChallengeOverviewScreen(): VNode {
}) })
.filter((ch) => ch.info !== undefined); .filter((ch) => ch.info !== undefined);
return { isPolicySolved, challenges }; return {
}); isPolicySolved,
challenges,
corrupted: row.length > challenges.length,
};
})
.filter((p) => !p.corrupted);
const atLeastThereIsOnePolicySolved = const atLeastThereIsOnePolicySolved =
policiesWithInfo.find((p) => p.isPolicySolved) !== undefined; policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
@ -92,19 +109,19 @@ export function ChallengeOverviewScreen(): VNode {
: undefined; : undefined;
return ( return (
<AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges"> <AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
{!policies.length ? ( {!policiesWithInfo.length ? (
<p class="block"> <p class="block">
No policies found, try with another version of the secret No policies found, try with another version of the secret
</p> </p>
) : policies.length === 1 ? ( ) : policiesWithInfo.length === 1 ? (
<p class="block"> <p class="block">
One policy found for this secret. You need to solve all the challenges One policy found for this secret. You need to solve all the challenges
in order to recover your secret. in order to recover your secret.
</p> </p>
) : ( ) : (
<p class="block"> <p class="block">
We have found {policies.length} polices. You need to solve all the We have found {policiesWithInfo.length} polices. You need to solve all
challenges from one policy in order to recover your secret. the challenges from one policy in order to recover your secret.
</p> </p>
)} )}
{policiesWithInfo.map((policy, policy_index) => { {policiesWithInfo.map((policy, policy_index) => {
@ -113,7 +130,8 @@ export function ChallengeOverviewScreen(): VNode {
const method = authMethods[info.type as KnownAuthMethods]; const method = authMethods[info.type as KnownAuthMethods];
if (!method) { if (!method) {
return <div return (
<div
key={uuid} key={uuid}
class="block" class="block"
style={{ display: "flex", justifyContent: "space-between" }} style={{ display: "flex", justifyContent: "space-between" }}
@ -121,66 +139,91 @@ export function ChallengeOverviewScreen(): VNode {
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<span>unknown challenge</span> <span>unknown challenge</span>
</div> </div>
</div> </div>
);
} }
function ChallengeButton({ id, feedback }: { id: string; feedback?: ChallengeFeedback }): VNode { function ChallengeButton({
id,
feedback,
}: {
id: string;
feedback?: ChallengeFeedback;
}): VNode {
function selectChallenge(): void { function selectChallenge(): void {
if (reducer) reducer.transition("select_challenge", { uuid: id }) if (reducer) reducer.transition("select_challenge", { uuid: id });
} }
if (!feedback) { if (!feedback) {
return <div> return (
<button class="button" onClick={selectChallenge}> <div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Solve Solve
</button> </button>
</div> </div>
);
} }
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.ServerFailure: case ChallengeFeedbackStatus.ServerFailure:
case ChallengeFeedbackStatus.Unsupported: case ChallengeFeedbackStatus.Unsupported:
case ChallengeFeedbackStatus.TruthUnknown: case ChallengeFeedbackStatus.TruthUnknown:
case ChallengeFeedbackStatus.RateLimitExceeded: return <div /> case ChallengeFeedbackStatus.RateLimitExceeded:
return <div />;
case ChallengeFeedbackStatus.AuthIban: case ChallengeFeedbackStatus.AuthIban:
case ChallengeFeedbackStatus.Payment: return <div> case ChallengeFeedbackStatus.Payment:
<button class="button" onClick={selectChallenge}> return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Pay Pay
</button> </button>
</div> </div>
case ChallengeFeedbackStatus.Redirect: return <div> );
<button class="button" onClick={selectChallenge}> case ChallengeFeedbackStatus.Redirect:
return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Go to {feedback.redirect_url} Go to {feedback.redirect_url}
</button> </button>
</div> </div>
case ChallengeFeedbackStatus.Solved: return <div> );
<div class="tag is-success is-large"> case ChallengeFeedbackStatus.Solved:
Solved return (
<div>
<div class="tag is-success is-large">Solved</div>
</div> </div>
</div> );
default: return <div> default:
<button class="button" onClick={selectChallenge}> return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Solve Solve
</button> </button>
</div> </div>
);
} }
// return <div>
// {feedback.state !== "solved" ? (
// <a
// class="button"
// onClick={() =>
// }
// >
// {isFree ? "Solve" : `Pay and Solve`}
// </a>
// ) : null}
// {feedback.state === "solved" ? (
// // <div class="block is-success" > Solved </div>
// <div class="tag is-success is-large">Solved</div>
// ) : null}
// </div>
} }
return ( return (
<div <div
@ -202,7 +245,6 @@ export function ChallengeOverviewScreen(): VNode {
</div> </div>
<ChallengeButton id={uuid} feedback={info.feedback} /> <ChallengeButton id={uuid} feedback={info.feedback} />
</div> </div>
); );
}); });
@ -210,11 +252,13 @@ export function ChallengeOverviewScreen(): VNode {
const policyName = policy.challenges const policyName = policy.challenges
.map((x) => x.info.type) .map((x) => x.info.type)
.join(" + "); .join(" + ");
const opa = !atLeastThereIsOnePolicySolved const opa = !atLeastThereIsOnePolicySolved
? undefined ? undefined
: policy.isPolicySolved : policy.isPolicySolved
? undefined ? undefined
: "0.6"; : "0.6";
return ( return (
<div <div
key={policy_index} key={policy_index}

View File

@ -19,20 +19,22 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen'; import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen";
export default { export default {
title: 'Pages/recovery/__ChallengePaying', title: "Pages/recovery/__ChallengePaying",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 10, order: 10,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying); export const Example = createExample(
TestedComponent,
reducerStatesExample.challengePaying,
);

View File

@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function ChallengePayingScreen(): VNode { export function ChallengePayingScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>;
} }
const payments = ['']; //reducer.currentReducerState.payments ?? const payments = [""]; //reducer.currentReducerState.payments ??
return ( return (
<AnastasisClientFrame <AnastasisClientFrame hideNav title="Recovery: Challenge Paying">
hideNav
title="Recovery: Challenge Paying"
>
<p> <p>
Some of the providers require a payment to store the encrypted Some of the providers require a payment to store the encrypted
authentication information. authentication information.

View File

@ -20,33 +20,38 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen'; import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen";
export default { export default {
title: 'Pages/Location', title: "Pages/Location",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 2, order: 2,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); export const BackupSelectContinent = createExample(
TestedComponent,
reducerStatesExample.backupSelectContinent,
);
export const BackupSelectCountry = createExample(TestedComponent, { export const BackupSelectCountry = createExample(TestedComponent, {
...reducerStatesExample.backupSelectContinent, ...reducerStatesExample.backupSelectContinent,
selected_continent: 'Testcontinent', selected_continent: "Testcontinent",
} as ReducerState); } as ReducerState);
export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); export const RecoverySelectContinent = createExample(
TestedComponent,
reducerStatesExample.recoverySelectContinent,
);
export const RecoverySelectCountry = createExample(TestedComponent, { export const RecoverySelectCountry = createExample(TestedComponent, {
...reducerStatesExample.recoverySelectContinent, ...reducerStatesExample.recoverySelectContinent,
selected_continent: 'Testcontinent', selected_continent: "Testcontinent",
} as ReducerState); } as ReducerState);

View File

@ -20,90 +20,122 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen'; import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen";
export default { export default {
title: 'Pages/backup/ReviewPolicies/EditPolicies', title: "Pages/backup/ReviewPolicies/EditPolicies",
args: { args: {
order: 6, order: 6,
}, },
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const EditingAPolicy = createExample(TestedComponent, { export const EditingAPolicy = createExample(
TestedComponent,
{
...reducerStatesExample.policyReview, ...reducerStatesExample.policyReview,
policies: [{ policies: [
methods: [{ {
methods: [
{
authentication_method: 1, authentication_method: 1,
provider: 'https://anastasis.demo.taler.net/' provider: "https://anastasis.demo.taler.net/",
}, { },
{
authentication_method: 2, authentication_method: 2,
provider: 'http://localhost:8086/' provider: "http://localhost:8086/",
}] },
}, { ],
methods: [{ },
{
methods: [
{
authentication_method: 1, authentication_method: 1,
provider: 'http://localhost:8086/' provider: "http://localhost:8086/",
}] },
}], ],
authentication_methods: [{ },
],
authentication_methods: [
{
type: "email", type: "email",
instructions: "Email to qwe@asd.com", instructions: "Email to qwe@asd.com",
challenge: "E5VPA" challenge: "E5VPA",
}, { },
{
type: "totp", type: "totp",
instructions: "Response code for 'Anastasis'", instructions: "Response code for 'Anastasis'",
challenge: "E5VPA" challenge: "E5VPA",
}, { },
{
type: "sms", type: "sms",
instructions: "SMS to 6666-6666", instructions: "SMS to 6666-6666",
challenge: "" challenge: "",
}, { },
{
type: "question", type: "question",
instructions: "How did the chicken cross the road?", instructions: "How did the chicken cross the road?",
challenge: "C5SP8" challenge: "C5SP8",
}] },
} as ReducerState, { index : 0}); ],
} as ReducerState,
{ index: 0 },
);
export const CreatingAPolicy = createExample(TestedComponent, { export const CreatingAPolicy = createExample(
TestedComponent,
{
...reducerStatesExample.policyReview, ...reducerStatesExample.policyReview,
policies: [{ policies: [
methods: [{ {
methods: [
{
authentication_method: 1, authentication_method: 1,
provider: 'https://anastasis.demo.taler.net/' provider: "https://anastasis.demo.taler.net/",
}, { },
{
authentication_method: 2, authentication_method: 2,
provider: 'http://localhost:8086/' provider: "http://localhost:8086/",
}] },
}, { ],
methods: [{ },
{
methods: [
{
authentication_method: 1, authentication_method: 1,
provider: 'http://localhost:8086/' provider: "http://localhost:8086/",
}] },
}], ],
authentication_methods: [{ },
],
authentication_methods: [
{
type: "email", type: "email",
instructions: "Email to qwe@asd.com", instructions: "Email to qwe@asd.com",
challenge: "E5VPA" challenge: "E5VPA",
}, { },
{
type: "totp", type: "totp",
instructions: "Response code for 'Anastasis'", instructions: "Response code for 'Anastasis'",
challenge: "E5VPA" challenge: "E5VPA",
}, { },
{
type: "sms", type: "sms",
instructions: "SMS to 6666-6666", instructions: "SMS to 6666-6666",
challenge: "" challenge: "",
}, { },
{
type: "question", type: "question",
instructions: "How did the chicken cross the road?", instructions: "How did the chicken cross the road?",
challenge: "C5SP8" challenge: "C5SP8",
}] },
} as ReducerState, { index : 3}); ],
} as ReducerState,
{ index: 3 },
);

View File

@ -20,7 +20,6 @@ interface Props {
index: number; index: number;
cancel: () => void; cancel: () => void;
confirm: (changes: MethodProvider[]) => void; confirm: (changes: MethodProvider[]) => void;
} }
export interface MethodProvider { export interface MethodProvider {
@ -28,89 +27,121 @@ export interface MethodProvider {
provider: string; provider: string;
} }
export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode { export function EditPoliciesScreen({
const [changedProvider, setChangedProvider] = useState<Array<string>>([]) index: policy_index,
cancel,
confirm,
}: Props): VNode {
const [changedProvider, setChangedProvider] = useState<Array<string>>([]);
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
} }
const selectableProviders: ProviderInfoByType = {} const selectableProviders: ProviderInfoByType = {};
const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {}) const allProviders = Object.entries(
reducer.currentReducerState.authentication_providers || {},
);
for (let index = 0; index < allProviders.length; index++) { for (let index = 0; index < allProviders.length; index++) {
const [url, status] = allProviders[index] const [url, status] = allProviders[index];
if ("methods" in status) { if ("methods" in status) {
status.methods.map(m => { status.methods.map((m) => {
const type: KnownAuthMethods = m.type as KnownAuthMethods const type: KnownAuthMethods = m.type as KnownAuthMethods;
const values = selectableProviders[type] || [] const values = selectableProviders[type] || [];
const isFree = !m.usage_fee || m.usage_fee.endsWith(":0") const isFree = !m.usage_fee || m.usage_fee.endsWith(":0");
values.push({ url, cost: m.usage_fee, isFree }) values.push({ url, cost: m.usage_fee, isFree });
selectableProviders[type] = values selectableProviders[type] = values;
}) });
} }
} }
const allAuthMethods = reducer.currentReducerState.authentication_methods ?? []; const allAuthMethods =
reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? []; const policies = reducer.currentReducerState.policies ?? [];
const policy = policies[policy_index] const policy = policies[policy_index];
for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) { for (
policy?.methods.find(m => m.authentication_method === method_index)?.provider let method_index = 0;
method_index < allAuthMethods.length;
method_index++
) {
policy?.methods.find((m) => m.authentication_method === method_index)
?.provider;
} }
function sendChanges(): void { function sendChanges(): void {
const newMethods: MethodProvider[] = [] const newMethods: MethodProvider[] = [];
allAuthMethods.forEach((method, index) => { allAuthMethods.forEach((method, index) => {
const oldValue = policy?.methods.find(m => m.authentication_method === index) const oldValue = policy?.methods.find(
(m) => m.authentication_method === index,
);
if (changedProvider[index] === undefined && oldValue !== undefined) { if (changedProvider[index] === undefined && oldValue !== undefined) {
newMethods.push(oldValue) newMethods.push(oldValue);
} }
if (changedProvider[index] !== undefined && changedProvider[index] !== "") { if (
changedProvider[index] !== undefined &&
changedProvider[index] !== ""
) {
newMethods.push({ newMethods.push({
authentication_method: index, authentication_method: index,
provider: changedProvider[index] provider: changedProvider[index],
}) });
} }
}) });
confirm(newMethods) confirm(newMethods);
} }
return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}> return (
<AnastasisClientFrame
hideNav
title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}
>
<section class="section"> <section class="section">
{!policy ? <p> {!policy ? (
Creating a new policy #{policy_index} <p>Creating a new policy #{policy_index}</p>
</p> : <p> ) : (
Editing policy #{policy_index} <p>Editing policy #{policy_index}</p>
</p>} )}
{allAuthMethods.map((method, index) => { {allAuthMethods.map((method, index) => {
//take the url from the updated change or from the policy //take the url from the updated change or from the policy
const providerURL = changedProvider[index] === undefined ? const providerURL =
policy?.methods.find(m => m.authentication_method === index)?.provider : changedProvider[index] === undefined
changedProvider[index]; ? policy?.methods.find((m) => m.authentication_method === index)
?.provider
: changedProvider[index];
const type: KnownAuthMethods = method.type as KnownAuthMethods const type: KnownAuthMethods = method.type as KnownAuthMethods;
function changeProviderTo(url: string): void { function changeProviderTo(url: string): void {
const copy = [...changedProvider] const copy = [...changedProvider];
copy[index] = url copy[index] = url;
setChangedProvider(copy) setChangedProvider(copy);
} }
return ( return (
<div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}> <div
<span class="icon"> key={index}
{authMethods[type]?.icon} class="block"
</span> style={{ display: "flex", alignItems: "center" }}
<span> >
{method.instructions} <span class="icon">{authMethods[type]?.icon}</span>
</span> <span>{method.instructions}</span>
<span> <span>
<span class="select "> <span class="select ">
<select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}> <select
<option key="none" value=""> &lt;&lt; off &gt;&gt; </option> onChange={(e) => changeProviderTo(e.currentTarget.value)}
{selectableProviders[type]?.map(prov => ( value={providerURL ?? ""}
>
<option key="none" value="">
{" "}
&lt;&lt; off &gt;&gt;{" "}
</option>
{selectableProviders[type]?.map((prov) => (
<option key={prov.url} value={prov.url}> <option key={prov.url} value={prov.url}>
{prov.url} {prov.url}
</option> </option>
@ -121,13 +152,26 @@ export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Pro
</div> </div>
); );
})} })}
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={cancel}>Cancel</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span class="buttons"> <span class="buttons">
<button class="button" onClick={() => setChangedProvider([])}>Reset</button> <button class="button" onClick={() => setChangedProvider([])}>
<button class="button is-info" onClick={sendChanges}>Confirm</button> Reset
</button>
<button class="button is-info" onClick={sendChanges}>
Confirm
</button>
</span> </span>
</div> </div>
</section> </section>
</AnastasisClientFrame> </AnastasisClientFrame>
);
} }

View File

@ -19,31 +19,36 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen'; import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen";
export default { export default {
title: 'Pages/backup/__PoliciesPaying', title: "Pages/backup/__PoliciesPaying",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 9, order: 9,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const Example = createExample(TestedComponent, reducerStatesExample.policyPay); export const Example = createExample(
TestedComponent,
reducerStatesExample.policyPay,
);
export const WithSomePaymentRequest = createExample(TestedComponent, { export const WithSomePaymentRequest = createExample(TestedComponent, {
...reducerStatesExample.policyPay, ...reducerStatesExample.policyPay,
policy_payment_requests: [{ policy_payment_requests: [
payto: 'payto://x-taler-bank/bank.taler/account-a', {
provider: 'provider1' payto: "payto://x-taler-bank/bank.taler/account-a",
}, { provider: "provider1",
payto: 'payto://x-taler-bank/bank.taler/account-b', },
provider: 'provider2' {
}] payto: "payto://x-taler-bank/bank.taler/account-b",
provider: "provider2",
},
],
} as ReducerState); } as ReducerState);

View File

@ -3,20 +3,23 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function PoliciesPayingScreen(): VNode { export function PoliciesPayingScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
} }
const payments = reducer.currentReducerState.policy_payment_requests ?? []; const payments = reducer.currentReducerState.policy_payment_requests ?? [];
return ( return (
<AnastasisClientFrame hideNav title="Backup: Recovery Document Payments"> <AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
<p> <p>
Some of the providers require a payment to store the encrypted Some of the providers require a payment to store the encrypted recovery
recovery document. document.
</p> </p>
<ul> <ul>
{payments.map((x, i) => { {payments.map((x, i) => {

View File

@ -20,26 +20,28 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScreen'; import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen";
export default { export default {
title: 'Pages/recovery/Finished', title: "Pages/recovery/Finished",
args: { args: {
order: 7, order: 7,
}, },
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const GoodEnding = createExample(TestedComponent, { export const GoodEnding = createExample(TestedComponent, {
...reducerStatesExample.recoveryFinished, ...reducerStatesExample.recoveryFinished,
core_secret: { mime: 'text/plain', value: 'hello' } core_secret: { mime: "text/plain", value: "hello" },
} as ReducerState); } as ReducerState);
export const BadEnding = createExample(TestedComponent, reducerStatesExample.recoveryFinished); export const BadEnding = createExample(
TestedComponent,
reducerStatesExample.recoveryFinished,
);

View File

@ -1,39 +1,53 @@
import { import { bytesToString, decodeCrock } from "@gnu-taler/taler-util";
bytesToString,
decodeCrock
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function RecoveryFinishedScreen(): VNode { export function RecoveryFinishedScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>;
} }
const encodedSecret = reducer.currentReducerState.core_secret const encodedSecret = reducer.currentReducerState.core_secret;
if (!encodedSecret) { if (!encodedSecret) {
return <AnastasisClientFrame title="Recovery Problem" hideNav>
<p>
Secret not found
</p>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div>
</AnastasisClientFrame>
}
const secret = bytesToString(decodeCrock(encodedSecret.value))
return ( return (
<AnastasisClientFrame title="Recovery Finished" hideNav> <AnastasisClientFrame title="Recovery Problem" hideNav>
<p> <p>Secret not found</p>
Secret: {secret} <div
</p> style={{
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> marginTop: "2em",
<button class="button" onClick={() => reducer.back()}>Back</button> display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
}
const secret = bytesToString(decodeCrock(encodedSecret.value));
return (
<AnastasisClientFrame title="Recovery Finished" hideNav>
<p>Your secret: {secret}</p>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -19,40 +19,47 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen'; import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen";
export default { export default {
title: 'Pages/backup/ReviewPolicies', title: "Pages/backup/ReviewPolicies",
args: { args: {
order: 6, order: 6,
}, },
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, { export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
...reducerStatesExample.policyReview, ...reducerStatesExample.policyReview,
policies: [{ policies: [
methods: [{ {
methods: [
{
authentication_method: 0, authentication_method: 0,
provider: 'asd' provider: "asd",
}, { },
{
authentication_method: 1, authentication_method: 1,
provider: 'asd' provider: "asd",
}] },
}, { ],
methods: [{ },
{
methods: [
{
authentication_method: 1, authentication_method: 1,
provider: 'asd' provider: "asd",
}] },
}], ],
authentication_methods: [] },
],
authentication_methods: [],
} as ReducerState); } as ReducerState);
export const SomePoliciesWithMethods = createExample(TestedComponent, { export const SomePoliciesWithMethods = createExample(TestedComponent, {
@ -62,186 +69,193 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
methods: [ methods: [
{ {
authentication_method: 0, authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/" provider: "https://kudos.demo.anastasis.lu/",
}, },
{ {
authentication_method: 1, authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/" provider: "https://kudos.demo.anastasis.lu/",
}, },
{ {
authentication_method: 2, authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/" provider: "https://kudos.demo.anastasis.lu/",
}
]
}, },
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
},
{
methods: [
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
}
], ],
authentication_methods: [{ },
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/",
},
],
},
],
authentication_methods: [
{
type: "email", type: "email",
instructions: "Email to qwe@asd.com", instructions: "Email to qwe@asd.com",
challenge: "E5VPA" challenge: "E5VPA",
}, { },
{
type: "sms", type: "sms",
instructions: "SMS to 555-555", instructions: "SMS to 555-555",
challenge: "" challenge: "",
}, { },
{
type: "question", type: "question",
instructions: "Does P equal NP?", instructions: "Does P equal NP?",
challenge: "C5SP8" challenge: "C5SP8",
},{ },
{
type: "totp", type: "totp",
instructions: "Response code for 'Anastasis'", instructions: "Response code for 'Anastasis'",
challenge: "E5VPA" challenge: "E5VPA",
}, { },
{
type: "sms", type: "sms",
instructions: "SMS to 6666-6666", instructions: "SMS to 6666-6666",
challenge: "" challenge: "",
}, { },
{
type: "question", type: "question",
instructions: "How did the chicken cross the road?", instructions: "How did the chicken cross the road?",
challenge: "C5SP8" challenge: "C5SP8",
}] },
],
} as ReducerState); } as ReducerState);

View File

@ -6,16 +6,20 @@ import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode { export function ReviewPoliciesScreen(): VNode {
const [editingPolicy, setEditingPolicy] = useState<number | undefined>() const [editingPolicy, setEditingPolicy] = useState<number | undefined>();
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
} }
const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? []; const configuredAuthMethods =
reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? []; const policies = reducer.currentReducerState.policies ?? [];
if (editingPolicy !== undefined) { if (editingPolicy !== undefined) {
@ -28,58 +32,109 @@ export function ReviewPoliciesScreen(): VNode {
policy_index: editingPolicy, policy_index: editingPolicy,
policy: newMethods, policy: newMethods,
}); });
setEditingPolicy(undefined) setEditingPolicy(undefined);
}} }}
/> />
) );
} }
const errors = policies.length < 1 ? 'Need more policies' : undefined const errors = policies.length < 1 ? "Need more policies" : undefined;
return ( return (
<AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies"> <AnastasisClientFrame
{policies.length > 0 && <p class="block"> hideNext={errors}
Based on your configured authentication method you have created, some policies title="Backup: Review Recovery Policies"
have been configured. In order to recover your secret you have to solve all the >
challenges of at least one policy. {policies.length > 0 && (
</p>} <p class="block">
{policies.length < 1 && <p class="block"> Based on your configured authentication method you have created, some
No policies had been created. Go back and add more authentication methods. policies have been configured. In order to recover your secret you
</p>} have to solve all the challenges of at least one policy.
<div class="block" style={{ justifyContent: 'flex-end' }} > </p>
<button class="button is-success" onClick={() => setEditingPolicy(policies.length + 1)}>Add new policy</button> )}
{policies.length < 1 && (
<p class="block">
No policies had been created. Go back and add more authentication
methods.
</p>
)}
<div class="block" style={{ justifyContent: "flex-end" }}>
<button
class="button is-success"
onClick={() => setEditingPolicy(policies.length + 1)}
>
Add new policy
</button>
</div> </div>
{policies.map((p, policy_index) => { {policies.map((p, policy_index) => {
const methods = p.methods const methods = p.methods
.map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider })) .map(
.filter(x => !!x) (x) =>
configuredAuthMethods[x.authentication_method] && {
...configuredAuthMethods[x.authentication_method],
provider: x.provider,
},
)
.filter((x) => !!x);
const policyName = methods.map(x => x.type).join(" + "); const policyName = methods.map((x) => x.type).join(" + ");
if (p.methods.length > methods.length) {
//there is at least one authentication method that is corrupted
return null;
}
return ( return (
<div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> <div
key={policy_index}
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<div> <div>
<h3 class="subtitle"> <h3 class="subtitle">
Policy #{policy_index + 1}: {policyName} Policy #{policy_index + 1}: {policyName}
</h3> </h3>
{!methods.length && <p> {!methods.length && <p>No auth method found</p>}
No auth method found
</p>}
{methods.map((m, i) => { {methods.map((m, i) => {
return ( return (
<p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}> <p
key={i}
class="block"
style={{ display: "flex", alignItems: "center" }}
>
<span class="icon"> <span class="icon">
{authMethods[m.type as KnownAuthMethods]?.icon} {authMethods[m.type as KnownAuthMethods]?.icon}
</span> </span>
<span> <span>
{m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a> {m.instructions} recovery provided by{" "}
<a href={m.provider}>{m.provider}</a>
</span> </span>
</p> </p>
); );
})} })}
</div> </div>
<div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}> <div
<button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button> style={{
<button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button> marginTop: "auto",
marginBottom: "auto",
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
}}
>
<button
class="button is-info block"
onClick={() => setEditingPolicy(policy_index)}
>
Edit
</button>
<button
class="button is-danger block"
onClick={() =>
reducer.transition("delete_policy", { policy_index })
}
>
Delete
</button>
</div> </div>
</div> </div>
); );

View File

@ -19,26 +19,25 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen'; import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen";
export default { export default {
title: 'Pages/backup/SecretInput', title: "Pages/backup/SecretInput",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 7, order: 7,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const WithSecretNamePreselected = createExample(TestedComponent, { export const WithSecretNamePreselected = createExample(TestedComponent, {
...reducerStatesExample.secretEdition, ...reducerStatesExample.secretEdition,
secret_name: 'someSecretName', secret_name: "someSecretName",
} as ReducerState); } as ReducerState);
export const WithoutName = createExample(TestedComponent, { export const WithoutName = createExample(TestedComponent, {

View File

@ -19,33 +19,31 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScreen'; import { SecretSelectionScreen as TestedComponent } from "./SecretSelectionScreen";
export default { export default {
title: 'Pages/recovery/SecretSelection', title: "Pages/recovery/SecretSelection",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 4, order: 4,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const Example = createExample(TestedComponent, { export const Example = createExample(TestedComponent, {
...reducerStatesExample.secretSelection, ...reducerStatesExample.secretSelection,
recovery_document: { recovery_document: {
provider_url: 'https://kudos.demo.anastasis.lu/', provider_url: "https://kudos.demo.anastasis.lu/",
secret_name: 'secretName', secret_name: "secretName",
version: 1, version: 1,
}, },
} as ReducerState); } as ReducerState);
export const NoRecoveryDocumentFound = createExample(TestedComponent, { export const NoRecoveryDocumentFound = createExample(TestedComponent, {
...reducerStatesExample.secretSelection, ...reducerStatesExample.secretSelection,
recovery_document: undefined, recovery_document: undefined,

View File

@ -8,18 +8,23 @@ import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode { export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false); const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
const [manageProvider, setManageProvider] = useState(false) const [manageProvider, setManageProvider] = useState(false);
const currentVersion = (reducer?.currentReducerState const currentVersion =
&& ("recovery_document" in reducer.currentReducerState) (reducer?.currentReducerState &&
&& reducer.currentReducerState.recovery_document?.version) || 0; "recovery_document" in reducer.currentReducerState &&
reducer.currentReducerState.recovery_document?.version) ||
0;
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>;
} }
async function doSelectVersion(p: string, n: number): Promise<void> { async function doSelectVersion(p: string, n: number): Promise<void> {
@ -33,72 +38,101 @@ export function SecretSelectionScreen(): VNode {
}); });
} }
const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {}) const providerList = Object.keys(
const recoveryDocument = reducer.currentReducerState.recovery_document reducer.currentReducerState.authentication_providers ?? {},
);
const recoveryDocument = reducer.currentReducerState.recovery_document;
if (!recoveryDocument) { if (!recoveryDocument) {
return <ChooseAnotherProviderScreen return (
providers={providerList} selected="" <ChooseAnotherProviderScreen
providers={providerList}
selected=""
onChange={(newProv) => doSelectVersion(newProv, 0)} onChange={(newProv) => doSelectVersion(newProv, 0)}
/> />
);
} }
if (selectingVersion) { if (selectingVersion) {
return <SelectOtherVersionProviderScreen providers={providerList} return (
provider={recoveryDocument.provider_url} version={recoveryDocument.version} <SelectOtherVersionProviderScreen
providers={providerList}
provider={recoveryDocument.provider_url}
version={recoveryDocument.version}
onCancel={() => setSelectingVersion(false)} onCancel={() => setSelectingVersion(false)}
onConfirm={doSelectVersion} onConfirm={doSelectVersion}
/> />
);
} }
if (manageProvider) { if (manageProvider) {
return <AddingProviderScreen onCancel={() => setManageProvider(false)} /> return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
} }
return ( return (
<AnastasisClientFrame title="Recovery: Select secret"> <AnastasisClientFrame title="Recovery: Select secret">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="box" style={{ border: '2px solid green' }}> <div class="box" style={{ border: "2px solid green" }}>
<h1 class="subtitle">{recoveryDocument.provider_url}</h1> <h1 class="subtitle">{recoveryDocument.provider_url}</h1>
<div class="block"> <div class="block">
{currentVersion === 0 ? <p> {currentVersion === 0 ? (
Set to recover the latest version <p>Set to recover the latest version</p>
</p> : <p> ) : (
Set to recover the version number {currentVersion} <p>Set to recover the version number {currentVersion}</p>
</p>} )}
</div> </div>
<div class="buttons is-right"> <div class="buttons is-right">
<button class="button" onClick={(e) => setSelectingVersion(true)}>Change secret's version</button> <button class="button" onClick={(e) => setSelectingVersion(true)}>
Change secret's version
</button>
</div> </div>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<p>Secret found, you can select another version or continue to the challenges solving</p> <p>
<p class="block"> Secret found, you can select another version or continue to the
<button class="button is-info" onClick={() => setManageProvider(true)}> challenges solving
Manage recovery providers </p>
</button> <p class="block">
<a onClick={() => setManageProvider(true)}>
Manage recovery providers
</a>
</p> </p>
</div> </div>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
function ChooseAnotherProviderScreen({
function ChooseAnotherProviderScreen({ providers, selected, onChange }: { selected: string; providers: string[]; onChange: (prov: string) => void }): VNode { providers,
selected,
onChange,
}: {
selected: string;
providers: string[];
onChange: (prov: string) => void;
}): VNode {
return ( return (
<AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem"> <AnastasisClientFrame
hideNext="Recovery document not found"
title="Recovery: Problem"
>
<p>No recovery document found, try with another provider</p> <p>No recovery document found, try with another provider</p>
<div class="field"> <div class="field">
<label class="label">Provider</label> <label class="label">Provider</label>
<div class="control is-expanded has-icons-left"> <div class="control is-expanded has-icons-left">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select onChange={(e) => onChange(e.currentTarget.value)} value={selected}> <select
<option key="none" disabled selected value=""> Choose a provider </option> onChange={(e) => onChange(e.currentTarget.value)}
{providers.map(prov => ( value={selected}
>
<option key="none" disabled selected value="">
{" "}
Choose a provider{" "}
</option>
{providers.map((prov) => (
<option key={prov} value={prov}> <option key={prov} value={prov}>
{prov} {prov}
</option> </option>
@ -114,9 +148,23 @@ function ChooseAnotherProviderScreen({ providers, selected, onChange }: { select
); );
} }
function SelectOtherVersionProviderScreen({ providers, provider, version, onConfirm, onCancel }: { onCancel: () => void; provider: string; version: number; providers: string[]; onConfirm: (prov: string, v: number) => Promise<void>; }): VNode { function SelectOtherVersionProviderScreen({
providers,
provider,
version,
onConfirm,
onCancel,
}: {
onCancel: () => void;
provider: string;
version: number;
providers: string[];
onConfirm: (prov: string, v: number) => Promise<void>;
}): VNode {
const [otherProvider, setOtherProvider] = useState<string>(provider); const [otherProvider, setOtherProvider] = useState<string>(provider);
const [otherVersion, setOtherVersion] = useState(version > 0 ? String(version) : ""); const [otherVersion, setOtherVersion] = useState(
version > 0 ? String(version) : "",
);
return ( return (
<AnastasisClientFrame hideNav title="Recovery: Select secret"> <AnastasisClientFrame hideNav title="Recovery: Select secret">
@ -125,11 +173,11 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
<div class="box"> <div class="box">
<h1 class="subtitle">Provider {otherProvider}</h1> <h1 class="subtitle">Provider {otherProvider}</h1>
<div class="block"> <div class="block">
{version === 0 ? <p> {version === 0 ? (
Set to recover the latest version <p>Set to recover the latest version</p>
</p> : <p> ) : (
Set to recover the version number {version} <p>Set to recover the version number {version}</p>
</p>} )}
<p>Specify other version below or use the latest</p> <p>Specify other version below or use the latest</p>
</div> </div>
@ -137,9 +185,15 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
<label class="label">Provider</label> <label class="label">Provider</label>
<div class="control is-expanded has-icons-left"> <div class="control is-expanded has-icons-left">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select onChange={(e) => setOtherProvider(e.currentTarget.value)} value={otherProvider}> <select
<option key="none" disabled selected value=""> Choose a provider </option> onChange={(e) => setOtherProvider(e.currentTarget.value)}
{providers.map(prov => ( value={otherProvider}
>
<option key="none" disabled selected value="">
{" "}
Choose a provider{" "}
</option>
{providers.map((prov) => (
<option key={prov} value={prov}> <option key={prov} value={prov}>
{prov} {prov}
</option> </option>
@ -156,23 +210,40 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
label="Version" label="Version"
placeholder="version number to recover" placeholder="version number to recover"
grabFocus grabFocus
bind={[otherVersion, setOtherVersion]} /> bind={[otherVersion, setOtherVersion]}
/>
</div> </div>
</div> </div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={onCancel}>Cancel</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={onCancel}>
Cancel
</button>
<div class="buttons"> <div class="buttons">
<AsyncButton class="button" onClick={() => onConfirm(otherProvider, 0)}>Use latest</AsyncButton> <AsyncButton
<AsyncButton class="button is-info" onClick={() => onConfirm(otherProvider, parseInt(otherVersion, 10))}>Confirm</AsyncButton> class="button"
onClick={() => onConfirm(otherProvider, 0)}
>
Use latest
</AsyncButton>
<AsyncButton
class="button is-info"
onClick={() =>
onConfirm(otherProvider, parseInt(otherVersion, 10))
}
>
Confirm
</AsyncButton>
</div> </div>
</div> </div>
</div> </div>
<div class="column"> <div class="column">.</div>
.
</div> </div>
</div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }

View File

@ -19,51 +19,59 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from 'anastasis-core'; import {
import { createExample, reducerStatesExample } from '../../utils'; ChallengeFeedbackStatus,
import { SolveScreen as TestedComponent } from './SolveScreen'; RecoveryStates,
ReducerState,
} from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { SolveScreen as TestedComponent } from "./SolveScreen";
export default { export default {
title: 'Pages/recovery/SolveChallenge/Solve', title: "Pages/recovery/SolveChallenge/Solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 6, order: 6,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const NoInformation = createExample(TestedComponent, reducerStatesExample.challengeSolving); export const NoInformation = createExample(
TestedComponent,
reducerStatesExample.challengeSolving,
);
export const NotSupportedChallenge = createExample(TestedComponent, { export const NotSupportedChallenge = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'chall-type', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "chall-type",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
} as ReducerState); } as ReducerState);
export const MismatchedChallengeId = createExample(TestedComponent, { export const MismatchedChallengeId = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'chall-type', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "chall-type",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'no-no-no' selected_challenge_uuid: "no-no-no",
} as ReducerState); } as ReducerState);

View File

@ -2,76 +2,126 @@ import { h, VNode } from "preact";
import { AnastasisClientFrame } from "."; import { AnastasisClientFrame } from ".";
import { import {
ChallengeFeedback, ChallengeFeedback,
ChallengeFeedbackStatus ChallengeFeedbackStatus,
} from "../../../../anastasis-core/lib"; } from "../../../../anastasis-core/lib";
import { Notifications } from "../../components/Notifications"; import { Notifications } from "../../components/Notifications";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod"; import { authMethods, KnownAuthMethods } from "./authMethod";
export function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }): VNode { export function SolveOverviewFeedbackDisplay(props: {
feedback?: ChallengeFeedback;
}): VNode {
const { feedback } = props; const { feedback } = props;
if (!feedback) { if (!feedback) {
return <div />; return <div />;
} }
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.Message: case ChallengeFeedbackStatus.Message:
return (<Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "INFO", type: "INFO",
message: `Message from provider`, message: `Message from provider`,
description: feedback.message description: feedback.message,
}]} />); },
]}
/>
);
case ChallengeFeedbackStatus.Payment: case ChallengeFeedbackStatus.Payment:
return <Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "INFO", type: "INFO",
message: `Message from provider`, message: `Message from provider`,
description: <span> description: (
<span>
To pay you can <a href={feedback.taler_pay_uri}>click here</a> To pay you can <a href={feedback.taler_pay_uri}>click here</a>
</span> </span>
}]} /> ),
},
]}
/>
);
case ChallengeFeedbackStatus.AuthIban: case ChallengeFeedbackStatus.AuthIban:
return <Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "INFO", type: "INFO",
message: `Message from provider`, message: `Message from provider`,
description: `Need to send a wire transfer to "${feedback.business_name}"` description: `Need to send a wire transfer to "${feedback.business_name}"`,
}]} />; },
]}
/>
);
case ChallengeFeedbackStatus.ServerFailure: case ChallengeFeedbackStatus.ServerFailure:
return (<Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "ERROR", type: "ERROR",
message: `Server error: Code ${feedback.http_status}`, message: `Server error: Code ${feedback.http_status}`,
description: feedback.error_response description: feedback.error_response,
}]} />); },
]}
/>
);
case ChallengeFeedbackStatus.RateLimitExceeded: case ChallengeFeedbackStatus.RateLimitExceeded:
return (<Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "ERROR", type: "ERROR",
message: `Message from provider`, message: `Message from provider`,
description: "There were to many failed attempts." description: "There were to many failed attempts.",
}]} />); },
]}
/>
);
case ChallengeFeedbackStatus.Redirect: case ChallengeFeedbackStatus.Redirect:
return (<Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "INFO", type: "INFO",
message: `Message from provider`, message: `Message from provider`,
description: <span> description: (
<span>
Please visit this link: <a>{feedback.redirect_url}</a> Please visit this link: <a>{feedback.redirect_url}</a>
</span> </span>
}]} />); ),
},
]}
/>
);
case ChallengeFeedbackStatus.Unsupported: case ChallengeFeedbackStatus.Unsupported:
return (<Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "ERROR", type: "ERROR",
message: `This client doesn't support solving this type of challenge`, message: `This client doesn't support solving this type of challenge`,
description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"` description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"`,
}]} />); },
]}
/>
);
case ChallengeFeedbackStatus.TruthUnknown: case ChallengeFeedbackStatus.TruthUnknown:
return (<Notifications notifications={[{ return (
<Notifications
notifications={[
{
type: "ERROR", type: "ERROR",
message: `Provider doesn't recognize the type of challenge`, message: `Provider doesn't recognize the type of challenge`,
description: "Contact the provider for further information" description: "Contact the provider for further information",
}]} />); },
default: ]}
return ( />
<div>
<pre>{JSON.stringify(feedback)}</pre>
</div>
); );
default:
return <div />;
} }
} }
@ -110,8 +160,16 @@ export function SolveScreen(): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -120,26 +178,36 @@ export function SolveScreen(): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Not implemented"> <AnastasisClientFrame hideNav title="Not implemented">
<p> <p>
The challenge selected is not supported for this UI. Please update this The challenge selected is not supported for this UI. Please update
version or try using another policy. this version or try using another policy.
</p> </p>
{reducer && {reducer && (
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
} )}
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
const chArr = reducer.currentReducerState.recovery_information.challenges; const chArr = reducer.currentReducerState.recovery_information.challenges;
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid; const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
const selectedChallenge = chArr.find(ch => ch.uuid === selectedUuid) const selectedChallenge = chArr.find((ch) => ch.uuid === selectedUuid);
const SolveDialog = !selectedChallenge || !authMethods[selectedChallenge.type as KnownAuthMethods] ? const SolveDialog =
SolveNotImplemented : !selectedChallenge ||
authMethods[selectedChallenge.type as KnownAuthMethods].solve ?? SolveNotImplemented !authMethods[selectedChallenge.type as KnownAuthMethods]
? SolveNotImplemented
: authMethods[selectedChallenge.type as KnownAuthMethods].solve ??
SolveNotImplemented;
return <SolveDialog id={selectedUuid} /> return <SolveDialog id={selectedUuid} />;
} }

View File

@ -19,20 +19,22 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { StartScreen as TestedComponent } from './StartScreen'; import { StartScreen as TestedComponent } from "./StartScreen";
export default { export default {
title: 'Pages/Start', title: "Pages/Start",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 1, order: 1,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const InitialState = createExample(TestedComponent, reducerStatesExample.initial); export const InitialState = createExample(
TestedComponent,
reducerStatesExample.initial,
);

View File

@ -1,27 +1,36 @@
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function StartScreen(): VNode { export function StartScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
return ( return (
<AnastasisClientFrame hideNav title="Home"> <AnastasisClientFrame hideNav title="Home">
<div class="columns"> <div class="columns">
<div class="column" /> <div class="column" />
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="buttons"> <div class="buttons">
<button class="button is-success" autoFocus onClick={() => reducer.startBackup()}> <button
<div class="icon"><i class="mdi mdi-arrow-up" /></div> class="button is-success"
autoFocus
onClick={() => reducer.startBackup()}
>
<div class="icon">
<i class="mdi mdi-arrow-up" />
</div>
<span>Backup a secret</span> <span>Backup a secret</span>
</button> </button>
<button class="button is-info" onClick={() => reducer.startRecover()}> <button
<div class="icon"><i class="mdi mdi-arrow-down" /></div> class="button is-info"
onClick={() => reducer.startRecover()}
>
<div class="icon">
<i class="mdi mdi-arrow-down" />
</div>
<span>Recover a secret</span> <span>Recover a secret</span>
</button> </button>
@ -30,7 +39,6 @@ export function StartScreen(): VNode {
<span>Restore a session</span> <span>Restore a session</span>
</button> */} </button> */}
</div> </div>
</div> </div>
<div class="column" /> <div class="column" />
</div> </div>

View File

@ -19,25 +19,27 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from "../../utils";
import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen'; import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen";
export default { export default {
title: 'Pages/backup/__TruthsPaying', title: "Pages/backup/__TruthsPaying",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 10, order: 10,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
export const Example = createExample(TestedComponent, reducerStatesExample.truthsPaying); export const Example = createExample(
TestedComponent,
reducerStatesExample.truthsPaying,
);
export const WithPaytoList = createExample(TestedComponent, { export const WithPaytoList = createExample(TestedComponent, {
...reducerStatesExample.truthsPaying, ...reducerStatesExample.truthsPaying,
payments: ['payto://x-taler-bank/bank/account'] payments: ["payto://x-taler-bank/bank/account"],
} as ReducerState); } as ReducerState);

View File

@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function TruthsPayingScreen(): VNode { export function TruthsPayingScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
} }
const payments = reducer.currentReducerState.payments ?? []; const payments = reducer.currentReducerState.payments ?? [];
return ( return (
<AnastasisClientFrame <AnastasisClientFrame hideNext={"FIXME"} title="Backup: Truths Paying">
hideNext={"FIXME"}
title="Backup: Truths Paying"
>
<p> <p>
Some of the providers require a payment to store the encrypted Some of the providers require a payment to store the encrypted
authentication information. authentication information.

View File

@ -19,47 +19,63 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/email', title: "Pages/backup/AuthorizationMethod/AuthMethods/email",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'email' const type: KnownAuthMethods = "email";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithOneExample = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Email to sebasjm@email.com ', instructions: "Email to sebasjm@email.com ",
remove: () => null remove: () => null,
}] },
}); ],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithMoreExamples = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Email to sebasjm@email.com', instructions: "Email to sebasjm@email.com",
remove: () => null remove: () => null,
},{ },
challenge: 'qwe', {
challenge: "qwe",
type, type,
instructions: 'Email to someone@sebasjm.com', instructions: "Email to someone@sebasjm.com",
remove: () => null remove: () => null,
}] },
}); ],
},
);

View File

@ -1,57 +1,90 @@
import { import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { EmailInput } from "../../../components/fields/EmailInput"; import { EmailInput } from "../../../components/fields/EmailInput";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { AuthMethodSetupProps } from "./index"; import { AuthMethodSetupProps } from "./index";
const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export function AuthMethodEmailSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode { export function AuthMethodEmailSetup({
cancel,
addAuthMethod,
configured,
}: AuthMethodSetupProps): VNode {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const addEmailAuth = (): void => addAuthMethod({ const addEmailAuth = (): void =>
addAuthMethod({
authentication_method: { authentication_method: {
type: "email", type: "email",
instructions: `Email to ${email}`, instructions: `Email to ${email}`,
challenge: encodeCrock(stringToBytes(email)), challenge: encodeCrock(stringToBytes(email)),
}, },
}); });
const emailError = !EMAIL_PATTERN.test(email) ? 'Email address is not valid' : undefined const emailError = !EMAIL_PATTERN.test(email)
const errors = !email ? 'Add your email' : emailError ? "Email address is not valid"
: undefined;
const errors = !email ? "Add your email" : emailError;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<p> <p>
For email authentication, you need to provide an email address. When For email authentication, you need to provide an email address. When
recovering your secret, you will need to enter the code you receive by recovering your secret, you will need to enter the code you receive by
email. email. Add the uuid from the challenge
</p> </p>
<div> <div>
<EmailInput <EmailInput
label="Email address" label="Email address"
error={emailError} error={emailError}
placeholder="email@domain.com" placeholder="email@domain.com"
bind={[email, setEmail]} /> bind={[email, setEmail]}
/>
</div> </div>
{configured.length > 0 && <section class="section"> {configured.length > 0 && (
<section class="section">
<div class="block">Your emails:</div>
<div class="block"> <div class="block">
Your emails:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> <div
<div><button class="button is-danger" onClick={c.remove} >Delete</button></div> key={i}
</div> class="box"
})} style={{ display: "flex", justifyContent: "space-between" }}
</div></section>} >
<p style={{ marginBottom: "auto", marginTop: "auto" }}>
{c.instructions}
</p>
<div> <div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <button class="button is-danger" onClick={c.remove}>
<button class="button" onClick={cancel}>Cancel</button> Delete
</button>
</div>
</div>
);
})}
</div>
</section>
)}
<div>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={errors !== undefined} onClick={addEmailAuth}>Add</button> <button
class="button is-info"
disabled={errors !== undefined}
onClick={addEmailAuth}
>
Add
</button>
</span> </span>
</div> </div>
</div> </div>

View File

@ -19,62 +19,72 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/email', title: "Pages/recovery/SolveChallenge/AuthMethods/email",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'email' const type: KnownAuthMethods = "email";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);
export const PaymentFeedback = createExample(TestedComponent[type].solve, { export const PaymentFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
challenge_feedback: { challenge_feedback: {
'uuid-1': { "uuid-1": {
state: ChallengeFeedbackStatus.Payment, state: ChallengeFeedbackStatus.Payment,
taler_pay_uri: "taler://pay/...", taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/", provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG" payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
} },
} },
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);

View File

@ -44,8 +44,16 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,19 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>
An email has been sent to "<b>{selectedChallenge.instructions}</b>". Type the An email has been sent to "<b>{selectedChallenge.instructions}</b>".
code below Type the code below.
<b>Here we need to add the code "{selectedUuid}"</b>
</p> </p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
@ -97,9 +105,11 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -19,46 +19,62 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/IBAN', title: "Pages/backup/AuthorizationMethod/AuthMethods/IBAN",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'iban' const type: KnownAuthMethods = "iban";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithOneExample = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Wire transfer from QWEASD123123 with holder Sebastian', instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
remove: () => null remove: () => null,
}] },
}); ],
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { },
configured: [{ );
challenge: 'qwe', export const WithMoreExamples = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Wire transfer from QWEASD123123 with holder Javier', instructions: "Wire transfer from QWEASD123123 with holder Javier",
remove: () => null remove: () => null,
},{ },
challenge: 'qwe', {
challenge: "qwe",
type, type,
instructions: 'Wire transfer from QWEASD123123 with holder Sebastian', instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
remove: () => null remove: () => null,
}] },
},); ],
},
);

View File

@ -1,7 +1,7 @@
import { import {
canonicalJson, canonicalJson,
encodeCrock, encodeCrock,
stringToBytes stringToBytes,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -9,56 +9,98 @@ import { AuthMethodSetupProps } from ".";
import { TextInput } from "../../../components/fields/TextInput"; import { TextInput } from "../../../components/fields/TextInput";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
export function AuthMethodIbanSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { export function AuthMethodIbanSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [account, setAccount] = useState(""); const [account, setAccount] = useState("");
const addIbanAuth = (): void => addAuthMethod({ const addIbanAuth = (): void =>
addAuthMethod({
authentication_method: { authentication_method: {
type: "iban", type: "iban",
instructions: `Wire transfer from ${account} with holder ${name}`, instructions: `Wire transfer from ${account} with holder ${name}`,
challenge: encodeCrock(stringToBytes(canonicalJson({ challenge: encodeCrock(
name, account stringToBytes(
}))), canonicalJson({
name,
account,
}),
),
),
}, },
}); });
const errors = !name ? 'Add an account name' : ( const errors = !name
!account ? 'Add an account IBAN number' : undefined ? "Add an account name"
) : !account
? "Add an account IBAN number"
: undefined;
return ( return (
<AnastasisClientFrame hideNav title="Add bank transfer authentication"> <AnastasisClientFrame hideNav title="Add bank transfer authentication">
<p> <p>
For bank transfer authentication, you need to provide a bank For bank transfer authentication, you need to provide a bank account
account (account holder name and IBAN). When recovering your (account holder name and IBAN). When recovering your secret, you will be
secret, you will be asked to pay the recovery fee via bank asked to pay the recovery fee via bank transfer from the account you
transfer from the account you provided here. provided here.
</p> </p>
<div> <div>
<TextInput <TextInput
label="Bank account holder name" label="Bank account holder name"
grabFocus grabFocus
placeholder="John Smith" placeholder="John Smith"
bind={[name, setName]} /> bind={[name, setName]}
/>
<TextInput <TextInput
label="IBAN" label="IBAN"
placeholder="DE91100000000123456789" placeholder="DE91100000000123456789"
bind={[account, setAccount]} /> bind={[account, setAccount]}
/>
</div> </div>
{configured.length > 0 && <section class="section"> {configured.length > 0 && (
<section class="section">
<div class="block">Your bank accounts:</div>
<div class="block"> <div class="block">
Your bank accounts:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> <div
<div><button class="button is-danger" onClick={c.remove} >Delete</button></div> key={i}
</div> class="box"
})} style={{ display: "flex", justifyContent: "space-between" }}
</div></section>} >
<p style={{ marginBottom: "auto", marginTop: "auto" }}>
{c.instructions}
</p>
<div> <div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <button class="button is-danger" onClick={c.remove}>
<button class="button" onClick={cancel}>Cancel</button> Delete
</button>
</div>
</div>
);
})}
</div>
</section>
)}
<div>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={errors !== undefined} onClick={addIbanAuth}>Add</button> <button
class="button is-info"
disabled={errors !== undefined}
onClick={addIbanAuth}
>
Add
</button>
</span> </span>
</div> </div>
</div> </div>

View File

@ -19,38 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/Iban', title: "Pages/recovery/SolveChallenge/AuthMethods/Iban",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'iban' const type: KnownAuthMethods = "iban";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);

View File

@ -44,8 +44,16 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,19 +79,17 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>Send a wire transfer to the address,</p>
Send a wire transfer to the address <button class="button">Check</button>
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div <div
style={{ style={{
@ -96,9 +101,11 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -20,47 +20,63 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Post', title: "Pages/backup/AuthorizationMethod/AuthMethods/Post",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'post' const type: KnownAuthMethods = "post";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithOneExample = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Letter to address in postal code QWE456', instructions: "Letter to address in postal code QWE456",
remove: () => null remove: () => null,
}] },
}); ],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithMoreExamples = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Letter to address in postal code QWE456', instructions: "Letter to address in postal code QWE456",
remove: () => null remove: () => null,
},{ },
challenge: 'qwe', {
challenge: "qwe",
type, type,
instructions: 'Letter to address in postal code ABC123', instructions: "Letter to address in postal code ABC123",
remove: () => null remove: () => null,
}] },
}); ],
},
);

View File

@ -1,6 +1,7 @@
import { import {
canonicalJson, encodeCrock, canonicalJson,
stringToBytes encodeCrock,
stringToBytes,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -8,7 +9,11 @@ import { AnastasisClientFrame } from "..";
import { TextInput } from "../../../components/fields/TextInput"; import { TextInput } from "../../../components/fields/TextInput";
import { AuthMethodSetupProps } from "./index"; import { AuthMethodSetupProps } from "./index";
export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { export function AuthMethodPostSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [fullName, setFullName] = useState(""); const [fullName, setFullName] = useState("");
const [street, setStreet] = useState(""); const [street, setStreet] = useState("");
const [city, setCity] = useState(""); const [city, setCity] = useState("");
@ -32,68 +37,83 @@ export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthM
}); });
}; };
const errors = !fullName ? 'The full name is missing' : ( const errors = !fullName
!street ? 'The street is missing' : ( ? "The full name is missing"
!city ? 'The city is missing' : ( : !street
!postcode ? 'The postcode is missing' : ( ? "The street is missing"
!country ? 'The country is missing' : undefined : !city
) ? "The city is missing"
) : !postcode
) ? "The postcode is missing"
) : !country
? "The country is missing"
: undefined;
return ( return (
<AnastasisClientFrame hideNav title="Add postal authentication"> <AnastasisClientFrame hideNav title="Add postal authentication">
<p> <p>
For postal letter authentication, you need to provide a postal For postal letter authentication, you need to provide a postal address.
address. When recovering your secret, you will be asked to enter a When recovering your secret, you will be asked to enter a code that you
code that you will receive in a letter to that address. will receive in a letter to that address.
</p> </p>
<div> <div>
<TextInput <TextInput grabFocus label="Full Name" bind={[fullName, setFullName]} />
grabFocus
label="Full Name"
bind={[fullName, setFullName]}
/>
</div> </div>
<div> <div>
<TextInput <TextInput label="Street" bind={[street, setStreet]} />
label="Street"
bind={[street, setStreet]}
/>
</div> </div>
<div> <div>
<TextInput <TextInput label="City" bind={[city, setCity]} />
label="City" bind={[city, setCity]}
/>
</div> </div>
<div> <div>
<TextInput <TextInput label="Postal Code" bind={[postcode, setPostcode]} />
label="Postal Code" bind={[postcode, setPostcode]}
/>
</div> </div>
<div> <div>
<TextInput <TextInput label="Country" bind={[country, setCountry]} />
label="Country"
bind={[country, setCountry]}
/>
</div> </div>
{configured.length > 0 && <section class="section"> {configured.length > 0 && (
<section class="section">
<div class="block">Your postal code:</div>
<div class="block"> <div class="block">
Your postal code:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> <div
<div><button class="button is-danger" onClick={c.remove} >Delete</button></div> key={i}
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<p style={{ marginBottom: "auto", marginTop: "auto" }}>
{c.instructions}
</p>
<div>
<button class="button is-danger" onClick={c.remove}>
Delete
</button>
</div> </div>
</div>
);
})} })}
</div> </div>
</section>} </section>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> )}
<button class="button" onClick={cancel}>Cancel</button> <div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button> <button
class="button is-info"
disabled={errors !== undefined}
onClick={addPostAuth}
>
Add
</button>
</span> </span>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>

View File

@ -19,38 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/post', title: "Pages/recovery/SolveChallenge/AuthMethods/post",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'post' const type: KnownAuthMethods = "post";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);

View File

@ -44,8 +44,16 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>Wait for the answer</p>
Wait for the answer
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div <div
@ -96,9 +101,11 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -20,47 +20,65 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Question', title: "Pages/backup/AuthorizationMethod/AuthMethods/Question",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'question' const type: KnownAuthMethods = "question";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithOneExample = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Is integer factorization polynomial? (non-quantum computer)', instructions:
remove: () => null "Is integer factorization polynomial? (non-quantum computer)",
}] remove: () => null,
}); },
],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithMoreExamples = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Does P equal NP?', instructions: "Does P equal NP?",
remove: () => null remove: () => null,
},{ },
challenge: 'asd', {
challenge: "asd",
type, type,
instructions: 'Are continuous groups automatically differential groups?', instructions:
remove: () => null "Are continuous groups automatically differential groups?",
}] remove: () => null,
}); },
],
},
);

View File

@ -1,17 +1,19 @@
import { import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./index"; import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput"; import { TextInput } from "../../../components/fields/TextInput";
export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode { export function AuthMethodQuestionSetup({
cancel,
addAuthMethod,
configured,
}: AuthMethodSetupProps): VNode {
const [questionText, setQuestionText] = useState(""); const [questionText, setQuestionText] = useState("");
const [answerText, setAnswerText] = useState(""); const [answerText, setAnswerText] = useState("");
const addQuestionAuth = (): void => addAuthMethod({ const addQuestionAuth = (): void =>
addAuthMethod({
authentication_method: { authentication_method: {
type: "question", type: "question",
instructions: questionText, instructions: questionText,
@ -19,9 +21,11 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
}, },
}); });
const errors = !questionText ? "Add your security question" : ( const errors = !questionText
!answerText ? 'Add the answer to your question' : undefined ? "Add your security question"
) : !answerText
? "Add the answer to your question"
: undefined;
return ( return (
<AnastasisClientFrame hideNav title="Add Security Question"> <AnastasisClientFrame hideNav title="Add Security Question">
<div> <div>
@ -36,7 +40,8 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
label="Security question" label="Security question"
grabFocus grabFocus
placeholder="Your question" placeholder="Your question"
bind={[questionText, setQuestionText]} /> bind={[questionText, setQuestionText]}
/>
</div> </div>
<div> <div>
<TextInput <TextInput
@ -46,24 +51,52 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
/> />
</div> </div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={cancel}>Cancel</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button> <button
class="button is-info"
disabled={errors !== undefined}
onClick={addQuestionAuth}
>
Add
</button>
</span> </span>
</div> </div>
{configured.length > 0 && <section class="section"> {configured.length > 0 && (
<section class="section">
<div class="block">Your security questions:</div>
<div class="block"> <div class="block">
Your security questions:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> <div
<div><button class="button is-danger" onClick={c.remove} >Delete</button></div> key={i}
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<p style={{ marginBottom: "auto", marginTop: "auto" }}>
{c.instructions}
</p>
<div>
<button class="button is-danger" onClick={c.remove}>
Delete
</button>
</div> </div>
</div>
);
})} })}
</div></section>} </div>
</section>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -19,182 +19,201 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/question', title: "Pages/recovery/SolveChallenge/AuthMethods/question",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'question' const type: KnownAuthMethods = "question";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);
export const MessageFeedback = createExample(TestedComponent[type].solve, { export const MessageFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Message, state: ChallengeFeedbackStatus.Message,
message: 'Challenge should be solved' message: "Challenge should be solved",
} },
} },
} as ReducerState); } as ReducerState);
export const ServerFailureFeedback = createExample(TestedComponent[type].solve, { export const ServerFailureFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.ServerFailure, state: ChallengeFeedbackStatus.ServerFailure,
http_status: 500, http_status: 500,
error_response: "Couldn't connect to mysql" error_response: "Couldn't connect to mysql",
} },
} },
} as ReducerState,
} as ReducerState); );
export const RedirectFeedback = createExample(TestedComponent[type].solve, { export const RedirectFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Redirect, state: ChallengeFeedbackStatus.Redirect,
http_status: 302, http_status: 302,
redirect_url: 'http://video.taler.net' redirect_url: "http://video.taler.net",
} },
} },
} as ReducerState); } as ReducerState);
export const MessageRateLimitExceededFeedback = createExample(TestedComponent[type].solve, { export const MessageRateLimitExceededFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.RateLimitExceeded, state: ChallengeFeedbackStatus.RateLimitExceeded,
} },
} },
} as ReducerState,
} as ReducerState); );
export const UnsupportedFeedback = createExample(TestedComponent[type].solve, { export const UnsupportedFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Unsupported, state: ChallengeFeedbackStatus.Unsupported,
http_status: 500, http_status: 500,
unsupported_method: 'Question' unsupported_method: "Question",
} },
} },
} as ReducerState); } as ReducerState);
export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, { export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.TruthUnknown, state: ChallengeFeedbackStatus.TruthUnknown,
} },
} },
} as ReducerState); } as ReducerState);
export const AuthIbanFeedback = createExample(TestedComponent[type].solve, { export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.AuthIban, state: ChallengeFeedbackStatus.AuthIban,
challenge_amount: "EUR:1", challenge_amount: "EUR:1",
credit_iban: "DE12345789000", credit_iban: "DE12345789000",
@ -210,30 +229,30 @@ export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
wire_transfer_subject: "foo", wire_transfer_subject: "foo",
}, },
method: "iban", method: "iban",
} },
} },
} as ReducerState); } as ReducerState);
export const PaymentFeedback = createExample(TestedComponent[type].solve, { export const PaymentFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'ASDASDSAD!1' type: "question",
}], uuid: "ASDASDSAD!1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1', selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
'ASDASDSAD!1': { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Payment, state: ChallengeFeedbackStatus.Payment,
taler_pay_uri: "taler://pay/...", taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/", provider: "https://localhost:8080/",
payment_secret : "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG" payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
} },
} },
} as ReducerState); } as ReducerState);

View File

@ -44,8 +44,16 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>Answer the question please</p>
Answer the question please
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div <div
@ -96,9 +101,11 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -20,47 +20,63 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Sms', title: "Pages/backup/AuthorizationMethod/AuthMethods/Sms",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'sms' const type: KnownAuthMethods = "sms";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithOneExample = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'SMS to +11-1234-2345', instructions: "SMS to +11-1234-2345",
remove: () => null remove: () => null,
}] },
}); ],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithMoreExamples = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'SMS to +11-1234-2345', instructions: "SMS to +11-1234-2345",
remove: () => null remove: () => null,
},{ },
challenge: 'qwe', {
challenge: "qwe",
type, type,
instructions: 'SMS to +11-5555-2345', instructions: "SMS to +11-5555-2345",
remove: () => null remove: () => null,
}] },
}); ],
},
);

View File

@ -1,14 +1,15 @@
import { import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { AuthMethodSetupProps } from "."; import { AuthMethodSetupProps } from ".";
import { PhoneNumberInput } from "../../../components/fields/NumberInput"; import { PhoneNumberInput } from "../../../components/fields/NumberInput";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { export function AuthMethodSmsSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [mobileNumber, setMobileNumber] = useState(""); const [mobileNumber, setMobileNumber] = useState("");
const addSmsAuth = (): void => { const addSmsAuth = (): void => {
addAuthMethod({ addAuthMethod({
@ -23,7 +24,7 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
useLayoutEffect(() => { useLayoutEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
const errors = !mobileNumber ? 'Add a mobile number' : undefined const errors = !mobileNumber ? "Add a mobile number" : undefined;
return ( return (
<AnastasisClientFrame hideNav title="Add SMS authentication"> <AnastasisClientFrame hideNav title="Add SMS authentication">
<div> <div>
@ -37,23 +38,52 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
label="Mobile number" label="Mobile number"
placeholder="Your mobile number" placeholder="Your mobile number"
grabFocus grabFocus
bind={[mobileNumber, setMobileNumber]} /> bind={[mobileNumber, setMobileNumber]}
/>
</div> </div>
{configured.length > 0 && <section class="section"> {configured.length > 0 && (
<section class="section">
<div class="block">Your mobile numbers:</div>
<div class="block"> <div class="block">
Your mobile numbers:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p> <div
<div><button class="button is-danger" onClick={c.remove}>Delete</button></div> key={i}
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<p style={{ marginTop: "auto", marginBottom: "auto" }}>
{c.instructions}
</p>
<div>
<button class="button is-danger" onClick={c.remove}>
Delete
</button>
</div> </div>
</div>
);
})} })}
</div></section>} </div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> </section>
<button class="button" onClick={cancel}>Cancel</button> )}
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button> <button
class="button is-info"
disabled={errors !== undefined}
onClick={addSmsAuth}
>
Add
</button>
</span> </span>
</div> </div>
</div> </div>

View File

@ -19,38 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/sms', title: "Pages/recovery/SolveChallenge/AuthMethods/sms",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'sms' const type: KnownAuthMethods = "sms";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);

View File

@ -44,8 +44,16 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,18 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>
An sms has been sent to "<b>{selectedChallenge.instructions}</b>". Type the code An sms has been sent to "<b>{selectedChallenge.instructions}</b>". Type
below the code below
</p> </p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
@ -97,9 +104,11 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -20,45 +20,61 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/TOTP', title: "Pages/backup/AuthorizationMethod/AuthMethods/TOTP",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'totp' const type: KnownAuthMethods = "totp";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { {
configured: [{ configured: [],
challenge: 'qwe', },
);
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Enter 8 digits code for "Anastasis"', instructions: 'Enter 8 digits code for "Anastasis"',
remove: () => null remove: () => null,
}] },
}); ],
export const WithMoreExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { },
configured: [{ );
challenge: 'qwe', export const WithMoreExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: 'Enter 8 digits code for "Anastasis1"', instructions: 'Enter 8 digits code for "Anastasis1"',
remove: () => null remove: () => null,
},{ },
challenge: 'qwe', {
challenge: "qwe",
type, type,
instructions: 'Enter 8 digits code for "Anastasis2"', instructions: 'Enter 8 digits code for "Anastasis2"',
remove: () => null remove: () => null,
}] },
}); ],
},
);

View File

@ -1,7 +1,4 @@
import { import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks"; import { useMemo, useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./index"; import { AuthMethodSetupProps } from "./index";
@ -10,18 +7,23 @@ import { TextInput } from "../../../components/fields/TextInput";
import { QR } from "../../../components/QR"; import { QR } from "../../../components/QR";
import { base32enc, computeTOTPandCheck } from "./totp"; import { base32enc, computeTOTPandCheck } from "./totp";
export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { export function AuthMethodTotpSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("anastasis"); const [name, setName] = useState("anastasis");
const [test, setTest] = useState(""); const [test, setTest] = useState("");
const digits = 8 const digits = 8;
const secretKey = useMemo(() => { const secretKey = useMemo(() => {
const array = new Uint8Array(32) const array = new Uint8Array(32);
return window.crypto.getRandomValues(array) return window.crypto.getRandomValues(array);
}, []) }, []);
const secret32 = base32enc(secretKey); const secret32 = base32enc(secretKey);
const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}` const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`;
const addTotpAuth = (): void => addAuthMethod({ const addTotpAuth = (): void =>
addAuthMethod({
authentication_method: { authentication_method: {
type: "totp", type: "totp",
instructions: `Enter ${digits} digits code for "${name}"`, instructions: `Enter ${digits} digits code for "${name}"`,
@ -31,9 +33,11 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10)); const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
const errors = !name ? 'The TOTP name is missing' : ( const errors = !name
!testCodeMatches ? 'The test code doesnt match' : undefined ? "The TOTP name is missing"
); : !testCodeMatches
? "The test code doesnt match"
: undefined;
return ( return (
<AnastasisClientFrame hideNav title="Add TOTP authentication"> <AnastasisClientFrame hideNav title="Add TOTP authentication">
<p> <p>
@ -42,10 +46,7 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
with your TOTP App to import the TOTP secret into your TOTP App. with your TOTP App to import the TOTP secret into your TOTP App.
</p> </p>
<div class="block"> <div class="block">
<TextInput <TextInput label="TOTP Name" grabFocus bind={[name, setName]} />
label="TOTP Name"
grabFocus
bind={[name, setName]} />
</div> </div>
<div style={{ height: 300 }}> <div style={{ height: 300 }}>
<QR text={totpURL} /> <QR text={totpURL} />
@ -53,25 +54,51 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
<p> <p>
After scanning the code with your TOTP App, test it in the input below. After scanning the code with your TOTP App, test it in the input below.
</p> </p>
<TextInput <TextInput label="Test code" bind={[test, setTest]} />
label="Test code" {configured.length > 0 && (
bind={[test, setTest]} /> <section class="section">
{configured.length > 0 && <section class="section"> <div class="block">Your TOTP numbers:</div>
<div class="block"> <div class="block">
Your TOTP numbers:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p> <div
<div><button class="button is-danger" onClick={c.remove}>Delete</button></div> key={i}
</div> class="box"
})} style={{ display: "flex", justifyContent: "space-between" }}
</div></section>} >
<p style={{ marginTop: "auto", marginBottom: "auto" }}>
{c.instructions}
</p>
<div> <div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <button class="button is-danger" onClick={c.remove}>
<button class="button" onClick={cancel}>Cancel</button> Delete
</button>
</div>
</div>
);
})}
</div>
</section>
)}
<div>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={errors !== undefined} onClick={addTotpAuth}>Add</button> <button
class="button is-info"
disabled={errors !== undefined}
onClick={addTotpAuth}
>
Add
</button>
</span> </span>
</div> </div>
</div> </div>

View File

@ -19,38 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/totp', title: "Pages/recovery/SolveChallenge/AuthMethods/totp",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'totp' const type: KnownAuthMethods = "totp";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);

View File

@ -44,8 +44,16 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>enter the totp solution</p>
enter the totp solution
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div <div
@ -96,9 +101,11 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -20,47 +20,64 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
import logoImage from '../../../assets/logo.jpeg' import logoImage from "../../../assets/logo.jpeg";
export default { export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Video', title: "Pages/backup/AuthorizationMethod/AuthMethods/Video",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'video' const type: KnownAuthMethods = "video";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const Empty = createExample(
configured: [] TestedComponent[type].setup,
}); reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithOneExample = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: logoImage, instructions: logoImage,
remove: () => null remove: () => null,
}] },
}); ],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, { export const WithMoreExamples = createExample(
configured: [{ TestedComponent[type].setup,
challenge: 'qwe', reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type, type,
instructions: logoImage, instructions: logoImage,
remove: () => null remove: () => null,
},{ },
challenge: 'qwe', {
challenge: "qwe",
type, type,
instructions: logoImage, instructions: logoImage,
remove: () => null remove: () => null,
}] },
}); ],
},
);

View File

@ -1,53 +1,86 @@
import { import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { ImageInput } from "../../../components/fields/ImageInput"; import { ImageInput } from "../../../components/fields/ImageInput";
import { AuthMethodSetupProps } from "./index"; import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
export function AuthMethodVideoSetup({cancel, addAuthMethod, configured}: AuthMethodSetupProps): VNode { export function AuthMethodVideoSetup({
cancel,
addAuthMethod,
configured,
}: AuthMethodSetupProps): VNode {
const [image, setImage] = useState(""); const [image, setImage] = useState("");
const addVideoAuth = (): void => { const addVideoAuth = (): void => {
addAuthMethod({ addAuthMethod({
authentication_method: { authentication_method: {
type: "video", type: "video",
instructions: 'Join a video call', instructions: "Join a video call",
challenge: encodeCrock(stringToBytes(image)), challenge: encodeCrock(stringToBytes(image)),
}, },
}) });
}; };
return ( return (
<AnastasisClientFrame hideNav title="Add video authentication"> <AnastasisClientFrame hideNav title="Add video authentication">
<p> <p>
For video identification, you need to provide a passport-style For video identification, you need to provide a passport-style
photograph. When recovering your secret, you will be asked to join a photograph. When recovering your secret, you will be asked to join a
video call. During that call, a human will use the photograph to video call. During that call, a human will use the photograph to verify
verify your identity. your identity.
</p> </p>
<div style={{textAlign:'center'}}> <div style={{ textAlign: "center" }}>
<ImageInput <ImageInput
label="Choose photograph" label="Choose photograph"
grabFocus grabFocus
bind={[image, setImage]} /> bind={[image, setImage]}
/>
</div> </div>
{configured.length > 0 && <section class="section"> {configured.length > 0 && (
<section class="section">
<div class="block">Your photographs:</div>
<div class="block"> <div class="block">
Your photographs:
</div><div class="block">
{configured.map((c, i) => { {configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> return (
<img style={{ marginTop: 'auto', marginBottom: 'auto', width: 100, height:100, border: 'solid 1px black' }} src={c.instructions} /> <div
<div style={{marginTop: 'auto', marginBottom: 'auto'}}><button class="button is-danger" onClick={c.remove}>Delete</button></div> key={i}
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<img
style={{
marginTop: "auto",
marginBottom: "auto",
width: 100,
height: 100,
border: "solid 1px black",
}}
src={c.instructions}
/>
<div style={{ marginTop: "auto", marginBottom: "auto" }}>
<button class="button is-danger" onClick={c.remove}>
Delete
</button>
</div> </div>
</div>
);
})} })}
</div></section>} </div>
</section>
)}
<div> <div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={cancel}>Cancel</button> style={{
<button class="button is-info" onClick={addVideoAuth}>Add</button> marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<button class="button is-info" onClick={addVideoAuth}>
Add
</button>
</div> </div>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>

View File

@ -19,38 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from '../../../utils'; import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from './index'; import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default { export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/video', title: "Pages/recovery/SolveChallenge/AuthMethods/video",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,
}, },
argTypes: { argTypes: {
onUpdate: { action: 'onUpdate' }, onUpdate: { action: "onUpdate" },
onBack: { action: 'onBack' }, onBack: { action: "onBack" },
}, },
}; };
const type: KnownAuthMethods = 'video' const type: KnownAuthMethods = "video";
export const WithoutFeedback = createExample(TestedComponent[type].solve, { export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
challenges: [{ challenges: [
cost: 'USD:1', {
instructions: 'does P equals NP?', cost: "USD:1",
type: 'question', instructions: "does P equals NP?",
uuid: 'uuid-1' type: "question",
}], uuid: "uuid-1",
},
],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'uuid-1', selected_challenge_uuid: "uuid-1",
} as ReducerState, { } as ReducerState,
id: 'uuid-1', {
}); id: "uuid-1",
},
);

View File

@ -44,8 +44,16 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={() => reducer.back()}>Back</button> style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -62,8 +70,7 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch; challenges[ch.uuid] = ch;
} }
const selectedChallenge = challenges[selectedUuid]; const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid] const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|| feedback?.state === ChallengeFeedbackStatus.Redirect feedback?.state === ChallengeFeedbackStatus.Redirect ||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Add email authentication"> <AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
<p> <p>You are gonna be called to check your identity</p>
You are gonna be called to check your identity
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div <div
@ -96,9 +101,11 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton>} </AsyncButton>
)}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -1,9 +1,9 @@
import { AuthMethod } from "anastasis-core"; import { AuthMethod } from "anastasis-core";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import postalIcon from '../../../assets/icons/auth_method/postal.svg'; import postalIcon from "../../../assets/icons/auth_method/postal.svg";
import questionIcon from '../../../assets/icons/auth_method/question.svg'; import questionIcon from "../../../assets/icons/auth_method/question.svg";
import smsIcon from '../../../assets/icons/auth_method/sms.svg'; import smsIcon from "../../../assets/icons/auth_method/sms.svg";
import videoIcon from '../../../assets/icons/auth_method/video.svg'; import videoIcon from "../../../assets/icons/auth_method/video.svg";
import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup"; import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup";
import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve"; import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve";
import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup"; import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup";
@ -20,8 +20,7 @@ import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve";
import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve"; import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve";
import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve"; import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve";
export type AuthMethodWithRemove = AuthMethod & { remove: () => void };
export type AuthMethodWithRemove = AuthMethod & { remove: () => void }
export interface AuthMethodSetupProps { export interface AuthMethodSetupProps {
method: string; method: string;
@ -43,10 +42,18 @@ interface AuthMethodConfiguration {
} }
// export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban"; // export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
const ALL_METHODS = ['sms', 'email', 'post', 'question', 'video' , 'totp', 'iban'] as const; const ALL_METHODS = [
export type KnownAuthMethods = (typeof ALL_METHODS)[number]; "sms",
"email",
"post",
"question",
"video",
"totp",
"iban",
] as const;
export type KnownAuthMethods = typeof ALL_METHODS[number];
export function isKnownAuthMethods(value: string): value is KnownAuthMethods { export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
return ALL_METHODS.includes(value as KnownAuthMethods) return ALL_METHODS.includes(value as KnownAuthMethods);
} }
type KnowMethodConfig = { type KnowMethodConfig = {
@ -96,5 +103,5 @@ export const authMethods: KnowMethodConfig = {
setup: VideoSetup, setup: VideoSetup,
solve: VideoSolve, solve: VideoSolve,
skip: true, skip: true,
} },
} };

View File

@ -1,54 +1,61 @@
/* eslint-disable @typescript-eslint/camelcase */ /* eslint-disable @typescript-eslint/camelcase */
import jssha from 'jssha' import jssha from "jssha";
const SEARCH_RANGE = 16 const SEARCH_RANGE = 16;
const timeStep = 30 const timeStep = 30;
export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean { export function computeTOTPandCheck(
const now = new Date().getTime() secretKey: Uint8Array,
digits: number,
code: number,
): boolean {
const now = new Date().getTime();
const epoch = Math.floor(Math.round(now / 1000.0) / timeStep); const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) { for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
const movingFactor = (epoch + ms).toString(16).padStart(16, "0"); const movingFactor = (epoch + ms).toString(16).padStart(16, "0");
const hmacSha = new jssha('SHA-1', 'HEX', { hmacKey: { value: secretKey, format: 'UINT8ARRAY' } }); const hmacSha = new jssha("SHA-1", "HEX", {
hmacKey: { value: secretKey, format: "UINT8ARRAY" },
});
hmacSha.update(movingFactor); hmacSha.update(movingFactor);
const hmac_text = hmacSha.getHMAC('UINT8ARRAY'); const hmac_text = hmacSha.getHMAC("UINT8ARRAY");
const offset = (hmac_text[hmac_text.length - 1] & 0xf) const offset = hmac_text[hmac_text.length - 1] & 0xf;
const otp = (( const otp =
(hmac_text[offset + 0] << 24) + (((hmac_text[offset + 0] << 24) +
(hmac_text[offset + 1] << 16) + (hmac_text[offset + 1] << 16) +
(hmac_text[offset + 2] << 8) + (hmac_text[offset + 2] << 8) +
(hmac_text[offset + 3]) hmac_text[offset + 3]) &
) & 0x7fffffff) % Math.pow(10, digits) 0x7fffffff) %
Math.pow(10, digits);
if (otp == code) return true if (otp == code) return true;
} }
return false return false;
} }
const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split('') const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split("");
export function base32enc(buffer: Uint8Array): string { export function base32enc(buffer: Uint8Array): string {
let rpos = 0 let rpos = 0;
let bits = 0 let bits = 0;
let vbit = 0 let vbit = 0;
let result = "" let result = "";
while ((rpos < buffer.length) || (vbit > 0)) { while (rpos < buffer.length || vbit > 0) {
if ((rpos < buffer.length) && (vbit < 5)) { if (rpos < buffer.length && vbit < 5) {
bits = (bits << 8) | buffer[rpos++]; bits = (bits << 8) | buffer[rpos++];
vbit += 8; vbit += 8;
} }
if (vbit < 5) { if (vbit < 5) {
bits <<= (5 - vbit); bits <<= 5 - vbit;
vbit = 5; vbit = 5;
} }
result += encTable__[(bits >> (vbit - 5)) & 31]; result += encTable__[(bits >> (vbit - 5)) & 31];
vbit -= 5; vbit -= 5;
} }
return result return result;
} }
// const array = new Uint8Array(256) // const array = new Uint8Array(256)

View File

@ -1,5 +1,5 @@
import { FunctionalComponent, h } from 'preact'; import { FunctionalComponent, h } from "preact";
import { Link } from 'preact-router/match'; import { Link } from "preact-router/match";
const Notfound: FunctionalComponent = () => { const Notfound: FunctionalComponent = () => {
return ( return (

View File

@ -1,5 +1,5 @@
import { FunctionalComponent, h } from 'preact'; import { FunctionalComponent, h } from "preact";
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from "preact/hooks";
interface Props { interface Props {
user: string; user: string;
@ -33,8 +33,7 @@ const Profile: FunctionalComponent<Props> = (props: Props) => {
<div>Current time: {new Date(time).toLocaleString()}</div> <div>Current time: {new Date(time).toLocaleString()}</div>
<p> <p>
<button onClick={increment}>Click Me</button> Clicked {count}{' '} <button onClick={increment}>Click Me</button> Clicked {count} times.
times.
</p> </p>
</div> </div>
); );

View File

@ -1,15 +1,56 @@
<!--
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
-->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"> <html
lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title><% preact.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
<% preact.headEnd %> <link
rel="icon"
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
/>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
<meta
name="theme-color"
content="<%= htmlWebpackPlugin.options.manifest.theme_color %>"
/>
<% } %> <% for (const index in htmlWebpackPlugin.files.css) { %> <% const
file = htmlWebpackPlugin.files.css[index] %>
<style data-href="<%= file %>">
<%= compilation.assets[file.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</style>
<% } %>
</head> </head>
<body> <body>
<% preact.bodyEnd %> <script>
<%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
<script>
<%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
</body> </body>
</html> </html>

View File

@ -1,45 +1,67 @@
/* eslint-disable @typescript-eslint/camelcase */ /* eslint-disable @typescript-eslint/camelcase */
import { BackupStates, RecoveryStates, ReducerState } from 'anastasis-core'; import { BackupStates, RecoveryStates, ReducerState } from "anastasis-core";
import { FunctionalComponent, h, VNode } from 'preact'; import { FunctionalComponent, h, VNode } from "preact";
import { AnastasisProvider } from '../context/anastasis'; import { AnastasisProvider } from "../context/anastasis";
export function createExample<Props>(Component: FunctionalComponent<Props>, currentReducerState?: ReducerState, props?: Partial<Props>): { (args: Props): VNode } { export function createExample<Props>(
Component: FunctionalComponent<Props>,
currentReducerState?: ReducerState,
props?: Partial<Props>,
): { (args: Props): VNode } {
const r = (args: Props): VNode => { const r = (args: Props): VNode => {
return <AnastasisProvider value={{ return (
<AnastasisProvider
value={{
currentReducerState, currentReducerState,
currentError: undefined, currentError: undefined,
back: async () => { null }, back: async () => {
dismissError: async () => { null }, null;
reset: () => { null }, },
runTransaction: async () => { null }, dismissError: async () => {
startBackup: () => { null }, null;
startRecover: () => { null }, },
transition: async () => { null }, reset: () => {
}}> null;
},
runTransaction: async () => {
null;
},
startBackup: () => {
null;
},
startRecover: () => {
null;
},
transition: async () => {
null;
},
}}
>
<Component {...args} /> <Component {...args} />
</AnastasisProvider> </AnastasisProvider>
} );
r.args = props };
return r r.args = props;
return r;
} }
const base = { const base = {
continents: [ continents: [
{ {
name: "Europe" name: "Europe",
}, },
{ {
name: "India" name: "India",
}, },
{ {
name: "Asia" name: "Asia",
}, },
{ {
name: "North America" name: "North America",
}, },
{ {
name: "Testcontinent" name: "Testcontinent",
} },
], ],
countries: [ countries: [
{ {
@ -47,33 +69,33 @@ const base = {
name: "Testland", name: "Testland",
continent: "Testcontinent", continent: "Testcontinent",
continent_i18n: { continent_i18n: {
de_DE: "Testkontinent" de_DE: "Testkontinent",
}, },
name_i18n: { name_i18n: {
de_DE: "Testlandt", de_DE: "Testlandt",
de_CH: "Testlandi", de_CH: "Testlandi",
fr_FR: "Testpais", fr_FR: "Testpais",
en_UK: "Testland" en_UK: "Testland",
}, },
currency: "TESTKUDOS", currency: "TESTKUDOS",
call_code: "+00" call_code: "+00",
}, },
{ {
code: "xy", code: "xy",
name: "Demoland", name: "Demoland",
continent: "Testcontinent", continent: "Testcontinent",
continent_i18n: { continent_i18n: {
de_DE: "Testkontinent" de_DE: "Testkontinent",
}, },
name_i18n: { name_i18n: {
de_DE: "Demolandt", de_DE: "Demolandt",
de_CH: "Demolandi", de_CH: "Demolandi",
fr_FR: "Demopais", fr_FR: "Demopais",
en_UK: "Demoland" en_UK: "Demoland",
}, },
currency: "KUDOS", currency: "KUDOS",
call_code: "+01" call_code: "+01",
} },
], ],
authentication_providers: { authentication_providers: {
"http://localhost:8086/": { "http://localhost:8086/": {
@ -85,18 +107,20 @@ const base = {
methods: [ methods: [
{ {
type: "question", type: "question",
usage_fee: "COL:0" usage_fee: "COL:0",
}, { },
{
type: "sms", type: "sms",
usage_fee: "COL:0" usage_fee: "COL:0",
}, { },
{
type: "email", type: "email",
usage_fee: "COL:0" usage_fee: "COL:0",
}, },
], ],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16, storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0" truth_upload_fee: "COL:0",
}, },
"https://kudos.demo.anastasis.lu/": { "https://kudos.demo.anastasis.lu/": {
http_status: 200, http_status: 200,
@ -107,15 +131,16 @@ const base = {
methods: [ methods: [
{ {
type: "question", type: "question",
usage_fee: "COL:0" usage_fee: "COL:0",
}, { },
{
type: "email", type: "email",
usage_fee: "COL:0" usage_fee: "COL:0",
}, },
], ],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16, storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0" truth_upload_fee: "COL:0",
}, },
"https://anastasis.demo.taler.net/": { "https://anastasis.demo.taler.net/": {
http_status: 200, http_status: 200,
@ -126,43 +151,45 @@ const base = {
methods: [ methods: [
{ {
type: "question", type: "question",
usage_fee: "COL:0" usage_fee: "COL:0",
}, { },
{
type: "sms", type: "sms",
usage_fee: "COL:0" usage_fee: "COL:0",
}, { },
{
type: "totp", type: "totp",
usage_fee: "COL:0" usage_fee: "COL:0",
}, },
], ],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16, storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0" truth_upload_fee: "COL:0",
}, },
"http://localhost:8087/": { "http://localhost:8087/": {
code: 8414, code: 8414,
hint: "request to provider failed" hint: "request to provider failed",
}, },
"http://localhost:8088/": { "http://localhost:8088/": {
code: 8414, code: 8414,
hint: "request to provider failed" hint: "request to provider failed",
}, },
"http://localhost:8089/": { "http://localhost:8089/": {
code: 8414, code: 8414,
hint: "request to provider failed" hint: "request to provider failed",
} },
}, },
// expiration: { // expiration: {
// d_ms: 1792525051855 // check t_ms // d_ms: 1792525051855 // check t_ms
// }, // },
} as Partial<ReducerState> } as Partial<ReducerState>;
export const reducerStatesExample = { export const reducerStatesExample = {
initial: undefined, initial: undefined,
recoverySelectCountry: { recoverySelectCountry: {
...base, ...base,
recovery_state: RecoveryStates.CountrySelecting recovery_state: RecoveryStates.CountrySelecting,
} as ReducerState, } as ReducerState,
recoverySelectContinent: { recoverySelectContinent: {
...base, ...base,
@ -190,11 +217,11 @@ export const reducerStatesExample = {
} as ReducerState, } as ReducerState,
recoveryAttributeEditing: { recoveryAttributeEditing: {
...base, ...base,
recovery_state: RecoveryStates.UserAttributesCollecting recovery_state: RecoveryStates.UserAttributesCollecting,
} as ReducerState, } as ReducerState,
backupSelectCountry: { backupSelectCountry: {
...base, ...base,
backup_state: BackupStates.CountrySelecting backup_state: BackupStates.CountrySelecting,
} as ReducerState, } as ReducerState,
backupSelectContinent: { backupSelectContinent: {
...base, ...base,
@ -218,15 +245,14 @@ export const reducerStatesExample = {
} as ReducerState, } as ReducerState,
authEditing: { authEditing: {
...base, ...base,
backup_state: BackupStates.AuthenticationsEditing backup_state: BackupStates.AuthenticationsEditing,
} as ReducerState, } as ReducerState,
backupAttributeEditing: { backupAttributeEditing: {
...base, ...base,
backup_state: BackupStates.UserAttributesCollecting backup_state: BackupStates.UserAttributesCollecting,
} as ReducerState, } as ReducerState,
truthsPaying: { truthsPaying: {
...base, ...base,
backup_state: BackupStates.TruthsPaying backup_state: BackupStates.TruthsPaying,
} as ReducerState, } as ReducerState,
};
}

View File

@ -7,10 +7,12 @@ importers:
'@linaria/esbuild': ^3.0.0-beta.13 '@linaria/esbuild': ^3.0.0-beta.13
'@linaria/shaker': ^3.0.0-beta.13 '@linaria/shaker': ^3.0.0-beta.13
esbuild: ^0.12.29 esbuild: ^0.12.29
prettier: ^2.2.1
devDependencies: devDependencies:
'@linaria/esbuild': 3.0.0-beta.13 '@linaria/esbuild': 3.0.0-beta.13
'@linaria/shaker': 3.0.0-beta.13 '@linaria/shaker': 3.0.0-beta.13
esbuild: 0.12.29 esbuild: 0.12.29
prettier: 2.2.1
packages/anastasis-core: packages/anastasis-core:
specifiers: specifiers:
@ -62,6 +64,7 @@ importers:
'@typescript-eslint/eslint-plugin': ^5.3.0 '@typescript-eslint/eslint-plugin': ^5.3.0
'@typescript-eslint/parser': ^5.3.0 '@typescript-eslint/parser': ^5.3.0
anastasis-core: workspace:^0.0.1 anastasis-core: workspace:^0.0.1
base64-inline-loader: 1.1.1
bulma: ^0.9.3 bulma: ^0.9.3
bulma-checkbox: ^1.1.1 bulma-checkbox: ^1.1.1
bulma-radio: ^1.1.1 bulma-radio: ^1.1.1
@ -86,6 +89,7 @@ importers:
dependencies: dependencies:
'@gnu-taler/taler-util': link:../taler-util '@gnu-taler/taler-util': link:../taler-util
anastasis-core: link:../anastasis-core anastasis-core: link:../anastasis-core
base64-inline-loader: 1.1.1
date-fns: 2.25.0 date-fns: 2.25.0
jed: 1.1.1 jed: 1.1.1
preact: 10.5.15 preact: 10.5.15
@ -10800,7 +10804,6 @@ packages:
ajv: ^6.9.1 ajv: ^6.9.1
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
dev: true
/ajv/6.12.6: /ajv/6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -10809,7 +10812,6 @@ packages:
fast-json-stable-stringify: 2.1.0 fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1 json-schema-traverse: 0.4.1
uri-js: 4.4.1 uri-js: 4.4.1
dev: true
/ajv/7.0.3: /ajv/7.0.3:
resolution: {integrity: sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==} resolution: {integrity: sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==}
@ -11871,6 +11873,17 @@ packages:
pascalcase: 0.1.1 pascalcase: 0.1.1
dev: true dev: true
/base64-inline-loader/1.1.1:
resolution: {integrity: sha512-v/bHvXQ8sW28t9ZwBsFGgyqZw2bpT49/dtPTtlmixoSM/s9wnOngOKFVQLRH8BfMTy6jTl5m5CdvqpZt8y5d6g==}
engines: {node: '>=6.2', npm: '>=3.8'}
peerDependencies:
webpack: ^4.x
dependencies:
file-loader: 1.1.11
loader-utils: 1.4.0
mime-types: 2.1.33
dev: false
/base64-js/1.5.1: /base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true dev: true
@ -11907,7 +11920,6 @@ packages:
/big.js/5.2.2: /big.js/5.2.2:
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
dev: true
/binary-extensions/1.13.1: /binary-extensions/1.13.1:
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
@ -13794,7 +13806,7 @@ packages:
dependencies: dependencies:
globby: 11.0.4 globby: 11.0.4
graceful-fs: 4.2.8 graceful-fs: 4.2.8
is-glob: 4.0.1 is-glob: 4.0.3
is-path-cwd: 2.2.0 is-path-cwd: 2.2.0
is-path-inside: 3.0.3 is-path-inside: 3.0.3
p-map: 4.0.0 p-map: 4.0.0
@ -14177,7 +14189,6 @@ packages:
/emojis-list/3.0.0: /emojis-list/3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
dev: true
/emotion-theming/10.0.27_5f216699bc8c1f24088b3bf77b7cbbdf: /emotion-theming/10.0.27_5f216699bc8c1f24088b3bf77b7cbbdf:
resolution: {integrity: sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==} resolution: {integrity: sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==}
@ -15174,7 +15185,6 @@ packages:
/fast-deep-equal/3.1.3: /fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-diff/1.2.0: /fast-diff/1.2.0:
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
@ -15217,7 +15227,6 @@ packages:
/fast-json-stable-stringify/2.1.0: /fast-json-stable-stringify/2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
dev: true
/fast-levenshtein/2.0.6: /fast-levenshtein/2.0.6:
resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=} resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=}
@ -15296,6 +15305,16 @@ packages:
flat-cache: 3.0.4 flat-cache: 3.0.4
dev: true dev: true
/file-loader/1.1.11:
resolution: {integrity: sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==}
engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'}
peerDependencies:
webpack: ^2.0.0 || ^3.0.0 || ^4.0.0
dependencies:
loader-utils: 1.4.0
schema-utils: 0.4.7
dev: false
/file-loader/6.2.0_webpack@4.46.0: /file-loader/6.2.0_webpack@4.46.0:
resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
@ -18694,7 +18713,6 @@ packages:
/json-schema-traverse/0.4.1: /json-schema-traverse/0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
/json-schema-traverse/1.0.0: /json-schema-traverse/1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
@ -18730,7 +18748,6 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
minimist: 1.2.5 minimist: 1.2.5
dev: true
/json5/2.1.3: /json5/2.1.3:
resolution: {integrity: sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==} resolution: {integrity: sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==}
@ -18970,7 +18987,6 @@ packages:
big.js: 5.2.2 big.js: 5.2.2
emojis-list: 3.0.0 emojis-list: 3.0.0
json5: 1.0.1 json5: 1.0.1
dev: true
/loader-utils/2.0.0: /loader-utils/2.0.0:
resolution: {integrity: sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==} resolution: {integrity: sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==}
@ -19420,14 +19436,12 @@ packages:
/mime-db/1.50.0: /mime-db/1.50.0:
resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==} resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: true
/mime-types/2.1.33: /mime-types/2.1.33:
resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==} resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dependencies: dependencies:
mime-db: 1.50.0 mime-db: 1.50.0
dev: true
/mime/1.6.0: /mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@ -19501,7 +19515,6 @@ packages:
/minimist/1.2.5: /minimist/1.2.5:
resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==}
dev: true
/minipass-collect/1.0.2: /minipass-collect/1.0.2:
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
@ -22013,7 +22026,6 @@ packages:
/punycode/2.1.1: /punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true
/pupa/2.1.1: /pupa/2.1.1:
resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==}
@ -23312,6 +23324,14 @@ packages:
object-assign: 4.1.1 object-assign: 4.1.1
dev: true dev: true
/schema-utils/0.4.7:
resolution: {integrity: sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==}
engines: {node: '>= 4'}
dependencies:
ajv: 6.12.6
ajv-keywords: 3.5.2_ajv@6.12.6
dev: false
/schema-utils/1.0.0: /schema-utils/1.0.0:
resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==} resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -25212,7 +25232,6 @@ packages:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies: dependencies:
punycode: 2.1.1 punycode: 2.1.1
dev: true
/urix/0.1.0: /urix/0.1.0:
resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=} resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=}