summaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui/src/pages')
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts131
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts167
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx93
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts45
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx309
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx184
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx287
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx42
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx69
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx46
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx51
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx109
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx300
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx69
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx98
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx297
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx318
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx42
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx45
-rw-r--r--packages/anastasis-webui/src/pages/home/ConfirmModal.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx61
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx163
-rw-r--r--packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx36
-rw-r--r--packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx27
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx141
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx187
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx65
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx35
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx59
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx137
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx315
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx165
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx54
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx113
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx99
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx497
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx25
-rw-r--r--packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx23
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx23
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx135
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.tsx277
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx25
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx12
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx43
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.tsx74
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx49
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx32
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx82
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx113
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx92
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx195
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx81
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx129
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx118
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx82
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx161
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx160
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx127
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx239
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx128
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx82
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx127
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx61
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx191
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx80
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx141
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx125
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/helpers.ts27
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/index.tsx109
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/totp.ts79
-rw-r--r--packages/anastasis-webui/src/pages/home/index.stories.tsx52
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx336
-rw-r--r--packages/anastasis-webui/src/pages/home/style.css0
-rw-r--r--packages/anastasis-webui/src/pages/notfound/index.tsx16
-rw-r--r--packages/anastasis-webui/src/pages/notfound/style.css0
-rw-r--r--packages/anastasis-webui/src/pages/profile/index.tsx43
-rw-r--r--packages/anastasis-webui/src/pages/profile/style.css0
81 files changed, 7638 insertions, 1460 deletions
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
new file mode 100644
index 000000000..ed8301d65
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
@@ -0,0 +1,131 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AuthenticationProviderStatus } from "@gnu-taler/anastasis-core";
+import InvalidState from "../../../components/InvalidState.js";
+import NoReducer from "../../../components/NoReducer.js";
+import { Notification } from "../../../components/Notifications.js";
+import { compose, StateViewMap } from "../../../utils/index.js";
+import useComponentState from "./state.js";
+import { WithoutProviderType, WithProviderType } from "./views.js";
+
+export type AuthProvByStatusMap = Record<
+ AuthenticationProviderStatus["status"],
+ (AuthenticationProviderStatus & { url: string })[]
+>;
+
+export type State = NoReducer | InvalidState | WithType | WithoutType;
+
+export interface NoReducer {
+ status: "no-reducer";
+}
+export interface InvalidState {
+ status: "invalid-state";
+}
+
+interface CommonProps {
+ addProvider?: () => Promise<void>;
+ deleteProvider: (url: string) => Promise<void>;
+ authProvidersByStatus: AuthProvByStatusMap;
+ error: string | undefined;
+ onCancel: () => Promise<void>;
+ testing: boolean;
+ setProviderURL: (url: string) => Promise<void>;
+ providerURL: string;
+ errors: string | undefined;
+ notifications: Notification[];
+}
+
+export interface WithType extends CommonProps {
+ status: "with-type";
+ providerLabel: string;
+}
+export interface WithoutType extends CommonProps {
+ status: "without-type";
+}
+
+const map: StateViewMap<State> = {
+ "no-reducer": NoReducer,
+ "invalid-state": InvalidState,
+ "with-type": WithProviderType,
+ "without-type": WithoutProviderType,
+};
+
+export default compose("AddingProviderScreen", useComponentState, map);
+
+const providerResponseCache = new Map<string, any>(); // `any` is the return type of res.json()
+export async function testProvider(
+ url: string,
+ expectedMethodType?: string,
+): Promise<void> {
+ const testFatalPrefix = `Encountered a fatal error whilst testing the provider ${url}`;
+ let configUrl = "";
+ try {
+ configUrl = new URL("config", url).href;
+ } catch (error) {
+ throw new Error(`${testFatalPrefix}: Invalid Provider URL: ${url}
+Error: ${error}`);
+ }
+ // TODO: look into using core.getProviderInfo :)
+ const providerHasUrl = providerResponseCache.has(url);
+ const json = providerHasUrl
+ ? providerResponseCache.get(url)
+ : await fetch(configUrl)
+ .catch((error) => {
+ throw new Error(`${testFatalPrefix}: Could not connect: ${error}
+Please check the URL.`);
+ })
+ .then(async (response) => {
+ if (!response.ok)
+ throw new Error(
+ `${testFatalPrefix}: The server ${response.url} responded with a non-2xx response.`,
+ );
+ try {
+ return await response.json();
+ } catch (error) {
+ throw new Error(
+ `${testFatalPrefix}: The server responded with malformed JSON.\nError: ${error}`,
+ );
+ }
+ });
+ if (typeof json !== "object")
+ throw new Error(
+ `${testFatalPrefix}: Did not get an object after decoding.`,
+ );
+ if (!("name" in json) || json.name !== "anastasis") {
+ throw new Error(
+ `${testFatalPrefix}: The provider does not appear to be an Anastasis provider. Please check the provider's URL.`,
+ );
+ }
+ if (!("methods" in json) || !Array.isArray(json.methods)) {
+ throw new Error(
+ "This provider doesn't have authentication method. Please check the provider's URL and ensure it is properly configured.",
+ );
+ }
+ if (!providerHasUrl) providerResponseCache.set(url, json);
+ 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 new Error(
+ `${testFatalPrefix}: This provider does not support authentication method ${expectedMethodType}`,
+ );
+ }
+ return;
+}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
new file mode 100644
index 000000000..30e4d750d
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
@@ -0,0 +1,167 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useEffect, useRef, useState } from "preact/hooks";
+import { Notification } from "../../../components/Notifications.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { authMethods, KnownAuthMethods } from "../authMethod/index.jsx";
+import { AuthProvByStatusMap, State, testProvider } from "./index.js";
+
+interface Props {
+ providerType?: KnownAuthMethods;
+ onCancel: () => Promise<void>;
+ notifications?: Notification[];
+}
+
+export default function useComponentState({
+ providerType,
+ onCancel,
+ notifications = [],
+}: Props): State {
+ const reducer = useAnastasisContext();
+
+ const [providerURL, setProviderURL] = useState("");
+
+ const [error, setError] = useState<string | undefined>();
+ const [testing, setTesting] = useState(false);
+
+ const providerLabel = providerType
+ ? authMethods[providerType].label
+ : undefined;
+
+ const allAuthProviders =
+ !reducer ||
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.reducer_type === "error" ||
+ !reducer.currentReducerState.authentication_providers
+ ? {}
+ : reducer.currentReducerState.authentication_providers;
+
+ const authProvidersByStatus = Object.keys(allAuthProviders).reduce(
+ (prev, url) => {
+ const p = allAuthProviders[url];
+ if (
+ providerLabel &&
+ p.status === "ok" &&
+ p.methods.findIndex((m) => m.type === providerType) !== -1
+ ) {
+ return prev;
+ }
+ prev[p.status].push({ ...p, url });
+ return prev;
+ },
+ {
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ ok: [],
+ } as AuthProvByStatusMap,
+ );
+ const authProviders = authProvidersByStatus["ok"].map((p) => p.url);
+
+ //FIXME: move this timeout logic into a hook
+ const timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
+ useEffect(() => {
+ if (timeout.current) clearTimeout(timeout.current);
+ timeout.current = setTimeout(async () => {
+ let url = providerURL;
+ if (!url || authProviders.includes(url)) return;
+ if (url && !url.match(/^(https?:)\/\/.+\/(?:config)?$/iu))
+ return setError(
+ "Malformed URL: Must be an HTTP(S) URL ending with a /",
+ );
+ if (url.endsWith("/config")) url = url.substring(0, url.length - 6);
+ try {
+ setTesting(true);
+ await testProvider(url, providerType);
+ setError("");
+ } catch (e) {
+ if (e instanceof Error) setError(e.message);
+ else
+ throw new Error(
+ `Unexpected Error Type: ${typeof e} - Cannot handle. Error: ${e}`,
+ );
+ }
+ setTesting(false);
+ }, 200);
+ }, [providerURL, reducer]);
+
+ if (!reducer) {
+ return {
+ status: "no-reducer",
+ };
+ }
+
+ if (
+ !reducer.currentReducerState ||
+ !("authentication_providers" in reducer.currentReducerState)
+ ) {
+ return {
+ status: "invalid-state",
+ };
+ }
+
+ const addProvider = async (provider_url: string): Promise<void> => {
+ await reducer.transition("add_provider", { provider_url });
+ onCancel();
+ };
+ const deleteProvider = async (provider_url: string): Promise<void> => {
+ reducer.transition("delete_provider", { provider_url });
+ };
+
+ let errors = !providerURL ? "Add provider URL" : undefined;
+ let url: string | undefined;
+ // We'll validate it in testProvider & via a regex above - there's no need in this :)
+ // try {
+ // url = new URL("", providerURL).href;
+ // } catch {
+ // errors = "Check the URL";
+ // }
+ const _url = url;
+
+ if (!!error && !errors) {
+ errors = error;
+ }
+ if (!errors && authProviders.includes(url!)) {
+ errors = "That provider is already known";
+ }
+
+ const commonState = {
+ addProvider: !_url ? undefined : async () => addProvider(_url),
+ deleteProvider: async (url: string) => deleteProvider(url),
+ allAuthProviders,
+ authProvidersByStatus,
+ onCancel,
+ providerURL,
+ testing,
+ setProviderURL: async (s: string) => setProviderURL(s),
+ errors,
+ error,
+ notifications,
+ };
+
+ if (!providerLabel) {
+ return {
+ status: "without-type",
+ ...commonState,
+ };
+ } else {
+ return {
+ status: "with-type",
+ providerLabel,
+ ...commonState,
+ };
+ }
+}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
new file mode 100644
index 000000000..548fc01a5
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AuthenticationProviderStatusOk } from "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { WithoutProviderType, WithProviderType } from "./views.jsx";
+
+export default {
+ title: "Adding Provider Screen",
+ args: {
+ order: 1,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const NewProvider = tests.createExample(WithoutProviderType, {
+ authProvidersByStatus: {
+ ok: [
+ {
+ business_name: "X provider",
+ status: "ok",
+ storage_limit_in_megabytes: 5,
+ methods: [
+ {
+ type: "question",
+ usage_fee: "KUDOS:1",
+ },
+ ],
+ url: "",
+ } as AuthenticationProviderStatusOk & { url: string },
+ ],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ notifications: [],
+});
+
+export const NewProviderWithoutProviderList = tests.createExample(
+ WithoutProviderType,
+ {
+ authProvidersByStatus: {
+ ok: [],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ notifications: [],
+ },
+);
+
+export const NewSmsProvider = tests.createExample(WithProviderType, {
+ authProvidersByStatus: {
+ ok: [],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ providerLabel: "sms",
+ notifications: [],
+});
+
+export const NewIBANProvider = tests.createExample(WithProviderType, {
+ authProvidersByStatus: {
+ ok: [],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ providerLabel: "IBAN",
+ notifications: [],
+});
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts
new file mode 100644
index 000000000..0aebbdc6c
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts
@@ -0,0 +1,45 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+import useComponentState from "./state.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+describe("AddingProviderScreen states", () => {
+ it("should not load more if has reach the end", async () => {
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ return useComponentState({
+ providerType: "email",
+ async onCancel() {},
+ });
+ },
+ {},
+ [
+ ({ status }) => {
+ expect(status).eq("no-reducer");
+ },
+ ],
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
new file mode 100644
index 000000000..00a42a949
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
@@ -0,0 +1,309 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AuthenticationProviderStatusError,
+ AuthenticationProviderStatusOk,
+} from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { Notifications } from "../../../components/Notifications.js";
+import { AnastasisClientFrame } from "../index.js";
+import { testProvider, WithoutType, WithType } from "./index.js";
+import { useTranslationContext } from "../../../context/translation.js";
+
+export function WithProviderType(props: WithType): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <AnastasisClientFrame
+ hideNav
+ title="Backup: Manage providers1"
+ hideNext={props.errors}
+ >
+ <div>
+ <Notifications notifications={props.notifications} />
+ <p>{i18n.str`Add a provider url for a ${props.providerLabel} service`}</p>
+ <div class="container">
+ <TextInput
+ label="Provider URL"
+ placeholder="https://provider.com"
+ grabFocus
+ error={props.errors}
+ bind={[props.providerURL, props.setProviderURL]}
+ />
+ </div>
+ <p class="block">Example: https://kudos.demo.anastasis.lu</p>
+ {props.testing && <p class="has-text-info">Testing</p>}
+
+ <div
+ class="block"
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={props.onCancel}>
+ Cancel
+ </button>
+ <span data-tooltip={props.errors}>
+ <button
+ class="button is-info"
+ disabled={props.error !== "" || props.testing}
+ onClick={props.addProvider}
+ >
+ Add
+ </button>
+ </span>
+ </div>
+
+ {props.authProvidersByStatus["ok"].length > 0 ? (
+ <p class="subtitle">
+ Current providers for {props.providerLabel} service
+ </p>
+ ) : (
+ <p class="subtitle">
+ No known providers for {props.providerLabel} service
+ </p>
+ )}
+
+ {props.authProvidersByStatus["ok"].map((k, i) => {
+ const p = k as AuthenticationProviderStatusOk;
+ return (
+ <TableRow
+ key={i}
+ url={k.url}
+ info={p}
+ onDelete={props.deleteProvider}
+ />
+ );
+ })}
+ <p class="subtitle">Providers with errors</p>
+ {props.authProvidersByStatus["error"].map((k, i) => {
+ const p = k as AuthenticationProviderStatusError;
+ return (
+ <TableRowError
+ key={i}
+ url={k.url}
+ info={p}
+ onDelete={props.deleteProvider}
+ />
+ );
+ })}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+export function WithoutProviderType(props: WithoutType): VNode {
+ return (
+ <AnastasisClientFrame
+ hideNav
+ title="Backup: Manage providers"
+ hideNext={props.errors}
+ >
+ <div>
+ <Notifications notifications={props.notifications} />
+ <p>Add a provider url</p>
+ <div class="container">
+ <TextInput
+ label="Provider URL"
+ placeholder="https://provider.com/"
+ grabFocus
+ error={props.errors}
+ bind={[props.providerURL, props.setProviderURL]}
+ />
+ </div>
+ <p class="block">Example: https://kudos.demo.anastasis.lu/</p>
+ {props.testing && <p class="has-text-info">Testing</p>}
+
+ <div
+ class="block"
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={props.onCancel}>
+ Cancel
+ </button>
+ <span data-tooltip={props.errors}>
+ <button
+ class="button is-info"
+ disabled={props.error !== "" || props.testing}
+ onClick={props.addProvider}
+ >
+ Add
+ </button>
+ </span>
+ </div>
+
+ {props.authProvidersByStatus["ok"].length > 0 ? (
+ <p class="subtitle">Current providers</p>
+ ) : (
+ <p class="subtitle">No known providers, add one.</p>
+ )}
+
+ {props.authProvidersByStatus["ok"].map((k, i) => {
+ const p = k as AuthenticationProviderStatusOk;
+ return (
+ <TableRow
+ key={i}
+ url={k.url}
+ info={p}
+ onDelete={props.deleteProvider}
+ />
+ );
+ })}
+ <p class="subtitle">Providers with errors</p>
+ {props.authProvidersByStatus["error"].map((k, i) => {
+ const p = k as AuthenticationProviderStatusError;
+ return (
+ <TableRowError
+ key={i}
+ url={k.url}
+ info={p}
+ onDelete={props.deleteProvider}
+ />
+ );
+ })}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+function TableRow({
+ url,
+ info,
+ onDelete,
+}: {
+ onDelete: (s: string) => Promise<void>;
+ url: string;
+ info: AuthenticationProviderStatusOk;
+}): VNode {
+ 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>
+ );
+}
+
+function TableRowError({
+ url,
+ info,
+ onDelete,
+}: {
+ onDelete: (s: string) => void;
+ url: string;
+ info: AuthenticationProviderStatusError;
+}): VNode {
+ 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>Error</b>
+ </dt>
+ <dd>{info.hint}</dd>
+ <dt>
+ <b>Code</b>
+ </dt>
+ <dd>{info.code}</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 d28a6df43..e6bc5f340 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -1,63 +1,161 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
+import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen.js";
export default {
- title: 'Pages/AttributeEntryScreen',
+ title: "Attribute Entry Screen",
component: TestedComponent,
+ args: {
+ order: 3,
+ },
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const WithSomeAttributes = createExample(TestedComponent, {
- ...reducerStatesExample.attributeEditing,
- required_attributes: [{
- name: 'first',
- label: 'first',
- type: 'type',
- uuid: 'asdasdsa1',
- widget: 'wid',
- }, {
- name: 'pepe',
- label: 'second',
- type: 'type',
- uuid: 'asdasdsa2',
- widget: 'wid',
- }, {
- name: 'pepe2',
- label: 'third',
- type: 'type',
- uuid: 'asdasdsa3',
- widget: 'calendar',
- }]
+export const Backup = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: [
+ {
+ name: "full_name",
+ label: "Full name",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ },
+ {
+ name: "birthplace",
+ label: "Birthplace",
+ type: "string",
+ uuid: "asdasdsa2",
+ widget: "wid",
+ },
+ {
+ name: "birthdate",
+ label: "birthdate",
+ type: "date",
+ uuid: "asdasdsa3",
+ widget: "calendar",
+ },
+ ],
} as ReducerState);
-export const Empty = createExample(TestedComponent, {
- ...reducerStatesExample.attributeEditing,
- required_attributes: undefined
+export const Recovery = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.recoveryAttributeEditing,
+ required_attributes: [
+ {
+ name: "full_name",
+ label: "Full name",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ },
+ {
+ name: "birthplace",
+ label: "Birthplace",
+ type: "string",
+ uuid: "asdasdsa2",
+ widget: "wid",
+ },
+ {
+ name: "pepe2",
+ label: "third",
+ type: "date",
+ uuid: "asdasdsa3",
+ widget: "calendar",
+ },
+ ],
} as ReducerState);
+
+export const WithNoRequiredAttribute = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: undefined,
+ } as ReducerState,
+);
+
+const allWidgets = [
+ "anastasis_gtk_ia_aadhar_in",
+ "anastasis_gtk_ia_ahv",
+ "anastasis_gtk_ia_birthdate",
+ "anastasis_gtk_ia_birthnumber_cz",
+ "anastasis_gtk_ia_birthnumber_sk",
+ "anastasis_gtk_ia_birthplace",
+ "anastasis_gtk_ia_cf_it",
+ "anastasis_gtk_ia_cpr_dk",
+ "anastasis_gtk_ia_es_dni",
+ "anastasis_gtk_ia_es_ssn",
+ "anastasis_gtk_ia_full_name",
+ "anastasis_gtk_ia_my_jp",
+ "anastasis_gtk_ia_nid_al",
+ "anastasis_gtk_ia_nid_be",
+ "anastasis_gtk_ia_ssn_de",
+ "anastasis_gtk_ia_ssn_us",
+ "anastasis_gtk_ia_tax_de",
+ "anastasis_gtk_xx_prime",
+ "anastasis_gtk_xx_square",
+];
+
+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";
+ return "string";
+}
+
+export const WithAllPosibleWidget = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: allWidgets.map((w) => ({
+ name: w,
+ label: `widget: ${w}`,
+ type: typeForWidget(w),
+ uuid: `uuid-${w}`,
+ widget: w,
+ })),
+} as ReducerState);
+
+export const WithAutocompleteFeature = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: [
+ {
+ name: "ahv_number",
+ label: "AHV Number",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ "validation-regex":
+ "^(756)\\.[0-9]{4}\\.[0-9]{4}\\.[0-9]{2}|(756)[0-9]{10}$",
+ "validation-logic": "CH_AHV_check",
+ autocomplete: "???.????.????.??",
+ },
+ ],
+ } as ReducerState,
+);
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
index 2f804f940..1f8cea7aa 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -1,65 +1,270 @@
-/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { UserAttributeSpec, validators } from "@gnu-taler/anastasis-core";
+import { isAfter, parse } from "date-fns";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { ReducerStateRecovery, ReducerStateBackup, UserAttributeSpec } from "anastasis-core/lib";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
-import { AnastasisClientFrame, withProcessLabel, LabeledInput } from "./index";
+import { DateInput } from "../../components/fields/DateInput.js";
+import { PhoneNumberInput } from "../../components/fields/NumberInput.js";
+import { TextInput } from "../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { ConfirmModal } from "./ConfirmModal.js";
+import { AnastasisClientFrame, withProcessLabel } from "./index.js";
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 isBackup = state?.reducer_type === "backup";
+ 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 || [];
+ let hasErrors = false;
+
+ const fieldList: VNode[] = reqAttr.map((spec, i: number) => {
+ const value = attrs[spec.name];
+ const error = checkIfValid(value, spec);
+
+ function addAutocomplete(newValue: string): string {
+ const ac = spec.autocomplete;
+ if (!ac || ac.length <= newValue.length || ac[newValue.length] === "?")
+ return newValue;
+
+ if (!value || newValue.length < value.length) {
+ return newValue.slice(0, -1);
+ }
+
+ return newValue + ac[newValue.length];
+ }
+
+ hasErrors = hasErrors || error !== undefined;
+ return (
+ <AttributeEntryField
+ key={i}
+ isFirst={i == 0}
+ setValue={(v: string) =>
+ setAttrs({ ...attrs, [spec.name]: addAutocomplete(v) })
+ }
+ spec={spec}
+ errorMessage={error}
+ onConfirm={() => {
+ if (!hasErrors) {
+ setAskUserIfSure(true);
+ }
+ }}
+ value={value}
+ />
+ );
+ });
+
+ const doConfirm = async () => {
+ await reducer.transition("enter_user_attributes", {
+ identity_attributes: {
+ application_id: "anastasis-standalone",
+ ...attrs,
+ },
+ });
+ };
+
+ function saveAsPDF(): void {
+ const printWindow = window.open("", "", "height=400,width=800");
+ const divContents = document.getElementById("printThis");
+
+ if (!printWindow || !divContents) return;
+ printWindow.document.write(
+ `<html><head><link rel="stylesheet" href="index.css" /><title>Anastasis Recovery Document</title><style>`,
+ );
+ printWindow.document.write("</style></head><body>&nbsp;</body></html>");
+ printWindow.document.close();
+ printWindow.document.body.appendChild(divContents.cloneNode(true));
+ printWindow.addEventListener("load", () => {
+ printWindow.print();
+ printWindow.close();
+ });
}
-
+
return (
<AnastasisClientFrame
- title={withProcessLabel(reducer, "Select Country")}
- onNext={() => reducer.transition("enter_user_attributes", {
- identity_attributes: attrs,
- })}
+ title={withProcessLabel(reducer, "Who are you?")}
+ hideNext={hasErrors ? "Complete the form." : undefined}
+ onNext={async () => (isBackup ? setAskUserIfSure(true) : doConfirm())}
>
- {reducer.currentReducerState.required_attributes?.map((x, i: number) => {
- return (
- <AttributeEntryField
- key={i}
- isFirst={i == 0}
- setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
- spec={x}
- value={attrs[x.name]} />
- );
- })}
+ {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={() => doConfirm().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.
+ <p>
+ {/* TODO: make this actually work reliably cross-browser lol (opens about:blank for me) */}
+ <a onClick={saveAsPDF}>Save the personal information as PDF</a>
+ </p>
+ </ConfirmModal>
+ ) : undefined}
+
+ <div class="columns" style={{ maxWidth: "unset" }}>
+ <div class="column" id="printThis">
+ {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>
+ <ul>
+ <li>
+ <span class="icon is-right">
+ <i class="mdi mdi-circle-small" />
+ </span>
+ Will be hashed, and therefore unreadable
+ </li>
+ <li>
+ <span class="icon is-right">
+ <i class="mdi mdi-circle-small" />
+ </span>
+ The non-hashed version is not shared
+ </li>
+ </ul>
+ </div>
+ </div>
</AnastasisClientFrame>
);
}
-interface AttributeEntryProps {
- reducer: AnastasisReducerApi;
- reducerState: ReducerStateRecovery | ReducerStateBackup;
-}
-
-export interface AttributeEntryFieldProps {
+interface AttributeEntryFieldProps {
isFirst: boolean;
value: string;
setValue: (newValue: string) => void;
spec: UserAttributeSpec;
+ errorMessage: string | undefined;
+ onConfirm: () => void;
}
-
-export function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
+const possibleBirthdayYear: Array<number> = [];
+for (let i = 0; i < 100; i++) {
+ possibleBirthdayYear.push(2020 - i);
+}
+function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
return (
- <div>
- <LabeledInput
- grabFocus={props.isFirst}
- label={props.spec.label}
- bind={[props.value, props.setValue]}
- />
+ <div style={{ marginTop: 16 }}>
+ {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" && (
+ <PhoneNumberInput
+ grabFocus={props.isFirst}
+ label={props.spec.label}
+ onConfirm={props.onConfirm}
+ error={props.errorMessage}
+ bind={[props.value, props.setValue]}
+ />
+ )}
+ {props.spec.type === "string" && (
+ <TextInput
+ grabFocus={props.isFirst}
+ label={props.spec.label}
+ onConfirm={props.onConfirm}
+ error={props.errorMessage}
+ bind={[props.value, props.setValue]}
+ />
+ )}
+ {props.spec.type === "string" && (
+ <div>
+ This field is case-sensitive. You must enter exactly the same value
+ during recovery.
+ </div>
+ )}
+ {props.spec.name === "full_name" && (
+ <div>
+ If possible, use &quot;LASTNAME, Firstname(s)&quot; without
+ abbreviations.
+ </div>
+ )}
+ <div class="block">
+ This stays private
+ <span class="icon is-right">
+ <i class="mdi mdi-eye-off" />
+ </span>
+ </div>
</div>
);
}
+const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/;
+
+function checkIfValid(
+ value: string,
+ spec: UserAttributeSpec,
+): string | undefined {
+ const pattern = spec["validation-regex"];
+ if (pattern) {
+ const re = new RegExp(pattern);
+ if (!re.test(value)) return "The value is invalid";
+ }
+ const logic = spec["validation-logic"];
+ if (logic) {
+ const func = (validators as any)[logic];
+ if (func && typeof func === "function" && !func(value))
+ return "Please check the value";
+ }
+ const optional = spec.optional;
+ if (!optional && !value) {
+ return "This value is required";
+ }
+ if ("date" === spec.type) {
+ if (!YEAR_REGEX.test(value)) {
+ return "The date doesn't follow the format";
+ }
+
+ try {
+ 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";
+ }
+ if ("birthdate" === spec.name && isAfter(v, new Date())) {
+ return "A birthdate cannot be in the future";
+ }
+ } catch (e) {
+ return "Could not parse the date";
+ }
+ }
+ return undefined;
+}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx
deleted file mode 100644
index 9567e0ef7..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-
-export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
- const [email, setEmail] = useState("");
- return (
- <AnastasisClientFrame hideNav title="Add email authentication">
- <p>
- For email authentication, you need to provide an email address. When
- recovering your secret, you will need to enter the code you receive by
- email.
- </p>
- <div>
- <LabeledInput
- label="Email address"
- grabFocus
- bind={[email, setEmail]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button
- onClick={() => props.addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Email to ${email}`,
- challenge: encodeCrock(stringToBytes(email)),
- },
- })}
- >
- Add
- </button>
- </div>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx
deleted file mode 100644
index 55e37a968..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- canonicalJson, encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { LabeledInput } from "./index";
-
-export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {
- const [fullName, setFullName] = useState("");
- const [street, setStreet] = useState("");
- const [city, setCity] = useState("");
- const [postcode, setPostcode] = useState("");
- const [country, setCountry] = useState("");
-
- const addPostAuth = () => {
- const challengeJson = {
- full_name: fullName,
- street,
- city,
- postcode,
- country,
- };
- props.addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Letter to address in postal code ${postcode}`,
- challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
- },
- });
- };
-
- return (
- <div class="home">
- <h1>Add {props.method} authentication</h1>
- <div>
- <p>
- For postal letter authentication, you need to provide a postal
- address. When recovering your secret, you will be asked to enter a
- code that you will receive in a letter to that address.
- </p>
- <div>
- <LabeledInput
- grabFocus
- label="Full Name"
- bind={[fullName, setFullName]} />
- </div>
- <div>
- <LabeledInput label="Street" bind={[street, setStreet]} />
- </div>
- <div>
- <LabeledInput label="City" bind={[city, setCity]} />
- </div>
- <div>
- <LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
- </div>
- <div>
- <LabeledInput label="Country" bind={[country, setCountry]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addPostAuth()}>Add</button>
- </div>
- </div>
- </div>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx
deleted file mode 100644
index 7699cdf34..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-
-export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
- const [questionText, setQuestionText] = useState("");
- const [answerText, setAnswerText] = useState("");
- const addQuestionAuth = (): void => props.addAuthMethod({
- authentication_method: {
- type: "question",
- instructions: questionText,
- challenge: encodeCrock(stringToBytes(answerText)),
- },
- });
- return (
- <AnastasisClientFrame hideNav title="Add Security Question">
- <div>
- <p>
- For security question authentication, you need to provide a question
- and its answer. When recovering your secret, you will be shown the
- question and you will need to type the answer exactly as you typed it
- here.
- </p>
- <div>
- <LabeledInput
- label="Security question"
- grabFocus
- bind={[questionText, setQuestionText]} />
- </div>
- <div>
- <LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addQuestionAuth()}>Add</button>
- </div>
- </div>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx
deleted file mode 100644
index 6f4797275..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState, useRef, useLayoutEffect } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { AnastasisClientFrame } from "./index";
-
-export function AuthMethodSmsSetup(props: AuthMethodSetupProps): VNode {
- const [mobileNumber, setMobileNumber] = useState("");
- const addSmsAuth = (): void => {
- props.addAuthMethod({
- authentication_method: {
- type: "sms",
- instructions: `SMS to ${mobileNumber}`,
- challenge: encodeCrock(stringToBytes(mobileNumber)),
- },
- });
- };
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- inputRef.current?.focus();
- }, []);
- return (
- <AnastasisClientFrame hideNav title="Add SMS authentication">
- <div>
- <p>
- For SMS authentication, you need to provide a mobile number. When
- recovering your secret, you will be asked to enter the code you
- receive via SMS.
- </p>
- <label>
- Mobile number:{" "}
- <input
- value={mobileNumber}
- ref={inputRef}
- style={{ display: "block" }}
- autoFocus
- onChange={(e) => setMobileNumber((e.target as any).value)}
- type="text" />
- </label>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addSmsAuth()}>Add</button>
- </div>
- </div>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index 44d3795b2..22f8dd697 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -1,35 +1,108 @@
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
+import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen.js";
export default {
- title: 'Pages/AuthenticationEditorScreen',
+ title: "Authentication Editor Screen",
component: TestedComponent,
+ args: {
+ 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 = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.authEditing,
+);
+export const OneAuthMethodConfigured = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.authEditing,
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
+ } as ReducerState,
+);
+
+export const SomeMoreAuthMethodConfigured = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.authEditing,
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "qwe",
+ },
+ {
+ type: "sms",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
+ } as ReducerState,
+);
+
+export const NoAuthMethodProvided = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.authEditing,
+ authentication_providers: {},
+ authentication_methods: [],
+} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index e9ffccbac..54bbc626d 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -1,112 +1,256 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import { AuthMethod, ReducerStateBackup } from "anastasis-core";
-import { h, VNode } from "preact";
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AuthMethod, ReducerStateBackup } from "@gnu-taler/anastasis-core";
+import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
-import { AuthMethodEmailSetup } from "./AuthMethodEmailSetup";
-import { AuthMethodPostSetup } from "./AuthMethodPostSetup";
-import { AuthMethodQuestionSetup } from "./AuthMethodQuestionSetup";
-import { AuthMethodSmsSetup } from "./AuthMethodSmsSetup";
-import { AnastasisClientFrame } from "./index";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import AddingProviderScreen from "./AddingProviderScreen/index.js";
+import {
+ authMethods,
+ AuthMethodSetupProps,
+ AuthMethodWithRemove,
+ isKnownAuthMethods,
+ KnownAuthMethods,
+} from "./authMethod/index.js";
+import { ConfirmModal } from "./ConfirmModal.js";
+import { AnastasisClientFrame } from "./index.js";
+
+const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
export function AuthenticationEditorScreen(): VNode {
- const [selectedMethod, setSelectedMethod] = 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_type !== "backup") {
+ return <div>invalid state</div>;
}
+ const configuredAuthMethods: AuthMethod[] =
+ reducer.currentReducerState.authentication_methods ?? [];
+
+ function removeByIndex(index: number): void {
+ if (reducer)
+ reducer.transition("delete_authentication", {
+ authentication_method: index,
+ });
+ }
+
+ const camByType: { [s: string]: AuthMethodWithRemove[] } = {};
+ for (let index = 0; index < configuredAuthMethods.length; index++) {
+ const cam = {
+ ...configuredAuthMethods[index],
+ remove: () => removeByIndex(index),
+ };
+ const prevValue = camByType[cam.type] || [];
+ prevValue.push(cam);
+ camByType[cam.type] = prevValue;
+ }
+
const providers = reducer.currentReducerState.authentication_providers!;
+
const 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 (p.status === "ok") {
for (const meth of p.methods) {
authAvailableSet.add(meth.type);
}
}
}
+
+ if (manageProvider !== undefined) {
+ return (
+ <AddingProviderScreen
+ onCancel={async () => setManageProvider(undefined)}
+ providerType={
+ isKnownAuthMethods(manageProvider) ? manageProvider : undefined
+ }
+ />
+ );
+ }
+
if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => {
reducer.transition("add_authentication", args);
setSelectedMethod(undefined);
};
- const methodMap: Record<
- string, (props: AuthMethodSetupProps) => h.JSX.Element
- > = {
- sms: AuthMethodSmsSetup,
- question: AuthMethodQuestionSetup,
- email: AuthMethodEmailSetup,
- post: AuthMethodPostSetup,
- };
- const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
+
+ const AuthSetup =
+ authMethods[selectedMethod].setup ?? AuthMethodNotImplemented;
return (
- <AuthSetup
- cancel={cancel}
- addAuthMethod={addMethod}
- method={selectedMethod} />
+ <Fragment>
+ <AuthSetup
+ cancel={cancel}
+ configured={camByType[selectedMethod] || []}
+ addAuthMethod={addMethod}
+ method={selectedMethod}
+ />
+
+ {!authAvailableSet.has(selectedMethod) && (
+ <ConfirmModal
+ active
+ onCancel={cancel}
+ description="No providers found"
+ 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>
);
}
- function MethodButton(props: { method: string; label: string }): VNode {
+
+ function MethodButton(props: { method: KnownAuthMethods }): VNode {
+ if (authMethods[props.method].skip) return <div />;
+
return (
- <button
- disabled={!authAvailableSet.has(props.method)}
- onClick={() => {
- setSelectedMethod(props.method);
- if (reducer) reducer.dismissError();
- }}
- >
- {props.label}
- </button>
+ <div class="block">
+ <button
+ 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>
+ {!authAvailableSet.has(props.method) && (
+ <span class="icon has-text-danger">
+ <i class="mdi mdi-exclamation-thick" />
+ </span>
+ )}
+ {camByType[props.method] && (
+ <span class="tag is-info">{camByType[props.method].length}</span>
+ )}
+ </button>
+ </div>
);
}
- const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
- const haveMethodsConfigured = configuredAuthMethods.length;
+ 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">
- <div>
- <MethodButton method="sms" label="SMS" />
- <MethodButton method="email" label="Email" />
- <MethodButton method="question" label="Question" />
- <MethodButton method="post" label="Physical Mail" />
- <MethodButton method="totp" label="TOTP" />
- <MethodButton method="iban" label="IBAN" />
- </div>
- <h2>Configured authentication methods</h2>
- {haveMethodsConfigured ? (
- configuredAuthMethods.map((x, i) => {
- return (
- <p key={i}>
- {x.type} ({x.instructions}){" "}
- <button
- onClick={() => reducer.transition("delete_authentication", {
- authentication_method: i,
- })}
- >
- Delete
- </button>
+ <AnastasisClientFrame
+ title="Backup: Configure Authentication Methods"
+ hideNext={errors}
+ onNext={handleNext}
+ >
+ <div class="columns">
+ <div class="column">
+ <div>
+ {getKeys(authMethods).map((method) => (
+ <MethodButton key={method} method={method} />
+ ))}
+ </div>
+ {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 found"
+ 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">
+ <p class="block">
+ 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"
+ 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>
- );
- })
- ) : (
- <p>No authentication methods configured yet.</p>
- )}
+ )}
+ </div>
+ </div>
</AnastasisClientFrame>
);
}
-export interface AuthMethodSetupProps {
- method: string;
- addAuthMethod: (x: any) => void;
- cancel: () => void;
-}
-
function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
return (
<AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
@@ -115,9 +259,3 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
</AnastasisClientFrame>
);
}
-
-interface AuthenticationEditorProps {
- reducer: AnastasisReducerApi;
- backupState: ReducerStateBackup;
-}
-
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index 65a2b7e13..a51940615 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -1,60 +1,67 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/BackupFinishedScreen',
+ title: "Backup finish",
component: TestedComponent,
+ args: {
+ order: 8,
+ },
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Simple = createExample(TestedComponent, reducerStatesExample.backupFinished);
+export const WithoutName = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.backupFinished,
+);
-export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
- secret_name: 'super_secret',
+export const WithName = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.backupFinished,
+ secret_name: "super_secret",
} as ReducerState);
-export const WithDetails = createExample(TestedComponent, {
+export const WithDetails = tests.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_s: "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_s: new Date().getTime() + 60 * 60 * 24,
},
- 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 218f1d1fd..9b63c9887 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
@@ -1,33 +1,81 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AuthenticationProviderStatusOk } from "@gnu-taler/anastasis-core";
+import { format } from "date-fns";
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
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_type !== "backup") {
+ return <div>invalid state</div>;
}
- const details = reducer.currentReducerState.success_details
- return (<AnastasisClientFrame hideNext title="Backup finished">
- <p>
- Your backup of secret "{reducer.currentReducerState.secret_name ?? "??"}" was
- successful.
- </p>
- <p>The backup is stored by the following providers:</p>
+ const details = reducer.currentReducerState.success_details;
+ const providers = reducer.currentReducerState.authentication_providers ?? {};
+
+ return (
+ <AnastasisClientFrame hideNav title="Backup success!">
+ <p>Your backup is complete.</p>
- {details && <ul>
- {Object.keys(details).map((x, i) => {
- const sd = details[x];
- return (
- <li key={i}>
- {x} (Policy version {sd.policy_version})
- </li>
- );
- })}
- </ul>}
- <button onClick={() => reducer.reset()}>Back to start</button>
- </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_s !== "never"
+ ? ` expires at: ${format(
+ new Date(sd.policy_expiration.t_s * 1000),
+ "dd-MM-yyyy",
+ )}`
+ : " without expiration date"}
+ </p>
+ </div>
+ );
+ })}
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "center",
+ }}
+ >
+ <p>
+ <div class="buttons ml-4">
+ <button
+ class="button is-primary is-right"
+ onClick={() => reducer.reset()}
+ >
+ Start again
+ </button>
+ </div>
+ </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 4f186c031..84df615f3 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -1,83 +1,272 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { ChallengeOverviewScreen as TestedComponent } from './ChallengeOverviewScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import {
+ ChallengeFeedbackStatus,
+ RecoveryStates,
+ ReducerState,
+} from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { AmountString } from "@gnu-taler/taler-util";
export default {
- title: 'Pages/ChallengeOverviewScreen',
+ title: "Challenge overview",
component: TestedComponent,
+ args: {
+ order: 5,
+ },
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const OneChallenge = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting,
+export const OneUnsolvedPolicy = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.challengeSelecting,
recovery_information: {
- policies: [[{uuid:'1'}]],
- challenges: [{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '1',
- }]
+ policies: [[{ uuid: "1" }]],
+ challenges: [
+ {
+ instructions: "just go for it",
+ type: "question",
+ uuid: "1",
+ },
+ ],
},
} as ReducerState);
-export const MoreChallenges = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting,
+export const SomePoliciesOneSolved = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.challengeSelecting,
recovery_information: {
- policies: [[{uuid:'1'}, {uuid:'2'}],[{uuid:'3'}]],
- challenges: [{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '1',
- },{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '2',
- },{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '3',
- }]
+ policies: [[{ uuid: "1" }, { uuid: "2" }], [{ uuid: "uuid-3" }]],
+ challenges: [
+ {
+ instructions: "this question cost 1 USD",
+ type: "question",
+ uuid: "1",
+ },
+ {
+ instructions: "answering this question is free",
+ type: "question",
+ uuid: "2",
+ },
+ {
+ instructions: "this question is already answered",
+ type: "question",
+ uuid: "uuid-3",
+ },
+ ],
+ },
+ challenge_feedback: {
+ "uuid-3": {
+ state: "solved",
+ },
},
} as ReducerState);
-export const OneBadConfiguredPolicy = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting,
+export const OneBadConfiguredPolicy = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.challengeSelecting,
recovery_information: {
- policies: [[{uuid:'2'}]],
- challenges: [{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'sasd',
- uuid: '1',
- }]
+ policies: [[{ uuid: "1" }, { uuid: "2" }]],
+ challenges: [
+ {
+ instructions: "this policy has a missing uuid (the other auth method)",
+ type: "totp",
+ uuid: "1",
+ },
+ ],
},
} as ReducerState);
-export const NoPolicies = createExample(TestedComponent, reducerStatesExample.challengeSelecting);
+export const OnePolicyWithAllTheChallenges = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.challengeSelecting,
+ recovery_information: {
+ policies: [
+ [
+ { uuid: "1" },
+ { uuid: "2" },
+ { uuid: "3" },
+ { uuid: "4" },
+ { uuid: "5" },
+ { uuid: "6" },
+ { uuid: "7" },
+ { uuid: "8" },
+ ],
+ ],
+ challenges: [
+ {
+ instructions: "Does P equals NP?",
+ type: "question",
+ uuid: "1",
+ },
+ {
+ instructions: "SMS to 555-555",
+ type: "sms",
+ uuid: "2",
+ },
+ {
+ instructions: "Email to qwe@asd.com",
+ type: "email",
+ uuid: "3",
+ },
+ {
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ type: "totp",
+ uuid: "4",
+ },
+ {
+ //
+ instructions:
+ "Wire transfer from ASDXCVQWE123123 with holder Florian",
+ type: "iban",
+ uuid: "5",
+ },
+ {
+ instructions: "Join a video call",
+ type: "video", //Enter 8 digits code for "Anastasis"
+ uuid: "7",
+ },
+ {},
+ {
+ instructions: "Letter to address in postal code DE123123",
+ type: "post", //Enter 8 digits code for "Anastasis"
+ uuid: "8",
+ },
+ {
+ instructions: "instruction for an unknown type of challenge",
+ type: "new-type-of-challenge",
+ uuid: "6",
+ },
+ ],
+ },
+ } as ReducerState,
+);
+
+export const OnePolicyWithAllTheChallengesInDifferentState =
+ tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.challengeSelecting,
+ recovery_state: RecoveryStates.ChallengeSelecting,
+ recovery_information: {
+ policies: [
+ [
+ { 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" },
+ { uuid: "uuid-10" },
+ ],
+ ],
+ challenges: [
+ {
+ instructions: 'this challenge is in state "solved"',
+ type: "question",
+ uuid: "uuid-1",
+ },
+ {
+ instructions: 'this challenge is in state "code-in-file"',
+ type: "question",
+ uuid: "uuid-2",
+ },
+ {
+ instructions: 'this challenge is in state "code-sent"',
+ type: "question",
+ uuid: "uuid-3",
+ },
+ {
+ instructions: 'this challenge is in state "server-failure "',
+ type: "question",
+ uuid: "uuid-4",
+ },
+ {
+ instructions: 'this challenge is in state "truth-unknown"',
+ type: "question",
+ uuid: "uuid-5",
+ },
+ {
+ instructions: 'this challenge is in state "taler-payment"',
+ type: "question",
+ uuid: "uuid-6",
+ },
+ {
+ instructions: 'this challenge is in state "unsupported"',
+ type: "question",
+ uuid: "uuid-7",
+ },
+ {
+ instructions: 'this challenge is in state "rate-limit-exceeded"',
+ type: "question",
+ uuid: "uuid-8",
+ },
+ {
+ instructions: 'this challenge is in state "iban-instructions"',
+ type: "question",
+ uuid: "uuid-9",
+ },
+ {
+ instructions: 'this challenge is in state "incorrect-answer"',
+ type: "question",
+ uuid: "uuid-10",
+ },
+ ],
+ },
+ challenge_feedback: {
+ "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
+ "uuid-2": { state: ChallengeFeedbackStatus.CodeInFile.toString() },
+ "uuid-3": { state: ChallengeFeedbackStatus.CodeSent.toString() },
+ "uuid-4": {
+ state: ChallengeFeedbackStatus.ServerFailure.toString(),
+ http_status: 500,
+ error_response: "some error message or error object",
+ },
+ "uuid-5": { state: ChallengeFeedbackStatus.TruthUnknown.toString() },
+ "uuid-6": {
+ state: ChallengeFeedbackStatus.TalerPayment.toString(),
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ "uuid-7": { state: ChallengeFeedbackStatus.Unsupported.toString() },
+ "uuid-8": { state: ChallengeFeedbackStatus.RateLimitExceeded.toString() },
+ "uuid-9": {
+ state: ChallengeFeedbackStatus.IbanInstructions.toString(),
+ challenge_amount: "EUR:1" as AmountString,
+ target_iban: "DE12345789000",
+ target_business_name: "Data Loss Incorporated",
+ wire_transfer_subject: "Anastasis 987654321",
+ },
+ "uuid-10": { state: ChallengeFeedbackStatus.IncorrectAnswer.toString() },
+ },
+ } as ReducerState);
+export const NoPolicies = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.challengeSelecting,
+);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
index c9b52e91b..5b9c11bab 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
@@ -1,77 +1,293 @@
-import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ ChallengeFeedback,
+ ChallengeFeedbackStatus,
+} from "@gnu-taler/anastasis-core";
+import { Fragment, h, VNode } from "preact";
+import { AsyncButton } from "../../components/AsyncButton.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { authMethods, KnownAuthMethods } from "./authMethod/index.js";
+import { AnastasisClientFrame } from "./index.js";
+
+function OverviewFeedbackDisplay(props: {
+ feedback?: ChallengeFeedback;
+}): VNode {
+ const { feedback } = props;
+ if (!feedback) {
+ return <Fragment />;
+ }
+
+ switch (feedback.state) {
+ case ChallengeFeedbackStatus.Solved:
+ return <div />;
+ case ChallengeFeedbackStatus.IbanInstructions:
+ return <div class="block has-text-info">Payment required.</div>;
+ case ChallengeFeedbackStatus.ServerFailure:
+ return <div class="block has-text-danger">Server error.</div>;
+ case ChallengeFeedbackStatus.RateLimitExceeded:
+ return (
+ <div class="block has-text-danger">
+ There were to many failed attempts.
+ </div>
+ );
+ case ChallengeFeedbackStatus.Unsupported:
+ return (
+ <div class="block has-text-danger">
+ This client doesn&apos;t support solving this type of challenge. Use
+ another version or contact the provider.
+ </div>
+ );
+ case ChallengeFeedbackStatus.TruthUnknown:
+ return (
+ <div class="block has-text-danger">
+ Provider doesn&apos;t recognize the type of challenge. Use another
+ version or contact the provider.
+ </div>
+ );
+ case ChallengeFeedbackStatus.IncorrectAnswer:
+ return (
+ <div class="block has-text-danger">The answer was not correct.</div>
+ );
+ case ChallengeFeedbackStatus.CodeInFile:
+ return <div class="block has-text-info">code in file</div>;
+ case ChallengeFeedbackStatus.CodeSent:
+ return <div class="block has-text-info">Code sent</div>;
+ case ChallengeFeedbackStatus.TalerPayment:
+ return <div class="block has-text-info">Payment required</div>;
+ }
+}
export function ChallengeOverviewScreen(): 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_type !== "recovery") {
+ return <div>invalid state</div>;
}
- const policies = reducer.currentReducerState.recovery_information?.policies ?? [];
- const chArr = reducer.currentReducerState.recovery_information?.challenges ?? [];
- const challengeFeedback = reducer.currentReducerState?.challenge_feedback;
+ const policies =
+ reducer.currentReducerState.recovery_information?.policies ?? [];
+ const knownChallengesArray =
+ reducer.currentReducerState.recovery_information?.challenges ?? [];
+ const challengeFeedback =
+ reducer.currentReducerState?.challenge_feedback ?? {};
- const challenges: {
+ const knownChallengesMap: {
[uuid: string]: {
type: string;
instructions: string;
- cost: string;
+ feedback: ChallengeFeedback | undefined;
};
} = {};
- for (const ch of chArr) {
- challenges[ch.uuid] = {
+ for (const ch of knownChallengesArray) {
+ knownChallengesMap[ch.uuid] = {
type: ch.type,
- cost: ch.cost,
instructions: ch.instructions,
+ 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);
+
+ return {
+ isPolicySolved,
+ challenges,
+ corrupted: row.length > challenges.length,
+ };
+ })
+ .filter((p) => !p.corrupted);
+
+ const atLeastThereIsOnePolicySolved =
+ policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
+
+ const errors = !atLeastThereIsOnePolicySolved
+ ? "Solve one policy before proceeding"
+ : undefined;
return (
- <AnastasisClientFrame title="Recovery: Solve challenges">
- <h2>Policies</h2>
- {!policies.length && <p>
- No policies found
- </p>}
- {policies.map((row, i) => {
- return (
- <div key={i}>
- <h3>Policy #{i + 1}</h3>
- {row.map(column => {
- const ch = challenges[column.uuid];
- if (!ch) return <div>
- There is no challenge for this policy
+ <AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
+ {!policiesWithInfo.length ? (
+ <p class="block">
+ No policies found, try with another version of the secret
+ </p>
+ ) : 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 {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 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>
- const feedback = challengeFeedback?.[column.uuid];
+ );
+ }
+
+ 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 key={column.uuid}
- style={{
- borderLeft: "2px solid gray",
- paddingLeft: "0.5em",
- borderRadius: "0.5em",
- marginTop: "0.5em",
- marginBottom: "0.5em",
- }}
- >
- <h4>
- {ch.type} ({ch.instructions})
- </h4>
- <p>Status: {feedback?.state ?? "unknown"}</p>
- {feedback?.state !== "solved" ? (
- <button
- onClick={() => reducer.transition("select_challenge", {
- uuid: column.uuid,
- })}
+ <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.IbanInstructions:
+ case ChallengeFeedbackStatus.TalerPayment:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Pay
+ </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
- </button>
- ) : null}
+ </AsyncButton>
+ </div>
+ );
+ }
+ }
+ return (
+ <div
+ key={uuid}
+ class="block"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ <span class="icon">{method?.icon}</span>
+ <span>{info.instructions}</span>
</div>
- );
- })}
+ <OverviewFeedbackDisplay feedback={info.feedback} />
+ </div>
+
+ <ChallengeButton id={uuid} feedback={info.feedback} />
+ </div>
+ );
+ });
+
+ const policyName = policy.challenges
+ .map((x) => x.info.type)
+ .join(" + ");
+
+ const opa = !atLeastThereIsOnePolicySolved
+ ? undefined
+ : policy.isPolicySolved
+ ? undefined
+ : "0.6";
+
+ return (
+ <div
+ key={policy_index}
+ class="box"
+ style={{
+ opacity: opa,
+ }}
+ >
+ <h3 class="subtitle">
+ Policy #{policy_index + 1}: {policyName}
+ </h3>
+ {policy.challenges.length === 0 && (
+ <p>This policy doesn&apos;t have any challenges.</p>
+ )}
+ {policy.challenges.length === 1 && (
+ <p>This policy has one challenge.</p>
+ )}
+ {policy.challenges.length > 1 && (
+ <p>This policy has {policy.challenges.length} challenges.</p>
+ )}
+ {tableBody}
</div>
);
})}
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
new file mode 100644
index 000000000..0489e5a11
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
+import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen.js";
+
+export default {
+ title: "Challenge paying",
+ component: TestedComponent,
+ args: {
+ order: 10,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.challengePaying,
+);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
new file mode 100644
index 000000000..9f1201797
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
@@ -0,0 +1,45 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
+
+export function ChallengePayingScreen(): VNode {
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return <div>no reducer in context</div>;
+ }
+ if (reducer.currentReducerState?.reducer_type !== "recovery") {
+ return <div>invalid state</div>;
+ }
+ const payments = [""]; //reducer.currentReducerState.payments ??
+ return (
+ <AnastasisClientFrame hideNav title="Recovery: Challenge Paying">
+ <p>
+ Some of the providers require a payment to store the encrypted
+ authentication information.
+ </p>
+ <ul>
+ {payments.map((x, i) => {
+ return <li key={i}>{x}</li>;
+ })}
+ </ul>
+ <button onClick={() => reducer.transition("pay", {})}>
+ Check payment status now
+ </button>
+ </AnastasisClientFrame>
+ );
+}
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..0111815f5
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx
@@ -0,0 +1,84 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { AsyncButton } from "../../components/AsyncButton.js";
+
+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 aad37cd7f..646165341 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -1,36 +1,59 @@
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/ContinentSelectionScreen',
+ title: "Continent selection",
component: TestedComponent,
+ args: {
+ order: 2,
+ },
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry);
-export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry);
+export const BackupSelectContinent = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.backupSelectContinent,
+);
+
+export const BackupSelectCountry = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.backupSelectContinent,
+ selected_continent: "Testcontinent",
+} as ReducerState);
+
+export const RecoverySelectContinent = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.recoverySelectContinent,
+);
+
+export const RecoverySelectCountry = tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.recoverySelectContinent,
+ 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 ad529a4a7..3231e61e4 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -1,20 +1,159 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, withProcessLabel } from "./index";
+import { useState } from "preact/hooks";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame, withProcessLabel } from "./index.js";
export function ContinentSelectionScreen(): VNode {
- const reducer = useAnastasisContext()
- if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) {
- return <div />
+ const reducer = useAnastasisContext();
+
+ // 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 />;
}
- const sel = (x: string): void => reducer.transition("select_continent", { continent: x });
+ const selectContinent = (continent: string): void => {
+ reducer.transition("select_continent", { continent });
+ };
+ const selectCountry = (country: string): void => {
+ setCountryCode(country);
+ };
+
+ const continentList = reducer.currentReducerState.continents || [];
+ const countryList = reducer.currentReducerState.countries || [];
+ const theContinent = reducer.currentReducerState.selected_continent || "";
+ // const cc = reducer.currentReducerState.selected_country || "";
+ const theCountry = countryList.find((c) => c.code === countryCode);
+ const selectCountryAction = async () => {
+ // selection should be when the select box changes it value
+ if (!theCountry) return;
+ // FIXME: Why is there no await?
+ reducer.transition("select_country", {
+ country_code: countryCode,
+ });
+ };
+
+ // const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
+ // reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
+
+ // FIXME: i18n
+ const errors = !theCountry ? "Select a country" : undefined;
+
+ 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();
+ };
+
return (
- <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Continent")}>
- {reducer.currentReducerState.continents.map((x: any) => (
- <button onClick={() => sel(x.name)} key={x.name}>
- {x.name}
- </button>
- ))}
+ <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) => (
+ <option key={prov.name} value={prov.name}>
+ {prov.name}
+ </option>
+ ))}
+ </select>
+ <div class="icon is-small is-left">
+ <i class="mdi mdi-earth" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <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) => (
+ <option key={prov.name} value={prov.code}>
+ {prov.name}
+ </option>
+ ))}
+ </select>
+ <div class="icon is-small is-left">
+ <i class="mdi mdi-earth" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="column is-two-third">
+ <p>
+ 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 recommend 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>
+ </div> */}
+ </div>
+ </div>
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx
deleted file mode 100644
index adf36980f..000000000
--- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { CountrySelectionScreen as TestedComponent } from './CountrySelectionScreen';
-
-
-export default {
- title: 'Pages/CountrySelectionScreen',
- component: TestedComponent,
- argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
- },
-};
-
-export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry);
-export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry);
diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx
deleted file mode 100644
index 555622c1d..000000000
--- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, withProcessLabel } from "./index";
-
-export function CountrySelectionScreen(): VNode {
- const reducer = useAnastasisContext()
- if (!reducer) {
- return <div>no reducer in context</div>
- }
- if (!reducer.currentReducerState || !("countries" in reducer.currentReducerState)) {
- return <div>invalid state</div>
- }
- const sel = (x: any): void => reducer.transition("select_country", {
- country_code: x.code,
- currencies: [x.currency],
- });
- return (
- <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} >
- {reducer.currentReducerState.countries.map((x: any) => (
- <button onClick={() => sel(x)} key={x.name}>
- {x.name} ({x.currency})
- </button>
- ))}
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
new file mode 100644
index 000000000..3c9fd7f50
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -0,0 +1,141 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
+import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen.js";
+
+export default {
+ title: "Edit policies",
+ args: {
+ order: 6,
+ },
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const EditingAPolicy = tests.createExample(
+ TestedComponent,
+ { index: 0 },
+ {
+ ...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,
+);
+
+export const CreatingAPolicy = tests.createExample(
+ TestedComponent,
+ { index: 3 },
+ {
+ ...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,
+);
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
new file mode 100644
index 000000000..24550f89e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
@@ -0,0 +1,187 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { authMethods, KnownAuthMethods } from "./authMethod/index.js";
+import { AnastasisClientFrame } from "./index.js";
+
+export interface ProviderInfo {
+ url: string;
+ cost: string;
+ isFree: boolean;
+}
+
+export type ProviderInfoByType = {
+ [type in KnownAuthMethods]?: ProviderInfo[];
+};
+
+interface Props {
+ index: number;
+ cancel: () => void;
+ confirm: (changes: MethodProvider[]) => void;
+}
+
+export interface MethodProvider {
+ authentication_method: number;
+ provider: string;
+}
+
+export function EditPoliciesScreen({
+ index: policy_index,
+ cancel,
+ confirm,
+}: Props): VNode {
+ const [changedProvider, setChangedProvider] = useState<Array<string>>([]);
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return <div>no reducer in context</div>;
+ }
+ if (reducer.currentReducerState?.reducer_type !== "backup") {
+ return <div>invalid state</div>;
+ }
+
+ const selectableProviders: ProviderInfoByType = {};
+ const allProviders = Object.entries(
+ reducer.currentReducerState.authentication_providers || {},
+ );
+ for (let index = 0; index < allProviders.length; 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;
+ });
+ }
+ }
+
+ 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;
+ }
+
+ function sendChanges(): void {
+ const newMethods: MethodProvider[] = [];
+ allAuthMethods.forEach((method, index) => {
+ const oldValue = policy?.methods.find(
+ (m) => m.authentication_method === index,
+ );
+ if (changedProvider[index] === undefined && oldValue !== undefined) {
+ newMethods.push(oldValue);
+ }
+ if (
+ changedProvider[index] !== undefined &&
+ changedProvider[index] !== ""
+ ) {
+ newMethods.push({
+ authentication_method: index,
+ provider: changedProvider[index],
+ });
+ }
+ });
+ 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];
+
+ 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}
+ </option>
+ ))}
+ </select>
+ </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>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index 1a9462b88..ea88b74a0 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -1,47 +1,56 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/PoliciesPayingScreen',
+ title: "Policies paying",
component: TestedComponent,
+ args: {
+ order: 9,
+ },
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.policyPay);
-export const WithSomePaymentRequest = createExample(TestedComponent, {
+export const Example = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.policyPay,
+);
+export const WithSomePaymentRequest = tests.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 8a39cf0e4..c48236b9d 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
@@ -1,22 +1,37 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
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_type !== "backup") {
+ return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
-
+
return (
- <AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
+ <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 0c1842420..97e0821fd 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -1,42 +1,57 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { reducerStatesExample } from "../../utils/index.js";
+import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/RecoveryFinishedScreen',
+ title: "Recovery Finished",
+ args: {
+ order: 7,
+ },
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const NormalEnding = createExample(TestedComponent, {
+export const GoodEnding = tests.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 = tests.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 8c8a2c7c8..f528bc207 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -1,34 +1,129 @@
-import {
- bytesToString,
- decodeCrock
-} from "@gnu-taler/taler-util";
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { bytesToString, decodeCrock } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+import { useEffect, useState } from "preact/hooks";
+import { QR } from "../../components/QR.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
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_type !== "recovery") {
+ return <div>invalid state</div>;
}
- const encodedSecret = reducer.currentReducerState.core_secret?.value
+ const secretName = reducer.currentReducerState.recovery_document?.secret_name;
+ const encodedSecret = reducer.currentReducerState.core_secret;
if (!encodedSecret) {
- return <AnastasisClientFrame title="Recovery Problem" hideNext>
- <p>
- Secret not found
- </p>
- </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))
+ const secret = bytesToString(decodeCrock(encodedSecret.value));
+ const plainText =
+ encodedSecret.value.length < 1000 && encodedSecret.mime === "text/plain";
+
+ let [uri, setUri] = useState(`data:${encodedSecret.mime},${secret}`);
+ fetch(`data:${encodedSecret.mime},${secret}`) // TODO: look into using new Blob
+ .then((v) => v.blob())
+ .then((blob) => URL.createObjectURL(blob))
+ .then((newUri) => {
+ setUri(newUri);
+ });
return (
- <AnastasisClientFrame title="Recovery Finished" hideNext>
- <p>
- Secret: {secret}
- </p>
+ <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}>
+ {plainText ? (
+ <button
+ class="button"
+ onClick={() => {
+ navigator.clipboard.writeText(secret);
+ setCopied(true);
+ }}
+ >
+ {!copied ? "Copy" : "Copied"}
+ </button>
+ ) : undefined}
+
+ <a
+ class="button is-info"
+ download={
+ encodedSecret.filename ? encodedSecret.filename : "secret.file"
+ }
+ href={uri}
+ >
+ <div class="icon is-small ">
+ <i class="mdi mdi-download" />
+ </div>
+ <span>Download content</span>
+ </a>
+ </div>
+
+ {plainText ? (
+ <div class="block">
+ <QR text={secret} />
+ </div>
+ ) : undefined}
+
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "center",
+ }}
+ >
+ <p>
+ <div class="buttons ml-4">
+ <button
+ class="button is-primary is-right"
+ onClick={() => reducer.reset()}
+ >
+ Start again
+ </button>
+ </div>
+ </p>
+ </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 b52699e7b..71144917a 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -1,81 +1,270 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/ReviewPoliciesScreen',
+ title: "Reviewing Policies",
+ 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: []
-} as ReducerState);
+export const HasPoliciesButMethodListIsEmpty = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.policyReview,
+ 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, {
- ...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 0,
- provider: 'asd'
- },{
- authentication_method: 1,
- provider: 'asd'
- }]
- },{
- methods: [{
- authentication_method: 1,
- provider: 'asd'
- }]
- }],
- authentication_methods: [{
- challenge: 'asd',
- instructions: 'ins',
- type: 'type',
- },{
- challenge: 'asd2',
- instructions: 'ins2',
- type: 'type2',
- }]
-} as ReducerState);
+export const SomePoliciesWithMethods = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ 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 b360ccaf0..3755dac9c 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
@@ -1,51 +1,160 @@
-/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AuthenticationProviderStatusOk } from "@gnu-taler/anastasis-core";
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+import { useState } from "preact/hooks";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { authMethods, KnownAuthMethods } from "./authMethod/index.js";
+import { EditPoliciesScreen } from "./EditPoliciesScreen.js";
+import { AnastasisClientFrame } from "./index.js";
export function ReviewPoliciesScreen(): VNode {
- const reducer = useAnastasisContext()
+ const [editingPolicy, setEditingPolicy] = useState<number | undefined>();
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (reducer.currentReducerState?.reducer_type !== "backup") {
+ return <div>invalid state</div>;
}
- const authMethods = 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
+ index={editingPolicy}
+ cancel={() => setEditingPolicy(undefined)}
+ confirm={async (newMethods) => {
+ await reducer.transition("update_policy", {
+ policy_index: editingPolicy,
+ policy: newMethods,
+ });
+ setEditingPolicy(undefined);
+ }}
+ />
+ );
+ }
+
+ const errors = policies.length < 1 ? "Need more policies" : undefined;
return (
- <AnastasisClientFrame title="Backup: Review Recovery Policies">
+ <AnastasisClientFrame
+ hideNext={errors}
+ title="Backup: Review Recovery Policies"
+ >
+ {policies.length > 0 && (
+ <p class="block">
+ Based on your configured authentication method you have created, some
+ policies have been configured. In order to recover your secret you
+ have to solve all the challenges of at least one policy.
+ </p>
+ )}
+ {policies.length < 1 && (
+ <p class="block">
+ No policies had been created. Go back and add more authentication
+ methods.
+ </p>
+ )}
+ <div class="block">
+ <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 => authMethods[x.authentication_method] && ({ ...authMethods[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="policy">
- <h3>
- Policy #{policy_index + 1}: {policyName}
- </h3>
- Required Authentications:
- {!methods.length && <p>
- No auth method found
- </p>}
- <ul>
+ <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.map((m, i) => {
+ const p = providers[
+ m.provider
+ ] as AuthenticationProviderStatusOk;
return (
- <li key={i}>
- {m.type} ({m.instructions}) at provider {m.provider}
- </li>
+ <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} target="_blank" rel="noreferrer">
+ {p.business_name}
+ </a>
+ </span>
+ </p>
);
})}
- </ul>
- <div>
+ </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
- onClick={() => reducer.transition("delete_policy", { policy_index })}
+ class="button is-danger block"
+ onClick={() =>
+ reducer.transition("delete_policy", { policy_index })
+ }
>
- Delete Policy
+ Delete
</button>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index 18560356a..24bbb2927 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -1,44 +1,50 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/SecretEditorScreen',
+ title: "Secret editor",
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',
-} as ReducerState);
+export const WithSecretNamePreselected = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.secretEdition,
+ secret_name: "someSecretName",
+ } as ReducerState,
+);
-export const WithoutName = createExample(TestedComponent, {
+export const WithoutName = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.secretEdition,
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
index a5235d66c..93a27837c 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
@@ -1,65 +1,124 @@
-/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
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,
- LabeledInput
-} from "./index";
+ FileInput,
+ FileTypeContent,
+} from "../../components/fields/FileInput.js";
+import { TextInput } from "../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
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_type !== "backup") {
+ return <div>invalid state</div>;
}
- const secretNext = (): void => {
- reducer.runTransaction(async (tx) => {
+ const secretNext = async (): Promise<void> => {
+ const secret = secretFile
+ ? {
+ value: encodeCrock(stringToBytes(secretFile.content)),
+ 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,
+ t_s: new Date().getTime() + 60 * 60 * 24 * 365 * 5,
},
});
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
- title="Backup: Provide secret"
+ hideNext={errors}
+ title="Backup: Provide secret to backup"
onNext={() => secretNext()}
>
- <div>
- <LabeledInput
- label="Secret Name:"
+ <div class="block">
+ <TextInput
+ label="Secret name:"
+ tooltip="This allows you to uniquely identify a secret if you have made multiple back ups. The value entered here will NOT be protected by the authentication checks!"
grabFocus
+ onConfirm={goNextIfNoErrors}
bind={[secretName, setSecretName]}
/>
+ <div>
+ Names should be unique, so that you can easily identify your secret
+ later.
+ </div>
</div>
- <div>
- <LabeledInput
- label="Secret Value:"
+ <div class="block">
+ <TextInput
+ inputType="multiline"
+ disabled={!!secretFile}
+ onConfirm={goNextIfNoErrors}
+ label="Enter the secret as text:"
bind={[secretValue, setSecretValue]}
/>
</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 e9c597023..fb3b26e15 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -1,50 +1,83 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
+import {
+ SecretSelectionScreen,
+ SecretSelectionScreenFound,
+} from "./SecretSelectionScreen.js";
export default {
- title: 'Pages/SecretSelectionScreen',
- component: TestedComponent,
- argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ title: "Secret selection",
+ component: SecretSelectionScreen,
+ args: {
+ order: 4,
},
};
-export const Example = createExample(TestedComponent, {
- ...reducerStatesExample.secretSelection,
- recovery_document: {
- provider_url: 'http://anastasis.url/',
- secret_name: 'secretName',
- version: 1,
+export const Example = tests.createExample(
+ SecretSelectionScreenFound,
+ {
+ policies: [
+ {
+ secret_name: "The secret name 1",
+ attribute_mask: 1,
+ policy_hash: "abcdefghijklmnopqrstuvwxyz",
+ providers: [
+ {
+ url: "http://someurl",
+ version: 1,
+ },
+ ],
+ },
+ {
+ secret_name: "The secret name 2",
+ attribute_mask: 1,
+ policy_hash: "abcdefghijklmnopqrstuvwxyz",
+ providers: [
+ {
+ url: "http://someurl",
+ version: 1,
+ },
+ ],
+ },
+ ],
},
-} as ReducerState);
-
+ {
+ ...reducerStatesExample.secretSelection,
+ recovery_document: {
+ provider_url: "https://kudos.demo.anastasis.lu/",
+ secret_name: "secretName",
+ version: 1,
+ },
+ } as ReducerState,
+);
-export const NoRecoveryDocumentFound = createExample(TestedComponent, {
- ...reducerStatesExample.secretSelection,
- recovery_document: undefined,
-} as ReducerState);
+export const NoRecoveryDocumentFound = tests.createExample(
+ SecretSelectionScreen,
+ {},
+ {
+ ...reducerStatesExample.secretSelection,
+ recovery_document: undefined,
+ } as ReducerState,
+);
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 903f57868..ce44b0884 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -1,87 +1,452 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
-export function SecretSelectionScreen(): VNode {
- const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
- const [otherProvider, setOtherProvider] = useState<string>("");
- const reducer = useAnastasisContext()
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
- const currentVersion = reducer?.currentReducerState
- && ("recovery_document" in reducer.currentReducerState)
- && reducer.currentReducerState.recovery_document?.version;
+ GNU Anastasis 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 Affero General Public License for more details.
- const [otherVersion, setOtherVersion] = useState<number>(currentVersion || 0);
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AggregatedPolicyMetaInfo,
+ AuthenticationProviderStatus,
+ AuthenticationProviderStatusOk,
+} from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton.js";
+import { PhoneNumberInput } from "../../components/fields/NumberInput.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import AddingProviderScreen from "./AddingProviderScreen/index.js";
+import { AnastasisClientFrame } from "./index.js";
+export function SecretSelectionScreenFound({
+ policies,
+ onManageProvider,
+ onNext,
+}: {
+ policies: AggregatedPolicyMetaInfo[];
+ onManageProvider: () => void;
+ onNext: (version: AggregatedPolicyMetaInfo) => void;
+}): VNode {
+ 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.reducer_type !== "recovery"
+ ) {
+ return <div>invalid state</div>;
}
+ return (
+ <AnastasisClientFrame
+ title="Recovery: Select secret"
+ hideNext="Please select version to recover"
+ >
+ <div class="columns">
+ <div class="column">
+ <p class="block">Found versions:</p>
+ {policies.map((version, i) => (
+ <div key={i} class="box">
+ <div
+ class="block"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ <b>Name:</b>&nbsp;<span>{version.secret_name}</span>
+ </div>
+ <div style={{ display: "flex", alignItems: "center" }}>
+ <b>Id:</b>&nbsp;
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip={version.policy_hash}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ <span>{version.policy_hash.substring(0, 22)}...</span>
+ </div>
+ </div>
+
+ <div>
+ <AsyncButton
+ class="button"
+ onClick={async () => onNext(version)}
+ >
+ Recover
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ <div class="column">
+ <p>
+ Secret found, you can select another version or continue to the
+ challenges solving
+ </p>
+ <p class="block">
+ <a onClick={onManageProvider}>Manage recovery providers</a>
+ </p>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+export function SecretSelectionScreen(): VNode {
+ const reducer = useAnastasisContext();
+ const [manageProvider, setManageProvider] = useState(false);
+
+ useEffect(() => {
+ async function f() {
+ if (reducer) {
+ await reducer.discoverStart();
+ }
+ }
+ f().catch((e) => console.log(e));
+ }, []);
- function selectVersion(p: string, n: number): void {
- if (!reducer) return;
- reducer.runTransaction(async (tx) => {
- await tx.transition("change_version", {
- version: n,
- provider_url: p,
- });
- setSelectingVersion(false);
- });
+ if (!reducer) {
+ return <div>no reducer in context</div>;
}
- const recoveryDocument = reducer.currentReducerState.recovery_document
- if (!recoveryDocument) {
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.reducer_type !== "recovery"
+ ) {
+ return <div>invalid state</div>;
+ }
+
+ if (manageProvider) {
return (
- <AnastasisClientFrame hideNav title="Recovery: Problem">
- <p>No recovery document found</p>
- </AnastasisClientFrame>
- )
+ <AddingProviderScreen onCancel={async () => setManageProvider(false)} />
+ );
+ }
+
+ if (
+ reducer.discoveryState.state === "none" ||
+ reducer.discoveryState.state === "active"
+ ) {
+ // Can this even happen?
+ return <SecretSelectionScreenWaiting />;
}
- if (selectingVersion) {
+
+ const policies = reducer.discoveryState.aggregatedPolicies ?? [];
+
+ if (policies.length === 0) {
return (
- <AnastasisClientFrame hideNav title="Recovery: Select secret">
- <p>Select a different version of the secret</p>
- <select onChange={(e) => setOtherProvider((e.target as any).value)}>
- {Object.keys(reducer.currentReducerState.authentication_providers ?? {}).map(
- (x, i) => (
- <option key={i} selected={x === recoveryDocument.provider_url} value={x}>
- {x}
+ <AddingProviderScreen
+ onCancel={async () => setManageProvider(false)}
+ notifications={[
+ {
+ message: "Secret not found",
+ type: "ERROR",
+ description:
+ "With the information you provided we could not found secret in any of the providers. You can try adding more providers if you think the data is correct.",
+ },
+ ]}
+ />
+ );
+ }
+
+ return (
+ <SecretSelectionScreenFound
+ policies={policies}
+ onNext={(version) => reducer.transition("select_version", version)}
+ onManageProvider={async () => setManageProvider(false)}
+ />
+ );
+}
+
+// export function OldSecretSelectionScreen(): VNode {
+// const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
+// const reducer = useAnastasisContext();
+// const [manageProvider, setManageProvider] = useState(false);
+
+// useEffect(() => {
+// async function f() {
+// if (reducer) {
+// await reducer.discoverStart();
+// }
+// }
+// f().catch((e) => console.log(e));
+// }, []);
+
+// const currentVersion =
+// (reducer?.currentReducerState &&
+// "recovery_document" in reducer.currentReducerState &&
+// reducer.currentReducerState.recovery_document?.version) ||
+// 0;
+
+// if (!reducer) {
+// return <div>no reducer in context</div>;
+// }
+// if (
+// !reducer.currentReducerState ||
+// reducer.currentReducerState.reducer_type !== "recovery"
+// ) {
+// return <div>invalid state</div>;
+// }
+
+// async function doSelectVersion(p: string, n: number): Promise<void> {
+// if (!reducer) return Promise.resolve();
+// return reducer.runTransaction(async (tx) => {
+// await tx.transition("select_version", {
+// version: n,
+// provider_url: p,
+// });
+// setSelectingVersion(false);
+// });
+// }
+
+// const provs = reducer.currentReducerState.authentication_providers ?? {};
+// const recoveryDocument = reducer.currentReducerState.recovery_document;
+
+// if (!recoveryDocument) {
+// return (
+// <ChooseAnotherProviderScreen
+// providers={provs}
+// selected=""
+// onChange={(newProv) => doSelectVersion(newProv, 0)}
+// />
+// );
+// }
+
+// if (selectingVersion) {
+// return (
+// <SelectOtherVersionProviderScreen
+// providers={provs}
+// provider={recoveryDocument.provider_url}
+// version={recoveryDocument.version}
+// onCancel={() => setSelectingVersion(false)}
+// onConfirm={doSelectVersion}
+// />
+// );
+// }
+
+// if (manageProvider) {
+// return (
+// <AddingProviderScreen onCancel={async () => setManageProvider(false)} />
+// );
+// }
+
+// const providerInfo = 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">{providerInfo.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>
+// )}
+// </div>
+// <div class="buttons is-right">
+// <button class="button" onClick={(e) => setSelectingVersion(true)}>
+// Change secret&apos;s version
+// </button>
+// </div>
+// </div>
+// </div>
+// <div class="column">
+// <p>
+// Secret found, you can select another version or continue to the
+// challenges solving
+// </p>
+// <p class="block">
+// <a onClick={() => setManageProvider(true)}>
+// Manage recovery providers
+// </a>
+// </p>
+// </div>
+// </div>
+// </AnastasisClientFrame>
+// );
+// }
+
+function ChooseAnotherProviderScreen({
+ onChange,
+}: {
+ onChange: (prov: string) => void;
+}): VNode {
+ const reducer = useAnastasisContext();
+
+ if (!reducer) {
+ return <div>no reducer in context</div>;
+ }
+
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.reducer_type !== "recovery"
+ ) {
+ return <div>invalid state</div>;
+ }
+ const providers = reducer.currentReducerState.authentication_providers ?? {};
+
+ return (
+ <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="">
+ <option key="none" disabled selected value="">
+ Choose a provider
</option>
- )
- )}
- </select>
- <div>
- <input
- value={otherVersion}
- onChange={(e) => setOtherVersion(Number((e.target as HTMLInputElement).value))}
- type="number" />
- <button onClick={() => selectVersion(otherProvider, otherVersion)}>
- Use this version
- </button>
+ {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" />
+ </div>
+ </div>
</div>
- <div>
- <button onClick={() => selectVersion(otherProvider, 0)}>
- Use latest version
- </button>
- </div>
- <div>
- <button onClick={() => setSelectingVersion(false)}>Cancel</button>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+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 > 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 {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>
+ )}
+ <p>Specify other version below or use the latest</p>
+ </div>
+
+ <div class="field">
+ <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>
+ {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" />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="container">
+ <PhoneNumberInput
+ label="Version"
+ placeholder="version number to recover"
+ grabFocus
+ bind={[otherVersion, setOtherVersion]}
+ />
+ </div>
+ </div>
+ <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>
+ </div>
+ </div>
</div>
- </AnastasisClientFrame>
- );
- }
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+function SecretSelectionScreenWaiting(): VNode {
return (
<AnastasisClientFrame title="Recovery: Select secret">
- <p>Provider: {recoveryDocument.provider_url}</p>
- <p>Secret version: {recoveryDocument.version}</p>
- <p>Secret name: {recoveryDocument.secret_name}</p>
- <button onClick={() => setSelectingVersion(true)}>
- Select different secret
- </button>
+ <div>loading secret versions</div>
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx
deleted file mode 100644
index 2c27895c2..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveEmailEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", {
- answer,
- })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx
deleted file mode 100644
index 1a824acb8..000000000
--- a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolvePostEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", { answer })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx
deleted file mode 100644
index 72dadbe89..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveQuestionEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", { answer })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>Question: {challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index 69af9be42..dc707a052 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -1,121 +1,72 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 { ReducerState } from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { SolveScreen as TestedComponent } from "./SolveScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/SolveScreen',
+ title: "Solve Screen",
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 NotSupportedChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'follow htis instructions',
- type: 'chall-type',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const MismatchedChallengeId = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'follow htis instructions',
- 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: 'follow htis instructions',
- 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: 'follow htis instructions',
- type: 'question',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
+export const NoInformation = tests.createExample(
+ TestedComponent,
+ reducerStatesExample.challengeSolving,
+);
-export const EmailChallenge = createExample(TestedComponent, {
+export const NotSupportedChallenge = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSolving,
recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'follow htis instructions',
- type: 'email',
- uuid: 'ASDASDSAD!1'
- }],
+ challenges: [
+ {
+ 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 PostChallenge = createExample(TestedComponent, {
+export const MismatchedChallengeId = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSolving,
recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'follow htis instructions',
- type: 'post',
- uuid: 'ASDASDSAD!1'
- }],
+ challenges: [
+ {
+ 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 05ae50b48..7f4d5aa18 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
@@ -1,54 +1,255 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ ChallengeFeedback,
+ ChallengeFeedbackStatus,
+} from "@gnu-taler/anastasis-core";
import { h, VNode } from "preact";
-import { ChallengeFeedback, ChallengeInfo } from "../../../../anastasis-core/lib";
-import { useAnastasisContext } from "../../context/anastasis";
-import { SolveEmailEntry } from "./SolveEmailEntry";
-import { SolvePostEntry } from "./SolvePostEntry";
-import { SolveQuestionEntry } from "./SolveQuestionEntry";
-import { SolveSmsEntry } from "./SolveSmsEntry";
-import { SolveUnsupportedEntry } from "./SolveUnsupportedEntry";
+import { Notifications } from "../../components/Notifications.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { authMethods, KnownAuthMethods } from "./authMethod/index.js";
+import { AnastasisClientFrame } from "./index.js";
+
+export function SolveOverviewFeedbackDisplay(props: {
+ feedback?: ChallengeFeedback;
+}): VNode {
+ const { feedback } = props;
+ if (!feedback) {
+ return <div />;
+ }
+ switch (feedback.state) {
+ case ChallengeFeedbackStatus.TalerPayment:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: (
+ <span>
+ To pay you can{" "}
+ <a
+ href={feedback.taler_pay_uri}
+ target="_blank"
+ rel="noreferrer"
+ >
+ click here
+ </a>
+ </span>
+ ),
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.IbanInstructions:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: `Need to send a wire transfer to "${feedback.target_business_name}"`,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.ServerFailure:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Server error: response code ${feedback.http_status}`,
+ description: !feedback.error_response
+ ? undefined
+ : `More information: ${JSON.stringify(
+ feedback.error_response,
+ )}`,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.RateLimitExceeded:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: "There were to many failed attempts.",
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.Unsupported:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `This client doesn't support solving this type of challenge`,
+ description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"`,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.TruthUnknown:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Provider doesn't recognize the type of challenge`,
+ description: "Contact the provider for further information",
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.CodeInFile:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Required TAN can be found in file "${feedback.filename}"`,
+ description: feedback.display_hint
+ ? `HINT: ${feedback.display_hint}`
+ : undefined,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.CodeSent:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Code sent to address "${feedback.address_hint}"`,
+ description: feedback.display_hint
+ ? `HINT: ${feedback.display_hint}`
+ : undefined,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.IncorrectAnswer:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `The answer is wrong.`,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.Solved:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "SUCCESS",
+ message: `This challenge is solved`,
+ },
+ ]}
+ />
+ );
+ }
+}
export function SolveScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (reducer.currentReducerState?.reducer_type !== "recovery") {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
}
if (!reducer.currentReducerState.recovery_information) {
- return <div>no recovery information found</div>
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
}
if (!reducer.currentReducerState.selected_challenge_uuid) {
- return <div>no selected uuid</div>
+ 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>
+ );
}
+ 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 = dialogMap[selectedChallenge?.type] ?? SolveUnsupportedEntry;
- return (
- <SolveDialog
- challenge={selectedChallenge}
- feedback={challengeFeedback[selectedUuid]} />
- );
-}
+ const selectedChallenge = chArr.find((ch) => ch.uuid === selectedUuid);
-export interface SolveEntryProps {
- challenge: ChallengeInfo;
- feedback?: ChallengeFeedback;
-}
+ const SolveDialog =
+ !selectedChallenge ||
+ !authMethods[selectedChallenge.type as KnownAuthMethods]
+ ? SolveNotImplemented
+ : authMethods[selectedChallenge.type as KnownAuthMethods].solve ??
+ SolveNotImplemented;
+ return <SolveDialog id={selectedUuid} />;
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx
deleted file mode 100644
index 163e0d1f3..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveSmsEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", {
- answer,
- })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx
deleted file mode 100644
index 7f538d249..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { h, VNode } from "preact";
-import { AnastasisClientFrame } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
- return (
- <AnastasisClientFrame hideNext title="Recovery: Solve challenge">
- <p>{JSON.stringify(props.challenge)}</p>
- <p>Challenge not supported.</p>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index ad84cd8f2..1f6145345 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -1,35 +1,42 @@
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { StartScreen as TestedComponent } from './StartScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
+import { StartScreen as TestedComponent } from "./StartScreen.js";
export default {
- title: 'Pages/StartScreen',
+ title: "Start screen",
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 = tests.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 6625ec5b8..03399cfba 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
@@ -1,33 +1,71 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+import { FileButton } from "../../components/FlieButton.js";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
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>
- <section class="section is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
-
- <div class="buttons is-right">
- <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
- Backup
- </button>
+ <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>
+ <span>Backup a secret</span>
+ </button>
- <button class="button is-info" onClick={() => reducer.startRecover()}>Recover</button>
+ <button
+ class="button is-info"
+ onClick={() => reducer.startRecover()}
+ >
+ <div class="icon">
+ <i class="mdi mdi-arrow-down" />
</div>
+ <span>Recover a secret</span>
+ </button>
+
+ <FileButton
+ label="Restore a session"
+ onChange={(content) => {
+ if (content?.type === "application/json") {
+ reducer.importState(content.content);
+ }
+ }}
+ />
- </div>
- <div class="column" />
+ {/* <button class="button">
+ <div class="icon"><i class="mdi mdi-file" /></div>
+ <span>Restore a session</span>
+ </button> */}
</div>
- </section>
+ </div>
+ <div class="column" />
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index e2f3d521e..424c4884a 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -1,40 +1,47 @@
/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE. See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @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 "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../utils/index.js";
+import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'Pages/TruthsPayingScreen',
+ title: "Truths Paying",
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 WithPaytoList = createExample(TestedComponent, {
+export const Example = tests.createExample(
+ TestedComponent,
+ {},
+ reducerStatesExample.truthsPaying,
+);
+export const WithPaytoList = tests.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 319f590a0..c9a555c35 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
@@ -1,21 +1,33 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame } from "./index";
+import { useAnastasisContext } from "../../context/anastasis.js";
+import { AnastasisClientFrame } from "./index.js";
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_type !== "backup") {
+ return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.payments ?? [];
return (
- <AnastasisClientFrame
- hideNext
- title="Backup: Authentication Storage Payments"
- >
+ <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
new file mode 100644
index 000000000..aee7829ff
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+
+export default {
+ title: "Auth method: Email setup",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "email";
+
+export const Empty = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithOneExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to sebasjm@email.com ",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithMoreExamples = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to sebasjm@email.com",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to someone@sebasjm.com",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
new file mode 100644
index 000000000..b3af0f080
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
@@ -0,0 +1,113 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { EmailInput } from "../../../components/fields/EmailInput.js";
+import { AnastasisClientFrame } from "../index.js";
+import { AuthMethodSetupProps } from "./index.js";
+
+const EMAIL_PATTERN =
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+
+export function AuthMethodEmailSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
+ const [email, setEmail] = useState("");
+ const addEmailAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "email",
+ instructions: `Email to ${email}`,
+ challenge: encodeCrock(stringToBytes(email)),
+ },
+ });
+ const emailError = !EMAIL_PATTERN.test(email)
+ ? "Email address is not valid"
+ : undefined;
+ const errors = !email ? "Add your email" : emailError;
+
+ 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. Add the uuid from the challenge
+ </p>
+ <div>
+ <EmailInput
+ label="Email address"
+ error={emailError}
+ onConfirm={goNextIfNoErrors}
+ placeholder="email@domain.com"
+ bind={[email, setEmail]}
+ />
+ </div>
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your emails:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
+ <div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <span data-tooltip={errors}>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addEmailAuth}
+ >
+ Add
+ </button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
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..075bab2a7
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ChallengeFeedbackStatus,
+ ReducerState,
+} from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+export default {
+ title: "Auth method: Email solve",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "email";
+
+export const WithoutFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "uuid-1",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "Email to me@domain.com",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+);
+
+export const PaymentFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "uuid-1",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "Email to me@domain.com",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ challenge_feedback: {
+ "uuid-1": {
+ state: ChallengeFeedbackStatus.TalerPayment,
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ },
+ } as ReducerState,
+);
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..6a9595a83
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
@@ -0,0 +1,195 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { ChallengeInfo } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton.js";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { useTranslationContext } from "../../../context/translation.js";
+import { AnastasisClientFrame } from "../index.js";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
+import { shouldHideConfirm } from "./helpers.js";
+import { AuthMethodSolveProps } from "./index.js";
+
+export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, _setAnswer] = useState("A-");
+
+ function setAnswer(str: string): void {
+ //A-12345-678-1234-5678
+ const unformatted = str
+ .replace(/^A-/, "")
+ .replace(/-/g, "")
+ .toLocaleUpperCase();
+
+ let result = `A-${unformatted.substring(0, 5)}`;
+ if (unformatted.length > 5) {
+ result += `-${unformatted.substring(5, 8)}`;
+ }
+ if (unformatted.length > 8) {
+ result += `-${unformatted.substring(8, 12)}`;
+ }
+ if (unformatted.length > 12) {
+ result += `-${unformatted.substring(12)}`;
+ }
+
+ _setAnswer(result);
+ }
+ const [expanded, setExpanded] = useState(false);
+ const { i18n } = useTranslationContext();
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (reducer.currentReducerState?.reducer_type !== "recovery") {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state, no recovery 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, no challenge id</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: `A-${answer.replace(/^A-/, "").replace(/-/g, "").trim()}`,
+ });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const error =
+ answer.length > 21
+ ? i18n.str`The answer should not be greater than 21 characters.`
+ : undefined;
+
+ return (
+ <AnastasisClientFrame hideNav title="Email challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ An email has been sent to &quot;<b>{selectedChallenge.instructions}</b>
+ &quot;. The message has and identification code and recovery code that
+ starts with &quot;
+ <b>A-</b>&quot;. Wait the message to arrive and the enter the recovery
+ code below.
+ </p>
+ {!expanded ? (
+ <p>
+ The identification code in the email should start with &quot;
+ {selectedUuid.substring(0, 10)}&quot;
+ <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 &quot;{selectedUuid}&quot;
+ <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]}
+ error={error}
+ placeholder="A-12345-678-1234-5678"
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm(feedback) && (
+ <AsyncButton
+ class="button is-info"
+ onClick={onNext}
+ disabled={!!error}
+ >
+ 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
new file mode 100644
index 000000000..d571093f7
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+
+export default {
+ title: "Auth method: IBAN setup",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "iban";
+
+export const Empty = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithOneExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
+export const WithMoreExamples = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ 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,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
new file mode 100644
index 000000000..663ccb644
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
@@ -0,0 +1,129 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ canonicalJson,
+ encodeCrock,
+ stringToBytes,
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { AnastasisClientFrame } from "../index.js";
+import { AuthMethodSetupProps } from "./index.js";
+
+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;
+ 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.
+ </p>
+ <div>
+ <TextInput
+ label="Bank account holder name"
+ grabFocus
+ placeholder="John Smith"
+ onConfirm={goNextIfNoErrors}
+ bind={[name, setName]}
+ />
+ <TextInput
+ label="IBAN"
+ placeholder="DE91100000000123456789"
+ 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>
+ )}
+ <div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <span data-tooltip={errors}>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addIbanAuth}
+ >
+ Add
+ </button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
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..2a16c8456
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { KnownAuthMethods, authMethods as TestedComponent } from "./index.js";
+
+export default {
+ title: "Auth method: IBAN Solve",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "iban";
+
+export const WithoutFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "uuid-1",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+);
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..dec65812e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ChallengeInfo } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { AnastasisClientFrame } from "../index.js";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
+import { shouldHideConfirm } from "./helpers.js";
+import { AuthMethodSolveProps } from "./index.js";
+
+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_type !== "recovery") {
+ 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();
+ }
+
+ 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(feedback) && (
+ <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
new file mode 100644
index 000000000..a893c923e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+
+export default {
+ title: "Auth method: Post setup",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "post";
+
+export const Empty = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithOneExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code QWE456",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithMoreExamples = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ 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,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
new file mode 100644
index 000000000..2a8199783
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
@@ -0,0 +1,161 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ canonicalJson,
+ encodeCrock,
+ stringToBytes,
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { AnastasisClientFrame } from "../index.js";
+import { AuthMethodSetupProps } from "./index.js";
+
+export function AuthMethodPostSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
+ const [fullName, setFullName] = useState("");
+ const [street, setStreet] = useState("");
+ const [city, setCity] = useState("");
+ const [postcode, setPostcode] = useState("");
+ const [country, setCountry] = useState("");
+
+ const addPostAuth = () => {
+ const challengeJson = {
+ full_name: fullName,
+ street,
+ city,
+ postcode,
+ country,
+ };
+ addAuthMethod({
+ authentication_method: {
+ type: "post",
+ instructions: `Letter to address in postal code ${postcode}`,
+ challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
+ },
+ });
+ };
+
+ const errors = !fullName
+ ? "The full name is missing"
+ : !street
+ ? "The street is missing"
+ : !city
+ ? "The city is missing"
+ : !postcode
+ ? "The postcode is missing"
+ : !country
+ ? "The country is missing"
+ : undefined;
+
+ 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.
+ </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
+ onConfirm={goNextIfNoErrors}
+ label="City"
+ bind={[city, setCity]}
+ />
+ </div>
+ <div>
+ <TextInput
+ 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>
+ <span data-tooltip={errors}>
+ <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..3495f7f63
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+export default {
+ title: "Auth method: Post solve",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "post";
+
+export const WithoutFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "uuid-1",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+);
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..8204ab1cf
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
@@ -0,0 +1,160 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ChallengeInfo } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton.js";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { useTranslationContext } from "../../../context/translation.js";
+import { AnastasisClientFrame } from "../index.js";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
+import { shouldHideConfirm } from "./helpers.js";
+import { AuthMethodSolveProps } from "./index.js";
+
+export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, _setAnswer] = useState("A-");
+
+ function setAnswer(str: string): void {
+ //A-12345-678-1234-5678
+ const unformatted = str
+ .replace(/^A-/, "")
+ .replace(/-/g, "")
+ .toLocaleUpperCase();
+
+ let result = `A-${unformatted.substring(0, 5)}`;
+ if (unformatted.length > 5) {
+ result += `-${unformatted.substring(5, 8)}`;
+ }
+ if (unformatted.length > 8) {
+ result += `-${unformatted.substring(8, 12)}`;
+ }
+ if (unformatted.length > 12) {
+ result += `-${unformatted.substring(12)}`;
+ }
+
+ _setAnswer(result);
+ }
+ const { i18n } = useTranslationContext();
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (reducer.currentReducerState?.reducer_type !== "recovery") {
+ 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: `A-${answer.replace(/^A-/, "").replace(/-/g, "").trim()}`,
+ });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const error =
+ answer.length > 21
+ ? i18n.str`The answer should not be greater than 21 characters.`
+ : undefined;
+
+ return (
+ <AnastasisClientFrame hideNav title="Postal Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>Wait for the answer</p>
+ <TextInput
+ onConfirm={onNext}
+ label="Answer"
+ grabFocus
+ placeholder="A-12345-678-1234-5678"
+ error={error}
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm(feedback) && (
+ <AsyncButton
+ class="button is-info"
+ onClick={onNext}
+ disabled={!!error}
+ >
+ 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
new file mode 100644
index 000000000..c9bc127f7
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -0,0 +1,84 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+
+export default {
+ title: "Auth method: Question setup",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "question";
+
+export const Empty = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithOneExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions:
+ "Is integer factorization polynomial? (non-quantum computer)",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithMoreExamples = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Does P equal NP?",
+ remove: () => null,
+ },
+ {
+ challenge: "asd",
+ type,
+ instructions:
+ "Are continuous groups automatically differential groups?",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
new file mode 100644
index 000000000..7dc6fcc0c
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
@@ -0,0 +1,127 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { AnastasisClientFrame } from "../index.js";
+import { AuthMethodSetupProps } from "./index.js";
+
+export function AuthMethodQuestionSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
+ const [questionText, setQuestionText] = useState("");
+ const [answerText, setAnswerText] = useState("");
+ const addQuestionAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "question",
+ instructions: questionText,
+ challenge: encodeCrock(stringToBytes(answerText)),
+ },
+ });
+
+ const errors = !questionText
+ ? "Add your security question"
+ : !answerText
+ ? "Add the answer to your question"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addQuestionAuth();
+ }
+ return (
+ <AnastasisClientFrame hideNav title="Add Security Question">
+ <div>
+ <p>
+ For security question authentication, you need to provide a question
+ and its answer. When recovering your secret, you will be shown the
+ question and you will need to type the answer exactly as you typed it
+ here.
+ </p>
+ <p class="notification is-warning">
+ Note that the answer is case-sensitive and must be entered in exactly
+ the same way (punctuation, spaces) during recovery.
+ </p>
+ <div>
+ <TextInput
+ label="Security question"
+ grabFocus
+ onConfirm={goNextIfNoErrors}
+ placeholder="Your question"
+ 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>
+ <span data-tooltip={errors}>
+ <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>
+ )}
+ </div>
+ </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..dbb17ddab
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
@@ -0,0 +1,239 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ChallengeFeedbackStatus,
+ ReducerState,
+} from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { AmountString } from "@gnu-taler/taler-util";
+
+export default {
+ title: "Auth method: Question solve",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "question";
+
+export const WithoutFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "uuid-1",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+);
+
+const recovery_information = {
+ challenges: [
+ {
+ instructions: "does P equal NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+};
+
+export const CodeInFileFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.CodeInFile,
+ filename: "asd",
+ display_hint: "hint",
+ },
+ },
+ } as ReducerState,
+);
+
+export const CodeSentFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.CodeSent,
+ address_hint: "asdasd",
+ display_hint: "qweqweqw",
+ },
+ },
+ } as ReducerState,
+);
+
+export const SolvedFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Solved,
+ },
+ },
+ } as ReducerState,
+);
+
+export const ServerFailureFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.ServerFailure,
+ http_status: 500,
+ },
+ },
+ } as ReducerState,
+);
+
+export const TruthUnknownFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.TruthUnknown,
+ },
+ },
+ } as ReducerState,
+);
+
+export const TalerPaymentFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.TalerPayment,
+ payment_secret: "secret",
+ provider: "asdasdas",
+ taler_pay_uri: "taler://pay/...",
+ },
+ },
+ } as ReducerState,
+);
+
+export const UnsupportedFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Unsupported,
+ unsupported_method: "method",
+ },
+ },
+ } as ReducerState,
+);
+
+export const RateLimitExceededFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.RateLimitExceeded,
+ },
+ },
+ } as ReducerState,
+);
+
+export const IbanInstructionsFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.IbanInstructions,
+ challenge_amount: "EUR:1" as AmountString,
+ target_iban: "DE12345789000",
+ target_business_name: "Data Loss Incorporated",
+ wire_transfer_subject: "Anastasis 987654321",
+ answer_code: 987654321,
+ },
+ },
+ } as ReducerState,
+);
+
+export const IncorrectAnswerFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.IncorrectAnswer,
+ },
+ },
+ } 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..abb200eb1
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx
@@ -0,0 +1,128 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ChallengeInfo } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton.js";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { AnastasisClientFrame } from "../index.js";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
+import { shouldHideConfirm } from "./helpers.js";
+import { AuthMethodSolveProps } from "./index.js";
+
+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_type !== "recovery") {
+ 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();
+ }
+
+ 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(feedback) && (
+ <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
new file mode 100644
index 000000000..fbf345779
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+
+export default {
+ title: "Auth method: SMS setup",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "sms";
+
+export const Empty = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithOneExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-1234-2345",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
+
+export const WithMoreExamples = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-1234-2345",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-5555-2345",
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
new file mode 100644
index 000000000..87064237c
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
@@ -0,0 +1,127 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { PhoneNumberInput } from "../../../components/fields/NumberInput.js";
+import { AnastasisClientFrame } from "../index.js";
+import { AuthMethodSetupProps } from "./index.js";
+
+const REGEX_JUST_NUMBERS = /^\+[0-9 ]*$/;
+
+function isJustNumbers(str: string): boolean {
+ return REGEX_JUST_NUMBERS.test(str);
+}
+
+export function AuthMethodSmsSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
+ const [mobileNumber, setMobileNumber] = useState("+");
+ const addSmsAuth = (): void => {
+ addAuthMethod({
+ authentication_method: {
+ type: "sms",
+ instructions: `SMS to ${mobileNumber}`,
+ challenge: encodeCrock(stringToBytes(mobileNumber)),
+ },
+ });
+ };
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+ const errors = !mobileNumber
+ ? "Add a mobile number"
+ : !mobileNumber.startsWith("+")
+ ? "Mobile number should start with '+'"
+ : !isJustNumbers(mobileNumber)
+ ? "Mobile number can't have other than numbers"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addSmsAuth();
+ }
+ return (
+ <AnastasisClientFrame hideNav title="Add SMS authentication">
+ <div>
+ <p>
+ For SMS authentication, you need to provide a mobile number. When
+ recovering your secret, you will be asked to enter the code you
+ receive via SMS.
+ </p>
+ <div class="container">
+ <PhoneNumberInput
+ label="Mobile number"
+ placeholder="Your mobile number"
+ onConfirm={goNextIfNoErrors}
+ error={errors}
+ grabFocus
+ bind={[mobileNumber, setMobileNumber]}
+ />
+ <div>
+ Enter mobile number including +CC international dialing prefix.
+ </div>
+ </div>
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your mobile numbers:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginTop: "auto", marginBottom: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <span data-tooltip={errors}>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addSmsAuth}
+ >
+ Add
+ </button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
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..8e3fb1a16
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+export default {
+ title: "Auth method: SMS solve",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "sms";
+
+export const WithoutFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "AHCC4ZJ3Z1AF8TWBKGVGEKCQ3R7HXHJ51MJ45NHNZMHYZTKJ9NW0",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "SMS to +54 11 2233 4455",
+ type: "question",
+ uuid: "AHCC4ZJ3Z1AF8TWBKGVGEKCQ3R7HXHJ51MJ45NHNZMHYZTKJ9NW0",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid:
+ "AHCC4ZJ3Z1AF8TWBKGVGEKCQ3R7HXHJ51MJ45NHNZMHYZTKJ9NW0",
+ } as ReducerState,
+);
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..58bb53c4f
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
@@ -0,0 +1,191 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ChallengeInfo } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton.js";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { useTranslationContext } from "../../../context/translation.js";
+import { AnastasisClientFrame } from "../index.js";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
+import { shouldHideConfirm } from "./helpers.js";
+import { AuthMethodSolveProps } from "./index.js";
+
+export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, _setAnswer] = useState("A-");
+
+ function setAnswer(str: string): void {
+ //A-12345-678-1234-5678
+ const unformatted = str
+ .replace(/^A-/, "")
+ .replace(/-/g, "")
+ .toLocaleUpperCase();
+
+ let result = `A-${unformatted.substring(0, 5)}`;
+ if (unformatted.length > 5) {
+ result += `-${unformatted.substring(5, 8)}`;
+ }
+ if (unformatted.length > 8) {
+ result += `-${unformatted.substring(8, 12)}`;
+ }
+ if (unformatted.length > 12) {
+ result += `-${unformatted.substring(12)}`;
+ }
+
+ _setAnswer(result);
+ }
+ const { i18n } = useTranslationContext();
+
+ 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_type !== "recovery") {
+ 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: `A-${answer.replace(/^A-/, "").replace(/-/g, "").trim()}`,
+ });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const error =
+ answer.length > 21
+ ? i18n.str`The answer should not be greater than 21 characters.`
+ : undefined;
+
+ return (
+ <AnastasisClientFrame hideNav title="SMS Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ An sms has been sent to &quot;<b>{selectedChallenge.instructions}</b>
+ &quot;. The message has and identification code and recovery code that
+ starts with &quot;
+ <b>A-</b>&quot;. Wait the message to arrive and the enter the recovery
+ code below.
+ </p>
+ {!expanded ? (
+ <p>
+ The identification code in the SMS should start with &quot;
+ {selectedUuid.substring(0, 10)}&quot;
+ <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 &quot;{selectedUuid}&quot;
+ <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]}
+ error={error}
+ placeholder="A-12345-678-1234-5678"
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm(feedback) && (
+ <AsyncButton
+ class="button is-info"
+ onClick={onNext}
+ disabled={!!error}
+ >
+ 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
new file mode 100644
index 000000000..ee66fcee1
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+
+export default {
+ title: "Auth method: Totp setup",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "totp";
+
+export const Empty = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [],
+ },
+ reducerStatesExample.authEditing,
+);
+export const WithOneExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ remove: () => null,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
+export const WithMoreExample = tests.createExample(
+ TestedComponent[type].setup,
+ {
+ 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,
+ },
+ ],
+ },
+ reducerStatesExample.authEditing,
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
new file mode 100644
index 000000000..acdcef3ac
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
@@ -0,0 +1,141 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { encodeCrock } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useMemo, useState } from "preact/hooks";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { QR } from "../../../components/QR.js";
+import { AnastasisClientFrame } from "../index.js";
+import { AuthMethodSetupProps } from "./index.js";
+import { base32enc, computeTOTPandCheck } from "./totp.js";
+
+/**
+ * This is hard-coded in the protocol for TOTP auth.
+ */
+const ANASTASIS_TOTP_DIGITS = 8;
+
+export function AuthMethodTotpSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
+ const [name, setName] = useState("anastasis");
+ const [test, setTest] = useState("");
+ const secretKey = useMemo(() => {
+ const array = new Uint8Array(32);
+ if (typeof window === "undefined") return array;
+ return window.crypto.getRandomValues(array);
+ }, []);
+
+ const secret32 = base32enc(secretKey);
+ const totpURL = `otpauth://totp/${name}?digits=${ANASTASIS_TOTP_DIGITS}&secret=${secret32}`;
+
+ const addTotpAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "totp",
+ instructions: `Enter ${ANASTASIS_TOTP_DIGITS} digits code for "${name}"`,
+ challenge: encodeCrock(secretKey),
+ },
+ });
+
+ const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
+
+ const errors = !name
+ ? "The TOTP name is missing"
+ : !testCodeMatches
+ ? "The test code doesn't match"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addTotpAuth();
+ }
+ return (
+ <AnastasisClientFrame hideNav title="Add TOTP authentication">
+ <p>
+ For Time-based One-Time Password (TOTP) authentication, you need to set
+ a name for the TOTP secret. Then, you must scan the generated QR code
+ with your TOTP App to import the TOTP secret into your TOTP App.
+ </p>
+ <div class="block">
+ <TextInput label="TOTP Name" grabFocus bind={[name, setName]} />
+ </div>
+ <div style={{ height: 300 }}>
+ <QR text={totpURL} />
+ </div>
+ <p>
+ Confirm that your TOTP App works by entering the current 8-digit TOTP
+ code here:
+ </p>
+ <TextInput
+ label="Test code"
+ onConfirm={goNextIfNoErrors}
+ bind={[test, setTest]}
+ />
+ <div>
+ We note that Google&apos;s implementation of TOTP is incomplete and will
+ not work. We recommend using FreeOTP+.
+ </div>
+
+ {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>
+ <span data-tooltip={errors}>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addTotpAuth}
+ >
+ Add
+ </button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
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..c120aaadc
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ReducerState } from "@gnu-taler/anastasis-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { KnownAuthMethods, authMethods as TestedComponent } from "./index.js";
+
+export default {
+ title: "Auth method: Totp solve",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "totp";
+
+export const WithoutFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {
+ id: "uuid-1",
+ },
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+);
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..0ce0e1016
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx
@@ -0,0 +1,125 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ChallengeInfo } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton.js";
+import { TextInput } from "../../../components/fields/TextInput.js";
+import { useAnastasisContext } from "../../../context/anastasis.js";
+import { AnastasisClientFrame } from "../index.js";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
+import { shouldHideConfirm } from "./helpers.js";
+import { AuthMethodSolveProps } from "./index.js";
+
+export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode {
+ const [answerCode, setAnswerCode] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (reducer.currentReducerState?.reducer_type !== "recovery") {
+ 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 feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", {
+ answer: answerCode,
+ });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ return (
+ <AnastasisClientFrame hideNav title="TOTP Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>enter the totp solution</p>
+ <TextInput
+ label="Answer"
+ onConfirm={onNext}
+ grabFocus
+ bind={[answerCode, setAnswerCode]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm(feedback) && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/helpers.ts b/packages/anastasis-webui/src/pages/home/authMethod/helpers.ts
new file mode 100644
index 000000000..b6d9f5bbd
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/helpers.ts
@@ -0,0 +1,27 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ ChallengeFeedback,
+ ChallengeFeedbackStatus,
+} from "@gnu-taler/anastasis-core";
+
+export function shouldHideConfirm(feedback: ChallengeFeedback): boolean {
+ return (
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
new file mode 100644
index 000000000..9f7f4a197
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
@@ -0,0 +1,109 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AuthMethod } from "@gnu-taler/anastasis-core";
+import { h, VNode } from "preact";
+import postalIcon from "../../../assets/icons/auth_method/postal.svg";
+import questionIcon from "../../../assets/icons/auth_method/question.svg";
+import smsIcon from "../../../assets/icons/auth_method/sms.svg";
+import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup.js";
+import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve.js";
+import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup.js";
+import { AuthMethodIbanSolve as IbanSolve } from "./AuthMethodIbanSolve.js";
+import { AuthMethodPostSetup as PostalSetup } from "./AuthMethodPostSetup.js";
+import { AuthMethodPostSolve as PostalSolve } from "./AuthMethodPostSolve.js";
+import { AuthMethodQuestionSetup as QuestionSetup } from "./AuthMethodQuestionSetup.js";
+import { AuthMethodQuestionSolve as QuestionSolve } from "./AuthMethodQuestionSolve.js";
+import { AuthMethodSmsSetup as SmsSetup } from "./AuthMethodSmsSetup.js";
+import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve.js";
+import { AuthMethodTotpSetup as TotpSetup } from "./AuthMethodTotpSetup.js";
+import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve.js";
+
+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;
+ setup: (props: AuthMethodSetupProps) => VNode;
+ solve: (props: AuthMethodSolveProps) => VNode;
+ skip?: boolean;
+}
+
+const ALL_METHODS = [
+ "sms",
+ "email",
+ "post",
+ "question",
+ "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;
+};
+
+export const authMethods: KnowMethodConfig = {
+ question: {
+ icon: <img src={questionIcon} />,
+ label: "Question",
+ setup: QuestionSetup,
+ solve: QuestionSolve,
+ },
+ sms: {
+ icon: <img src={smsIcon} />,
+ label: "SMS",
+ setup: SmsSetup,
+ solve: SmsSolve,
+ },
+ email: {
+ icon: <i class="mdi mdi-email" />,
+ label: "Email",
+ setup: EmailSetup,
+ solve: EmailSolve,
+ },
+ iban: {
+ icon: <i class="mdi mdi-bank" />,
+ label: "IBAN",
+ setup: IbanSetup,
+ solve: IbanSolve,
+ },
+ post: {
+ icon: <img src={postalIcon} />,
+ label: "Physical mail",
+ setup: PostalSetup,
+ solve: PostalSolve,
+ },
+ totp: {
+ icon: <i class="mdi mdi-devices" />,
+ label: "TOTP",
+ setup: TotpSetup,
+ solve: TotpSolve,
+ },
+};
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
new file mode 100644
index 000000000..ff8027ced
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
@@ -0,0 +1,79 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+//@ts-ignore
+import jssha from "jssha";
+
+const SEARCH_RANGE = 16;
+const timeStep = 30;
+
+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" },
+ });
+ hmacSha.update(movingFactor);
+ const hmac_text = hmacSha.getHMAC("UINT8ARRAY");
+
+ 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);
+
+ if (otp == code) return true;
+ }
+ return false;
+}
+
+const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split("");
+export function base32enc(buffer: Uint8Array): string {
+ let rpos = 0;
+ let bits = 0;
+ let vbit = 0;
+
+ 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;
+ vbit = 5;
+ }
+ result += encTable__[(bits >> (vbit - 5)) & 31];
+ vbit -= 5;
+ }
+ return result;
+}
+
+// const array = new Uint8Array(256)
+// const secretKey = window.crypto.getRandomValues(array)
+// console.log(base32enc(secretKey))
diff --git a/packages/anastasis-webui/src/pages/home/index.stories.tsx b/packages/anastasis-webui/src/pages/home/index.stories.tsx
new file mode 100644
index 000000000..b4525b423
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/index.stories.tsx
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export * as AddingProviderScreen from "./AddingProviderScreen/stories.js";
+export * as AttributeEntryScreen from "./AttributeEntryScreen.stories.js";
+
+export * as AuthenticationEditorScreen from "./AuthenticationEditorScreen.stories.js";
+export * as authMethod_AuthMethodEmailSetup from "./authMethod/AuthMethodEmailSetup.stories.js";
+export * as authMethod_AuthMethodEmailSolve from "./authMethod/AuthMethodEmailSolve.stories.js";
+export * as authMethod_AuthMethodIbanSetup from "./authMethod/AuthMethodIbanSetup.stories.js";
+export * as authMethod_AuthMethodIbanSolve from "./authMethod/AuthMethodIbanSolve.stories.js";
+export * as authMethod_AuthMethodPostSetup from "./authMethod/AuthMethodPostSetup.stories.js";
+export * as authMethod_AuthMethodPostSolve from "./authMethod/AuthMethodPostSolve.stories.js";
+export * as authMethod_AuthMethodQuestionSetup from "./authMethod/AuthMethodQuestionSetup.stories.js";
+export * as authMethod_AuthMethodQuestionSolve from "./authMethod/AuthMethodQuestionSolve.stories.js";
+export * as authMethod_AuthMethodSmsSetup from "./authMethod/AuthMethodSmsSetup.stories.js";
+export * as authMethod_AuthMethodSmsSolve from "./authMethod/AuthMethodSmsSolve.stories.js";
+export * as authMethod_AuthMethodTotpSetup from "./authMethod/AuthMethodTotpSetup.stories.js";
+export * as authMethod_AuthMethodTotpSolve from "./authMethod/AuthMethodTotpSolve.stories.js";
+
+export * as BackupFinishedScreen from "./BackupFinishedScreen.stories.js";
+export * as ChallengeOverviewScreen from "./ChallengeOverviewScreen.stories.js";
+export * as ChallengePayingScreen from "./ChallengePayingScreen.stories.js";
+export * as ContinentSelectionScreen from "./ContinentSelectionScreen.stories.js";
+export * as EditPoliciesScreen from "./EditPoliciesScreen.stories.js";
+export * as PoliciesPayingScreen from "./PoliciesPayingScreen.stories.js";
+export * as RecoveryFinishedScreen from "./RecoveryFinishedScreen.stories.js";
+
+export * as ReviewPoliciesScreen from "./ReviewPoliciesScreen.stories.js";
+export * as SecretEditorScreen from "./SecretEditorScreen.stories.js";
+export * as SecretSelectionScreen from "./SecretSelectionScreen.stories.js";
+export * as SolveScreen from "./SolveScreen.stories.js";
+export * as StartScreen from "./StartScreen.stories.js";
+export * as TruthsPayingScreen from "./TruthsPayingScreen.stories.js";
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 4cec47ec8..c665144a4 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -1,43 +1,55 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core";
import {
- BackupStates,
- RecoveryStates,
- ReducerStateBackup,
- ReducerStateRecovery
-} from "anastasis-core";
-import {
- ComponentChildren, Fragment,
+ ComponentChildren,
+ Fragment,
FunctionalComponent,
h,
- VNode
+ VNode,
} from "preact";
+import { useCallback, useEffect, useErrorBoundary } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton.js";
+import { Menu } from "../../components/menu/index.js";
+import { Notifications } from "../../components/Notifications.js";
import {
- useErrorBoundary,
- useLayoutEffect,
- useRef
-} from "preact/hooks";
-import { Menu } from "../../components/menu";
-import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
+ AnastasisProvider,
+ useAnastasisContext,
+} from "../../context/anastasis.js";
import {
AnastasisReducerApi,
- useAnastasisReducer
-} from "../../hooks/use-anastasis-reducer";
-import { AttributeEntryScreen } from "./AttributeEntryScreen";
-import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
-import { BackupFinishedScreen } from "./BackupFinishedScreen";
-import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
-import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
-import { CountrySelectionScreen } from "./CountrySelectionScreen";
-import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
-import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
-import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
-import { SecretEditorScreen } from "./SecretEditorScreen";
-import { SecretSelectionScreen } from "./SecretSelectionScreen";
-import { SolveScreen } from "./SolveScreen";
-import { StartScreen } from "./StartScreen";
-import { TruthsPayingScreen } from "./TruthsPayingScreen";
+ useAnastasisReducer,
+} from "../../hooks/use-anastasis-reducer.js";
+import { AttributeEntryScreen } from "./AttributeEntryScreen.js";
+import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen.js";
+import { BackupFinishedScreen } from "./BackupFinishedScreen.js";
+import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen.js";
+import { ChallengePayingScreen } from "./ChallengePayingScreen.js";
+import { ContinentSelectionScreen } from "./ContinentSelectionScreen.js";
+import { PoliciesPayingScreen } from "./PoliciesPayingScreen.js";
+import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen.js";
+import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen.js";
+import { SecretEditorScreen } from "./SecretEditorScreen.js";
+import { SecretSelectionScreen } from "./SecretSelectionScreen.js";
+import { SolveScreen } from "./SolveScreen.js";
+import { StartScreen } from "./StartScreen.js";
+import { TruthsPayingScreen } from "./TruthsPayingScreen.js";
function isBackup(reducer: AnastasisReducerApi): boolean {
- return !!reducer.currentReducerState?.backup_state;
+ return reducer.currentReducerState?.reducer_type === "backup";
}
export function withProcessLabel(
@@ -51,7 +63,11 @@ export function withProcessLabel(
}
interface AnastasisClientFrameProps {
- onNext?(): void;
+ onNext?(): Promise<void>;
+ /**
+ * Override for the "back" functionality.
+ */
+ onBack?(): Promise<void>;
title: string;
children: ComponentChildren;
/**
@@ -61,7 +77,7 @@ interface AnastasisClientFrameProps {
/**
* Hide only the "next" button.
*/
- hideNext?: boolean;
+ hideNext?: string;
}
function ErrorBoundary(props: {
@@ -69,7 +85,7 @@ function ErrorBoundary(props: {
children: ComponentChildren;
}): VNode {
const [error, resetError] = useErrorBoundary((error) =>
- console.log("got error", error),
+ console.log("ErrorBoundary got error", error),
);
if (error) {
return (
@@ -91,39 +107,100 @@ function ErrorBoundary(props: {
return <div>{props.children}</div>;
}
+let currentHistoryId = 0;
+
export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
const reducer = useAnastasisContext();
- if (!reducer) {
- return <p>Fatal: Reducer must be in context.</p>;
- }
- const next = (): void => {
+
+ const doBack = async (): Promise<void> => {
+ if (props.onBack) {
+ await props.onBack();
+ } else {
+ if (!reducer) return;
+ await reducer.back();
+ }
+ };
+ const doNext = async (fromPopstate?: boolean): Promise<void> => {
+ if (!fromPopstate) {
+ try {
+ const nextId: number =
+ (history.state && typeof history.state.id === "number"
+ ? history.state.id
+ : 0) + 1;
+
+ currentHistoryId = nextId;
+
+ history.pushState({ id: nextId }, "unused", `#${nextId}`);
+ } catch (e) {
+ console.log("ERROR doNext ", e);
+ }
+ }
+
if (props.onNext) {
- props.onNext();
+ await props.onNext();
} else {
- reducer.transition("next", {});
+ if (!reducer) return;
+ await reducer.transition("next", {});
}
};
const handleKeyPress = (
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
): void => {
- console.log("Got key press", e.key);
+ // console.log("Got key press", e.key);
// FIXME: By default, "next" action should be executed here
};
+
+ const browserOnBackButton = useCallback(async (ev: PopStateEvent) => {
+ //check if we are going back or forward
+ if (!ev.state || ev.state.id === 0 || ev.state.id < currentHistoryId) {
+ await doBack();
+ } else {
+ await doNext(true);
+ }
+
+ // reducer
+ return false;
+ }, []);
+ useEffect(() => {
+ window.addEventListener("popstate", browserOnBackButton);
+
+ return () => {
+ window.removeEventListener("popstate", browserOnBackButton);
+ };
+ }, []);
+ // if (!reducer) {
+ // return <p>Fatal: Reducer must be in context.</p>;
+ // }
+
return (
<Fragment>
- <Menu title="Anastasis" />
- <div>
- <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
- <h1>{props.title}</h1>
- <ErrorBanner />
+ <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
+ <h1 class="title">{props.title}</h1>
+ <ErrorBanner />
+ <section class="section is-main-section">
{props.children}
{!props.hideNav ? (
- <div>
- <button onClick={() => reducer.back()}>Back</button>
- {!props.hideNext ? <button onClick={next}>Next</button> : null}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => doBack()}>
+ Back
+ </button>
+ <AsyncButton
+ class="button is-info"
+ data-tooltip={props.hideNext}
+ onClick={() => doNext()}
+ disabled={props.hideNext !== undefined}
+ >
+ Next
+ </AsyncButton>
</div>
) : null}
- </div>
+ </section>
</div>
</Fragment>
);
@@ -134,14 +211,15 @@ const AnastasisClient: FunctionalComponent = () => {
return (
<AnastasisProvider value={reducer}>
<ErrorBoundary reducer={reducer}>
+ <Menu title="Anastasis" />
<AnastasisClientImpl />
</ErrorBoundary>
</AnastasisProvider>
);
};
-const AnastasisClientImpl: FunctionalComponent = () => {
- const reducer = useAnastasisContext()
+function AnastasisClientImpl(): VNode {
+ const reducer = useAnastasisContext();
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
@@ -149,115 +227,113 @@ const AnastasisClientImpl: FunctionalComponent = () => {
if (!state) {
return <StartScreen />;
}
- console.log("state", reducer.currentReducerState);
+
+ // FIXME: Use switch statements here!
if (
- state.backup_state === BackupStates.ContinentSelecting ||
- state.recovery_state === RecoveryStates.ContinentSelecting
+ (state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.ContinentSelecting) ||
+ (state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ContinentSelecting) ||
+ (state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.CountrySelecting) ||
+ (state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.CountrySelecting)
) {
- return (
- <ContinentSelectionScreen />
- );
+ return <ContinentSelectionScreen />;
}
if (
- state.backup_state === BackupStates.CountrySelecting ||
- state.recovery_state === RecoveryStates.CountrySelecting
+ (state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.UserAttributesCollecting) ||
+ (state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.UserAttributesCollecting)
) {
- return (
- <CountrySelectionScreen />
- );
+ return <AttributeEntryScreen />;
}
if (
- state.backup_state === BackupStates.UserAttributesCollecting ||
- state.recovery_state === RecoveryStates.UserAttributesCollecting
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.AuthenticationsEditing
) {
- return (
- <AttributeEntryScreen />
- );
+ return <AuthenticationEditorScreen />;
}
- if (state.backup_state === BackupStates.AuthenticationsEditing) {
- return (
- <AuthenticationEditorScreen />
- );
- }
- if (state.backup_state === BackupStates.PoliciesReviewing) {
- return (
- <ReviewPoliciesScreen />
- );
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.PoliciesReviewing
+ ) {
+ return <ReviewPoliciesScreen />;
}
- if (state.backup_state === BackupStates.SecretEditing) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.SecretEditing
+ ) {
return <SecretEditorScreen />;
}
- if (state.backup_state === BackupStates.BackupFinished) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.BackupFinished
+ ) {
return <BackupFinishedScreen />;
}
- if (state.backup_state === BackupStates.TruthsPaying) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.TruthsPaying
+ ) {
return <TruthsPayingScreen />;
}
- if (state.backup_state === BackupStates.PoliciesPaying) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.PoliciesPaying
+ ) {
return <PoliciesPayingScreen />;
}
- if (state.recovery_state === RecoveryStates.SecretSelecting) {
- return (
- <SecretSelectionScreen />
- );
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.SecretSelecting
+ ) {
+ return <SecretSelectionScreen />;
}
- if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
- return (
- <ChallengeOverviewScreen />
- );
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ChallengeSelecting
+ ) {
+ return <ChallengeOverviewScreen />;
}
- if (state.recovery_state === RecoveryStates.ChallengeSolving) {
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ChallengeSolving
+ ) {
return <SolveScreen />;
}
- if (state.recovery_state === RecoveryStates.RecoveryFinished) {
- return (
- <RecoveryFinishedScreen />
- );
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.RecoveryFinished
+ ) {
+ return <RecoveryFinishedScreen />;
+ }
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ChallengePaying
+ ) {
+ return <ChallengePayingScreen />;
}
-
console.log("unknown state", reducer.currentReducerState);
return (
<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>
);
-};
-
-interface LabeledInputProps {
- label: string;
- grabFocus?: boolean;
- bind: [string, (x: string) => void];
-}
-
-export function LabeledInput(props: LabeledInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) {
- inputRef.current?.focus();
- }
- }, [props.grabFocus]);
- return (
- <label>
- {props.label}
- <input
- value={props.bind[0]}
- onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)}
- ref={inputRef}
- style={{ display: "block" }}
- />
- </label>
- );
}
/**
@@ -267,12 +343,16 @@ function ErrorBanner(): VNode | null {
const reducer = useAnastasisContext();
if (!reducer || !reducer.currentError) return null;
return (
- <div id="error">
- <p>Error: {JSON.stringify(reducer.currentError)}</p>
- <button onClick={() => reducer.dismissError()}>
- Dismiss Error
- </button>
- </div>
+ <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/home/style.css b/packages/anastasis-webui/src/pages/home/style.css
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/anastasis-webui/src/pages/home/style.css
+++ /dev/null
diff --git a/packages/anastasis-webui/src/pages/notfound/index.tsx b/packages/anastasis-webui/src/pages/notfound/index.tsx
deleted file mode 100644
index 4e74d1d9f..000000000
--- a/packages/anastasis-webui/src/pages/notfound/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-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>
- );
-};
-
-export default Notfound;
diff --git a/packages/anastasis-webui/src/pages/notfound/style.css b/packages/anastasis-webui/src/pages/notfound/style.css
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/anastasis-webui/src/pages/notfound/style.css
+++ /dev/null
diff --git a/packages/anastasis-webui/src/pages/profile/index.tsx b/packages/anastasis-webui/src/pages/profile/index.tsx
deleted file mode 100644
index 859a83ed4..000000000
--- a/packages/anastasis-webui/src/pages/profile/index.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { FunctionalComponent, h } from 'preact';
-import { useEffect, useState } from 'preact/hooks';
-
-interface Props {
- 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);
- };
-
- 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>
-
- <p>
- <button onClick={increment}>Click Me</button> Clicked {count}{' '}
- times.
- </p>
- </div>
- );
-};
-
-export default Profile;
diff --git a/packages/anastasis-webui/src/pages/profile/style.css b/packages/anastasis-webui/src/pages/profile/style.css
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/anastasis-webui/src/pages/profile/style.css
+++ /dev/null