more styling
added placeholders for inputs import declaration for png next button now has tooltip providing info about whats missing a lot more of examples for UI testing added qr dependency for totp rendering added email and field input types added all auth method setup screens added modal when there is not auth provider merge continent and country into location section others improvements as well...
@ -47,7 +47,7 @@ export interface ReducerStateBackup {
|
|||||||
code?: undefined;
|
code?: undefined;
|
||||||
currencies?: string[];
|
currencies?: string[];
|
||||||
continents?: ContinentInfo[];
|
continents?: ContinentInfo[];
|
||||||
countries?: any;
|
countries?: CountryInfo[];
|
||||||
identity_attributes?: { [n: string]: string };
|
identity_attributes?: { [n: string]: string };
|
||||||
authentication_providers?: { [url: string]: AuthenticationProviderStatus };
|
authentication_providers?: { [url: string]: AuthenticationProviderStatus };
|
||||||
authentication_methods?: AuthMethod[];
|
authentication_methods?: AuthMethod[];
|
||||||
@ -129,8 +129,8 @@ export interface ReducerStateRecovery {
|
|||||||
|
|
||||||
identity_attributes?: { [n: string]: string };
|
identity_attributes?: { [n: string]: string };
|
||||||
|
|
||||||
continents?: any;
|
continents?: ContinentInfo[];
|
||||||
countries?: any;
|
countries?: CountryInfo[];
|
||||||
|
|
||||||
selected_continent?: string;
|
selected_continent?: string;
|
||||||
selected_country?: string;
|
selected_country?: string;
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
"jed": "1.1.1",
|
"jed": "1.1.1",
|
||||||
"preact": "^10.3.1",
|
"preact": "^10.3.1",
|
||||||
"preact-render-to-string": "^5.1.4",
|
"preact-render-to-string": "^5.1.4",
|
||||||
"preact-router": "^3.2.1"
|
"preact-router": "^3.2.1",
|
||||||
|
"qrcode-generator": "^1.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@creativebulma/bulma-tooltip": "^1.2.0",
|
"@creativebulma/bulma-tooltip": "^1.2.0",
|
||||||
|
BIN
packages/anastasis-webui/src/assets/empty.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packages/anastasis-webui/src/assets/example/id1.jpg
Normal file
After Width: | Height: | Size: 101 KiB |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0l-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg>
|
After Width: | Height: | Size: 274 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 15h2v2h-2zM17 11h2v2h-2zM17 7h2v2h-2zM13.74 7l1.26.84V7z"/><path d="M10 3v1.51l2 1.33V5h9v14h-4v2h6V3z"/><path d="M8.17 5.7L15 10.25V21H1V10.48L8.17 5.7zM10 19h3v-7.84L8.17 8.09 3 11.38V19h3v-6h4v6z"/></svg>
|
After Width: | Height: | Size: 359 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 23.59v-3.6c-5.01-.26-9-4.42-9-9.49C2 5.26 6.26 1 11.5 1S21 5.26 21 10.5c0 4.95-3.44 9.93-8.57 12.4l-1.43.69zM11.5 3C7.36 3 4 6.36 4 10.5S7.36 18 11.5 18H13v2.3c3.64-2.3 6-6.08 6-9.8C19 6.36 15.64 3 11.5 3zm-1 11.5h2v2h-2zm2-1.5h-2c0-3.25 3-3 3-5 0-1.1-.9-2-2-2s-2 .9-2 2h-2c0-2.21 1.79-4 4-4s4 1.79 4 4c0 2.5-3 2.75-3 5z"/></svg>
|
After Width: | Height: | Size: 483 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-1.99.9-1.99 2v18c0 1.1.89 2 1.99 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"/></svg>
|
After Width: | Height: | Size: 272 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M18,10.48V6c0-1.1-0.9-2-2-2H4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-4.48l4,3.98v-11L18,10.48z M16,9.69V18H4V6h12V9.69z"/><circle cx="10" cy="10" r="2"/><path d="M14,15.43c0-0.81-0.48-1.53-1.22-1.85C11.93,13.21,10.99,13,10,13c-0.99,0-1.93,0.21-2.78,0.58C6.48,13.9,6,14.62,6,15.43 V16h8V15.43z"/></g></g></svg>
|
After Width: | Height: | Size: 525 B |
35
packages/anastasis-webui/src/components/QR.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
|
import qrcode from "qrcode-generator";
|
||||||
|
|
||||||
|
export function QR({ text }: { text: string }): VNode {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const qr = qrcode(0, 'L');
|
||||||
|
qr.addData(text);
|
||||||
|
qr.make();
|
||||||
|
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>;
|
||||||
|
}
|
@ -25,7 +25,7 @@ export function DateInput(props: DateInputProps): VNode {
|
|||||||
setOpened2(v)
|
setOpened2(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = props.bind[0];
|
const value = props.bind[0] || "";
|
||||||
const [dirty, setDirty] = useState(false)
|
const [dirty, setDirty] = useState(false)
|
||||||
const showError = dirty && props.error
|
const showError = dirty && props.error
|
||||||
|
|
||||||
@ -40,7 +40,8 @@ export function DateInput(props: DateInputProps): VNode {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class={showError ? 'input is-danger' : 'input'}
|
class={showError ? 'input is-danger' : 'input'}
|
||||||
onClick={() => { setOpened(true) }}
|
readonly
|
||||||
|
onFocus={() => { setOpened(true) } }
|
||||||
value={value}
|
value={value}
|
||||||
ref={inputRef} />
|
ref={inputRef} />
|
||||||
|
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
export interface TextInputProps {
|
||||||
|
label: string;
|
||||||
|
grabFocus?: boolean;
|
||||||
|
error?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
bind: [string, (x: string) => void];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmailInput(props: TextInputProps): VNode {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (props.grabFocus) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [props.grabFocus]);
|
||||||
|
const value = props.bind[0];
|
||||||
|
const [dirty, setDirty] = useState(false)
|
||||||
|
const showError = dirty && props.error
|
||||||
|
return (<div class="field">
|
||||||
|
<label class="label">
|
||||||
|
{props.label}
|
||||||
|
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||||
|
<i class="mdi mdi-information" />
|
||||||
|
</span>}
|
||||||
|
</label>
|
||||||
|
<div class="control has-icons-right">
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
required
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
type="email"
|
||||||
|
class={showError ? 'input is-danger' : 'input'}
|
||||||
|
onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
||||||
|
ref={inputRef}
|
||||||
|
style={{ display: "block" }} />
|
||||||
|
</div>
|
||||||
|
{showError && <p class="help is-danger">{props.error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
81
packages/anastasis-webui/src/components/fields/FileInput.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { TextInputProps } from "./TextInput";
|
||||||
|
|
||||||
|
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
|
||||||
|
|
||||||
|
export function FileInput(props: TextInputProps): VNode {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (props.grabFocus) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [props.grabFocus]);
|
||||||
|
|
||||||
|
const value = props.bind[0];
|
||||||
|
// const [dirty, setDirty] = useState(false)
|
||||||
|
const 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>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import emptyImage from "../../assets/empty.png";
|
||||||
|
import { TextInputProps } from "./TextInput";
|
||||||
|
|
||||||
|
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
|
||||||
|
|
||||||
|
export function ImageInput(props: TextInputProps): VNode {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (props.grabFocus) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [props.grabFocus]);
|
||||||
|
|
||||||
|
const value = props.bind[0];
|
||||||
|
// const [dirty, setDirty] = useState(false)
|
||||||
|
const 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>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ export interface TextInputProps {
|
|||||||
label: string;
|
label: string;
|
||||||
grabFocus?: boolean;
|
grabFocus?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
placeholder?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
bind: [string, (x: string) => void];
|
bind: [string, (x: string) => void];
|
||||||
}
|
}
|
||||||
@ -30,6 +31,7 @@ export function NumberInput(props: TextInputProps): VNode {
|
|||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
type="number"
|
type="number"
|
||||||
|
placeholder={props.placeholder}
|
||||||
class={showError ? 'input is-danger' : 'input'}
|
class={showError ? 'input is-danger' : 'input'}
|
||||||
onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
@ -5,6 +5,7 @@ export interface TextInputProps {
|
|||||||
label: string;
|
label: string;
|
||||||
grabFocus?: boolean;
|
grabFocus?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
placeholder?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
bind: [string, (x: string) => void];
|
bind: [string, (x: string) => void];
|
||||||
}
|
}
|
||||||
@ -29,6 +30,7 @@ export function TextInput(props: TextInputProps): VNode {
|
|||||||
<div class="control has-icons-right">
|
<div class="control has-icons-right">
|
||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
|
placeholder={props.placeholder}
|
||||||
class={showError ? 'input is-danger' : 'input'}
|
class={showError ? 'input is-danger' : 'input'}
|
||||||
onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
@ -33,6 +33,7 @@ interface Props {
|
|||||||
export function Sidebar({ mobile }: Props): VNode {
|
export function Sidebar({ mobile }: Props): VNode {
|
||||||
// const config = useConfigContext();
|
// const config = useConfigContext();
|
||||||
const config = { version: 'none' }
|
const config = { version: 'none' }
|
||||||
|
// FIXME: add replacement for __VERSION__ with the current version
|
||||||
const process = { env: { __VERSION__: '0.0.0' } }
|
const process = { env: { __VERSION__: '0.0.0' } }
|
||||||
const reducer = useAnastasisContext()!
|
const reducer = useAnastasisContext()!
|
||||||
|
|
||||||
@ -105,12 +106,12 @@ export function Sidebar({ mobile }: Props): VNode {
|
|||||||
<span class="menu-item-label"><Translate>Backup completed</Translate></span>
|
<span class="menu-item-label"><Translate>Backup completed</Translate></span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
|
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
|
|
||||||
<span class="menu-item-label"><Translate>Truth Paying</Translate></span>
|
<span class="menu-item-label"><Translate>Truth Paying</Translate></span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li> */}
|
||||||
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
|
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
|
||||||
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
|
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
|
||||||
reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
|
reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
|
||||||
|
@ -10,6 +10,10 @@ declare module '*.jpeg' {
|
|||||||
const content: any;
|
const content: any;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
declare module 'jed' {
|
declare module 'jed' {
|
||||||
const x: any;
|
const x: any;
|
||||||
export = x;
|
export = x;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
import { UserAttributeSpec, validators } from "anastasis-core";
|
import { UserAttributeSpec, validators } from "anastasis-core";
|
||||||
import { h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
import { AnastasisClientFrame, withProcessLabel } from "./index";
|
import { AnastasisClientFrame, withProcessLabel } from "./index";
|
||||||
@ -20,53 +20,38 @@ export function AttributeEntryScreen(): VNode {
|
|||||||
if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
|
if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
|
||||||
return <div>invalid state</div>
|
return <div>invalid state</div>
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
return (
|
||||||
|
<AttributeEntryField
|
||||||
|
key={i}
|
||||||
|
isFirst={i == 0}
|
||||||
|
setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
|
||||||
|
spec={spec}
|
||||||
|
errorMessage={error}
|
||||||
|
value={value} />
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame
|
<AnastasisClientFrame
|
||||||
title={withProcessLabel(reducer, "Who are you?")}
|
title={withProcessLabel(reducer, "Who are you?")}
|
||||||
|
hideNext={hasErrors ? "Complete the form." : undefined}
|
||||||
onNext={() => reducer.transition("enter_user_attributes", {
|
onNext={() => reducer.transition("enter_user_attributes", {
|
||||||
identity_attributes: attrs,
|
identity_attributes: attrs,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
|
{fieldList}
|
||||||
{reducer.currentReducerState.required_attributes?.map((x, i: number) => {
|
|
||||||
const value = attrs[x.name]
|
|
||||||
function checkIfValid(): string | undefined {
|
|
||||||
const pattern = x['validation-regex']
|
|
||||||
if (pattern) {
|
|
||||||
const re = new RegExp(pattern)
|
|
||||||
if (!re.test(value)) return 'The value is invalid'
|
|
||||||
}
|
|
||||||
const logic = x['validation-logic']
|
|
||||||
if (logic) {
|
|
||||||
const func = (validators as any)[logic];
|
|
||||||
if (func && typeof func === 'function' && !func(value)) return 'Please check the value'
|
|
||||||
}
|
|
||||||
const optional = x.optional
|
|
||||||
console.log('optiona', optional)
|
|
||||||
if (!optional && !value) {
|
|
||||||
return 'This value is required'
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AttributeEntryField
|
|
||||||
key={i}
|
|
||||||
isFirst={i == 0}
|
|
||||||
setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
|
|
||||||
spec={x}
|
|
||||||
isValid={checkIfValid}
|
|
||||||
value={value} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half" >
|
<div class="column is-half" >
|
||||||
<p>This personal information will help to locate your secret in the first place</p>
|
<p>This personal information will help to locate your secret.</p>
|
||||||
<h1><b>This stay private</b></h1>
|
<h1><b>This stay private</b></h1>
|
||||||
<p>The information you have entered here:
|
<p>The information you have entered here:
|
||||||
</p>
|
</p>
|
||||||
@ -92,14 +77,13 @@ interface AttributeEntryFieldProps {
|
|||||||
value: string;
|
value: string;
|
||||||
setValue: (newValue: string) => void;
|
setValue: (newValue: string) => void;
|
||||||
spec: UserAttributeSpec;
|
spec: UserAttributeSpec;
|
||||||
isValid: () => string | undefined;
|
errorMessage: string | undefined;
|
||||||
}
|
}
|
||||||
const possibleBirthdayYear: Array<number> = []
|
const possibleBirthdayYear: Array<number> = []
|
||||||
for (let i = 0; i < 100; i++ ) {
|
for (let i = 0; i < 100; i++) {
|
||||||
possibleBirthdayYear.push(2020 - i)
|
possibleBirthdayYear.push(2020 - i)
|
||||||
}
|
}
|
||||||
function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
||||||
const errorMessage = props.isValid()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -108,14 +92,14 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
|||||||
grabFocus={props.isFirst}
|
grabFocus={props.isFirst}
|
||||||
label={props.spec.label}
|
label={props.spec.label}
|
||||||
years={possibleBirthdayYear}
|
years={possibleBirthdayYear}
|
||||||
error={errorMessage}
|
error={props.errorMessage}
|
||||||
bind={[props.value, props.setValue]}
|
bind={[props.value, props.setValue]}
|
||||||
/>}
|
/>}
|
||||||
{props.spec.type === 'number' &&
|
{props.spec.type === 'number' &&
|
||||||
<NumberInput
|
<NumberInput
|
||||||
grabFocus={props.isFirst}
|
grabFocus={props.isFirst}
|
||||||
label={props.spec.label}
|
label={props.spec.label}
|
||||||
error={errorMessage}
|
error={props.errorMessage}
|
||||||
bind={[props.value, props.setValue]}
|
bind={[props.value, props.setValue]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -123,7 +107,7 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
|||||||
<TextInput
|
<TextInput
|
||||||
grabFocus={props.isFirst}
|
grabFocus={props.isFirst}
|
||||||
label={props.spec.label}
|
label={props.spec.label}
|
||||||
error={errorMessage}
|
error={props.errorMessage}
|
||||||
bind={[props.value, props.setValue]}
|
bind={[props.value, props.setValue]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -136,3 +120,21 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 logic = spec['validation-logic']
|
||||||
|
if (logic) {
|
||||||
|
const func = (validators as any)[logic];
|
||||||
|
if (func && typeof func === 'function' && !func(value)) return 'Please check the value'
|
||||||
|
}
|
||||||
|
const optional = spec.optional
|
||||||
|
if (!optional && !value) {
|
||||||
|
return 'This value is required'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
import {
|
|
||||||
encodeCrock,
|
|
||||||
stringToBytes
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { h, VNode } from "preact";
|
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
|
|
||||||
import { AnastasisClientFrame } from "./index";
|
|
||||||
import { TextInput } from "../../components/fields/TextInput";
|
|
||||||
|
|
||||||
export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<TextInput
|
|
||||||
label="Email address"
|
|
||||||
grabFocus
|
|
||||||
bind={[email, setEmail]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
|
||||||
<button
|
|
||||||
onClick={() => props.addAuthMethod({
|
|
||||||
authentication_method: {
|
|
||||||
type: "email",
|
|
||||||
instructions: `Email to ${email}`,
|
|
||||||
challenge: encodeCrock(stringToBytes(email)),
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</AnastasisClientFrame>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
import {
|
|
||||||
canonicalJson, encodeCrock,
|
|
||||||
stringToBytes
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { h, VNode } from "preact";
|
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
|
|
||||||
import { TextInput } from "../../components/fields/TextInput";
|
|
||||||
|
|
||||||
export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {
|
|
||||||
const [fullName, setFullName] = useState("");
|
|
||||||
const [street, setStreet] = useState("");
|
|
||||||
const [city, setCity] = useState("");
|
|
||||||
const [postcode, setPostcode] = useState("");
|
|
||||||
const [country, setCountry] = useState("");
|
|
||||||
|
|
||||||
const addPostAuth = () => {
|
|
||||||
const challengeJson = {
|
|
||||||
full_name: fullName,
|
|
||||||
street,
|
|
||||||
city,
|
|
||||||
postcode,
|
|
||||||
country,
|
|
||||||
};
|
|
||||||
props.addAuthMethod({
|
|
||||||
authentication_method: {
|
|
||||||
type: "email",
|
|
||||||
instructions: `Letter to address in postal code ${postcode}`,
|
|
||||||
challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="home">
|
|
||||||
<h1>Add {props.method} authentication</h1>
|
|
||||||
<div>
|
|
||||||
<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.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<TextInput
|
|
||||||
grabFocus
|
|
||||||
label="Full Name"
|
|
||||||
bind={[fullName, setFullName]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInput label="Street" bind={[street, setStreet]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInput label="City" bind={[city, setCity]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInput label="Postal Code" bind={[postcode, setPostcode]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInput label="Country" bind={[country, setCountry]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
|
||||||
<button onClick={() => addPostAuth()}>Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
import {
|
|
||||||
encodeCrock,
|
|
||||||
stringToBytes
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { h, VNode } from "preact";
|
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
|
|
||||||
import { AnastasisClientFrame } from "./index";
|
|
||||||
import { TextInput } from "../../components/fields/TextInput";
|
|
||||||
|
|
||||||
export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
|
|
||||||
const [questionText, setQuestionText] = useState("");
|
|
||||||
const [answerText, setAnswerText] = useState("");
|
|
||||||
const addQuestionAuth = (): void => props.addAuthMethod({
|
|
||||||
authentication_method: {
|
|
||||||
type: "question",
|
|
||||||
instructions: questionText,
|
|
||||||
challenge: encodeCrock(stringToBytes(answerText)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<AnastasisClientFrame hideNav title="Add Security Question">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
For security question authentication, you need to provide a question
|
|
||||||
and its answer. When recovering your secret, you will be shown the
|
|
||||||
question and you will need to type the answer exactly as you typed it
|
|
||||||
here.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<TextInput
|
|
||||||
label="Security question"
|
|
||||||
grabFocus
|
|
||||||
bind={[questionText, setQuestionText]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInput label="Answer" bind={[answerText, setAnswerText]} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
|
||||||
<button onClick={() => addQuestionAuth()}>Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AnastasisClientFrame>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
import {
|
|
||||||
encodeCrock,
|
|
||||||
stringToBytes
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { h, VNode } from "preact";
|
|
||||||
import { useState, useRef, useLayoutEffect } from "preact/hooks";
|
|
||||||
import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
|
|
||||||
import { AnastasisClientFrame } from "./index";
|
|
||||||
|
|
||||||
export function AuthMethodSmsSetup(props: AuthMethodSetupProps): VNode {
|
|
||||||
const [mobileNumber, setMobileNumber] = useState("");
|
|
||||||
const addSmsAuth = (): void => {
|
|
||||||
props.addAuthMethod({
|
|
||||||
authentication_method: {
|
|
||||||
type: "sms",
|
|
||||||
instructions: `SMS to ${mobileNumber}`,
|
|
||||||
challenge: encodeCrock(stringToBytes(mobileNumber)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<AnastasisClientFrame hideNav title="Add SMS authentication">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
For SMS authentication, you need to provide a mobile number. When
|
|
||||||
recovering your secret, you will be asked to enter the code you
|
|
||||||
receive via SMS.
|
|
||||||
</p>
|
|
||||||
<label>
|
|
||||||
Mobile number:{" "}
|
|
||||||
<input
|
|
||||||
value={mobileNumber}
|
|
||||||
ref={inputRef}
|
|
||||||
style={{ display: "block" }}
|
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setMobileNumber((e.target as any).value)}
|
|
||||||
type="text" />
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
|
||||||
<button onClick={() => addSmsAuth()}>Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AnastasisClientFrame>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
(C) 2021 Taler Systems S.A.
|
(C) 2021 Taler Systems S.A.
|
||||||
@ -19,6 +20,7 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ReducerState } from 'anastasis-core';
|
||||||
import { createExample, reducerStatesExample } from '../../utils';
|
import { createExample, reducerStatesExample } from '../../utils';
|
||||||
import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
|
import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
|
||||||
|
|
||||||
@ -36,3 +38,56 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Example = createExample(TestedComponent, reducerStatesExample.authEditing);
|
export const Example = createExample(TestedComponent, reducerStatesExample.authEditing);
|
||||||
|
export const OneAuthMethodConfigured = createExample(TestedComponent, {
|
||||||
|
...reducerStatesExample.authEditing,
|
||||||
|
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',
|
||||||
|
}]
|
||||||
|
} as ReducerState);
|
||||||
|
|
||||||
|
export const NoAuthMethodProvided = createExample(TestedComponent, {
|
||||||
|
...reducerStatesExample.authEditing,
|
||||||
|
authentication_providers: {},
|
||||||
|
authentication_methods: []
|
||||||
|
} as ReducerState);
|
||||||
|
|
||||||
|
// type: string;
|
||||||
|
// instructions: string;
|
||||||
|
// challenge: string;
|
||||||
|
// mime_type?: string;
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
import { AuthMethod, ReducerStateBackup } from "anastasis-core";
|
import { AuthMethod } from "anastasis-core";
|
||||||
import { h, VNode } from "preact";
|
import { ComponentChildren, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
|
import { authMethods, KnownAuthMethods } from "./authMethodSetup";
|
||||||
import { AuthMethodEmailSetup } from "./AuthMethodEmailSetup";
|
|
||||||
import { AuthMethodPostSetup } from "./AuthMethodPostSetup";
|
|
||||||
import { AuthMethodQuestionSetup } from "./AuthMethodQuestionSetup";
|
|
||||||
import { AuthMethodSmsSetup } from "./AuthMethodSmsSetup";
|
|
||||||
import { AnastasisClientFrame } from "./index";
|
import { AnastasisClientFrame } from "./index";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
|
||||||
|
|
||||||
export function AuthenticationEditorScreen(): VNode {
|
export function AuthenticationEditorScreen(): VNode {
|
||||||
const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
|
const [noProvidersAck, setNoProvidersAck] = useState(false)
|
||||||
undefined
|
const [selectedMethod, setSelectedMethod] = useState<KnownAuthMethods | undefined>(undefined);
|
||||||
);
|
|
||||||
const reducer = useAnastasisContext()
|
const reducer = useAnastasisContext()
|
||||||
if (!reducer) {
|
if (!reducer) {
|
||||||
return <div>no reducer in context</div>
|
return <div>no reducer in context</div>
|
||||||
@ -21,7 +21,29 @@ export function AuthenticationEditorScreen(): VNode {
|
|||||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||||
return <div>invalid state</div>
|
return <div>invalid state</div>
|
||||||
}
|
}
|
||||||
|
const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
|
||||||
|
const haveMethodsConfigured = configuredAuthMethods.length > 0;
|
||||||
|
|
||||||
|
function removeByIndex(index: number): void {
|
||||||
|
if (reducer) reducer.transition("delete_authentication", {
|
||||||
|
authentication_method: index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const camByType: { [s: string]: AuthMethodWithRemove[] } = {}
|
||||||
|
for (let index = 0; index < configuredAuthMethods.length; index++) {
|
||||||
|
const cam = {
|
||||||
|
...configuredAuthMethods[index],
|
||||||
|
remove: () => removeByIndex(index)
|
||||||
|
}
|
||||||
|
const prevValue = camByType[cam.type] || []
|
||||||
|
prevValue.push(cam)
|
||||||
|
camByType[cam.type] = prevValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const providers = reducer.currentReducerState.authentication_providers!;
|
const providers = reducer.currentReducerState.authentication_providers!;
|
||||||
|
|
||||||
const authAvailableSet = new Set<string>();
|
const authAvailableSet = new Set<string>();
|
||||||
for (const provKey of Object.keys(providers)) {
|
for (const provKey of Object.keys(providers)) {
|
||||||
const p = providers[provKey];
|
const p = providers[provKey];
|
||||||
@ -31,79 +53,106 @@ export function AuthenticationEditorScreen(): VNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMethod) {
|
if (selectedMethod) {
|
||||||
const cancel = (): void => setSelectedMethod(undefined);
|
const cancel = (): void => setSelectedMethod(undefined);
|
||||||
const addMethod = (args: any): void => {
|
const addMethod = (args: any): void => {
|
||||||
reducer.transition("add_authentication", args);
|
reducer.transition("add_authentication", args);
|
||||||
setSelectedMethod(undefined);
|
setSelectedMethod(undefined);
|
||||||
};
|
};
|
||||||
const methodMap: Record<
|
|
||||||
string, (props: AuthMethodSetupProps) => h.JSX.Element
|
const AuthSetup = authMethods[selectedMethod].screen ?? AuthMethodNotImplemented;
|
||||||
> = {
|
|
||||||
sms: AuthMethodSmsSetup,
|
|
||||||
question: AuthMethodQuestionSetup,
|
|
||||||
email: AuthMethodEmailSetup,
|
|
||||||
post: AuthMethodPostSetup,
|
|
||||||
};
|
|
||||||
const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
|
|
||||||
return (
|
return (
|
||||||
<AuthSetup
|
<AuthSetup
|
||||||
cancel={cancel}
|
cancel={cancel}
|
||||||
|
configured={camByType[selectedMethod] || []}
|
||||||
addAuthMethod={addMethod}
|
addAuthMethod={addMethod}
|
||||||
method={selectedMethod} />
|
method={selectedMethod} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function MethodButton(props: { method: string; label: string }): VNode {
|
function MethodButton(props: { method: KnownAuthMethods }): VNode {
|
||||||
return (
|
return (
|
||||||
<button
|
<div class="block">
|
||||||
disabled={!authAvailableSet.has(props.method)}
|
<button
|
||||||
onClick={() => {
|
style={{ justifyContent: 'space-between' }}
|
||||||
setSelectedMethod(props.method);
|
class="button is-fullwidth"
|
||||||
if (reducer) reducer.dismissError();
|
onClick={() => {
|
||||||
}}
|
if (!authAvailableSet.has(props.method)) {
|
||||||
>
|
//open add sms dialog
|
||||||
{props.label}
|
} else {
|
||||||
</button>
|
setSelectedMethod(props.method);
|
||||||
|
}
|
||||||
|
if (reducer) reducer.dismissError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<span class="icon ">
|
||||||
|
{authMethods[props.method].icon}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{authMethods[props.method].label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!authAvailableSet.has(props.method) &&
|
||||||
|
<span class="icon has-text-danger" >
|
||||||
|
<i class="mdi mdi-exclamation-thick" />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{camByType[props.method] &&
|
||||||
|
<span class="tag is-info" >
|
||||||
|
{camByType[props.method].length}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
|
const errors = !haveMethodsConfigured ? "There is not enough authentication methods." : undefined;
|
||||||
const haveMethodsConfigured = configuredAuthMethods.length;
|
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame title="Backup: Configure Authentication Methods">
|
<AnastasisClientFrame title="Backup: Configure Authentication Methods" hideNext={errors}>
|
||||||
<div>
|
<div class="columns">
|
||||||
<MethodButton method="sms" label="SMS" />
|
<div class="column is-half">
|
||||||
<MethodButton method="email" label="Email" />
|
<div>
|
||||||
<MethodButton method="question" label="Question" />
|
{getKeys(authMethods).map(method => <MethodButton key={method} method={method} />)}
|
||||||
<MethodButton method="post" label="Physical Mail" />
|
</div>
|
||||||
<MethodButton method="totp" label="TOTP" />
|
{authAvailableSet.size === 0 && <ConfirmModal active={!noProvidersAck} onCancel={() => setNoProvidersAck(true)} description="No providers founds" label="Add a provider manually">
|
||||||
<MethodButton method="iban" label="IBAN" />
|
We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
|
||||||
</div>
|
To add a provider you must know the provider URL (e.g. https://provider.com)
|
||||||
<h2>Configured authentication methods</h2>
|
<p>
|
||||||
{haveMethodsConfigured ? (
|
<a>More about cloud providers</a>
|
||||||
configuredAuthMethods.map((x, i) => {
|
|
||||||
return (
|
|
||||||
<p key={i}>
|
|
||||||
{x.type} ({x.instructions}){" "}
|
|
||||||
<button
|
|
||||||
onClick={() => reducer.transition("delete_authentication", {
|
|
||||||
authentication_method: i,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
);
|
</ConfirmModal>}
|
||||||
})
|
|
||||||
) : (
|
{/* {haveMethodsConfigured && (
|
||||||
<p>No authentication methods configured yet.</p>
|
configuredAuthMethods.map((x, i) => {
|
||||||
)}
|
return (
|
||||||
|
<p key={i}>
|
||||||
|
{x.type} ({x.instructions}){" "}
|
||||||
|
<button class="button is-danger is-small"
|
||||||
|
onClick={() => reducer.transition("delete_authentication", {
|
||||||
|
authentication_method: i,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
When recovering your wallet, you will be asked to verify your identity via the methods you configure here.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthMethodWithRemove = AuthMethod & { remove: () => void }
|
||||||
export interface AuthMethodSetupProps {
|
export interface AuthMethodSetupProps {
|
||||||
method: string;
|
method: string;
|
||||||
addAuthMethod: (x: any) => void;
|
addAuthMethod: (x: any) => void;
|
||||||
|
configured: AuthMethodWithRemove[];
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +165,36 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthenticationEditorProps {
|
|
||||||
reducer: AnastasisReducerApi;
|
function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode {
|
||||||
backupState: ReducerStateBackup;
|
return <div class={active ? "modal is-active" : "modal"}>
|
||||||
|
<div class="modal-background " onClick={onCancel} />
|
||||||
|
<div class="modal-card" style={{ maxWidth: 700 }}>
|
||||||
|
<header class="modal-card-head">
|
||||||
|
{!description ? null : <p class="modal-card-title"><b>{description}</b></p>}
|
||||||
|
<button class="delete " aria-label="close" onClick={onCancel} />
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<button class="button" onClick={onCancel} >Dismiss</button>
|
||||||
|
<div class="buttons is-right" style={{ width: '100%' }}>
|
||||||
|
<button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} >{label}</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large " aria-label="close" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
active?: boolean;
|
||||||
|
description?: string;
|
||||||
|
onCancel?: () => void;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
label?: string;
|
||||||
|
children?: ComponentChildren;
|
||||||
|
danger?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
@ -37,7 +37,7 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Simple = createExample(TestedComponent, reducerStatesExample.backupFinished);
|
export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished);
|
||||||
|
|
||||||
export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
|
export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
|
||||||
secret_name: 'super_secret',
|
secret_name: 'super_secret',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { format } from "date-fns";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
import { AnastasisClientFrame } from "./index";
|
import { AnastasisClientFrame } from "./index";
|
||||||
@ -11,23 +12,30 @@ export function BackupFinishedScreen(): VNode {
|
|||||||
return <div>invalid state</div>
|
return <div>invalid state</div>
|
||||||
}
|
}
|
||||||
const details = reducer.currentReducerState.success_details
|
const details = reducer.currentReducerState.success_details
|
||||||
return (<AnastasisClientFrame hideNext title="Backup finished">
|
|
||||||
<p>
|
|
||||||
Your backup of secret "{reducer.currentReducerState.secret_name ?? "??"}" was
|
|
||||||
successful.
|
|
||||||
</p>
|
|
||||||
<p>The backup is stored by the following providers:</p>
|
|
||||||
|
|
||||||
{details && <ul>
|
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) => {
|
{Object.keys(details).map((x, i) => {
|
||||||
const sd = details[x];
|
const sd = details[x];
|
||||||
return (
|
return (
|
||||||
<li key={i}>
|
<div key={i} class="box">
|
||||||
{x} (Policy version {sd.policy_version})
|
{x}
|
||||||
</li>
|
<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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>}
|
</div>}
|
||||||
<button onClick={() => reducer.reset()}>Back to start</button>
|
|
||||||
</AnastasisClientFrame>);
|
</AnastasisClientFrame>);
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,9 @@ export function ChallengeOverviewScreen(): VNode {
|
|||||||
|
|
||||||
const atLeastThereIsOnePolicySolved = policiesWithInfo.find(p => p.isPolicySolved) !== undefined
|
const atLeastThereIsOnePolicySolved = policiesWithInfo.find(p => p.isPolicySolved) !== undefined
|
||||||
|
|
||||||
|
const errors = !atLeastThereIsOnePolicySolved ? "Solve one policy before proceeding" : undefined;
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame hideNext={!atLeastThereIsOnePolicySolved} title="Recovery: Solve challenges">
|
<AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
|
||||||
{!policies.length ? <p>
|
{!policies.length ? <p>
|
||||||
No policies found, try with another version of the secret
|
No policies found, try with another version of the secret
|
||||||
</p> : (policies.length === 1 ? <p>
|
</p> : (policies.length === 1 ? <p>
|
||||||
|
@ -13,7 +13,7 @@ export function ChallengePayingScreen(): VNode {
|
|||||||
const payments = ['']; //reducer.currentReducerState.payments ??
|
const payments = ['']; //reducer.currentReducerState.payments ??
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame
|
<AnastasisClientFrame
|
||||||
hideNext
|
hideNav
|
||||||
title="Recovery: Challenge Paying"
|
title="Recovery: Challenge Paying"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,20 +1,108 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import { BackupStates, ContinentInfo, RecoveryStates } from "anastasis-core";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
import { AnastasisClientFrame, withProcessLabel } from "./index";
|
import { AnastasisClientFrame, withProcessLabel } from "./index";
|
||||||
|
|
||||||
export function ContinentSelectionScreen(): VNode {
|
export function ContinentSelectionScreen(): VNode {
|
||||||
const reducer = useAnastasisContext()
|
const reducer = useAnastasisContext()
|
||||||
|
|
||||||
|
//FIXME: remove this when #7056 is fixed
|
||||||
|
const [countryCode, setCountryCode] = useState("")
|
||||||
|
|
||||||
if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) {
|
if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) {
|
||||||
return <div />
|
return <div />
|
||||||
}
|
}
|
||||||
const select = (continent: string) => (): void => reducer.transition("select_continent", { continent });
|
const selectContinent = (continent: string): void => {
|
||||||
|
reducer.transition("select_continent", { continent })
|
||||||
|
};
|
||||||
|
const selectCountry = (country: string): void => {
|
||||||
|
setCountryCode(country)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const continentList = reducer.currentReducerState.continents || [];
|
||||||
|
const countryList = reducer.currentReducerState.countries || [];
|
||||||
|
const theContinent = reducer.currentReducerState.selected_continent || ""
|
||||||
|
// const cc = reducer.currentReducerState.selected_country || "";
|
||||||
|
const theCountry = countryList.find(c => c.code === countryCode)
|
||||||
|
const selectCountryAction = () => {
|
||||||
|
//selection should be when the select box changes it value
|
||||||
|
if (!theCountry) return;
|
||||||
|
reducer.transition("select_country", {
|
||||||
|
country_code: countryCode,
|
||||||
|
currencies: [theCountry.currency],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
|
||||||
|
reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
|
||||||
|
|
||||||
|
const errors = !theCountry ? "Select a country" : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Continent")}>
|
<AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Select location")} onNext={selectCountryAction}>
|
||||||
{reducer.currentReducerState.continents.map((x: any) => (
|
<div class="columns">
|
||||||
<button class="button" onClick={select(x.name)} key={x.name}>
|
<div class="column is-half">
|
||||||
{x.name}
|
<div class="field">
|
||||||
</button>
|
<label class="label">Continent</label>
|
||||||
))}
|
<div class="control has-icons-left">
|
||||||
|
<div class="select " >
|
||||||
|
<select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} disabled={!step1}>
|
||||||
|
<option key="none" disabled selected value=""> Choose a continent </option>
|
||||||
|
{continentList.map(prov => (
|
||||||
|
<option key={prov.name} value={prov.name}>
|
||||||
|
{prov.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-earth" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!step1 && <span class="control">
|
||||||
|
<a class="button is-danger" onClick={() => reducer.back()}>
|
||||||
|
X
|
||||||
|
</a>
|
||||||
|
</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Country</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<div class="select" >
|
||||||
|
<select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}>
|
||||||
|
<option key="none" disabled selected value=""> Choose a country </option>
|
||||||
|
{countryList.map(prov => (
|
||||||
|
<option key={prov.name} value={prov.code}>
|
||||||
|
{prov.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-earth" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{theCountry && <div class="field">
|
||||||
|
<label class="label">Available currencies:</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input is-small" type="text" readonly value={theCountry.currency} />
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
<p>
|
||||||
|
A location will help to define a common information that will be use to locate your secret and a currency
|
||||||
|
for payments if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export function CountrySelectionScreen(): VNode {
|
|||||||
return (
|
return (
|
||||||
<AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} >
|
<AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} >
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
{reducer.currentReducerState.countries.map((x: any) => (
|
{reducer.currentReducerState.countries!.map((x: any) => (
|
||||||
<div key={x.name}>
|
<div key={x.name}>
|
||||||
<button class="button" onClick={() => sel(x)} >
|
<button class="button" onClick={() => sel(x)} >
|
||||||
{x.name} ({x.currency})
|
{x.name} ({x.currency})
|
||||||
|
@ -13,7 +13,7 @@ export function PoliciesPayingScreen(): VNode {
|
|||||||
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
|
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
|
<AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
|
||||||
<p>
|
<p>
|
||||||
Some of the providers require a payment to store the encrypted
|
Some of the providers require a payment to store the encrypted
|
||||||
recovery document.
|
recovery document.
|
||||||
|
@ -17,7 +17,7 @@ export function RecoveryFinishedScreen(): VNode {
|
|||||||
}
|
}
|
||||||
const encodedSecret = reducer.currentReducerState.core_secret?.value
|
const encodedSecret = reducer.currentReducerState.core_secret?.value
|
||||||
if (!encodedSecret) {
|
if (!encodedSecret) {
|
||||||
return <AnastasisClientFrame title="Recovery Problem" hideNext>
|
return <AnastasisClientFrame title="Recovery Problem" hideNav>
|
||||||
<p>
|
<p>
|
||||||
Secret not found
|
Secret not found
|
||||||
</p>
|
</p>
|
||||||
@ -25,7 +25,7 @@ export function RecoveryFinishedScreen(): VNode {
|
|||||||
}
|
}
|
||||||
const secret = bytesToString(decodeCrock(encodedSecret))
|
const secret = bytesToString(decodeCrock(encodedSecret))
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame title="Recovery Finished" hideNext>
|
<AnastasisClientFrame title="Recovery Finished" hideNav>
|
||||||
<p>
|
<p>
|
||||||
Secret: {secret}
|
Secret: {secret}
|
||||||
</p>
|
</p>
|
||||||
|
@ -43,11 +43,11 @@ export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
|
|||||||
methods: [{
|
methods: [{
|
||||||
authentication_method: 0,
|
authentication_method: 0,
|
||||||
provider: 'asd'
|
provider: 'asd'
|
||||||
},{
|
}, {
|
||||||
authentication_method: 1,
|
authentication_method: 1,
|
||||||
provider: 'asd'
|
provider: 'asd'
|
||||||
}]
|
}]
|
||||||
},{
|
}, {
|
||||||
methods: [{
|
methods: [{
|
||||||
authentication_method: 1,
|
authentication_method: 1,
|
||||||
provider: 'asd'
|
provider: 'asd'
|
||||||
@ -58,27 +58,191 @@ export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
|
|||||||
|
|
||||||
export const SomePoliciesWithMethods = createExample(TestedComponent, {
|
export const SomePoliciesWithMethods = createExample(TestedComponent, {
|
||||||
...reducerStatesExample.policyReview,
|
...reducerStatesExample.policyReview,
|
||||||
policies: [{
|
policies: [
|
||||||
methods: [{
|
{
|
||||||
authentication_method: 0,
|
methods: [
|
||||||
provider: 'asd'
|
{
|
||||||
},{
|
authentication_method: 0,
|
||||||
authentication_method: 1,
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
provider: 'asd'
|
},
|
||||||
}]
|
{
|
||||||
},{
|
authentication_method: 1,
|
||||||
methods: [{
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
authentication_method: 1,
|
},
|
||||||
provider: 'asd'
|
{
|
||||||
}]
|
authentication_method: 2,
|
||||||
}],
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 0,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 1,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 3,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 0,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 1,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 4,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 0,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 2,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 3,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 0,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 2,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 4,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 0,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 3,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 4,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 1,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 2,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 3,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 1,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 2,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 4,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 1,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 3,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 4,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
authentication_method: 2,
|
||||||
|
provider: "https://kudos.demo.anastasis.lu/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 3,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authentication_method: 4,
|
||||||
|
provider: "https://anastasis.demo.taler.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
authentication_methods: [{
|
authentication_methods: [{
|
||||||
challenge: 'asd',
|
type: "email",
|
||||||
instructions: 'ins',
|
instructions: "Email to qwe@asd.com",
|
||||||
type: 'type',
|
challenge: "E5VPA"
|
||||||
|
}, {
|
||||||
|
type: "sms",
|
||||||
|
instructions: "SMS to 555-555",
|
||||||
|
challenge: ""
|
||||||
|
}, {
|
||||||
|
type: "question",
|
||||||
|
instructions: "Does P equal NP?",
|
||||||
|
challenge: "C5SP8"
|
||||||
},{
|
},{
|
||||||
challenge: 'asd2',
|
type: "email",
|
||||||
instructions: 'ins2',
|
instructions: "Email to qwe@asd.com",
|
||||||
type: 'type2',
|
challenge: "E5VPA"
|
||||||
|
}, {
|
||||||
|
type: "sms",
|
||||||
|
instructions: "SMS to 555-555",
|
||||||
|
challenge: ""
|
||||||
|
}, {
|
||||||
|
type: "question",
|
||||||
|
instructions: "Does P equal NP?",
|
||||||
|
challenge: "C5SP8"
|
||||||
}]
|
}]
|
||||||
} as ReducerState);
|
} as ReducerState);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
import { AnastasisClientFrame } from "./index";
|
import { AnastasisClientFrame } from "./index";
|
||||||
|
import { authMethods, KnownAuthMethods } from "./authMethodSetup";
|
||||||
|
|
||||||
export function ReviewPoliciesScreen(): VNode {
|
export function ReviewPoliciesScreen(): VNode {
|
||||||
const reducer = useAnastasisContext()
|
const reducer = useAnastasisContext()
|
||||||
@ -11,43 +12,50 @@ export function ReviewPoliciesScreen(): VNode {
|
|||||||
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
|
||||||
return <div>invalid state</div>
|
return <div>invalid state</div>
|
||||||
}
|
}
|
||||||
const authMethods = reducer.currentReducerState.authentication_methods ?? [];
|
const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
|
||||||
const policies = reducer.currentReducerState.policies ?? [];
|
const policies = reducer.currentReducerState.policies ?? [];
|
||||||
|
|
||||||
|
const errors = policies.length < 1 ? 'Need more policies' : undefined
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame title="Backup: Review Recovery Policies">
|
<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> }
|
||||||
{policies.map((p, policy_index) => {
|
{policies.map((p, policy_index) => {
|
||||||
const methods = p.methods
|
const methods = p.methods
|
||||||
.map(x => authMethods[x.authentication_method] && ({ ...authMethods[x.authentication_method], provider: x.provider }))
|
.map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
|
||||||
.filter(x => !!x)
|
.filter(x => !!x)
|
||||||
|
|
||||||
const policyName = methods.map(x => x.type).join(" + ");
|
const policyName = methods.map(x => x.type).join(" + ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={policy_index} class="policy">
|
<div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<h3>
|
<div>
|
||||||
Policy #{policy_index + 1}: {policyName}
|
<h3 class="subtitle">
|
||||||
</h3>
|
Policy #{policy_index + 1}: {policyName}
|
||||||
Required Authentications:
|
</h3>
|
||||||
{!methods.length && <p>
|
{!methods.length && <p>
|
||||||
No auth method found
|
No auth method found
|
||||||
</p>}
|
</p>}
|
||||||
<ul>
|
|
||||||
{methods.map((m, i) => {
|
{methods.map((m, i) => {
|
||||||
return (
|
return (
|
||||||
<li key={i}>
|
<p key={i} class="block" style={{display:'flex', alignItems:'center'}}>
|
||||||
{m.type} ({m.instructions}) at provider {m.provider}
|
<span class="icon">
|
||||||
</li>
|
{authMethods[m.type as KnownAuthMethods]?.icon}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() => reducer.transition("delete_policy", { policy_index })}
|
|
||||||
>
|
|
||||||
Delete Policy
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginTop: 'auto', marginBottom: 'auto' }}><button class="button is-danger" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -6,6 +6,7 @@ import { useAnastasisContext } from "../../context/anastasis";
|
|||||||
import {
|
import {
|
||||||
AnastasisClientFrame} from "./index";
|
AnastasisClientFrame} from "./index";
|
||||||
import { TextInput } from "../../components/fields/TextInput";
|
import { TextInput } from "../../components/fields/TextInput";
|
||||||
|
import { FileInput } from "../../components/fields/FileInput";
|
||||||
|
|
||||||
export function SecretEditorScreen(): VNode {
|
export function SecretEditorScreen(): VNode {
|
||||||
const reducer = useAnastasisContext()
|
const reducer = useAnastasisContext()
|
||||||
@ -57,6 +58,10 @@ export function SecretEditorScreen(): VNode {
|
|||||||
<TextInput
|
<TextInput
|
||||||
label="Secret Value:"
|
label="Secret Value:"
|
||||||
bind={[secretValue, setSecretValue]}
|
bind={[secretValue, setSecretValue]}
|
||||||
|
/> or import a file
|
||||||
|
<FileInput
|
||||||
|
label="Open file from your device"
|
||||||
|
bind={[secretValue, setSecretValue]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
|
@ -37,7 +37,7 @@ export function SecretSelectionScreen(): VNode {
|
|||||||
const recoveryDocument = reducer.currentReducerState.recovery_document
|
const recoveryDocument = reducer.currentReducerState.recovery_document
|
||||||
if (!recoveryDocument) {
|
if (!recoveryDocument) {
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame hideNext title="Recovery: Problem">
|
<AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem">
|
||||||
<p>No recovery document found, try with another provider</p>
|
<p>No recovery document found, try with another provider</p>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -44,7 +44,7 @@ export const NotSupportedChallenge = createExample(TestedComponent, {
|
|||||||
recovery_information: {
|
recovery_information: {
|
||||||
challenges: [{
|
challenges: [{
|
||||||
cost: 'USD:1',
|
cost: 'USD:1',
|
||||||
instructions: 'follow htis instructions',
|
instructions: 'does P equals NP?',
|
||||||
type: 'chall-type',
|
type: 'chall-type',
|
||||||
uuid: 'ASDASDSAD!1'
|
uuid: 'ASDASDSAD!1'
|
||||||
}],
|
}],
|
||||||
@ -58,7 +58,7 @@ export const MismatchedChallengeId = createExample(TestedComponent, {
|
|||||||
recovery_information: {
|
recovery_information: {
|
||||||
challenges: [{
|
challenges: [{
|
||||||
cost: 'USD:1',
|
cost: 'USD:1',
|
||||||
instructions: 'follow htis instructions',
|
instructions: 'does P equals NP?',
|
||||||
type: 'chall-type',
|
type: 'chall-type',
|
||||||
uuid: 'ASDASDSAD!1'
|
uuid: 'ASDASDSAD!1'
|
||||||
}],
|
}],
|
||||||
@ -72,7 +72,7 @@ export const SmsChallenge = createExample(TestedComponent, {
|
|||||||
recovery_information: {
|
recovery_information: {
|
||||||
challenges: [{
|
challenges: [{
|
||||||
cost: 'USD:1',
|
cost: 'USD:1',
|
||||||
instructions: 'follow htis instructions',
|
instructions: 'SMS to 555-5555',
|
||||||
type: 'sms',
|
type: 'sms',
|
||||||
uuid: 'ASDASDSAD!1'
|
uuid: 'ASDASDSAD!1'
|
||||||
}],
|
}],
|
||||||
@ -86,7 +86,7 @@ export const QuestionChallenge = createExample(TestedComponent, {
|
|||||||
recovery_information: {
|
recovery_information: {
|
||||||
challenges: [{
|
challenges: [{
|
||||||
cost: 'USD:1',
|
cost: 'USD:1',
|
||||||
instructions: 'follow htis instructions',
|
instructions: 'does P equals NP?',
|
||||||
type: 'question',
|
type: 'question',
|
||||||
uuid: 'ASDASDSAD!1'
|
uuid: 'ASDASDSAD!1'
|
||||||
}],
|
}],
|
||||||
@ -100,7 +100,7 @@ export const EmailChallenge = createExample(TestedComponent, {
|
|||||||
recovery_information: {
|
recovery_information: {
|
||||||
challenges: [{
|
challenges: [{
|
||||||
cost: 'USD:1',
|
cost: 'USD:1',
|
||||||
instructions: 'follow htis instructions',
|
instructions: 'Email to sebasjm@some-domain.com',
|
||||||
type: 'email',
|
type: 'email',
|
||||||
uuid: 'ASDASDSAD!1'
|
uuid: 'ASDASDSAD!1'
|
||||||
}],
|
}],
|
||||||
@ -114,7 +114,7 @@ export const PostChallenge = createExample(TestedComponent, {
|
|||||||
recovery_information: {
|
recovery_information: {
|
||||||
challenges: [{
|
challenges: [{
|
||||||
cost: 'USD:1',
|
cost: 'USD:1',
|
||||||
instructions: 'follow htis instructions',
|
instructions: 'Letter to address in postal code ABC123',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
uuid: 'ASDASDSAD!1'
|
uuid: 'ASDASDSAD!1'
|
||||||
}],
|
}],
|
||||||
|
@ -10,24 +10,24 @@ export function SolveScreen(): VNode {
|
|||||||
const [answer, setAnswer] = useState("");
|
const [answer, setAnswer] = useState("");
|
||||||
|
|
||||||
if (!reducer) {
|
if (!reducer) {
|
||||||
return <AnastasisClientFrame hideNext title="Recovery problem">
|
return <AnastasisClientFrame hideNav title="Recovery problem">
|
||||||
<div>no reducer in context</div>
|
<div>no reducer in context</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
}
|
}
|
||||||
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
|
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
|
||||||
return <AnastasisClientFrame hideNext title="Recovery problem">
|
return <AnastasisClientFrame hideNav title="Recovery problem">
|
||||||
<div>invalid state</div>
|
<div>invalid state</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reducer.currentReducerState.recovery_information) {
|
if (!reducer.currentReducerState.recovery_information) {
|
||||||
return <AnastasisClientFrame hideNext title="Recovery problem">
|
return <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery problem">
|
||||||
<div>no recovery information found</div>
|
<div>no recovery information found</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
}
|
}
|
||||||
if (!reducer.currentReducerState.selected_challenge_uuid) {
|
if (!reducer.currentReducerState.selected_challenge_uuid) {
|
||||||
return <AnastasisClientFrame hideNext title="Recovery problem">
|
return <AnastasisClientFrame hideNav title="Recovery problem">
|
||||||
<div>no selected uuid</div>
|
<div>invalid state</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,9 +70,9 @@ export function SolveScreen(): VNode {
|
|||||||
feedback={challengeFeedback[selectedUuid]} />
|
feedback={challengeFeedback[selectedUuid]} />
|
||||||
|
|
||||||
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<button class="button" onClick={onCancel}>Cancel</button>
|
<button class="button" onClick={onCancel}>Cancel</button>
|
||||||
<button class="button is-info" onClick={onNext} >Confirm</button>
|
<button class="button is-info" onClick={onNext} >Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -82,13 +82,13 @@ export interface SolveEntryProps {
|
|||||||
challenge: ChallengeInfo;
|
challenge: ChallengeInfo;
|
||||||
feedback?: ChallengeFeedback;
|
feedback?: ChallengeFeedback;
|
||||||
answer: string;
|
answer: string;
|
||||||
setAnswer: (s:string) => void;
|
setAnswer: (s: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SolveSmsEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode {
|
function SolveSmsEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode {
|
||||||
return (<Fragment>
|
return (<Fragment>
|
||||||
<p>An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below</p>
|
<p>An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below</p>
|
||||||
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/email',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'email'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'Email to sebasjm@email.com ',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithMoreExamples = createExample(TestedComponent[type].screen, 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
|
||||||
|
}]
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import {
|
||||||
|
encodeCrock,
|
||||||
|
stringToBytes
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
import { AnastasisClientFrame } from "../index";
|
||||||
|
import { TextInput } from "../../../components/fields/TextInput";
|
||||||
|
import { EmailInput } from "../../../components/fields/EmailInput";
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<EmailInput
|
||||||
|
label="Email address"
|
||||||
|
error={emailError}
|
||||||
|
placeholder="email@domain.com"
|
||||||
|
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>}
|
||||||
|
<div>
|
||||||
|
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<button class="button" onClick={cancel}>Canceul</button>
|
||||||
|
<span data-tooltip={errors}>
|
||||||
|
<button class="button is-info" disabled={errors !== undefined} onClick={addEmailAuth}>Add</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/IBAN',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'iban'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
export const WithMoreExamples = createExample(TestedComponent[type].screen, 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
|
||||||
|
}]
|
||||||
|
},);
|
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import {
|
||||||
|
canonicalJson,
|
||||||
|
encodeCrock,
|
||||||
|
stringToBytes
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { TextInput } from "../../../components/fields/TextInput";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
import { AnastasisClientFrame } from "../index";
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="Bank account holder name"
|
||||||
|
grabFocus
|
||||||
|
placeholder="John Smith"
|
||||||
|
bind={[name, setName]} />
|
||||||
|
<TextInput
|
||||||
|
label="IBAN"
|
||||||
|
placeholder="DE91100000000123456789"
|
||||||
|
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>}
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/Post',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'post'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'Letter to address in postal code QWE456',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithMoreExamples = createExample(TestedComponent[type].screen, 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
|
||||||
|
}]
|
||||||
|
});
|
@ -0,0 +1,102 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import {
|
||||||
|
canonicalJson, encodeCrock,
|
||||||
|
stringToBytes
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
import { TextInput } from "../../../components/fields/TextInput";
|
||||||
|
import { AnastasisClientFrame } from "..";
|
||||||
|
|
||||||
|
export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
|
||||||
|
const [fullName, setFullName] = useState("");
|
||||||
|
const [street, setStreet] = useState("");
|
||||||
|
const [city, setCity] = useState("");
|
||||||
|
const [postcode, setPostcode] = useState("");
|
||||||
|
const [country, setCountry] = useState("");
|
||||||
|
|
||||||
|
const addPostAuth = () => {
|
||||||
|
const challengeJson = {
|
||||||
|
full_name: fullName,
|
||||||
|
street,
|
||||||
|
city,
|
||||||
|
postcode,
|
||||||
|
country,
|
||||||
|
};
|
||||||
|
addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "post",
|
||||||
|
instructions: `Letter to address in postal code ${postcode}`,
|
||||||
|
challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
grabFocus
|
||||||
|
label="Full Name"
|
||||||
|
bind={[fullName, setFullName]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="Street"
|
||||||
|
bind={[street, setStreet]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="City" bind={[city, setCity]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="Postal Code" bind={[postcode, setPostcode]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
<span data-tooltip={errors}>
|
||||||
|
<button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/Question',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'question'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'Is integer factorization polynomial? (non-quantum computer)',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithMoreExamples = createExample(TestedComponent[type].screen, 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
|
||||||
|
}]
|
||||||
|
});
|
@ -0,0 +1,70 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import {
|
||||||
|
encodeCrock,
|
||||||
|
stringToBytes
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
import { AnastasisClientFrame } from "../index";
|
||||||
|
import { TextInput } from "../../../components/fields/TextInput";
|
||||||
|
|
||||||
|
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 errors = !questionText ? "Add your security question" : (
|
||||||
|
!answerText ? 'Add the answer to your question' : undefined
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<AnastasisClientFrame hideNav title="Add Security Question">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
For security question authentication, you need to provide a question
|
||||||
|
and its answer. When recovering your secret, you will be shown the
|
||||||
|
question and you will need to type the answer exactly as you typed it
|
||||||
|
here.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="Security question"
|
||||||
|
grabFocus
|
||||||
|
placeholder="Your question"
|
||||||
|
bind={[questionText, setQuestionText]} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="Answer"
|
||||||
|
placeholder="Your answer"
|
||||||
|
bind={[answerText, setAnswerText]}
|
||||||
|
/>
|
||||||
|
</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>}
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame >
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/Sms',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'sms'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'SMS to +11-1234-2345',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithMoreExamples = createExample(TestedComponent[type].screen, 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
|
||||||
|
}]
|
||||||
|
});
|
@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import {
|
||||||
|
encodeCrock,
|
||||||
|
stringToBytes
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { NumberInput } from "../../../components/fields/NumberInput";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
import { AnastasisClientFrame } from "../index";
|
||||||
|
|
||||||
|
export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
|
||||||
|
const [mobileNumber, setMobileNumber] = useState("");
|
||||||
|
const addSmsAuth = (): void => {
|
||||||
|
addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "sms",
|
||||||
|
instructions: `SMS to ${mobileNumber}`,
|
||||||
|
challenge: encodeCrock(stringToBytes(mobileNumber)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
const errors = !mobileNumber ? 'Add a mobile number' : undefined
|
||||||
|
return (
|
||||||
|
<AnastasisClientFrame hideNav title="Add SMS authentication">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
For SMS authentication, you need to provide a mobile number. When
|
||||||
|
recovering your secret, you will be asked to enter the code you
|
||||||
|
receive via SMS.
|
||||||
|
</p>
|
||||||
|
<div class="container">
|
||||||
|
<NumberInput
|
||||||
|
label="Mobile number"
|
||||||
|
placeholder="Your mobile number"
|
||||||
|
grabFocus
|
||||||
|
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>
|
||||||
|
<span data-tooltip={errors}>
|
||||||
|
<button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/TOTP',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'totp'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'instr',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
export const WithMoreExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'instr',
|
||||||
|
remove: () => null
|
||||||
|
},{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: 'instr',
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
@ -0,0 +1,47 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
import {
|
||||||
|
encodeCrock,
|
||||||
|
stringToBytes
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
import { AnastasisClientFrame } from "../index";
|
||||||
|
import { TextInput } from "../../../components/fields/TextInput";
|
||||||
|
import { QR } from "../../../components/QR";
|
||||||
|
|
||||||
|
export function AuthMethodTotpSetup({addAuthMethod, cancel, configured}: AuthMethodSetupProps): VNode {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const addTotpAuth = (): void => addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "totp",
|
||||||
|
instructions: `Enter code for ${name}`,
|
||||||
|
challenge: encodeCrock(stringToBytes(name)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const errors = !name ? 'The TOTP name is missing' : undefined;
|
||||||
|
return (
|
||||||
|
<AnastasisClientFrame hideNav title="Add TOTP authentication">
|
||||||
|
<p>
|
||||||
|
For Time-based One-Time Password (TOTP) authentication, you need to set
|
||||||
|
a name for the TOTP secret. Then, you must scan the generated QR code
|
||||||
|
with your TOTP App to import the TOTP secret into your TOTP App.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
label="TOTP Name"
|
||||||
|
grabFocus
|
||||||
|
bind={[name, setName]} />
|
||||||
|
</div>
|
||||||
|
<QR text={`sometext ${name}`} />
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createExample, reducerStatesExample } from '../../../utils';
|
||||||
|
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
|
||||||
|
import logoImage from '../../../assets/logo.jpeg'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Pages/backup/authMethods/Video',
|
||||||
|
component: TestedComponent,
|
||||||
|
args: {
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: 'onUpdate' },
|
||||||
|
onBack: { action: 'onBack' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const type: KnownAuthMethods = 'video'
|
||||||
|
|
||||||
|
export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: logoImage,
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
|
||||||
|
configured: [{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: logoImage,
|
||||||
|
remove: () => null
|
||||||
|
},{
|
||||||
|
challenge: 'qwe',
|
||||||
|
type,
|
||||||
|
instructions: logoImage,
|
||||||
|
remove: () => null
|
||||||
|
}]
|
||||||
|
});
|
@ -0,0 +1,56 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
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 "../AuthenticationEditorScreen";
|
||||||
|
import { AnastasisClientFrame } from "../index";
|
||||||
|
|
||||||
|
export function AuthMethodVideoSetup({cancel, addAuthMethod, configured}: AuthMethodSetupProps): VNode {
|
||||||
|
const [image, setImage] = useState("");
|
||||||
|
const addVideoAuth = (): void => {
|
||||||
|
addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "video",
|
||||||
|
instructions: image,
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div style={{textAlign:'center'}}>
|
||||||
|
<ImageInput
|
||||||
|
label="Choose photograph"
|
||||||
|
grabFocus
|
||||||
|
bind={[image, setImage]} />
|
||||||
|
</div>
|
||||||
|
{configured.length > 0 && <section class="section">
|
||||||
|
<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>
|
||||||
|
})}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
|
||||||
|
|
||||||
|
import { AuthMethodEmailSetup as EmailScreen } from "./AuthMethodEmailSetup";
|
||||||
|
import { AuthMethodIbanSetup as IbanScreen } from "./AuthMethodIbanSetup";
|
||||||
|
import { AuthMethodPostSetup as PostalScreen } from "./AuthMethodPostSetup";
|
||||||
|
import { AuthMethodQuestionSetup as QuestionScreen } from "./AuthMethodQuestionSetup";
|
||||||
|
import { AuthMethodSmsSetup as SmsScreen } from "./AuthMethodSmsSetup";
|
||||||
|
import { AuthMethodTotpSetup as TotpScreen } from "./AuthMethodTotpSetup";
|
||||||
|
import { AuthMethodVideoSetup as VideScreen } from "./AuthMethodVideoSetup";
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface AuthMethodConfiguration {
|
||||||
|
icon: VNode;
|
||||||
|
label: string;
|
||||||
|
screen: (props: AuthMethodSetupProps) => VNode;
|
||||||
|
}
|
||||||
|
export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
|
||||||
|
|
||||||
|
type KnowMethodConfig = {
|
||||||
|
[name in KnownAuthMethods]: AuthMethodConfiguration;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authMethods: KnowMethodConfig = {
|
||||||
|
question: {
|
||||||
|
icon: <img src={questionIcon} />,
|
||||||
|
label: "Question",
|
||||||
|
screen: QuestionScreen
|
||||||
|
},
|
||||||
|
sms: {
|
||||||
|
icon: <img src={smsIcon} />,
|
||||||
|
label: "SMS",
|
||||||
|
screen: SmsScreen
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
icon: <i class="mdi mdi-email" />,
|
||||||
|
label: "Email",
|
||||||
|
screen: EmailScreen
|
||||||
|
|
||||||
|
},
|
||||||
|
iban: {
|
||||||
|
icon: <i class="mdi mdi-bank" />,
|
||||||
|
label: "IBAN",
|
||||||
|
screen: IbanScreen
|
||||||
|
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
icon: <img src={postalIcon} />,
|
||||||
|
label: "Physical mail",
|
||||||
|
screen: PostalScreen
|
||||||
|
|
||||||
|
},
|
||||||
|
totp: {
|
||||||
|
icon: <i class="mdi mdi-devices" />,
|
||||||
|
label: "TOTP",
|
||||||
|
screen: TotpScreen
|
||||||
|
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
icon: <img src={videoIcon} />,
|
||||||
|
label: "Video",
|
||||||
|
screen: VideScreen
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,8 @@ import {
|
|||||||
VNode
|
VNode
|
||||||
} from "preact";
|
} from "preact";
|
||||||
import {
|
import {
|
||||||
useErrorBoundary} from "preact/hooks";
|
useErrorBoundary
|
||||||
|
} from "preact/hooks";
|
||||||
import { Menu } from "../../components/menu";
|
import { Menu } from "../../components/menu";
|
||||||
import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
|
import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
|
||||||
import {
|
import {
|
||||||
@ -59,7 +60,7 @@ interface AnastasisClientFrameProps {
|
|||||||
/**
|
/**
|
||||||
* Hide only the "next" button.
|
* Hide only the "next" button.
|
||||||
*/
|
*/
|
||||||
hideNext?: boolean;
|
hideNext?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ErrorBoundary(props: {
|
function ErrorBoundary(props: {
|
||||||
@ -112,13 +113,15 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
|
|||||||
<Menu title="Anastasis" />
|
<Menu title="Anastasis" />
|
||||||
<div>
|
<div>
|
||||||
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
|
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
|
||||||
<h1>{props.title}</h1>
|
<h1 class="title">{props.title}</h1>
|
||||||
<ErrorBanner />
|
<ErrorBanner />
|
||||||
{props.children}
|
{props.children}
|
||||||
{!props.hideNav ? (
|
{!props.hideNav ? (
|
||||||
<div style={{marginTop: '2em', display:'flex', justifyContent:'space-between'}}>
|
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<button class="button" onClick={() => reducer.back()}>Back</button>
|
<button class="button" onClick={() => reducer.back()}>Back</button>
|
||||||
{!props.hideNext ? <button class="button is-info"onClick={next}>Next</button> : null}
|
<span data-tooltip={props.hideNext}>
|
||||||
|
<button class="button is-info" onClick={next} disabled={props.hideNext !== undefined}>Next</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@ -151,18 +154,12 @@ const AnastasisClientImpl: FunctionalComponent = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
state.backup_state === BackupStates.ContinentSelecting ||
|
state.backup_state === BackupStates.ContinentSelecting ||
|
||||||
state.recovery_state === RecoveryStates.ContinentSelecting
|
state.recovery_state === RecoveryStates.ContinentSelecting ||
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<ContinentSelectionScreen />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
state.backup_state === BackupStates.CountrySelecting ||
|
state.backup_state === BackupStates.CountrySelecting ||
|
||||||
state.recovery_state === RecoveryStates.CountrySelecting
|
state.recovery_state === RecoveryStates.CountrySelecting
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<CountrySelectionScreen />
|
<ContinentSelectionScreen />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -198,10 +198,10 @@ div[data-tooltip]::before {
|
|||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home div {
|
// .home div {
|
||||||
margin-top: 0.5em;
|
// margin-top: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
// margin-bottom: 0.5em;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.policy {
|
.policy {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
@ -86,7 +86,13 @@ const base = {
|
|||||||
{
|
{
|
||||||
type: "question",
|
type: "question",
|
||||||
usage_fee: "COL:0"
|
usage_fee: "COL:0"
|
||||||
}
|
},{
|
||||||
|
type: "sms",
|
||||||
|
usage_fee: "COL:0"
|
||||||
|
},{
|
||||||
|
type: "email",
|
||||||
|
usage_fee: "COL:0"
|
||||||
|
},
|
||||||
],
|
],
|
||||||
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
|
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
|
||||||
storage_limit_in_megabytes: 16,
|
storage_limit_in_megabytes: 16,
|
||||||
|
@ -61,6 +61,7 @@ importers:
|
|||||||
preact-cli: ^3.2.2
|
preact-cli: ^3.2.2
|
||||||
preact-render-to-string: ^5.1.4
|
preact-render-to-string: ^5.1.4
|
||||||
preact-router: ^3.2.1
|
preact-router: ^3.2.1
|
||||||
|
qrcode-generator: ^1.4.4
|
||||||
sass: ^1.32.13
|
sass: ^1.32.13
|
||||||
sass-loader: ^10.1.1
|
sass-loader: ^10.1.1
|
||||||
sirv-cli: ^1.0.0-next.3
|
sirv-cli: ^1.0.0-next.3
|
||||||
@ -73,6 +74,7 @@ importers:
|
|||||||
preact: 10.5.14
|
preact: 10.5.14
|
||||||
preact-render-to-string: 5.1.19_preact@10.5.14
|
preact-render-to-string: 5.1.19_preact@10.5.14
|
||||||
preact-router: 3.2.1_preact@10.5.14
|
preact-router: 3.2.1_preact@10.5.14
|
||||||
|
qrcode-generator: 1.4.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@creativebulma/bulma-tooltip': 1.2.0
|
'@creativebulma/bulma-tooltip': 1.2.0
|
||||||
'@storybook/addon-a11y': 6.3.7
|
'@storybook/addon-a11y': 6.3.7
|
||||||
|