aboutsummaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui/src')
-rw-r--r--packages/anastasis-webui/src/.babelrc4
-rw-r--r--packages/anastasis-webui/src/components/AsyncButton.tsx37
-rw-r--r--packages/anastasis-webui/src/components/Notifications.tsx59
-rw-r--r--packages/anastasis-webui/src/components/QR.tsx27
-rw-r--r--packages/anastasis-webui/src/components/app.tsx1
-rw-r--r--packages/anastasis-webui/src/components/fields/DateInput.tsx112
-rw-r--r--packages/anastasis-webui/src/components/fields/EmailInput.tsx55
-rw-r--r--packages/anastasis-webui/src/components/fields/FileInput.tsx125
-rw-r--r--packages/anastasis-webui/src/components/fields/ImageInput.tsx96
-rw-r--r--packages/anastasis-webui/src/components/fields/NumberInput.tsx55
-rw-r--r--packages/anastasis-webui/src/components/fields/TextInput.tsx53
-rw-r--r--packages/anastasis-webui/src/components/menu/LangSelector.tsx99
-rw-r--r--packages/anastasis-webui/src/components/menu/NavigationBar.tsx81
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx304
-rw-r--r--packages/anastasis-webui/src/components/menu/index.tsx133
-rw-r--r--packages/anastasis-webui/src/components/picker/DatePicker.tsx314
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx45
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.tsx217
-rw-r--r--packages/anastasis-webui/src/context/anastasis.ts20
-rw-r--r--packages/anastasis-webui/src/context/translation.ts43
-rw-r--r--packages/anastasis-webui/src/declaration.d.ts28
-rw-r--r--packages/anastasis-webui/src/hooks/async.ts29
-rw-r--r--packages/anastasis-webui/src/hooks/index.ts113
-rw-r--r--packages/anastasis-webui/src/i18n/index.tsx54
-rw-r--r--packages/anastasis-webui/src/i18n/strings.ts38
-rw-r--r--packages/anastasis-webui/src/index.ts4
-rw-r--r--packages/anastasis-webui/src/manifest.json2
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx53
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx289
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx123
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx146
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx113
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx303
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx48
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx70
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx114
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx192
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx18
-rw-r--r--packages/anastasis-webui/src/pages/home/ConfirmModal.tsx58
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx115
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx190
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx200
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx44
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx17
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx36
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx89
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx217
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx158
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx24
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx213
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx117
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.tsx315
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.tsx26
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx26
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx16
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx85
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx107
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx90
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx148
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx85
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx118
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx112
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx116
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx117
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx86
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx105
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx258
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx121
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx83
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx148
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx115
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx118
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx85
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx88
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx112
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/index.tsx94
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/totp.ts57
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx103
-rw-r--r--packages/anastasis-webui/src/pages/notfound/index.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/profile/index.tsx61
-rw-r--r--packages/anastasis-webui/src/scss/DurationPicker.scss1
-rw-r--r--packages/anastasis-webui/src/scss/_aside.scss100
-rw-r--r--packages/anastasis-webui/src/scss/_card.scss4
-rw-r--r--packages/anastasis-webui/src/scss/_custom-calendar.scss83
-rw-r--r--packages/anastasis-webui/src/scss/_footer.scss2
-rw-r--r--packages/anastasis-webui/src/scss/_form.scss33
-rw-r--r--packages/anastasis-webui/src/scss/_hero-bar.scss8
-rw-r--r--packages/anastasis-webui/src/scss/_main-section.scss2
-rw-r--r--packages/anastasis-webui/src/scss/_mixins.scss6
-rw-r--r--packages/anastasis-webui/src/scss/_modal.scss2
-rw-r--r--packages/anastasis-webui/src/scss/_nav-bar.scss16
-rw-r--r--packages/anastasis-webui/src/scss/_table.scss28
-rw-r--r--packages/anastasis-webui/src/scss/_tiles.scss3
-rw-r--r--packages/anastasis-webui/src/scss/_title-bar.scss8
-rw-r--r--packages/anastasis-webui/src/scss/main.scss9
-rw-r--r--packages/anastasis-webui/src/template.html62
-rw-r--r--packages/anastasis-webui/src/utils/index.tsx159
110 files changed, 6332 insertions, 3041 deletions
diff --git a/packages/anastasis-webui/src/.babelrc b/packages/anastasis-webui/src/.babelrc
index 123002210..05f4dcc81 100644
--- a/packages/anastasis-webui/src/.babelrc
+++ b/packages/anastasis-webui/src/.babelrc
@@ -1,5 +1,3 @@
{
- "presets": [
- "preact-cli/babel"
- ]
+ "presets": ["preact-cli/babel"]
}
diff --git a/packages/anastasis-webui/src/components/AsyncButton.tsx b/packages/anastasis-webui/src/components/AsyncButton.tsx
index 92bef2219..8f855f29f 100644
--- a/packages/anastasis-webui/src/components/AsyncButton.tsx
+++ b/packages/anastasis-webui/src/components/AsyncButton.tsx
@@ -15,11 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h, VNode } from "preact";
+import { useLayoutEffect, useRef } from "preact/hooks";
// import { LoadingModal } from "../modal";
import { useAsync } from "../hooks/async";
// import { Translate } from "../../i18n";
@@ -28,22 +29,38 @@ type Props = {
children: ComponentChildren;
disabled?: boolean;
onClick?: () => Promise<void>;
+ grabFocus?: boolean;
[rest: string]: any;
};
-export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode {
+export function AsyncButton({
+ onClick,
+ grabFocus,
+ disabled,
+ children,
+ ...rest
+}: Props): VNode {
const { isLoading, request } = useAsync(onClick);
+ const buttonRef = useRef<HTMLButtonElement>(null);
+ useLayoutEffect(() => {
+ if (grabFocus) {
+ buttonRef.current?.focus();
+ }
+ }, [grabFocus]);
+
// if (isSlow) {
// return <LoadingModal onCancel={cancel} />;
// }
- if (isLoading) {
+ if (isLoading) {
return <button class="button">Loading...</button>;
}
- return <span data-tooltip={rest['data-tooltip']} style={{marginLeft: 5}}>
- <button {...rest} onClick={request} disabled={disabled}>
- {children}
- </button>
- </span>;
+ return (
+ <span data-tooltip={rest["data-tooltip"]} style={{ marginLeft: 5 }}>
+ <button {...rest} ref={buttonRef} onClick={request} disabled={disabled}>
+ {children}
+ </button>
+ </span>
+ );
}
diff --git a/packages/anastasis-webui/src/components/Notifications.tsx b/packages/anastasis-webui/src/components/Notifications.tsx
index c916020d7..e34550386 100644
--- a/packages/anastasis-webui/src/components/Notifications.tsx
+++ b/packages/anastasis-webui/src/components/Notifications.tsx
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
@@ -27,7 +27,7 @@ export interface Notification {
type: MessageType;
}
-export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
interface Props {
notifications: Notification[];
@@ -36,24 +36,39 @@ interface Props {
function messageStyle(type: MessageType): string {
switch (type) {
- case "INFO": return "message is-info";
- case "WARN": return "message is-warning";
- case "ERROR": return "message is-danger";
- case "SUCCESS": return "message is-success";
- default: return "message"
+ case "INFO":
+ return "message is-info";
+ case "WARN":
+ return "message is-warning";
+ case "ERROR":
+ return "message is-danger";
+ case "SUCCESS":
+ return "message is-success";
+ default:
+ return "message";
}
}
-export function Notifications({ notifications, removeNotification }: Props): VNode {
- return <div class="block">
- {notifications.map((n,i) => <article key={i} class={messageStyle(n.type)}>
- <div class="message-header">
- <p>{n.message}</p>
- <button class="delete" onClick={() => removeNotification && removeNotification(n)} />
- </div>
- {n.description && <div class="message-body">
- {n.description}
- </div>}
- </article>)}
- </div>
-} \ No newline at end of file
+export function Notifications({
+ notifications,
+ removeNotification,
+}: Props): VNode {
+ return (
+ <div class="block">
+ {notifications.map((n, i) => (
+ <article key={i} class={messageStyle(n.type)}>
+ <div class="message-header">
+ <p>{n.message}</p>
+ {removeNotification && (
+ <button
+ class="delete"
+ onClick={() => removeNotification && removeNotification(n)}
+ />
+ )}
+ </div>
+ {n.description && <div class="message-body">{n.description}</div>}
+ </article>
+ ))}
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/components/QR.tsx b/packages/anastasis-webui/src/components/QR.tsx
index 48f1a7c12..9a05f6097 100644
--- a/packages/anastasis-webui/src/components/QR.tsx
+++ b/packages/anastasis-webui/src/components/QR.tsx
@@ -21,15 +21,28 @@ import qrcode from "qrcode-generator";
export function QR({ text }: { text: string }): VNode {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
- const qr = qrcode(0, 'L');
+ const qr = qrcode(0, "L");
qr.addData(text);
qr.make();
- if (divRef.current) divRef.current.innerHTML = qr.createSvgTag({
- scalable: true,
- });
+ if (divRef.current)
+ divRef.current.innerHTML = qr.createSvgTag({
+ scalable: true,
+ });
});
- return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
- <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
- </div>;
+ return (
+ <div
+ style={{
+ width: "100%",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ }}
+ >
+ <div
+ style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
+ ref={divRef}
+ />
+ </div>
+ );
}
diff --git a/packages/anastasis-webui/src/components/app.tsx b/packages/anastasis-webui/src/components/app.tsx
index c6b4cfc14..4c6683c0c 100644
--- a/packages/anastasis-webui/src/components/app.tsx
+++ b/packages/anastasis-webui/src/components/app.tsx
@@ -1,6 +1,5 @@
import { FunctionalComponent, h } from "preact";
import { TranslationProvider } from "../context/translation";
-
import AnastasisClient from "../pages/home";
const App: FunctionalComponent = () => {
diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx
index 3148c953f..18ef89908 100644
--- a/packages/anastasis-webui/src/components/fields/DateInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx
@@ -1,4 +1,4 @@
-import { format, isAfter, parse, sub, subYears } from "date-fns";
+import { format, subYears } from "date-fns";
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { DatePicker } from "../picker/DatePicker";
@@ -9,6 +9,7 @@ export interface DateInputProps {
tooltip?: string;
error?: string;
years?: Array<number>;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
@@ -19,56 +20,71 @@ export function DateInput(props: DateInputProps): VNode {
inputRef.current?.focus();
}
}, [props.grabFocus]);
- const [opened, setOpened] = useState(false)
+ const [opened, setOpened] = useState(false);
const value = props.bind[0] || "";
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
- const calendar = subYears(new Date(), 30)
-
- return <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control">
- <div class="field has-addons">
- <p class="control">
- <input
- type="text"
- class={showError ? 'input is-danger' : 'input'}
- value={value}
- onInput={(e) => {
- const text = e.currentTarget.value
- setDirty(true)
- props.bind[1](text);
- }}
- ref={inputRef} />
- </p>
- <p class="control">
- <a class="button" onClick={() => { setOpened(true) }}>
- <span class="icon"><i class="mdi mdi-calendar" /></span>
- </a>
- </p>
+ const calendar = subYears(new Date(), 30);
+
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control">
+ <div class="field has-addons">
+ <p class="control">
+ <input
+ type="text"
+ class={showError ? "input is-danger" : "input"}
+ value={value}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ const text = e.currentTarget.value;
+ setDirty(true);
+ props.bind[1](text);
+ }}
+ ref={inputRef}
+ />
+ </p>
+ <p class="control">
+ <a
+ class="button"
+ onClick={() => {
+ setOpened(true);
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </p>
+ </div>
</div>
+ <p class="help">Using the format yyyy-mm-dd</p>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ <DatePicker
+ opened={opened}
+ initialDate={calendar}
+ years={props.years}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ setDirty(true);
+ const v = format(d, "yyyy-MM-dd");
+ props.bind[1](v);
+ }}
+ />
</div>
- <p class="help">Using the format yyyy-mm-dd</p>
- {showError && <p class="help is-danger">{props.error}</p>}
- <DatePicker
- opened={opened}
- initialDate={calendar}
- years={props.years}
- closeFunction={() => setOpened(false)}
- dateReceiver={(d) => {
- setDirty(true)
- const v = format(d, 'yyyy-MM-dd')
- props.bind[1](v);
- }}
- />
- </div>
- ;
-
+ );
}
diff --git a/packages/anastasis-webui/src/components/fields/EmailInput.tsx b/packages/anastasis-webui/src/components/fields/EmailInput.tsx
index e21418fea..4c35c0686 100644
--- a/packages/anastasis-webui/src/components/fields/EmailInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/EmailInput.tsx
@@ -7,6 +7,7 @@ export interface TextInputProps {
error?: string;
placeholder?: string;
tooltip?: string;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
@@ -18,27 +19,39 @@ export function EmailInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
- return (<div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- required
- placeholder={props.placeholder}
- type="email"
- class={showError ? 'input is-danger' : 'input'}
- onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
- ref={inputRef}
- style={{ display: "block" }} />
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ required
+ placeholder={props.placeholder}
+ type="email"
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ ref={inputRef}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
</div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
);
}
diff --git a/packages/anastasis-webui/src/components/fields/FileInput.tsx b/packages/anastasis-webui/src/components/fields/FileInput.tsx
index 8b144ea43..adf51afb0 100644
--- a/packages/anastasis-webui/src/components/fields/FileInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/FileInput.tsx
@@ -15,16 +15,31 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
-import { TextInputProps } from "./TextInput";
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
+
+export interface FileTypeContent {
+ content: string;
+ type: string;
+ name: string;
+}
+
+export interface FileInputProps {
+ label: string;
+ grabFocus?: boolean;
+ disabled?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ onChange: (v: FileTypeContent | undefined) => void;
+}
-export function FileInput(props: TextInputProps): VNode {
+export function FileInput(props: FileInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
if (props.grabFocus) {
@@ -32,50 +47,58 @@ export function FileInput(props: TextInputProps): VNode {
}
}, [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>}
+ const fileInputRef = useRef<HTMLInputElement>(null);
+ const [sizeError, setSizeError] = useState(false);
+ return (
+ <div class="field">
+ <label class="label">
+ <a class="button" onClick={(e) => fileInputRef.current?.click()}>
+ <div class="icon is-small ">
+ <i class="mdi mdi-folder" />
+ </div>
+ <span>
+ {props.label}
+ </span>
+ </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={fileInputRef}
+ style={{ display: "none" }}
+ type="file"
+ // name={String(name)}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return props.onChange(undefined);
+ }
+ console.log(f)
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return props.onChange(undefined);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return props.onChange({content: `data:${f[0].type};base64,${b64}`, name: f[0].name, type: f[0].type});
+ });
+ }}
+ />
+ {props.error && <p class="help is-danger">{props.error}</p>}
+ {sizeError && (
+ <p class="help is-danger">File should be smaller than 1 MB</p>
+ )}
+ </div>
</div>
- </div>
+ );
}
-
diff --git a/packages/anastasis-webui/src/components/fields/ImageInput.tsx b/packages/anastasis-webui/src/components/fields/ImageInput.tsx
index d5bf643d4..3f8cc58dd 100644
--- a/packages/anastasis-webui/src/components/fields/ImageInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/ImageInput.tsx
@@ -15,15 +15,15 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png";
import { TextInputProps } from "./TextInput";
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
export function ImageInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
@@ -35,47 +35,59 @@ export function ImageInput(props: TextInputProps): VNode {
const value = props.bind[0];
// const [dirty, setDirty] = useState(false)
- const image = useRef<HTMLInputElement>(null)
- const [sizeError, setSizeError] = useState(false)
+ const image = useRef<HTMLInputElement>(null);
+ const [sizeError, setSizeError] = useState(false);
function onChange(v: string): void {
// setDirty(true);
props.bind[1](v);
}
- return <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control">
- <img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} />
- <input
- ref={image} style={{ display: 'none' }}
- type="file" name={String(name)}
- onChange={e => {
- const f: FileList | null = e.currentTarget.files
- if (!f || f.length != 1) {
- return onChange(emptyImage)
- }
- if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
- setSizeError(true)
- return onChange(emptyImage)
- }
- setSizeError(false)
- return f[0].arrayBuffer().then(b => {
- const b64 = btoa(
- new Uint8Array(b)
- .reduce((data, byte) => data + String.fromCharCode(byte), '')
- )
- return onChange(`data:${f[0].type};base64,${b64}` as any)
- })
- }} />
- {props.error && <p class="help is-danger">{props.error}</p>}
- {sizeError && <p class="help is-danger">
- Image should be smaller than 1 MB
- </p>}
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control">
+ <img
+ src={!value ? emptyImage : value}
+ style={{ width: 200, height: 200 }}
+ onClick={() => image.current?.click()}
+ />
+ <input
+ ref={image}
+ style={{ display: "none" }}
+ type="file"
+ name={String(name)}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(emptyImage);
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return onChange(emptyImage);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ {props.error && <p class="help is-danger">{props.error}</p>}
+ {sizeError && (
+ <p class="help is-danger">Image should be smaller than 1 MB</p>
+ )}
+ </div>
</div>
- </div>
+ );
}
-
diff --git a/packages/anastasis-webui/src/components/fields/NumberInput.tsx b/packages/anastasis-webui/src/components/fields/NumberInput.tsx
index 2afb242b8..4856131c7 100644
--- a/packages/anastasis-webui/src/components/fields/NumberInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/NumberInput.tsx
@@ -7,10 +7,11 @@ export interface TextInputProps {
error?: string;
placeholder?: string;
tooltip?: string;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
-export function NumberInput(props: TextInputProps): VNode {
+export function PhoneNumberInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
if (props.grabFocus) {
@@ -18,26 +19,38 @@ export function NumberInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
- return (<div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- type="number"
- placeholder={props.placeholder}
- class={showError ? 'input is-danger' : 'input'}
- onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
- ref={inputRef}
- style={{ display: "block" }} />
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ type="tel"
+ placeholder={props.placeholder}
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ ref={inputRef}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
</div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
);
}
diff --git a/packages/anastasis-webui/src/components/fields/TextInput.tsx b/packages/anastasis-webui/src/components/fields/TextInput.tsx
index c093689c5..efa95d84e 100644
--- a/packages/anastasis-webui/src/components/fields/TextInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/TextInput.tsx
@@ -4,9 +4,11 @@ import { useLayoutEffect, useRef, useState } from "preact/hooks";
export interface TextInputProps {
label: string;
grabFocus?: boolean;
+ disabled?: boolean;
error?: string;
placeholder?: string;
tooltip?: string;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
@@ -18,25 +20,38 @@ export function TextInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
- return (<div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- placeholder={props.placeholder}
- class={showError ? 'input is-danger' : 'input'}
- onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
- ref={inputRef}
- style={{ display: "block" }} />
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ disabled={props.disabled}
+ placeholder={props.placeholder}
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ ref={inputRef}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
</div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
);
}
diff --git a/packages/anastasis-webui/src/components/menu/LangSelector.tsx b/packages/anastasis-webui/src/components/menu/LangSelector.tsx
index 0f91abd7e..fa22a29c0 100644
--- a/packages/anastasis-webui/src/components/menu/LangSelector.tsx
+++ b/packages/anastasis-webui/src/components/menu/LangSelector.tsx
@@ -15,59 +15,78 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import langIcon from '../../assets/icons/languageicon.svg';
+import langIcon from "../../assets/icons/languageicon.svg";
import { useTranslationContext } from "../../context/translation";
-import { strings as messages } from '../../i18n/strings'
+import { strings as messages } from "../../i18n/strings";
type LangsNames = {
- [P in keyof typeof messages]: string
-}
+ [P in keyof typeof messages]: string;
+};
const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
function getLangName(s: keyof LangsNames | string): string {
- if (names[s]) return names[s]
- return String(s)
+ if (names[s]) return names[s];
+ return String(s);
}
export function LangSelector(): VNode {
- const [updatingLang, setUpdatingLang] = useState(false)
- const { lang, changeLanguage } = useTranslationContext()
+ const [updatingLang, setUpdatingLang] = useState(false);
+ const { lang, changeLanguage } = useTranslationContext();
- return <div class="dropdown is-active ">
- <div class="dropdown-trigger">
- <button class="button has-tooltip-left"
- data-tooltip="change language selection"
- aria-haspopup="true"
- aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}>
- <div class="icon is-small is-left">
- <img src={langIcon} />
- </div>
- <span>{getLangName(lang)}</span>
- <div class="icon is-right">
- <i class="mdi mdi-chevron-down" />
+ return (
+ <div class="dropdown is-active ">
+ <div class="dropdown-trigger">
+ <button
+ class="button has-tooltip-left"
+ data-tooltip="change language selection"
+ aria-haspopup="true"
+ aria-controls="dropdown-menu"
+ onClick={() => setUpdatingLang(!updatingLang)}
+ >
+ <div class="icon is-small is-left">
+ <img src={langIcon} />
+ </div>
+ <span>{getLangName(lang)}</span>
+ <div class="icon is-right">
+ <i class="mdi mdi-chevron-down" />
+ </div>
+ </button>
+ </div>
+ {updatingLang && (
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ <div class="dropdown-content">
+ {Object.keys(messages)
+ .filter((l) => l !== lang)
+ .map((l) => (
+ <a
+ key={l}
+ class="dropdown-item"
+ value={l}
+ onClick={() => {
+ changeLanguage(l);
+ setUpdatingLang(false);
+ }}
+ >
+ {getLangName(l)}
+ </a>
+ ))}
+ </div>
</div>
- </button>
+ )}
</div>
- {updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
- <div class="dropdown-content">
- {Object.keys(messages)
- .filter((l) => l !== lang)
- .map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
- </div>
- </div>}
- </div>
-} \ No newline at end of file
+ );
+}
diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
index 935951ab9..8d5a0473b 100644
--- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
@@ -15,13 +15,13 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
-import logo from '../../assets/logo.jpeg';
-import { LangSelector } from './LangSelector';
+import { h, VNode } from "preact";
+import logo from "../../assets/logo.jpeg";
+import { LangSelector } from "./LangSelector";
interface Props {
onMobileMenu: () => void;
@@ -29,30 +29,51 @@ interface Props {
}
export function NavigationBar({ onMobileMenu, title }: Props): VNode {
- return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
- <div class="navbar-brand">
- <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>{title}</span>
-
- <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onClick={(e) => {
- onMobileMenu()
- e.stopPropagation()
- }}>
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- </a>
- </div>
-
- <div class="navbar-menu ">
- <a class="navbar-start is-justify-content-center is-flex-grow-1" href="https://taler.net">
- <img src={logo} style={{ height: 50, maxHeight: 50 }} />
- </a>
- <div class="navbar-end">
- <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- {/* <LangSelector /> */}
+ return (
+ <nav
+ class="navbar is-fixed-top"
+ role="navigation"
+ aria-label="main navigation"
+ >
+ <div class="navbar-brand">
+ <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
+ {title}
+ </span>
+ <a
+ href="mailto:contact@anastasis.lu"
+ style={{ alignSelf: "center", padding: "0.5em" }}
+ >
+ Contact us
+ </a>
+ <a
+ href="https://bugs.anastasis.li/"
+ style={{ alignSelf: "center", padding: "0.5em" }}
+ >
+ Report a bug
+ </a>
+ {/* <a
+ role="button"
+ class="navbar-burger"
+ aria-label="menu"
+ aria-expanded="false"
+ onClick={(e) => {
+ onMobileMenu();
+ e.stopPropagation();
+ }}
+ >
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ </a> */}
+ </div>
+
+ <div class="navbar-menu ">
+ <div class="navbar-end">
+ <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
+ {/* <LangSelector /> */}
+ </div>
</div>
</div>
- </div>
- </nav>
+ </nav>
);
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 72655662f..c73369dd6 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -15,16 +15,15 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h, VNode } from 'preact';
-import { BackupStates, RecoveryStates } from '../../../../anastasis-core/lib';
-import { useAnastasisContext } from '../../context/anastasis';
-import { Translate } from '../../i18n';
-import { LangSelector } from './LangSelector';
+import { Fragment, h, VNode } from "preact";
+import { BackupStates, RecoveryStates } from "../../../../anastasis-core/lib";
+import { useAnastasisContext } from "../../context/anastasis";
+import { Translate } from "../../i18n";
+import { LangSelector } from "./LangSelector";
interface Props {
mobile?: boolean;
@@ -32,10 +31,10 @@ interface Props {
export function Sidebar({ mobile }: Props): VNode {
// const config = useConfigContext();
- const config = { version: 'none' }
+ const config = { version: "none" };
// FIXME: add replacement for __VERSION__ with the current version
- const process = { env: { __VERSION__: '0.0.0' } }
- const reducer = useAnastasisContext()!
+ const process = { env: { __VERSION__: "0.0.0" } };
+ const reducer = useAnastasisContext()!;
return (
<aside class="aside is-placed-left is-expanded">
@@ -44,114 +43,235 @@ export function Sidebar({ mobile }: Props): VNode {
</div>} */}
<div class="aside-tools">
<div class="aside-tools-label">
- <div><b>Anastasis</b> Reducer</div>
- <div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}>
- {process.env.__VERSION__} ({config.version})
+ <div>
+ <b>Anastasis</b>
+ </div>
+ <div
+ class="is-size-7 has-text-right"
+ style={{ lineHeight: 0, marginTop: -10 }}
+ >
+ Version {process.env.__VERSION__} ({config.version})
</div>
</div>
</div>
<div class="menu is-menu-main">
- {!reducer.currentReducerState &&
+ {!reducer.currentReducerState && (
<p class="menu-label">
<Translate>Backup or Recorver</Translate>
</p>
- }
+ )}
<ul class="menu-list">
- {!reducer.currentReducerState &&
+ {!reducer.currentReducerState && (
<li>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Select one option</Translate></span>
- </div>
- </li>
- }
- {reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment>
- <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
- reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Location</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Personal information</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Authorization methods</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Policies</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Secret input</Translate></span>
+ <span class="menu-item-label">
+ <Translate>Select one option</Translate>
+ </span>
</div>
</li>
- {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
+ )}
+ {reducer.currentReducerState &&
+ reducer.currentReducerState.backup_state ? (
+ <Fragment>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.ContinentSelecting ||
+ reducer.currentReducerState.backup_state ===
+ BackupStates.CountrySelecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Location</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.UserAttributesCollecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Personal information</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.AuthenticationsEditing
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Authorization methods</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.PoliciesReviewing
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Policies</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.SecretEditing
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Secret input</Translate>
+ </span>
+ </div>
+ </li>
+ {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div>
</li> */}
- <li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Backup completed</Translate></span>
- </div>
- </li>
- {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.BackupFinished
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Backup completed</Translate>
+ </span>
+ </div>
+ </li>
+ {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Truth Paying</Translate></span>
</div>
</li> */}
- </Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
- reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Location</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Personal information</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Secret selection</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ||
- reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Solve Challenges</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Secret recovered</Translate></span>
- </div>
- </li>
- </Fragment>)}
- {reducer.currentReducerState &&
+ </Fragment>
+ ) : (
+ reducer.currentReducerState &&
+ reducer.currentReducerState?.recovery_state && (
+ <Fragment>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.ContinentSelecting ||
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.CountrySelecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Location</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.UserAttributesCollecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Personal information</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.SecretSelecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Secret selection</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.ChallengeSelecting ||
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.ChallengeSolving
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Solve Challenges</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.RecoveryFinished
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Secret recovered</Translate>
+ </span>
+ </div>
+ </li>
+ </Fragment>
+ )
+ )}
+ {reducer.currentReducerState && (
<li>
<div class="buttons ml-4">
- <button class="button is-danger is-right" onClick={() => reducer.reset()}>Reset session</button>
+ <button
+ class="button is-danger is-right"
+ onClick={() => reducer.reset()}
+ >
+ Reset session
+ </button>
</div>
</li>
- }
-
+ )}
+ {/* <li>
+ <div class="buttons ml-4">
+ <button class="button is-info is-right" >Manage providers</button>
+ </div>
+ </li> */}
</ul>
</div>
</aside>
);
}
-
diff --git a/packages/anastasis-webui/src/components/menu/index.tsx b/packages/anastasis-webui/src/components/menu/index.tsx
index febcd79c8..99d0f7646 100644
--- a/packages/anastasis-webui/src/components/menu/index.tsx
+++ b/packages/anastasis-webui/src/components/menu/index.tsx
@@ -15,41 +15,53 @@
*/
import { ComponentChildren, Fragment, h, VNode } from "preact";
-import Match from 'preact-router/match';
+import Match from "preact-router/match";
import { useEffect, useState } from "preact/hooks";
import { NavigationBar } from "./NavigationBar";
import { Sidebar } from "./SideBar";
-
-
-
interface MenuProps {
title: string;
}
-function WithTitle({ title, children }: { title: string; children: ComponentChildren }): VNode {
+function WithTitle({
+ title,
+ children,
+}: {
+ title: string;
+ children: ComponentChildren;
+}): VNode {
useEffect(() => {
- document.title = `Taler Backoffice: ${title}`
- }, [title])
- return <Fragment>{children}</Fragment>
+ document.title = `${title}`;
+ }, [title]);
+ return <Fragment>{children}</Fragment>;
}
export function Menu({ title }: MenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false)
-
- return <Match>{({ path }: { path: string }) => {
- const titleWithSubtitle = title // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
- return (<WithTitle title={titleWithSubtitle}>
- <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}>
- <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={titleWithSubtitle} />
-
- <Sidebar mobile={mobileOpen} />
-
- </div>
- </WithTitle>
- )
- }}</Match>
-
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ return (
+ <Match>
+ {({ path }: { path: string }) => {
+ const titleWithSubtitle = title; // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
+ return (
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ <Sidebar mobile={mobileOpen} />
+ </div>
+ </WithTitle>
+ );
+ }}
+ </Match>
+ );
}
interface NotYetReadyAppMenuProps {
@@ -60,37 +72,57 @@ interface NotYetReadyAppMenuProps {
interface NotifProps {
notification?: Notification;
}
-export function NotificationCard({ notification: n }: NotifProps): VNode | null {
- if (!n) return null
- return <div class="notification">
- <div class="columns is-vcentered">
- <div class="column is-12">
- <article class={n.type === 'ERROR' ? "message is-danger" : (n.type === 'WARN' ? "message is-warning" : "message is-info")}>
- <div class="message-header">
- <p>{n.message}</p>
- </div>
- {n.description &&
- <div class="message-body">
- {n.description}
- </div>}
- </article>
+export function NotificationCard({
+ notification: n,
+}: NotifProps): VNode | null {
+ if (!n) return null;
+ return (
+ <div class="notification">
+ <div class="columns is-vcentered">
+ <div class="column is-12">
+ <article
+ class={
+ n.type === "ERROR"
+ ? "message is-danger"
+ : n.type === "WARN"
+ ? "message is-warning"
+ : "message is-info"
+ }
+ >
+ <div class="message-header">
+ <p>{n.message}</p>
+ </div>
+ {n.description && <div class="message-body">{n.description}</div>}
+ </article>
+ </div>
</div>
</div>
- </div>
+ );
}
-export function NotYetReadyAppMenu({ onLogout, title }: NotYetReadyAppMenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false)
+export function NotYetReadyAppMenu({
+ onLogout,
+ title,
+}: NotYetReadyAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
- document.title = `Taler Backoffice: ${title}`
- }, [title])
-
- return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}>
- <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} />
- {onLogout && <Sidebar mobile={mobileOpen} />}
- </div>
-
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class="has-aside-mobile-expanded"
+ // class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ {onLogout && <Sidebar mobile={mobileOpen} />}
+ </div>
+ );
}
export interface Notification {
@@ -99,6 +131,5 @@ export interface Notification {
type: MessageType;
}
-export type ValueOrFunction<T> = T | ((p: T) => T)
-export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
-
+export type ValueOrFunction<T> = T | ((p: T) => T);
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
index eb5d8145d..d689db386 100644
--- a/packages/anastasis-webui/src/components/picker/DatePicker.tsx
+++ b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, Component } from "preact";
@@ -34,83 +34,71 @@ interface State {
selectYearMode: boolean;
currentDate: Date;
}
-const now = new Date()
+const now = new Date();
const monthArrShortFull = [
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December'
-]
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
const monthArrShort = [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec'
-]
-
-const dayArr = [
- 'Sun',
- 'Mon',
- 'Tue',
- 'Wed',
- 'Thu',
- 'Fri',
- 'Sat'
-]
-
-const yearArr: number[] = []
-
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+];
+
+const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+const yearArr: number[] = [];
// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
export class DatePicker extends Component<Props, State> {
-
closeDatePicker() {
this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
}
/**
- * Gets fired when a day gets clicked.
- * @param {object} e The event thrown by the <span /> element clicked
- */
+ * Gets fired when a day gets clicked.
+ * @param {object} e The event thrown by the <span /> element clicked
+ */
dayClicked(e: any) {
-
const element = e.target; // the actual element clicked
- if (element.innerHTML === '') return false; // don't continue if <span /> empty
+ if (element.innerHTML === "") return false; // don't continue if <span /> empty
// get date from clicked element (gets attached when rendered)
- const date = new Date(element.getAttribute('data-value'));
+ const date = new Date(element.getAttribute("data-value"));
// update the state
this.setState({ currentDate: date });
- this.passDateToParent(date)
+ this.passDateToParent(date);
}
/**
- * returns days in month as array
- * @param {number} month the month to display
- * @param {number} year the year to display
- */
+ * returns days in month as array
+ * @param {number} month the month to display
+ * @param {number} year the year to display
+ */
getDaysByMonth(month: number, year: number) {
-
const calendar = [];
const date = new Date(year, month, 1); // month to display
@@ -122,15 +110,17 @@ export class DatePicker extends Component<Props, State> {
// the calendar is 7*6 fields big, so 42 loops
for (let i = 0; i < 42; i++) {
-
if (i >= firstDay && day !== null) day = day + 1;
if (day !== null && day > lastDate) day = null;
// append the calendar Array
calendar.push({
- day: (day === 0 || day === null) ? null : day, // null or number
- date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date()
- today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean
+ day: day === 0 || day === null ? null : day, // null or number
+ date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date()
+ today:
+ day === now.getDate() &&
+ month === now.getMonth() &&
+ year === now.getFullYear(), // boolean
});
}
@@ -138,51 +128,48 @@ export class DatePicker extends Component<Props, State> {
}
/**
- * Display previous month by updating state
- */
+ * Display previous month by updating state
+ */
displayPrevMonth() {
if (this.state.displayedMonth <= 0) {
this.setState({
displayedMonth: 11,
- displayedYear: this.state.displayedYear - 1
+ displayedYear: this.state.displayedYear - 1,
});
- }
- else {
+ } else {
this.setState({
- displayedMonth: this.state.displayedMonth - 1
+ displayedMonth: this.state.displayedMonth - 1,
});
}
}
/**
- * Display next month by updating state
- */
+ * Display next month by updating state
+ */
displayNextMonth() {
if (this.state.displayedMonth >= 11) {
this.setState({
displayedMonth: 0,
- displayedYear: this.state.displayedYear + 1
+ displayedYear: this.state.displayedYear + 1,
});
- }
- else {
+ } else {
this.setState({
- displayedMonth: this.state.displayedMonth + 1
+ displayedMonth: this.state.displayedMonth + 1,
});
}
}
/**
- * Display the selected month (gets fired when clicking on the date string)
- */
+ * Display the selected month (gets fired when clicking on the date string)
+ */
displaySelectedMonth() {
if (this.state.selectYearMode) {
this.toggleYearSelector();
- }
- else {
+ } else {
if (!this.state.currentDate) return false;
this.setState({
displayedMonth: this.state.currentDate.getMonth(),
- displayedYear: this.state.currentDate.getFullYear()
+ displayedYear: this.state.currentDate.getFullYear(),
});
}
}
@@ -194,17 +181,21 @@ export class DatePicker extends Component<Props, State> {
changeDisplayedYear(e: any) {
const element = e.target;
this.toggleYearSelector();
- this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 });
+ this.setState({
+ displayedYear: parseInt(element.innerHTML, 10),
+ displayedMonth: 0,
+ });
}
/**
- * Pass the selected date to parent when 'OK' is clicked
- */
+ * Pass the selected date to parent when 'OK' is clicked
+ */
passSavedDateDateToParent() {
- this.passDateToParent(this.state.currentDate)
+ this.passDateToParent(this.state.currentDate);
}
passDateToParent(date: Date) {
- if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date);
+ if (typeof this.props.dateReceiver === "function")
+ this.props.dateReceiver(date);
this.closeDatePicker();
}
@@ -233,94 +224,133 @@ export class DatePicker extends Component<Props, State> {
currentDate: initial,
displayedMonth: initial.getMonth(),
displayedYear: initial.getFullYear(),
- selectYearMode: false
- }
+ selectYearMode: false,
+ };
}
render() {
-
- const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
+ const {
+ currentDate,
+ displayedMonth,
+ displayedYear,
+ selectYearMode,
+ } = this.state;
return (
<div>
- <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}>
-
+ <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}>
<div class="datePicker--titles">
- <h3 style={{
- color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
- }} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3>
- <h2 style={{
- color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
- }} onClick={this.displaySelectedMonth}>
- {dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
+ <h3
+ style={{
+ color: selectYearMode
+ ? "rgba(255,255,255,.87)"
+ : "rgba(255,255,255,.57)",
+ }}
+ onClick={this.toggleYearSelector}
+ >
+ {currentDate.getFullYear()}
+ </h3>
+ <h2
+ style={{
+ color: !selectYearMode
+ ? "rgba(255,255,255,.87)"
+ : "rgba(255,255,255,.57)",
+ }}
+ onClick={this.displaySelectedMonth}
+ >
+ {dayArr[currentDate.getDay()]},{" "}
+ {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
</h2>
</div>
- {!selectYearMode && <nav>
- <span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span>
- <h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4>
- <span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span>
- </nav>}
+ {!selectYearMode && (
+ <nav>
+ <span onClick={this.displayPrevMonth} class="icon">
+ <i
+ style={{ transform: "rotate(180deg)" }}
+ class="mdi mdi-forward"
+ />
+ </span>
+ <h4>
+ {monthArrShortFull[displayedMonth]} {displayedYear}
+ </h4>
+ <span onClick={this.displayNextMonth} class="icon">
+ <i class="mdi mdi-forward" />
+ </span>
+ </nav>
+ )}
<div class="datePicker--scroll">
-
- {!selectYearMode && <div class="datePicker--calendar" >
-
- <div class="datePicker--dayNames">
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)}
- </div>
-
- <div onClick={this.dayClicked} class="datePicker--days">
-
- {/*
+ {!selectYearMode && (
+ <div class="datePicker--calendar">
+ <div class="datePicker--dayNames">
+ {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
+ <span key={i}>{day}</span>
+ ))}
+ </div>
+
+ <div onClick={this.dayClicked} class="datePicker--days">
+ {/*
Loop through the calendar object returned by getDaysByMonth().
*/}
- {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear)
- .map(
- day => {
- let selected = false;
-
- if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString());
-
- return (<span key={day.day}
- class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')}
+ {this.getDaysByMonth(
+ this.state.displayedMonth,
+ this.state.displayedYear,
+ ).map((day) => {
+ let selected = false;
+
+ if (currentDate && day.date)
+ selected =
+ currentDate.toLocaleDateString() ===
+ day.date.toLocaleDateString();
+
+ return (
+ <span
+ key={day.day}
+ class={
+ (day.today ? "datePicker--today " : "") +
+ (selected ? "datePicker--selected" : "")
+ }
disabled={!day.date}
data-value={day.date}
>
{day.day}
- </span>)
- }
- )
- }
-
+ </span>
+ );
+ })}
+ </div>
</div>
-
- </div>}
-
- {selectYearMode && <div class="datePicker--selectYear">
- {(this.props.years || yearArr).map(year => (
- <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
- {year}
- </span>
- ))}
-
- </div>}
-
+ )}
+
+ {selectYearMode && (
+ <div class="datePicker--selectYear">
+ {(this.props.years || yearArr).map((year) => (
+ <span
+ key={year}
+ class={year === displayedYear ? "selected" : ""}
+ onClick={this.changeDisplayedYear}
+ >
+ {year}
+ </span>
+ ))}
+ </div>
+ )}
</div>
</div>
- <div class="datePicker--background" onClick={this.closeDatePicker} style={{
- display: this.props.opened ? 'block' : 'none',
- }}
+ <div
+ class="datePicker--background"
+ onClick={this.closeDatePicker}
+ style={{
+ display: this.props.opened ? "block" : "none",
+ }}
/>
-
</div>
- )
+ );
}
}
-
for (let i = 2010; i <= now.getFullYear() + 10; i++) {
yearArr.push(i);
}
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
index 275c80fa6..7f96cc15b 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
@@ -15,36 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, FunctionalComponent } from 'preact';
-import { useState } from 'preact/hooks';
-import { DurationPicker as TestedComponent } from './DurationPicker';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, FunctionalComponent } from "preact";
+import { useState } from "preact/hooks";
+import { DurationPicker as TestedComponent } from "./DurationPicker";
export default {
- title: 'Components/Picker/Duration',
+ title: "Components/Picker/Duration",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- goBack: { action: 'goBack' },
- }
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
};
-function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => <Component {...args} />
- r.args = props
- return r
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
}
export const Example = createExample(TestedComponent, {
- days: true, minutes: true, hours: true, seconds: true,
- value: 10000000
+ days: true,
+ minutes: true,
+ hours: true,
+ seconds: true,
+ value: 10000000,
});
export const WithState = () => {
- const [v,s] = useState<number>(1000000)
- return <TestedComponent value={v} onChange={s} days minutes hours seconds />
-}
+ const [v, s] = useState<number>(1000000);
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
+};
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
index 235a63e2d..8a1faf4d0 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -30,75 +30,123 @@ export interface Props {
seconds?: boolean;
days?: boolean;
onChange: (value: number) => void;
- value: number
+ value: number;
}
// inspiration taken from https://github.com/flurmbo/react-duration-picker
-export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode {
- const ss = 1000
- const ms = ss * 60
- const hs = ms * 60
- const ds = hs * 24
- const i18n = useTranslator()
-
- return <div class="rdp-picker">
- {days && <DurationColumn unit={i18n`days`} max={99}
- value={Math.floor(value / ds)}
- onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
- onChange={diff => onChange(value + diff * ds)}
- />}
- {hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
- value={Math.floor(value / hs) % 24}
- onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
- onChange={diff => onChange(value + diff * hs)}
- />}
- {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
- value={Math.floor(value / ms) % 60}
- onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
- onChange={diff => onChange(value + diff * ms)}
- />}
- {seconds && <DurationColumn unit={i18n`seconds`} max={59}
- value={Math.floor(value / ss) % 60}
- onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
- onChange={diff => onChange(value + diff * ss)}
- />}
- </div>
+export function DurationPicker({
+ days,
+ hours,
+ minutes,
+ seconds,
+ onChange,
+ value,
+}: Props): VNode {
+ const ss = 1000;
+ const ms = ss * 60;
+ const hs = ms * 60;
+ const ds = hs * 24;
+ const i18n = useTranslator();
+
+ return (
+ <div class="rdp-picker">
+ {days && (
+ <DurationColumn
+ unit={i18n`days`}
+ max={99}
+ value={Math.floor(value / ds)}
+ onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
+ onChange={(diff) => onChange(value + diff * ds)}
+ />
+ )}
+ {hours && (
+ <DurationColumn
+ unit={i18n`hours`}
+ max={23}
+ min={1}
+ value={Math.floor(value / hs) % 24}
+ onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
+ onChange={(diff) => onChange(value + diff * hs)}
+ />
+ )}
+ {minutes && (
+ <DurationColumn
+ unit={i18n`minutes`}
+ max={59}
+ min={1}
+ value={Math.floor(value / ms) % 60}
+ onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
+ onChange={(diff) => onChange(value + diff * ms)}
+ />
+ )}
+ {seconds && (
+ <DurationColumn
+ unit={i18n`seconds`}
+ max={59}
+ value={Math.floor(value / ss) % 60}
+ onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
+ onChange={(diff) => onChange(value + diff * ss)}
+ />
+ )}
+ </div>
+ );
}
interface ColProps {
- unit: string,
- min?: number,
- max: number,
- value: number,
+ unit: string;
+ min?: number;
+ max: number;
+ value: number;
onIncrease?: () => void;
onDecrease?: () => void;
onChange?: (diff: number) => void;
}
-function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) {
- const [value, handler] = useState<{v:string}>({
- v: toTwoDigitString(initial)
- })
-
- return <input
- value={value.v}
- onBlur={(e) => onChange(parseInt(value.v, 10))}
- onInput={(e) => {
- e.preventDefault()
- const n = Number.parseInt(e.currentTarget.value, 10);
- if (isNaN(n)) return handler({v:toTwoDigitString(initial)})
- return handler({v:toTwoDigitString(n)})
- }}
- style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} />
-}
+function InputNumber({
+ initial,
+ onChange,
+}: {
+ initial: number;
+ onChange: (n: number) => void;
+}) {
+ const [value, handler] = useState<{ v: string }>({
+ v: toTwoDigitString(initial),
+ });
-function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode {
+ return (
+ <input
+ value={value.v}
+ onBlur={(e) => onChange(parseInt(value.v, 10))}
+ onInput={(e) => {
+ e.preventDefault();
+ const n = Number.parseInt(e.currentTarget.value, 10);
+ if (isNaN(n)) return handler({ v: toTwoDigitString(initial) });
+ return handler({ v: toTwoDigitString(n) });
+ }}
+ style={{
+ width: 50,
+ border: "none",
+ fontSize: "inherit",
+ background: "inherit",
+ }}
+ />
+ );
+}
- const cellHeight = 35
+function DurationColumn({
+ unit,
+ min = 0,
+ max,
+ value,
+ onIncrease,
+ onDecrease,
+ onChange,
+}: ColProps): VNode {
+ const cellHeight = 35;
return (
<div class="rdp-column-container">
<div class="rdp-masked-div">
@@ -106,49 +154,58 @@ function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onC
<hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
<div class="rdp-column" style={{ top: 0 }}>
-
<div class="rdp-cell" key={value - 2}>
- {onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
- onClick={onDecrease}>
- <span class="icon">
- <i class="mdi mdi-chevron-up" />
- </span>
- </button>}
+ {onDecrease && (
+ <button
+ style={{ width: "100%", textAlign: "center", margin: 5 }}
+ onClick={onDecrease}
+ >
+ <span class="icon">
+ <i class="mdi mdi-chevron-up" />
+ </span>
+ </button>
+ )}
</div>
<div class="rdp-cell" key={value - 1}>
- {value > min ? toTwoDigitString(value - 1) : ''}
+ {value > min ? toTwoDigitString(value - 1) : ""}
</div>
<div class="rdp-cell rdp-center" key={value}>
- {onChange ?
- <InputNumber initial={value} onChange={(n) => onChange(n - value)} /> :
+ {onChange ? (
+ <InputNumber
+ initial={value}
+ onChange={(n) => onChange(n - value)}
+ />
+ ) : (
toTwoDigitString(value)
- }
+ )}
<div>{unit}</div>
</div>
<div class="rdp-cell" key={value + 1}>
- {value < max ? toTwoDigitString(value + 1) : ''}
+ {value < max ? toTwoDigitString(value + 1) : ""}
</div>
<div class="rdp-cell" key={value + 2}>
- {onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
- onClick={onIncrease}>
- <span class="icon">
- <i class="mdi mdi-chevron-down" />
- </span>
- </button>}
+ {onIncrease && (
+ <button
+ style={{ width: "100%", textAlign: "center", margin: 5 }}
+ onClick={onIncrease}
+ >
+ <span class="icon">
+ <i class="mdi mdi-chevron-down" />
+ </span>
+ </button>
+ )}
</div>
-
</div>
</div>
</div>
);
}
-
function toTwoDigitString(n: number) {
if (n < 10) {
return `0${n}`;
}
return `${n}`;
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/context/anastasis.ts b/packages/anastasis-webui/src/context/anastasis.ts
index e7f93ed43..c2e7b2a47 100644
--- a/packages/anastasis-webui/src/context/anastasis.ts
+++ b/packages/anastasis-webui/src/context/anastasis.ts
@@ -15,19 +15,19 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext, h, VNode } from 'preact';
-import { useContext } from 'preact/hooks';
-import { AnastasisReducerApi } from '../hooks/use-anastasis-reducer';
+import { createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
type Type = AnastasisReducerApi | undefined;
-const initial = undefined
+const initial = undefined;
-const Context = createContext<Type>(initial)
+const Context = createContext<Type>(initial);
interface Props {
value: AnastasisReducerApi;
@@ -36,6 +36,6 @@ interface Props {
export const AnastasisProvider = ({ value, children }: Props): VNode => {
return h(Context.Provider, { value, children });
-}
+};
-export const useAnastasisContext = (): Type => useContext(Context); \ No newline at end of file
+export const useAnastasisContext = (): Type => useContext(Context);
diff --git a/packages/anastasis-webui/src/context/translation.ts b/packages/anastasis-webui/src/context/translation.ts
index 5ceb5d428..a47864d75 100644
--- a/packages/anastasis-webui/src/context/translation.ts
+++ b/packages/anastasis-webui/src/context/translation.ts
@@ -15,13 +15,13 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext, h, VNode } from 'preact'
-import { useContext, useEffect } from 'preact/hooks'
-import { useLang } from '../hooks'
+import { createContext, h, VNode } from "preact";
+import { useContext, useEffect } from "preact/hooks";
+import { useLang } from "../hooks";
import * as jedLib from "jed";
import { strings } from "../i18n/strings";
@@ -31,13 +31,13 @@ interface Type {
changeLanguage: (l: string) => void;
}
const initial = {
- lang: 'en',
+ lang: "en",
handler: null,
changeLanguage: () => {
// do not change anything
- }
-}
-const Context = createContext<Type>(initial)
+ },
+};
+const Context = createContext<Type>(initial);
interface Props {
initial?: string;
@@ -45,15 +45,22 @@ interface Props {
forceLang?: string;
}
-export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
- const [lang, changeLanguage] = useLang(initial)
+export const TranslationProvider = ({
+ initial,
+ children,
+ forceLang,
+}: Props): VNode => {
+ const [lang, changeLanguage] = useLang(initial);
useEffect(() => {
if (forceLang) {
- changeLanguage(forceLang)
+ changeLanguage(forceLang);
}
- })
- const handler = new jedLib.Jed(strings[lang] || strings['en']);
- return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
-}
+ });
+ const handler = new jedLib.Jed(strings[lang] || strings["en"]);
+ return h(Context.Provider, {
+ value: { lang, handler, changeLanguage },
+ children,
+ });
+};
-export const useTranslationContext = (): Type => useContext(Context); \ No newline at end of file
+export const useTranslationContext = (): Type => useContext(Context);
diff --git a/packages/anastasis-webui/src/declaration.d.ts b/packages/anastasis-webui/src/declaration.d.ts
index 2c4b7cb3a..00b3d41d5 100644
--- a/packages/anastasis-webui/src/declaration.d.ts
+++ b/packages/anastasis-webui/src/declaration.d.ts
@@ -1,20 +1,20 @@
declare module "*.css" {
- const mapping: Record<string, string>;
- export default mapping;
+ const mapping: Record<string, string>;
+ export default mapping;
}
-declare module '*.svg' {
- const content: any;
- export default content;
+declare module "*.svg" {
+ const content: any;
+ export default content;
}
-declare module '*.jpeg' {
- const content: any;
- export default content;
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
}
-declare module '*.png' {
- const content: any;
- export default content;
+declare module "*.png" {
+ const content: any;
+ export default content;
}
-declare module 'jed' {
- const x: any;
- export = x;
+declare module "jed" {
+ const x: any;
+ export = x;
}
diff --git a/packages/anastasis-webui/src/hooks/async.ts b/packages/anastasis-webui/src/hooks/async.ts
index ea3ff6acf..0fc197554 100644
--- a/packages/anastasis-webui/src/hooks/async.ts
+++ b/packages/anastasis-webui/src/hooks/async.ts
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
// import { cancelPendingRequest } from "./backend";
@@ -34,36 +34,39 @@ export interface AsyncOperationApi<T> {
error: string | undefined;
}
-export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> {
+export function useAsync<T>(
+ fn?: (...args: any) => Promise<T>,
+ { slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
+): AsyncOperationApi<T> {
const [data, setData] = useState<T | undefined>(undefined);
const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(undefined);
- const [isSlow, setSlow] = useState(false)
+ const [isSlow, setSlow] = useState(false);
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
const handler = setTimeout(() => {
- setSlow(true)
- }, tooLong)
+ setSlow(true);
+ }, tooLong);
try {
- console.log("calling async", args)
+ console.log("calling async", args);
const result = await fn(...args);
- console.log("async back", result)
+ console.log("async back", result);
setData(result);
} catch (error) {
setError(error);
}
setLoading(false);
- setSlow(false)
- clearTimeout(handler)
+ setSlow(false);
+ clearTimeout(handler);
};
function cancel() {
// cancelPendingRequest()
setLoading(false);
- setSlow(false)
+ setSlow(false);
}
return {
@@ -72,6 +75,6 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
data,
isSlow,
isLoading,
- error
+ error,
};
}
diff --git a/packages/anastasis-webui/src/hooks/index.ts b/packages/anastasis-webui/src/hooks/index.ts
index 15df4f154..9a1b50a11 100644
--- a/packages/anastasis-webui/src/hooks/index.ts
+++ b/packages/anastasis-webui/src/hooks/index.ts
@@ -15,81 +15,110 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { StateUpdater, useState } from "preact/hooks";
-export type ValueOrFunction<T> = T | ((p: T) => T)
-
+export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => {
- const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
- return rootPath
-}
-
-export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
- const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
+ const rootPath =
+ typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : "/";
+ return rootPath;
+};
+
+export function useBackendURL(
+ url?: string,
+): [string, boolean, StateUpdater<string>, () => void] {
+ const [value, setter] = useNotNullLocalStorage(
+ "backend-url",
+ url || calculateRootPath(),
+ );
+ const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog('yes')
- return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
- }
+ setTriedToLog("yes");
+ return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
+ };
const resetBackend = () => {
- setTriedToLog(undefined)
- }
- return [value, !!triedToLog, checkedSetter, resetBackend]
+ setTriedToLog(undefined);
+ };
+ return [value, !!triedToLog, checkedSetter, resetBackend];
}
-export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
- return useLocalStorage('backend-token')
+export function useBackendDefaultToken(): [
+ string | undefined,
+ StateUpdater<string | undefined>,
+] {
+ return useLocalStorage("backend-token");
}
-export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
- const [token, setToken] = useLocalStorage(`backend-token-${id}`)
- const [defaultToken, defaultSetToken] = useBackendDefaultToken()
+export function useBackendInstanceToken(
+ id: string,
+): [string | undefined, StateUpdater<string | undefined>] {
+ const [token, setToken] = useLocalStorage(`backend-token-${id}`);
+ const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token
- if (id === 'default') {
- return [defaultToken, defaultSetToken]
+ if (id === "default") {
+ return [defaultToken, defaultSetToken];
}
- return [token, setToken]
+ return [token, setToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {
- const browserLang = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined;
- const defaultLang = (browserLang || initial || 'en').substring(0, 2)
- return useNotNullLocalStorage('lang-preference', defaultLang)
+ const browserLang =
+ typeof window !== "undefined"
+ ? navigator.language || (navigator as any).userLanguage
+ : undefined;
+ const defaultLang = (browserLang || initial || "en").substring(0, 2);
+ return useNotNullLocalStorage("lang-preference", defaultLang);
}
-export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
- const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
+export function useLocalStorage(
+ key: string,
+ initialValue?: string,
+): [string | undefined, StateUpdater<string | undefined>] {
+ const [storedValue, setStoredValue] = useState<string | undefined>(():
+ | string
+ | undefined => {
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
});
- const setValue = (value?: string | ((val?: string) => string | undefined)) => {
- setStoredValue(p => {
- const toStore = value instanceof Function ? value(p) : value
+ const setValue = (
+ value?: string | ((val?: string) => string | undefined),
+ ) => {
+ setStoredValue((p) => {
+ const toStore = value instanceof Function ? value(p) : value;
if (typeof window !== "undefined") {
if (!toStore) {
- window.localStorage.removeItem(key)
+ window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, toStore);
}
}
- return toStore
- })
+ return toStore;
+ });
};
return [storedValue, setValue];
}
-export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
+export function useNotNullLocalStorage(
+ key: string,
+ initialValue: string,
+): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
@@ -97,7 +126,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
if (!valueToStore) {
- window.localStorage.removeItem(key)
+ window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, valueToStore);
}
@@ -106,5 +135,3 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
return [storedValue, setValue];
}
-
-
diff --git a/packages/anastasis-webui/src/i18n/index.tsx b/packages/anastasis-webui/src/i18n/index.tsx
index 63c8e1934..6e2c4e79a 100644
--- a/packages/anastasis-webui/src/i18n/index.tsx
+++ b/packages/anastasis-webui/src/i18n/index.tsx
@@ -27,23 +27,25 @@ import { useTranslationContext } from "../context/translation";
export function useTranslator() {
const ctx = useTranslationContext();
- const jed = ctx.handler
- return function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
+ const jed = ctx.handler;
+ return function str(
+ stringSeq: TemplateStringsArray,
+ ...values: any[]
+ ): string {
const s = toI18nString(stringSeq);
- if (!s) return s
+ if (!s) return s;
const tr = jed
.translate(s)
.ifPlural(1, s)
.fetch(...values);
return tr;
- }
+ };
}
-
/**
* Convert template strings to a msgid
*/
- function toI18nString(stringSeq: ReadonlyArray<string>): string {
+function toI18nString(stringSeq: ReadonlyArray<string>): string {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
@@ -54,7 +56,6 @@ export function useTranslator() {
return s;
}
-
interface TranslateSwitchProps {
target: number;
children: ComponentChildren;
@@ -110,7 +111,7 @@ function getTranslatedChildren(
// Text
result.push(tr[i]);
} else {
- const childIdx = Number.parseInt(tr[i],10) - 1;
+ const childIdx = Number.parseInt(tr[i], 10) - 1;
result.push(placeholderChildren[childIdx]);
}
}
@@ -131,9 +132,9 @@ function getTranslatedChildren(
*/
export function Translate({ children }: TranslateProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation: string = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children)
+ const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}
@@ -154,14 +155,16 @@ export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
let plural: VNode<TranslationPluralProps> | undefined;
// const children = this.props.children;
if (children) {
- (children instanceof Array ? children : [children]).forEach((child: any) => {
- if (child.type === TranslatePlural) {
- plural = child;
- }
- if (child.type === TranslateSingular) {
- singular = child;
- }
- });
+ (children instanceof Array ? children : [children]).forEach(
+ (child: any) => {
+ if (child.type === TranslatePlural) {
+ plural = child;
+ }
+ if (child.type === TranslateSingular) {
+ singular = child;
+ }
+ },
+ );
}
if (!singular || !plural) {
console.error("translation not found");
@@ -182,9 +185,12 @@ interface TranslationPluralProps {
/**
* See [[TranslateSwitch]].
*/
-export function TranslatePlural({ children, target }: TranslationPluralProps): VNode {
+export function TranslatePlural({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
@@ -193,11 +199,13 @@ export function TranslatePlural({ children, target }: TranslationPluralProps): V
/**
* See [[TranslateSwitch]].
*/
-export function TranslateSingular({ children, target }: TranslationPluralProps): VNode {
+export function TranslateSingular({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, target);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
-
}
diff --git a/packages/anastasis-webui/src/i18n/strings.ts b/packages/anastasis-webui/src/i18n/strings.ts
index b4f376ce0..d12e63e88 100644
--- a/packages/anastasis-webui/src/i18n/strings.ts
+++ b/packages/anastasis-webui/src/i18n/strings.ts
@@ -15,30 +15,30 @@
*/
/*eslint quote-props: ["error", "consistent"]*/
-export const strings: {[s: string]: any} = {};
+export const strings: { [s: string]: any } = {};
-strings['de'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
+strings["de"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
"": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
},
- }
- }
+ },
+ },
};
-strings['en'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
+strings["en"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
"": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
},
- }
- }
+ },
+ },
};
diff --git a/packages/anastasis-webui/src/index.ts b/packages/anastasis-webui/src/index.ts
index e78b9c194..4bd7b28f3 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -1,4 +1,4 @@
-import App from './components/app';
-import './scss/main.scss';
+import App from "./components/app";
+import "./scss/main.scss";
export default App;
diff --git a/packages/anastasis-webui/src/manifest.json b/packages/anastasis-webui/src/manifest.json
index 6b44a2b31..2752dad77 100644
--- a/packages/anastasis-webui/src/manifest.json
+++ b/packages/anastasis-webui/src/manifest.json
@@ -18,4 +18,4 @@
"sizes": "512x512"
}
]
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
index 43807fefe..9b067127d 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,24 +15,23 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { AddingProviderScreen as TestedComponent } from './AddingProviderScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen";
export default {
- title: 'Pages/backup/AddingProviderScreen',
+ title: "Pages/ManageProvider",
component: TestedComponent,
args: {
- order: 4,
+ order: 1,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
@@ -41,10 +39,31 @@ export const NewProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState);
-export const NewSMSProvider = createExample(TestedComponent, {
+export const NewProviderWithoutProviderList = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
-} as ReducerState, { providerType: 'sms'});
+ authentication_providers: {},
+} as ReducerState);
-export const NewIBANProvider = createExample(TestedComponent, {
- ...reducerStatesExample.authEditing,
-} as ReducerState, { providerType: 'iban' });
+export const NewVideoProvider = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.authEditing,
+ } as ReducerState,
+ { providerType: "video" },
+);
+
+export const NewSmsProvider = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.authEditing,
+ } as ReducerState,
+ { providerType: "sms" },
+);
+
+export const NewIBANProvider = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.authEditing,
+ } as ReducerState,
+ { providerType: "iban" },
+);
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
index 9c83da49e..96b38e92d 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
@@ -1,101 +1,260 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
import { TextInput } from "../../components/fields/TextInput";
+import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
import { AnastasisClientFrame } from "./index";
interface Props {
providerType?: KnownAuthMethods;
- cancel: () => void;
+ onCancel: () => void;
}
-export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
+
+async function testProvider(
+ url: string,
+ expectedMethodType?: string,
+): Promise<void> {
+ try {
+ const response = await fetch(new URL("config", url).href);
+ const json = await response.json().catch((d) => ({}));
+ if (!("methods" in json) || !Array.isArray(json.methods)) {
+ throw Error(
+ "This provider doesn't have authentication method. Check the provider URL",
+ );
+ }
+ console.log("expected", expectedMethodType);
+ if (!expectedMethodType) {
+ return;
+ }
+ let found = false;
+ for (let i = 0; i < json.methods.length && !found; i++) {
+ found = json.methods[i].type === expectedMethodType;
+ }
+ if (!found) {
+ throw Error(
+ `This provider does not support authentication method ${expectedMethodType}`,
+ );
+ }
+ return;
+ } catch (e) {
+ console.log("error", e);
+ const error =
+ e instanceof Error
+ ? Error(
+ `There was an error testing this provider, try another one. ${e.message}`,
+ )
+ : Error(`There was an error testing this provider, try another one.`);
+ throw error;
+ }
+}
+
+export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
+ const reducer = useAnastasisContext();
+
const [providerURL, setProviderURL] = useState("");
- const [error, setError] = useState<string | undefined>()
- const providerLabel = providerType ? authMethods[providerType].label : undefined
+ const [error, setError] = useState<string | undefined>();
+ const [testing, setTesting] = useState(false);
+ const providerLabel = providerType
+ ? authMethods[providerType].label
+ : undefined;
- function testProvider(): void {
- setError(undefined)
+ //FIXME: move this timeout logic into a hook
+ const timeout = useRef<number | undefined>(undefined);
+ useEffect(() => {
+ if (timeout) window.clearTimeout(timeout.current);
+ timeout.current = window.setTimeout(async () => {
+ const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
+ if (!providerURL || authProviders.includes(url)) return;
+ try {
+ setTesting(true);
+ await testProvider(url, providerType);
+ // this is use as tested but everything when ok
+ // undefined will mean that the field is not dirty
+ setError("");
+ } catch (e) {
+ console.log("tuvieja", e);
+ if (e instanceof Error) setError(e.message);
+ }
+ setTesting(false);
+ }, 200);
+ }, [providerURL, reducer]);
- fetch(`${providerURL}/config`)
- .then(r => r.json().catch(d => ({})))
- .then(r => {
- if (!("methods" in r) || !Array.isArray(r.methods)) {
- setError("This provider doesn't have authentication method. Check the provider URL")
- return;
- }
- if (!providerLabel) {
- setError("")
- return
- }
- let found = false
- for (let i = 0; i < r.methods.length && !found; i++) {
- found = r.methods[i].type !== providerType
- }
- if (!found) {
- setError(`This provider does not support authentication method ${providerLabel}`)
- }
- })
- .catch(e => {
- setError(`There was an error testing this provider, try another one. ${e.message}`)
- })
+ if (!reducer) {
+ return <div>no reducer in context</div>;
+ }
+ if (
+ !reducer.currentReducerState ||
+ !("authentication_providers" in reducer.currentReducerState)
+ ) {
+ return <div>invalid state</div>;
}
- function addProvider(): void {
- // addAuthMethod({
- // authentication_method: {
- // type: "sms",
- // instructions: `SMS to ${providerURL}`,
- // challenge: encodeCrock(stringToBytes(providerURL)),
- // },
- // });
+
+ async function addProvider(provider_url: string): Promise<void> {
+ await reducer?.transition("add_provider", { provider_url });
+ onCancel();
+ }
+ function deleteProvider(provider_url: string): void {
+ reducer?.transition("delete_provider", { provider_url });
}
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- inputRef.current?.focus();
- }, []);
- let errors = !providerURL ? 'Add provider URL' : undefined
+ const allAuthProviders =
+ reducer.currentReducerState.authentication_providers || {};
+ const authProviders = Object.keys(allAuthProviders).filter((provUrl) => {
+ const p = allAuthProviders[provUrl];
+ if (!providerLabel) {
+ return p && "currency" in p;
+ } else {
+ return (
+ p &&
+ "currency" in p &&
+ p.methods.findIndex((m) => m.type === providerType) !== -1
+ );
+ }
+ });
+
+ let errors = !providerURL ? "Add provider URL" : undefined;
+ let url: string | undefined;
try {
- new URL(providerURL)
+ url = new URL("", providerURL).href;
} catch {
- errors = 'Check the URL'
+ errors = "Check the URL";
}
if (!!error && !errors) {
- errors = error
+ errors = error;
+ }
+ if (!errors && authProviders.includes(url!)) {
+ errors = "That provider is already known";
}
return (
- <AnastasisClientFrame hideNav
- title={!providerLabel ? `Backup: Adding a provider` : `Backup: Adding a ${providerLabel} provider`}
- hideNext={errors}>
+ <AnastasisClientFrame
+ hideNav
+ title="Backup: Manage providers"
+ hideNext={errors}
+ >
<div>
- <p>
- Add a provider url {errors}
- </p>
+ {!providerLabel ? (
+ <p>Add a provider url</p>
+ ) : (
+ <p>Add a provider url for a {providerLabel} service</p>
+ )}
<div class="container">
<TextInput
label="Provider URL"
placeholder="https://provider.com"
grabFocus
- bind={[providerURL, setProviderURL]} />
- </div>
- {!!error && <p class="block has-text-danger">{error}</p>}
- {error === "" && <p class="block has-text-success">This provider worked!</p>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={testProvider}>TEST</button>
+ error={errors}
+ bind={[providerURL, setProviderURL]}
+ />
</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <p class="block">Example: https://kudos.demo.anastasis.lu</p>
+ {testing && <p class="has-text-info">Testing</p>}
+
+ <div
+ class="block"
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addProvider}>Add</button>
+ <button
+ class="button is-info"
+ disabled={error !== "" || testing}
+ onClick={() => addProvider(url!)}
+ >
+ Add
+ </button>
</span>
</div>
+
+ {authProviders.length > 0 ? (
+ !providerLabel ? (
+ <p class="subtitle">Current providers</p>
+ ) : (
+ <p class="subtitle">
+ Current providers for {providerLabel} service
+ </p>
+ )
+ ) : !providerLabel ? (
+ <p class="subtitle">No known providers, add one.</p>
+ ) : (
+ <p class="subtitle">No known providers for {providerLabel} service</p>
+ )}
+
+ {authProviders.map((k) => {
+ const p = allAuthProviders[k] as AuthenticationProviderStatusOk;
+ return <TableRow url={k} info={p} onDelete={deleteProvider} />;
+ })}
</div>
</AnastasisClientFrame>
);
}
+function TableRow({
+ url,
+ info,
+ onDelete,
+}: {
+ onDelete: (s: string) => void;
+ url: string;
+ info: AuthenticationProviderStatusOk;
+}) {
+ const [status, setStatus] = useState("checking");
+ useEffect(function () {
+ testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url)
+ .then(function () {
+ setStatus("responding");
+ })
+ .catch(function () {
+ setStatus("failed to contact");
+ });
+ });
+ return (
+ <div
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div>
+ <div class="subtitle">{url}</div>
+ <dl>
+ <dt>
+ <b>Business Name</b>
+ </dt>
+ <dd>{info.business_name}</dd>
+ <dt>
+ <b>Supported methods</b>
+ </dt>
+ <dd>{info.methods.map((m) => m.type).join(",")}</dd>
+ <dt>
+ <b>Maximum storage</b>
+ </dt>
+ <dd>{info.storage_limit_in_megabytes} Mb</dd>
+ <dt>
+ <b>Status</b>
+ </dt>
+ <dd>{status}</dd>
+ </dl>
+ </div>
+ <div
+ class="block"
+ style={{
+ marginTop: "auto",
+ marginBottom: "auto",
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "column",
+ }}
+ >
+ <button class="button is-danger" onClick={() => onDelete(url)}>
+ Remove
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
index 549686616..d48e94403 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,76 +15,83 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen";
export default {
- title: 'Pages/AttributeEntryScreen',
+ title: "Pages/PersonalInformation",
component: TestedComponent,
args: {
- order: 4,
+ order: 3,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const Backup = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
- required_attributes: [{
- name: 'first name',
- label: 'first',
- type: 'string',
- uuid: 'asdasdsa1',
- widget: 'wid',
- }, {
- name: 'last name',
- label: 'second',
- type: 'string',
- uuid: 'asdasdsa2',
- widget: 'wid',
- }, {
- name: 'birthdate',
- label: 'birthdate',
- type: 'date',
- uuid: 'asdasdsa3',
- widget: 'calendar',
- }]
+ required_attributes: [
+ {
+ name: "first name",
+ label: "first",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ },
+ {
+ name: "last name",
+ label: "second",
+ type: "string",
+ uuid: "asdasdsa2",
+ widget: "wid",
+ },
+ {
+ name: "birthdate",
+ label: "birthdate",
+ type: "date",
+ uuid: "asdasdsa3",
+ widget: "calendar",
+ },
+ ],
} as ReducerState);
export const Recovery = createExample(TestedComponent, {
...reducerStatesExample.recoveryAttributeEditing,
- required_attributes: [{
- name: 'first',
- label: 'first',
- type: 'string',
- uuid: 'asdasdsa1',
- widget: 'wid',
- }, {
- name: 'pepe',
- label: 'second',
- type: 'string',
- uuid: 'asdasdsa2',
- widget: 'wid',
- }, {
- name: 'pepe2',
- label: 'third',
- type: 'date',
- uuid: 'asdasdsa3',
- widget: 'calendar',
- }]
+ required_attributes: [
+ {
+ name: "first",
+ label: "first",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ },
+ {
+ name: "pepe",
+ label: "second",
+ type: "string",
+ uuid: "asdasdsa2",
+ widget: "wid",
+ },
+ {
+ name: "pepe2",
+ label: "third",
+ type: "date",
+ uuid: "asdasdsa3",
+ widget: "calendar",
+ },
+ ],
} as ReducerState);
export const WithNoRequiredAttribute = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
- required_attributes: undefined
+ required_attributes: undefined,
} as ReducerState);
const allWidgets = [
@@ -108,23 +114,22 @@ const allWidgets = [
"anastasis_gtk_ia_tax_de",
"anastasis_gtk_xx_prime",
"anastasis_gtk_xx_square",
-]
+];
function typeForWidget(name: string): string {
- if (["anastasis_gtk_xx_prime",
- "anastasis_gtk_xx_square",
- ].includes(name)) return "number";
- if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date"
+ if (["anastasis_gtk_xx_prime", "anastasis_gtk_xx_square"].includes(name))
+ return "number";
+ if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date";
return "string";
}
export const WithAllPosibleWidget = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
- required_attributes: allWidgets.map(w => ({
+ required_attributes: allWidgets.map((w) => ({
name: w,
label: `widget: ${w}`,
type: typeForWidget(w),
uuid: `uuid-${w}`,
- widget: w
- }))
+ widget: w,
+ })),
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
index f86994c97..1b50779e0 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -1,33 +1,42 @@
-/* eslint-disable @typescript-eslint/camelcase */
import { UserAttributeSpec, validators } from "anastasis-core";
-import { Fragment, h, VNode } from "preact";
+import { isAfter, parse } from "date-fns";
+import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { DateInput } from "../../components/fields/DateInput";
+import { PhoneNumberInput } from "../../components/fields/NumberInput";
+import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
+import { ConfirmModal } from "./ConfirmModal";
import { AnastasisClientFrame, withProcessLabel } from "./index";
-import { TextInput } from "../../components/fields/TextInput";
-import { DateInput } from "../../components/fields/DateInput";
-import { NumberInput } from "../../components/fields/NumberInput";
-import { isAfter, parse } from "date-fns";
export function AttributeEntryScreen(): VNode {
- const reducer = useAnastasisContext()
- const state = reducer?.currentReducerState
- const currentIdentityAttributes = state && "identity_attributes" in state ? (state.identity_attributes || {}) : {}
- const [attrs, setAttrs] = useState<Record<string, string>>(currentIdentityAttributes);
+ const reducer = useAnastasisContext();
+ const state = reducer?.currentReducerState;
+ const currentIdentityAttributes =
+ state && "identity_attributes" in state
+ ? state.identity_attributes || {}
+ : {};
+ const [attrs, setAttrs] = useState<Record<string, string>>(
+ currentIdentityAttributes,
+ );
+ const [askUserIfSure, setAskUserIfSure] = useState(false);
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ !("required_attributes" in reducer.currentReducerState)
+ ) {
+ return <div>invalid state</div>;
}
- const reqAttr = reducer.currentReducerState.required_attributes || []
+ const reqAttr = reducer.currentReducerState.required_attributes || [];
let hasErrors = false;
const fieldList: VNode[] = reqAttr.map((spec, i: number) => {
- const value = attrs[spec.name]
- const error = checkIfValid(value, spec)
- hasErrors = hasErrors || error !== undefined
+ const value = attrs[spec.name];
+ const error = checkIfValid(value, spec);
+ hasErrors = hasErrors || error !== undefined;
return (
<AttributeEntryField
key={i}
@@ -35,23 +44,42 @@ export function AttributeEntryScreen(): VNode {
setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
spec={spec}
errorMessage={error}
- value={value} />
+ onConfirm={() => {
+ if (!hasErrors) {
+ setAskUserIfSure(true)
+ }
+ }}
+ value={value}
+ />
);
- })
+ });
return (
<AnastasisClientFrame
title={withProcessLabel(reducer, "Who are you?")}
hideNext={hasErrors ? "Complete the form." : undefined}
- onNext={() => reducer.transition("enter_user_attributes", {
- identity_attributes: attrs,
- })}
+ onNext={async () => setAskUserIfSure(true) }
>
- <div class="columns" style={{ maxWidth: 'unset' }}>
- <div class="column is-half">
- {fieldList}
- </div>
- <div class="column is-is-half" >
+ {askUserIfSure ? (
+ <ConfirmModal
+ active
+ onCancel={() => setAskUserIfSure(false)}
+ description="The values in the form must be correct"
+ label="I am sure"
+ cancelLabel="Wait, I want to check"
+ onConfirm={() => reducer.transition("enter_user_attributes", {
+ identity_attributes: attrs,
+ }).then(() => setAskUserIfSure(false) )}
+ >
+ You personal information is used to define the location where your
+ secret will be safely stored. If you forget what you have entered or
+ if there is a misspell you will be unable to recover your secret.
+ </ConfirmModal>
+ ) : null}
+
+ <div class="columns" style={{ maxWidth: "unset" }}>
+ <div class="column">{fieldList}</div>
+ <div class="column">
<p>This personal information will help to locate your secret.</p>
<h1 class="title">This stays private</h1>
<p>The information you have entered here:</p>
@@ -62,9 +90,12 @@ export function AttributeEntryScreen(): VNode {
</span>
Will be hashed, and therefore unreadable
</li>
- <li><span class="icon is-right">
- <i class="mdi mdi-circle-small" />
- </span>The non-hashed version is not shared</li>
+ <li>
+ <span class="icon is-right">
+ <i class="mdi mdi-circle-small" />
+ </span>
+ The non-hashed version is not shared
+ </li>
</ul>
</div>
</div>
@@ -78,39 +109,43 @@ interface AttributeEntryFieldProps {
setValue: (newValue: string) => void;
spec: UserAttributeSpec;
errorMessage: string | undefined;
+ onConfirm: () => void;
}
-const possibleBirthdayYear: Array<number> = []
+const possibleBirthdayYear: Array<number> = [];
for (let i = 0; i < 100; i++) {
- possibleBirthdayYear.push(2020 - i)
+ possibleBirthdayYear.push(2020 - i);
}
function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
-
return (
<div>
- {props.spec.type === 'date' &&
+ {props.spec.type === "date" &&
<DateInput
grabFocus={props.isFirst}
label={props.spec.label}
years={possibleBirthdayYear}
+ onConfirm={props.onConfirm}
error={props.errorMessage}
bind={[props.value, props.setValue]}
- />}
+ />
+ }
{props.spec.type === 'number' &&
- <NumberInput
+ <PhoneNumberInput
grabFocus={props.isFirst}
label={props.spec.label}
+ onConfirm={props.onConfirm}
error={props.errorMessage}
bind={[props.value, props.setValue]}
/>
}
- {props.spec.type === 'string' &&
+ {props.spec.type === "string" && (
<TextInput
grabFocus={props.isFirst}
label={props.spec.label}
+ onConfirm={props.onConfirm}
error={props.errorMessage}
bind={[props.value, props.setValue]}
/>
- }
+ )}
<div class="block">
This stays private
<span class="icon is-right">
@@ -120,40 +155,43 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
</div>
);
}
-const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
-
+const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/;
-function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
- const pattern = spec['validation-regex']
+function checkIfValid(
+ value: string,
+ spec: UserAttributeSpec,
+): string | undefined {
+ const pattern = spec["validation-regex"];
if (pattern) {
- const re = new RegExp(pattern)
- if (!re.test(value)) return 'The value is invalid'
+ const re = new RegExp(pattern);
+ if (!re.test(value)) return "The value is invalid";
}
- const logic = spec['validation-logic']
+ const logic = spec["validation-logic"];
if (logic) {
const func = (validators as any)[logic];
- if (func && typeof func === 'function' && !func(value)) return 'Please check the value'
+ if (func && typeof func === "function" && !func(value))
+ return "Please check the value";
}
- const optional = spec.optional
+ const optional = spec.optional;
if (!optional && !value) {
- return 'This value is required'
+ return "This value is required";
}
if ("date" === spec.type) {
if (!YEAR_REGEX.test(value)) {
- return "The date doesn't follow the format"
+ return "The date doesn't follow the format";
}
try {
- const v = parse(value, 'yyyy-MM-dd', new Date());
+ const v = parse(value, "yyyy-MM-dd", new Date());
if (Number.isNaN(v.getTime())) {
- return "Some numeric values seems out of range for a date"
+ return "Some numeric values seems out of range for a date";
}
if ("birthdate" === spec.name && isAfter(v, new Date())) {
- return "A birthdate cannot be in the future"
+ return "A birthdate cannot be in the future";
}
} catch (e) {
- return "Could not parse the date"
+ return "Could not parse the date";
}
}
- return undefined
+ return undefined;
}
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index 5077c3eb0..8acf1c8c8 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,78 +15,84 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen";
export default {
- title: 'Pages/backup/AuthenticationEditorScreen',
+ title: "Pages/backup/AuthorizationMethod",
component: TestedComponent,
args: {
- order: 5,
+ order: 4,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.authEditing);
+export const InitialState = createExample(
+ TestedComponent,
+ reducerStatesExample.authEditing,
+);
export const OneAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
- authentication_methods: [{
- type: 'question',
- instructions: 'what time is it?',
- challenge: 'asd',
- }]
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
} as ReducerState);
-
export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
- authentication_methods: [{
- type: 'question',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'question',
- instructions: 'what time is it?',
- challenge: 'qwe',
- },{
- type: 'sms',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- }]
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "qwe",
+ },
+ {
+ type: "sms",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
} as ReducerState);
export const NoAuthMethodProvided = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
authentication_providers: {},
- authentication_methods: []
+ authentication_methods: [],
} as ReducerState);
-
- // type: string;
- // instructions: string;
- // challenge: string;
- // mime_type?: string;
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index 93ca81194..91195971d 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -1,61 +1,85 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import { AuthMethod } from "anastasis-core";
+import { AuthMethod, ReducerStateBackup } from "anastasis-core";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
-import { authMethods, KnownAuthMethods } from "./authMethod";
+import { AddingProviderScreen } from "./AddingProviderScreen";
+import {
+ authMethods,
+ AuthMethodSetupProps,
+ AuthMethodWithRemove,
+ isKnownAuthMethods,
+ KnownAuthMethods,
+} from "./authMethod";
+import { ConfirmModal } from "./ConfirmModal";
import { AnastasisClientFrame } from "./index";
-
-
-const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
+const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
export function AuthenticationEditorScreen(): VNode {
- const [noProvidersAck, setNoProvidersAck] = useState(false)
- const [selectedMethod, setSelectedMethod] = useState<KnownAuthMethods | undefined>(undefined);
- const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
+ const [noProvidersAck, setNoProvidersAck] = useState(false);
+ const [selectedMethod, setSelectedMethod] = useState<
+ KnownAuthMethods | undefined
+ >(undefined);
+ const [tooFewAuths, setTooFewAuths] = useState(false);
+ const [manageProvider, setManageProvider] = useState<string | undefined>(
+ undefined,
+ );
- const reducer = useAnastasisContext()
+ // const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
- const haveMethodsConfigured = configuredAuthMethods.length > 0;
+ const configuredAuthMethods: AuthMethod[] =
+ reducer.currentReducerState.authentication_methods ?? [];
function removeByIndex(index: number): void {
- if (reducer) reducer.transition("delete_authentication", {
- authentication_method: index,
- })
+ if (reducer)
+ reducer.transition("delete_authentication", {
+ authentication_method: index,
+ });
}
- const camByType: { [s: string]: AuthMethodWithRemove[] } = {}
+ 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)
+ remove: () => removeByIndex(index),
+ };
+ const prevValue = camByType[cam.type] || [];
+ prevValue.push(cam);
camByType[cam.type] = prevValue;
}
-
const providers = reducer.currentReducerState.authentication_providers!;
const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) {
const p = providers[provKey];
- if ("http_status" in p && (!("error_code" in p)) && p.methods) {
+ if ("http_status" in p && !("error_code" in p) && p.methods) {
for (const meth of p.methods) {
authAvailableSet.add(meth.type);
}
}
}
+ if (manageProvider !== undefined) {
+ return (
+ <AddingProviderScreen
+ onCancel={() => setManageProvider(undefined)}
+ providerType={
+ isKnownAuthMethods(manageProvider) ? manageProvider : undefined
+ }
+ />
+ );
+ }
+
if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => {
@@ -63,120 +87,154 @@ export function AuthenticationEditorScreen(): VNode {
setSelectedMethod(undefined);
};
- const AuthSetup = authMethods[selectedMethod].screen ?? AuthMethodNotImplemented;
- return (<Fragment>
- <AuthSetup
- cancel={cancel}
- configured={camByType[selectedMethod] || []}
- addAuthMethod={addMethod}
- method={selectedMethod} />
-
- {!authAvailableSet.has(selectedMethod) && <ConfirmModal active
- onCancel={cancel} description="No providers founds" label="Add a provider manually"
- onConfirm={() => {
- null
- }}
- >
- We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
- To add a provider you must know the provider URL (e.g. https://provider.com)
- <p>
- <a>More about cloud providers</a>
- </p>
- </ConfirmModal>}
-
- </Fragment>
+ const AuthSetup =
+ authMethods[selectedMethod].setup ?? AuthMethodNotImplemented;
+ return (
+ <Fragment>
+ <AuthSetup
+ cancel={cancel}
+ configured={camByType[selectedMethod] || []}
+ addAuthMethod={addMethod}
+ method={selectedMethod}
+ />
+
+ {!authAvailableSet.has(selectedMethod) && (
+ <ConfirmModal
+ active
+ onCancel={cancel}
+ description="No providers founds"
+ label="Add a provider manually"
+ onConfirm={async () => {
+ setManageProvider(selectedMethod);
+ }}
+ >
+ <p>
+ We have found no Anastasis providers that support this
+ authentication method. You can add a provider manually. To add a
+ provider you must know the provider URL (e.g.
+ https://provider.com)
+ </p>
+ <p>
+ <a>Learn more about Anastasis providers</a>
+ </p>
+ </ConfirmModal>
+ )}
+ </Fragment>
);
}
- if (addingProvider !== undefined) {
- return <div />
- }
-
function MethodButton(props: { method: KnownAuthMethods }): VNode {
- if (authMethods[props.method].skip) return <div />
-
+ if (authMethods[props.method].skip) return <div />;
+
return (
<div class="block">
<button
- style={{ justifyContent: 'space-between' }}
+ style={{ justifyContent: "space-between" }}
class="button is-fullwidth"
onClick={() => {
setSelectedMethod(props.method);
}}
>
- <div style={{ display: 'flex' }}>
- <span class="icon ">
- {authMethods[props.method].icon}
- </span>
- {authAvailableSet.has(props.method) ?
- <span>
- Add a {authMethods[props.method].label} challenge
- </span> :
- <span>
- Add a {authMethods[props.method].label} provider
- </span>
- }
+ <div style={{ display: "flex" }}>
+ <span class="icon ">{authMethods[props.method].icon}</span>
+ {authAvailableSet.has(props.method) ? (
+ <span>Add a {authMethods[props.method].label} challenge</span>
+ ) : (
+ <span>Add a {authMethods[props.method].label} provider</span>
+ )}
</div>
- {!authAvailableSet.has(props.method) &&
- <span class="icon has-text-danger" >
+ {!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>
- }
+ )}
+ {camByType[props.method] && (
+ <span class="tag is-info">{camByType[props.method].length}</span>
+ )}
</button>
</div>
);
}
- const errors = !haveMethodsConfigured ? "There is not enough authentication methods." : undefined;
+ const errors = configuredAuthMethods.length < 2 ? "There is not enough authentication methods." : undefined;
+ const handleNext = async () => {
+ const st = reducer.currentReducerState as ReducerStateBackup;
+ if ((st.authentication_methods ?? []).length <= 2) {
+ setTooFewAuths(true);
+ } else {
+ await reducer.transition("next", {});
+ }
+ };
return (
- <AnastasisClientFrame title="Backup: Configure Authentication Methods" hideNext={errors}>
+ <AnastasisClientFrame
+ title="Backup: Configure Authentication Methods"
+ hideNext={errors}
+ onNext={handleNext}
+ >
<div class="columns">
- <div class="column is-half">
+ <div class="column">
<div>
- {getKeys(authMethods).map(method => <MethodButton key={method} method={method} />)}
+ {getKeys(authMethods).map((method) => (
+ <MethodButton key={method} method={method} />
+ ))}
</div>
- {authAvailableSet.size === 0 && <ConfirmModal active={!noProvidersAck}
- onCancel={() => setNoProvidersAck(true)} description="No providers founds" label="Add a provider manually"
- onConfirm={() => {
- null
- }}
- >
- We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
- To add a provider you must know the provider URL (e.g. https://provider.com)
- <p>
- <a>More about cloud providers</a>
- </p>
- </ConfirmModal>}
+ {tooFewAuths ? (
+ <ConfirmModal
+ active={tooFewAuths}
+ onCancel={() => setTooFewAuths(false)}
+ description="Too few auth methods configured"
+ label="Proceed anyway"
+ onConfirm={() => reducer.transition("next", {})}
+ >
+ You have selected fewer than 3 authentication methods. We
+ recommend that you add at least 3.
+ </ConfirmModal>
+ ) : null}
+ {authAvailableSet.size === 0 && (
+ <ConfirmModal
+ active={!noProvidersAck}
+ onCancel={() => setNoProvidersAck(true)}
+ description="No providers founds"
+ label="Add a provider manually"
+ onConfirm={async () => {
+ setManageProvider("");
+ }}
+ >
+ <p>
+ We have found no Anastasis providers for your chosen country /
+ currency. You can add a providers manually. To add a provider
+ you must know the provider URL (e.g. https://provider.com)
+ </p>
+ <p>
+ <a>Learn more about Anastasis providers</a>
+ </p>
+ </ConfirmModal>
+ )}
</div>
- <div class="column is-half">
+ <div class="column">
<p class="block">
- When recovering your wallet, you will be asked to verify your identity via the methods you configure here.
- The list of authentication method is defined by the backup provider list.
+ When recovering your secret data, you will be asked to verify your
+ identity via the methods you configure here. The list of
+ authentication method is defined by the backup provider list.
</p>
<p class="block">
- <button class="button is-info">Manage the backup provider's list</button>
+ <button
+ class="button is-info"
+ onClick={() => setManageProvider("")}
+ >
+ Manage backup providers
+ </button>
</p>
- {authAvailableSet.size > 0 && <p class="block">
- We couldn't find provider for some of the authentication methods.
- </p>}
+ {authAvailableSet.size > 0 && (
+ <p class="block">
+ We couldn't find provider for some of the authentication methods.
+ </p>
+ )}
</div>
</div>
</AnastasisClientFrame>
);
}
-type AuthMethodWithRemove = AuthMethod & { remove: () => void }
-export interface AuthMethodSetupProps {
- method: string;
- addAuthMethod: (x: any) => void;
- configured: AuthMethodWithRemove[];
- cancel: () => void;
-}
-
function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
return (
<AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
@@ -186,36 +244,3 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
);
}
-
-function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode {
- 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;
-}
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index b71a79727..c3ff7e746 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,48 +15,51 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen";
export default {
- title: 'Pages/backup/FinishedScreen',
+ title: "Pages/backup/Finished",
component: TestedComponent,
args: {
- order: 9,
+ order: 8,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished);
+export const WithoutName = createExample(
+ TestedComponent,
+ reducerStatesExample.backupFinished,
+);
-export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
- secret_name: 'super_secret',
+export const WithName = createExample(TestedComponent, {
+ ...reducerStatesExample.backupFinished,
+ secret_name: "super_secret",
} as ReducerState);
export const WithDetails = createExample(TestedComponent, {
...reducerStatesExample.backupFinished,
- secret_name: 'super_secret',
+ secret_name: "super_secret",
success_details: {
- 'http://anastasis.net': {
+ "https://anastasis.demo.taler.net/": {
policy_expiration: {
- t_ms: 'never'
+ t_ms: "never",
},
- policy_version: 0
+ policy_version: 0,
},
- 'http://taler.net': {
+ "https://kudos.demo.anastasis.lu/": {
policy_expiration: {
- t_ms: new Date().getTime() + 60*60*24*1000
+ t_ms: new Date().getTime() + 60 * 60 * 24 * 1000,
},
- policy_version: 1
+ policy_version: 1,
},
- }
+ },
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
index 7938baca4..129f1e9e4 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
@@ -1,44 +1,50 @@
+import { AuthenticationProviderStatusOk } from "anastasis-core";
import { format } from "date-fns";
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function BackupFinishedScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const details = reducer.currentReducerState.success_details
+ const details = reducer.currentReducerState.success_details;
+ const providers = reducer.currentReducerState.authentication_providers ?? {}
- return (<AnastasisClientFrame hideNav title="Backup finished">
- {reducer.currentReducerState.secret_name ? <p>
- Your backup of secret <b>"{reducer.currentReducerState.secret_name}"</b> was
- successful.
- </p> :
- <p>
- Your secret was successfully backed up.
- </p>}
+ return (
+ <AnastasisClientFrame hideNav title="Backup success!">
+ <p>Your backup is complete.</p>
- {details && <div class="block">
- <p>The backup is stored by the following providers:</p>
- {Object.keys(details).map((x, i) => {
- const sd = details[x];
- return (
- <div key={i} class="box">
- {x}
- <p>
- version {sd.policy_version}
- {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'}
- </p>
- </div>
- );
- })}
- </div>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- </div>
- </AnastasisClientFrame>);
+ {details && (
+ <div class="block">
+ <p>The backup is stored by the following providers:</p>
+ {Object.keys(details).map((url, i) => {
+ const sd = details[url];
+ const p = providers[url] as AuthenticationProviderStatusOk
+ return (
+ <div key={i} class="box">
+ <a href={url} target="_blank" rel="noreferrer">{p.business_name}</a>
+ <p>
+ version {sd.policy_version}
+ {sd.policy_expiration.t_ms !== "never"
+ ? ` expires at: ${format(
+ new Date(sd.policy_expiration.t_ms),
+ "dd-MM-yyyy",
+ )}`
+ : " without expiration date"}
+ </p>
+ </div>
+ );
+ })}
+ </div>
+ )}
+ </AnastasisClientFrame>
+ );
}
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
index 48115c798..56aee8763 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -20,12 +19,16 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { RecoveryStates, ReducerState } from "anastasis-core";
+import {
+ ChallengeFeedbackStatus,
+ RecoveryStates,
+ ReducerState,
+} from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
export default {
- title: "Pages/recovery/ChallengeOverviewScreen",
+ title: "Pages/recovery/SolveChallenge/Overview",
component: TestedComponent,
args: {
order: 5,
@@ -176,16 +179,15 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
recovery_information: {
policies: [
[
- { uuid: "1" },
- { uuid: "2" },
- { uuid: "3" },
- { uuid: "4" },
- { uuid: "5" },
- { uuid: "6" },
- { uuid: "7" },
- { uuid: "8" },
- { uuid: "9" },
- { uuid: "10" },
+ { uuid: "uuid-1" },
+ { uuid: "uuid-2" },
+ { uuid: "uuid-3" },
+ { uuid: "uuid-4" },
+ { uuid: "uuid-5" },
+ { uuid: "uuid-6" },
+ { uuid: "uuid-7" },
+ { uuid: "uuid-8" },
+ { uuid: "uuid-9" },
],
],
challenges: [
@@ -193,20 +195,96 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
cost: "USD:1",
instructions: 'in state "solved"',
type: "question",
- uuid: "1",
+ uuid: "uuid-1",
},
{
cost: "USD:1",
instructions: 'in state "message"',
type: "question",
- uuid: "2",
+ uuid: "uuid-2",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "auth iban"',
+ type: "question",
+ uuid: "uuid-3",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "payment "',
+ type: "question",
+ uuid: "uuid-4",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "rate limit"',
+ type: "question",
+ uuid: "uuid-5",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "redirect"',
+ type: "question",
+ uuid: "uuid-6",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "server failure"',
+ type: "question",
+ uuid: "uuid-7",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "truth unknown"',
+ type: "question",
+ uuid: "uuid-8",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "unsupported"',
+ type: "question",
+ uuid: "uuid-9",
},
],
},
challenge_feedback: {
- 1: { state: "solved" },
- 2: { state: "message", message: "Security question was not solved correctly" },
- // FIXME: add missing feedback states here!
+ "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
+ "uuid-2": {
+ state: ChallengeFeedbackStatus.Message.toString(),
+ message: "Challenge should be solved",
+ },
+ "uuid-3": {
+ state: ChallengeFeedbackStatus.AuthIban.toString(),
+ challenge_amount: "EUR:1",
+ credit_iban: "DE12345789000",
+ business_name: "Data Loss Incorporated",
+ wire_transfer_subject: "Anastasis 987654321",
+ },
+ "uuid-4": {
+ state: ChallengeFeedbackStatus.Payment.toString(),
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ "uuid-5": {
+ state: ChallengeFeedbackStatus.RateLimitExceeded.toString(),
+ // "error_code": 8121
+ },
+ "uuid-6": {
+ state: ChallengeFeedbackStatus.Redirect.toString(),
+ redirect_url: "https://videoconf.example.com/",
+ http_status: 303,
+ },
+ "uuid-7": {
+ state: ChallengeFeedbackStatus.ServerFailure.toString(),
+ http_status: 500,
+ error_response: "some error message or error object",
+ },
+ "uuid-8": {
+ state: ChallengeFeedbackStatus.TruthUnknown.toString(),
+ // "error_code": 8108
+ },
+ "uuid-9": { state: ChallengeFeedbackStatus.Unsupported.toString() },
},
} as ReducerState,
);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
index ed34bbde2..d0c9b2f5d 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
@@ -3,6 +3,7 @@ import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
import { authMethods, KnownAuthMethods } from "./authMethod";
+import { AsyncButton } from "../../components/AsyncButton";
function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
const { feedback } = props;
@@ -11,28 +12,37 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
- return (
- <div>
- <p>{feedback.message}</p>
- </div>
- );
+ return <div class="block has-text-danger">{feedback.message}</div>;
+ case ChallengeFeedbackStatus.Solved:
+ return <div />;
case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban:
return null;
+ case ChallengeFeedbackStatus.ServerFailure:
+ return <div class="block has-text-danger">Server error.</div>;
case ChallengeFeedbackStatus.RateLimitExceeded:
- return <div>Rate limit exceeded.</div>;
- case ChallengeFeedbackStatus.Redirect:
- return <div>Redirect (FIXME: not supported)</div>;
+ return (
+ <div class="block has-text-danger">
+ There were to many failed attempts.
+ </div>
+ );
case ChallengeFeedbackStatus.Unsupported:
- return <div>Challenge not supported by client.</div>;
+ return (
+ <div class="block has-text-danger">
+ This client doesn't support solving this type of challenge. Use
+ another version or contact the provider.
+ </div>
+ );
case ChallengeFeedbackStatus.TruthUnknown:
- return <div>Truth unknown</div>;
- default:
return (
- <div>
- <pre>{JSON.stringify(feedback)}</pre>
+ <div class="block has-text-danger">
+ Provider doesn't recognize the challenge of the policy. Contact the
+ provider for further information.
</div>
);
+ case ChallengeFeedbackStatus.Redirect:
+ default:
+ return <div />;
}
}
@@ -72,19 +82,25 @@ export function ChallengeOverviewScreen(): VNode {
feedback: challengeFeedback[ch.uuid],
};
}
- const policiesWithInfo = policies.map((row) => {
- let isPolicySolved = true;
- const challenges = row
- .map(({ uuid }) => {
- const info = knownChallengesMap[uuid];
- const isChallengeSolved = info?.feedback?.state === "solved";
- isPolicySolved = isPolicySolved && isChallengeSolved;
- return { info, uuid, isChallengeSolved };
- })
- .filter((ch) => ch.info !== undefined);
+ const policiesWithInfo = policies
+ .map((row) => {
+ let isPolicySolved = true;
+ const challenges = row
+ .map(({ uuid }) => {
+ const info = knownChallengesMap[uuid];
+ const isChallengeSolved = info?.feedback?.state === "solved";
+ isPolicySolved = isPolicySolved && isChallengeSolved;
+ return { info, uuid, isChallengeSolved };
+ })
+ .filter((ch) => ch.info !== undefined);
- return { isPolicySolved, challenges };
- });
+ return {
+ isPolicySolved,
+ challenges,
+ corrupted: row.length > challenges.length,
+ };
+ })
+ .filter((p) => !p.corrupted);
const atLeastThereIsOnePolicySolved =
policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
@@ -94,25 +110,124 @@ export function ChallengeOverviewScreen(): VNode {
: undefined;
return (
<AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
- {!policies.length ? (
+ {!policiesWithInfo.length ? (
<p class="block">
No policies found, try with another version of the secret
</p>
- ) : policies.length === 1 ? (
+ ) : policiesWithInfo.length === 1 ? (
<p class="block">
One policy found for this secret. You need to solve all the challenges
in order to recover your secret.
</p>
) : (
<p class="block">
- We have found {policies.length} polices. You need to solve all the
- challenges from one policy in order to recover your secret.
+ We have found {policiesWithInfo.length} polices. You need to solve all
+ the challenges from one policy in order to recover your secret.
</p>
)}
{policiesWithInfo.map((policy, policy_index) => {
const tableBody = policy.challenges.map(({ info, uuid }) => {
const isFree = !info.cost || info.cost.endsWith(":0");
const method = authMethods[info.type as KnownAuthMethods];
+
+ if (!method) {
+ return (
+ <div
+ key={uuid}
+ class="block"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ <span>unknown challenge</span>
+ </div>
+ </div>
+ );
+ }
+
+ function ChallengeButton({
+ id,
+ feedback,
+ }: {
+ id: string;
+ feedback?: ChallengeFeedback;
+ }): VNode {
+ async function selectChallenge(): Promise<void> {
+ if (reducer) {
+ return reducer.transition("select_challenge", { uuid: id });
+ }
+ }
+ if (!feedback) {
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Solve
+ </AsyncButton>
+ </div>
+ );
+ }
+ switch (feedback.state) {
+ case ChallengeFeedbackStatus.ServerFailure:
+ case ChallengeFeedbackStatus.Unsupported:
+ case ChallengeFeedbackStatus.TruthUnknown:
+ case ChallengeFeedbackStatus.RateLimitExceeded:
+ return <div />;
+ case ChallengeFeedbackStatus.AuthIban:
+ case ChallengeFeedbackStatus.Payment:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Pay
+ </AsyncButton>
+ </div>
+ );
+ case ChallengeFeedbackStatus.Redirect:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Go to {feedback.redirect_url}
+ </AsyncButton>
+ </div>
+ );
+ case ChallengeFeedbackStatus.Solved:
+ return (
+ <div>
+ <div class="tag is-success is-large">Solved</div>
+ </div>
+ );
+ default:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Solve
+ </AsyncButton>
+ </div>
+ );
+ }
+ }
return (
<div
key={uuid}
@@ -131,21 +246,8 @@ export function ChallengeOverviewScreen(): VNode {
</div>
<OverviewFeedbackDisplay feedback={info.feedback} />
</div>
- <div>
- {method && info.feedback?.state !== "solved" ? (
- <a
- class="button"
- onClick={() =>
- reducer.transition("select_challenge", { uuid })
- }
- >
- {isFree ? "Solve" : `Pay and Solve`}
- </a>
- ) : null}
- {info.feedback?.state === "solved" ? (
- <a class="button is-success"> Solved </a>
- ) : null}
- </div>
+
+ <ChallengeButton id={uuid} feedback={info.feedback} />
</div>
);
});
@@ -153,11 +255,13 @@ export function ChallengeOverviewScreen(): VNode {
const policyName = policy.challenges
.map((x) => x.info.type)
.join(" + ");
+
const opa = !atLeastThereIsOnePolicySolved
? undefined
: policy.isPolicySolved
? undefined
: "0.6";
+
return (
<div
key={policy_index}
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
index e5fe09e99..8c788e556 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -15,24 +15,26 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../utils";
+import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen";
export default {
- title: 'Pages/recovery/__ChallengePayingScreen',
+ title: "Pages/recovery/__ChallengePaying",
component: TestedComponent,
args: {
order: 10,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying);
+export const Example = createExample(
+ TestedComponent,
+ reducerStatesExample.challengePaying,
+);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
index 84896a2ec..ffcc8fafc 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
@@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function ChallengePayingScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const payments = ['']; //reducer.currentReducerState.payments ??
+ const payments = [""]; //reducer.currentReducerState.payments ??
return (
- <AnastasisClientFrame
- hideNav
- title="Recovery: Challenge Paying"
- >
+ <AnastasisClientFrame hideNav title="Recovery: Challenge Paying">
<p>
Some of the providers require a payment to store the encrypted
authentication information.
diff --git a/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx b/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx
new file mode 100644
index 000000000..c9c59c1b4
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx
@@ -0,0 +1,58 @@
+import { differenceInBusinessDays } from "date-fns";
+import { ComponentChildren, h, VNode } from "preact";
+import { useLayoutEffect, useRef } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton";
+
+export interface ConfirmModelProps {
+ active?: boolean;
+ description?: string;
+ onCancel?: () => void;
+ onConfirm?: () => Promise<void>;
+ label?: string;
+ cancelLabel?: string;
+ children?: ComponentChildren;
+ danger?: boolean;
+ disabled?: boolean;
+}
+
+export function ConfirmModal({
+ active, description, onCancel, onConfirm, children, danger, disabled, label = "Confirm", cancelLabel = "Dismiss"
+}: ConfirmModelProps): VNode {
+ 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}>
+ {cancelLabel}
+ </button>
+ <div class="buttons is-right" style={{ width: "100%" }} onKeyDown={(e) => {
+ if (e.key === 'Escape' && onCancel) onCancel()
+ }}>
+ <AsyncButton
+ grabFocus
+ class={danger ? "button is-danger " : "button is-info "}
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ {label}
+ </AsyncButton>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel} />
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
index 6bdb3515d..0948d603e 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -16,37 +16,42 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen";
export default {
- title: 'Pages/Location',
+ title: "Pages/Location",
component: TestedComponent,
args: {
order: 2,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
+export const BackupSelectContinent = createExample(
+ TestedComponent,
+ reducerStatesExample.backupSelectContinent,
+);
export const BackupSelectCountry = createExample(TestedComponent, {
...reducerStatesExample.backupSelectContinent,
- selected_continent: 'Testcontinent',
+ selected_continent: "Testcontinent",
} as ReducerState);
-export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
+export const RecoverySelectContinent = createExample(
+ TestedComponent,
+ reducerStatesExample.recoverySelectContinent,
+);
export const RecoverySelectCountry = createExample(TestedComponent, {
...reducerStatesExample.recoverySelectContinent,
- selected_continent: 'Testcontinent',
+ selected_continent: "Testcontinent",
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
index 0e43f982d..aafde6e8c 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -1,58 +1,81 @@
/* eslint-disable @typescript-eslint/camelcase */
+import { BackupStates, RecoveryStates } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index";
export function ContinentSelectionScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
- //FIXME: remove this when #7056 is fixed
- const countryFromReducer = (reducer?.currentReducerState as any).selected_country || ""
- const [countryCode, setCountryCode] = useState( countryFromReducer )
+ // FIXME: remove this when #7056 is fixed
+ const countryFromReducer =
+ (reducer?.currentReducerState as any).selected_country || "";
+ const [countryCode, setCountryCode] = useState(countryFromReducer);
- if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) {
- return <div />
+ if (
+ !reducer ||
+ !reducer.currentReducerState ||
+ !("continents" in reducer.currentReducerState)
+ ) {
+ return <div />;
}
const selectContinent = (continent: string): void => {
- reducer.transition("select_continent", { continent })
+ reducer.transition("select_continent", { continent });
};
const selectCountry = (country: string): void => {
- setCountryCode(country)
+ setCountryCode(country);
};
-
-
+
const continentList = reducer.currentReducerState.continents || [];
const countryList = reducer.currentReducerState.countries || [];
- const theContinent = reducer.currentReducerState.selected_continent || ""
+ const theContinent = reducer.currentReducerState.selected_continent || "";
// const cc = reducer.currentReducerState.selected_country || "";
- const theCountry = countryList.find(c => c.code === countryCode)
- const selectCountryAction = () => {
+ const theCountry = countryList.find((c) => c.code === countryCode);
+ const selectCountryAction = async () => {
//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
+ const errors = !theCountry ? "Select a country" : undefined;
- return (
- <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}>
+ const handleBack = async () => {
+ // We want to go to the start, even if we already selected
+ // a country.
+ // FIXME: What if we don't want to lose all information here?
+ // Can we do some kind of soft reset?
+ reducer.reset();
+ };
- <div class="columns" >
+ return (
+ <AnastasisClientFrame
+ hideNext={errors}
+ title={withProcessLabel(reducer, "Where do you live?")}
+ onNext={selectCountryAction}
+ onBack={handleBack}
+ >
+ <div class="columns">
<div class="column is-one-third">
<div class="field">
<label class="label">Continent</label>
<div class="control is-expanded has-icons-left">
- <div class="select is-fullwidth" >
- <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} >
- <option key="none" disabled selected value=""> Choose a continent </option>
- {continentList.map(prov => (
+ <div class="select is-fullwidth">
+ <select
+ onChange={(e) => selectContinent(e.currentTarget.value)}
+ value={theContinent}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a continent{" "}
+ </option>
+ {continentList.map((prov) => (
<option key={prov.name} value={prov.name}>
{prov.name}
</option>
@@ -68,10 +91,17 @@ export function ContinentSelectionScreen(): VNode {
<div class="field">
<label class="label">Country</label>
<div class="control is-expanded has-icons-left">
- <div class="select is-fullwidth" >
- <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 => (
+ <div class="select is-fullwidth">
+ <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>
@@ -93,12 +123,37 @@ export function ContinentSelectionScreen(): VNode {
</div>
<div class="column is-two-third">
<p>
- Your location will help us to determine which personal information
- ask you for the next step.
+ Your selection will help us ask right information to uniquely
+ identify you when you want to recover your secret again.
+ </p>
+ <p>
+ Choose the country that issued most of your long-term legal
+ documents or personal identifiers.
</p>
+ <div
+ style={{
+ border: "1px solid gray",
+ borderRadius: "0.5em",
+ backgroundColor: "#fbfcbd",
+ padding: "0.5em",
+ }}
+ >
+ <p>
+ If you just want to try out Anastasis, we recomment that you
+ choose <b>Testcontinent</b> with <b>Demoland</b>. For this special
+ country, you will be asked for a simple number and not real,
+ personal identifiable information.
+ </p>
+ {/*
+ <p>
+ Because of the diversity of personally identifying information in
+ different countries and cultures, we do not support all countries
+ yet. If you want to improve the supported countries,{" "}
+ <a href="mailto:contact@anastasis.lu">contact us</a>.
+ </p> */}
+ </div>
</div>
</div>
-
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
index fc339e48e..4cbeb8308 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -16,94 +16,126 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen";
export default {
- title: 'Pages/backup/ReviewPoliciesScreen/EditPoliciesScreen',
+ title: "Pages/backup/ReviewPolicies/EditPolicies",
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const EditingAPolicy = createExample(TestedComponent, {
- ...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 1,
- provider: 'https://anastasis.demo.taler.net/'
- }, {
- authentication_method: 2,
- provider: 'http://localhost:8086/'
- }]
- }, {
- methods: [{
- authentication_method: 1,
- provider: 'http://localhost:8086/'
- }]
- }],
- authentication_methods: [{
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA"
- }, {
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: ""
- }, {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8"
- }]
-} as ReducerState, { index : 0});
-
-export const CreatingAPolicy = createExample(TestedComponent, {
- ...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 1,
- provider: 'https://anastasis.demo.taler.net/'
- }, {
- authentication_method: 2,
- provider: 'http://localhost:8086/'
- }]
- }, {
- methods: [{
- authentication_method: 1,
- provider: 'http://localhost:8086/'
- }]
- }],
- authentication_methods: [{
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA"
- }, {
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: ""
- }, {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8"
- }]
-} as ReducerState, { index : 3});
+export const EditingAPolicy = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 2,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
+ ],
+ } as ReducerState,
+ { index: 0 },
+);
+export const CreatingAPolicy = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 2,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
+ ],
+ } as ReducerState,
+ { index: 3 },
+);
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
index 85cc96c46..198209399 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
@@ -20,7 +20,6 @@ interface Props {
index: number;
cancel: () => void;
confirm: (changes: MethodProvider[]) => void;
-
}
export interface MethodProvider {
@@ -28,106 +27,151 @@ export interface MethodProvider {
provider: string;
}
-export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode {
- const [changedProvider, setChangedProvider] = useState<Array<string>>([])
+export function EditPoliciesScreen({
+ index: policy_index,
+ cancel,
+ confirm,
+}: Props): VNode {
+ const [changedProvider, setChangedProvider] = useState<Array<string>>([]);
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const selectableProviders: ProviderInfoByType = {}
- const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {})
+ const selectableProviders: ProviderInfoByType = {};
+ const allProviders = Object.entries(
+ reducer.currentReducerState.authentication_providers || {},
+ );
for (let index = 0; index < allProviders.length; index++) {
- const [url, status] = allProviders[index]
+ const [url, status] = allProviders[index];
if ("methods" in status) {
- status.methods.map(m => {
- const type: KnownAuthMethods = m.type as KnownAuthMethods
- const values = selectableProviders[type] || []
- const isFree = !m.usage_fee || m.usage_fee.endsWith(":0")
- values.push({ url, cost: m.usage_fee, isFree })
- selectableProviders[type] = values
- })
+ status.methods.map((m) => {
+ const type: KnownAuthMethods = m.type as KnownAuthMethods;
+ const values = selectableProviders[type] || [];
+ const isFree = !m.usage_fee || m.usage_fee.endsWith(":0");
+ values.push({ url, cost: m.usage_fee, isFree });
+ selectableProviders[type] = values;
+ });
}
}
- const allAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
+ const allAuthMethods =
+ reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
- const policy = policies[policy_index]
-
- for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) {
- policy?.methods.find(m => m.authentication_method === method_index)?.provider
+ const policy = policies[policy_index];
+
+ for (
+ let method_index = 0;
+ method_index < allAuthMethods.length;
+ method_index++
+ ) {
+ policy?.methods.find((m) => m.authentication_method === method_index)
+ ?.provider;
}
function sendChanges(): void {
- const newMethods: MethodProvider[] = []
+ const newMethods: MethodProvider[] = [];
allAuthMethods.forEach((method, index) => {
- const oldValue = policy?.methods.find(m => m.authentication_method === index)
+ const oldValue = policy?.methods.find(
+ (m) => m.authentication_method === index,
+ );
if (changedProvider[index] === undefined && oldValue !== undefined) {
- newMethods.push(oldValue)
+ newMethods.push(oldValue);
}
- if (changedProvider[index] !== undefined && changedProvider[index] !== "") {
+ if (
+ changedProvider[index] !== undefined &&
+ changedProvider[index] !== ""
+ ) {
newMethods.push({
authentication_method: index,
- provider: changedProvider[index]
- })
+ provider: changedProvider[index],
+ });
}
- })
- confirm(newMethods)
+ });
+ confirm(newMethods);
}
- return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}>
- <section class="section">
- {!policy ? <p>
- Creating a new policy #{policy_index}
- </p> : <p>
- Editing policy #{policy_index}
- </p>}
- {allAuthMethods.map((method, index) => {
- //take the url from the updated change or from the policy
- const providerURL = changedProvider[index] === undefined ?
- policy?.methods.find(m => m.authentication_method === index)?.provider :
- changedProvider[index];
+ return (
+ <AnastasisClientFrame
+ hideNav
+ title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}
+ >
+ <section class="section">
+ {!policy ? (
+ <p>Creating a new policy #{policy_index}</p>
+ ) : (
+ <p>Editing policy #{policy_index}</p>
+ )}
+ {allAuthMethods.map((method, index) => {
+ //take the url from the updated change or from the policy
+ const providerURL =
+ changedProvider[index] === undefined
+ ? policy?.methods.find((m) => m.authentication_method === index)
+ ?.provider
+ : changedProvider[index];
- const type: KnownAuthMethods = method.type as KnownAuthMethods
- function changeProviderTo(url: string): void {
- const copy = [...changedProvider]
- copy[index] = url
- setChangedProvider(copy)
- }
- return (
- <div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}>
- <span class="icon">
- {authMethods[type]?.icon}
- </span>
- <span>
- {method.instructions}
- </span>
- <span>
- <span class="select " >
- <select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}>
- <option key="none" value=""> &lt;&lt; off &gt;&gt; </option>
- {selectableProviders[type]?.map(prov => (
- <option key={prov.url} value={prov.url}>
- {prov.url}
+ const type: KnownAuthMethods = method.type as KnownAuthMethods;
+ function changeProviderTo(url: string): void {
+ const copy = [...changedProvider];
+ copy[index] = url;
+ setChangedProvider(copy);
+ }
+ return (
+ <div
+ key={index}
+ class="block"
+ style={{ display: "flex", alignItems: "center" }}
+ >
+ <span class="icon">{authMethods[type]?.icon}</span>
+ <span>{method.instructions}</span>
+ <span>
+ <span class="select ">
+ <select
+ onChange={(e) => changeProviderTo(e.currentTarget.value)}
+ value={providerURL ?? ""}
+ >
+ <option key="none" value="">
+ {" "}
+ &lt;&lt; off &gt;&gt;{" "}
</option>
- ))}
- </select>
+ {selectableProviders[type]?.map((prov) => (
+ <option key={prov.url} value={prov.url}>
+ {prov.url}
+ </option>
+ ))}
+ </select>
+ </span>
</span>
- </span>
- </div>
- );
- })}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
- <span class="buttons">
- <button class="button" onClick={() => setChangedProvider([])}>Reset</button>
- <button class="button is-info" onClick={sendChanges}>Confirm</button>
- </span>
- </div>
- </section>
- </AnastasisClientFrame>
+ </div>
+ );
+ })}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <span class="buttons">
+ <button class="button" onClick={() => setChangedProvider([])}>
+ Reset
+ </button>
+ <button class="button is-info" onClick={sendChanges}>
+ Confirm
+ </button>
+ </span>
+ </div>
+ </section>
+ </AnastasisClientFrame>
+ );
}
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index e952ab28d..9bebcfbc9 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,35 +15,40 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen";
export default {
- title: 'Pages/backup/PoliciesPayingScreen',
+ title: "Pages/backup/__PoliciesPaying",
component: TestedComponent,
args: {
- order: 8,
+ order: 9,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.policyPay);
+export const Example = createExample(
+ TestedComponent,
+ reducerStatesExample.policyPay,
+);
export const WithSomePaymentRequest = createExample(TestedComponent, {
...reducerStatesExample.policyPay,
- policy_payment_requests: [{
- payto: 'payto://x-taler-bank/bank.taler/account-a',
- provider: 'provider1'
- }, {
- payto: 'payto://x-taler-bank/bank.taler/account-b',
- provider: 'provider2'
- }]
+ policy_payment_requests: [
+ {
+ payto: "payto://x-taler-bank/bank.taler/account-a",
+ provider: "provider1",
+ },
+ {
+ payto: "payto://x-taler-bank/bank.taler/account-b",
+ provider: "provider2",
+ },
+ ],
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
index a470f5155..c3568b32d 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
@@ -3,20 +3,23 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function PoliciesPayingScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
-
+
return (
<AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
<p>
- Some of the providers require a payment to store the encrypted
- recovery document.
+ Some of the providers require a payment to store the encrypted recovery
+ document.
</p>
<ul>
{payments.map((x, i) => {
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
index 0d2ebb778..1c05cd6e1 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,30 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen";
export default {
- title: 'Pages/recovery/FinishedScreen',
+ title: "Pages/recovery/Finished",
args: {
order: 7,
},
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const GoodEnding = createExample(TestedComponent, {
...reducerStatesExample.recoveryFinished,
- core_secret: { mime: 'text/plain', value: 'hello' }
+ recovery_document: {
+ secret_name: "the_name_of_the_secret",
+ },
+ core_secret: {
+ mime: "text/plain",
+ value: encodeCrock(
+ stringToBytes("hello this is my secret, don't tell anybody"),
+ ),
+ },
} as ReducerState);
-export const BadEnding = createExample(TestedComponent, reducerStatesExample.recoveryFinished);
+export const BadEnding = createExample(
+ TestedComponent,
+ reducerStatesExample.recoveryFinished,
+);
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
index a61ef9efa..d83482559 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -1,39 +1,80 @@
-import {
- bytesToString,
- decodeCrock
-} from "@gnu-taler/taler-util";
+import { bytesToString, decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { stringToBytes } from "qrcode-generator";
+import { QR } from "../../components/QR";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function RecoveryFinishedScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
+ const [copied, setCopied] = useState(false);
+ useEffect(() => {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }, [copied]);
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const encodedSecret = reducer.currentReducerState.core_secret
+ const secretName = reducer.currentReducerState.recovery_document?.secret_name;
+ const encodedSecret = reducer.currentReducerState.core_secret;
if (!encodedSecret) {
- return <AnastasisClientFrame title="Recovery Problem" hideNav>
- <p>
- Secret not found
- </p>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- </div>
- </AnastasisClientFrame>
+ return (
+ <AnastasisClientFrame title="Recovery Problem" hideNav>
+ <p>Secret not found</p>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
}
- const secret = bytesToString(decodeCrock(encodedSecret.value))
+ const secret = bytesToString(decodeCrock(encodedSecret.value));
+ const contentURI = `data:${encodedSecret.mime},${secret}`;
+ // const fileName = encodedSecret['filename']
+ // data:plain/text;base64,asdasd
return (
- <AnastasisClientFrame title="Recovery Finished" hideNav>
- <p>
- Secret: {secret}
- </p>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
+ <AnastasisClientFrame title="Recovery Success" hideNav>
+ <h2 class="subtitle">Your secret was recovered</h2>
+ {secretName && (
+ <p class="block">
+ <b>Secret name:</b> {secretName}
+ </p>
+ )}
+ <div class="block buttons" disabled={copied}>
+ <button
+ class="button"
+ onClick={() => {
+ navigator.clipboard.writeText(secret);
+ setCopied(true);
+ }}
+ >
+ {!copied ? "Copy" : "Copied"}
+ </button>
+ <a class="button is-info" download="secret.txt" href={contentURI}>
+ <div class="icon is-small ">
+ <i class="mdi mdi-download" />
+ </div>
+ <span>Save as</span>
+ </a>
+ </div>
+ <div class="block">
+ <QR text={secret} />
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
index 9f7e26c16..4a1cba6a8 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,44 +15,51 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen";
export default {
- title: 'Pages/backup/ReviewPoliciesScreen',
+ title: "Pages/backup/ReviewPolicies",
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 0,
- provider: 'asd'
- }, {
- authentication_method: 1,
- provider: 'asd'
- }]
- }, {
- methods: [{
- authentication_method: 1,
- provider: 'asd'
- }]
- }],
- authentication_methods: []
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "asd",
+ },
+ {
+ authentication_method: 1,
+ provider: "asd",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "asd",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [],
} as ReducerState);
export const SomePoliciesWithMethods = createExample(TestedComponent, {
@@ -63,186 +69,193 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
- }
- ]
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
+ provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
+ provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
+ provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
- }
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 555-555",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "Does P equal NP?",
+ challenge: "C5SP8",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
],
- authentication_methods: [{
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 555-555",
- challenge: ""
- }, {
- type: "question",
- instructions: "Does P equal NP?",
- challenge: "C5SP8"
- },{
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: ""
- }, {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8"
-}]
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
index f93963f67..0ed08e037 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
@@ -1,24 +1,33 @@
-/* eslint-disable @typescript-eslint/camelcase */
+import { AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton";
import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
+import { ConfirmModal } from "./ConfirmModal";
import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode {
- const [editingPolicy, setEditingPolicy] = useState<number | undefined>()
- const reducer = useAnastasisContext()
+ const [editingPolicy, setEditingPolicy] = useState<number | undefined>();
+ const [confirmReset, setConfirmReset] = useState(false);
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
+ const configuredAuthMethods =
+ reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
+ const providers = reducer.currentReducerState.authentication_providers ?? {};
+
if (editingPolicy !== undefined) {
return (
<EditPoliciesScreen
@@ -29,62 +38,145 @@ export function ReviewPoliciesScreen(): VNode {
policy_index: editingPolicy,
policy: newMethods,
});
- setEditingPolicy(undefined)
+ setEditingPolicy(undefined);
}}
/>
- )
+ );
+ }
+ async function resetPolicies(): Promise<void> {
+ if (!reducer) return Promise.resolve();
+ return reducer.runTransaction(async (tx) => {
+ await tx.transition("back", {});
+ await tx.transition("next", {});
+ setConfirmReset(false);
+ });
}
- const errors = policies.length < 1 ? 'Need more policies' : undefined
+ const errors = policies.length < 1 ? "Need more policies" : undefined;
return (
- <AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies">
- {policies.length > 0 && <p class="block">
- Based on your configured authentication method you have created, some policies
- have been configured. In order to recover your secret you have to solve all the
- challenges of at least one policy.
- </p>}
- {policies.length < 1 && <p class="block">
- No policies had been created. Go back and add more authentication methods.
- </p>}
- <div class="block" style={{ justifyContent: 'flex-end' }} >
- <button class="button is-success" onClick={() => setEditingPolicy(policies.length + 1)}>Add new policy</button>
+ <AnastasisClientFrame
+ hideNext={errors}
+ title="Backup: Review Recovery Policies"
+ >
+ {policies.length > 0 && (
+ <p class="block">
+ Based on your configured authentication method you have created, some
+ policies have been configured. In order to recover your secret you
+ have to solve all the challenges of at least one policy.
+ </p>
+ )}
+ {policies.length < 1 && (
+ <p class="block">
+ No policies had been created. Go back and add more authentication
+ methods.
+ </p>
+ )}
+ <div class="block">
+ <AsyncButton class="button" onClick={async () => setConfirmReset(true)}>
+ Reset policies
+ </AsyncButton>
+ <button
+ class="button is-success"
+ style={{ marginLeft: 10 }}
+ onClick={() => setEditingPolicy(policies.length)}
+ >
+ Add new policy
+ </button>
</div>
{policies.map((p, policy_index) => {
const methods = p.methods
- .map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
- .filter(x => !!x)
+ .map(
+ (x) =>
+ configuredAuthMethods[x.authentication_method] && {
+ ...configuredAuthMethods[x.authentication_method],
+ provider: x.provider,
+ },
+ )
+ .filter((x) => !!x);
+
+ const policyName = methods.map((x) => x.type).join(" + ");
- const policyName = methods.map(x => x.type).join(" + ");
+ if (p.methods.length > methods.length) {
+ //there is at least one authentication method that is corrupted
+ return null;
+ }
return (
- <div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <div
+ key={policy_index}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
<div>
<h3 class="subtitle">
Policy #{policy_index + 1}: {policyName}
</h3>
- {!methods.length && <p>
- No auth method found
- </p>}
+ {!methods.length && <p>No auth method found</p>}
{methods.map((m, i) => {
+ const p = providers[
+ m.provider
+ ] as AuthenticationProviderStatusOk;
return (
- <p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}>
+ <p
+ key={i}
+ class="block"
+ style={{ display: "flex", alignItems: "center" }}
+ >
<span class="icon">
{authMethods[m.type as KnownAuthMethods]?.icon}
</span>
<span>
- {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a>
+ {m.instructions} recovery provided by{" "}
+ <a href={m.provider} target="_blank" rel="noreferrer">
+ {p.business_name}
+ </a>
</span>
</p>
);
})}
</div>
- <div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
- <button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button>
- <button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button>
+ <div
+ style={{
+ marginTop: "auto",
+ marginBottom: "auto",
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "column",
+ }}
+ >
+ <button
+ class="button is-info block"
+ onClick={() => setEditingPolicy(policy_index)}
+ >
+ Edit
+ </button>
+ <button
+ class="button is-danger block"
+ onClick={() =>
+ reducer.transition("delete_policy", { policy_index })
+ }
+ >
+ Delete
+ </button>
</div>
</div>
);
})}
+ {confirmReset && (
+ <ConfirmModal
+ active
+ onCancel={() => setConfirmReset(false)}
+ description="Do you want to reset the policies to default state?"
+ label="Reset policies"
+ cancelLabel="Cancel"
+ onConfirm={resetPolicies}
+ >
+ <p>
+ All policies will be recalculated based on the authentication
+ providers configured and any change that you did will be lost
+ </p>
+ </ConfirmModal>
+ )}
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index 49dd8fca8..3f2c6a245 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,30 +15,29 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen";
export default {
- title: 'Pages/backup/SecretEditorScreen',
+ title: "Pages/backup/SecretInput",
component: TestedComponent,
args: {
order: 7,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const WithSecretNamePreselected = createExample(TestedComponent, {
...reducerStatesExample.secretEdition,
- secret_name: 'someSecretName',
+ secret_name: "someSecretName",
} as ReducerState);
export const WithoutName = createExample(TestedComponent, {
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
index 1b36a1b21..6d4ffbf88 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
@@ -1,41 +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 { useAnastasisContext } from "../../context/anastasis";
-import {
- AnastasisClientFrame
-} from "./index";
+import { AnastasisClientFrame } from "./index";
import { TextInput } from "../../components/fields/TextInput";
-import { FileInput } from "../../components/fields/FileInput";
+import { FileInput, FileTypeContent } from "../../components/fields/FileInput";
export function SecretEditorScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
const [secretValue, setSecretValue] = useState("");
+ const [secretFile, _setSecretFile] = useState<FileTypeContent | undefined>(
+ undefined,
+ );
+ function setSecretFile(v: FileTypeContent | undefined): void {
+ setSecretValue(""); // reset secret value when uploading a file
+ _setSecretFile(v);
+ }
- const currentSecretName = reducer?.currentReducerState
- && ("secret_name" in reducer.currentReducerState)
- && reducer.currentReducerState.secret_name;
+ const currentSecretName =
+ reducer?.currentReducerState &&
+ "secret_name" in reducer.currentReducerState &&
+ reducer.currentReducerState.secret_name;
const [secretName, setSecretName] = useState(currentSecretName || "");
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
const secretNext = async (): Promise<void> => {
+ const secret = secretFile
+ ? {
+ value: encodeCrock(stringToBytes(secretValue)),
+ filename: secretFile.name,
+ mime: secretFile.type,
+ }
+ : {
+ value: encodeCrock(stringToBytes(secretValue)),
+ mime: "text/plain",
+ };
return reducer.runTransaction(async (tx) => {
await tx.transition("enter_secret_name", {
name: secretName,
});
await tx.transition("enter_secret", {
- secret: {
- value: encodeCrock(stringToBytes(secretValue)),
- mime: "text/plain",
- },
+ secret,
expiration: {
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
},
@@ -43,31 +58,46 @@ export function SecretEditorScreen(): VNode {
await tx.transition("next", {});
});
};
+ const errors = !secretName
+ ? "Add a secret name"
+ : !secretValue && !secretFile
+ ? "Add a secret value or a choose a file to upload"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) secretNext();
+ }
return (
<AnastasisClientFrame
+ hideNext={errors}
title="Backup: Provide secret to backup"
onNext={() => secretNext()}
>
- <div>
+ <div class="block">
<TextInput
- label="Secret's name:"
+ label="Secret name:"
+ tooltip="The secret name allows you to identify your secret when restoring it. It is a label that you can choose freely."
grabFocus
+ onConfirm={goNextIfNoErrors}
bind={[secretName, setSecretName]}
/>
</div>
- <div>
+ <div class="block">
<TextInput
+ disabled={!!secretFile}
+ onConfirm={goNextIfNoErrors}
label="Enter the secret as text:"
bind={[secretValue, setSecretValue]}
/>
- <div style={{display:'flex',}}>
- or&nbsp;
- <FileInput
- label="click here"
- bind={[secretValue, setSecretValue]}
- />
- &nbsp;to import a file
- </div>
+ </div>
+ <div class="block">
+ Or upload a secret file
+ <FileInput label="Choose file" onChange={setSecretFile} />
+ {secretFile && (
+ <div>
+ Uploading secret file <b>{secretFile.name}</b>{" "}
+ <a onClick={() => setSecretFile(undefined)}>cancel</a>
+ </div>
+ )}
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
index 6919eebad..01ce3f0a7 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -15,37 +15,35 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { SecretSelectionScreen as TestedComponent } from "./SecretSelectionScreen";
export default {
- title: 'Pages/recovery/SecretSelectionScreen',
+ title: "Pages/recovery/SecretSelection",
component: TestedComponent,
args: {
order: 4,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const Example = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: {
- provider_url: 'https://kudos.demo.anastasis.lu/',
- secret_name: 'secretName',
+ provider_url: "https://kudos.demo.anastasis.lu/",
+ secret_name: "secretName",
version: 1,
},
} as ReducerState);
-
export const NoRecoveryDocumentFound = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: undefined,
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 8aa5ed2f7..7e517abfe 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -1,23 +1,31 @@
+import { AuthenticationProviderStatus, AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
-import { NumberInput } from "../../components/fields/NumberInput";
+import { PhoneNumberInput } from "../../components/fields/NumberInput";
import { useAnastasisContext } from "../../context/anastasis";
+import { AddingProviderScreen } from "./AddingProviderScreen";
import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
- const currentVersion = (reducer?.currentReducerState
- && ("recovery_document" in reducer.currentReducerState)
- && reducer.currentReducerState.recovery_document?.version) || 0;
+ const [manageProvider, setManageProvider] = useState(false);
+ const currentVersion =
+ (reducer?.currentReducerState &&
+ "recovery_document" in reducer.currentReducerState &&
+ reducer.currentReducerState.recovery_document?.version) ||
+ 0;
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
async function doSelectVersion(p: string, n: number): Promise<void> {
@@ -31,66 +39,108 @@ export function SecretSelectionScreen(): VNode {
});
}
- const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {})
- const recoveryDocument = reducer.currentReducerState.recovery_document
+ const provs = reducer.currentReducerState.authentication_providers ?? {};
+ const recoveryDocument = reducer.currentReducerState.recovery_document;
if (!recoveryDocument) {
- return <ChooseAnotherProviderScreen
- providers={providerList} selected=""
- onChange={(newProv) => doSelectVersion(newProv, 0)}
- />
+ return (
+ <ChooseAnotherProviderScreen
+ providers={provs}
+ selected=""
+ onChange={(newProv) => doSelectVersion(newProv, 0)}
+ />
+ );
}
if (selectingVersion) {
- return <SelectOtherVersionProviderScreen providers={providerList}
- provider={recoveryDocument.provider_url} version={recoveryDocument.version}
- onCancel={() => setSelectingVersion(false)}
- onConfirm={doSelectVersion}
- />
+ return (
+ <SelectOtherVersionProviderScreen
+ providers={provs}
+ provider={recoveryDocument.provider_url}
+ version={recoveryDocument.version}
+ onCancel={() => setSelectingVersion(false)}
+ onConfirm={doSelectVersion}
+ />
+ );
}
+ if (manageProvider) {
+ return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
+ }
+
+ const provierInfo = provs[recoveryDocument.provider_url] as AuthenticationProviderStatusOk
return (
<AnastasisClientFrame title="Recovery: Select secret">
<div class="columns">
<div class="column">
- <div class="box" style={{ border: '2px solid green' }}>
- <h1 class="subtitle">{recoveryDocument.provider_url}</h1>
+ <div class="box" style={{ border: "2px solid green" }}>
+ <h1 class="subtitle">
+ {provierInfo.business_name}
+ </h1>
<div class="block">
- {currentVersion === 0 ? <p>
- Set to recover the latest version
- </p> : <p>
- Set to recover the version number {currentVersion}
- </p>}
+ {currentVersion === 0 ? (
+ <p>Set to recover the latest version</p>
+ ) : (
+ <p>Set to recover the version number {currentVersion}</p>
+ )}
</div>
<div class="buttons is-right">
- <button class="button" onClick={(e) => setSelectingVersion(true)}>Change secret's version</button>
+ <button class="button" onClick={(e) => setSelectingVersion(true)}>
+ Change secret's version
+ </button>
</div>
</div>
</div>
<div class="column">
- <p>Secret found, you can select another version or continue to the challenges solving</p>
+ <p>
+ Secret found, you can select another version or continue to the
+ challenges solving
+ </p>
+ <p class="block">
+ <a onClick={() => setManageProvider(true)}>
+ Manage recovery providers
+ </a>
+ </p>
</div>
</div>
</AnastasisClientFrame>
);
}
-
-function ChooseAnotherProviderScreen({ providers, selected, onChange }: { selected: string; providers: string[]; onChange: (prov: string) => void }): VNode {
+function ChooseAnotherProviderScreen({
+ providers,
+ selected,
+ onChange,
+}: {
+ selected: string;
+ providers: { [url: string]: AuthenticationProviderStatus };
+ onChange: (prov: string) => void;
+}): VNode {
return (
- <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem">
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery: Problem"
+ >
<p>No recovery document found, try with another provider</p>
<div class="field">
<label class="label">Provider</label>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
- <select onChange={(e) => onChange(e.currentTarget.value)} value={selected}>
- <option key="none" disabled selected value=""> Choose a provider </option>
- {providers.map(prov => (
- <option key={prov} value={prov}>
- {prov}
+ <select
+ onChange={(e) => onChange(e.currentTarget.value)}
+ value={selected}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a provider{" "}
+ </option>
+ {Object.keys(providers).map((url) => {
+ const p = providers[url]
+ if (!("methods" in p)) return null
+ return <option key={url} value={url}>
+ {p.business_name}
</option>
- ))}
+ })}
</select>
<div class="icon is-small is-left">
<i class="mdi mdi-earth" />
@@ -102,22 +152,37 @@ function ChooseAnotherProviderScreen({ providers, selected, onChange }: { select
);
}
-function SelectOtherVersionProviderScreen({ providers, provider, version, onConfirm, onCancel }: { onCancel: () => void; provider: string; version: number; providers: string[]; onConfirm: (prov: string, v: number) => Promise<void>; }): VNode {
+function SelectOtherVersionProviderScreen({
+ providers,
+ provider,
+ version,
+ onConfirm,
+ onCancel,
+}: {
+ onCancel: () => void;
+ provider: string;
+ version: number;
+ providers: { [url: string]: AuthenticationProviderStatus };
+ onConfirm: (prov: string, v: number) => Promise<void>;
+}): VNode {
const [otherProvider, setOtherProvider] = useState<string>(provider);
- const [otherVersion, setOtherVersion] = useState(`${version}`);
+ const [otherVersion, setOtherVersion] = useState(
+ version > 0 ? String(version) : "",
+ );
+ const otherProviderInfo = providers[otherProvider] as AuthenticationProviderStatusOk
return (
<AnastasisClientFrame hideNav title="Recovery: Select secret">
<div class="columns">
<div class="column">
<div class="box">
- <h1 class="subtitle">Provider {otherProvider}</h1>
+ <h1 class="subtitle">Provider {otherProviderInfo.business_name}</h1>
<div class="block">
- {version === 0 ? <p>
- Set to recover the latest version
- </p> : <p>
- Set to recover the version number {version}
- </p>}
+ {version === 0 ? (
+ <p>Set to recover the latest version</p>
+ ) : (
+ <p>Set to recover the version number {version}</p>
+ )}
<p>Specify other version below or use the latest</p>
</div>
@@ -125,13 +190,21 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
<label class="label">Provider</label>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
- <select onChange={(e) => setOtherProvider(e.currentTarget.value)} value={otherProvider}>
- <option key="none" disabled selected value=""> Choose a provider </option>
- {providers.map(prov => (
- <option key={prov} value={prov}>
- {prov}
+ <select
+ onChange={(e) => setOtherProvider(e.currentTarget.value)}
+ value={otherProvider}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a provider{" "}
+ </option>
+ {Object.keys(providers).map((url) => {
+ const p = providers[url]
+ if (!("methods" in p)) return null
+ return <option key={url} value={url}>
+ {p.business_name}
</option>
- ))}
+ })}
</select>
<div class="icon is-small is-left">
<i class="mdi mdi-earth" />
@@ -140,27 +213,43 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
</div>
</div>
<div class="container">
- <NumberInput
+ <PhoneNumberInput
label="Version"
placeholder="version number to recover"
grabFocus
- bind={[otherVersion, setOtherVersion]} />
+ bind={[otherVersion, setOtherVersion]}
+ />
</div>
</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={onCancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
<div class="buttons">
- <AsyncButton class="button" onClick={() => onConfirm(otherProvider, 0)}>Use latest</AsyncButton>
- <AsyncButton class="button is-info" onClick={() => onConfirm(otherProvider, parseInt(otherVersion, 10))}>Confirm</AsyncButton>
+ <AsyncButton
+ class="button"
+ onClick={() => onConfirm(otherProvider, 0)}
+ >
+ Use latest
+ </AsyncButton>
+ <AsyncButton
+ class="button is-info"
+ onClick={() =>
+ onConfirm(otherProvider, parseInt(otherVersion, 10))
+ }
+ >
+ Confirm
+ </AsyncButton>
</div>
</div>
</div>
- <div class="column">
- .
- </div>
</div>
-
</AnastasisClientFrame>
);
-
}
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index cb6561b3f..76d0700db 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,109 +15,63 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { SolveScreen as TestedComponent } from './SolveScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import {
+ ChallengeFeedbackStatus,
+ RecoveryStates,
+ ReducerState,
+} from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { SolveScreen as TestedComponent } from "./SolveScreen";
export default {
- title: 'Pages/recovery/SolveScreen',
+ title: "Pages/recovery/SolveChallenge/Solve",
component: TestedComponent,
args: {
order: 6,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const NoInformation = createExample(TestedComponent, reducerStatesExample.challengeSolving);
+export const NoInformation = createExample(
+ TestedComponent,
+ reducerStatesExample.challengeSolving,
+);
export const NotSupportedChallenge = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving,
recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'does P equals NP?',
- type: 'chall-type',
- uuid: 'ASDASDSAD!1'
- }],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "chall-type",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
policies: [],
},
- selected_challenge_uuid: 'ASDASDSAD!1'
+ selected_challenge_uuid: "ASDASDSAD!1",
} as ReducerState);
export const MismatchedChallengeId = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving,
recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'does P equals NP?',
- type: 'chall-type',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'no-no-no'
-} as ReducerState);
-
-export const SmsChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'SMS to 555-5555',
- type: 'sms',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const QuestionChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'does P equals NP?',
- type: 'question',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const EmailChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'Email to sebasjm@some-domain.com',
- type: 'email',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const PostChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'Letter to address in postal code ABC123',
- type: 'post',
- uuid: 'ASDASDSAD!1'
- }],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "chall-type",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
policies: [],
},
- selected_challenge_uuid: 'ASDASDSAD!1'
+ selected_challenge_uuid: "no-no-no",
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
index bc1a88db3..b87dad2ce 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
@@ -1,50 +1,132 @@
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { h, VNode } from "preact";
import { AnastasisClientFrame } from ".";
import {
ChallengeFeedback,
ChallengeFeedbackStatus,
- ChallengeInfo,
} from "../../../../anastasis-core/lib";
-import { AsyncButton } from "../../components/AsyncButton";
-import { TextInput } from "../../components/fields/TextInput";
+import { Notifications } from "../../components/Notifications";
import { useAnastasisContext } from "../../context/anastasis";
+import { authMethods, KnownAuthMethods } from "./authMethod";
-function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
+export function SolveOverviewFeedbackDisplay(props: {
+ feedback?: ChallengeFeedback;
+}): VNode {
const { feedback } = props;
if (!feedback) {
- return null;
+ return <div />;
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
return (
- <div>
- <p>{feedback.message}</p>
- </div>
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: feedback.message,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.Payment:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: (
+ <span>
+ To pay you can <a href={feedback.taler_pay_uri}>click here</a>
+ </span>
+ ),
+ },
+ ]}
+ />
);
- case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban:
- return null;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: `Need to send a wire transfer to "${feedback.business_name}"`,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.ServerFailure:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Server error: Code ${feedback.http_status}`,
+ description: feedback.error_response,
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.RateLimitExceeded:
- return <div>Rate limit exceeded.</div>;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Message from provider`,
+ description: "There were to many failed attempts.",
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.Redirect:
- return <div>Redirect (FIXME: not supported)</div>;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: (
+ <span>
+ Please visit this link: <a>{feedback.redirect_url}</a>
+ </span>
+ ),
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.Unsupported:
- return <div>Challenge not supported by client.</div>;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `This client doesn't support solving this type of challenge`,
+ description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"`,
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.TruthUnknown:
- return <div>Truth unknown</div>;
- default:
return (
- <div>
- <pre>{JSON.stringify(feedback)}</pre>
- </div>
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Provider doesn't recognize the type of challenge`,
+ description: "Contact the provider for further information",
+ },
+ ]}
+ />
);
+ default:
+ return <div />;
}
}
export function SolveScreen(): VNode {
const reducer = useAnastasisContext();
- const [answer, setAnswer] = useState("");
if (!reducer) {
return (
@@ -78,161 +160,54 @@ export function SolveScreen(): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- </div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+ function SolveNotImplemented(): VNode {
+ return (
+ <AnastasisClientFrame hideNav title="Not implemented">
+ <p>
+ The challenge selected is not supported for this UI. Please update
+ this version or try using another policy.
+ </p>
+ {reducer && (
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ )}
</AnastasisClientFrame>
);
}
const chArr = reducer.currentReducerState.recovery_information.challenges;
- const challengeFeedback =
- reducer.currentReducerState.challenge_feedback ?? {};
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
- const challenges: {
- [uuid: string]: ChallengeInfo;
- } = {};
- for (const ch of chArr) {
- challenges[ch.uuid] = ch;
- }
- const selectedChallenge = challenges[selectedUuid];
- const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
- question: SolveQuestionEntry,
- sms: SolveSmsEntry,
- email: SolveEmailEntry,
- post: SolvePostEntry,
- };
- const SolveDialog =
- selectedChallenge === undefined
- ? SolveUndefinedEntry
- : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
-
- async function onNext(): Promise<void> {
- return reducer?.transition("solve_challenge", { answer });
- }
- function onCancel(): void {
- reducer?.back();
- }
+ const selectedChallenge = chArr.find((ch) => ch.uuid === selectedUuid);
- return (
- <AnastasisClientFrame hideNav title="Recovery: Solve challenge">
- <SolveOverviewFeedbackDisplay
- feedback={challengeFeedback[selectedUuid]}
- />
- <SolveDialog
- id={selectedUuid}
- answer={answer}
- setAnswer={setAnswer}
- challenge={selectedChallenge}
- feedback={challengeFeedback[selectedUuid]}
- />
-
- <div
- style={{
- marginTop: "2em",
- display: "flex",
- justifyContent: "space-between",
- }}
- >
- <button class="button" onClick={onCancel}>
- Cancel
- </button>
- <AsyncButton class="button is-info" onClick={onNext}>
- Confirm
- </AsyncButton>
- </div>
- </AnastasisClientFrame>
- );
-}
-
-export interface SolveEntryProps {
- id: string;
- challenge: ChallengeInfo;
- feedback?: ChallengeFeedback;
- answer: string;
- setAnswer: (s: string) => void;
-}
-
-function SolveSmsEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- An sms has been sent to "<b>{challenge.instructions}</b>". Type the code
- below
- </p>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
-function SolveQuestionEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>Type the answer to the following question:</p>
- <pre>{challenge.instructions}</pre>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
-
-function SolvePostEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- instruction for post type challenge "<b>{challenge.instructions}</b>"
- </p>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
-
-function SolveEmailEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- An email has been sent to "<b>{challenge.instructions}</b>". Type the
- code below
- </p>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
+ const SolveDialog =
+ !selectedChallenge ||
+ !authMethods[selectedChallenge.type as KnownAuthMethods]
+ ? SolveNotImplemented
+ : authMethods[selectedChallenge.type as KnownAuthMethods].solve ??
+ SolveNotImplemented;
-function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- The challenge selected is not supported for this UI. Please update this
- version or try using another policy.
- </p>
- <p>
- <b>Challenge type:</b> {props.challenge.type}
- </p>
- </Fragment>
- );
-}
-function SolveUndefinedEntry(props: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- There is no challenge information for id <b>"{props.id}"</b>. Try
- resetting the recovery session.
- </p>
- </Fragment>
- );
+ return <SolveDialog id={selectedUuid} />;
}
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index 657a2dd74..fcddaf87a 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -15,24 +15,26 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { StartScreen as TestedComponent } from './StartScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../utils";
+import { StartScreen as TestedComponent } from "./StartScreen";
export default {
- title: 'Pages/StartScreen',
+ title: "Pages/Start",
component: TestedComponent,
args: {
order: 1,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const InitialState = createExample(TestedComponent, reducerStatesExample.initial); \ No newline at end of file
+export const InitialState = createExample(
+ TestedComponent,
+ reducerStatesExample.initial,
+);
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
index d53df4cae..8b24ef684 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
@@ -1,27 +1,36 @@
-
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function StartScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
return (
<AnastasisClientFrame hideNav title="Home">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
-
<div class="buttons">
- <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
- <div class="icon"><i class="mdi mdi-arrow-up" /></div>
+ <button
+ class="button is-success"
+ autoFocus
+ onClick={() => reducer.startBackup()}
+ >
+ <div class="icon">
+ <i class="mdi mdi-arrow-up" />
+ </div>
<span>Backup a secret</span>
</button>
- <button class="button is-info" onClick={() => reducer.startRecover()}>
- <div class="icon"><i class="mdi mdi-arrow-down" /></div>
+ <button
+ class="button is-info"
+ onClick={() => reducer.startRecover()}
+ >
+ <div class="icon">
+ <i class="mdi mdi-arrow-down" />
+ </div>
<span>Recover a secret</span>
</button>
@@ -30,7 +39,6 @@ export function StartScreen(): VNode {
<span>Restore a session</span>
</button> */}
</div>
-
</div>
<div class="column" />
</div>
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index 7568ccd69..245ad8889 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -15,29 +15,31 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen";
export default {
- title: 'Pages/backup/__TruthsPayingScreen',
+ title: "Pages/backup/__TruthsPaying",
component: TestedComponent,
args: {
order: 10,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.truthsPaying);
+export const Example = createExample(
+ TestedComponent,
+ reducerStatesExample.truthsPaying,
+);
export const WithPaytoList = createExample(TestedComponent, {
...reducerStatesExample.truthsPaying,
- payments: ['payto://x-taler-bank/bank/account']
+ payments: ["payto://x-taler-bank/bank/account"],
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
index 0b32e0db5..6f95fa93b 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
@@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function TruthsPayingScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.payments ?? [];
return (
- <AnastasisClientFrame
- hideNext={"FIXME"}
- title="Backup: Truths Paying"
- >
+ <AnastasisClientFrame hideNext={"FIXME"} title="Backup: Truths Paying">
<p>
Some of the providers require a payment to store the encrypted
authentication information.
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
index e178a4955..080a7ab31 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,51 +15,67 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/email',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/email",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'email'
+const type: KnownAuthMethods = "email";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ 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 WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to sebasjm@email.com ",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].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
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to sebasjm@email.com",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to someone@sebasjm.com",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
index 1a6be1b61..556e3bdbf 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
@@ -1,59 +1,94 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+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 { EmailInput } from "../../../components/fields/EmailInput";
+import { AnastasisClientFrame } from "../index";
+import { AuthMethodSetupProps } from "./index";
-const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-export function AuthMethodEmailSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodEmailSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [email, setEmail] = useState("");
- const addEmailAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Email to ${email}`,
- challenge: encodeCrock(stringToBytes(email)),
- },
- });
- const emailError = !EMAIL_PATTERN.test(email) ? 'Email address is not valid' : undefined
- const errors = !email ? 'Add your email' : emailError
+ const addEmailAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "email",
+ instructions: `Email to ${email}`,
+ challenge: encodeCrock(stringToBytes(email)),
+ },
+ });
+ const emailError = !EMAIL_PATTERN.test(email)
+ ? "Email address is not valid"
+ : undefined;
+ const errors = !email ? "Add your email" : emailError;
+ function goNextIfNoErrors(): void {
+ if (!errors) addEmailAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<p>
For email authentication, you need to provide an email address. When
recovering your secret, you will need to enter the code you receive by
- email.
+ email. Add the uuid from the challenge
</p>
<div>
<EmailInput
label="Email address"
error={emailError}
+ onConfirm={goNextIfNoErrors}
placeholder="email@domain.com"
- bind={[email, setEmail]} />
+ bind={[email, setEmail]}
+ />
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your emails:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div></section>}
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your emails:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addEmailAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addEmailAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
new file mode 100644
index 000000000..729fa8a1b
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/email",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "email";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "Email to me@domain.com",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
+
+export const PaymentFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "Email to me@domain.com",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ challenge_feedback: {
+ "uuid-1": {
+ state: ChallengeFeedbackStatus.Payment,
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ },
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
new file mode 100644
index 000000000..e50c3bb20
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
@@ -0,0 +1,148 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("A-");
+ const [expanded, setExpanded] = useState(false);
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Email challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ An email has been sent to "<b>{selectedChallenge.instructions}</b>". The
+ message has and identification code and recovery code that starts with "
+ <b>A-</b>". Wait the message to arrive and the enter the recovery code
+ below.
+ </p>
+ {!expanded ? (
+ <p>
+ The identification code in the email should start with "
+ {selectedUuid.substring(0, 10)}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to expand"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ ) : (
+ <p>
+ The identification code in the email is "{selectedUuid}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to show less code"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ )}
+ <TextInput
+ label="Answer"
+ grabFocus
+ onConfirm={onNext}
+ bind={[answer, setAnswer]}
+ placeholder="A-1234567812345678"
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
index 71f618646..c521e18fd 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,50 +15,66 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/IBAN',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/IBAN",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'iban'
+const type: KnownAuthMethods = "iban";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ 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
- }]
-},);
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
+ remove: () => null,
+ },
+ ],
+ },
+);
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Javier",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
index c9edbfa07..501a40600 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
@@ -1,65 +1,111 @@
-/* eslint-disable @typescript-eslint/camelcase */
import {
canonicalJson,
encodeCrock,
- stringToBytes
+ stringToBytes,
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { AuthMethodSetupProps } from ".";
import { TextInput } from "../../../components/fields/TextInput";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
import { AnastasisClientFrame } from "../index";
-export function AuthMethodIbanSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodIbanSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("");
const [account, setAccount] = useState("");
- const addIbanAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "iban",
- instructions: `Wire transfer from ${account} with holder ${name}`,
- challenge: encodeCrock(stringToBytes(canonicalJson({
- name, account
- }))),
- },
- });
- const errors = !name ? 'Add an account name' : (
- !account ? 'Add an account IBAN number' : undefined
- )
+ const addIbanAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "iban",
+ instructions: `Wire transfer from ${account} with holder ${name}`,
+ challenge: encodeCrock(
+ stringToBytes(
+ canonicalJson({
+ name,
+ account,
+ }),
+ ),
+ ),
+ },
+ });
+ const errors = !name
+ ? "Add an account name"
+ : !account
+ ? "Add an account IBAN number"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addIbanAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add bank transfer authentication">
<p>
- For bank transfer authentication, you need to provide a bank
- account (account holder name and IBAN). When recovering your
- secret, you will be asked to pay the recovery fee via bank
- transfer from the account you provided here.
+ For bank transfer authentication, you need to provide a bank account
+ (account holder name and IBAN). When recovering your secret, you will be
+ asked to pay the recovery fee via bank transfer from the account you
+ provided here.
</p>
<div>
<TextInput
label="Bank account holder name"
grabFocus
placeholder="John Smith"
- bind={[name, setName]} />
+ onConfirm={goNextIfNoErrors}
+ bind={[name, setName]}
+ />
<TextInput
label="IBAN"
placeholder="DE91100000000123456789"
- bind={[account, setAccount]} />
+ onConfirm={goNextIfNoErrors}
+ bind={[account, setAccount]}
+ />
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your bank accounts:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div></section>}
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your bank accounts:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addIbanAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addIbanAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
new file mode 100644
index 000000000..cbbc253e9
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/Iban",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "iban";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx
new file mode 100644
index 000000000..5cff7bf01
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx
@@ -0,0 +1,112 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="IBAN Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>Send a wire transfer to the address,</p>
+ <button class="button">Check</button>
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
index 0f1c17495..2977586ac 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -16,51 +16,67 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/Post',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Post",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'post'
+const type: KnownAuthMethods = "post";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ 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 WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code QWE456",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].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
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code QWE456",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code ABC123",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
index bfeaaa832..04e00500c 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
@@ -1,15 +1,19 @@
-/* eslint-disable @typescript-eslint/camelcase */
import {
- canonicalJson, encodeCrock,
- stringToBytes
+ canonicalJson,
+ encodeCrock,
+ stringToBytes,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
-import { TextInput } from "../../../components/fields/TextInput";
import { AnastasisClientFrame } from "..";
+import { TextInput } from "../../../components/fields/TextInput";
+import { AuthMethodSetupProps } from "./index";
-export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodPostSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [fullName, setFullName] = useState("");
const [street, setStreet] = useState("");
const [city, setCity] = useState("");
@@ -33,68 +37,108 @@ export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthM
});
};
- const errors = !fullName ? 'The full name is missing' : (
- !street ? 'The street is missing' : (
- !city ? 'The city is missing' : (
- !postcode ? 'The postcode is missing' : (
- !country ? 'The country is missing' : undefined
- )
- )
- )
- )
+ const errors = !fullName
+ ? "The full name is missing"
+ : !street
+ ? "The street is missing"
+ : !city
+ ? "The city is missing"
+ : !postcode
+ ? "The postcode is missing"
+ : !country
+ ? "The country is missing"
+ : undefined;
+
+ function goNextIfNoErrors(): void {
+ if (!errors) addPostAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add postal authentication">
<p>
- For postal letter authentication, you need to provide a postal
- address. When recovering your secret, you will be asked to enter a
- code that you will receive in a letter to that address.
+ For postal letter authentication, you need to provide a postal address.
+ When recovering your secret, you will be asked to enter a code that you
+ will receive in a letter to that address.
</p>
<div>
<TextInput
grabFocus
label="Full Name"
bind={[fullName, setFullName]}
+ onConfirm={goNextIfNoErrors}
/>
</div>
<div>
<TextInput
+ onConfirm={goNextIfNoErrors}
label="Street"
bind={[street, setStreet]}
/>
</div>
<div>
<TextInput
- label="City" bind={[city, setCity]}
+ onConfirm={goNextIfNoErrors}
+ label="City"
+ bind={[city, setCity]}
/>
</div>
<div>
<TextInput
- label="Postal Code" bind={[postcode, setPostcode]}
+ onConfirm={goNextIfNoErrors}
+ label="Postal Code"
+ bind={[postcode, setPostcode]}
/>
</div>
<div>
<TextInput
+ onConfirm={goNextIfNoErrors}
label="Country"
bind={[country, setCountry]}
/>
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your postal code:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div>
- </section>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your postal code:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addPostAuth}
+ >
+ Add
+ </button>
</span>
</div>
</AnastasisClientFrame>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
new file mode 100644
index 000000000..3b67ee884
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/post",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "post";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
new file mode 100644
index 000000000..1bbbbfc03
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
@@ -0,0 +1,117 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("A-");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Postal Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>Wait for the answer</p>
+ <TextInput
+ onConfirm={onNext}
+ label="Answer"
+ grabFocus
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
index 3ba4a84ca..991301cbf 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -16,51 +16,69 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/Question',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Question",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'question'
+const type: KnownAuthMethods = "question";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ 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 WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions:
+ "Is integer factorization polynomial? (non-quantum computer)",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].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
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Does P equal NP?",
+ remove: () => null,
+ },
+ {
+ challenge: "asd",
+ type,
+ instructions:
+ "Are continuous groups automatically differential groups?",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
index 04fa00d59..19260c4ff 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
@@ -1,33 +1,39 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput";
-export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodQuestionSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [questionText, setQuestionText] = useState("");
const [answerText, setAnswerText] = useState("");
- const addQuestionAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "question",
- instructions: questionText,
- challenge: encodeCrock(stringToBytes(answerText)),
- },
- });
+ const addQuestionAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "question",
+ instructions: questionText,
+ challenge: encodeCrock(stringToBytes(answerText)),
+ },
+ });
- const errors = !questionText ? "Add your security question" : (
- !answerText ? 'Add the answer to your question' : undefined
- )
+ const errors = !questionText
+ ? "Add your security question"
+ : !answerText
+ ? "Add the answer to your question"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addQuestionAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add Security Question">
<div>
<p>
- For2 security question authentication, you need to provide a question
+ 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.
@@ -36,36 +42,67 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
<TextInput
label="Security question"
grabFocus
+ onConfirm={goNextIfNoErrors}
placeholder="Your question"
- bind={[questionText, setQuestionText]} />
+ bind={[questionText, setQuestionText]}
+ />
</div>
<div>
<TextInput
label="Answer"
+ onConfirm={goNextIfNoErrors}
placeholder="Your answer"
bind={[answerText, setAnswerText]}
/>
</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addQuestionAuth}
+ >
+ Add
+ </button>
</span>
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your security questions:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div></section>}
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your security questions:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
</div>
- </AnastasisClientFrame >
+ </AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
new file mode 100644
index 000000000..1fa9fd6ec
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
@@ -0,0 +1,258 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/question",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "question";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
+
+export const MessageFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Message,
+ message: "Challenge should be solved",
+ },
+ },
+} as ReducerState);
+
+export const ServerFailureFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.ServerFailure,
+ http_status: 500,
+ error_response: "Couldn't connect to mysql",
+ },
+ },
+ } as ReducerState,
+);
+
+export const RedirectFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Redirect,
+ http_status: 302,
+ redirect_url: "http://video.taler.net",
+ },
+ },
+} as ReducerState);
+
+export const MessageRateLimitExceededFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.RateLimitExceeded,
+ },
+ },
+ } as ReducerState,
+);
+
+export const UnsupportedFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Unsupported,
+ http_status: 500,
+ unsupported_method: "Question",
+ },
+ },
+} as ReducerState);
+
+export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.TruthUnknown,
+ },
+ },
+} as ReducerState);
+
+export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.AuthIban,
+ challenge_amount: "EUR:1",
+ credit_iban: "DE12345789000",
+ business_name: "Data Loss Incorporated",
+ wire_transfer_subject: "Anastasis 987654321",
+ answer_code: 987654321,
+ // Fields that follow are only for compatibility with C reducer,
+ // will be removed eventually,
+ details: {
+ business_name: "foo",
+ challenge_amount: "foo",
+ credit_iban: "foo",
+ wire_transfer_subject: "foo",
+ },
+ method: "iban",
+ },
+ },
+} as ReducerState);
+
+export const PaymentFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Payment,
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ },
+} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx
new file mode 100644
index 000000000..2636ca47c
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx
@@ -0,0 +1,121 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Question challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ In this challenge you need to provide the answer for the next question:
+ </p>
+ <pre>{selectedChallenge.instructions}</pre>
+ <p>Type the answer below</p>
+ <TextInput
+ label="Answer"
+ onConfirm={onNext}
+ grabFocus
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
index ae8297ef7..3a44c7ad0 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -16,51 +16,67 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/Sms',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Sms",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'sms'
+const type: KnownAuthMethods = "sms";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ 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 WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-1234-2345",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].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
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-1234-2345",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-5555-2345",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
index 9e85af2b2..e70b2a53b 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
@@ -1,15 +1,15 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
-import { NumberInput } from "../../../components/fields/NumberInput";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from ".";
+import { PhoneNumberInput } from "../../../components/fields/NumberInput";
import { AnastasisClientFrame } from "../index";
-export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodSmsSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [mobileNumber, setMobileNumber] = useState("");
const addSmsAuth = (): void => {
addAuthMethod({
@@ -24,7 +24,10 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
useLayoutEffect(() => {
inputRef.current?.focus();
}, []);
- const errors = !mobileNumber ? 'Add a mobile number' : undefined
+ const errors = !mobileNumber ? "Add a mobile number" : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addSmsAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add SMS authentication">
<div>
@@ -34,27 +37,57 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
receive via SMS.
</p>
<div class="container">
- <NumberInput
+ <PhoneNumberInput
label="Mobile number"
placeholder="Your mobile number"
+ onConfirm={goNextIfNoErrors}
grabFocus
- bind={[mobileNumber, setMobileNumber]} />
+ bind={[mobileNumber, setMobileNumber]}
+ />
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your mobile numbers:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
- </div>
- })}
- </div></section>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your mobile numbers:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginTop: "auto", marginBottom: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addSmsAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
new file mode 100644
index 000000000..e8961cccf
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/sms",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "sms";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "SMS to +54 11 2233 4455",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
new file mode 100644
index 000000000..3370c76d0
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
@@ -0,0 +1,148 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("A-");
+
+ const [expanded, setExpanded] = useState(false);
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="SMS Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ An sms has been sent to "<b>{selectedChallenge.instructions}</b>". The
+ message has and identification code and recovery code that starts with "
+ <b>A-</b>". Wait the message to arrive and the enter the recovery code
+ below.
+ </p>
+ {!expanded ? (
+ <p>
+ The identification code in the SMS should start with "
+ {selectedUuid.substring(0, 10)}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to expand"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ ) : (
+ <p>
+ The identification code in the SMS is "{selectedUuid}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to show less code"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ )}
+ <TextInput
+ label="Answer"
+ grabFocus
+ onConfirm={onNext}
+ bind={[answer, setAnswer]}
+ placeholder="A-1234567812345678"
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
index 4e46b600e..bc4628828 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -16,49 +16,65 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/TOTP',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/TOTP",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'totp'
+const type: KnownAuthMethods = "totp";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Enter 8 digits code for "Anastasis"',
- remove: () => null
- }]
-});
-export const WithMoreExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Enter 8 digits code for "Anastasis1"',
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: 'Enter 8 digits code for "Anastasis2"',
- remove: () => null
- }]
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ remove: () => null,
+ },
+ ],
+ },
+);
+export const WithMoreExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis1"',
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis2"',
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
index fd0bd0224..6b0dd7a79 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
@@ -1,40 +1,46 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput";
import { QR } from "../../../components/QR";
import { base32enc, computeTOTPandCheck } from "./totp";
-export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodTotpSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("anastasis");
const [test, setTest] = useState("");
- const digits = 8
+ const digits = 8;
const secretKey = useMemo(() => {
- const array = new Uint8Array(32)
- return window.crypto.getRandomValues(array)
- }, [])
+ const array = new Uint8Array(32);
+ return window.crypto.getRandomValues(array);
+ }, []);
const secret32 = base32enc(secretKey);
- const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`
+ const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`;
- const addTotpAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "totp",
- instructions: `Enter ${digits} digits code for "${name}"`,
- challenge: encodeCrock(stringToBytes(totpURL)),
- },
- });
+ const addTotpAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "totp",
+ instructions: `Enter ${digits} digits code for "${name}"`,
+ challenge: encodeCrock(stringToBytes(totpURL)),
+ },
+ });
const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
- const errors = !name ? 'The TOTP name is missing' : (
- !testCodeMatches ? 'The test code doesnt match' : undefined
- );
+ const errors = !name
+ ? "The TOTP name is missing"
+ : !testCodeMatches
+ ? "The test code doesnt match"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addTotpAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add TOTP authentication">
<p>
@@ -43,10 +49,7 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
with your TOTP App to import the TOTP secret into your TOTP App.
</p>
<div class="block">
- <TextInput
- label="TOTP Name"
- grabFocus
- bind={[name, setName]} />
+ <TextInput label="TOTP Name" grabFocus bind={[name, setName]} />
</div>
<div style={{ height: 300 }}>
<QR text={totpURL} />
@@ -56,23 +59,53 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
</p>
<TextInput
label="Test code"
- bind={[test, setTest]} />
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your TOTP numbers:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
- </div>
- })}
- </div></section>}
+ onConfirm={goNextIfNoErrors}
+ bind={[test, setTest]}
+ />
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your TOTP numbers:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginTop: "auto", marginBottom: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addTotpAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addTotpAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
new file mode 100644
index 000000000..8743c5a73
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/totp",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "totp";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx
new file mode 100644
index 000000000..347f9bf03
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx
@@ -0,0 +1,118 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="TOTP Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>enter the totp solution</p>
+ <TextInput
+ label="Answer"
+ onConfirm={onNext}
+ grabFocus
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+// NKE8 VD857T X033X6RG WEGPYP6D70 Q7YE XN8D2 ZN79SCN 231B4QK0
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
index 3c4c7bf39..4aad0a097 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
@@ -16,51 +16,68 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
-import logoImage from '../../../assets/logo.jpeg'
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+import logoImage from "../../../assets/logo.jpeg";
export default {
- title: 'Pages/backup/authMethods/Video',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Video",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'video'
+const type: KnownAuthMethods = "video";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: logoImage,
- remove: () => null
- }]
-});
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: logoImage,
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: logoImage,
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: logoImage,
- remove: () => null
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: logoImage,
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: logoImage,
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
index 8be999b3f..04a129c4a 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
@@ -1,54 +1,90 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ImageInput } from "../../../components/fields/ImageInput";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
-export function AuthMethodVideoSetup({cancel, addAuthMethod, configured}: AuthMethodSetupProps): VNode {
+export function AuthMethodVideoSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [image, setImage] = useState("");
const addVideoAuth = (): void => {
addAuthMethod({
authentication_method: {
type: "video",
- instructions: 'Join a video call',
+ instructions: "Join a video call",
challenge: encodeCrock(stringToBytes(image)),
},
- })
+ });
};
+ function goNextIfNoErrors(): void {
+ addVideoAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add video authentication">
<p>
- For video identification, you need to provide a passport-style
- photograph. When recovering your secret, you will be asked to join a
- video call. During that call, a human will use the photograph to
- verify your identity.
+ For video identification, you need to provide a passport-style
+ photograph. When recovering your secret, you will be asked to join a
+ video call. During that call, a human will use the photograph to verify
+ your identity.
</p>
- <div style={{textAlign:'center'}}>
+ <div style={{ textAlign: "center" }}>
<ImageInput
label="Choose photograph"
grabFocus
- bind={[image, setImage]} />
+ onConfirm={goNextIfNoErrors}
+ bind={[image, setImage]}
+ />
</div>
- {configured.length > 0 && <section class="section">
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your photographs:</div>
<div class="block">
- Your photographs:
- </div><div class="block">
{configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <img style={{ marginTop: 'auto', marginBottom: 'auto', width: 100, height:100, border: 'solid 1px black' }} src={c.instructions} />
- <div style={{marginTop: 'auto', marginBottom: 'auto'}}><button class="button is-danger" onClick={c.remove}>Delete</button></div>
- </div>
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <img
+ style={{
+ marginTop: "auto",
+ marginBottom: "auto",
+ width: 100,
+ height: 100,
+ border: "solid 1px black",
+ }}
+ src={c.instructions}
+ />
+ <div style={{ marginTop: "auto", marginBottom: "auto" }}>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
})}
- </div></section>}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
- <button class="button is-info" onClick={addVideoAuth}>Add</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <button class="button is-info" onClick={addVideoAuth}>
+ Add
+ </button>
</div>
</div>
</AnastasisClientFrame>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx
new file mode 100644
index 000000000..7c5511c5a
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/video",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "video";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx
new file mode 100644
index 000000000..efadb9a9a
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx
@@ -0,0 +1,112 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Add email authentication">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>You are gonna be called to check your identity</p>
+ <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
index 7b0cce883..b4f649488 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
@@ -1,25 +1,60 @@
+import { AuthMethod } from "anastasis-core";
import { h, VNode } from "preact";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import postalIcon from "../../../assets/icons/auth_method/postal.svg";
+import questionIcon from "../../../assets/icons/auth_method/question.svg";
+import smsIcon from "../../../assets/icons/auth_method/sms.svg";
+import videoIcon from "../../../assets/icons/auth_method/video.svg";
+import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup";
+import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve";
+import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup";
+import { AuthMethodPostSetup as PostalSetup } from "./AuthMethodPostSetup";
+import { AuthMethodQuestionSetup as QuestionSetup } from "./AuthMethodQuestionSetup";
+import { AuthMethodSmsSetup as SmsSetup } from "./AuthMethodSmsSetup";
+import { AuthMethodTotpSetup as TotpSetup } from "./AuthMethodTotpSetup";
+import { AuthMethodVideoSetup as VideoSetup } from "./AuthMethodVideoSetup";
-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';
+import { AuthMethodIbanSolve as IbanSolve } from "./AuthMethodIbanSolve";
+import { AuthMethodPostSolve as PostalSolve } from "./AuthMethodPostSolve";
+import { AuthMethodQuestionSolve as QuestionSolve } from "./AuthMethodQuestionSolve";
+import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve";
+import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve";
+import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve";
+
+export type AuthMethodWithRemove = AuthMethod & { remove: () => void };
+
+export interface AuthMethodSetupProps {
+ method: string;
+ addAuthMethod: (x: any) => void;
+ configured: AuthMethodWithRemove[];
+ cancel: () => void;
+}
+
+export interface AuthMethodSolveProps {
+ id: string;
+}
interface AuthMethodConfiguration {
icon: VNode;
label: string;
- screen: (props: AuthMethodSetupProps) => VNode;
+ setup: (props: AuthMethodSetupProps) => VNode;
+ solve: (props: AuthMethodSolveProps) => VNode;
skip?: boolean;
}
-export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
+// export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
+
+const ALL_METHODS = [
+ "sms",
+ "email",
+ "post",
+ "question",
+ "video",
+ "totp",
+ "iban",
+] as const;
+export type KnownAuthMethods = typeof ALL_METHODS[number];
+export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
+ return ALL_METHODS.includes(value as KnownAuthMethods);
+}
type KnowMethodConfig = {
[name in KnownAuthMethods]: AuthMethodConfiguration;
@@ -29,41 +64,44 @@ export const authMethods: KnowMethodConfig = {
question: {
icon: <img src={questionIcon} />,
label: "Question",
- screen: QuestionScreen
+ setup: QuestionSetup,
+ solve: QuestionSolve,
},
sms: {
icon: <img src={smsIcon} />,
label: "SMS",
- screen: SmsScreen
+ setup: SmsSetup,
+ solve: SmsSolve,
},
email: {
icon: <i class="mdi mdi-email" />,
label: "Email",
- screen: EmailScreen
-
+ setup: EmailSetup,
+ solve: EmailSolve,
},
iban: {
icon: <i class="mdi mdi-bank" />,
label: "IBAN",
- screen: IbanScreen
-
+ setup: IbanSetup,
+ solve: IbanSolve,
},
post: {
icon: <img src={postalIcon} />,
label: "Physical mail",
- screen: PostalScreen
-
+ setup: PostalSetup,
+ solve: PostalSolve,
},
totp: {
icon: <i class="mdi mdi-devices" />,
label: "TOTP",
- screen: TotpScreen
-
+ setup: TotpSetup,
+ solve: TotpSolve,
},
video: {
icon: <img src={videoIcon} />,
label: "Video",
- screen: VideScreen,
- skip: true,
- }
-} \ No newline at end of file
+ setup: VideoSetup,
+ solve: VideoSolve,
+ skip: true,
+ },
+};
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
index 0bc3feaf8..c2288671c 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
+++ b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
@@ -1,54 +1,61 @@
/* eslint-disable @typescript-eslint/camelcase */
-import jssha from 'jssha'
+import jssha from "jssha";
-const SEARCH_RANGE = 16
-const timeStep = 30
+const SEARCH_RANGE = 16;
+const timeStep = 30;
-export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean {
- const now = new Date().getTime()
+export function computeTOTPandCheck(
+ secretKey: Uint8Array,
+ digits: number,
+ code: number,
+): boolean {
+ const now = new Date().getTime();
const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
const movingFactor = (epoch + ms).toString(16).padStart(16, "0");
- const hmacSha = new jssha('SHA-1', 'HEX', { hmacKey: { value: secretKey, format: 'UINT8ARRAY' } });
+ const hmacSha = new jssha("SHA-1", "HEX", {
+ hmacKey: { value: secretKey, format: "UINT8ARRAY" },
+ });
hmacSha.update(movingFactor);
- const hmac_text = hmacSha.getHMAC('UINT8ARRAY');
+ const hmac_text = hmacSha.getHMAC("UINT8ARRAY");
- const offset = (hmac_text[hmac_text.length - 1] & 0xf)
+ const offset = hmac_text[hmac_text.length - 1] & 0xf;
- const otp = ((
- (hmac_text[offset + 0] << 24) +
- (hmac_text[offset + 1] << 16) +
- (hmac_text[offset + 2] << 8) +
- (hmac_text[offset + 3])
- ) & 0x7fffffff) % Math.pow(10, digits)
+ const otp =
+ (((hmac_text[offset + 0] << 24) +
+ (hmac_text[offset + 1] << 16) +
+ (hmac_text[offset + 2] << 8) +
+ hmac_text[offset + 3]) &
+ 0x7fffffff) %
+ Math.pow(10, digits);
- if (otp == code) return true
+ if (otp == code) return true;
}
- return false
+ return false;
}
-const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split('')
+const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split("");
export function base32enc(buffer: Uint8Array): string {
- let rpos = 0
- let bits = 0
- let vbit = 0
+ let rpos = 0;
+ let bits = 0;
+ let vbit = 0;
- let result = ""
- while ((rpos < buffer.length) || (vbit > 0)) {
- if ((rpos < buffer.length) && (vbit < 5)) {
+ let result = "";
+ while (rpos < buffer.length || vbit > 0) {
+ if (rpos < buffer.length && vbit < 5) {
bits = (bits << 8) | buffer[rpos++];
vbit += 8;
}
if (vbit < 5) {
- bits <<= (5 - vbit);
+ bits <<= 5 - vbit;
vbit = 5;
}
result += encTable__[(bits >> (vbit - 5)) & 31];
vbit -= 5;
}
- return result
+ return result;
}
// const array = new Uint8Array(256)
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 07bc7c604..d83442e62 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -1,25 +1,22 @@
+import { BackupStates, RecoveryStates } from "anastasis-core";
import {
- BackupStates,
- RecoveryStates,
- ReducerStateBackup,
- ReducerStateRecovery
-} from "anastasis-core";
-import {
- ComponentChildren, Fragment,
+ ComponentChildren,
+ Fragment,
FunctionalComponent,
h,
- VNode
+ VNode,
} from "preact";
-import {
- useErrorBoundary
-} from "preact/hooks";
+import { useErrorBoundary } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
import { Menu } from "../../components/menu";
import { Notifications } from "../../components/Notifications";
-import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
+import {
+ AnastasisProvider,
+ useAnastasisContext,
+} from "../../context/anastasis";
import {
AnastasisReducerApi,
- useAnastasisReducer
+ useAnastasisReducer,
} from "../../hooks/use-anastasis-reducer";
import { AttributeEntryScreen } from "./AttributeEntryScreen";
import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
@@ -51,7 +48,11 @@ export function withProcessLabel(
}
interface AnastasisClientFrameProps {
- onNext?(): void;
+ onNext?(): Promise<void>;
+ /**
+ * Override for the "back" functionality.
+ */
+ onBack?(): Promise<void>;
title: string;
children: ComponentChildren;
/**
@@ -118,9 +119,27 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
<section class="section is-main-section">
{props.children}
{!props.hideNav ? (
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- <AsyncButton class="button is-info" data-tooltip={props.hideNext} onClick={next} disabled={props.hideNext !== undefined}>Next</AsyncButton>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button
+ class="button"
+ onClick={() => (props.onBack ?? reducer.back)()}
+ >
+ Back
+ </button>
+ <AsyncButton
+ class="button is-info"
+ data-tooltip={props.hideNext}
+ onClick={next}
+ disabled={props.hideNext !== undefined}
+ >
+ Next
+ </AsyncButton>
</div>
) : null}
</section>
@@ -141,7 +160,7 @@ const AnastasisClient: FunctionalComponent = () => {
};
function AnastasisClientImpl(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
@@ -157,27 +176,19 @@ function AnastasisClientImpl(): VNode {
state.backup_state === BackupStates.CountrySelecting ||
state.recovery_state === RecoveryStates.CountrySelecting
) {
- return (
- <ContinentSelectionScreen />
- );
+ return <ContinentSelectionScreen />;
}
if (
state.backup_state === BackupStates.UserAttributesCollecting ||
state.recovery_state === RecoveryStates.UserAttributesCollecting
) {
- return (
- <AttributeEntryScreen />
- );
+ return <AttributeEntryScreen />;
}
if (state.backup_state === BackupStates.AuthenticationsEditing) {
- return (
- <AuthenticationEditorScreen />
- );
+ return <AuthenticationEditorScreen />;
}
if (state.backup_state === BackupStates.PoliciesReviewing) {
- return (
- <ReviewPoliciesScreen />
- );
+ return <ReviewPoliciesScreen />;
}
if (state.backup_state === BackupStates.SecretEditing) {
return <SecretEditorScreen />;
@@ -196,15 +207,11 @@ function AnastasisClientImpl(): VNode {
}
if (state.recovery_state === RecoveryStates.SecretSelecting) {
- return (
- <SecretSelectionScreen />
- );
+ return <SecretSelectionScreen />;
}
if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
- return (
- <ChallengeOverviewScreen />
- );
+ return <ChallengeOverviewScreen />;
}
if (state.recovery_state === RecoveryStates.ChallengeSolving) {
@@ -212,9 +219,7 @@ function AnastasisClientImpl(): VNode {
}
if (state.recovery_state === RecoveryStates.RecoveryFinished) {
- return (
- <RecoveryFinishedScreen />
- );
+ return <RecoveryFinishedScreen />;
}
if (state.recovery_state === RecoveryStates.ChallengePaying) {
return <ChallengePayingScreen />;
@@ -224,7 +229,9 @@ function AnastasisClientImpl(): VNode {
<AnastasisClientFrame hideNav title="Bug">
<p>Bug: Unknown state.</p>
<div class="buttons is-right">
- <button class="button" onClick={() => reducer.reset()}>Reset</button>
+ <button class="button" onClick={() => reducer.reset()}>
+ Reset
+ </button>
</div>
</AnastasisClientFrame>
);
@@ -236,11 +243,17 @@ function AnastasisClientImpl(): VNode {
function ErrorBanner(): VNode | null {
const reducer = useAnastasisContext();
if (!reducer || !reducer.currentError) return null;
- return (<Notifications removeNotification={reducer.dismissError} notifications={[{
- type: "ERROR",
- message: `Error code: ${reducer.currentError.code}`,
- description: reducer.currentError.hint
- }]} />
+ return (
+ <Notifications
+ removeNotification={reducer.dismissError}
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Error code: ${reducer.currentError.code}`,
+ description: reducer.currentError.hint,
+ },
+ ]}
+ />
);
}
diff --git a/packages/anastasis-webui/src/pages/notfound/index.tsx b/packages/anastasis-webui/src/pages/notfound/index.tsx
index 4e74d1d9f..bb22429b0 100644
--- a/packages/anastasis-webui/src/pages/notfound/index.tsx
+++ b/packages/anastasis-webui/src/pages/notfound/index.tsx
@@ -1,16 +1,16 @@
-import { FunctionalComponent, h } from 'preact';
-import { Link } from 'preact-router/match';
+import { FunctionalComponent, h } from "preact";
+import { Link } from "preact-router/match";
const Notfound: FunctionalComponent = () => {
- return (
- <div>
- <h1>Error 404</h1>
- <p>That page doesn&apos;t exist.</p>
- <Link href="/">
- <h4>Back to Home</h4>
- </Link>
- </div>
- );
+ return (
+ <div>
+ <h1>Error 404</h1>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
};
export default Notfound;
diff --git a/packages/anastasis-webui/src/pages/profile/index.tsx b/packages/anastasis-webui/src/pages/profile/index.tsx
index 859a83ed4..bcd26370e 100644
--- a/packages/anastasis-webui/src/pages/profile/index.tsx
+++ b/packages/anastasis-webui/src/pages/profile/index.tsx
@@ -1,43 +1,42 @@
-import { FunctionalComponent, h } from 'preact';
-import { useEffect, useState } from 'preact/hooks';
+import { FunctionalComponent, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
interface Props {
- user: string;
+ user: string;
}
const Profile: FunctionalComponent<Props> = (props: Props) => {
- const { user } = props;
- const [time, setTime] = useState<number>(Date.now());
- const [count, setCount] = useState<number>(0);
-
- // gets called when this route is navigated to
- useEffect(() => {
- const timer = window.setInterval(() => setTime(Date.now()), 1000);
-
- // gets called just before navigating away from the route
- return (): void => {
- clearInterval(timer);
- };
- }, []);
-
- // update the current time
- const increment = (): void => {
- setCount(count + 1);
+ const { user } = props;
+ const [time, setTime] = useState<number>(Date.now());
+ const [count, setCount] = useState<number>(0);
+
+ // gets called when this route is navigated to
+ useEffect(() => {
+ const timer = window.setInterval(() => setTime(Date.now()), 1000);
+
+ // gets called just before navigating away from the route
+ return (): void => {
+ clearInterval(timer);
};
+ }, []);
+
+ // update the current time
+ const increment = (): void => {
+ setCount(count + 1);
+ };
- return (
- <div>
- <h1>Profile: {user}</h1>
- <p>This is the user profile for a user named {user}.</p>
+ return (
+ <div>
+ <h1>Profile: {user}</h1>
+ <p>This is the user profile for a user named {user}.</p>
- <div>Current time: {new Date(time).toLocaleString()}</div>
+ <div>Current time: {new Date(time).toLocaleString()}</div>
- <p>
- <button onClick={increment}>Click Me</button> Clicked {count}{' '}
- times.
- </p>
- </div>
- );
+ <p>
+ <button onClick={increment}>Click Me</button> Clicked {count} times.
+ </p>
+ </div>
+ );
};
export default Profile;
diff --git a/packages/anastasis-webui/src/scss/DurationPicker.scss b/packages/anastasis-webui/src/scss/DurationPicker.scss
index a35575324..aa75b9916 100644
--- a/packages/anastasis-webui/src/scss/DurationPicker.scss
+++ b/packages/anastasis-webui/src/scss/DurationPicker.scss
@@ -1,4 +1,3 @@
-
.rdp-picker {
display: flex;
height: 175px;
diff --git a/packages/anastasis-webui/src/scss/_aside.scss b/packages/anastasis-webui/src/scss/_aside.scss
index c9332b252..11809990b 100644
--- a/packages/anastasis-webui/src/scss/_aside.scss
+++ b/packages/anastasis-webui/src/scss/_aside.scss
@@ -19,37 +19,35 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-@include desktop {
- html {
- &.has-aside-left {
- &.has-aside-expanded {
- nav.navbar,
- body {
- padding-left: $aside-width;
- }
- }
- aside.is-placed-left {
- display: block;
+html {
+ &.has-aside-left {
+ &.has-aside-expanded {
+ nav.navbar,
+ body {
+ padding-left: $aside-width;
}
}
+ aside.is-placed-left {
+ display: block;
+ }
}
+}
- aside.aside.is-expanded {
- width: $aside-width;
+aside.aside.is-expanded {
+ width: $aside-width;
- .menu-list {
- @include icon-with-update-mark($aside-icon-width);
+ .menu-list {
+ @include icon-with-update-mark($aside-icon-width);
- span.menu-item-label {
- display: inline-block;
- }
+ span.menu-item-label {
+ display: inline-block;
+ }
- li.is-active {
- ul {
- display: block;
- }
- background-color: $body-background-color;
+ li.is-active {
+ ul {
+ display: block;
}
+ background-color: $body-background-color;
}
}
}
@@ -128,59 +126,3 @@ aside.aside {
margin-bottom: $default-padding * 0.5;
}
}
-
-@include touch {
- nav.navbar {
- @include transition(margin-left);
- }
- aside.aside {
- @include transition(left);
- }
- html.has-aside-mobile-transition {
- body {
- overflow-x: hidden;
- }
- body,
- nav.navbar {
- width: 100vw;
- }
- aside.aside {
- width: $aside-mobile-width;
- display: block;
- left: $aside-mobile-width * -1;
-
- .image {
- img {
- max-width: $aside-mobile-width * 0.33;
- }
- }
-
- .menu-list {
- li.is-active {
- ul {
- display: block;
- }
- background-color: $body-background-color;
- }
- li {
- @include icon-with-update-mark($aside-icon-width);
- margin-top: 8px;
- margin-bottom: 8px;
- }
- a {
- span.menu-item-label {
- display: inline-block;
- }
- }
- }
- }
- }
- div.has-aside-mobile-expanded {
- nav.navbar {
- margin-left: $aside-mobile-width;
- }
- aside.aside {
- left: 0;
- }
- }
-}
diff --git a/packages/anastasis-webui/src/scss/_card.scss b/packages/anastasis-webui/src/scss/_card.scss
index b2eec27a1..3f71aeb6a 100644
--- a/packages/anastasis-webui/src/scss/_card.scss
+++ b/packages/anastasis-webui/src/scss/_card.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -39,7 +39,7 @@
&.is-card-widget {
.card-content {
- padding: $default-padding * .5;
+ padding: $default-padding * 0.5;
}
}
diff --git a/packages/anastasis-webui/src/scss/_custom-calendar.scss b/packages/anastasis-webui/src/scss/_custom-calendar.scss
index bff68cf79..e0334b62d 100644
--- a/packages/anastasis-webui/src/scss/_custom-calendar.scss
+++ b/packages/anastasis-webui/src/scss/_custom-calendar.scss
@@ -16,31 +16,30 @@
:root {
--primary-color: #3298dc;
-
- --primary-text-color-dark: rgba(0,0,0,.87);
- --secondary-text-color-dark: rgba(0,0,0,.57);
- --disabled-text-color-dark: rgba(0,0,0,.13);
-
- --primary-text-color-light: rgba(255,255,255,.87);
- --secondary-text-color-light: rgba(255,255,255,.57);
- --disabled-text-color-light: rgba(255,255,255,.13);
-
- --font-stack: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-
+
+ --primary-text-color-dark: rgba(0, 0, 0, 0.87);
+ --secondary-text-color-dark: rgba(0, 0, 0, 0.57);
+ --disabled-text-color-dark: rgba(0, 0, 0, 0.13);
+
+ --primary-text-color-light: rgba(255, 255, 255, 0.87);
+ --secondary-text-color-light: rgba(255, 255, 255, 0.57);
+ --disabled-text-color-light: rgba(255, 255, 255, 0.13);
+
+ --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
--primary-card-color: #fff;
--primary-background-color: #f2f2f2;
-
+
--box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
+ 0 1px 2px rgba(0, 0, 0, 0.24);
--box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
+ 0 3px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
- 0 6px 6px rgba(0, 0, 0, 0.23);
+ 0 6px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
- 0 10px 10px rgba(0, 0, 0, 0.22);
+ 0 10px 10px rgba(0, 0, 0, 0.22);
}
-
.home .datePicker div {
margin-top: 0px;
margin-bottom: 0px;
@@ -56,7 +55,7 @@
width: 90vw;
max-width: 448px;
transform-origin: top left;
- transition: transform .22s ease-in-out, opacity .22s ease-in-out;
+ transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out;
top: 50%;
left: 50%;
opacity: 0;
@@ -67,7 +66,7 @@
opacity: 1;
transform: scale(1) translate(-50%, -50%);
}
-
+
.datePicker--titles {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
@@ -75,7 +74,8 @@
height: 100px;
background: var(--primary-color);
- h2, h3 {
+ h2,
+ h3 {
cursor: pointer;
color: #fff;
line-height: 1;
@@ -85,7 +85,7 @@
}
h3 {
- color: rgba(255,255,255,.57);
+ color: rgba(255, 255, 255, 0.57);
font-size: 18px;
padding-bottom: 2px;
}
@@ -114,13 +114,13 @@
font-size: 26px;
user-select: none;
border-radius: 50%;
-
+
&:hover {
background: var(--disabled-text-color-dark);
}
}
}
-
+
.datePicker--scroll {
overflow-y: auto;
max-height: calc(90vh - 56px - 100px);
@@ -133,9 +133,11 @@
width: 100%;
display: grid;
text-align: center;
-
+
// there's probably a better way to do this, but wanted to try out CSS grid
- grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+ grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(
+ 100% / 7
+ ) calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--secondary-text-color-dark);
@@ -149,14 +151,16 @@
width: 100%;
display: grid;
text-align: center;
- grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+ grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(
+ 100% / 7
+ ) calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--primary-text-color-dark);
line-height: 42px;
font-size: 14px;
display: inline-grid;
- transition: color .22s;
+ transition: color 0.22s;
height: 42px;
position: relative;
cursor: pointer;
@@ -164,7 +168,7 @@
border-radius: 50%;
&::before {
- content: '';
+ content: "";
position: absolute;
z-index: -1;
height: 42px;
@@ -172,12 +176,12 @@
left: calc(50% - 21px);
background: var(--primary-color);
border-radius: 50%;
- transition: transform .22s, opacity .22s;
+ transition: transform 0.22s, opacity 0.22s;
transform: scale(0);
opacity: 0;
}
-
- &[disabled=true] {
+
+ &[disabled="true"] {
cursor: unset;
}
@@ -186,7 +190,7 @@
}
&.datePicker--selected {
- color: rgba(255,255,255,.87);
+ color: rgba(255, 255, 255, 0.87);
&:before {
transform: scale(1);
@@ -196,21 +200,21 @@
}
}
}
-
+
.datePicker--selectYear {
padding: 0 20px;
display: block;
width: 100%;
text-align: center;
max-height: 362px;
-
+
span {
display: block;
width: 100%;
font-size: 24px;
margin: 20px auto;
cursor: pointer;
-
+
&.selected {
font-size: 42px;
color: var(--primary-color);
@@ -236,9 +240,10 @@
appearance: none;
padding: 0 16px;
border-radius: 3px;
- transition: background-color .13s;
+ transition: background-color 0.13s;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
outline: none;
background-color: var(--disabled-text-color-dark);
}
@@ -253,6 +258,6 @@
left: 0;
bottom: 0;
right: 0;
- background: rgba(0,0,0,.52);
- animation: fadeIn .22s forwards;
+ background: rgba(0, 0, 0, 0.52);
+ animation: fadeIn 0.22s forwards;
}
diff --git a/packages/anastasis-webui/src/scss/_footer.scss b/packages/anastasis-webui/src/scss/_footer.scss
index 027a5ca8b..112522ed8 100644
--- a/packages/anastasis-webui/src/scss/_footer.scss
+++ b/packages/anastasis-webui/src/scss/_footer.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/anastasis-webui/src/scss/_form.scss b/packages/anastasis-webui/src/scss/_form.scss
index 71f0d4da4..786044eff 100644
--- a/packages/anastasis-webui/src/scss/_form.scss
+++ b/packages/anastasis-webui/src/scss/_form.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -22,11 +22,12 @@
.field {
&.has-check {
.field-body {
- margin-top: $default-padding * .125;
+ margin-top: $default-padding * 0.125;
}
}
.control {
- .mdi-24px.mdi-set, .mdi-24px.mdi:before {
+ .mdi-24px.mdi-set,
+ .mdi-24px.mdi:before {
font-size: inherit;
}
}
@@ -37,28 +38,34 @@
}
}
-.input, .textarea, select {
+.input,
+.textarea,
+select {
box-shadow: none;
- &:focus, &:active {
- box-shadow: none!important;
+ &:focus,
+ &:active {
+ box-shadow: none !important;
}
}
-.switch input[type=checkbox]+.check:before {
+.switch input[type="checkbox"] + .check:before {
box-shadow: none;
}
-.switch, .b-checkbox.checkbox {
- input[type=checkbox] {
- &:focus + .check, &:focus:checked + .check {
- box-shadow: none!important;
+.switch,
+.b-checkbox.checkbox {
+ input[type="checkbox"] {
+ &:focus + .check,
+ &:focus:checked + .check {
+ box-shadow: none !important;
}
}
}
-.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
- &+.check {
+.b-checkbox.checkbox input[type="checkbox"],
+.b-radio.radio input[type="radio"] {
+ & + .check {
border: $checkbox-border;
}
}
diff --git a/packages/anastasis-webui/src/scss/_hero-bar.scss b/packages/anastasis-webui/src/scss/_hero-bar.scss
index 90b67a2ed..31b7e623e 100644
--- a/packages/anastasis-webui/src/scss/_hero-bar.scss
+++ b/packages/anastasis-webui/src/scss/_hero-bar.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -32,17 +32,17 @@ section.hero.is-hero-bar {
}
> div > .level {
- margin-bottom: $default-padding * .5;
+ margin-bottom: $default-padding * 0.5;
}
.subtitle + p {
- margin-top: $default-padding * .5;
+ margin-top: $default-padding * 0.5;
}
}
.button {
&.is-hero-button {
- background-color: rgba($white, .5);
+ background-color: rgba($white, 0.5);
font-weight: 300;
@include transition(background-color);
diff --git a/packages/anastasis-webui/src/scss/_main-section.scss b/packages/anastasis-webui/src/scss/_main-section.scss
index 1a4fad81d..01edc24bf 100644
--- a/packages/anastasis-webui/src/scss/_main-section.scss
+++ b/packages/anastasis-webui/src/scss/_main-section.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss b/packages/anastasis-webui/src/scss/_mixins.scss
index 0809033ed..b52e590e3 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/anastasis-webui/src/scss/_mixins.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -23,12 +23,12 @@
transition: $t 250ms ease-in-out 50ms;
}
-@mixin icon-with-update-mark ($icon-base-width) {
+@mixin icon-with-update-mark($icon-base-width) {
.icon {
width: $icon-base-width;
&.has-update-mark:after {
- right: ($icon-base-width / 2) - .85;
+ right: ($icon-base-width / 2) - 0.85;
}
}
}
diff --git a/packages/anastasis-webui/src/scss/_modal.scss b/packages/anastasis-webui/src/scss/_modal.scss
index 3edbb8d3a..b3a31ebf1 100644
--- a/packages/anastasis-webui/src/scss/_modal.scss
+++ b/packages/anastasis-webui/src/scss/_modal.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/anastasis-webui/src/scss/_nav-bar.scss b/packages/anastasis-webui/src/scss/_nav-bar.scss
index 09f1e2326..c6dd04263 100644
--- a/packages/anastasis-webui/src/scss/_nav-bar.scss
+++ b/packages/anastasis-webui/src/scss/_nav-bar.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -25,7 +25,7 @@ nav.navbar {
.navbar-item {
&.has-user-avatar {
.is-user-avatar {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
display: inline-flex;
width: $navbar-avatar-size;
height: $navbar-avatar-size;
@@ -98,11 +98,11 @@ nav.navbar {
.navbar-item {
.icon:first-child {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
&.has-dropdown {
- >.navbar-link {
+ > .navbar-link {
background-color: $white-ter;
.icon:last-child {
display: none;
@@ -111,11 +111,11 @@ nav.navbar {
}
&.has-user-avatar {
- >.navbar-link {
+ > .navbar-link {
display: flex;
align-items: center;
- padding-top: $default-padding * .5;
- padding-bottom: $default-padding * .5;
+ padding-top: $default-padding * 0.5;
+ padding-bottom: $default-padding * 0.5;
}
}
}
@@ -131,7 +131,7 @@ nav.navbar {
&:not(.is-desktop-icon-only) {
.icon:first-child {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
}
&.is-desktop-icon-only {
diff --git a/packages/anastasis-webui/src/scss/_table.scss b/packages/anastasis-webui/src/scss/_table.scss
index 9cf6f4dcd..b68d50e4f 100644
--- a/packages/anastasis-webui/src/scss/_table.scss
+++ b/packages/anastasis-webui/src/scss/_table.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -26,7 +26,8 @@ table.table {
}
}
- td, th {
+ td,
+ th {
&.checkbox-cell {
.b-checkbox.checkbox:not(.button) {
margin-right: 0;
@@ -83,7 +84,9 @@ table.table {
}
}
- .pagination-previous, .pagination-next, .pagination-link {
+ .pagination-previous,
+ .pagination-next,
+ .pagination-link {
border-color: $button-border-color;
color: $base-color;
@@ -108,24 +111,25 @@ table.table {
&.has-mobile-sort-spaced {
.b-table {
.field.table-mobile-sort {
- padding-top: $default-padding * .5;
+ padding-top: $default-padding * 0.5;
}
}
}
}
.b-table {
.field.table-mobile-sort {
- padding: 0 $default-padding * .5;
+ padding: 0 $default-padding * 0.5;
}
.table-wrapper.has-mobile-cards {
tr {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
- margin-bottom: 3px!important;
+ margin-bottom: 3px !important;
}
td {
&.is-progress-col {
- span, progress {
+ span,
+ progress {
display: flex;
width: 45%;
align-items: center;
@@ -133,11 +137,13 @@ table.table {
}
}
- &.checkbox-cell, &.is-image-cell {
- border-bottom: 0!important;
+ &.checkbox-cell,
+ &.is-image-cell {
+ border-bottom: 0 !important;
}
- &.checkbox-cell, &.is-actions-cell {
+ &.checkbox-cell,
+ &.is-actions-cell {
&:before {
display: none;
}
@@ -163,7 +169,7 @@ table.table {
.image {
width: $table-avatar-size-mobile;
height: auto;
- margin: 0 auto $default-padding * .25;
+ margin: 0 auto $default-padding * 0.25;
}
}
}
diff --git a/packages/anastasis-webui/src/scss/_tiles.scss b/packages/anastasis-webui/src/scss/_tiles.scss
index 94fc04e70..e69d995f0 100644
--- a/packages/anastasis-webui/src/scss/_tiles.scss
+++ b/packages/anastasis-webui/src/scss/_tiles.scss
@@ -14,12 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-
.is-tiles-wrapper {
margin-bottom: $default-padding;
}
diff --git a/packages/anastasis-webui/src/scss/_title-bar.scss b/packages/anastasis-webui/src/scss/_title-bar.scss
index 736f26cbd..932f8e65d 100644
--- a/packages/anastasis-webui/src/scss/_title-bar.scss
+++ b/packages/anastasis-webui/src/scss/_title-bar.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -26,14 +26,14 @@ section.section.is-title-bar {
ul {
li {
display: inline-block;
- padding: 0 $default-padding * .5 0 0;
+ padding: 0 $default-padding * 0.5 0 0;
font-size: $default-padding;
color: $title-bar-color;
&:after {
display: inline-block;
- content: '/';
- padding-left: $default-padding * .5;
+ content: "/";
+ padding-left: $default-padding * 0.5;
}
&:last-child {
diff --git a/packages/anastasis-webui/src/scss/main.scss b/packages/anastasis-webui/src/scss/main.scss
index b5335073f..9311fbba0 100644
--- a/packages/anastasis-webui/src/scss/main.scss
+++ b/packages/anastasis-webui/src/scss/main.scss
@@ -190,7 +190,6 @@ div[data-tooltip]::before {
border: solid 1px #f2e9bf;
}
-
.home {
padding: 1em 1em;
min-height: 100%;
@@ -218,9 +217,9 @@ div[data-tooltip]::before {
}
.profile {
- padding: 56px 20px;
- min-height: 100%;
- width: 100%;
+ padding: 56px 20px;
+ min-height: 100%;
+ width: 100%;
}
.notfound {
@@ -232,4 +231,4 @@ h1 {
font-size: 1.5em;
margin-top: 0.8em;
margin-bottom: 0.8em;
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/template.html b/packages/anastasis-webui/src/template.html
index 351f1829c..8ae2fe104 100644
--- a/packages/anastasis-webui/src/template.html
+++ b/packages/anastasis-webui/src/template.html
@@ -1,15 +1,51 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
<!DOCTYPE html>
-<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
- <head>
- <meta charset="utf-8">
- <title><% preact.title %></title>
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="apple-mobile-web-app-capable" content="yes">
- <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
- <% preact.headEnd %>
- </head>
- <body>
- <% preact.bodyEnd %>
- </body>
+<html
+ lang="en"
+ class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
+>
+ <head>
+ <meta charset="utf-8" />
+ <title><%= htmlWebpackPlugin.options.title %></title>
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+
+ <% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
+ <meta
+ name="theme-color"
+ content="<%= htmlWebpackPlugin.options.manifest.theme_color %>"
+ />
+ <% } %>
+ </head>
+ <body>
+ <script>
+ <%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
+ </script>
+ <script>
+ <%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
+ </script>
+ </body>
</html>
diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx
index 9c01aa6ba..a8f6c3101 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -1,45 +1,67 @@
/* eslint-disable @typescript-eslint/camelcase */
-import { BackupStates, RecoveryStates, ReducerState } from 'anastasis-core';
-import { FunctionalComponent, h, VNode } from 'preact';
-import { AnastasisProvider } from '../context/anastasis';
+import { BackupStates, RecoveryStates, ReducerState } from "anastasis-core";
+import { FunctionalComponent, h, VNode } from "preact";
+import { AnastasisProvider } from "../context/anastasis";
-export function createExample<Props>(Component: FunctionalComponent<Props>, currentReducerState?: ReducerState, props?: Partial<Props>): { (args: Props): VNode } {
+export function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ currentReducerState?: ReducerState,
+ props?: Partial<Props>,
+): { (args: Props): VNode } {
const r = (args: Props): VNode => {
- return <AnastasisProvider value={{
- currentReducerState,
- currentError: undefined,
- back: async () => { null },
- dismissError: async () => { null },
- reset: () => { null },
- runTransaction: async () => { null },
- startBackup: () => { null },
- startRecover: () => { null },
- transition: async () => { null },
- }}>
- <Component {...args} />
- </AnastasisProvider>
- }
- r.args = props
- return r
+ return (
+ <AnastasisProvider
+ value={{
+ currentReducerState,
+ currentError: undefined,
+ back: async () => {
+ null;
+ },
+ dismissError: async () => {
+ null;
+ },
+ reset: () => {
+ null;
+ },
+ runTransaction: async () => {
+ null;
+ },
+ startBackup: () => {
+ null;
+ },
+ startRecover: () => {
+ null;
+ },
+ transition: async () => {
+ null;
+ },
+ }}
+ >
+ <Component {...args} />
+ </AnastasisProvider>
+ );
+ };
+ r.args = props;
+ return r;
}
const base = {
continents: [
{
- name: "Europe"
+ name: "Europe",
},
{
- name: "India"
+ name: "India",
},
{
- name: "Asia"
+ name: "Asia",
},
{
- name: "North America"
+ name: "North America",
},
{
- name: "Testcontinent"
- }
+ name: "Testcontinent",
+ },
],
countries: [
{
@@ -47,122 +69,124 @@ const base = {
name: "Testland",
continent: "Testcontinent",
continent_i18n: {
- de_DE: "Testkontinent"
+ de_DE: "Testkontinent",
},
name_i18n: {
de_DE: "Testlandt",
de_CH: "Testlandi",
fr_FR: "Testpais",
- en_UK: "Testland"
+ en_UK: "Testland",
},
currency: "TESTKUDOS",
- call_code: "+00"
+ call_code: "+00",
},
{
code: "xy",
name: "Demoland",
continent: "Testcontinent",
continent_i18n: {
- de_DE: "Testkontinent"
+ de_DE: "Testkontinent",
},
name_i18n: {
de_DE: "Demolandt",
de_CH: "Demolandi",
fr_FR: "Demopais",
- en_UK: "Demoland"
+ en_UK: "Demoland",
},
currency: "KUDOS",
- call_code: "+01"
- }
+ call_code: "+01",
+ },
],
authentication_providers: {
"http://localhost:8086/": {
http_status: 200,
annual_fee: "COL:0",
- business_name: "ana",
+ business_name: "Anastasis Local",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "sms",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "email",
- usage_fee: "COL:0"
+ usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
- truth_upload_fee: "COL:0"
+ truth_upload_fee: "COL:0",
},
"https://kudos.demo.anastasis.lu/": {
http_status: 200,
annual_fee: "COL:0",
- business_name: "ana",
+ business_name: "Anastasis Kudo",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "email",
- usage_fee: "COL:0"
+ usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
- truth_upload_fee: "COL:0"
+ truth_upload_fee: "COL:0",
},
"https://anastasis.demo.taler.net/": {
http_status: 200,
annual_fee: "COL:0",
- business_name: "ana",
+ business_name: "Anastasis Demo",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "sms",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "totp",
- usage_fee: "COL:0"
+ usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
- truth_upload_fee: "COL:0"
+ truth_upload_fee: "COL:0",
},
"http://localhost:8087/": {
code: 8414,
- hint: "request to provider failed"
+ hint: "request to provider failed",
},
"http://localhost:8088/": {
code: 8414,
- hint: "request to provider failed"
+ hint: "request to provider failed",
},
"http://localhost:8089/": {
code: 8414,
- hint: "request to provider failed"
- }
+ hint: "request to provider failed",
+ },
},
- // expiration: {
- // d_ms: 1792525051855 // check t_ms
- // },
-} as Partial<ReducerState>
+} as Partial<ReducerState>;
export const reducerStatesExample = {
initial: undefined,
recoverySelectCountry: {
...base,
- recovery_state: RecoveryStates.CountrySelecting
+ recovery_state: RecoveryStates.CountrySelecting,
} as ReducerState,
recoverySelectContinent: {
...base,
@@ -190,11 +214,11 @@ export const reducerStatesExample = {
} as ReducerState,
recoveryAttributeEditing: {
...base,
- recovery_state: RecoveryStates.UserAttributesCollecting
+ recovery_state: RecoveryStates.UserAttributesCollecting,
} as ReducerState,
backupSelectCountry: {
...base,
- backup_state: BackupStates.CountrySelecting
+ backup_state: BackupStates.CountrySelecting,
} as ReducerState,
backupSelectContinent: {
...base,
@@ -218,15 +242,14 @@ export const reducerStatesExample = {
} as ReducerState,
authEditing: {
...base,
- backup_state: BackupStates.AuthenticationsEditing
+ backup_state: BackupStates.AuthenticationsEditing,
} as ReducerState,
backupAttributeEditing: {
...base,
- backup_state: BackupStates.UserAttributesCollecting
+ backup_state: BackupStates.UserAttributesCollecting,
} as ReducerState,
truthsPaying: {
...base,
- backup_state: BackupStates.TruthsPaying
+ backup_state: BackupStates.TruthsPaying,
} as ReducerState,
-
-}
+};