add testing to web components

This commit is contained in:
Sebastian 2022-06-11 19:10:26 -03:00
parent 716da3246b
commit 6d06b52605
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
18 changed files with 801 additions and 451 deletions

View File

@ -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

View File

@ -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",

View File

@ -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>;
}

View File

@ -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>;
}

View File

@ -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" },
);

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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
}
}
}

View File

@ -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",
});

View File

@ -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();
});
});

View File

@ -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>
);
}

View File

@ -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
}

View File

@ -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[

View File

@ -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";

View File

@ -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>

View File

@ -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;
}

View File

@ -1,4 +1,19 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import linaria from '@linaria/esbuild'
import esbuild from 'esbuild'
@ -61,7 +76,7 @@ export const buildConfig = {
],
format: 'iife',
platform: 'browser',
sourcemap: true,
sourcemap: true,
jsxFactory: 'h',
jsxFragment: 'Fragment',
// define: {

View File

@ -56,10 +56,12 @@ importers:
'@creativebulma/bulma-tooltip': ^1.2.0
'@gnu-taler/anastasis-core': workspace:*
'@gnu-taler/taler-util': workspace:*
'@types/chai': ^4.3.0
'@types/mocha': ^9.0.0
bulma: ^0.9.3
bulma-checkbox: ^1.1.1
bulma-radio: ^1.1.1
chai: ^4.3.6
chokidar: ^3.5.3
date-fns: 2.28.0
eslint-plugin-header: ^3.1.1
@ -76,6 +78,8 @@ importers:
dependencies:
'@gnu-taler/anastasis-core': link:../anastasis-core
'@gnu-taler/taler-util': link:../taler-util
'@types/chai': 4.3.0
chai: 4.3.6
date-fns: 2.28.0
jed: 1.1.1
preact: 10.6.5
@ -3488,7 +3492,6 @@ packages:
/@types/chai/4.3.0:
resolution: {integrity: sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==}
dev: true
/@types/chrome/0.0.176:
resolution: {integrity: sha512-LOveFOMIUhMJjvRzZv5whGBpncP/gdJ4hcxeAqg94wGi6CyKaCmLgFSofgItf85GuLTl/0BQ6J/Y1e8BqZWfEg==}
@ -4330,7 +4333,6 @@ packages:
/assertion-error/1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/assign-symbols/1.0.0:
resolution: {integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=}
@ -5208,7 +5210,6 @@ packages:
loupe: 2.3.4
pathval: 1.1.1
type-detect: 4.0.8
dev: true
/chalk/0.4.0:
resolution: {integrity: sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=}
@ -5264,8 +5265,7 @@ packages:
dev: true
/check-error/1.0.2:
resolution: {integrity: sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=}
dev: true
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
/chokidar/2.1.8:
resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==}
@ -6242,7 +6242,6 @@ packages:
engines: {node: '>=0.12'}
dependencies:
type-detect: 4.0.8
dev: true
/deep-equal/1.1.1:
resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==}
@ -7773,8 +7772,7 @@ packages:
dev: true
/get-func-name/2.0.0:
resolution: {integrity: sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=}
dev: true
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
/get-intrinsic/1.1.1:
resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==}
@ -9312,7 +9310,6 @@ packages:
resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==}
dependencies:
get-func-name: 2.0.0
dev: true
/lower-case/1.1.4:
resolution: {integrity: sha1-miyr0bno4K6ZOkv31YdcOcQujqw=}
@ -10453,7 +10450,6 @@ packages:
/pathval/1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: true
/pbkdf2/3.1.2:
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
@ -13165,7 +13161,6 @@ packages:
/type-detect/4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
dev: true
/type-fest/0.13.1:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}