prettier
This commit is contained in:
parent
e03b0d1b9b
commit
a62deeef5d
@ -10,6 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@linaria/esbuild": "^3.0.0-beta.13",
|
||||
"@linaria/shaker": "^3.0.0-beta.13",
|
||||
"esbuild": "^0.12.29"
|
||||
"esbuild": "^0.12.29",
|
||||
"prettier": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,14 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "preact build --no-sw --no-esm",
|
||||
"serve": "sirv build --port 8080 --cors --single",
|
||||
"dev": "preact watch --no-sw --no-esm",
|
||||
"serve": "sirv build --port ${PORT:=8080} --cors --single",
|
||||
"dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"test": "jest ./tests",
|
||||
"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"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@ -25,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@gnu-taler/taler-util": "workspace:^0.8.3",
|
||||
"anastasis-core": "workspace:^0.0.1",
|
||||
"base64-inline-loader": "1.1.1",
|
||||
"date-fns": "2.25.0",
|
||||
"jed": "1.1.1",
|
||||
"preact": "^10.5.15",
|
||||
|
@ -1,5 +1,3 @@
|
||||
{
|
||||
"presets": [
|
||||
"preact-cli/babel"
|
||||
]
|
||||
"presets": ["preact-cli/babel"]
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ComponentChildren, h, VNode } from "preact";
|
||||
// import { LoadingModal } from "../modal";
|
||||
@ -31,19 +31,26 @@ type Props = {
|
||||
[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);
|
||||
|
||||
// if (isSlow) {
|
||||
// return <LoadingModal onCancel={cancel} />;
|
||||
// }
|
||||
if (isLoading) {
|
||||
if (isLoading) {
|
||||
return <button class="button">Loading...</button>;
|
||||
}
|
||||
|
||||
return <span data-tooltip={rest['data-tooltip']} style={{marginLeft: 5}}>
|
||||
<button {...rest} onClick={request} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
</span>;
|
||||
return (
|
||||
<span data-tooltip={rest["data-tooltip"]} style={{ marginLeft: 5 }}>
|
||||
<button {...rest} onClick={request} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { h, VNode } from "preact";
|
||||
|
||||
@ -27,7 +27,7 @@ export interface Notification {
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
|
||||
export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
|
||||
|
||||
interface Props {
|
||||
notifications: Notification[];
|
||||
@ -36,24 +36,39 @@ interface Props {
|
||||
|
||||
function messageStyle(type: MessageType): string {
|
||||
switch (type) {
|
||||
case "INFO": return "message is-info";
|
||||
case "WARN": return "message is-warning";
|
||||
case "ERROR": return "message is-danger";
|
||||
case "SUCCESS": return "message is-success";
|
||||
default: return "message"
|
||||
case "INFO":
|
||||
return "message is-info";
|
||||
case "WARN":
|
||||
return "message is-warning";
|
||||
case "ERROR":
|
||||
return "message is-danger";
|
||||
case "SUCCESS":
|
||||
return "message is-success";
|
||||
default:
|
||||
return "message";
|
||||
}
|
||||
}
|
||||
|
||||
export function Notifications({ notifications, removeNotification }: Props): VNode {
|
||||
return <div class="block">
|
||||
{notifications.map((n, i) => <article key={i} class={messageStyle(n.type)}>
|
||||
<div class="message-header">
|
||||
<p>{n.message}</p>
|
||||
{removeNotification && <button class="delete" onClick={() => removeNotification && removeNotification(n)} />}
|
||||
</div>
|
||||
{n.description && <div class="message-body">
|
||||
{n.description}
|
||||
</div>}
|
||||
</article>)}
|
||||
</div>
|
||||
}
|
||||
export function Notifications({
|
||||
notifications,
|
||||
removeNotification,
|
||||
}: Props): VNode {
|
||||
return (
|
||||
<div class="block">
|
||||
{notifications.map((n, i) => (
|
||||
<article key={i} class={messageStyle(n.type)}>
|
||||
<div class="message-header">
|
||||
<p>{n.message}</p>
|
||||
{removeNotification && (
|
||||
<button
|
||||
class="delete"
|
||||
onClick={() => removeNotification && removeNotification(n)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{n.description && <div class="message-body">{n.description}</div>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,15 +21,28 @@ import qrcode from "qrcode-generator";
|
||||
export function QR({ text }: { text: string }): VNode {
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const qr = qrcode(0, 'L');
|
||||
const qr = qrcode(0, "L");
|
||||
qr.addData(text);
|
||||
qr.make();
|
||||
if (divRef.current) divRef.current.innerHTML = qr.createSvgTag({
|
||||
scalable: true,
|
||||
});
|
||||
if (divRef.current)
|
||||
divRef.current.innerHTML = qr.createSvgTag({
|
||||
scalable: true,
|
||||
});
|
||||
});
|
||||
|
||||
return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
|
||||
</div>;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
|
||||
ref={divRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { FunctionalComponent, h } from "preact";
|
||||
import { TranslationProvider } from "../context/translation";
|
||||
|
||||
import AnastasisClient from "../pages/home";
|
||||
|
||||
const App: FunctionalComponent = () => {
|
||||
|
@ -19,56 +19,66 @@ export function DateInput(props: DateInputProps): VNode {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [props.grabFocus]);
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const value = props.bind[0] || "";
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const showError = dirty && props.error
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const showError = dirty && props.error;
|
||||
|
||||
const calendar = subYears(new Date(), 30)
|
||||
|
||||
return <div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="field has-addons">
|
||||
<p class="control">
|
||||
<input
|
||||
type="text"
|
||||
class={showError ? 'input is-danger' : 'input'}
|
||||
value={value}
|
||||
onInput={(e) => {
|
||||
const text = e.currentTarget.value
|
||||
setDirty(true)
|
||||
props.bind[1](text);
|
||||
}}
|
||||
ref={inputRef} />
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button" onClick={() => { setOpened(true) }}>
|
||||
<span class="icon"><i class="mdi mdi-calendar" /></span>
|
||||
</a>
|
||||
</p>
|
||||
const calendar = subYears(new Date(), 30);
|
||||
|
||||
return (
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && (
|
||||
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="field has-addons">
|
||||
<p class="control">
|
||||
<input
|
||||
type="text"
|
||||
class={showError ? "input is-danger" : "input"}
|
||||
value={value}
|
||||
onInput={(e) => {
|
||||
const text = e.currentTarget.value;
|
||||
setDirty(true);
|
||||
props.bind[1](text);
|
||||
}}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a
|
||||
class="button"
|
||||
onClick={() => {
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-calendar" />
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">Using the format yyyy-mm-dd</p>
|
||||
{showError && <p class="help is-danger">{props.error}</p>}
|
||||
<DatePicker
|
||||
opened={opened}
|
||||
initialDate={calendar}
|
||||
years={props.years}
|
||||
closeFunction={() => setOpened(false)}
|
||||
dateReceiver={(d) => {
|
||||
setDirty(true);
|
||||
const v = format(d, "yyyy-MM-dd");
|
||||
props.bind[1](v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p class="help">Using the format yyyy-mm-dd</p>
|
||||
{showError && <p class="help is-danger">{props.error}</p>}
|
||||
<DatePicker
|
||||
opened={opened}
|
||||
initialDate={calendar}
|
||||
years={props.years}
|
||||
closeFunction={() => setOpened(false)}
|
||||
dateReceiver={(d) => {
|
||||
setDirty(true)
|
||||
const v = format(d, 'yyyy-MM-dd')
|
||||
props.bind[1](v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
;
|
||||
|
||||
);
|
||||
}
|
||||
|
@ -18,27 +18,34 @@ export function EmailInput(props: TextInputProps): VNode {
|
||||
}
|
||||
}, [props.grabFocus]);
|
||||
const value = props.bind[0];
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const showError = dirty && props.error
|
||||
return (<div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>}
|
||||
</label>
|
||||
<div class="control has-icons-right">
|
||||
<input
|
||||
value={value}
|
||||
required
|
||||
placeholder={props.placeholder}
|
||||
type="email"
|
||||
class={showError ? 'input is-danger' : 'input'}
|
||||
onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
||||
ref={inputRef}
|
||||
style={{ display: "block" }} />
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const showError = dirty && props.error;
|
||||
return (
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && (
|
||||
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div class="control has-icons-right">
|
||||
<input
|
||||
value={value}
|
||||
required
|
||||
placeholder={props.placeholder}
|
||||
type="email"
|
||||
class={showError ? "input is-danger" : "input"}
|
||||
onInput={(e) => {
|
||||
setDirty(true);
|
||||
props.bind[1]((e.target as HTMLInputElement).value);
|
||||
}}
|
||||
ref={inputRef}
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
</div>
|
||||
{showError && <p class="help is-danger">{props.error}</p>}
|
||||
</div>
|
||||
{showError && <p class="help is-danger">{props.error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,14 +15,14 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
import { h, VNode } from "preact";
|
||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import { TextInputProps } from "./TextInput";
|
||||
|
||||
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
|
||||
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
|
||||
|
||||
export function FileInput(props: TextInputProps): VNode {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -34,48 +34,54 @@ export function FileInput(props: TextInputProps): VNode {
|
||||
|
||||
const value = props.bind[0];
|
||||
// const [dirty, setDirty] = useState(false)
|
||||
const image = useRef<HTMLInputElement>(null)
|
||||
const [sizeError, setSizeError] = useState(false)
|
||||
const image = useRef<HTMLInputElement>(null);
|
||||
const [sizeError, setSizeError] = useState(false);
|
||||
function onChange(v: string): void {
|
||||
// setDirty(true);
|
||||
props.bind[1](v);
|
||||
}
|
||||
return <div class="field">
|
||||
<label class="label">
|
||||
<a onClick={() => image.current?.click()}>
|
||||
{props.label}
|
||||
</a>
|
||||
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
ref={image} style={{ display: 'none' }}
|
||||
type="file" name={String(name)}
|
||||
onChange={e => {
|
||||
const f: FileList | null = e.currentTarget.files
|
||||
if (!f || f.length != 1) {
|
||||
return onChange("")
|
||||
}
|
||||
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
|
||||
setSizeError(true)
|
||||
return onChange("")
|
||||
}
|
||||
setSizeError(false)
|
||||
return f[0].arrayBuffer().then(b => {
|
||||
const b64 = btoa(
|
||||
new Uint8Array(b)
|
||||
.reduce((data, byte) => data + String.fromCharCode(byte), '')
|
||||
)
|
||||
return onChange(`data:${f[0].type};base64,${b64}` as any)
|
||||
})
|
||||
}} />
|
||||
{props.error && <p class="help is-danger">{props.error}</p>}
|
||||
{sizeError && <p class="help is-danger">
|
||||
File should be smaller than 1 MB
|
||||
</p>}
|
||||
return (
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<a onClick={() => image.current?.click()}>{props.label}</a>
|
||||
{props.tooltip && (
|
||||
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
ref={image}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
name={String(name)}
|
||||
onChange={(e) => {
|
||||
const f: FileList | null = e.currentTarget.files;
|
||||
if (!f || f.length != 1) {
|
||||
return onChange("");
|
||||
}
|
||||
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
|
||||
setSizeError(true);
|
||||
return onChange("");
|
||||
}
|
||||
setSizeError(false);
|
||||
return f[0].arrayBuffer().then((b) => {
|
||||
const b64 = btoa(
|
||||
new Uint8Array(b).reduce(
|
||||
(data, byte) => data + String.fromCharCode(byte),
|
||||
"",
|
||||
),
|
||||
);
|
||||
return onChange(`data:${f[0].type};base64,${b64}` as any);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{props.error && <p class="help is-danger">{props.error}</p>}
|
||||
{sizeError && (
|
||||
<p class="help is-danger">File should be smaller than 1 MB</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -15,15 +15,15 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
import { h, VNode } from "preact";
|
||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import emptyImage from "../../assets/empty.png";
|
||||
import { TextInputProps } from "./TextInput";
|
||||
|
||||
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
|
||||
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
|
||||
|
||||
export function ImageInput(props: TextInputProps): VNode {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -35,47 +35,59 @@ export function ImageInput(props: TextInputProps): VNode {
|
||||
|
||||
const value = props.bind[0];
|
||||
// const [dirty, setDirty] = useState(false)
|
||||
const image = useRef<HTMLInputElement>(null)
|
||||
const [sizeError, setSizeError] = useState(false)
|
||||
const image = useRef<HTMLInputElement>(null);
|
||||
const [sizeError, setSizeError] = useState(false);
|
||||
function onChange(v: string): void {
|
||||
// setDirty(true);
|
||||
props.bind[1](v);
|
||||
}
|
||||
return <div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>}
|
||||
</label>
|
||||
<div class="control">
|
||||
<img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} />
|
||||
<input
|
||||
ref={image} style={{ display: 'none' }}
|
||||
type="file" name={String(name)}
|
||||
onChange={e => {
|
||||
const f: FileList | null = e.currentTarget.files
|
||||
if (!f || f.length != 1) {
|
||||
return onChange(emptyImage)
|
||||
}
|
||||
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
|
||||
setSizeError(true)
|
||||
return onChange(emptyImage)
|
||||
}
|
||||
setSizeError(false)
|
||||
return f[0].arrayBuffer().then(b => {
|
||||
const b64 = btoa(
|
||||
new Uint8Array(b)
|
||||
.reduce((data, byte) => data + String.fromCharCode(byte), '')
|
||||
)
|
||||
return onChange(`data:${f[0].type};base64,${b64}` as any)
|
||||
})
|
||||
}} />
|
||||
{props.error && <p class="help is-danger">{props.error}</p>}
|
||||
{sizeError && <p class="help is-danger">
|
||||
Image should be smaller than 1 MB
|
||||
</p>}
|
||||
return (
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && (
|
||||
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div class="control">
|
||||
<img
|
||||
src={!value ? emptyImage : value}
|
||||
style={{ width: 200, height: 200 }}
|
||||
onClick={() => image.current?.click()}
|
||||
/>
|
||||
<input
|
||||
ref={image}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
name={String(name)}
|
||||
onChange={(e) => {
|
||||
const f: FileList | null = e.currentTarget.files;
|
||||
if (!f || f.length != 1) {
|
||||
return onChange(emptyImage);
|
||||
}
|
||||
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
|
||||
setSizeError(true);
|
||||
return onChange(emptyImage);
|
||||
}
|
||||
setSizeError(false);
|
||||
return f[0].arrayBuffer().then((b) => {
|
||||
const b64 = btoa(
|
||||
new Uint8Array(b).reduce(
|
||||
(data, byte) => data + String.fromCharCode(byte),
|
||||
"",
|
||||
),
|
||||
);
|
||||
return onChange(`data:${f[0].type};base64,${b64}` as any);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{props.error && <p class="help is-danger">{props.error}</p>}
|
||||
{sizeError && (
|
||||
<p class="help is-danger">Image should be smaller than 1 MB</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -18,25 +18,32 @@ export function TextInput(props: TextInputProps): VNode {
|
||||
}
|
||||
}, [props.grabFocus]);
|
||||
const value = props.bind[0];
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const showError = dirty && props.error
|
||||
return (<div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>}
|
||||
</label>
|
||||
<div class="control has-icons-right">
|
||||
<input
|
||||
value={value}
|
||||
placeholder={props.placeholder}
|
||||
class={showError ? 'input is-danger' : 'input'}
|
||||
onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
||||
ref={inputRef}
|
||||
style={{ display: "block" }} />
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const showError = dirty && props.error;
|
||||
return (
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{props.label}
|
||||
{props.tooltip && (
|
||||
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||
<i class="mdi mdi-information" />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div class="control has-icons-right">
|
||||
<input
|
||||
value={value}
|
||||
placeholder={props.placeholder}
|
||||
class={showError ? "input is-danger" : "input"}
|
||||
onInput={(e) => {
|
||||
setDirty(true);
|
||||
props.bind[1]((e.target as HTMLInputElement).value);
|
||||
}}
|
||||
ref={inputRef}
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
</div>
|
||||
{showError && <p class="help is-danger">{props.error}</p>}
|
||||
</div>
|
||||
{showError && <p class="help is-danger">{props.error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,59 +15,78 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { h, VNode } from "preact";
|
||||
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 { strings as messages } from '../../i18n/strings'
|
||||
import { strings as messages } from "../../i18n/strings";
|
||||
|
||||
type LangsNames = {
|
||||
[P in keyof typeof messages]: string
|
||||
}
|
||||
[P in keyof typeof messages]: string;
|
||||
};
|
||||
|
||||
const names: LangsNames = {
|
||||
es: 'Español [es]',
|
||||
en: 'English [en]',
|
||||
fr: 'Français [fr]',
|
||||
de: 'Deutsch [de]',
|
||||
sv: 'Svenska [sv]',
|
||||
it: 'Italiano [it]',
|
||||
}
|
||||
es: "Español [es]",
|
||||
en: "English [en]",
|
||||
fr: "Français [fr]",
|
||||
de: "Deutsch [de]",
|
||||
sv: "Svenska [sv]",
|
||||
it: "Italiano [it]",
|
||||
};
|
||||
|
||||
function getLangName(s: keyof LangsNames | string): string {
|
||||
if (names[s]) return names[s]
|
||||
return String(s)
|
||||
if (names[s]) return names[s];
|
||||
return String(s);
|
||||
}
|
||||
|
||||
export function LangSelector(): VNode {
|
||||
const [updatingLang, setUpdatingLang] = useState(false)
|
||||
const { lang, changeLanguage } = useTranslationContext()
|
||||
const [updatingLang, setUpdatingLang] = useState(false);
|
||||
const { lang, changeLanguage } = useTranslationContext();
|
||||
|
||||
return <div class="dropdown is-active ">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button has-tooltip-left"
|
||||
data-tooltip="change language selection"
|
||||
aria-haspopup="true"
|
||||
aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}>
|
||||
<div class="icon is-small is-left">
|
||||
<img src={langIcon} />
|
||||
</div>
|
||||
<span>{getLangName(lang)}</span>
|
||||
<div class="icon is-right">
|
||||
<i class="mdi mdi-chevron-down" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
{Object.keys(messages)
|
||||
.filter((l) => l !== lang)
|
||||
.map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
|
||||
return (
|
||||
<div class="dropdown is-active ">
|
||||
<div class="dropdown-trigger">
|
||||
<button
|
||||
class="button has-tooltip-left"
|
||||
data-tooltip="change language selection"
|
||||
aria-haspopup="true"
|
||||
aria-controls="dropdown-menu"
|
||||
onClick={() => setUpdatingLang(!updatingLang)}
|
||||
>
|
||||
<div class="icon is-small is-left">
|
||||
<img src={langIcon} />
|
||||
</div>
|
||||
<span>{getLangName(lang)}</span>
|
||||
<div class="icon is-right">
|
||||
<i class="mdi mdi-chevron-down" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
{updatingLang && (
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
{Object.keys(messages)
|
||||
.filter((l) => l !== lang)
|
||||
.map((l) => (
|
||||
<a
|
||||
key={l}
|
||||
class="dropdown-item"
|
||||
value={l}
|
||||
onClick={() => {
|
||||
changeLanguage(l);
|
||||
setUpdatingLang(false);
|
||||
}}
|
||||
>
|
||||
{getLangName(l)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,16 +15,15 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
|
||||
import { Fragment, h, VNode } from 'preact';
|
||||
import { BackupStates, RecoveryStates } from '../../../../anastasis-core/lib';
|
||||
import { useAnastasisContext } from '../../context/anastasis';
|
||||
import { Translate } from '../../i18n';
|
||||
import { LangSelector } from './LangSelector';
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { BackupStates, RecoveryStates } from "../../../../anastasis-core/lib";
|
||||
import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { Translate } from "../../i18n";
|
||||
import { LangSelector } from "./LangSelector";
|
||||
|
||||
interface Props {
|
||||
mobile?: boolean;
|
||||
@ -32,10 +31,10 @@ interface Props {
|
||||
|
||||
export function Sidebar({ mobile }: Props): VNode {
|
||||
// const config = useConfigContext();
|
||||
const config = { version: 'none' }
|
||||
const config = { version: "none" };
|
||||
// FIXME: add replacement for __VERSION__ with the current version
|
||||
const process = { env: { __VERSION__: '0.0.0' } }
|
||||
const reducer = useAnastasisContext()!
|
||||
const process = { env: { __VERSION__: "0.0.0" } };
|
||||
const reducer = useAnastasisContext()!;
|
||||
|
||||
return (
|
||||
<aside class="aside is-placed-left is-expanded">
|
||||
@ -44,114 +43,235 @@ export function Sidebar({ mobile }: Props): VNode {
|
||||
</div>} */}
|
||||
<div class="aside-tools">
|
||||
<div class="aside-tools-label">
|
||||
<div><b>Anastasis</b></div>
|
||||
<div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}>
|
||||
<div>
|
||||
<b>Anastasis</b>
|
||||
</div>
|
||||
<div
|
||||
class="is-size-7 has-text-right"
|
||||
style={{ lineHeight: 0, marginTop: -10 }}
|
||||
>
|
||||
Version {process.env.__VERSION__} ({config.version})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu is-menu-main">
|
||||
{!reducer.currentReducerState &&
|
||||
{!reducer.currentReducerState && (
|
||||
<p class="menu-label">
|
||||
<Translate>Backup or Recorver</Translate>
|
||||
</p>
|
||||
}
|
||||
)}
|
||||
<ul class="menu-list">
|
||||
{!reducer.currentReducerState &&
|
||||
{!reducer.currentReducerState && (
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
}
|
||||
{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">
|
||||
<span class="menu-item-label"><Translate>Location</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label"><Translate>Personal information</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
|
||||
<span class="menu-item-label"><Translate>Authorization methods</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
|
||||
<span class="menu-item-label"><Translate>Policies</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
|
||||
<span class="menu-item-label"><Translate>Secret input</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? '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">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Location</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.backup_state ===
|
||||
BackupStates.UserAttributesCollecting
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Personal information</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.backup_state ===
|
||||
BackupStates.AuthenticationsEditing
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Authorization methods</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.backup_state ===
|
||||
BackupStates.PoliciesReviewing
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Policies</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.backup_state ===
|
||||
BackupStates.SecretEditing
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Secret input</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
|
||||
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
|
||||
</div>
|
||||
</li> */}
|
||||
<li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
|
||||
<span class="menu-item-label"><Translate>Backup completed</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.backup_state ===
|
||||
BackupStates.BackupFinished
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Backup completed</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
|
||||
<span class="menu-item-label"><Translate>Truth Paying</Translate></span>
|
||||
</div>
|
||||
</li> */}
|
||||
</Fragment> : (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">
|
||||
<span class="menu-item-label"><Translate>Location</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label"><Translate>Personal information</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label"><Translate>Secret selection</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ||
|
||||
reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label"><Translate>Solve Challenges</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label"><Translate>Secret recovered</Translate></span>
|
||||
</div>
|
||||
</li>
|
||||
</Fragment>)}
|
||||
{reducer.currentReducerState &&
|
||||
</Fragment>
|
||||
) : (
|
||||
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">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Location</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.recovery_state ===
|
||||
RecoveryStates.UserAttributesCollecting
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Personal information</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.recovery_state ===
|
||||
RecoveryStates.SecretSelecting
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Secret selection</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.recovery_state ===
|
||||
RecoveryStates.ChallengeSelecting ||
|
||||
reducer.currentReducerState.recovery_state ===
|
||||
RecoveryStates.ChallengeSolving
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Solve Challenges</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class={
|
||||
reducer.currentReducerState.recovery_state ===
|
||||
RecoveryStates.RecoveryFinished
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div class="ml-4">
|
||||
<span class="menu-item-label">
|
||||
<Translate>Secret recovered</Translate>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
{reducer.currentReducerState && (
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
}
|
||||
|
||||
)}
|
||||
{/* <li>
|
||||
<div class="buttons ml-4">
|
||||
<button class="button is-info is-right" >Manage providers</button>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -85,8 +85,8 @@ export function NotificationCard({
|
||||
n.type === "ERROR"
|
||||
? "message is-danger"
|
||||
: n.type === "WARN"
|
||||
? "message is-warning"
|
||||
: "message is-info"
|
||||
? "message is-warning"
|
||||
: "message is-info"
|
||||
}
|
||||
>
|
||||
<div class="message-header">
|
||||
@ -113,7 +113,7 @@ export function NotYetReadyAppMenu({
|
||||
return (
|
||||
<div
|
||||
class="has-aside-mobile-expanded"
|
||||
// class={mobileOpen ? "has-aside-mobile-expanded" : ""}
|
||||
// class={mobileOpen ? "has-aside-mobile-expanded" : ""}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
<NavigationBar
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { h, Component } from "preact";
|
||||
|
||||
@ -34,83 +34,71 @@ interface State {
|
||||
selectYearMode: boolean;
|
||||
currentDate: Date;
|
||||
}
|
||||
const now = new Date()
|
||||
const now = new Date();
|
||||
|
||||
const monthArrShortFull = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
]
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
const monthArrShort = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
const dayArr = [
|
||||
'Sun',
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat'
|
||||
]
|
||||
|
||||
const yearArr: number[] = []
|
||||
const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
const yearArr: number[] = [];
|
||||
|
||||
// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
|
||||
export class DatePicker extends Component<Props, State> {
|
||||
|
||||
closeDatePicker() {
|
||||
this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets fired when a day gets clicked.
|
||||
* @param {object} e The event thrown by the <span /> element clicked
|
||||
*/
|
||||
* Gets fired when a day gets clicked.
|
||||
* @param {object} e The event thrown by the <span /> element clicked
|
||||
*/
|
||||
dayClicked(e: any) {
|
||||
|
||||
const element = e.target; // the actual element clicked
|
||||
|
||||
if (element.innerHTML === '') return false; // don't continue if <span /> empty
|
||||
if (element.innerHTML === "") return false; // don't continue if <span /> empty
|
||||
|
||||
// get date from clicked element (gets attached when rendered)
|
||||
const date = new Date(element.getAttribute('data-value'));
|
||||
const date = new Date(element.getAttribute("data-value"));
|
||||
|
||||
// update the state
|
||||
this.setState({ currentDate: date });
|
||||
this.passDateToParent(date)
|
||||
this.passDateToParent(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns days in month as array
|
||||
* @param {number} month the month to display
|
||||
* @param {number} year the year to display
|
||||
*/
|
||||
* returns days in month as array
|
||||
* @param {number} month the month to display
|
||||
* @param {number} year the year to display
|
||||
*/
|
||||
getDaysByMonth(month: number, year: number) {
|
||||
|
||||
const calendar = [];
|
||||
|
||||
const date = new Date(year, month, 1); // month to display
|
||||
@ -122,15 +110,17 @@ export class DatePicker extends Component<Props, State> {
|
||||
|
||||
// the calendar is 7*6 fields big, so 42 loops
|
||||
for (let i = 0; i < 42; i++) {
|
||||
|
||||
if (i >= firstDay && day !== null) day = day + 1;
|
||||
if (day !== null && day > lastDate) day = null;
|
||||
|
||||
// append the calendar Array
|
||||
calendar.push({
|
||||
day: (day === 0 || day === null) ? null : day, // null or number
|
||||
date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date()
|
||||
today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean
|
||||
day: day === 0 || day === null ? null : day, // null or number
|
||||
date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date()
|
||||
today:
|
||||
day === now.getDate() &&
|
||||
month === now.getMonth() &&
|
||||
year === now.getFullYear(), // boolean
|
||||
});
|
||||
}
|
||||
|
||||
@ -138,51 +128,48 @@ export class DatePicker extends Component<Props, State> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display previous month by updating state
|
||||
*/
|
||||
* Display previous month by updating state
|
||||
*/
|
||||
displayPrevMonth() {
|
||||
if (this.state.displayedMonth <= 0) {
|
||||
this.setState({
|
||||
displayedMonth: 11,
|
||||
displayedYear: this.state.displayedYear - 1
|
||||
displayedYear: this.state.displayedYear - 1,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.setState({
|
||||
displayedMonth: this.state.displayedMonth - 1
|
||||
displayedMonth: this.state.displayedMonth - 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display next month by updating state
|
||||
*/
|
||||
* Display next month by updating state
|
||||
*/
|
||||
displayNextMonth() {
|
||||
if (this.state.displayedMonth >= 11) {
|
||||
this.setState({
|
||||
displayedMonth: 0,
|
||||
displayedYear: this.state.displayedYear + 1
|
||||
displayedYear: this.state.displayedYear + 1,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.setState({
|
||||
displayedMonth: this.state.displayedMonth + 1
|
||||
displayedMonth: this.state.displayedMonth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the selected month (gets fired when clicking on the date string)
|
||||
*/
|
||||
* Display the selected month (gets fired when clicking on the date string)
|
||||
*/
|
||||
displaySelectedMonth() {
|
||||
if (this.state.selectYearMode) {
|
||||
this.toggleYearSelector();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!this.state.currentDate) return false;
|
||||
this.setState({
|
||||
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) {
|
||||
const element = e.target;
|
||||
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() {
|
||||
this.passDateToParent(this.state.currentDate)
|
||||
this.passDateToParent(this.state.currentDate);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@ -233,94 +224,133 @@ export class DatePicker extends Component<Props, State> {
|
||||
currentDate: initial,
|
||||
displayedMonth: initial.getMonth(),
|
||||
displayedYear: initial.getFullYear(),
|
||||
selectYearMode: false
|
||||
}
|
||||
selectYearMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
|
||||
const {
|
||||
currentDate,
|
||||
displayedMonth,
|
||||
displayedYear,
|
||||
selectYearMode,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}>
|
||||
|
||||
<div class={`datePicker ${this.props.opened && "datePicker--opened"}`}>
|
||||
<div class="datePicker--titles">
|
||||
<h3 style={{
|
||||
color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
|
||||
}} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3>
|
||||
<h2 style={{
|
||||
color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
|
||||
}} onClick={this.displaySelectedMonth}>
|
||||
{dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
|
||||
<h3
|
||||
style={{
|
||||
color: selectYearMode
|
||||
? "rgba(255,255,255,.87)"
|
||||
: "rgba(255,255,255,.57)",
|
||||
}}
|
||||
onClick={this.toggleYearSelector}
|
||||
>
|
||||
{currentDate.getFullYear()}
|
||||
</h3>
|
||||
<h2
|
||||
style={{
|
||||
color: !selectYearMode
|
||||
? "rgba(255,255,255,.87)"
|
||||
: "rgba(255,255,255,.57)",
|
||||
}}
|
||||
onClick={this.displaySelectedMonth}
|
||||
>
|
||||
{dayArr[currentDate.getDay()]},{" "}
|
||||
{monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{!selectYearMode && <nav>
|
||||
<span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span>
|
||||
<h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4>
|
||||
<span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span>
|
||||
</nav>}
|
||||
{!selectYearMode && (
|
||||
<nav>
|
||||
<span onClick={this.displayPrevMonth} class="icon">
|
||||
<i
|
||||
style={{ transform: "rotate(180deg)" }}
|
||||
class="mdi mdi-forward"
|
||||
/>
|
||||
</span>
|
||||
<h4>
|
||||
{monthArrShortFull[displayedMonth]} {displayedYear}
|
||||
</h4>
|
||||
<span onClick={this.displayNextMonth} class="icon">
|
||||
<i class="mdi mdi-forward" />
|
||||
</span>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<div class="datePicker--scroll">
|
||||
{!selectYearMode && (
|
||||
<div class="datePicker--calendar">
|
||||
<div class="datePicker--dayNames">
|
||||
{["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
|
||||
<span key={i}>{day}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!selectYearMode && <div class="datePicker--calendar" >
|
||||
|
||||
<div class="datePicker--dayNames">
|
||||
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)}
|
||||
</div>
|
||||
|
||||
<div onClick={this.dayClicked} class="datePicker--days">
|
||||
|
||||
{/*
|
||||
<div onClick={this.dayClicked} class="datePicker--days">
|
||||
{/*
|
||||
Loop through the calendar object returned by getDaysByMonth().
|
||||
*/}
|
||||
|
||||
{this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear)
|
||||
.map(
|
||||
day => {
|
||||
let selected = false;
|
||||
{this.getDaysByMonth(
|
||||
this.state.displayedMonth,
|
||||
this.state.displayedYear,
|
||||
).map((day) => {
|
||||
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}
|
||||
class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')}
|
||||
return (
|
||||
<span
|
||||
key={day.day}
|
||||
class={
|
||||
(day.today ? "datePicker--today " : "") +
|
||||
(selected ? "datePicker--selected" : "")
|
||||
}
|
||||
disabled={!day.date}
|
||||
data-value={day.date}
|
||||
>
|
||||
{day.day}
|
||||
</span>)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>}
|
||||
|
||||
{selectYearMode && <div class="datePicker--selectYear">
|
||||
{(this.props.years || yearArr).map(year => (
|
||||
<span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
|
||||
{year}
|
||||
</span>
|
||||
))}
|
||||
|
||||
</div>}
|
||||
|
||||
{selectYearMode && (
|
||||
<div class="datePicker--selectYear">
|
||||
{(this.props.years || yearArr).map((year) => (
|
||||
<span
|
||||
key={year}
|
||||
class={year === displayedYear ? "selected" : ""}
|
||||
onClick={this.changeDisplayedYear}
|
||||
>
|
||||
{year}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="datePicker--background" onClick={this.closeDatePicker} style={{
|
||||
display: this.props.opened ? 'block' : 'none',
|
||||
}}
|
||||
<div
|
||||
class="datePicker--background"
|
||||
onClick={this.closeDatePicker}
|
||||
style={{
|
||||
display: this.props.opened ? "block" : "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (let i = 2010; i <= now.getFullYear() + 10; i++) {
|
||||
yearArr.push(i);
|
||||
}
|
||||
|
@ -15,36 +15,41 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { h, FunctionalComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { DurationPicker as TestedComponent } from './DurationPicker';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { h, FunctionalComponent } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { DurationPicker as TestedComponent } from "./DurationPicker";
|
||||
|
||||
export default {
|
||||
title: 'Components/Picker/Duration',
|
||||
title: "Components/Picker/Duration",
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onCreate: { action: 'onCreate' },
|
||||
goBack: { action: 'goBack' },
|
||||
}
|
||||
onCreate: { action: "onCreate" },
|
||||
goBack: { action: "goBack" },
|
||||
},
|
||||
};
|
||||
|
||||
function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
|
||||
const r = (args: any) => <Component {...args} />
|
||||
r.args = props
|
||||
return r
|
||||
function createExample<Props>(
|
||||
Component: FunctionalComponent<Props>,
|
||||
props: Partial<Props>,
|
||||
) {
|
||||
const r = (args: any) => <Component {...args} />;
|
||||
r.args = props;
|
||||
return r;
|
||||
}
|
||||
|
||||
export const Example = createExample(TestedComponent, {
|
||||
days: true, minutes: true, hours: true, seconds: true,
|
||||
value: 10000000
|
||||
days: true,
|
||||
minutes: true,
|
||||
hours: true,
|
||||
seconds: true,
|
||||
value: 10000000,
|
||||
});
|
||||
|
||||
export const WithState = () => {
|
||||
const [v,s] = useState<number>(1000000)
|
||||
return <TestedComponent value={v} onChange={s} days minutes hours seconds />
|
||||
}
|
||||
const [v, s] = useState<number>(1000000);
|
||||
return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
|
||||
};
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
@ -30,75 +30,123 @@ export interface Props {
|
||||
seconds?: boolean;
|
||||
days?: boolean;
|
||||
onChange: (value: number) => void;
|
||||
value: number
|
||||
value: number;
|
||||
}
|
||||
|
||||
// inspiration taken from https://github.com/flurmbo/react-duration-picker
|
||||
export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode {
|
||||
const ss = 1000
|
||||
const ms = ss * 60
|
||||
const hs = ms * 60
|
||||
const ds = hs * 24
|
||||
const i18n = useTranslator()
|
||||
|
||||
return <div class="rdp-picker">
|
||||
{days && <DurationColumn unit={i18n`days`} max={99}
|
||||
value={Math.floor(value / ds)}
|
||||
onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
|
||||
onChange={diff => onChange(value + diff * ds)}
|
||||
/>}
|
||||
{hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
|
||||
value={Math.floor(value / hs) % 24}
|
||||
onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
|
||||
onChange={diff => onChange(value + diff * hs)}
|
||||
/>}
|
||||
{minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
|
||||
value={Math.floor(value / ms) % 60}
|
||||
onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
|
||||
onChange={diff => onChange(value + diff * ms)}
|
||||
/>}
|
||||
{seconds && <DurationColumn unit={i18n`seconds`} max={59}
|
||||
value={Math.floor(value / ss) % 60}
|
||||
onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
|
||||
onChange={diff => onChange(value + diff * ss)}
|
||||
/>}
|
||||
</div>
|
||||
export function DurationPicker({
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
onChange,
|
||||
value,
|
||||
}: Props): VNode {
|
||||
const ss = 1000;
|
||||
const ms = ss * 60;
|
||||
const hs = ms * 60;
|
||||
const ds = hs * 24;
|
||||
const i18n = useTranslator();
|
||||
|
||||
return (
|
||||
<div class="rdp-picker">
|
||||
{days && (
|
||||
<DurationColumn
|
||||
unit={i18n`days`}
|
||||
max={99}
|
||||
value={Math.floor(value / ds)}
|
||||
onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
|
||||
onChange={(diff) => onChange(value + diff * ds)}
|
||||
/>
|
||||
)}
|
||||
{hours && (
|
||||
<DurationColumn
|
||||
unit={i18n`hours`}
|
||||
max={23}
|
||||
min={1}
|
||||
value={Math.floor(value / hs) % 24}
|
||||
onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
|
||||
onChange={(diff) => onChange(value + diff * hs)}
|
||||
/>
|
||||
)}
|
||||
{minutes && (
|
||||
<DurationColumn
|
||||
unit={i18n`minutes`}
|
||||
max={59}
|
||||
min={1}
|
||||
value={Math.floor(value / ms) % 60}
|
||||
onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
|
||||
onChange={(diff) => onChange(value + diff * ms)}
|
||||
/>
|
||||
)}
|
||||
{seconds && (
|
||||
<DurationColumn
|
||||
unit={i18n`seconds`}
|
||||
max={59}
|
||||
value={Math.floor(value / ss) % 60}
|
||||
onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
|
||||
onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
|
||||
onChange={(diff) => onChange(value + diff * ss)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ColProps {
|
||||
unit: string,
|
||||
min?: number,
|
||||
max: number,
|
||||
value: number,
|
||||
unit: string;
|
||||
min?: number;
|
||||
max: number;
|
||||
value: number;
|
||||
onIncrease?: () => void;
|
||||
onDecrease?: () => void;
|
||||
onChange?: (diff: number) => void;
|
||||
}
|
||||
|
||||
function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) {
|
||||
const [value, handler] = useState<{v:string}>({
|
||||
v: toTwoDigitString(initial)
|
||||
})
|
||||
function InputNumber({
|
||||
initial,
|
||||
onChange,
|
||||
}: {
|
||||
initial: number;
|
||||
onChange: (n: number) => void;
|
||||
}) {
|
||||
const [value, handler] = useState<{ v: string }>({
|
||||
v: toTwoDigitString(initial),
|
||||
});
|
||||
|
||||
return <input
|
||||
value={value.v}
|
||||
onBlur={(e) => onChange(parseInt(value.v, 10))}
|
||||
onInput={(e) => {
|
||||
e.preventDefault()
|
||||
const n = Number.parseInt(e.currentTarget.value, 10);
|
||||
if (isNaN(n)) return handler({v:toTwoDigitString(initial)})
|
||||
return handler({v:toTwoDigitString(n)})
|
||||
}}
|
||||
style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} />
|
||||
return (
|
||||
<input
|
||||
value={value.v}
|
||||
onBlur={(e) => onChange(parseInt(value.v, 10))}
|
||||
onInput={(e) => {
|
||||
e.preventDefault();
|
||||
const n = Number.parseInt(e.currentTarget.value, 10);
|
||||
if (isNaN(n)) return handler({ v: toTwoDigitString(initial) });
|
||||
return handler({ v: toTwoDigitString(n) });
|
||||
}}
|
||||
style={{
|
||||
width: 50,
|
||||
border: "none",
|
||||
fontSize: "inherit",
|
||||
background: "inherit",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode {
|
||||
|
||||
const cellHeight = 35
|
||||
function DurationColumn({
|
||||
unit,
|
||||
min = 0,
|
||||
max,
|
||||
value,
|
||||
onIncrease,
|
||||
onDecrease,
|
||||
onChange,
|
||||
}: ColProps): VNode {
|
||||
const cellHeight = 35;
|
||||
return (
|
||||
<div class="rdp-column-container">
|
||||
<div class="rdp-masked-div">
|
||||
@ -106,49 +154,58 @@ function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onC
|
||||
<hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
|
||||
|
||||
<div class="rdp-column" style={{ top: 0 }}>
|
||||
|
||||
<div class="rdp-cell" key={value - 2}>
|
||||
{onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
|
||||
onClick={onDecrease}>
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-chevron-up" />
|
||||
</span>
|
||||
</button>}
|
||||
{onDecrease && (
|
||||
<button
|
||||
style={{ width: "100%", textAlign: "center", margin: 5 }}
|
||||
onClick={onDecrease}
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-chevron-up" />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div class="rdp-cell" key={value - 1}>
|
||||
{value > min ? toTwoDigitString(value - 1) : ''}
|
||||
{value > min ? toTwoDigitString(value - 1) : ""}
|
||||
</div>
|
||||
<div class="rdp-cell rdp-center" key={value}>
|
||||
{onChange ?
|
||||
<InputNumber initial={value} onChange={(n) => onChange(n - value)} /> :
|
||||
{onChange ? (
|
||||
<InputNumber
|
||||
initial={value}
|
||||
onChange={(n) => onChange(n - value)}
|
||||
/>
|
||||
) : (
|
||||
toTwoDigitString(value)
|
||||
}
|
||||
)}
|
||||
<div>{unit}</div>
|
||||
</div>
|
||||
|
||||
<div class="rdp-cell" key={value + 1}>
|
||||
{value < max ? toTwoDigitString(value + 1) : ''}
|
||||
{value < max ? toTwoDigitString(value + 1) : ""}
|
||||
</div>
|
||||
|
||||
<div class="rdp-cell" key={value + 2}>
|
||||
{onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
|
||||
onClick={onIncrease}>
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-chevron-down" />
|
||||
</span>
|
||||
</button>}
|
||||
{onIncrease && (
|
||||
<button
|
||||
style={{ width: "100%", textAlign: "center", margin: 5 }}
|
||||
onClick={onIncrease}
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-chevron-down" />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function toTwoDigitString(n: number) {
|
||||
if (n < 10) {
|
||||
return `0${n}`;
|
||||
}
|
||||
return `${n}`;
|
||||
}
|
||||
}
|
||||
|
@ -15,19 +15,19 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createContext, h, VNode } from 'preact';
|
||||
import { useContext } from 'preact/hooks';
|
||||
import { AnastasisReducerApi } from '../hooks/use-anastasis-reducer';
|
||||
import { createContext, h, VNode } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
|
||||
|
||||
type Type = AnastasisReducerApi | undefined;
|
||||
|
||||
const initial = undefined
|
||||
const initial = undefined;
|
||||
|
||||
const Context = createContext<Type>(initial)
|
||||
const Context = createContext<Type>(initial);
|
||||
|
||||
interface Props {
|
||||
value: AnastasisReducerApi;
|
||||
@ -36,6 +36,6 @@ interface Props {
|
||||
|
||||
export const AnastasisProvider = ({ value, children }: Props): VNode => {
|
||||
return h(Context.Provider, { value, children });
|
||||
}
|
||||
};
|
||||
|
||||
export const useAnastasisContext = (): Type => useContext(Context);
|
||||
export const useAnastasisContext = (): Type => useContext(Context);
|
||||
|
@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createContext, h, VNode } from 'preact'
|
||||
import { useContext, useEffect } from 'preact/hooks'
|
||||
import { useLang } from '../hooks'
|
||||
import { createContext, h, VNode } from "preact";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { useLang } from "../hooks";
|
||||
import * as jedLib from "jed";
|
||||
import { strings } from "../i18n/strings";
|
||||
|
||||
@ -31,13 +31,13 @@ interface Type {
|
||||
changeLanguage: (l: string) => void;
|
||||
}
|
||||
const initial = {
|
||||
lang: 'en',
|
||||
lang: "en",
|
||||
handler: null,
|
||||
changeLanguage: () => {
|
||||
// do not change anything
|
||||
}
|
||||
}
|
||||
const Context = createContext<Type>(initial)
|
||||
},
|
||||
};
|
||||
const Context = createContext<Type>(initial);
|
||||
|
||||
interface Props {
|
||||
initial?: string;
|
||||
@ -45,15 +45,22 @@ interface Props {
|
||||
forceLang?: string;
|
||||
}
|
||||
|
||||
export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
|
||||
const [lang, changeLanguage] = useLang(initial)
|
||||
export const TranslationProvider = ({
|
||||
initial,
|
||||
children,
|
||||
forceLang,
|
||||
}: Props): VNode => {
|
||||
const [lang, changeLanguage] = useLang(initial);
|
||||
useEffect(() => {
|
||||
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);
|
||||
|
28
packages/anastasis-webui/src/declaration.d.ts
vendored
28
packages/anastasis-webui/src/declaration.d.ts
vendored
@ -1,20 +1,20 @@
|
||||
declare module "*.css" {
|
||||
const mapping: Record<string, string>;
|
||||
export default mapping;
|
||||
const mapping: Record<string, string>;
|
||||
export default mapping;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
declare module "*.jpeg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const content: any;
|
||||
export default content;
|
||||
declare module "*.png" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
declare module 'jed' {
|
||||
const x: any;
|
||||
export = x;
|
||||
declare module "jed" {
|
||||
const x: any;
|
||||
export = x;
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
import { useState } from "preact/hooks";
|
||||
// import { cancelPendingRequest } from "./backend";
|
||||
|
||||
@ -34,36 +34,39 @@ export interface AsyncOperationApi<T> {
|
||||
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 [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<any>(undefined);
|
||||
const [isSlow, setSlow] = useState(false)
|
||||
const [isSlow, setSlow] = useState(false);
|
||||
|
||||
const request = async (...args: any) => {
|
||||
if (!fn) return;
|
||||
setLoading(true);
|
||||
const handler = setTimeout(() => {
|
||||
setSlow(true)
|
||||
}, tooLong)
|
||||
setSlow(true);
|
||||
}, tooLong);
|
||||
|
||||
try {
|
||||
console.log("calling async", args)
|
||||
console.log("calling async", args);
|
||||
const result = await fn(...args);
|
||||
console.log("async back", result)
|
||||
console.log("async back", result);
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
setLoading(false);
|
||||
setSlow(false)
|
||||
clearTimeout(handler)
|
||||
setSlow(false);
|
||||
clearTimeout(handler);
|
||||
};
|
||||
|
||||
function cancel() {
|
||||
// cancelPendingRequest()
|
||||
setLoading(false);
|
||||
setSlow(false)
|
||||
setSlow(false);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -72,6 +75,6 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
|
||||
data,
|
||||
isSlow,
|
||||
isLoading,
|
||||
error
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
@ -15,81 +15,110 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
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 rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
|
||||
return rootPath
|
||||
}
|
||||
const rootPath =
|
||||
typeof window !== undefined
|
||||
? window.location.origin + window.location.pathname
|
||||
: "/";
|
||||
return rootPath;
|
||||
};
|
||||
|
||||
export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
|
||||
const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
|
||||
const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
|
||||
export function useBackendURL(
|
||||
url?: string,
|
||||
): [string, boolean, StateUpdater<string>, () => void] {
|
||||
const [value, setter] = useNotNullLocalStorage(
|
||||
"backend-url",
|
||||
url || calculateRootPath(),
|
||||
);
|
||||
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
|
||||
|
||||
const checkedSetter = (v: ValueOrFunction<string>) => {
|
||||
setTriedToLog('yes')
|
||||
return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
|
||||
}
|
||||
setTriedToLog("yes");
|
||||
return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
|
||||
};
|
||||
|
||||
const resetBackend = () => {
|
||||
setTriedToLog(undefined)
|
||||
}
|
||||
return [value, !!triedToLog, checkedSetter, resetBackend]
|
||||
setTriedToLog(undefined);
|
||||
};
|
||||
return [value, !!triedToLog, checkedSetter, resetBackend];
|
||||
}
|
||||
|
||||
export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
|
||||
return useLocalStorage('backend-token')
|
||||
export function useBackendDefaultToken(): [
|
||||
string | undefined,
|
||||
StateUpdater<string | undefined>,
|
||||
] {
|
||||
return useLocalStorage("backend-token");
|
||||
}
|
||||
|
||||
export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
|
||||
const [token, setToken] = useLocalStorage(`backend-token-${id}`)
|
||||
const [defaultToken, defaultSetToken] = useBackendDefaultToken()
|
||||
export function useBackendInstanceToken(
|
||||
id: string,
|
||||
): [string | undefined, StateUpdater<string | undefined>] {
|
||||
const [token, setToken] = useLocalStorage(`backend-token-${id}`);
|
||||
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
|
||||
|
||||
// instance named 'default' use the default token
|
||||
if (id === 'default') {
|
||||
return [defaultToken, defaultSetToken]
|
||||
if (id === "default") {
|
||||
return [defaultToken, defaultSetToken];
|
||||
}
|
||||
|
||||
return [token, setToken]
|
||||
return [token, setToken];
|
||||
}
|
||||
|
||||
export function useLang(initial?: string): [string, StateUpdater<string>] {
|
||||
const browserLang = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined;
|
||||
const defaultLang = (browserLang || initial || 'en').substring(0, 2)
|
||||
return useNotNullLocalStorage('lang-preference', defaultLang)
|
||||
const browserLang =
|
||||
typeof window !== "undefined"
|
||||
? 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>] {
|
||||
const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
|
||||
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
|
||||
export function useLocalStorage(
|
||||
key: string,
|
||||
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)) => {
|
||||
setStoredValue(p => {
|
||||
const toStore = value instanceof Function ? value(p) : value
|
||||
const setValue = (
|
||||
value?: string | ((val?: string) => string | undefined),
|
||||
) => {
|
||||
setStoredValue((p) => {
|
||||
const toStore = value instanceof Function ? value(p) : value;
|
||||
if (typeof window !== "undefined") {
|
||||
if (!toStore) {
|
||||
window.localStorage.removeItem(key)
|
||||
window.localStorage.removeItem(key);
|
||||
} else {
|
||||
window.localStorage.setItem(key, toStore);
|
||||
}
|
||||
}
|
||||
return toStore
|
||||
})
|
||||
return toStore;
|
||||
});
|
||||
};
|
||||
|
||||
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 => {
|
||||
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)) => {
|
||||
@ -97,7 +126,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
|
||||
setStoredValue(valueToStore);
|
||||
if (typeof window !== "undefined") {
|
||||
if (!valueToStore) {
|
||||
window.localStorage.removeItem(key)
|
||||
window.localStorage.removeItem(key);
|
||||
} else {
|
||||
window.localStorage.setItem(key, valueToStore);
|
||||
}
|
||||
@ -106,5 +135,3 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
||||
|
||||
|
@ -27,23 +27,25 @@ import { useTranslationContext } from "../context/translation";
|
||||
|
||||
export function useTranslator() {
|
||||
const ctx = useTranslationContext();
|
||||
const jed = ctx.handler
|
||||
return function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
|
||||
const jed = ctx.handler;
|
||||
return function str(
|
||||
stringSeq: TemplateStringsArray,
|
||||
...values: any[]
|
||||
): string {
|
||||
const s = toI18nString(stringSeq);
|
||||
if (!s) return s
|
||||
if (!s) return s;
|
||||
const tr = jed
|
||||
.translate(s)
|
||||
.ifPlural(1, s)
|
||||
.fetch(...values);
|
||||
return tr;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert template strings to a msgid
|
||||
*/
|
||||
function toI18nString(stringSeq: ReadonlyArray<string>): string {
|
||||
function toI18nString(stringSeq: ReadonlyArray<string>): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < stringSeq.length; i++) {
|
||||
s += stringSeq[i];
|
||||
@ -54,7 +56,6 @@ export function useTranslator() {
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
interface TranslateSwitchProps {
|
||||
target: number;
|
||||
children: ComponentChildren;
|
||||
@ -110,7 +111,7 @@ function getTranslatedChildren(
|
||||
// Text
|
||||
result.push(tr[i]);
|
||||
} else {
|
||||
const childIdx = Number.parseInt(tr[i],10) - 1;
|
||||
const childIdx = Number.parseInt(tr[i], 10) - 1;
|
||||
result.push(placeholderChildren[childIdx]);
|
||||
}
|
||||
}
|
||||
@ -131,9 +132,9 @@ function getTranslatedChildren(
|
||||
*/
|
||||
export function Translate({ children }: TranslateProps): VNode {
|
||||
const s = stringifyChildren(children);
|
||||
const ctx = useTranslationContext()
|
||||
const ctx = useTranslationContext();
|
||||
const translation: string = ctx.handler.ngettext(s, s, 1);
|
||||
const result = getTranslatedChildren(translation, children)
|
||||
const result = getTranslatedChildren(translation, children);
|
||||
return <Fragment>{result}</Fragment>;
|
||||
}
|
||||
|
||||
@ -154,14 +155,16 @@ export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
|
||||
let plural: VNode<TranslationPluralProps> | undefined;
|
||||
// const children = this.props.children;
|
||||
if (children) {
|
||||
(children instanceof Array ? children : [children]).forEach((child: any) => {
|
||||
if (child.type === TranslatePlural) {
|
||||
plural = child;
|
||||
}
|
||||
if (child.type === TranslateSingular) {
|
||||
singular = child;
|
||||
}
|
||||
});
|
||||
(children instanceof Array ? children : [children]).forEach(
|
||||
(child: any) => {
|
||||
if (child.type === TranslatePlural) {
|
||||
plural = child;
|
||||
}
|
||||
if (child.type === TranslateSingular) {
|
||||
singular = child;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!singular || !plural) {
|
||||
console.error("translation not found");
|
||||
@ -182,9 +185,12 @@ interface TranslationPluralProps {
|
||||
/**
|
||||
* See [[TranslateSwitch]].
|
||||
*/
|
||||
export function TranslatePlural({ children, target }: TranslationPluralProps): VNode {
|
||||
export function TranslatePlural({
|
||||
children,
|
||||
target,
|
||||
}: TranslationPluralProps): VNode {
|
||||
const s = stringifyChildren(children);
|
||||
const ctx = useTranslationContext()
|
||||
const ctx = useTranslationContext();
|
||||
const translation = ctx.handler.ngettext(s, s, 1);
|
||||
const result = getTranslatedChildren(translation, children);
|
||||
return <Fragment>{result}</Fragment>;
|
||||
@ -193,11 +199,13 @@ export function TranslatePlural({ children, target }: TranslationPluralProps): V
|
||||
/**
|
||||
* See [[TranslateSwitch]].
|
||||
*/
|
||||
export function TranslateSingular({ children, target }: TranslationPluralProps): VNode {
|
||||
export function TranslateSingular({
|
||||
children,
|
||||
target,
|
||||
}: TranslationPluralProps): VNode {
|
||||
const s = stringifyChildren(children);
|
||||
const ctx = useTranslationContext()
|
||||
const ctx = useTranslationContext();
|
||||
const translation = ctx.handler.ngettext(s, s, target);
|
||||
const result = getTranslatedChildren(translation, children);
|
||||
return <Fragment>{result}</Fragment>;
|
||||
|
||||
}
|
||||
|
@ -15,30 +15,30 @@
|
||||
*/
|
||||
|
||||
/*eslint quote-props: ["error", "consistent"]*/
|
||||
export const strings: {[s: string]: any} = {};
|
||||
export const strings: { [s: string]: any } = {};
|
||||
|
||||
strings['de'] = {
|
||||
"domain": "messages",
|
||||
"locale_data": {
|
||||
"messages": {
|
||||
strings["de"] = {
|
||||
domain: "messages",
|
||||
locale_data: {
|
||||
messages: {
|
||||
"": {
|
||||
"domain": "messages",
|
||||
"plural_forms": "nplurals=2; plural=(n != 1);",
|
||||
"lang": ""
|
||||
domain: "messages",
|
||||
plural_forms: "nplurals=2; plural=(n != 1);",
|
||||
lang: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
strings['en'] = {
|
||||
"domain": "messages",
|
||||
"locale_data": {
|
||||
"messages": {
|
||||
strings["en"] = {
|
||||
domain: "messages",
|
||||
locale_data: {
|
||||
messages: {
|
||||
"": {
|
||||
"domain": "messages",
|
||||
"plural_forms": "nplurals=2; plural=(n != 1);",
|
||||
"lang": ""
|
||||
domain: "messages",
|
||||
plural_forms: "nplurals=2; plural=(n != 1);",
|
||||
lang: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import App from './components/app';
|
||||
import './scss/main.scss';
|
||||
import App from "./components/app";
|
||||
import "./scss/main.scss";
|
||||
|
||||
export default App;
|
||||
|
@ -18,4 +18,4 @@
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -15,24 +15,23 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { AddingProviderScreen as TestedComponent } from './AddingProviderScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/ManageProvider',
|
||||
title: "Pages/ManageProvider",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 1,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
@ -40,20 +39,31 @@ export const NewProvider = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState);
|
||||
|
||||
|
||||
export const NewProviderWithoutProviderList = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
authentication_providers: {}
|
||||
authentication_providers: {},
|
||||
} as ReducerState);
|
||||
|
||||
export const NewVideoProvider = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState, { providerType: 'video'});
|
||||
export const NewVideoProvider = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState,
|
||||
{ providerType: "video" },
|
||||
);
|
||||
|
||||
export const NewSmsProvider = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState, { providerType: 'sms'});
|
||||
export const NewSmsProvider = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState,
|
||||
{ providerType: "sms" },
|
||||
);
|
||||
|
||||
export const NewIBANProvider = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState, { providerType: 'iban' });
|
||||
export const NewIBANProvider = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
...reducerStatesExample.authEditing,
|
||||
} as ReducerState,
|
||||
{ providerType: "iban" },
|
||||
);
|
||||
|
@ -11,185 +11,250 @@ interface Props {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
|
||||
async function testProvider(url: string, expectedMethodType?: string): Promise<void> {
|
||||
async function testProvider(
|
||||
url: string,
|
||||
expectedMethodType?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(new URL("config", url).href)
|
||||
const json = await (response.json().catch(d => ({})))
|
||||
const response = await fetch(new URL("config", url).href);
|
||||
const json = await response.json().catch((d) => ({}));
|
||||
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) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
let found = false
|
||||
let found = false;
|
||||
for (let i = 0; i < json.methods.length && !found; i++) {
|
||||
found = json.methods[i].type === expectedMethodType
|
||||
found = json.methods[i].type === expectedMethodType;
|
||||
}
|
||||
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) {
|
||||
console.log("error", e)
|
||||
const error = e instanceof Error ?
|
||||
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
|
||||
console.log("error", e);
|
||||
const error =
|
||||
e instanceof Error
|
||||
? 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 {
|
||||
const reducer = useAnastasisContext();
|
||||
|
||||
const [providerURL, setProviderURL] = useState("");
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const [testing, setTesting] = useState(false)
|
||||
const providerLabel = providerType ? authMethods[providerType].label : undefined
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [testing, setTesting] = useState(false);
|
||||
const providerLabel = providerType
|
||||
? authMethods[providerType].label
|
||||
: undefined;
|
||||
|
||||
//FIXME: move this timeout logic into a hook
|
||||
const timeout = useRef<number | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (timeout) window.clearTimeout(timeout.current)
|
||||
if (timeout) window.clearTimeout(timeout.current);
|
||||
timeout.current = window.setTimeout(async () => {
|
||||
const url = providerURL.endsWith('/') ? providerURL : (providerURL + '/')
|
||||
const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
|
||||
if (!providerURL || authProviders.includes(url)) return;
|
||||
try {
|
||||
setTesting(true)
|
||||
await testProvider(url, providerType)
|
||||
setTesting(true);
|
||||
await testProvider(url, providerType);
|
||||
// this is use as tested but everything when ok
|
||||
// undefined will mean that the field is not dirty
|
||||
setError("")
|
||||
setError("");
|
||||
} catch (e) {
|
||||
console.log("tuvieja", e)
|
||||
if (e instanceof Error) setError(e.message)
|
||||
console.log("tuvieja", e);
|
||||
if (e instanceof Error) setError(e.message);
|
||||
}
|
||||
setTesting(false)
|
||||
setTesting(false);
|
||||
}, 200);
|
||||
}, [providerURL, reducer])
|
||||
|
||||
}, [providerURL, reducer]);
|
||||
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
|
||||
if (!reducer.currentReducerState || !("authentication_providers" in reducer.currentReducerState)) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!reducer.currentReducerState ||
|
||||
!("authentication_providers" in reducer.currentReducerState)
|
||||
) {
|
||||
return <div>invalid state</div>;
|
||||
}
|
||||
|
||||
async function addProvider(provider_url: string): Promise<void> {
|
||||
await reducer?.transition("add_provider", { provider_url })
|
||||
onCancel()
|
||||
await reducer?.transition("add_provider", { provider_url });
|
||||
onCancel();
|
||||
}
|
||||
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 authProviders = Object.keys(allAuthProviders).filter(provUrl => {
|
||||
const allAuthProviders =
|
||||
reducer.currentReducerState.authentication_providers || {};
|
||||
const authProviders = Object.keys(allAuthProviders).filter((provUrl) => {
|
||||
const p = allAuthProviders[provUrl];
|
||||
if (!providerLabel) {
|
||||
return p && ("currency" in p)
|
||||
return p && "currency" in p;
|
||||
} 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;
|
||||
try {
|
||||
url = new URL("",providerURL).href
|
||||
url = new URL("", providerURL).href;
|
||||
} catch {
|
||||
errors = 'Check the URL'
|
||||
errors = "Check the URL";
|
||||
}
|
||||
if (!!error && !errors) {
|
||||
errors = error
|
||||
errors = error;
|
||||
}
|
||||
if (!errors && authProviders.includes(url!)) {
|
||||
errors = 'That provider is already known'
|
||||
errors = "That provider is already known";
|
||||
}
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav
|
||||
<AnastasisClientFrame
|
||||
hideNav
|
||||
title="Backup: Manage providers"
|
||||
hideNext={errors}>
|
||||
hideNext={errors}
|
||||
>
|
||||
<div>
|
||||
{!providerLabel ?
|
||||
<p>
|
||||
Add a provider url
|
||||
</p> :
|
||||
<p>
|
||||
Add a provider url for a {providerLabel} service
|
||||
</p>
|
||||
}
|
||||
{!providerLabel ? (
|
||||
<p>Add a provider url</p>
|
||||
) : (
|
||||
<p>Add a provider url for a {providerLabel} service</p>
|
||||
)}
|
||||
<div class="container">
|
||||
<TextInput
|
||||
label="Provider URL"
|
||||
placeholder="https://provider.com"
|
||||
grabFocus
|
||||
error={errors}
|
||||
bind={[providerURL, setProviderURL]} />
|
||||
bind={[providerURL, setProviderURL]}
|
||||
/>
|
||||
</div>
|
||||
<p class="block">
|
||||
Example: https://kudos.demo.anastasis.lu
|
||||
</p>
|
||||
<p class="block">Example: https://kudos.demo.anastasis.lu</p>
|
||||
{testing && <p class="has-text-info">Testing</p>}
|
||||
|
||||
<div class="block" style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={onCancel}>Cancel</button>
|
||||
|
||||
<div
|
||||
class="block"
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{authProviders.length > 0 ? (
|
||||
!providerLabel ?
|
||||
!providerLabel ? (
|
||||
<p class="subtitle">Current providers</p>
|
||||
) : (
|
||||
<p class="subtitle">
|
||||
Current providers
|
||||
</p> : <p class="subtitle">
|
||||
Current providers for {providerLabel} service
|
||||
</p>
|
||||
)
|
||||
) : !providerLabel ? (
|
||||
<p class="subtitle">No known providers, add one.</p>
|
||||
) : (
|
||||
!providerLabel ? <p class="subtitle">
|
||||
No known providers, add one.
|
||||
</p> : <p class="subtitle">
|
||||
No known providers for {providerLabel} service
|
||||
</p>
|
||||
<p class="subtitle">No known providers for {providerLabel} service</p>
|
||||
)}
|
||||
|
||||
{authProviders.map(k => {
|
||||
const p = allAuthProviders[k] as AuthenticationProviderStatusOk
|
||||
return <TableRow url={k} info={p} onDelete={deleteProvider} />
|
||||
{authProviders.map((k) => {
|
||||
const p = allAuthProviders[k] as AuthenticationProviderStatusOk;
|
||||
return <TableRow url={k} info={p} onDelete={deleteProvider} />;
|
||||
})}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
function TableRow({ url, info, onDelete }: { onDelete: (s: string) => void, url: string, info: AuthenticationProviderStatusOk }) {
|
||||
const [status, setStatus] = useState("checking")
|
||||
function TableRow({
|
||||
url,
|
||||
info,
|
||||
onDelete,
|
||||
}: {
|
||||
onDelete: (s: string) => void;
|
||||
url: string;
|
||||
info: AuthenticationProviderStatusOk;
|
||||
}) {
|
||||
const [status, setStatus] = useState("checking");
|
||||
useEffect(function () {
|
||||
testProvider(url.endsWith('/') ? url.substring(0, url.length - 1) : url)
|
||||
.then(function () { setStatus('responding') })
|
||||
.catch(function () { setStatus('failed to contact') })
|
||||
})
|
||||
return <div class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div class="subtitle">{url}</div>
|
||||
<dl>
|
||||
<dt><b>Business Name</b></dt>
|
||||
<dd>{info.business_name}</dd>
|
||||
<dt><b>Supported methods</b></dt>
|
||||
<dd>{info.methods.map(m => m.type).join(',')}</dd>
|
||||
<dt><b>Maximum storage</b></dt>
|
||||
<dd>{info.storage_limit_in_megabytes} Mb</dd>
|
||||
<dt><b>Status</b></dt>
|
||||
<dd>{status}</dd>
|
||||
</dl>
|
||||
testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url)
|
||||
.then(function () {
|
||||
setStatus("responding");
|
||||
})
|
||||
.catch(function () {
|
||||
setStatus("failed to contact");
|
||||
});
|
||||
});
|
||||
return (
|
||||
<div
|
||||
class="box"
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<div>
|
||||
<div class="subtitle">{url}</div>
|
||||
<dl>
|
||||
<dt>
|
||||
<b>Business Name</b>
|
||||
</dt>
|
||||
<dd>{info.business_name}</dd>
|
||||
<dt>
|
||||
<b>Supported methods</b>
|
||||
</dt>
|
||||
<dd>{info.methods.map((m) => m.type).join(",")}</dd>
|
||||
<dt>
|
||||
<b>Maximum storage</b>
|
||||
</dt>
|
||||
<dd>{info.storage_limit_in_megabytes} Mb</dd>
|
||||
<dt>
|
||||
<b>Status</b>
|
||||
</dt>
|
||||
<dd>{status}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
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 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>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -15,76 +15,83 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/PersonalInformation',
|
||||
title: "Pages/PersonalInformation",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 3,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Backup = createExample(TestedComponent, {
|
||||
...reducerStatesExample.backupAttributeEditing,
|
||||
required_attributes: [{
|
||||
name: 'first name',
|
||||
label: 'first',
|
||||
type: 'string',
|
||||
uuid: 'asdasdsa1',
|
||||
widget: 'wid',
|
||||
}, {
|
||||
name: 'last name',
|
||||
label: 'second',
|
||||
type: 'string',
|
||||
uuid: 'asdasdsa2',
|
||||
widget: 'wid',
|
||||
}, {
|
||||
name: 'birthdate',
|
||||
label: 'birthdate',
|
||||
type: 'date',
|
||||
uuid: 'asdasdsa3',
|
||||
widget: 'calendar',
|
||||
}]
|
||||
required_attributes: [
|
||||
{
|
||||
name: "first name",
|
||||
label: "first",
|
||||
type: "string",
|
||||
uuid: "asdasdsa1",
|
||||
widget: "wid",
|
||||
},
|
||||
{
|
||||
name: "last name",
|
||||
label: "second",
|
||||
type: "string",
|
||||
uuid: "asdasdsa2",
|
||||
widget: "wid",
|
||||
},
|
||||
{
|
||||
name: "birthdate",
|
||||
label: "birthdate",
|
||||
type: "date",
|
||||
uuid: "asdasdsa3",
|
||||
widget: "calendar",
|
||||
},
|
||||
],
|
||||
} as ReducerState);
|
||||
|
||||
export const Recovery = createExample(TestedComponent, {
|
||||
...reducerStatesExample.recoveryAttributeEditing,
|
||||
required_attributes: [{
|
||||
name: 'first',
|
||||
label: 'first',
|
||||
type: 'string',
|
||||
uuid: 'asdasdsa1',
|
||||
widget: 'wid',
|
||||
}, {
|
||||
name: 'pepe',
|
||||
label: 'second',
|
||||
type: 'string',
|
||||
uuid: 'asdasdsa2',
|
||||
widget: 'wid',
|
||||
}, {
|
||||
name: 'pepe2',
|
||||
label: 'third',
|
||||
type: 'date',
|
||||
uuid: 'asdasdsa3',
|
||||
widget: 'calendar',
|
||||
}]
|
||||
required_attributes: [
|
||||
{
|
||||
name: "first",
|
||||
label: "first",
|
||||
type: "string",
|
||||
uuid: "asdasdsa1",
|
||||
widget: "wid",
|
||||
},
|
||||
{
|
||||
name: "pepe",
|
||||
label: "second",
|
||||
type: "string",
|
||||
uuid: "asdasdsa2",
|
||||
widget: "wid",
|
||||
},
|
||||
{
|
||||
name: "pepe2",
|
||||
label: "third",
|
||||
type: "date",
|
||||
uuid: "asdasdsa3",
|
||||
widget: "calendar",
|
||||
},
|
||||
],
|
||||
} as ReducerState);
|
||||
|
||||
export const WithNoRequiredAttribute = createExample(TestedComponent, {
|
||||
...reducerStatesExample.backupAttributeEditing,
|
||||
required_attributes: undefined
|
||||
required_attributes: undefined,
|
||||
} as ReducerState);
|
||||
|
||||
const allWidgets = [
|
||||
@ -107,23 +114,22 @@ const allWidgets = [
|
||||
"anastasis_gtk_ia_tax_de",
|
||||
"anastasis_gtk_xx_prime",
|
||||
"anastasis_gtk_xx_square",
|
||||
]
|
||||
];
|
||||
|
||||
function typeForWidget(name: string): string {
|
||||
if (["anastasis_gtk_xx_prime",
|
||||
"anastasis_gtk_xx_square",
|
||||
].includes(name)) return "number";
|
||||
if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date"
|
||||
if (["anastasis_gtk_xx_prime", "anastasis_gtk_xx_square"].includes(name))
|
||||
return "number";
|
||||
if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date";
|
||||
return "string";
|
||||
}
|
||||
|
||||
export const WithAllPosibleWidget = createExample(TestedComponent, {
|
||||
...reducerStatesExample.backupAttributeEditing,
|
||||
required_attributes: allWidgets.map(w => ({
|
||||
required_attributes: allWidgets.map((w) => ({
|
||||
name: w,
|
||||
label: `widget: ${w}`,
|
||||
type: typeForWidget(w),
|
||||
uuid: `uuid-${w}`,
|
||||
widget: w
|
||||
}))
|
||||
widget: w,
|
||||
})),
|
||||
} as ReducerState);
|
||||
|
@ -9,24 +9,32 @@ import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame, withProcessLabel } from "./index";
|
||||
|
||||
export function AttributeEntryScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const state = reducer?.currentReducerState
|
||||
const currentIdentityAttributes = state && "identity_attributes" in state ? (state.identity_attributes || {}) : {}
|
||||
const [attrs, setAttrs] = useState<Record<string, string>>(currentIdentityAttributes);
|
||||
const reducer = useAnastasisContext();
|
||||
const state = reducer?.currentReducerState;
|
||||
const currentIdentityAttributes =
|
||||
state && "identity_attributes" in state
|
||||
? state.identity_attributes || {}
|
||||
: {};
|
||||
const [attrs, setAttrs] = useState<Record<string, string>>(
|
||||
currentIdentityAttributes,
|
||||
);
|
||||
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!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;
|
||||
|
||||
const fieldList: VNode[] = reqAttr.map((spec, i: number) => {
|
||||
const value = attrs[spec.name]
|
||||
const error = checkIfValid(value, spec)
|
||||
hasErrors = hasErrors || error !== undefined
|
||||
const value = attrs[spec.name];
|
||||
const error = checkIfValid(value, spec);
|
||||
hasErrors = hasErrors || error !== undefined;
|
||||
return (
|
||||
<AttributeEntryField
|
||||
key={i}
|
||||
@ -34,23 +42,24 @@ export function AttributeEntryScreen(): VNode {
|
||||
setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
|
||||
spec={spec}
|
||||
errorMessage={error}
|
||||
value={value} />
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame
|
||||
title={withProcessLabel(reducer, "Who are you?")}
|
||||
hideNext={hasErrors ? "Complete the form." : undefined}
|
||||
onNext={() => reducer.transition("enter_user_attributes", {
|
||||
identity_attributes: attrs,
|
||||
})}
|
||||
onNext={() =>
|
||||
reducer.transition("enter_user_attributes", {
|
||||
identity_attributes: attrs,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div class="columns" style={{ maxWidth: 'unset' }}>
|
||||
<div class="columns" style={{ maxWidth: "unset" }}>
|
||||
<div class="column">{fieldList}</div>
|
||||
<div class="column">
|
||||
{fieldList}
|
||||
</div>
|
||||
<div class="column" >
|
||||
<p>This personal information will help to locate your secret.</p>
|
||||
<h1 class="title">This stays private</h1>
|
||||
<p>The information you have entered here:</p>
|
||||
@ -61,9 +70,12 @@ export function AttributeEntryScreen(): VNode {
|
||||
</span>
|
||||
Will be hashed, and therefore unreadable
|
||||
</li>
|
||||
<li><span class="icon is-right">
|
||||
<i class="mdi mdi-circle-small" />
|
||||
</span>The non-hashed version is not shared</li>
|
||||
<li>
|
||||
<span class="icon is-right">
|
||||
<i class="mdi mdi-circle-small" />
|
||||
</span>
|
||||
The non-hashed version is not shared
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -78,22 +90,22 @@ interface AttributeEntryFieldProps {
|
||||
spec: UserAttributeSpec;
|
||||
errorMessage: string | undefined;
|
||||
}
|
||||
const possibleBirthdayYear: Array<number> = []
|
||||
const possibleBirthdayYear: Array<number> = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
possibleBirthdayYear.push(2020 - i)
|
||||
possibleBirthdayYear.push(2020 - i);
|
||||
}
|
||||
function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.spec.type === 'date' &&
|
||||
{props.spec.type === "date" &&
|
||||
<DateInput
|
||||
grabFocus={props.isFirst}
|
||||
label={props.spec.label}
|
||||
years={possibleBirthdayYear}
|
||||
error={props.errorMessage}
|
||||
bind={[props.value, props.setValue]}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{props.spec.type === 'number' &&
|
||||
<PhoneNumberInput
|
||||
grabFocus={props.isFirst}
|
||||
@ -102,14 +114,14 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
||||
bind={[props.value, props.setValue]}
|
||||
/>
|
||||
}
|
||||
{props.spec.type === 'string' &&
|
||||
{props.spec.type === "string" && (
|
||||
<TextInput
|
||||
grabFocus={props.isFirst}
|
||||
label={props.spec.label}
|
||||
error={props.errorMessage}
|
||||
bind={[props.value, props.setValue]}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<div class="block">
|
||||
This stays private
|
||||
<span class="icon is-right">
|
||||
@ -119,40 +131,43 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
|
||||
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/;
|
||||
|
||||
|
||||
function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
|
||||
const pattern = spec['validation-regex']
|
||||
function checkIfValid(
|
||||
value: string,
|
||||
spec: UserAttributeSpec,
|
||||
): string | undefined {
|
||||
const pattern = spec["validation-regex"];
|
||||
if (pattern) {
|
||||
const re = new RegExp(pattern)
|
||||
if (!re.test(value)) return 'The value is invalid'
|
||||
const re = new RegExp(pattern);
|
||||
if (!re.test(value)) return "The value is invalid";
|
||||
}
|
||||
const logic = spec['validation-logic']
|
||||
const logic = spec["validation-logic"];
|
||||
if (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) {
|
||||
return 'This value is required'
|
||||
return "This value is required";
|
||||
}
|
||||
if ("date" === spec.type) {
|
||||
if (!YEAR_REGEX.test(value)) {
|
||||
return "The date doesn't follow the format"
|
||||
return "The date doesn't follow the format";
|
||||
}
|
||||
|
||||
try {
|
||||
const v = parse(value, 'yyyy-MM-dd', new Date());
|
||||
const v = parse(value, "yyyy-MM-dd", new Date());
|
||||
if (Number.isNaN(v.getTime())) {
|
||||
return "Some numeric values seems out of range for a date"
|
||||
return "Some numeric values seems out of range for a date";
|
||||
}
|
||||
if ("birthdate" === spec.name && isAfter(v, new Date())) {
|
||||
return "A birthdate cannot be in the future"
|
||||
return "A birthdate cannot be in the future";
|
||||
}
|
||||
} catch (e) {
|
||||
return "Could not parse the date"
|
||||
return "Could not parse the date";
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
@ -15,73 +15,84 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod',
|
||||
title: "Pages/backup/AuthorizationMethod",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 4,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const InitialState = createExample(TestedComponent, reducerStatesExample.authEditing);
|
||||
export const InitialState = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.authEditing,
|
||||
);
|
||||
export const OneAuthMethodConfigured = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
authentication_methods: [{
|
||||
type: 'question',
|
||||
instructions: 'what time is it?',
|
||||
challenge: 'asd',
|
||||
}]
|
||||
authentication_methods: [
|
||||
{
|
||||
type: "question",
|
||||
instructions: "what time is it?",
|
||||
challenge: "asd",
|
||||
},
|
||||
],
|
||||
} as ReducerState);
|
||||
|
||||
|
||||
export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
authentication_methods: [{
|
||||
type: 'question',
|
||||
instructions: 'what time is it?',
|
||||
challenge: 'asd',
|
||||
},{
|
||||
type: 'question',
|
||||
instructions: 'what time is it?',
|
||||
challenge: 'qwe',
|
||||
},{
|
||||
type: 'sms',
|
||||
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',
|
||||
},{
|
||||
type: 'email',
|
||||
instructions: 'what time is it?',
|
||||
challenge: 'asd',
|
||||
}]
|
||||
authentication_methods: [
|
||||
{
|
||||
type: "question",
|
||||
instructions: "what time is it?",
|
||||
challenge: "asd",
|
||||
},
|
||||
{
|
||||
type: "question",
|
||||
instructions: "what time is it?",
|
||||
challenge: "qwe",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
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",
|
||||
},
|
||||
{
|
||||
type: "email",
|
||||
instructions: "what time is it?",
|
||||
challenge: "asd",
|
||||
},
|
||||
],
|
||||
} as ReducerState);
|
||||
|
||||
export const NoAuthMethodProvided = createExample(TestedComponent, {
|
||||
...reducerStatesExample.authEditing,
|
||||
authentication_providers: {},
|
||||
authentication_methods: []
|
||||
authentication_methods: [],
|
||||
} as ReducerState);
|
||||
|
@ -20,7 +20,9 @@ export function AuthenticationEditorScreen(): VNode {
|
||||
KnownAuthMethods | undefined
|
||||
>(undefined);
|
||||
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 reducer = useAnastasisContext();
|
||||
@ -68,11 +70,14 @@ export function AuthenticationEditorScreen(): VNode {
|
||||
}
|
||||
|
||||
if (manageProvider !== undefined) {
|
||||
|
||||
return <AddingProviderScreen
|
||||
onCancel={() => setManageProvider(undefined)}
|
||||
providerType={isKnownAuthMethods(manageProvider) ? manageProvider : undefined}
|
||||
/>
|
||||
return (
|
||||
<AddingProviderScreen
|
||||
onCancel={() => setManageProvider(undefined)}
|
||||
providerType={
|
||||
isKnownAuthMethods(manageProvider) ? manageProvider : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedMethod) {
|
||||
@ -100,7 +105,7 @@ export function AuthenticationEditorScreen(): VNode {
|
||||
description="No providers founds"
|
||||
label="Add a provider manually"
|
||||
onConfirm={() => {
|
||||
setManageProvider(selectedMethod)
|
||||
setManageProvider(selectedMethod);
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
@ -193,7 +198,7 @@ export function AuthenticationEditorScreen(): VNode {
|
||||
description="No providers founds"
|
||||
label="Add a provider manually"
|
||||
onConfirm={() => {
|
||||
setManageProvider("")
|
||||
setManageProvider("");
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
@ -214,7 +219,10 @@ export function AuthenticationEditorScreen(): VNode {
|
||||
authentication method is defined by the backup provider list.
|
||||
</p>
|
||||
<p class="block">
|
||||
<button class="button is-info" onClick={() => setManageProvider("")}>
|
||||
<button
|
||||
class="button is-info"
|
||||
onClick={() => setManageProvider("")}
|
||||
>
|
||||
Manage backup providers
|
||||
</button>
|
||||
</p>
|
||||
|
@ -15,48 +15,51 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/Finished',
|
||||
title: "Pages/backup/Finished",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 8,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished);
|
||||
export const WithoutName = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.backupFinished,
|
||||
);
|
||||
|
||||
export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
|
||||
secret_name: 'super_secret',
|
||||
export const WithName = createExample(TestedComponent, {
|
||||
...reducerStatesExample.backupFinished,
|
||||
secret_name: "super_secret",
|
||||
} as ReducerState);
|
||||
|
||||
export const WithDetails = createExample(TestedComponent, {
|
||||
...reducerStatesExample.backupFinished,
|
||||
secret_name: 'super_secret',
|
||||
secret_name: "super_secret",
|
||||
success_details: {
|
||||
'http://anastasis.net': {
|
||||
"http://anastasis.net": {
|
||||
policy_expiration: {
|
||||
t_ms: 'never'
|
||||
t_ms: "never",
|
||||
},
|
||||
policy_version: 0
|
||||
policy_version: 0,
|
||||
},
|
||||
'http://taler.net': {
|
||||
"http://taler.net": {
|
||||
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);
|
||||
|
@ -4,41 +4,62 @@ import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function BackupFinishedScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!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">
|
||||
{reducer.currentReducerState.secret_name ? <p>
|
||||
Your backup of secret <b>"{reducer.currentReducerState.secret_name}"</b> was
|
||||
successful.
|
||||
</p> :
|
||||
<p>
|
||||
Your secret was successfully backed up.
|
||||
</p>}
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Backup finished">
|
||||
{reducer.currentReducerState.secret_name ? (
|
||||
<p>
|
||||
Your backup of secret{" "}
|
||||
<b>"{reducer.currentReducerState.secret_name}"</b> was successful.
|
||||
</p>
|
||||
) : (
|
||||
<p>Your secret was successfully backed up.</p>
|
||||
)}
|
||||
|
||||
{details && <div class="block">
|
||||
<p>The backup is stored by the following providers:</p>
|
||||
{Object.keys(details).map((x, i) => {
|
||||
const sd = details[x];
|
||||
return (
|
||||
<div key={i} class="box">
|
||||
{x}
|
||||
<p>
|
||||
version {sd.policy_version}
|
||||
{sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>}
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>);
|
||||
{details && (
|
||||
<div class="block">
|
||||
<p>The backup is stored by the following providers:</p>
|
||||
{Object.keys(details).map((x, i) => {
|
||||
const sd = details[x];
|
||||
return (
|
||||
<div key={i} class="box">
|
||||
{x}
|
||||
<p>
|
||||
version {sd.policy_version}
|
||||
{sd.policy_expiration.t_ms !== "never"
|
||||
? ` expires at: ${format(
|
||||
sd.policy_expiration.t_ms,
|
||||
"dd-MM-yyyy",
|
||||
)}`
|
||||
: " without expiration date"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
@ -19,7 +19,11 @@
|
||||
* @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 { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
|
||||
|
||||
@ -247,20 +251,20 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
|
||||
"uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
|
||||
"uuid-2": {
|
||||
state: ChallengeFeedbackStatus.Message.toString(),
|
||||
message: 'Challenge should be solved'
|
||||
message: "Challenge should be solved",
|
||||
},
|
||||
"uuid-3": {
|
||||
state: ChallengeFeedbackStatus.AuthIban.toString(),
|
||||
challenge_amount: "EUR:1",
|
||||
credit_iban: "DE12345789000",
|
||||
business_name: "Data Loss Incorporated",
|
||||
wire_transfer_subject: "Anastasis 987654321"
|
||||
wire_transfer_subject: "Anastasis 987654321",
|
||||
},
|
||||
"uuid-4": {
|
||||
state: ChallengeFeedbackStatus.Payment.toString(),
|
||||
taler_pay_uri: "taler://pay/...",
|
||||
provider: "https://localhost:8080/",
|
||||
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG"
|
||||
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
|
||||
},
|
||||
"uuid-5": {
|
||||
state: ChallengeFeedbackStatus.RateLimitExceeded.toString(),
|
||||
@ -269,7 +273,7 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
|
||||
"uuid-6": {
|
||||
state: ChallengeFeedbackStatus.Redirect.toString(),
|
||||
redirect_url: "https://videoconf.example.com/",
|
||||
http_status: 303
|
||||
http_status: 303,
|
||||
},
|
||||
"uuid-7": {
|
||||
state: ChallengeFeedbackStatus.ServerFailure.toString(),
|
||||
|
@ -11,23 +11,34 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
|
||||
}
|
||||
switch (feedback.state) {
|
||||
case ChallengeFeedbackStatus.Message:
|
||||
return (
|
||||
<div class="block has-text-danger">{feedback.message}</div>
|
||||
);
|
||||
return <div class="block has-text-danger">{feedback.message}</div>;
|
||||
case ChallengeFeedbackStatus.Solved:
|
||||
return <div />
|
||||
return <div />;
|
||||
case ChallengeFeedbackStatus.Pending:
|
||||
case ChallengeFeedbackStatus.Solved:
|
||||
case ChallengeFeedbackStatus.AuthIban:
|
||||
return null;
|
||||
case ChallengeFeedbackStatus.ServerFailure:
|
||||
return <div class="block has-text-danger">Server error.</div>;
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
default:
|
||||
return <div />;
|
||||
@ -70,19 +81,25 @@ export function ChallengeOverviewScreen(): VNode {
|
||||
feedback: challengeFeedback[ch.uuid],
|
||||
};
|
||||
}
|
||||
const policiesWithInfo = policies.map((row) => {
|
||||
let isPolicySolved = true;
|
||||
const challenges = row
|
||||
.map(({ uuid }) => {
|
||||
const info = knownChallengesMap[uuid];
|
||||
const isChallengeSolved = info?.feedback?.state === "solved";
|
||||
isPolicySolved = isPolicySolved && isChallengeSolved;
|
||||
return { info, uuid, isChallengeSolved };
|
||||
})
|
||||
.filter((ch) => ch.info !== undefined);
|
||||
const policiesWithInfo = policies
|
||||
.map((row) => {
|
||||
let isPolicySolved = true;
|
||||
const challenges = row
|
||||
.map(({ uuid }) => {
|
||||
const info = knownChallengesMap[uuid];
|
||||
const isChallengeSolved = info?.feedback?.state === "solved";
|
||||
isPolicySolved = isPolicySolved && isChallengeSolved;
|
||||
return { info, uuid, isChallengeSolved };
|
||||
})
|
||||
.filter((ch) => ch.info !== undefined);
|
||||
|
||||
return { isPolicySolved, challenges };
|
||||
});
|
||||
return {
|
||||
isPolicySolved,
|
||||
challenges,
|
||||
corrupted: row.length > challenges.length,
|
||||
};
|
||||
})
|
||||
.filter((p) => !p.corrupted);
|
||||
|
||||
const atLeastThereIsOnePolicySolved =
|
||||
policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
|
||||
@ -92,19 +109,19 @@ export function ChallengeOverviewScreen(): VNode {
|
||||
: undefined;
|
||||
return (
|
||||
<AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
|
||||
{!policies.length ? (
|
||||
{!policiesWithInfo.length ? (
|
||||
<p class="block">
|
||||
No policies found, try with another version of the secret
|
||||
</p>
|
||||
) : policies.length === 1 ? (
|
||||
) : policiesWithInfo.length === 1 ? (
|
||||
<p class="block">
|
||||
One policy found for this secret. You need to solve all the challenges
|
||||
in order to recover your secret.
|
||||
</p>
|
||||
) : (
|
||||
<p class="block">
|
||||
We have found {policies.length} polices. You need to solve all the
|
||||
challenges from one policy in order to recover your secret.
|
||||
We have found {policiesWithInfo.length} polices. You need to solve all
|
||||
the challenges from one policy in order to recover your secret.
|
||||
</p>
|
||||
)}
|
||||
{policiesWithInfo.map((policy, policy_index) => {
|
||||
@ -113,74 +130,100 @@ export function ChallengeOverviewScreen(): VNode {
|
||||
const method = authMethods[info.type as KnownAuthMethods];
|
||||
|
||||
if (!method) {
|
||||
return <div
|
||||
key={uuid}
|
||||
class="block"
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span>unknown challenge</span>
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
class="block"
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span>unknown challenge</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChallengeButton({ id, feedback }: { id: string; feedback?: ChallengeFeedback }): VNode {
|
||||
function ChallengeButton({
|
||||
id,
|
||||
feedback,
|
||||
}: {
|
||||
id: string;
|
||||
feedback?: ChallengeFeedback;
|
||||
}): VNode {
|
||||
function selectChallenge(): void {
|
||||
if (reducer) reducer.transition("select_challenge", { uuid: id })
|
||||
if (reducer) reducer.transition("select_challenge", { uuid: id });
|
||||
}
|
||||
if (!feedback) {
|
||||
return <div>
|
||||
<button class="button" onClick={selectChallenge}>
|
||||
Solve
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
class="button"
|
||||
disabled={
|
||||
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
|
||||
}
|
||||
onClick={selectChallenge}
|
||||
>
|
||||
Solve
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
switch (feedback.state) {
|
||||
case ChallengeFeedbackStatus.ServerFailure:
|
||||
case ChallengeFeedbackStatus.Unsupported:
|
||||
case ChallengeFeedbackStatus.TruthUnknown:
|
||||
case ChallengeFeedbackStatus.RateLimitExceeded: return <div />
|
||||
case ChallengeFeedbackStatus.RateLimitExceeded:
|
||||
return <div />;
|
||||
case ChallengeFeedbackStatus.AuthIban:
|
||||
case ChallengeFeedbackStatus.Payment: return <div>
|
||||
<button class="button" onClick={selectChallenge}>
|
||||
Pay
|
||||
</button>
|
||||
</div>
|
||||
case ChallengeFeedbackStatus.Redirect: return <div>
|
||||
<button class="button" onClick={selectChallenge}>
|
||||
Go to {feedback.redirect_url}
|
||||
</button>
|
||||
</div>
|
||||
case ChallengeFeedbackStatus.Solved: return <div>
|
||||
<div class="tag is-success is-large">
|
||||
Solved
|
||||
</div>
|
||||
</div>
|
||||
default: return <div>
|
||||
<button class="button" onClick={selectChallenge}>
|
||||
Solve
|
||||
</button>
|
||||
</div>
|
||||
|
||||
case ChallengeFeedbackStatus.Payment:
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
class="button"
|
||||
disabled={
|
||||
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
|
||||
}
|
||||
onClick={selectChallenge}
|
||||
>
|
||||
Pay
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
case ChallengeFeedbackStatus.Redirect:
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
class="button"
|
||||
disabled={
|
||||
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
|
||||
}
|
||||
onClick={selectChallenge}
|
||||
>
|
||||
Go to {feedback.redirect_url}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
case ChallengeFeedbackStatus.Solved:
|
||||
return (
|
||||
<div>
|
||||
<div class="tag is-success is-large">Solved</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
class="button"
|
||||
disabled={
|
||||
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
|
||||
}
|
||||
onClick={selectChallenge}
|
||||
>
|
||||
Solve
|
||||
</button>
|
||||
</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 (
|
||||
<div
|
||||
@ -202,7 +245,6 @@ export function ChallengeOverviewScreen(): VNode {
|
||||
</div>
|
||||
|
||||
<ChallengeButton id={uuid} feedback={info.feedback} />
|
||||
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -210,11 +252,13 @@ export function ChallengeOverviewScreen(): VNode {
|
||||
const policyName = policy.challenges
|
||||
.map((x) => x.info.type)
|
||||
.join(" + ");
|
||||
|
||||
const opa = !atLeastThereIsOnePolicySolved
|
||||
? undefined
|
||||
: policy.isPolicySolved
|
||||
? undefined
|
||||
: "0.6";
|
||||
? undefined
|
||||
: "0.6";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={policy_index}
|
||||
|
@ -15,24 +15,26 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/__ChallengePaying',
|
||||
title: "Pages/recovery/__ChallengePaying",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 10,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying);
|
||||
export const Example = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.challengePaying,
|
||||
);
|
||||
|
@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function ChallengePayingScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!reducer.currentReducerState ||
|
||||
reducer.currentReducerState.recovery_state === undefined
|
||||
) {
|
||||
return <div>invalid state</div>;
|
||||
}
|
||||
const payments = ['']; //reducer.currentReducerState.payments ??
|
||||
const payments = [""]; //reducer.currentReducerState.payments ??
|
||||
return (
|
||||
<AnastasisClientFrame
|
||||
hideNav
|
||||
title="Recovery: Challenge Paying"
|
||||
>
|
||||
<AnastasisClientFrame hideNav title="Recovery: Challenge Paying">
|
||||
<p>
|
||||
Some of the providers require a payment to store the encrypted
|
||||
authentication information.
|
||||
|
@ -16,37 +16,42 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/Location',
|
||||
title: "Pages/Location",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 2,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
|
||||
export const BackupSelectContinent = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.backupSelectContinent,
|
||||
);
|
||||
|
||||
export const BackupSelectCountry = createExample(TestedComponent, {
|
||||
...reducerStatesExample.backupSelectContinent,
|
||||
selected_continent: 'Testcontinent',
|
||||
selected_continent: "Testcontinent",
|
||||
} as ReducerState);
|
||||
|
||||
export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
|
||||
export const RecoverySelectContinent = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.recoverySelectContinent,
|
||||
);
|
||||
|
||||
export const RecoverySelectCountry = createExample(TestedComponent, {
|
||||
...reducerStatesExample.recoverySelectContinent,
|
||||
selected_continent: 'Testcontinent',
|
||||
selected_continent: "Testcontinent",
|
||||
} as ReducerState);
|
||||
|
@ -16,94 +16,126 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/ReviewPolicies/EditPolicies',
|
||||
title: "Pages/backup/ReviewPolicies/EditPolicies",
|
||||
args: {
|
||||
order: 6,
|
||||
},
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const EditingAPolicy = createExample(TestedComponent, {
|
||||
...reducerStatesExample.policyReview,
|
||||
policies: [{
|
||||
methods: [{
|
||||
authentication_method: 1,
|
||||
provider: 'https://anastasis.demo.taler.net/'
|
||||
}, {
|
||||
authentication_method: 2,
|
||||
provider: 'http://localhost:8086/'
|
||||
}]
|
||||
}, {
|
||||
methods: [{
|
||||
authentication_method: 1,
|
||||
provider: 'http://localhost:8086/'
|
||||
}]
|
||||
}],
|
||||
authentication_methods: [{
|
||||
type: "email",
|
||||
instructions: "Email to qwe@asd.com",
|
||||
challenge: "E5VPA"
|
||||
}, {
|
||||
type: "totp",
|
||||
instructions: "Response code for 'Anastasis'",
|
||||
challenge: "E5VPA"
|
||||
}, {
|
||||
type: "sms",
|
||||
instructions: "SMS to 6666-6666",
|
||||
challenge: ""
|
||||
}, {
|
||||
type: "question",
|
||||
instructions: "How did the chicken cross the road?",
|
||||
challenge: "C5SP8"
|
||||
}]
|
||||
} as ReducerState, { index : 0});
|
||||
|
||||
export const CreatingAPolicy = createExample(TestedComponent, {
|
||||
...reducerStatesExample.policyReview,
|
||||
policies: [{
|
||||
methods: [{
|
||||
authentication_method: 1,
|
||||
provider: 'https://anastasis.demo.taler.net/'
|
||||
}, {
|
||||
authentication_method: 2,
|
||||
provider: 'http://localhost:8086/'
|
||||
}]
|
||||
}, {
|
||||
methods: [{
|
||||
authentication_method: 1,
|
||||
provider: 'http://localhost:8086/'
|
||||
}]
|
||||
}],
|
||||
authentication_methods: [{
|
||||
type: "email",
|
||||
instructions: "Email to qwe@asd.com",
|
||||
challenge: "E5VPA"
|
||||
}, {
|
||||
type: "totp",
|
||||
instructions: "Response code for 'Anastasis'",
|
||||
challenge: "E5VPA"
|
||||
}, {
|
||||
type: "sms",
|
||||
instructions: "SMS to 6666-6666",
|
||||
challenge: ""
|
||||
}, {
|
||||
type: "question",
|
||||
instructions: "How did the chicken cross the road?",
|
||||
challenge: "C5SP8"
|
||||
}]
|
||||
} as ReducerState, { index : 3});
|
||||
export const EditingAPolicy = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
...reducerStatesExample.policyReview,
|
||||
policies: [
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "http://localhost:8086/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "http://localhost:8086/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
authentication_methods: [
|
||||
{
|
||||
type: "email",
|
||||
instructions: "Email to qwe@asd.com",
|
||||
challenge: "E5VPA",
|
||||
},
|
||||
{
|
||||
type: "totp",
|
||||
instructions: "Response code for 'Anastasis'",
|
||||
challenge: "E5VPA",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
instructions: "SMS to 6666-6666",
|
||||
challenge: "",
|
||||
},
|
||||
{
|
||||
type: "question",
|
||||
instructions: "How did the chicken cross the road?",
|
||||
challenge: "C5SP8",
|
||||
},
|
||||
],
|
||||
} as ReducerState,
|
||||
{ index: 0 },
|
||||
);
|
||||
|
||||
export const CreatingAPolicy = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
...reducerStatesExample.policyReview,
|
||||
policies: [
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "http://localhost:8086/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "http://localhost:8086/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
authentication_methods: [
|
||||
{
|
||||
type: "email",
|
||||
instructions: "Email to qwe@asd.com",
|
||||
challenge: "E5VPA",
|
||||
},
|
||||
{
|
||||
type: "totp",
|
||||
instructions: "Response code for 'Anastasis'",
|
||||
challenge: "E5VPA",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
instructions: "SMS to 6666-6666",
|
||||
challenge: "",
|
||||
},
|
||||
{
|
||||
type: "question",
|
||||
instructions: "How did the chicken cross the road?",
|
||||
challenge: "C5SP8",
|
||||
},
|
||||
],
|
||||
} as ReducerState,
|
||||
{ index: 3 },
|
||||
);
|
||||
|
@ -20,7 +20,6 @@ interface Props {
|
||||
index: number;
|
||||
cancel: () => void;
|
||||
confirm: (changes: MethodProvider[]) => void;
|
||||
|
||||
}
|
||||
|
||||
export interface MethodProvider {
|
||||
@ -28,106 +27,151 @@ export interface MethodProvider {
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode {
|
||||
const [changedProvider, setChangedProvider] = useState<Array<string>>([])
|
||||
export function EditPoliciesScreen({
|
||||
index: policy_index,
|
||||
cancel,
|
||||
confirm,
|
||||
}: Props): VNode {
|
||||
const [changedProvider, setChangedProvider] = useState<Array<string>>([]);
|
||||
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!reducer.currentReducerState ||
|
||||
reducer.currentReducerState.backup_state === undefined
|
||||
) {
|
||||
return <div>invalid state</div>;
|
||||
}
|
||||
|
||||
const selectableProviders: ProviderInfoByType = {}
|
||||
const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {})
|
||||
const selectableProviders: ProviderInfoByType = {};
|
||||
const allProviders = Object.entries(
|
||||
reducer.currentReducerState.authentication_providers || {},
|
||||
);
|
||||
for (let index = 0; index < allProviders.length; index++) {
|
||||
const [url, status] = allProviders[index]
|
||||
const [url, status] = allProviders[index];
|
||||
if ("methods" in status) {
|
||||
status.methods.map(m => {
|
||||
const type: KnownAuthMethods = m.type as KnownAuthMethods
|
||||
const values = selectableProviders[type] || []
|
||||
const isFree = !m.usage_fee || m.usage_fee.endsWith(":0")
|
||||
values.push({ url, cost: m.usage_fee, isFree })
|
||||
selectableProviders[type] = values
|
||||
})
|
||||
status.methods.map((m) => {
|
||||
const type: KnownAuthMethods = m.type as KnownAuthMethods;
|
||||
const values = selectableProviders[type] || [];
|
||||
const isFree = !m.usage_fee || m.usage_fee.endsWith(":0");
|
||||
values.push({ url, cost: m.usage_fee, isFree });
|
||||
selectableProviders[type] = values;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
|
||||
const allAuthMethods =
|
||||
reducer.currentReducerState.authentication_methods ?? [];
|
||||
const policies = reducer.currentReducerState.policies ?? [];
|
||||
const policy = policies[policy_index]
|
||||
|
||||
for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) {
|
||||
policy?.methods.find(m => m.authentication_method === method_index)?.provider
|
||||
const policy = policies[policy_index];
|
||||
|
||||
for (
|
||||
let method_index = 0;
|
||||
method_index < allAuthMethods.length;
|
||||
method_index++
|
||||
) {
|
||||
policy?.methods.find((m) => m.authentication_method === method_index)
|
||||
?.provider;
|
||||
}
|
||||
|
||||
function sendChanges(): void {
|
||||
const newMethods: MethodProvider[] = []
|
||||
const newMethods: MethodProvider[] = [];
|
||||
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) {
|
||||
newMethods.push(oldValue)
|
||||
newMethods.push(oldValue);
|
||||
}
|
||||
if (changedProvider[index] !== undefined && changedProvider[index] !== "") {
|
||||
if (
|
||||
changedProvider[index] !== undefined &&
|
||||
changedProvider[index] !== ""
|
||||
) {
|
||||
newMethods.push({
|
||||
authentication_method: index,
|
||||
provider: changedProvider[index]
|
||||
})
|
||||
provider: changedProvider[index],
|
||||
});
|
||||
}
|
||||
})
|
||||
confirm(newMethods)
|
||||
});
|
||||
confirm(newMethods);
|
||||
}
|
||||
|
||||
return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}>
|
||||
<section class="section">
|
||||
{!policy ? <p>
|
||||
Creating a new policy #{policy_index}
|
||||
</p> : <p>
|
||||
Editing policy #{policy_index}
|
||||
</p>}
|
||||
{allAuthMethods.map((method, index) => {
|
||||
//take the url from the updated change or from the policy
|
||||
const providerURL = changedProvider[index] === undefined ?
|
||||
policy?.methods.find(m => m.authentication_method === index)?.provider :
|
||||
changedProvider[index];
|
||||
return (
|
||||
<AnastasisClientFrame
|
||||
hideNav
|
||||
title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}
|
||||
>
|
||||
<section class="section">
|
||||
{!policy ? (
|
||||
<p>Creating a new policy #{policy_index}</p>
|
||||
) : (
|
||||
<p>Editing policy #{policy_index}</p>
|
||||
)}
|
||||
{allAuthMethods.map((method, index) => {
|
||||
//take the url from the updated change or from the policy
|
||||
const providerURL =
|
||||
changedProvider[index] === undefined
|
||||
? policy?.methods.find((m) => m.authentication_method === index)
|
||||
?.provider
|
||||
: changedProvider[index];
|
||||
|
||||
const type: KnownAuthMethods = method.type as KnownAuthMethods
|
||||
function changeProviderTo(url: string): void {
|
||||
const copy = [...changedProvider]
|
||||
copy[index] = url
|
||||
setChangedProvider(copy)
|
||||
}
|
||||
return (
|
||||
<div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span class="icon">
|
||||
{authMethods[type]?.icon}
|
||||
</span>
|
||||
<span>
|
||||
{method.instructions}
|
||||
</span>
|
||||
<span>
|
||||
<span class="select " >
|
||||
<select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}>
|
||||
<option key="none" value=""> << off >> </option>
|
||||
{selectableProviders[type]?.map(prov => (
|
||||
<option key={prov.url} value={prov.url}>
|
||||
{prov.url}
|
||||
const type: KnownAuthMethods = method.type as KnownAuthMethods;
|
||||
function changeProviderTo(url: string): void {
|
||||
const copy = [...changedProvider];
|
||||
copy[index] = url;
|
||||
setChangedProvider(copy);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
class="block"
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span class="icon">{authMethods[type]?.icon}</span>
|
||||
<span>{method.instructions}</span>
|
||||
<span>
|
||||
<span class="select ">
|
||||
<select
|
||||
onChange={(e) => changeProviderTo(e.currentTarget.value)}
|
||||
value={providerURL ?? ""}
|
||||
>
|
||||
<option key="none" value="">
|
||||
{" "}
|
||||
<< off >>{" "}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectableProviders[type]?.map((prov) => (
|
||||
<option key={prov.url} value={prov.url}>
|
||||
{prov.url}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={cancel}>Cancel</button>
|
||||
<span class="buttons">
|
||||
<button class="button" onClick={() => setChangedProvider([])}>Reset</button>
|
||||
<button class="button is-info" onClick={sendChanges}>Confirm</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</AnastasisClientFrame>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={cancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<span class="buttons">
|
||||
<button class="button" onClick={() => setChangedProvider([])}>
|
||||
Reset
|
||||
</button>
|
||||
<button class="button is-info" onClick={sendChanges}>
|
||||
Confirm
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
@ -15,35 +15,40 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/__PoliciesPaying',
|
||||
title: "Pages/backup/__PoliciesPaying",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 9,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Example = createExample(TestedComponent, reducerStatesExample.policyPay);
|
||||
export const Example = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.policyPay,
|
||||
);
|
||||
export const WithSomePaymentRequest = createExample(TestedComponent, {
|
||||
...reducerStatesExample.policyPay,
|
||||
policy_payment_requests: [{
|
||||
payto: 'payto://x-taler-bank/bank.taler/account-a',
|
||||
provider: 'provider1'
|
||||
}, {
|
||||
payto: 'payto://x-taler-bank/bank.taler/account-b',
|
||||
provider: 'provider2'
|
||||
}]
|
||||
policy_payment_requests: [
|
||||
{
|
||||
payto: "payto://x-taler-bank/bank.taler/account-a",
|
||||
provider: "provider1",
|
||||
},
|
||||
{
|
||||
payto: "payto://x-taler-bank/bank.taler/account-b",
|
||||
provider: "provider2",
|
||||
},
|
||||
],
|
||||
} as ReducerState);
|
||||
|
@ -3,20 +3,23 @@ import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function PoliciesPayingScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!reducer.currentReducerState ||
|
||||
reducer.currentReducerState.backup_state === undefined
|
||||
) {
|
||||
return <div>invalid state</div>;
|
||||
}
|
||||
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
|
||||
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
|
||||
<p>
|
||||
Some of the providers require a payment to store the encrypted
|
||||
recovery document.
|
||||
Some of the providers require a payment to store the encrypted recovery
|
||||
document.
|
||||
</p>
|
||||
<ul>
|
||||
{payments.map((x, i) => {
|
||||
|
@ -16,30 +16,32 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/Finished',
|
||||
title: "Pages/recovery/Finished",
|
||||
args: {
|
||||
order: 7,
|
||||
},
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const GoodEnding = createExample(TestedComponent, {
|
||||
...reducerStatesExample.recoveryFinished,
|
||||
core_secret: { mime: 'text/plain', value: 'hello' }
|
||||
core_secret: { mime: "text/plain", value: "hello" },
|
||||
} as ReducerState);
|
||||
|
||||
export const BadEnding = createExample(TestedComponent, reducerStatesExample.recoveryFinished);
|
||||
export const BadEnding = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.recoveryFinished,
|
||||
);
|
||||
|
@ -1,39 +1,53 @@
|
||||
import {
|
||||
bytesToString,
|
||||
decodeCrock
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { bytesToString, decodeCrock } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function RecoveryFinishedScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!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) {
|
||||
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>
|
||||
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))
|
||||
const secret = bytesToString(decodeCrock(encodedSecret.value));
|
||||
return (
|
||||
<AnastasisClientFrame title="Recovery Finished" hideNav>
|
||||
<p>
|
||||
Secret: {secret}
|
||||
</p>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<p>Your secret: {secret}</p>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -15,44 +15,51 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/ReviewPolicies',
|
||||
title: "Pages/backup/ReviewPolicies",
|
||||
args: {
|
||||
order: 6,
|
||||
},
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
|
||||
...reducerStatesExample.policyReview,
|
||||
policies: [{
|
||||
methods: [{
|
||||
authentication_method: 0,
|
||||
provider: 'asd'
|
||||
}, {
|
||||
authentication_method: 1,
|
||||
provider: 'asd'
|
||||
}]
|
||||
}, {
|
||||
methods: [{
|
||||
authentication_method: 1,
|
||||
provider: 'asd'
|
||||
}]
|
||||
}],
|
||||
authentication_methods: []
|
||||
policies: [
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 0,
|
||||
provider: "asd",
|
||||
},
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "asd",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "asd",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
authentication_methods: [],
|
||||
} as ReducerState);
|
||||
|
||||
export const SomePoliciesWithMethods = createExample(TestedComponent, {
|
||||
@ -62,186 +69,193 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 0,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
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/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 3,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 0,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 4,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 0,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 3,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 0,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 4,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 0,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 3,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
{
|
||||
authentication_method: 4,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 3,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 4,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 1,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 3,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
{
|
||||
authentication_method: 4,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
authentication_method: 2,
|
||||
provider: "https://kudos.demo.anastasis.lu/"
|
||||
provider: "https://kudos.demo.anastasis.lu/",
|
||||
},
|
||||
{
|
||||
authentication_method: 3,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
{
|
||||
authentication_method: 4,
|
||||
provider: "https://anastasis.demo.taler.net/"
|
||||
}
|
||||
]
|
||||
}
|
||||
provider: "https://anastasis.demo.taler.net/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
authentication_methods: [
|
||||
{
|
||||
type: "email",
|
||||
instructions: "Email to qwe@asd.com",
|
||||
challenge: "E5VPA",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
instructions: "SMS to 555-555",
|
||||
challenge: "",
|
||||
},
|
||||
{
|
||||
type: "question",
|
||||
instructions: "Does P equal NP?",
|
||||
challenge: "C5SP8",
|
||||
},
|
||||
{
|
||||
type: "totp",
|
||||
instructions: "Response code for 'Anastasis'",
|
||||
challenge: "E5VPA",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
instructions: "SMS to 6666-6666",
|
||||
challenge: "",
|
||||
},
|
||||
{
|
||||
type: "question",
|
||||
instructions: "How did the chicken cross the road?",
|
||||
challenge: "C5SP8",
|
||||
},
|
||||
],
|
||||
authentication_methods: [{
|
||||
type: "email",
|
||||
instructions: "Email to qwe@asd.com",
|
||||
challenge: "E5VPA"
|
||||
}, {
|
||||
type: "sms",
|
||||
instructions: "SMS to 555-555",
|
||||
challenge: ""
|
||||
}, {
|
||||
type: "question",
|
||||
instructions: "Does P equal NP?",
|
||||
challenge: "C5SP8"
|
||||
},{
|
||||
type: "totp",
|
||||
instructions: "Response code for 'Anastasis'",
|
||||
challenge: "E5VPA"
|
||||
}, {
|
||||
type: "sms",
|
||||
instructions: "SMS to 6666-6666",
|
||||
challenge: ""
|
||||
}, {
|
||||
type: "question",
|
||||
instructions: "How did the chicken cross the road?",
|
||||
challenge: "C5SP8"
|
||||
}]
|
||||
} as ReducerState);
|
||||
|
@ -6,16 +6,20 @@ import { EditPoliciesScreen } from "./EditPoliciesScreen";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function ReviewPoliciesScreen(): VNode {
|
||||
const [editingPolicy, setEditingPolicy] = useState<number | undefined>()
|
||||
const reducer = useAnastasisContext()
|
||||
const [editingPolicy, setEditingPolicy] = useState<number | undefined>();
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!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 ?? [];
|
||||
|
||||
if (editingPolicy !== undefined) {
|
||||
@ -28,58 +32,109 @@ export function ReviewPoliciesScreen(): VNode {
|
||||
policy_index: editingPolicy,
|
||||
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 (
|
||||
<AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies">
|
||||
{policies.length > 0 && <p class="block">
|
||||
Based on your configured authentication method you have created, some policies
|
||||
have been configured. In order to recover your secret you have to solve all the
|
||||
challenges of at least one policy.
|
||||
</p>}
|
||||
{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>
|
||||
<AnastasisClientFrame
|
||||
hideNext={errors}
|
||||
title="Backup: Review Recovery Policies"
|
||||
>
|
||||
{policies.length > 0 && (
|
||||
<p class="block">
|
||||
Based on your configured authentication method you have created, some
|
||||
policies have been configured. In order to recover your secret you
|
||||
have to solve all the challenges of at least one policy.
|
||||
</p>
|
||||
)}
|
||||
{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>
|
||||
{policies.map((p, policy_index) => {
|
||||
const methods = p.methods
|
||||
.map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
|
||||
.filter(x => !!x)
|
||||
.map(
|
||||
(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 (
|
||||
<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>
|
||||
<h3 class="subtitle">
|
||||
Policy #{policy_index + 1}: {policyName}
|
||||
</h3>
|
||||
{!methods.length && <p>
|
||||
No auth method found
|
||||
</p>}
|
||||
{!methods.length && <p>No auth method found</p>}
|
||||
{methods.map((m, i) => {
|
||||
return (
|
||||
<p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<p
|
||||
key={i}
|
||||
class="block"
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span class="icon">
|
||||
{authMethods[m.type as KnownAuthMethods]?.icon}
|
||||
</span>
|
||||
<span>
|
||||
{m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a>
|
||||
{m.instructions} recovery provided by{" "}
|
||||
<a href={m.provider}>{m.provider}</a>
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
|
||||
<button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button>
|
||||
<button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="button is-info block"
|
||||
onClick={() => setEditingPolicy(policy_index)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="button is-danger block"
|
||||
onClick={() =>
|
||||
reducer.transition("delete_policy", { policy_index })
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -15,30 +15,29 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/SecretInput',
|
||||
title: "Pages/backup/SecretInput",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 7,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSecretNamePreselected = createExample(TestedComponent, {
|
||||
...reducerStatesExample.secretEdition,
|
||||
secret_name: 'someSecretName',
|
||||
secret_name: "someSecretName",
|
||||
} as ReducerState);
|
||||
|
||||
export const WithoutName = createExample(TestedComponent, {
|
||||
|
@ -15,37 +15,35 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { SecretSelectionScreen as TestedComponent } from "./SecretSelectionScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SecretSelection',
|
||||
title: "Pages/recovery/SecretSelection",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 4,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Example = createExample(TestedComponent, {
|
||||
...reducerStatesExample.secretSelection,
|
||||
recovery_document: {
|
||||
provider_url: 'https://kudos.demo.anastasis.lu/',
|
||||
secret_name: 'secretName',
|
||||
provider_url: "https://kudos.demo.anastasis.lu/",
|
||||
secret_name: "secretName",
|
||||
version: 1,
|
||||
},
|
||||
} as ReducerState);
|
||||
|
||||
|
||||
export const NoRecoveryDocumentFound = createExample(TestedComponent, {
|
||||
...reducerStatesExample.secretSelection,
|
||||
recovery_document: undefined,
|
||||
|
@ -8,18 +8,23 @@ import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function SecretSelectionScreen(): VNode {
|
||||
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
|
||||
const [manageProvider, setManageProvider] = useState(false)
|
||||
const currentVersion = (reducer?.currentReducerState
|
||||
&& ("recovery_document" in reducer.currentReducerState)
|
||||
&& reducer.currentReducerState.recovery_document?.version) || 0;
|
||||
const [manageProvider, setManageProvider] = useState(false);
|
||||
const currentVersion =
|
||||
(reducer?.currentReducerState &&
|
||||
"recovery_document" in reducer.currentReducerState &&
|
||||
reducer.currentReducerState.recovery_document?.version) ||
|
||||
0;
|
||||
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!reducer.currentReducerState ||
|
||||
reducer.currentReducerState.recovery_state === undefined
|
||||
) {
|
||||
return <div>invalid state</div>;
|
||||
}
|
||||
|
||||
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 recoveryDocument = reducer.currentReducerState.recovery_document
|
||||
const providerList = Object.keys(
|
||||
reducer.currentReducerState.authentication_providers ?? {},
|
||||
);
|
||||
const recoveryDocument = reducer.currentReducerState.recovery_document;
|
||||
|
||||
if (!recoveryDocument) {
|
||||
return <ChooseAnotherProviderScreen
|
||||
providers={providerList} selected=""
|
||||
onChange={(newProv) => doSelectVersion(newProv, 0)}
|
||||
/>
|
||||
return (
|
||||
<ChooseAnotherProviderScreen
|
||||
providers={providerList}
|
||||
selected=""
|
||||
onChange={(newProv) => doSelectVersion(newProv, 0)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectingVersion) {
|
||||
return <SelectOtherVersionProviderScreen providers={providerList}
|
||||
provider={recoveryDocument.provider_url} version={recoveryDocument.version}
|
||||
onCancel={() => setSelectingVersion(false)}
|
||||
onConfirm={doSelectVersion}
|
||||
/>
|
||||
return (
|
||||
<SelectOtherVersionProviderScreen
|
||||
providers={providerList}
|
||||
provider={recoveryDocument.provider_url}
|
||||
version={recoveryDocument.version}
|
||||
onCancel={() => setSelectingVersion(false)}
|
||||
onConfirm={doSelectVersion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (manageProvider) {
|
||||
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />
|
||||
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame title="Recovery: Select secret">
|
||||
<div class="columns">
|
||||
<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>
|
||||
<div class="block">
|
||||
{currentVersion === 0 ? <p>
|
||||
Set to recover the latest version
|
||||
</p> : <p>
|
||||
Set to recover the version number {currentVersion}
|
||||
</p>}
|
||||
{currentVersion === 0 ? (
|
||||
<p>Set to recover the latest version</p>
|
||||
) : (
|
||||
<p>Set to recover the version number {currentVersion}</p>
|
||||
)}
|
||||
</div>
|
||||
<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 class="column">
|
||||
<p>Secret found, you can select another version or continue to the challenges solving</p>
|
||||
<p class="block">
|
||||
<button class="button is-info" onClick={() => setManageProvider(true)}>
|
||||
Manage recovery providers
|
||||
</button>
|
||||
<p>
|
||||
Secret found, you can select another version or continue to the
|
||||
challenges solving
|
||||
</p>
|
||||
<p class="block">
|
||||
<a onClick={() => setManageProvider(true)}>
|
||||
Manage recovery providers
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ChooseAnotherProviderScreen({ providers, selected, onChange }: { selected: string; providers: string[]; onChange: (prov: string) => void }): VNode {
|
||||
function ChooseAnotherProviderScreen({
|
||||
providers,
|
||||
selected,
|
||||
onChange,
|
||||
}: {
|
||||
selected: string;
|
||||
providers: string[];
|
||||
onChange: (prov: string) => void;
|
||||
}): VNode {
|
||||
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>
|
||||
<div class="field">
|
||||
<label class="label">Provider</label>
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select onChange={(e) => onChange(e.currentTarget.value)} value={selected}>
|
||||
<option key="none" disabled selected value=""> Choose a provider </option>
|
||||
{providers.map(prov => (
|
||||
<select
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
value={selected}
|
||||
>
|
||||
<option key="none" disabled selected value="">
|
||||
{" "}
|
||||
Choose a provider{" "}
|
||||
</option>
|
||||
{providers.map((prov) => (
|
||||
<option key={prov} value={prov}>
|
||||
{prov}
|
||||
</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 [otherVersion, setOtherVersion] = useState(version > 0 ? String(version) : "");
|
||||
const [otherVersion, setOtherVersion] = useState(
|
||||
version > 0 ? String(version) : "",
|
||||
);
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery: Select secret">
|
||||
@ -125,11 +173,11 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
|
||||
<div class="box">
|
||||
<h1 class="subtitle">Provider {otherProvider}</h1>
|
||||
<div class="block">
|
||||
{version === 0 ? <p>
|
||||
Set to recover the latest version
|
||||
</p> : <p>
|
||||
Set to recover the version number {version}
|
||||
</p>}
|
||||
{version === 0 ? (
|
||||
<p>Set to recover the latest version</p>
|
||||
) : (
|
||||
<p>Set to recover the version number {version}</p>
|
||||
)}
|
||||
<p>Specify other version below or use the latest</p>
|
||||
</div>
|
||||
|
||||
@ -137,9 +185,15 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
|
||||
<label class="label">Provider</label>
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select onChange={(e) => setOtherProvider(e.currentTarget.value)} value={otherProvider}>
|
||||
<option key="none" disabled selected value=""> Choose a provider </option>
|
||||
{providers.map(prov => (
|
||||
<select
|
||||
onChange={(e) => setOtherProvider(e.currentTarget.value)}
|
||||
value={otherProvider}
|
||||
>
|
||||
<option key="none" disabled selected value="">
|
||||
{" "}
|
||||
Choose a provider{" "}
|
||||
</option>
|
||||
{providers.map((prov) => (
|
||||
<option key={prov} value={prov}>
|
||||
{prov}
|
||||
</option>
|
||||
@ -156,23 +210,40 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
|
||||
label="Version"
|
||||
placeholder="version number to recover"
|
||||
grabFocus
|
||||
bind={[otherVersion, setOtherVersion]} />
|
||||
bind={[otherVersion, setOtherVersion]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={onCancel}>Cancel</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<div class="buttons">
|
||||
<AsyncButton class="button" onClick={() => onConfirm(otherProvider, 0)}>Use latest</AsyncButton>
|
||||
<AsyncButton class="button is-info" onClick={() => onConfirm(otherProvider, parseInt(otherVersion, 10))}>Confirm</AsyncButton>
|
||||
<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 class="column">
|
||||
.
|
||||
</div>
|
||||
<div class="column">.</div>
|
||||
</div>
|
||||
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -15,55 +15,63 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { SolveScreen as TestedComponent } from './SolveScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import {
|
||||
ChallengeFeedbackStatus,
|
||||
RecoveryStates,
|
||||
ReducerState,
|
||||
} from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { SolveScreen as TestedComponent } from "./SolveScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/Solve',
|
||||
title: "Pages/recovery/SolveChallenge/Solve",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 6,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const NoInformation = createExample(TestedComponent, reducerStatesExample.challengeSolving);
|
||||
export const NoInformation = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.challengeSolving,
|
||||
);
|
||||
|
||||
export const NotSupportedChallenge = createExample(TestedComponent, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'chall-type',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "chall-type",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
} as ReducerState);
|
||||
|
||||
export const MismatchedChallengeId = createExample(TestedComponent, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'chall-type',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "chall-type",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'no-no-no'
|
||||
selected_challenge_uuid: "no-no-no",
|
||||
} as ReducerState);
|
||||
|
||||
|
@ -2,76 +2,126 @@ import { h, VNode } from "preact";
|
||||
import { AnastasisClientFrame } from ".";
|
||||
import {
|
||||
ChallengeFeedback,
|
||||
ChallengeFeedbackStatus
|
||||
ChallengeFeedbackStatus,
|
||||
} from "../../../../anastasis-core/lib";
|
||||
import { Notifications } from "../../components/Notifications";
|
||||
import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { authMethods, KnownAuthMethods } from "./authMethod";
|
||||
|
||||
export function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }): VNode {
|
||||
export function SolveOverviewFeedbackDisplay(props: {
|
||||
feedback?: ChallengeFeedback;
|
||||
}): VNode {
|
||||
const { feedback } = props;
|
||||
if (!feedback) {
|
||||
return <div />;
|
||||
}
|
||||
switch (feedback.state) {
|
||||
case ChallengeFeedbackStatus.Message:
|
||||
return (<Notifications notifications={[{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: feedback.message
|
||||
}]} />);
|
||||
case ChallengeFeedbackStatus.Payment:
|
||||
return <Notifications notifications={[{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: <span>
|
||||
To pay you can <a href={feedback.taler_pay_uri}>click here</a>
|
||||
</span>
|
||||
}]} />
|
||||
case ChallengeFeedbackStatus.AuthIban:
|
||||
return <Notifications notifications={[{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: `Need to send a wire transfer to "${feedback.business_name}"`
|
||||
}]} />;
|
||||
case ChallengeFeedbackStatus.ServerFailure:
|
||||
return (<Notifications notifications={[{
|
||||
type: "ERROR",
|
||||
message: `Server error: Code ${feedback.http_status}`,
|
||||
description: feedback.error_response
|
||||
}]} />);
|
||||
case ChallengeFeedbackStatus.RateLimitExceeded:
|
||||
return (<Notifications notifications={[{
|
||||
type: "ERROR",
|
||||
message: `Message from provider`,
|
||||
description: "There were to many failed attempts."
|
||||
}]} />);
|
||||
case ChallengeFeedbackStatus.Redirect:
|
||||
return (<Notifications notifications={[{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: <span>
|
||||
Please visit this link: <a>{feedback.redirect_url}</a>
|
||||
</span>
|
||||
}]} />);
|
||||
case ChallengeFeedbackStatus.Unsupported:
|
||||
return (<Notifications notifications={[{
|
||||
type: "ERROR",
|
||||
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}"`
|
||||
}]} />);
|
||||
case ChallengeFeedbackStatus.TruthUnknown:
|
||||
return (<Notifications notifications={[{
|
||||
type: "ERROR",
|
||||
message: `Provider doesn't recognize the type of challenge`,
|
||||
description: "Contact the provider for further information"
|
||||
}]} />);
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
<pre>{JSON.stringify(feedback)}</pre>
|
||||
</div>
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: feedback.message,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.Payment:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: (
|
||||
<span>
|
||||
To pay you can <a href={feedback.taler_pay_uri}>click here</a>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.AuthIban:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: `Need to send a wire transfer to "${feedback.business_name}"`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.ServerFailure:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "ERROR",
|
||||
message: `Server error: Code ${feedback.http_status}`,
|
||||
description: feedback.error_response,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.RateLimitExceeded:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "ERROR",
|
||||
message: `Message from provider`,
|
||||
description: "There were to many failed attempts.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.Redirect:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "INFO",
|
||||
message: `Message from provider`,
|
||||
description: (
|
||||
<span>
|
||||
Please visit this link: <a>{feedback.redirect_url}</a>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.Unsupported:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "ERROR",
|
||||
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}"`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case ChallengeFeedbackStatus.TruthUnknown:
|
||||
return (
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
type: "ERROR",
|
||||
message: `Provider doesn't recognize the type of challenge`,
|
||||
description: "Contact the provider for further information",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,8 +160,16 @@ export function SolveScreen(): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -120,26 +178,36 @@ export function SolveScreen(): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Not implemented">
|
||||
<p>
|
||||
The challenge selected is not supported for this UI. Please update this
|
||||
version or try using another policy.
|
||||
The challenge selected is not supported for this UI. Please update
|
||||
this version or try using another policy.
|
||||
</p>
|
||||
{reducer &&
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
{reducer && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const chArr = reducer.currentReducerState.recovery_information.challenges;
|
||||
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] ?
|
||||
SolveNotImplemented :
|
||||
authMethods[selectedChallenge.type as KnownAuthMethods].solve ?? SolveNotImplemented
|
||||
const SolveDialog =
|
||||
!selectedChallenge ||
|
||||
!authMethods[selectedChallenge.type as KnownAuthMethods]
|
||||
? SolveNotImplemented
|
||||
: authMethods[selectedChallenge.type as KnownAuthMethods].solve ??
|
||||
SolveNotImplemented;
|
||||
|
||||
return <SolveDialog id={selectedUuid} />
|
||||
return <SolveDialog id={selectedUuid} />;
|
||||
}
|
||||
|
@ -15,24 +15,26 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { StartScreen as TestedComponent } from './StartScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { StartScreen as TestedComponent } from "./StartScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/Start',
|
||||
title: "Pages/Start",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 1,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const InitialState = createExample(TestedComponent, reducerStatesExample.initial);
|
||||
export const InitialState = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.initial,
|
||||
);
|
||||
|
@ -1,27 +1,36 @@
|
||||
|
||||
import { h, VNode } from "preact";
|
||||
import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function StartScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Home">
|
||||
<div class="columns">
|
||||
<div class="column" />
|
||||
<div class="column is-four-fifths">
|
||||
|
||||
<div class="buttons">
|
||||
<button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
|
||||
<div class="icon"><i class="mdi mdi-arrow-up" /></div>
|
||||
<button
|
||||
class="button is-success"
|
||||
autoFocus
|
||||
onClick={() => reducer.startBackup()}
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="mdi mdi-arrow-up" />
|
||||
</div>
|
||||
<span>Backup a secret</span>
|
||||
</button>
|
||||
|
||||
<button class="button is-info" onClick={() => reducer.startRecover()}>
|
||||
<div class="icon"><i class="mdi mdi-arrow-down" /></div>
|
||||
<button
|
||||
class="button is-info"
|
||||
onClick={() => reducer.startRecover()}
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="mdi mdi-arrow-down" />
|
||||
</div>
|
||||
<span>Recover a secret</span>
|
||||
</button>
|
||||
|
||||
@ -30,7 +39,6 @@ export function StartScreen(): VNode {
|
||||
<span>Restore a session</span>
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="column" />
|
||||
</div>
|
||||
|
@ -15,29 +15,31 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../utils';
|
||||
import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../utils";
|
||||
import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/__TruthsPaying',
|
||||
title: "Pages/backup/__TruthsPaying",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 10,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Example = createExample(TestedComponent, reducerStatesExample.truthsPaying);
|
||||
export const Example = createExample(
|
||||
TestedComponent,
|
||||
reducerStatesExample.truthsPaying,
|
||||
);
|
||||
export const WithPaytoList = createExample(TestedComponent, {
|
||||
...reducerStatesExample.truthsPaying,
|
||||
payments: ['payto://x-taler-bank/bank/account']
|
||||
payments: ["payto://x-taler-bank/bank/account"],
|
||||
} as ReducerState);
|
||||
|
@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
|
||||
import { AnastasisClientFrame } from "./index";
|
||||
|
||||
export function TruthsPayingScreen(): VNode {
|
||||
const reducer = useAnastasisContext()
|
||||
const reducer = useAnastasisContext();
|
||||
if (!reducer) {
|
||||
return <div>no reducer in context</div>
|
||||
return <div>no reducer in context</div>;
|
||||
}
|
||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||
return <div>invalid state</div>
|
||||
if (
|
||||
!reducer.currentReducerState ||
|
||||
reducer.currentReducerState.backup_state === undefined
|
||||
) {
|
||||
return <div>invalid state</div>;
|
||||
}
|
||||
const payments = reducer.currentReducerState.payments ?? [];
|
||||
return (
|
||||
<AnastasisClientFrame
|
||||
hideNext={"FIXME"}
|
||||
title="Backup: Truths Paying"
|
||||
>
|
||||
<AnastasisClientFrame hideNext={"FIXME"} title="Backup: Truths Paying">
|
||||
<p>
|
||||
Some of the providers require a payment to store the encrypted
|
||||
authentication information.
|
||||
|
@ -15,51 +15,67 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/email',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/email",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'email'
|
||||
const type: KnownAuthMethods = "email";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Email to sebasjm@email.com ',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Email to sebasjm@email.com ",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Email to sebasjm@email.com',
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Email to someone@sebasjm.com',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExamples = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Email to sebasjm@email.com",
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Email to someone@sebasjm.com",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,57 +1,90 @@
|
||||
import {
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { EmailInput } from "../../../components/fields/EmailInput";
|
||||
import { AnastasisClientFrame } 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 addEmailAuth = (): void => addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "email",
|
||||
instructions: `Email to ${email}`,
|
||||
challenge: encodeCrock(stringToBytes(email)),
|
||||
},
|
||||
});
|
||||
const emailError = !EMAIL_PATTERN.test(email) ? 'Email address is not valid' : undefined
|
||||
const errors = !email ? 'Add your email' : emailError
|
||||
const addEmailAuth = (): void =>
|
||||
addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "email",
|
||||
instructions: `Email to ${email}`,
|
||||
challenge: encodeCrock(stringToBytes(email)),
|
||||
},
|
||||
});
|
||||
const emailError = !EMAIL_PATTERN.test(email)
|
||||
? "Email address is not valid"
|
||||
: undefined;
|
||||
const errors = !email ? "Add your email" : emailError;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<p>
|
||||
For email authentication, you need to provide an email address. When
|
||||
recovering your secret, you will need to enter the code you receive by
|
||||
email.
|
||||
email. Add the uuid from the challenge
|
||||
</p>
|
||||
<div>
|
||||
<EmailInput
|
||||
label="Email address"
|
||||
error={emailError}
|
||||
placeholder="email@domain.com"
|
||||
bind={[email, setEmail]} />
|
||||
bind={[email, setEmail]}
|
||||
/>
|
||||
</div>
|
||||
{configured.length > 0 && <section class="section">
|
||||
<div class="block">
|
||||
Your emails:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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></section>}
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your emails:</div>
|
||||
<div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
<div>
|
||||
<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}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,66 +15,76 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/email',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/email",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'email'
|
||||
const type: KnownAuthMethods = "email";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
);
|
||||
|
||||
export const PaymentFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const PaymentFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
challenge_feedback: {
|
||||
"uuid-1": {
|
||||
state: ChallengeFeedbackStatus.Payment,
|
||||
taler_pay_uri: "taler://pay/...",
|
||||
provider: "https://localhost:8080/",
|
||||
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
|
||||
},
|
||||
},
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
challenge_feedback: {
|
||||
'uuid-1': {
|
||||
state: ChallengeFeedbackStatus.Payment,
|
||||
taler_pay_uri: "taler://pay/...",
|
||||
provider: "https://localhost:8080/",
|
||||
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG"
|
||||
}
|
||||
}
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
|
||||
);
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,18 +79,19 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
An email has been sent to "<b>{selectedChallenge.instructions}</b>". Type the
|
||||
code below
|
||||
An email has been sent to "<b>{selectedChallenge.instructions}</b>".
|
||||
Type the code below.
|
||||
<b>Here we need to add the code "{selectedUuid}"</b>
|
||||
</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
|
||||
@ -97,9 +105,11 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -15,50 +15,66 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/IBAN',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/IBAN",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'iban'
|
||||
const type: KnownAuthMethods = "iban";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Wire transfer from QWEASD123123 with holder Javier',
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
|
||||
remove: () => null
|
||||
}]
|
||||
},);
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
export const WithMoreExamples = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Wire transfer from QWEASD123123 with holder Javier",
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
canonicalJson,
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
stringToBytes,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
@ -9,56 +9,98 @@ import { AuthMethodSetupProps } from ".";
|
||||
import { TextInput } from "../../../components/fields/TextInput";
|
||||
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 [account, setAccount] = useState("");
|
||||
const addIbanAuth = (): void => addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "iban",
|
||||
instructions: `Wire transfer from ${account} with holder ${name}`,
|
||||
challenge: encodeCrock(stringToBytes(canonicalJson({
|
||||
name, account
|
||||
}))),
|
||||
},
|
||||
});
|
||||
const errors = !name ? 'Add an account name' : (
|
||||
!account ? 'Add an account IBAN number' : undefined
|
||||
)
|
||||
const addIbanAuth = (): void =>
|
||||
addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "iban",
|
||||
instructions: `Wire transfer from ${account} with holder ${name}`,
|
||||
challenge: encodeCrock(
|
||||
stringToBytes(
|
||||
canonicalJson({
|
||||
name,
|
||||
account,
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
const errors = !name
|
||||
? "Add an account name"
|
||||
: !account
|
||||
? "Add an account IBAN number"
|
||||
: undefined;
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add bank transfer authentication">
|
||||
<p>
|
||||
For bank transfer authentication, you need to provide a bank
|
||||
account (account holder name and IBAN). When recovering your
|
||||
secret, you will be asked to pay the recovery fee via bank
|
||||
transfer from the account you provided here.
|
||||
For bank transfer authentication, you need to provide a bank account
|
||||
(account holder name and IBAN). When recovering your secret, you will be
|
||||
asked to pay the recovery fee via bank transfer from the account you
|
||||
provided here.
|
||||
</p>
|
||||
<div>
|
||||
<TextInput
|
||||
label="Bank account holder name"
|
||||
grabFocus
|
||||
placeholder="John Smith"
|
||||
bind={[name, setName]} />
|
||||
bind={[name, setName]}
|
||||
/>
|
||||
<TextInput
|
||||
label="IBAN"
|
||||
placeholder="DE91100000000123456789"
|
||||
bind={[account, setAccount]} />
|
||||
bind={[account, setAccount]}
|
||||
/>
|
||||
</div>
|
||||
{configured.length > 0 && <section class="section">
|
||||
<div class="block">
|
||||
Your bank accounts:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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></section>}
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your bank accounts:</div>
|
||||
<div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
<div>
|
||||
<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}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,42 +15,46 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/Iban',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/Iban",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'iban'
|
||||
const type: KnownAuthMethods = "iban";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
|
||||
);
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,19 +79,17 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
Send a wire transfer to the address
|
||||
</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
<p>Send a wire transfer to the address,</p>
|
||||
<button class="button">Check</button>
|
||||
|
||||
<div
|
||||
style={{
|
||||
@ -96,9 +101,11 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -16,51 +16,67 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Post',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/Post",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'post'
|
||||
const type: KnownAuthMethods = "post";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Letter to address in postal code QWE456',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Letter to address in postal code QWE456",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Letter to address in postal code QWE456',
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Letter to address in postal code ABC123',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExamples = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Letter to address in postal code QWE456",
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Letter to address in postal code ABC123",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
canonicalJson, encodeCrock,
|
||||
stringToBytes
|
||||
canonicalJson,
|
||||
encodeCrock,
|
||||
stringToBytes,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
@ -8,7 +9,11 @@ import { AnastasisClientFrame } from "..";
|
||||
import { TextInput } from "../../../components/fields/TextInput";
|
||||
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 [street, setStreet] = useState("");
|
||||
const [city, setCity] = useState("");
|
||||
@ -32,68 +37,83 @@ export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthM
|
||||
});
|
||||
};
|
||||
|
||||
const errors = !fullName ? 'The full name is missing' : (
|
||||
!street ? 'The street is missing' : (
|
||||
!city ? 'The city is missing' : (
|
||||
!postcode ? 'The postcode is missing' : (
|
||||
!country ? 'The country is missing' : undefined
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
const errors = !fullName
|
||||
? "The full name is missing"
|
||||
: !street
|
||||
? "The street is missing"
|
||||
: !city
|
||||
? "The city is missing"
|
||||
: !postcode
|
||||
? "The postcode is missing"
|
||||
: !country
|
||||
? "The country is missing"
|
||||
: undefined;
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add postal authentication">
|
||||
<p>
|
||||
For postal letter authentication, you need to provide a postal
|
||||
address. When recovering your secret, you will be asked to enter a
|
||||
code that you will receive in a letter to that address.
|
||||
For postal letter authentication, you need to provide a postal address.
|
||||
When recovering your secret, you will be asked to enter a code that you
|
||||
will receive in a letter to that address.
|
||||
</p>
|
||||
<div>
|
||||
<TextInput
|
||||
grabFocus
|
||||
label="Full Name"
|
||||
bind={[fullName, setFullName]}
|
||||
/>
|
||||
<TextInput grabFocus label="Full Name" bind={[fullName, setFullName]} />
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
label="Street"
|
||||
bind={[street, setStreet]}
|
||||
/>
|
||||
<TextInput label="Street" bind={[street, setStreet]} />
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
label="City" bind={[city, setCity]}
|
||||
/>
|
||||
<TextInput label="City" bind={[city, setCity]} />
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
label="Postal Code" bind={[postcode, setPostcode]}
|
||||
/>
|
||||
<TextInput label="Postal Code" bind={[postcode, setPostcode]} />
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
label="Country"
|
||||
bind={[country, setCountry]}
|
||||
/>
|
||||
<TextInput label="Country" bind={[country, setCountry]} />
|
||||
</div>
|
||||
|
||||
{configured.length > 0 && <section class="section">
|
||||
<div class="block">
|
||||
Your postal code:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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>
|
||||
</section>}
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={cancel}>Cancel</button>
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your postal code:</div>
|
||||
<div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={cancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<span data-tooltip={errors}>
|
||||
<button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button>
|
||||
<button
|
||||
class="button is-info"
|
||||
disabled={errors !== undefined}
|
||||
onClick={addPostAuth}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
|
@ -15,42 +15,46 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/post',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/post",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'post'
|
||||
const type: KnownAuthMethods = "post";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
|
||||
);
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,18 +79,16 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
Wait for the answer
|
||||
</p>
|
||||
<p>Wait for the answer</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
|
||||
<div
|
||||
@ -96,9 +101,11 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -16,51 +16,69 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Question',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/Question",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'question'
|
||||
const type: KnownAuthMethods = "question";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Is integer factorization polynomial? (non-quantum computer)',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions:
|
||||
"Is integer factorization polynomial? (non-quantum computer)",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Does P equal NP?',
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'asd',
|
||||
type,
|
||||
instructions: 'Are continuous groups automatically differential groups?',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExamples = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "Does P equal NP?",
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "asd",
|
||||
type,
|
||||
instructions:
|
||||
"Are continuous groups automatically differential groups?",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,27 +1,31 @@
|
||||
import {
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { AuthMethodSetupProps } from "./index";
|
||||
import { AnastasisClientFrame } from "../index";
|
||||
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 [answerText, setAnswerText] = useState("");
|
||||
const addQuestionAuth = (): void => addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "question",
|
||||
instructions: questionText,
|
||||
challenge: encodeCrock(stringToBytes(answerText)),
|
||||
},
|
||||
});
|
||||
const addQuestionAuth = (): void =>
|
||||
addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "question",
|
||||
instructions: questionText,
|
||||
challenge: encodeCrock(stringToBytes(answerText)),
|
||||
},
|
||||
});
|
||||
|
||||
const errors = !questionText ? "Add your security question" : (
|
||||
!answerText ? 'Add the answer to your question' : undefined
|
||||
)
|
||||
const errors = !questionText
|
||||
? "Add your security question"
|
||||
: !answerText
|
||||
? "Add the answer to your question"
|
||||
: undefined;
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add Security Question">
|
||||
<div>
|
||||
@ -36,7 +40,8 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
|
||||
label="Security question"
|
||||
grabFocus
|
||||
placeholder="Your question"
|
||||
bind={[questionText, setQuestionText]} />
|
||||
bind={[questionText, setQuestionText]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
@ -46,25 +51,53 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{configured.length > 0 && <section class="section">
|
||||
<div class="block">
|
||||
Your security questions:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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></section>}
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your security questions:</div>
|
||||
<div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame >
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
@ -15,186 +15,205 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/question',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/question",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'question'
|
||||
const type: KnownAuthMethods = "question";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
);
|
||||
|
||||
export const MessageFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.Message,
|
||||
message: 'Challenge should be solved'
|
||||
}
|
||||
}
|
||||
|
||||
} as ReducerState);
|
||||
|
||||
export const ServerFailureFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
policies: [],
|
||||
message: "Challenge should be solved",
|
||||
},
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
state: ChallengeFeedbackStatus.ServerFailure,
|
||||
http_status: 500,
|
||||
error_response: "Couldn't connect to mysql"
|
||||
}
|
||||
}
|
||||
|
||||
} as ReducerState);
|
||||
|
||||
export const ServerFailureFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.ServerFailure,
|
||||
http_status: 500,
|
||||
error_response: "Couldn't connect to mysql",
|
||||
},
|
||||
},
|
||||
} as ReducerState,
|
||||
);
|
||||
|
||||
export const RedirectFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.Redirect,
|
||||
http_status: 302,
|
||||
redirect_url: 'http://video.taler.net'
|
||||
}
|
||||
}
|
||||
|
||||
} as ReducerState);
|
||||
|
||||
export const MessageRateLimitExceededFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
policies: [],
|
||||
redirect_url: "http://video.taler.net",
|
||||
},
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
state: ChallengeFeedbackStatus.RateLimitExceeded,
|
||||
}
|
||||
}
|
||||
|
||||
} as ReducerState);
|
||||
|
||||
export const MessageRateLimitExceededFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.RateLimitExceeded,
|
||||
},
|
||||
},
|
||||
} as ReducerState,
|
||||
);
|
||||
|
||||
export const UnsupportedFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.Unsupported,
|
||||
http_status: 500,
|
||||
unsupported_method: 'Question'
|
||||
}
|
||||
}
|
||||
|
||||
unsupported_method: "Question",
|
||||
},
|
||||
},
|
||||
} as ReducerState);
|
||||
|
||||
export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.TruthUnknown,
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
} as ReducerState);
|
||||
|
||||
export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.AuthIban,
|
||||
challenge_amount: "EUR:1",
|
||||
credit_iban: "DE12345789000",
|
||||
@ -210,30 +229,30 @@ export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
|
||||
wire_transfer_subject: "foo",
|
||||
},
|
||||
method: "iban",
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
} as ReducerState);
|
||||
|
||||
export const PaymentFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'ASDASDSAD!1'
|
||||
}],
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "ASDASDSAD!1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: 'ASDASDSAD!1',
|
||||
selected_challenge_uuid: "ASDASDSAD!1",
|
||||
challenge_feedback: {
|
||||
'ASDASDSAD!1': {
|
||||
"ASDASDSAD!1": {
|
||||
state: ChallengeFeedbackStatus.Payment,
|
||||
taler_pay_uri : "taler://pay/...",
|
||||
provider : "https://localhost:8080/",
|
||||
payment_secret : "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG"
|
||||
}
|
||||
}
|
||||
taler_pay_uri: "taler://pay/...",
|
||||
provider: "https://localhost:8080/",
|
||||
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
|
||||
},
|
||||
},
|
||||
} as ReducerState);
|
||||
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,18 +79,16 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
Answer the question please
|
||||
</p>
|
||||
<p>Answer the question please</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
|
||||
<div
|
||||
@ -96,9 +101,11 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -16,51 +16,67 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Sms',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/Sms",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'sms'
|
||||
const type: KnownAuthMethods = "sms";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'SMS to +11-1234-2345',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "SMS to +11-1234-2345",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'SMS to +11-1234-2345',
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'SMS to +11-5555-2345',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExamples = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "SMS to +11-1234-2345",
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: "SMS to +11-5555-2345",
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,14 +1,15 @@
|
||||
import {
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import { AuthMethodSetupProps } from ".";
|
||||
import { PhoneNumberInput } from "../../../components/fields/NumberInput";
|
||||
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 addSmsAuth = (): void => {
|
||||
addAuthMethod({
|
||||
@ -23,7 +24,7 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
|
||||
useLayoutEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
const errors = !mobileNumber ? 'Add a mobile number' : undefined
|
||||
const errors = !mobileNumber ? "Add a mobile number" : undefined;
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add SMS authentication">
|
||||
<div>
|
||||
@ -37,23 +38,52 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
|
||||
label="Mobile number"
|
||||
placeholder="Your mobile number"
|
||||
grabFocus
|
||||
bind={[mobileNumber, setMobileNumber]} />
|
||||
bind={[mobileNumber, setMobileNumber]}
|
||||
/>
|
||||
</div>
|
||||
{configured.length > 0 && <section class="section">
|
||||
<div class="block">
|
||||
Your mobile numbers:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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></section>}
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={cancel}>Cancel</button>
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your mobile numbers:</div>
|
||||
<div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={cancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<span data-tooltip={errors}>
|
||||
<button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button>
|
||||
<button
|
||||
class="button is-info"
|
||||
disabled={errors !== undefined}
|
||||
onClick={addSmsAuth}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,42 +15,46 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/sms',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/sms",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'sms'
|
||||
const type: KnownAuthMethods = "sms";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
|
||||
);
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,18 +79,18 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
An sms has been sent to "<b>{selectedChallenge.instructions}</b>". Type the code
|
||||
below
|
||||
An sms has been sent to "<b>{selectedChallenge.instructions}</b>". Type
|
||||
the code below
|
||||
</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
|
||||
@ -97,9 +104,11 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -16,49 +16,65 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/TOTP',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/TOTP",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'totp'
|
||||
const type: KnownAuthMethods = "totp";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Enter 8 digits code for "Anastasis"',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Enter 8 digits code for "Anastasis1"',
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: 'Enter 8 digits code for "Anastasis2"',
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: 'Enter 8 digits code for "Anastasis"',
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
export const WithMoreExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: 'Enter 8 digits code for "Anastasis1"',
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: 'Enter 8 digits code for "Anastasis2"',
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,7 +1,4 @@
|
||||
import {
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { AuthMethodSetupProps } from "./index";
|
||||
@ -10,30 +7,37 @@ import { TextInput } from "../../../components/fields/TextInput";
|
||||
import { QR } from "../../../components/QR";
|
||||
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 [test, setTest] = useState("");
|
||||
const digits = 8
|
||||
const digits = 8;
|
||||
const secretKey = useMemo(() => {
|
||||
const array = new Uint8Array(32)
|
||||
return window.crypto.getRandomValues(array)
|
||||
}, [])
|
||||
const array = new Uint8Array(32);
|
||||
return window.crypto.getRandomValues(array);
|
||||
}, []);
|
||||
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({
|
||||
authentication_method: {
|
||||
type: "totp",
|
||||
instructions: `Enter ${digits} digits code for "${name}"`,
|
||||
challenge: encodeCrock(stringToBytes(totpURL)),
|
||||
},
|
||||
});
|
||||
const addTotpAuth = (): void =>
|
||||
addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "totp",
|
||||
instructions: `Enter ${digits} digits code for "${name}"`,
|
||||
challenge: encodeCrock(stringToBytes(totpURL)),
|
||||
},
|
||||
});
|
||||
|
||||
const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
|
||||
|
||||
const errors = !name ? 'The TOTP name is missing' : (
|
||||
!testCodeMatches ? 'The test code doesnt match' : undefined
|
||||
);
|
||||
const errors = !name
|
||||
? "The TOTP name is missing"
|
||||
: !testCodeMatches
|
||||
? "The test code doesnt match"
|
||||
: undefined;
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add TOTP authentication">
|
||||
<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.
|
||||
</p>
|
||||
<div class="block">
|
||||
<TextInput
|
||||
label="TOTP Name"
|
||||
grabFocus
|
||||
bind={[name, setName]} />
|
||||
<TextInput label="TOTP Name" grabFocus bind={[name, setName]} />
|
||||
</div>
|
||||
<div style={{ height: 300 }}>
|
||||
<QR text={totpURL} />
|
||||
@ -53,25 +54,51 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
|
||||
<p>
|
||||
After scanning the code with your TOTP App, test it in the input below.
|
||||
</p>
|
||||
<TextInput
|
||||
label="Test code"
|
||||
bind={[test, setTest]} />
|
||||
{configured.length > 0 && <section class="section">
|
||||
<div class="block">
|
||||
Your TOTP numbers:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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></section>}
|
||||
<TextInput label="Test code" bind={[test, setTest]} />
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your TOTP numbers:</div>
|
||||
<div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
<div>
|
||||
<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}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,42 +15,46 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/totp',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/totp",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'totp'
|
||||
const type: KnownAuthMethods = "totp";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
|
||||
);
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,18 +79,16 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
enter the totp solution
|
||||
</p>
|
||||
<p>enter the totp solution</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
|
||||
<div
|
||||
@ -96,9 +101,11 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -16,51 +16,68 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
import logoImage from '../../../assets/logo.jpeg'
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
import logoImage from "../../../assets/logo.jpeg";
|
||||
|
||||
export default {
|
||||
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Video',
|
||||
title: "Pages/backup/AuthorizationMethod/AuthMethods/Video",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'video'
|
||||
const type: KnownAuthMethods = "video";
|
||||
|
||||
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: []
|
||||
});
|
||||
export const Empty = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: logoImage,
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithOneExample = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: logoImage,
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
|
||||
configured: [{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: logoImage,
|
||||
remove: () => null
|
||||
},{
|
||||
challenge: 'qwe',
|
||||
type,
|
||||
instructions: logoImage,
|
||||
remove: () => null
|
||||
}]
|
||||
});
|
||||
export const WithMoreExamples = createExample(
|
||||
TestedComponent[type].setup,
|
||||
reducerStatesExample.authEditing,
|
||||
{
|
||||
configured: [
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: logoImage,
|
||||
remove: () => null,
|
||||
},
|
||||
{
|
||||
challenge: "qwe",
|
||||
type,
|
||||
instructions: logoImage,
|
||||
remove: () => null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
@ -1,53 +1,86 @@
|
||||
import {
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { ImageInput } from "../../../components/fields/ImageInput";
|
||||
import { AuthMethodSetupProps } 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 addVideoAuth = (): void => {
|
||||
addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "video",
|
||||
instructions: 'Join a video call',
|
||||
instructions: "Join a video call",
|
||||
challenge: encodeCrock(stringToBytes(image)),
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add video authentication">
|
||||
<p>
|
||||
For video identification, you need to provide a passport-style
|
||||
photograph. When recovering your secret, you will be asked to join a
|
||||
video call. During that call, a human will use the photograph to
|
||||
verify your identity.
|
||||
For video identification, you need to provide a passport-style
|
||||
photograph. When recovering your secret, you will be asked to join a
|
||||
video call. During that call, a human will use the photograph to verify
|
||||
your identity.
|
||||
</p>
|
||||
<div style={{textAlign:'center'}}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ImageInput
|
||||
label="Choose photograph"
|
||||
grabFocus
|
||||
bind={[image, setImage]} />
|
||||
bind={[image, setImage]}
|
||||
/>
|
||||
</div>
|
||||
{configured.length > 0 && <section class="section">
|
||||
{configured.length > 0 && (
|
||||
<section class="section">
|
||||
<div class="block">Your photographs:</div>
|
||||
<div class="block">
|
||||
Your photographs:
|
||||
</div><div class="block">
|
||||
{configured.map((c, i) => {
|
||||
return <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>
|
||||
return (
|
||||
<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></section>}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={cancel}>Cancel</button>
|
||||
<button class="button is-info" onClick={addVideoAuth}>Add</button>
|
||||
<div
|
||||
style={{
|
||||
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>
|
||||
</AnastasisClientFrame>
|
||||
|
@ -15,42 +15,46 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
|
||||
import { createExample, reducerStatesExample } from '../../../utils';
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
|
||||
import { createExample, reducerStatesExample } from "../../../utils";
|
||||
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
|
||||
|
||||
export default {
|
||||
title: 'Pages/recovery/SolveChallenge/AuthMethods/video',
|
||||
title: "Pages/recovery/SolveChallenge/AuthMethods/video",
|
||||
component: TestedComponent,
|
||||
args: {
|
||||
order: 5,
|
||||
},
|
||||
argTypes: {
|
||||
onUpdate: { action: 'onUpdate' },
|
||||
onBack: { action: 'onBack' },
|
||||
onUpdate: { action: "onUpdate" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
const type: KnownAuthMethods = 'video'
|
||||
const type: KnownAuthMethods = "video";
|
||||
|
||||
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [{
|
||||
cost: 'USD:1',
|
||||
instructions: 'does P equals NP?',
|
||||
type: 'question',
|
||||
uuid: 'uuid-1'
|
||||
}],
|
||||
policies: [],
|
||||
export const WithoutFeedback = createExample(
|
||||
TestedComponent[type].solve,
|
||||
{
|
||||
...reducerStatesExample.challengeSolving,
|
||||
recovery_information: {
|
||||
challenges: [
|
||||
{
|
||||
cost: "USD:1",
|
||||
instructions: "does P equals NP?",
|
||||
type: "question",
|
||||
uuid: "uuid-1",
|
||||
},
|
||||
],
|
||||
policies: [],
|
||||
},
|
||||
selected_challenge_uuid: "uuid-1",
|
||||
} as ReducerState,
|
||||
{
|
||||
id: "uuid-1",
|
||||
},
|
||||
selected_challenge_uuid: 'uuid-1',
|
||||
} as ReducerState, {
|
||||
id: 'uuid-1',
|
||||
});
|
||||
|
||||
);
|
||||
|
@ -44,8 +44,16 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery problem">
|
||||
<div>invalid state</div>
|
||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2em",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<button class="button" onClick={() => reducer.back()}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
@ -62,8 +70,7 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
const feedback = challengeFeedback[selectedUuid]
|
||||
|
||||
const feedback = challengeFeedback[selectedUuid];
|
||||
|
||||
async function onNext(): Promise<void> {
|
||||
return reducer?.transition("solve_challenge", { answer });
|
||||
@ -72,18 +79,16 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
reducer?.back();
|
||||
}
|
||||
|
||||
|
||||
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|
||||
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|
||||
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
|
||||
const shouldHideConfirm =
|
||||
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
|
||||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
|
||||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
|
||||
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<SolveOverviewFeedbackDisplay feedback={feedback} />
|
||||
<p>
|
||||
You are gonna be called to check your identity
|
||||
</p>
|
||||
<p>You are gonna be called to check your identity</p>
|
||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||
|
||||
<div
|
||||
@ -96,9 +101,11 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
|
||||
<button class="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>}
|
||||
{!shouldHideConfirm && (
|
||||
<AsyncButton class="button is-info" onClick={onNext}>
|
||||
Confirm
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { AuthMethod } from "anastasis-core";
|
||||
import { h, VNode } from "preact";
|
||||
import postalIcon from '../../../assets/icons/auth_method/postal.svg';
|
||||
import questionIcon from '../../../assets/icons/auth_method/question.svg';
|
||||
import smsIcon from '../../../assets/icons/auth_method/sms.svg';
|
||||
import videoIcon from '../../../assets/icons/auth_method/video.svg';
|
||||
import postalIcon from "../../../assets/icons/auth_method/postal.svg";
|
||||
import questionIcon from "../../../assets/icons/auth_method/question.svg";
|
||||
import smsIcon from "../../../assets/icons/auth_method/sms.svg";
|
||||
import videoIcon from "../../../assets/icons/auth_method/video.svg";
|
||||
import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup";
|
||||
import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve";
|
||||
import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup";
|
||||
@ -20,8 +20,7 @@ import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve";
|
||||
import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve";
|
||||
import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve";
|
||||
|
||||
|
||||
export type AuthMethodWithRemove = AuthMethod & { remove: () => void }
|
||||
export type AuthMethodWithRemove = AuthMethod & { remove: () => void };
|
||||
|
||||
export interface AuthMethodSetupProps {
|
||||
method: string;
|
||||
@ -43,10 +42,18 @@ interface AuthMethodConfiguration {
|
||||
}
|
||||
// export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
|
||||
|
||||
const ALL_METHODS = ['sms', 'email', 'post', 'question', 'video' , 'totp', 'iban'] as const;
|
||||
export type KnownAuthMethods = (typeof ALL_METHODS)[number];
|
||||
const ALL_METHODS = [
|
||||
"sms",
|
||||
"email",
|
||||
"post",
|
||||
"question",
|
||||
"video",
|
||||
"totp",
|
||||
"iban",
|
||||
] as const;
|
||||
export type KnownAuthMethods = typeof ALL_METHODS[number];
|
||||
export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
|
||||
return ALL_METHODS.includes(value as KnownAuthMethods)
|
||||
return ALL_METHODS.includes(value as KnownAuthMethods);
|
||||
}
|
||||
|
||||
type KnowMethodConfig = {
|
||||
@ -96,5 +103,5 @@ export const authMethods: KnowMethodConfig = {
|
||||
setup: VideoSetup,
|
||||
solve: VideoSolve,
|
||||
skip: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,54 +1,61 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import jssha from 'jssha'
|
||||
import jssha from "jssha";
|
||||
|
||||
const SEARCH_RANGE = 16
|
||||
const timeStep = 30
|
||||
const SEARCH_RANGE = 16;
|
||||
const timeStep = 30;
|
||||
|
||||
export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean {
|
||||
const now = new Date().getTime()
|
||||
export function computeTOTPandCheck(
|
||||
secretKey: Uint8Array,
|
||||
digits: number,
|
||||
code: number,
|
||||
): boolean {
|
||||
const now = new Date().getTime();
|
||||
const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
|
||||
|
||||
for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
|
||||
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);
|
||||
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 = ((
|
||||
(hmac_text[offset + 0] << 24) +
|
||||
(hmac_text[offset + 1] << 16) +
|
||||
(hmac_text[offset + 2] << 8) +
|
||||
(hmac_text[offset + 3])
|
||||
) & 0x7fffffff) % Math.pow(10, digits)
|
||||
const otp =
|
||||
(((hmac_text[offset + 0] << 24) +
|
||||
(hmac_text[offset + 1] << 16) +
|
||||
(hmac_text[offset + 2] << 8) +
|
||||
hmac_text[offset + 3]) &
|
||||
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 {
|
||||
let rpos = 0
|
||||
let bits = 0
|
||||
let vbit = 0
|
||||
let rpos = 0;
|
||||
let bits = 0;
|
||||
let vbit = 0;
|
||||
|
||||
let result = ""
|
||||
while ((rpos < buffer.length) || (vbit > 0)) {
|
||||
if ((rpos < buffer.length) && (vbit < 5)) {
|
||||
let result = "";
|
||||
while (rpos < buffer.length || vbit > 0) {
|
||||
if (rpos < buffer.length && vbit < 5) {
|
||||
bits = (bits << 8) | buffer[rpos++];
|
||||
vbit += 8;
|
||||
}
|
||||
if (vbit < 5) {
|
||||
bits <<= (5 - vbit);
|
||||
bits <<= 5 - vbit;
|
||||
vbit = 5;
|
||||
}
|
||||
result += encTable__[(bits >> (vbit - 5)) & 31];
|
||||
vbit -= 5;
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
// const array = new Uint8Array(256)
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { FunctionalComponent, h } from 'preact';
|
||||
import { Link } from 'preact-router/match';
|
||||
import { FunctionalComponent, h } from "preact";
|
||||
import { Link } from "preact-router/match";
|
||||
|
||||
const Notfound: FunctionalComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Error 404</h1>
|
||||
<p>That page doesn't exist.</p>
|
||||
<Link href="/">
|
||||
<h4>Back to Home</h4>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h1>Error 404</h1>
|
||||
<p>That page doesn't exist.</p>
|
||||
<Link href="/">
|
||||
<h4>Back to Home</h4>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notfound;
|
||||
|
@ -1,43 +1,42 @@
|
||||
import { FunctionalComponent, h } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { FunctionalComponent, h } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
interface Props {
|
||||
user: string;
|
||||
user: string;
|
||||
}
|
||||
|
||||
const Profile: FunctionalComponent<Props> = (props: Props) => {
|
||||
const { user } = props;
|
||||
const [time, setTime] = useState<number>(Date.now());
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const { user } = props;
|
||||
const [time, setTime] = useState<number>(Date.now());
|
||||
const [count, setCount] = useState<number>(0);
|
||||
|
||||
// gets called when this route is navigated to
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setTime(Date.now()), 1000);
|
||||
// gets called when this route is navigated to
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setTime(Date.now()), 1000);
|
||||
|
||||
// gets called just before navigating away from the route
|
||||
return (): void => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// update the current time
|
||||
const increment = (): void => {
|
||||
setCount(count + 1);
|
||||
// gets called just before navigating away from the route
|
||||
return (): void => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Profile: {user}</h1>
|
||||
<p>This is the user profile for a user named {user}.</p>
|
||||
// update the current time
|
||||
const increment = (): void => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
<div>Current time: {new Date(time).toLocaleString()}</div>
|
||||
return (
|
||||
<div>
|
||||
<h1>Profile: {user}</h1>
|
||||
<p>This is the user profile for a user named {user}.</p>
|
||||
|
||||
<p>
|
||||
<button onClick={increment}>Click Me</button> Clicked {count}{' '}
|
||||
times.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<div>Current time: {new Date(time).toLocaleString()}</div>
|
||||
|
||||
<p>
|
||||
<button onClick={increment}>Click Me</button> Clicked {count} times.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
@ -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>
|
||||
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><% preact.title %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="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 %>
|
||||
</head>
|
||||
<body>
|
||||
<% preact.bodyEnd %>
|
||||
</body>
|
||||
<html
|
||||
lang="en"
|
||||
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<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>
|
||||
<body>
|
||||
<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>
|
||||
</html>
|
||||
|
@ -1,45 +1,67 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { BackupStates, RecoveryStates, ReducerState } from 'anastasis-core';
|
||||
import { FunctionalComponent, h, VNode } from 'preact';
|
||||
import { AnastasisProvider } from '../context/anastasis';
|
||||
import { BackupStates, RecoveryStates, ReducerState } from "anastasis-core";
|
||||
import { FunctionalComponent, h, VNode } from "preact";
|
||||
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 => {
|
||||
return <AnastasisProvider value={{
|
||||
currentReducerState,
|
||||
currentError: undefined,
|
||||
back: async () => { null },
|
||||
dismissError: async () => { null },
|
||||
reset: () => { null },
|
||||
runTransaction: async () => { null },
|
||||
startBackup: () => { null },
|
||||
startRecover: () => { null },
|
||||
transition: async () => { null },
|
||||
}}>
|
||||
<Component {...args} />
|
||||
</AnastasisProvider>
|
||||
}
|
||||
r.args = props
|
||||
return r
|
||||
return (
|
||||
<AnastasisProvider
|
||||
value={{
|
||||
currentReducerState,
|
||||
currentError: undefined,
|
||||
back: async () => {
|
||||
null;
|
||||
},
|
||||
dismissError: async () => {
|
||||
null;
|
||||
},
|
||||
reset: () => {
|
||||
null;
|
||||
},
|
||||
runTransaction: async () => {
|
||||
null;
|
||||
},
|
||||
startBackup: () => {
|
||||
null;
|
||||
},
|
||||
startRecover: () => {
|
||||
null;
|
||||
},
|
||||
transition: async () => {
|
||||
null;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Component {...args} />
|
||||
</AnastasisProvider>
|
||||
);
|
||||
};
|
||||
r.args = props;
|
||||
return r;
|
||||
}
|
||||
|
||||
const base = {
|
||||
continents: [
|
||||
{
|
||||
name: "Europe"
|
||||
name: "Europe",
|
||||
},
|
||||
{
|
||||
name: "India"
|
||||
name: "India",
|
||||
},
|
||||
{
|
||||
name: "Asia"
|
||||
name: "Asia",
|
||||
},
|
||||
{
|
||||
name: "North America"
|
||||
name: "North America",
|
||||
},
|
||||
{
|
||||
name: "Testcontinent"
|
||||
}
|
||||
name: "Testcontinent",
|
||||
},
|
||||
],
|
||||
countries: [
|
||||
{
|
||||
@ -47,33 +69,33 @@ const base = {
|
||||
name: "Testland",
|
||||
continent: "Testcontinent",
|
||||
continent_i18n: {
|
||||
de_DE: "Testkontinent"
|
||||
de_DE: "Testkontinent",
|
||||
},
|
||||
name_i18n: {
|
||||
de_DE: "Testlandt",
|
||||
de_CH: "Testlandi",
|
||||
fr_FR: "Testpais",
|
||||
en_UK: "Testland"
|
||||
en_UK: "Testland",
|
||||
},
|
||||
currency: "TESTKUDOS",
|
||||
call_code: "+00"
|
||||
call_code: "+00",
|
||||
},
|
||||
{
|
||||
code: "xy",
|
||||
name: "Demoland",
|
||||
continent: "Testcontinent",
|
||||
continent_i18n: {
|
||||
de_DE: "Testkontinent"
|
||||
de_DE: "Testkontinent",
|
||||
},
|
||||
name_i18n: {
|
||||
de_DE: "Demolandt",
|
||||
de_CH: "Demolandi",
|
||||
fr_FR: "Demopais",
|
||||
en_UK: "Demoland"
|
||||
en_UK: "Demoland",
|
||||
},
|
||||
currency: "KUDOS",
|
||||
call_code: "+01"
|
||||
}
|
||||
call_code: "+01",
|
||||
},
|
||||
],
|
||||
authentication_providers: {
|
||||
"http://localhost:8086/": {
|
||||
@ -85,18 +107,20 @@ const base = {
|
||||
methods: [
|
||||
{
|
||||
type: "question",
|
||||
usage_fee: "COL:0"
|
||||
}, {
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
usage_fee: "COL:0"
|
||||
}, {
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
{
|
||||
type: "email",
|
||||
usage_fee: "COL:0"
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
],
|
||||
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
|
||||
storage_limit_in_megabytes: 16,
|
||||
truth_upload_fee: "COL:0"
|
||||
truth_upload_fee: "COL:0",
|
||||
},
|
||||
"https://kudos.demo.anastasis.lu/": {
|
||||
http_status: 200,
|
||||
@ -107,15 +131,16 @@ const base = {
|
||||
methods: [
|
||||
{
|
||||
type: "question",
|
||||
usage_fee: "COL:0"
|
||||
}, {
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
{
|
||||
type: "email",
|
||||
usage_fee: "COL:0"
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
],
|
||||
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
|
||||
storage_limit_in_megabytes: 16,
|
||||
truth_upload_fee: "COL:0"
|
||||
truth_upload_fee: "COL:0",
|
||||
},
|
||||
"https://anastasis.demo.taler.net/": {
|
||||
http_status: 200,
|
||||
@ -126,43 +151,45 @@ const base = {
|
||||
methods: [
|
||||
{
|
||||
type: "question",
|
||||
usage_fee: "COL:0"
|
||||
}, {
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
{
|
||||
type: "sms",
|
||||
usage_fee: "COL:0"
|
||||
}, {
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
{
|
||||
type: "totp",
|
||||
usage_fee: "COL:0"
|
||||
usage_fee: "COL:0",
|
||||
},
|
||||
],
|
||||
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
|
||||
storage_limit_in_megabytes: 16,
|
||||
truth_upload_fee: "COL:0"
|
||||
truth_upload_fee: "COL:0",
|
||||
},
|
||||
|
||||
"http://localhost:8087/": {
|
||||
code: 8414,
|
||||
hint: "request to provider failed"
|
||||
hint: "request to provider failed",
|
||||
},
|
||||
"http://localhost:8088/": {
|
||||
code: 8414,
|
||||
hint: "request to provider failed"
|
||||
hint: "request to provider failed",
|
||||
},
|
||||
"http://localhost:8089/": {
|
||||
code: 8414,
|
||||
hint: "request to provider failed"
|
||||
}
|
||||
hint: "request to provider failed",
|
||||
},
|
||||
},
|
||||
// expiration: {
|
||||
// d_ms: 1792525051855 // check t_ms
|
||||
// },
|
||||
} as Partial<ReducerState>
|
||||
} as Partial<ReducerState>;
|
||||
|
||||
export const reducerStatesExample = {
|
||||
initial: undefined,
|
||||
recoverySelectCountry: {
|
||||
...base,
|
||||
recovery_state: RecoveryStates.CountrySelecting
|
||||
recovery_state: RecoveryStates.CountrySelecting,
|
||||
} as ReducerState,
|
||||
recoverySelectContinent: {
|
||||
...base,
|
||||
@ -190,11 +217,11 @@ export const reducerStatesExample = {
|
||||
} as ReducerState,
|
||||
recoveryAttributeEditing: {
|
||||
...base,
|
||||
recovery_state: RecoveryStates.UserAttributesCollecting
|
||||
recovery_state: RecoveryStates.UserAttributesCollecting,
|
||||
} as ReducerState,
|
||||
backupSelectCountry: {
|
||||
...base,
|
||||
backup_state: BackupStates.CountrySelecting
|
||||
backup_state: BackupStates.CountrySelecting,
|
||||
} as ReducerState,
|
||||
backupSelectContinent: {
|
||||
...base,
|
||||
@ -218,15 +245,14 @@ export const reducerStatesExample = {
|
||||
} as ReducerState,
|
||||
authEditing: {
|
||||
...base,
|
||||
backup_state: BackupStates.AuthenticationsEditing
|
||||
backup_state: BackupStates.AuthenticationsEditing,
|
||||
} as ReducerState,
|
||||
backupAttributeEditing: {
|
||||
...base,
|
||||
backup_state: BackupStates.UserAttributesCollecting
|
||||
backup_state: BackupStates.UserAttributesCollecting,
|
||||
} as ReducerState,
|
||||
truthsPaying: {
|
||||
...base,
|
||||
backup_state: BackupStates.TruthsPaying
|
||||
backup_state: BackupStates.TruthsPaying,
|
||||
} as ReducerState,
|
||||
|
||||
}
|
||||
};
|
||||
|
@ -7,10 +7,12 @@ importers:
|
||||
'@linaria/esbuild': ^3.0.0-beta.13
|
||||
'@linaria/shaker': ^3.0.0-beta.13
|
||||
esbuild: ^0.12.29
|
||||
prettier: ^2.2.1
|
||||
devDependencies:
|
||||
'@linaria/esbuild': 3.0.0-beta.13
|
||||
'@linaria/shaker': 3.0.0-beta.13
|
||||
esbuild: 0.12.29
|
||||
prettier: 2.2.1
|
||||
|
||||
packages/anastasis-core:
|
||||
specifiers:
|
||||
@ -62,6 +64,7 @@ importers:
|
||||
'@typescript-eslint/eslint-plugin': ^5.3.0
|
||||
'@typescript-eslint/parser': ^5.3.0
|
||||
anastasis-core: workspace:^0.0.1
|
||||
base64-inline-loader: 1.1.1
|
||||
bulma: ^0.9.3
|
||||
bulma-checkbox: ^1.1.1
|
||||
bulma-radio: ^1.1.1
|
||||
@ -86,6 +89,7 @@ importers:
|
||||
dependencies:
|
||||
'@gnu-taler/taler-util': link:../taler-util
|
||||
anastasis-core: link:../anastasis-core
|
||||
base64-inline-loader: 1.1.1
|
||||
date-fns: 2.25.0
|
||||
jed: 1.1.1
|
||||
preact: 10.5.15
|
||||
@ -10800,7 +10804,6 @@ packages:
|
||||
ajv: ^6.9.1
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
dev: true
|
||||
|
||||
/ajv/6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
@ -10809,7 +10812,6 @@ packages:
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
dev: true
|
||||
|
||||
/ajv/7.0.3:
|
||||
resolution: {integrity: sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==}
|
||||
@ -11871,6 +11873,17 @@ packages:
|
||||
pascalcase: 0.1.1
|
||||
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:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: true
|
||||
@ -11907,7 +11920,6 @@ packages:
|
||||
|
||||
/big.js/5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
dev: true
|
||||
|
||||
/binary-extensions/1.13.1:
|
||||
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
|
||||
@ -13794,7 +13806,7 @@ packages:
|
||||
dependencies:
|
||||
globby: 11.0.4
|
||||
graceful-fs: 4.2.8
|
||||
is-glob: 4.0.1
|
||||
is-glob: 4.0.3
|
||||
is-path-cwd: 2.2.0
|
||||
is-path-inside: 3.0.3
|
||||
p-map: 4.0.0
|
||||
@ -14177,7 +14189,6 @@ packages:
|
||||
/emojis-list/3.0.0:
|
||||
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/emotion-theming/10.0.27_5f216699bc8c1f24088b3bf77b7cbbdf:
|
||||
resolution: {integrity: sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==}
|
||||
@ -15174,7 +15185,6 @@ packages:
|
||||
|
||||
/fast-deep-equal/3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
dev: true
|
||||
|
||||
/fast-diff/1.2.0:
|
||||
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
|
||||
@ -15217,7 +15227,6 @@ packages:
|
||||
|
||||
/fast-json-stable-stringify/2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
dev: true
|
||||
|
||||
/fast-levenshtein/2.0.6:
|
||||
resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=}
|
||||
@ -15296,6 +15305,16 @@ packages:
|
||||
flat-cache: 3.0.4
|
||||
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:
|
||||
resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@ -18694,7 +18713,6 @@ packages:
|
||||
|
||||
/json-schema-traverse/0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
dev: true
|
||||
|
||||
/json-schema-traverse/1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
@ -18730,7 +18748,6 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
minimist: 1.2.5
|
||||
dev: true
|
||||
|
||||
/json5/2.1.3:
|
||||
resolution: {integrity: sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==}
|
||||
@ -18970,7 +18987,6 @@ packages:
|
||||
big.js: 5.2.2
|
||||
emojis-list: 3.0.0
|
||||
json5: 1.0.1
|
||||
dev: true
|
||||
|
||||
/loader-utils/2.0.0:
|
||||
resolution: {integrity: sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==}
|
||||
@ -19420,14 +19436,12 @@ packages:
|
||||
/mime-db/1.50.0:
|
||||
resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types/2.1.33:
|
||||
resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.50.0
|
||||
dev: true
|
||||
|
||||
/mime/1.6.0:
|
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
@ -19501,7 +19515,6 @@ packages:
|
||||
|
||||
/minimist/1.2.5:
|
||||
resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==}
|
||||
dev: true
|
||||
|
||||
/minipass-collect/1.0.2:
|
||||
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
||||
@ -22013,7 +22026,6 @@ packages:
|
||||
/punycode/2.1.1:
|
||||
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/pupa/2.1.1:
|
||||
resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==}
|
||||
@ -23312,6 +23324,14 @@ packages:
|
||||
object-assign: 4.1.1
|
||||
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:
|
||||
resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -25212,7 +25232,6 @@ packages:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
dependencies:
|
||||
punycode: 2.1.1
|
||||
dev: true
|
||||
|
||||
/urix/0.1.0:
|
||||
resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=}
|
||||
|
Loading…
Reference in New Issue
Block a user