summaryrefslogtreecommitdiff
path: root/packages/anastasis-webui
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-06-11 19:10:26 -0300
committerSebastian <sebasjm@gmail.com>2022-06-11 19:10:26 -0300
commit6d06b52605005f4d25381fc73383c3c9e48f20f8 (patch)
treed1e01d71c538602a92848595f92d24bf214c264f /packages/anastasis-webui
parent716da3246b7d544fc81265d1942ae64067ecd8b7 (diff)
downloadwallet-core-6d06b52605005f4d25381fc73383c3c9e48f20f8.tar.gz
wallet-core-6d06b52605005f4d25381fc73383c3c9e48f20f8.tar.bz2
wallet-core-6d06b52605005f4d25381fc73383c3c9e48f20f8.zip
add testing to web components
Diffstat (limited to 'packages/anastasis-webui')
-rwxr-xr-xpackages/anastasis-webui/clean_and_build.sh2
-rw-r--r--packages/anastasis-webui/package.json4
-rw-r--r--packages/anastasis-webui/src/components/InvalidState.tsx21
-rw-r--r--packages/anastasis-webui/src/components/NoReducer.tsx21
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx61
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx367
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts102
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts147
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx89
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts42
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx304
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx4
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx10
-rw-r--r--packages/anastasis-webui/src/pages/home/index.storiesNo.tsx2
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx6
-rw-r--r--packages/anastasis-webui/src/utils/index.tsx36
16 files changed, 779 insertions, 439 deletions
diff --git a/packages/anastasis-webui/clean_and_build.sh b/packages/anastasis-webui/clean_and_build.sh
index 21107a905..c0e93e47d 100755
--- a/packages/anastasis-webui/clean_and_build.sh
+++ b/packages/anastasis-webui/clean_and_build.sh
@@ -37,6 +37,8 @@ echo compile
build_css &
build_js src/main.ts &
build_js src/main.test.ts &
+for file in $(find src/ -name test.ts); do build_js $file; done &
+wait -n
wait -n
wait -n
wait -n
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index 7c57160ce..551ae54bc 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -9,12 +9,14 @@
"dev": "./dev.mjs",
"prepare": "pnpm compile",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
- "test": "mocha --enable-source-maps 'dist/**/*.test.js'",
+ "test": "mocha --enable-source-maps 'dist/**/*test.js'",
"pretty": "prettier --write src"
},
"dependencies": {
"@gnu-taler/anastasis-core": "workspace:*",
"@gnu-taler/taler-util": "workspace:*",
+ "@types/chai": "^4.3.0",
+ "chai": "^4.3.6",
"date-fns": "2.28.0",
"jed": "1.1.1",
"preact": "^10.5.15",
diff --git a/packages/anastasis-webui/src/components/InvalidState.tsx b/packages/anastasis-webui/src/components/InvalidState.tsx
new file mode 100644
index 000000000..8e2edde5e
--- /dev/null
+++ b/packages/anastasis-webui/src/components/InvalidState.tsx
@@ -0,0 +1,21 @@
+/*
+ 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";
+
+export default function InvalidState(): VNode {
+ return <div>invalid state</div>;
+}
diff --git a/packages/anastasis-webui/src/components/NoReducer.tsx b/packages/anastasis-webui/src/components/NoReducer.tsx
new file mode 100644
index 000000000..550ddccaa
--- /dev/null
+++ b/packages/anastasis-webui/src/components/NoReducer.tsx
@@ -0,0 +1,21 @@
+/*
+ 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";
+
+export default function NoReducer(): VNode {
+ return <div>no reducer</div>;
+}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
deleted file mode 100644
index ce860ba29..000000000
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- 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 { createExample, reducerStatesExample } from "../../utils/index.js";
-import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen.js";
-
-export default {
- title: "Pages/ManageProvider",
- component: TestedComponent,
- args: {
- order: 1,
- },
- argTypes: {
- onUpdate: { action: "onUpdate" },
- onBack: { action: "onBack" },
- },
-};
-
-export const NewProvider = createExample(TestedComponent, {
- ...reducerStatesExample.authEditing,
-} as ReducerState);
-
-export const NewProviderWithoutProviderList = createExample(TestedComponent, {
- ...reducerStatesExample.authEditing,
- authentication_providers: {},
-} as ReducerState);
-
-export const NewSmsProvider = createExample(
- TestedComponent,
- {
- ...reducerStatesExample.authEditing,
- } as ReducerState,
- { providerType: "sms" },
-);
-
-export const NewIBANProvider = createExample(
- TestedComponent,
- {
- ...reducerStatesExample.authEditing,
- } as ReducerState,
- { providerType: "iban" },
-);
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
deleted file mode 100644
index 6aeee9e7a..000000000
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- 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,
- AuthenticationProviderStatusError,
- AuthenticationProviderStatusOk,
-} from "@gnu-taler/anastasis-core";
-import { h, VNode } from "preact";
-import { useEffect, useRef, useState } from "preact/hooks";
-import { TextInput } from "../../components/fields/TextInput.js";
-import { useAnastasisContext } from "../../context/anastasis.js";
-import { authMethods, KnownAuthMethods } from "./authMethod/index.js";
-import { AnastasisClientFrame } from "./index.js";
-
-interface Props {
- providerType?: KnownAuthMethods;
- onCancel: () => void;
-}
-
-async function testProvider(
- url: string,
- expectedMethodType?: string,
-): Promise<void> {
- try {
- const response = await fetch(new URL("config", url).href);
- const json = await response.json().catch((d) => ({}));
- if (!("methods" in json) || !Array.isArray(json.methods)) {
- throw Error(
- "This provider doesn't have authentication method. Check the provider URL",
- );
- }
- if (!expectedMethodType) {
- return;
- }
- let found = false;
- for (let i = 0; i < json.methods.length && !found; i++) {
- found = json.methods[i].type === expectedMethodType;
- }
- if (!found) {
- throw Error(
- `This provider does not support authentication method ${expectedMethodType}`,
- );
- }
- return;
- } catch (e) {
- console.log("error", e);
- const error =
- e instanceof Error
- ? Error(
- `There was an error testing this provider, try another one. ${e.message}`,
- )
- : Error(`There was an error testing this provider, try another one.`);
- throw error;
- }
-}
-
-export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
- const reducer = useAnastasisContext();
-
- const [providerURL, setProviderURL] = useState("");
-
- const [error, setError] = useState<string | undefined>();
- const [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;
- }
- const others = prev[p.status] ? prev[p.status] : [];
- others.push({ ...p, url });
- return {
- ...prev,
- [p.status]: others,
- };
- },
- {} as Record<
- AuthenticationProviderStatus["status"],
- (AuthenticationProviderStatus & { url: string })[]
- >,
- );
- const authProviders = authProvidersByStatus["ok"].map((p) => p.url);
-
- console.log("rodos", allAuthProviders);
- //FIXME: move this timeout logic into a hook
- const timeout = useRef<number | undefined>(undefined);
- useEffect(() => {
- if (timeout) window.clearTimeout(timeout.current);
- timeout.current = window.setTimeout(async () => {
- const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
- if (!providerURL || authProviders.includes(url)) return;
- try {
- setTesting(true);
- await testProvider(url, providerType);
- setError("");
- } catch (e) {
- if (e instanceof Error) setError(e.message);
- }
- setTesting(false);
- }, 200);
- }, [providerURL, reducer]);
-
- async function addProvider(provider_url: string): Promise<void> {
- await reducer?.transition("add_provider", { provider_url });
- onCancel();
- }
- function deleteProvider(provider_url: string): void {
- reducer?.transition("delete_provider", { provider_url });
- }
-
- if (!reducer) {
- return <div>no reducer in context</div>;
- }
-
- if (
- !reducer.currentReducerState ||
- !("authentication_providers" in reducer.currentReducerState)
- ) {
- return <div>invalid state</div>;
- }
-
- let errors = !providerURL ? "Add provider URL" : undefined;
- let url: string | undefined;
- try {
- url = new URL("", providerURL).href;
- } catch {
- errors = "Check the URL";
- }
- if (!!error && !errors) {
- errors = error;
- }
- if (!errors && authProviders.includes(url!)) {
- errors = "That provider is already known";
- }
-
- return (
- <AnastasisClientFrame
- hideNav
- title="Backup: Manage providers"
- hideNext={errors}
- >
- <div>
- {!providerLabel ? (
- <p>Add a provider url</p>
- ) : (
- <p>Add a provider url for a {providerLabel} service</p>
- )}
- <div class="container">
- <TextInput
- label="Provider URL"
- placeholder="https://provider.com"
- grabFocus
- error={errors}
- bind={[providerURL, setProviderURL]}
- />
- </div>
- <p class="block">Example: https://kudos.demo.anastasis.lu</p>
- {testing && <p class="has-text-info">Testing</p>}
-
- <div
- class="block"
- style={{
- marginTop: "2em",
- display: "flex",
- justifyContent: "space-between",
- }}
- >
- <button class="button" onClick={onCancel}>
- Cancel
- </button>
- <span data-tooltip={errors}>
- <button
- class="button is-info"
- disabled={error !== "" || testing}
- onClick={() => addProvider(url!)}
- >
- Add
- </button>
- </span>
- </div>
-
- {authProviders.length > 0 ? (
- !providerLabel ? (
- <p class="subtitle">Current providers</p>
- ) : (
- <p class="subtitle">
- Current providers for {providerLabel} service
- </p>
- )
- ) : !providerLabel ? (
- <p class="subtitle">No known providers, add one.</p>
- ) : (
- <p class="subtitle">No known providers for {providerLabel} service</p>
- )}
-
- {authProviders.map((k) => {
- const p = allAuthProviders[k] as AuthenticationProviderStatusOk;
- return (
- <TableRow key={k} url={k} info={p} onDelete={deleteProvider} />
- );
- })}
- {authProvidersByStatus["error"]?.map((k) => {
- const p = k as AuthenticationProviderStatusError;
- return (
- <TableRowError
- key={k}
- url={k.url}
- info={p}
- onDelete={deleteProvider}
- />
- );
- })}
- </div>
- </AnastasisClientFrame>
- );
-}
-function TableRow({
- url,
- info,
- onDelete,
-}: {
- onDelete: (s: string) => 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/AddingProviderScreen/index.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
new file mode 100644
index 000000000..5d5913ffc
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
@@ -0,0 +1,102 @@
+/*
+ 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 { 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;
+}
+
+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)
+
+
+export async function testProvider(
+ url: string,
+ expectedMethodType?: string,
+): Promise<void> {
+ try {
+ const response = await fetch(new URL("config", url).href);
+ const json = await response.json().catch((d) => ({}));
+ if (!("methods" in json) || !Array.isArray(json.methods)) {
+ throw Error(
+ "This provider doesn't have authentication method. Check the provider URL",
+ );
+ }
+ if (!expectedMethodType) {
+ return;
+ }
+ let found = false;
+ for (let i = 0; i < json.methods.length && !found; i++) {
+ found = json.methods[i].type === expectedMethodType;
+ }
+ if (!found) {
+ throw Error(
+ `This provider does not support authentication method ${expectedMethodType}`,
+ );
+ }
+ return;
+ } catch (e) {
+ console.log("error", e);
+ const error =
+ e instanceof Error
+ ? Error(
+ `There was an error testing this provider, try another one. ${e.message}`,
+ )
+ : Error(`There was an error testing this provider, try another one.`);
+ throw error;
+ }
+}
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..a04c7957b
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
@@ -0,0 +1,147 @@
+/*
+ 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 { 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>;
+}
+
+export default function useComponentState({ providerType, onCancel }: 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 () => {
+ const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
+ if (!providerURL || authProviders.includes(url)) return;
+ try {
+ setTesting(true);
+ await testProvider(url, providerType);
+ setError("");
+ } catch (e) {
+ if (e instanceof Error) setError(e.message);
+ }
+ 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;
+ 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,
+ }
+
+ 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..6d8a634d5
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
@@ -0,0 +1,89 @@
+/*
+ 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 { createExampleWithoutAnastasis } from "../../../utils/index.jsx";
+import { WithoutProviderType, WithProviderType } from "./views.jsx";
+
+export default {
+ title: "Pages/ManageProvider",
+ args: {
+ order: 1,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const NewProvider = createExampleWithoutAnastasis(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: [],
+ },
+});
+
+export const NewProviderWithoutProviderList = createExampleWithoutAnastasis(
+ WithoutProviderType,
+ {
+ authProvidersByStatus: {
+ ok: [],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ },
+);
+
+export const NewSmsProvider = createExampleWithoutAnastasis(WithProviderType, {
+ authProvidersByStatus: {
+ ok: [],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ providerLabel: "sms",
+});
+
+export const NewIBANProvider = createExampleWithoutAnastasis(WithProviderType, {
+ authProvidersByStatus: {
+ ok: [],
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ },
+ providerLabel: "IBAN",
+});
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..d051d7c0b
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts
@@ -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 { expect } from "chai";
+import { mountHook } from "../../../test-utils.js";
+import useComponentState from "./state.js";
+
+describe("AddingProviderScreen states", () => {
+ it("should have status 'no-balance' when balance is empty", async () => {
+ const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
+ mountHook(() =>
+ useComponentState({ onCancel: async () => { null } }),
+ );
+
+ {
+ const { status } = getLastResultOrThrow();
+ expect(status).equal("no-reducer");
+ }
+
+ await assertNoPendingUpdate();
+
+ });
+
+});
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..bb1283a82
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
@@ -0,0 +1,304 @@
+/*
+ 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 { AnastasisClientFrame } from "../index.js";
+import { testProvider, WithoutType, WithType } from "./index.js";
+
+export function WithProviderType(props: WithType): VNode {
+ return (
+ <AnastasisClientFrame
+ hideNav
+ title="Backup: Manage providers1"
+ hideNext={props.errors}
+ >
+ <div>
+ <p>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 providers2"
+ hideNext={props.errors}
+ >
+ <div>
+ <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/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index f93ecfd8a..3018f88dd 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -17,7 +17,7 @@ import { AuthMethod, ReducerStateBackup } from "@gnu-taler/anastasis-core";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis.js";
-import { AddingProviderScreen } from "./AddingProviderScreen.js";
+import AddingProviderScreen from "./AddingProviderScreen/index.js";
import {
authMethods,
AuthMethodSetupProps,
@@ -84,7 +84,7 @@ export function AuthenticationEditorScreen(): VNode {
if (manageProvider !== undefined) {
return (
<AddingProviderScreen
- onCancel={() => setManageProvider(undefined)}
+ onCancel={async () => setManageProvider(undefined)}
providerType={
isKnownAuthMethods(manageProvider) ? manageProvider : undefined
}
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 7d92bcd2c..11271aaa5 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -22,7 +22,7 @@ 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.js";
+import AddingProviderScreen from "./AddingProviderScreen/index.js";
import { AnastasisClientFrame } from "./index.js";
export function SecretSelectionScreen(): VNode {
@@ -54,7 +54,9 @@ export function SecretSelectionScreen(): VNode {
const recoveryDocument = reducer.currentReducerState.recovery_document;
if (manageProvider) {
- return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
+ return (
+ <AddingProviderScreen onCancel={async () => setManageProvider(false)} />
+ );
}
if (reducer.discoveryState.state === "none") {
@@ -220,7 +222,9 @@ export function OldSecretSelectionScreen(): VNode {
}
if (manageProvider) {
- return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
+ return (
+ <AddingProviderScreen onCancel={async () => setManageProvider(false)} />
+ );
}
const providerInfo = provs[
diff --git a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx
index 1c47b57ee..0d01a5c89 100644
--- a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import * as a23 from "./AddingProviderScreen.stories.js";
+import * as a23 from "./AddingProviderScreen/stories.js";
import * as a28 from "./AttributeEntryScreen.stories.js";
import * as a18 from "./AuthenticationEditorScreen.stories.js";
import * as a8 from "./authMethod/AuthMethodEmailSetup.stories.js";
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 57f935bd6..25d9c63d9 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -168,9 +168,9 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
window.removeEventListener("popstate", browserOnBackButton);
};
}, []);
- if (!reducer) {
- return <p>Fatal: Reducer must be in context.</p>;
- }
+ // if (!reducer) {
+ // return <p>Fatal: Reducer must be in context.</p>;
+ // }
return (
<Fragment>
diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx
index 204c48d18..63bed9392 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -21,13 +21,26 @@ import {
ReducerState,
ReducerStateRecovery,
} from "@gnu-taler/anastasis-core";
-import { FunctionalComponent, h, VNode } from "preact";
+import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
import { AnastasisProvider } from "../context/anastasis.js";
const noop = async (): Promise<void> => {
return;
};
+export function createExampleWithoutAnastasis<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props> | (() => Partial<Props>),
+): ComponentChildren {
+ //FIXME: props are evaluated on build time
+ // in some cases we want to evaluated the props on render time so we can get some relative timestamp
+ // check how we can build evaluatedProps in render time
+ const evaluatedProps = typeof props === "function" ? props() : props;
+ const Render = (args: any): VNode => h(Component, args);
+ Render.args = evaluatedProps;
+ return Render;
+}
+
export function createExample<Props>(
Component: FunctionalComponent<Props>,
currentReducerState?: ReducerState,
@@ -293,3 +306,24 @@ export const reducerStatesExample = {
backup_state: BackupStates.TruthsPaying,
} as ReducerState,
};
+
+export type StateFunc<S> = (p: S) => VNode;
+
+export type StateViewMap<StateType extends { status: string }> = {
+ [S in StateType as S["status"]]: StateFunc<S>;
+};
+
+export function compose<SType extends { status: string }, PType>(
+ name: string,
+ hook: (p: PType) => SType,
+ vs: StateViewMap<SType>,
+): (p: PType) => VNode {
+ const Component = (p: PType): VNode => {
+ const state = hook(p);
+ const s = state.status as unknown as SType["status"];
+ const c = vs[s] as unknown as StateFunc<SType>;
+ return c(state);
+ };
+ Component.name = `${name}`;
+ return Component;
+}