diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/hooks')
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/form.ts | 124 | ||||
-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.ts | 48 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts | 8 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/useCases.ts | 50 |
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: () => { |