aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/aml-backoffice-ui/src/hooks')
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts124
-rw-r--r--packages/aml-backoffice-ui/src/hooks/officer.ts (renamed from packages/aml-backoffice-ui/src/hooks/useOfficer.ts)67
-rw-r--r--packages/aml-backoffice-ui/src/hooks/preferences.ts (renamed from packages/aml-backoffice-ui/src/hooks/useSettings.ts)70
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useBackend.ts48
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts8
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts50
6 files changed, 244 insertions, 123 deletions
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
new file mode 100644
index 000000000..fae11c05c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -0,0 +1,124 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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 { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+
+export type UIField = {
+ value: string | undefined;
+ onUpdate: (s: string) => void;
+ error: TranslatedString | undefined;
+};
+
+type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIField
+ : T[k] extends AmountJson
+ ? UIField
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string
+ ? string | undefined
+ : T[k] extends AmountJson
+ ? string | undefined
+ : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ form: FormValues<T>,
+ updateForm: (d: FormValues<T>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+
+ const keys = Object.keys(form) as Array<keyof T>;
+
+ const handler = keys.reduce((prev, fieldName) => {
+ const currentValue: unknown = form[fieldName];
+ const currentError: unknown = errors ? errors[fieldName] : undefined;
+ function updater(newValue: unknown) {
+ updateForm({ ...form, [fieldName]: newValue });
+ }
+ if (typeof currentValue === "object") {
+ // @ts-expect-error FIXME better typing
+ const group = constructFormHandler(currentValue, updater, currentError);
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = group;
+ return prev;
+ }
+ const field: UIField = {
+ // @ts-expect-error FIXME better typing
+ error: currentError,
+ // @ts-expect-error FIXME better typing
+ value: currentValue,
+ onUpdate: updater,
+ };
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = field;
+ return prev;
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ defaultValue: FormValues<T>,
+ check: (f: FormValues<T>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] = useState<FormValues<T>>(defaultValue);
+
+ const status = check(form);
+ const handler = constructFormHandler(form, updateForm, status.errors);
+
+ return [handler, status];
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts
index b49c9db26..3ac4c857c 100644
--- a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
+++ b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -26,14 +26,11 @@ import {
createNewOfficerAccount,
decodeCrock,
encodeCrock,
- unlockOfficerAccount
+ unlockOfficerAccount,
} from "@gnu-taler/taler-util";
-import {
- buildStorageKey,
- useLocalStorage
-} from "@gnu-taler/web-util/browser";
+import { buildStorageKey, useExchangeApiContext, useLocalStorage } from "@gnu-taler/web-util/browser";
import { useMemo } from "preact/hooks";
-import { useMaybeExchangeApiContext } from "../context/config.js";
+import { usePreferences } from "./preferences.js";
export interface Officer {
account: LockedAccount;
@@ -43,9 +40,9 @@ export interface Officer {
const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
type OfficerAccountString = {
- id: string,
+ id: string;
strKey: string;
-}
+};
export const codecForOfficerAccount = (): Codec<OfficerAccountString> =>
buildCodecForObject<OfficerAccountString>()
@@ -78,46 +75,55 @@ interface OfficerReady {
}
const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
-const DEV_ACCOUNT_KEY = buildStorageKey("account-dev", codecForOfficerAccount());
+const DEV_ACCOUNT_KEY = buildStorageKey(
+ "account-dev",
+ codecForOfficerAccount(),
+);
export function useOfficer(): OfficerState {
- const exchangeContext = useMaybeExchangeApiContext();
- // dev account, is save when reloaded.
+ const exchangeContext = useExchangeApiContext();
+ const [pref] = usePreferences();
+ pref.keepSessionAfterReload;
+ // dev account, is kept on reloaded.
const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY);
const account = useMemo(() => {
- if (!accountStorage.value) return undefined
+ if (!accountStorage.value) return undefined;
return {
id: accountStorage.value.id as OfficerId,
- signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey
- }
- }, [accountStorage.value?.id, accountStorage.value?.strKey])
+ signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey,
+ };
+ }, [accountStorage.value?.id, accountStorage.value?.strKey]);
const officerStorage = useLocalStorage(OFFICER_KEY);
const officer = useMemo(() => {
- if (!officerStorage.value) return undefined
- return officerStorage.value
- }, [officerStorage.value?.account, officerStorage.value?.when.t_ms])
+ if (!officerStorage.value) return undefined;
+ return officerStorage.value;
+ }, [officerStorage.value?.account, officerStorage.value?.when.t_ms]);
if (officer === undefined) {
return {
state: "not-found",
create: async (pwd: string) => {
- if (!exchangeContext) return;
- const req = await fetch(new URL("seed", exchangeContext.api.baseUrl).href)
- const b = await req.blob()
- const ar = await b.arrayBuffer()
- const uintar = new Uint8Array(ar)
-
- const { id, safe, signingKey } = await createNewOfficerAccount(pwd, uintar);
+ const req = await fetch(
+ new URL("seed", exchangeContext.lib.exchange.baseUrl).href,
+ );
+ const b = await req.blob();
+ const ar = await b.arrayBuffer();
+ const uintar = new Uint8Array(ar);
+
+ const { id, safe, signingKey } = await createNewOfficerAccount(
+ pwd,
+ uintar,
+ );
officerStorage.update({
account: safe,
when: AbsoluteTime.now(),
});
// accountStorage.update({ id, signingKey });
- const strKey = encodeCrock(signingKey)
- accountStorage.update({ id, strKey })
+ const strKey = encodeCrock(signingKey);
+ accountStorage.update({ id, strKey });
},
};
}
@@ -131,7 +137,10 @@ export function useOfficer(): OfficerState {
tryUnlock: async (pwd: string) => {
const ac = await unlockOfficerAccount(officer.account, pwd);
// accountStorage.update(ac);
- accountStorage.update({ id: ac.id, strKey: encodeCrock(ac.signingKey) })
+ accountStorage.update({
+ id: ac.id,
+ strKey: encodeCrock(ac.signingKey),
+ });
},
};
}
diff --git a/packages/aml-backoffice-ui/src/hooks/useSettings.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
index 55cdafb23..12e85d249 100644
--- a/packages/aml-backoffice-ui/src/hooks/useSettings.ts
+++ b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -20,52 +20,66 @@ import {
buildCodecForObject,
codecForBoolean
} from "@gnu-taler/taler-util";
-import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
-interface Settings {
+interface Preferences {
allowInsecurePassword: boolean;
keepSessionAfterReload: boolean;
}
-export function getAllBooleanSettings(): Array<keyof Settings> {
- return ["allowInsecurePassword", "keepSessionAfterReload"]
-}
-
-export function getLabelForSetting(k: keyof Settings, i18n: ReturnType<typeof useTranslationContext>["i18n"]): TranslatedString {
- switch (k) {
- case "allowInsecurePassword": return i18n.str`Allow Insecure password`
- case "keepSessionAfterReload": return i18n.str`Keep session after reload`
- }
-}
-
-export const codecForSettings = (): Codec<Settings> =>
- buildCodecForObject<Settings>()
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
.property("allowInsecurePassword", (codecForBoolean()))
.property("keepSessionAfterReload", (codecForBoolean()))
- .build("Settings");
+ .build("Preferences");
-const defaultSettings: Settings = {
+const defaultPreferences: Preferences = {
allowInsecurePassword: false,
keepSessionAfterReload: false,
};
-const EXCHANGE_SETTINGS_KEY = buildStorageKey(
- "exchange-settings",
- codecForSettings(),
+const PREFERENCES_KEY = buildStorageKey(
+ "exchange-preferences",
+ codecForPreferences(),
);
-
-export function useSettings(): [
- Readonly<Settings>,
- <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
] {
const { value, update } = useLocalStorage(
- EXCHANGE_SETTINGS_KEY,
- defaultSettings,
+ PREFERENCES_KEY,
+ defaultPreferences,
);
- function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
const newValue = { ...value, [k]: v };
update(newValue);
}
return [value, updateField];
}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "allowInsecurePassword",
+ "keepSessionAfterReload",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "allowInsecurePassword": return i18n.str`Allow Insecure password`
+ case "keepSessionAfterReload": return i18n.str`Keep session after reload`
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts
deleted file mode 100644
index 310f7fe59..000000000
--- a/packages/aml-backoffice-ui/src/hooks/useBackend.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- 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 { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-import { uiSettings } from "../settings.js";
-
-
-export function getInitialBackendBaseURL(): string {
- const overrideUrl =
- typeof localStorage !== "undefined"
- ? localStorage.getItem("exchange-base-url")
- : undefined;
-
- let result: string;
-
- if (!overrideUrl) {
- //normal path
- if (!uiSettings.backendBaseURL) {
- console.error(
- "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
- );
- result = typeof window !== "undefined" ? window.origin : "localhost"
- } else {
- result = uiSettings.backendBaseURL;
- }
- } else {
- // testing/development path
- result = overrideUrl
- }
- try {
- return canonicalizeBaseUrl(result)
- } catch (e) {
- //fall back
- return canonicalizeBaseUrl(window.origin)
- }
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
index de1c5af17..78574ada4 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -16,15 +16,15 @@
import { OfficerAccount, PaytoString, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook } from "swr";
-import { useExchangeApiContext } from "../context/config.js";
-import { useOfficer } from "./useOfficer.js";
+import { useOfficer } from "./officer.js";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
export function useCaseDetails(paytoHash: string) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const { api } = useExchangeApiContext();
+ const { lib: {exchange: api} } = useExchangeApiContext();
async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
return await api.getDecisionDetails(officer, account)
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index 7c8bb5bc1..59d1c9001 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -16,11 +16,16 @@
import { useState } from "preact/hooks";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { OfficerAccount, OperationOk, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
+import {
+ OfficerAccount,
+ OperationOk,
+ TalerExchangeResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
-import { useExchangeApiContext } from "../context/config.js";
import { AmlExchangeBackend } from "../utils/types.js";
-import { useOfficer } from "./useOfficer.js";
+import { useOfficer } from "./officer.js";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
export const PAGINATED_LIST_SIZE = 10;
@@ -37,17 +42,28 @@ export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
export function useCases(state: AmlExchangeBackend.AmlState) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const { api } = useExchangeApiContext();
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
const [offset, setOffset] = useState<string>();
- async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) {
+ async function fetcher([officer, state, offset]: [
+ OfficerAccount,
+ AmlExchangeBackend.AmlState,
+ string | undefined,
+ ]) {
return await api.getDecisionsByState(officer, state, {
- order: "asc", offset, limit: PAGINATED_LIST_REQUEST
- })
+ order: "asc",
+ offset,
+ limit: PAGINATED_LIST_REQUEST,
+ });
}
- const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>(
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getDecisionsByState">,
+ TalerHttpError
+ >(
!session ? undefined : [session, state, offset, "getDecisionsByState"],
fetcher,
);
@@ -56,7 +72,9 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
if (data === undefined) return undefined;
if (data.type !== "ok") return data;
- return buildPaginatedResult(data.body.records, offset, setOffset, (d) => String(d.rowid));
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
}
type PaginatedResult<T> = OperationOk<T> & {
@@ -64,11 +82,15 @@ type PaginatedResult<T> = OperationOk<T> & {
isFirstPage: boolean;
loadNext(): void;
loadFirst(): void;
-}
+};
//TODO: consider sending this to web-util
-export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> {
-
+export function buildPaginatedResult<R, OffId>(
+ data: R[],
+ offset: OffId | undefined,
+ setOffset: (o: OffId | undefined) => void,
+ getId: (r: R) => OffId,
+): PaginatedResult<R[]> {
const isLastPage = data.length < PAGINATED_LIST_REQUEST;
const isFirstPage = offset === undefined;
@@ -83,7 +105,7 @@ export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefi
isFirstPage,
loadNext: () => {
if (!result.length) return;
- const id = getId(result[result.length - 1])
+ const id = getId(result[result.length - 1]);
setOffset(id);
},
loadFirst: () => {