aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-11-05 18:04:22 -0300
committerSebastian <sebasjm@gmail.com>2023-11-05 18:04:22 -0300
commitb58d53dd93bd8e97aecc28fae788c5c7051fd73d (patch)
tree7a402fafa4ae19a64f10eeb3042147f5f4733081 /packages/aml-backoffice-ui/src
parent31cf3187e447e2c4ec8a473362c5bacc07a874f1 (diff)
downloadwallet-core-b58d53dd93bd8e97aecc28fae788c5c7051fd73d.tar.xz
sharing components in web-util
Diffstat (limited to 'packages/aml-backoffice-ui/src')
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx11
-rw-r--r--packages/aml-backoffice-ui/src/Dashboard.tsx26
-rw-r--r--packages/aml-backoffice-ui/src/account.ts128
-rw-r--r--packages/aml-backoffice-ui/src/context/config.ts89
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Caption.tsx7
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useBackend.ts27
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts103
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts126
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useOfficer.ts20
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx83
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx46
-rw-r--r--packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx19
-rw-r--r--packages/aml-backoffice-ui/src/pages/Officer.tsx4
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx3
-rw-r--r--packages/aml-backoffice-ui/src/utils/errors.tsx77
15 files changed, 297 insertions, 472 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index 600131219..0e29279ff 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -1,12 +1,19 @@
import { TranslationProvider } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
-import { Dashboard } from "./Dashboard.js";
+import { ExchangeAmlFrame, Main } from "./Dashboard.js";
import "./scss/main.css";
+import { ExchangeApiProvider } from "./context/config.js";
+import { getInitialBackendBaseURL } from "./hooks/useBackend.js";
export function App(): VNode {
+ const baseUrl = getInitialBackendBaseURL();
return (
<TranslationProvider source={{}}>
- <Dashboard />
+ <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}>
+ <ExchangeAmlFrame>
+ <Main />
+ </ExchangeAmlFrame>
+ </ExchangeApiProvider>
</TranslationProvider>
);
}
diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx
index 6794ca1f8..bd8a48c45 100644
--- a/packages/aml-backoffice-ui/src/Dashboard.tsx
+++ b/packages/aml-backoffice-ui/src/Dashboard.tsx
@@ -182,7 +182,7 @@ function LeftMenu() {
);
}
-export function Dashboard({
+export function ExchangeAmlFrame({
children,
}: {
children?: ComponentChildren;
@@ -211,21 +211,25 @@ export function Dashboard({
}}
/>
<Notifications />
- <main class="py-10 px-4 sm:px-6 lg:px-8">
- <div class="mx-auto max-w-3xl">
- <Router
- pageList={pageList}
- onNotFound={() => {
- return <div>not found</div>;
- }}
- />
- </div>
- </main>
+ {children}
</div>
</Fragment>
);
}
+export function Main(): VNode {
+ return <main class="py-10 px-4 sm:px-6 lg:px-8">
+ <div class="mx-auto max-w-3xl">
+ <Router
+ pageList={pageList}
+ onNotFound={() => {
+ return <div>not found</div>;
+ }}
+ />
+ </div>
+ </main>
+}
+
const pageList = Object.values(Pages);
function NavigationBar({
diff --git a/packages/aml-backoffice-ui/src/account.ts b/packages/aml-backoffice-ui/src/account.ts
deleted file mode 100644
index 615d843c4..000000000
--- a/packages/aml-backoffice-ui/src/account.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import {
- Amounts,
- TalerSignaturePurpose,
- amountToBuffer,
- bufferForUint32,
- buildSigPS,
- createEddsaKeyPair,
- decodeCrock,
- decryptWithDerivedKey,
- eddsaGetPublic,
- eddsaSign,
- encodeCrock,
- encryptWithDerivedKey,
- getRandomBytesF,
- hash,
- hashTruncate32,
- stringToBytes,
- timestampRoundedToBuffer
-} from "@gnu-taler/taler-util";
-import { AmlExchangeBackend } from "./types.js";
-
-export interface Account {
- accountId: AccountId;
- signingKey: SigningKey;
-}
-
-/**
- * Restore previous session and unlock account with password
- *
- * @param salt string from which crypto params will be derived
- * @param key secured private key
- * @param password password for the private key
- * @returns
- */
-export async function unlockAccount(
- account: LockedAccount,
- password: string,
-): Promise<Account> {
- const rawKey = decodeCrock(account);
- const rawPassword = stringToBytes(password);
-
- const signingKey = (await decryptWithDerivedKey(
- rawKey,
- rawPassword,
- password,
- ).catch((e: Error) => {
- throw new UnwrapKeyError(e.message);
- })) as SigningKey;
-
- const publicKey = eddsaGetPublic(signingKey);
-
- const accountId = encodeCrock(publicKey) as AccountId;
-
- return { accountId, signingKey };
-}
-
-export function buildQuerySignature(key: SigningKey): string {
- const sigBlob = buildSigPS(
- TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
- ).build();
-
- return encodeCrock(eddsaSign(sigBlob, key));
-}
-
-export function buildDecisionSignature(
- key: SigningKey,
- decision: AmlExchangeBackend.AmlDecision,
-): string {
- const zero = new Uint8Array(new ArrayBuffer(64))
-
- const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
- //TODO: new need the null terminator, also in the exchange
- .put(hash(stringToBytes(decision.justification)))//check null
- .put(timestampRoundedToBuffer(decision.decision_time))
- .put(amountToBuffer(decision.new_threshold))
- .put(decodeCrock(decision.h_payto))
- .put(zero) //kyc_requirement
- .put(bufferForUint32(decision.new_state))
- .build();
-
- return encodeCrock(eddsaSign(sigBlob, key));
-}
-
-declare const opaque_Account: unique symbol;
-export type LockedAccount = string & { [opaque_Account]: true };
-
-declare const opaque_AccountId: unique symbol;
-export type AccountId = string & { [opaque_AccountId]: true };
-
-declare const opaque_SigningKey: unique symbol;
-export type SigningKey = Uint8Array & { [opaque_SigningKey]: true };
-
-/**
- * Create new account (secured private key)
- * secured with the given password
- *
- * @param sessionId
- * @param password
- * @returns
- */
-export async function createNewAccount(
- password: string,
-): Promise<Account & { safe: LockedAccount }> {
- const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
-
- const key = stringToBytes(password);
-
- const protectedPrivKey = await encryptWithDerivedKey(
- getRandomBytesF(24),
- key,
- eddsaPriv,
- password,
- );
-
- const signingKey = eddsaPriv as SigningKey;
- const accountId = encodeCrock(eddsaPub) as AccountId;
- const safe = encodeCrock(protectedPrivKey) as LockedAccount;
-
- return { accountId, signingKey, safe };
-}
-
-export class UnwrapKeyError extends Error {
- public cause: string;
- constructor(cause: string) {
- super(`Recovering private key failed on: ${cause}`);
- this.cause = cause;
- }
-}
diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts
new file mode 100644
index 000000000..2866717de
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/config.ts
@@ -0,0 +1,89 @@
+/*
+ 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 { TalerExchangeApi, TalerExchangeHttpClient, TalerError } from "@gnu-taler/taler-util";
+import { BrowserHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { ErrorLoading } from "@gnu-taler/web-util/browser";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = {
+ url: URL,
+ config: TalerExchangeApi.ExchangeVersionResponse,
+ api: TalerExchangeHttpClient,
+};
+
+const Context = createContext<Type>(undefined as any);
+
+export const useExchangeApiContext = (): Type => useContext(Context);
+
+export type ConfigResult = undefined
+ | { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse }
+ | { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string }
+ | { type: "error", error: TalerError }
+
+export const ExchangeApiProvider = ({
+ baseUrl,
+ children,
+ frameOnError,
+}: {
+ baseUrl: string,
+ children: ComponentChildren;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>,
+}): VNode => {
+ const [checked, setChecked] = useState<ConfigResult>()
+ const { i18n } = useTranslationContext();
+ const url = new URL(baseUrl)
+ const api = new TalerExchangeHttpClient(url.href, new BrowserHttpLib())
+ useEffect(() => {
+ api.getConfig()
+ .then((resp) => {
+ if (api.isCompatible(resp.body.version)) {
+ setChecked({ type: "ok", config: resp.body });
+ } else {
+ setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION })
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof TalerError) {
+ setChecked({ type: "error", error });
+ }
+ });
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, { children: h("div", {}, "loading...") })
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, { children: h(ErrorLoading, { error: checked.error, showDetail: true }) })
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) })
+ }
+ const value: Type = {
+ url, config: checked.config, api
+ }
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
diff --git a/packages/aml-backoffice-ui/src/handlers/Caption.tsx b/packages/aml-backoffice-ui/src/handlers/Caption.tsx
index fbf154d89..8facddec3 100644
--- a/packages/aml-backoffice-ui/src/handlers/Caption.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/Caption.tsx
@@ -1,11 +1,8 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import {
- IconAddon,
- InputLine,
- LabelWithTooltipMaybeRequired,
- UIFormProps,
+ LabelWithTooltipMaybeRequired
} from "./InputLine.js";
-import { TranslatedString } from "@gnu-taler/taler-util";
interface Props {
label: TranslatedString;
diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts
index b9d66fca6..95277a915 100644
--- a/packages/aml-backoffice-ui/src/hooks/useBackend.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useBackend.ts
@@ -1,3 +1,4 @@
+import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
import {
HttpResponseOk,
RequestOptions,
@@ -5,9 +6,6 @@ import {
} from "@gnu-taler/web-util/browser";
import { useCallback } from "preact/hooks";
import { uiSettings } from "../settings.js";
-import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-import { useOfficer } from "./useOfficer.js";
-import { buildQuerySignature } from "../account.js";
interface useBackendType {
request: <T>(
@@ -35,7 +33,7 @@ export function usePublicBackend(): useBackendType {
);
const fetcher = useCallback(
- function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string,string]): Promise<HttpResponseOk<T>> {
+ function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string, string]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
talerAmlOfficerSignature
});
@@ -66,18 +64,29 @@ export function usePublicBackend(): useBackendType {
export function getInitialBackendBaseURL(): string {
const overrideUrl =
typeof localStorage !== "undefined"
- ? localStorage.getItem("exchange-aml-base-url")
+ ? 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'",
);
- return canonicalizeBaseUrl(window.origin);
+ result = window.origin
+ } else {
+ result = uiSettings.backendBaseURL;
}
- return canonicalizeBaseUrl(uiSettings.backendBaseURL);
+ } else {
+ // testing/development path
+ result = overrideUrl
+ }
+ try {
+ return canonicalizeBaseUrl(result)
+ } catch (e) {
+ //fall back
+ return canonicalizeBaseUrl(window.origin)
}
- // testing/development path
- return canonicalizeBaseUrl(overrideUrl);
}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
index 980a35f21..9db1e2aec 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
@@ -1,34 +1,29 @@
import {
HttpResponse,
- HttpResponseOk,
- RequestError
+ HttpResponseOk
} from "@gnu-taler/web-util/browser";
import { AmlExchangeBackend } from "../types.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AmountString, OfficerAccount, PaytoString, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, useSWRConfig } from "swr";
-import { AccountId } from "../account.js";
+import { useExchangeApiContext } from "../context/config.js";
import { usePublicBackend } from "./useBackend.js";
+import { useOfficer } from "./useOfficer.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function useCaseDetails(
- account: AccountId,
- paytoHash: string,
- signature: string | undefined,
-): HttpResponse<
- AmlExchangeBackend.AmlDecisionDetails,
- AmlExchangeBackend.AmlError
-> {
- const { fetcher } = usePublicBackend();
+export function useCaseDetails(paytoHash: string) {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
- const { data, error } = useSWR<
- HttpResponseOk<AmlExchangeBackend.AmlDecisionDetails>,
- RequestError<AmlExchangeBackend.AmlError>
->( [
- `aml/${account}/decision/${(paytoHash)}`,
- signature,
-],
-fetcher, {
+ const { api } = useExchangeApiContext();
+
+ async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
+ return await api.getDecisionDetails(officer, account)
+ }
+
+ const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>(
+ !session ? undefined : [session, paytoHash], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -41,11 +36,11 @@ fetcher, {
});
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
-const example1: AmlExchangeBackend.AmlDecisionDetails = {
+const example1: TalerExchangeApi.AmlDecisionDetails = {
aml_history: [
{
justification: "Lack of documentation",
@@ -54,7 +49,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = {
t_s: Date.now() / 1000,
},
new_state: 2,
- new_threshold: "USD:0",
+ new_threshold: "USD:0" as AmountString,
},
{
justification: "Doing a transfer of high amount",
@@ -63,7 +58,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
},
new_state: 1,
- new_threshold: "USD:2000",
+ new_threshold: "USD:2000" as AmountString,
},
{
justification: "Account is known to the system",
@@ -72,7 +67,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = {
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
},
new_state: 0,
- new_threshold: "USD:100",
+ new_threshold: "USD:100" as AmountString,
},
],
kyc_attributes: [
@@ -103,60 +98,4 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = {
],
};
-export const exampleResponse: HttpResponse<AmlExchangeBackend.AmlDecisionDetails,AmlExchangeBackend.AmlError> = {
- ok: true,
- data: example1,
-}
-
-
-export function useAmlCasesAPI(): AmlCaseAPI {
- const { request } = usePublicBackend();
- const mutateAll = useMatchMutate();
-
- const updateDecision = async (
- officer: AccountId,
- data: AmlExchangeBackend.AmlDecision,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`aml/${officer}/decision`, {
- method: "POST",
- data,
- contentType: "json",
- });
- await mutateAll(/.*aml.*/);
- return res;
- };
-
- return {
- updateDecision,
- };
-}
-
-export interface AmlCaseAPI {
- updateDecision: (
- officer: AccountId,
- data: AmlExchangeBackend.AmlDecision,
- ) => Promise<HttpResponseOk<void>>;
-}
-
-function useMatchMutate(): (
- re: RegExp,
- value?: unknown,
-) => Promise<any> {
- const { cache, mutate } = useSWRConfig();
-
- if (!(cache instanceof Map)) {
- throw new Error(
- "matchMutate requires the cache provider to be a Map instance",
- );
- }
-
- return function matchRegexMutate(re: RegExp, value?: unknown) {
- const allKeys = Array.from(cache.keys());
- const keys = allKeys.filter((key) => re.test(key));
- const mutations = keys.map((key) => {
- return mutate(key, value, true);
- });
- return Promise.all(mutations);
- };
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index c07bd5f18..2a133f46d 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -1,16 +1,13 @@
-import { useEffect, useState } from "preact/hooks";
+import { useState } from "preact/hooks";
-import { AmlExchangeBackend } from "../types.js";
import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
+ HttpResponsePaginated
} from "@gnu-taler/web-util/browser";
+import { AmlExchangeBackend } from "../types.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AmountString, OfficerAccount, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
-import { usePublicBackend } from "./useBackend.js";
-import { AccountId, buildQuerySignature } from "../account.js";
+import { useExchangeApiContext } from "../context/config.js";
import { useOfficer } from "./useOfficer.js";
const useSWR = _useSWR as unknown as SWRHook;
@@ -22,59 +19,49 @@ const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
* @param args
* @returns
*/
-export function useCases(
- account: AccountId,
- state: AmlExchangeBackend.AmlState,
- signature: string | undefined,
-): HttpResponsePaginated<
- AmlExchangeBackend.AmlRecords,
- AmlExchangeBackend.AmlError
-> {
- const { paginatedFetcher } = usePublicBackend();
+export function useCases(state: AmlExchangeBackend.AmlState) {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const { api } = useExchangeApiContext();
- const [page, setPage] = useState(1);
+ const [offset, setOffet] = useState<string>();
+
+ async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) {
+ return await api.getDecisionsByState(officer, state, {
+ order: "asc", offset, limit: MAX_RESULT_SIZE
+ })
+ }
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<AmlExchangeBackend.AmlRecords>,
- RequestError<AmlExchangeBackend.AmlError>
- >(
- [
- `aml/${account}/decisions/${AmlExchangeBackend.AmlState[state]}`,
- page,
- PAGE_SIZE,
- signature,
- ],
- paginatedFetcher,
+ const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>(
+ !session ? undefined : [session, state, offset],
+ fetcher,
);
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError>
- >({ loading: true });
+ // const [lastAfter, setLastAfter] = useState<
+ // HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError>
+ // >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- }, [afterData]);
+ // useEffect(() => {
+ // if (afterData) setLastAfter(afterData);
+ // }, [afterData]);
- if (afterError) {
- return afterError.cause;
- }
+ // if (afterError) {
+ // return afterError.cause;
+ // }
// if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd =
- afterData && afterData.data && afterData.data.records.length < PAGE_SIZE;
- const isReachingStart = false;
+ const isLastPage =
+ data && data.type === "ok" && data.body.records.length < PAGE_SIZE;
+ const isFirstPage = !offset;
const pagination = {
- isReachingEnd,
- isReachingStart,
+ isLastPage,
+ isFirstPage,
loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data && afterData.data.records.length < MAX_RESULT_SIZE) {
- setPage(page + 1);
+ if (isLastPage || data?.type !== "ok") return;
+ const list = data.body.records
+ if (list.length < MAX_RESULT_SIZE) {
+ // setOffset(list[list.length-1].account_name);
}
},
loadMorePrev: () => {
@@ -82,65 +69,62 @@ export function useCases(
},
};
- const records = !afterData
- ? []
- : ((afterData ?? lastAfter).data ?? { records: [] }).records;
- if (loadingAfter) return { loading: true, data: { records } };
- if (afterData) {
- return { ok: true, data: { records }, ...pagination };
+ // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts;
+ if (data) {
+ if (data.type === "fail") {
+ return { data }
+ }
+ return { data, pagination }
+ }
+ if (error) {
+ return error;
}
- return { loading: true };
+ return undefined;
}
-const example1: AmlExchangeBackend.AmlRecords = {
+const example1: TalerExchangeApi.AmlRecords = {
records: [
{
current_state: 0,
h_payto: "QWEQWEQWEQWEWQE",
rowid: 1,
- threshold: "USD 100",
+ threshold: "USD 100" as AmountString,
},
{
current_state: 1,
h_payto: "ASDASDASD",
rowid: 1,
- threshold: "USD 100",
+ threshold: "USD 100" as AmountString,
},
{
current_state: 2,
h_payto: "ZXCZXCZXCXZC",
rowid: 1,
- threshold: "USD 1000",
+ threshold: "USD 1000" as AmountString,
},
{
current_state: 0,
h_payto: "QWEQWEQWEQWEWQE",
rowid: 1,
- threshold: "USD 100",
+ threshold: "USD 100" as AmountString,
},
{
current_state: 1,
h_payto: "ASDASDASD",
rowid: 1,
- threshold: "USD 100",
+ threshold: "USD 100" as AmountString,
},
{
current_state: 2,
h_payto: "ZXCZXCZXCXZC",
rowid: 1,
- threshold: "USD 1000",
+ threshold: "USD 1000" as AmountString,
},
].map((e, idx) => {
e.rowid = idx;
- e.threshold = `${e.threshold}${idx}`;
+ e.threshold = `${e.threshold}${idx}` as AmountString;
return e;
}),
};
-export const exampleResponse: HttpResponsePaginated<AmlExchangeBackend.AmlRecords,AmlExchangeBackend.AmlError> = {
- ok: true,
- data: example1,
- loadMore: () => {},
- loadMorePrev: () => {},
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
index 4ec43569b..0747170e8 100644
--- a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
@@ -1,17 +1,15 @@
import {
AbsoluteTime,
Codec,
+ LockedAccount,
+ OfficerAccount,
buildCodecForObject,
codecForAbsoluteTime,
codecForString,
+ createNewOfficerAccount,
+ unlockOfficerAccount,
} from "@gnu-taler/taler-util";
import {
- Account,
- LockedAccount,
- createNewAccount,
- unlockAccount,
-} from "../account.js";
-import {
buildStorageKey,
useLocalStorage,
useMemoryStorage,
@@ -43,7 +41,7 @@ interface OfficerLocked {
}
interface OfficerReady {
state: "ready";
- account: Account;
+ account: OfficerAccount;
forget: () => void;
lock: () => void;
}
@@ -52,7 +50,7 @@ const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
const ACCOUNT_KEY = "account";
export function useOfficer(): OfficerState {
- const accountStorage = useMemoryStorage<Account>(ACCOUNT_KEY);
+ const accountStorage = useMemoryStorage<OfficerAccount>(ACCOUNT_KEY);
const officerStorage = useLocalStorage(OFFICER_KEY);
const officer = officerStorage.value;
@@ -62,13 +60,13 @@ export function useOfficer(): OfficerState {
return {
state: "not-found",
create: async (pwd: string) => {
- const { accountId, safe, signingKey } = await createNewAccount(pwd);
+ const { id, safe, signingKey } = await createNewOfficerAccount(pwd);
officerStorage.update({
account: safe,
when: AbsoluteTime.now(),
});
- accountStorage.update({ accountId, signingKey });
+ accountStorage.update({ id, signingKey });
},
};
}
@@ -80,7 +78,7 @@ export function useOfficer(): OfficerState {
officerStorage.reset();
},
tryUnlock: async (pwd: string) => {
- const ac = await unlockAccount(officer.account, pwd);
+ const ac = await unlockOfficerAccount(officer.account, pwd);
accountStorage.update(ac);
},
};
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index ce820d612..f618a3592 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -1,24 +1,23 @@
-import { Fragment, VNode, h } from "preact";
import {
AbsoluteTime,
AmountJson,
Amounts,
+ PaytoString,
+ TalerError,
TranslatedString,
+ assertUnreachable,
} from "@gnu-taler/taler-util";
-import { format } from "date-fns";
+import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NiceForm } from "../NiceForm.js";
import { FlexibleForm } from "../forms/index.js";
import { UIFormField } from "../handlers/forms.js";
+import { useCaseDetails } from "../hooks/useCaseDetails.js";
import { Pages } from "../pages.js";
import { AmlExchangeBackend } from "../types.js";
-import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { useOfficer } from "../hooks/useOfficer.js";
-import { buildQuerySignature } from "../account.js";
-import { useCaseDetails } from "../hooks/useCaseDetails.js";
-import { handleNotOkResult } from "../utils/errors.js";
type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent;
type AmlFormEvent = {
@@ -85,30 +84,33 @@ function getEventsFromAmlHistory(
return ae.concat(ke).sort(selectSooner);
}
-export function CaseDetails({ account: paytoHash }: { account: string }) {
+export function CaseDetails({ account }: { account: string }) {
const [selected, setSelected] = useState<AmlEvent | undefined>(undefined);
- const officer = useOfficer();
const { i18n } = useTranslationContext();
- if (officer.state !== "ready") {
- return <HandleAccountNotReady officer={officer} />;
+ const details = useCaseDetails(account)
+ if (!details) {
+ return <Loading />
}
- const signature =
- officer.state === "ready"
- ? buildQuerySignature(officer.account.signingKey)
- : undefined;
- const details = useCaseDetails(officer.account.accountId, paytoHash, signature)
- if (!details.ok && !details.loading) {
- return handleNotOkResult(i18n)(details);
+ if (details instanceof TalerError) {
+ return <ErrorLoading error={details} />
}
- const aml_history = details.loading ? [] : details.data.aml_history
- const kyc_attributes = details.loading ? [] : details.data.kyc_attributes
- const events = getEventsFromAmlHistory(aml_history,kyc_attributes);
-
+ if (details.type === "fail") {
+ switch (details.case) {
+ case "unauthorized":
+ case "officer-not-found":
+ case "officer-disabled": return <div />
+ default: assertUnreachable(details)
+ }
+ }
+ const { aml_history, kyc_attributes } = details.body
+
+ const events = getEventsFromAmlHistory(aml_history, kyc_attributes);
+
return (
<div>
<a
- href={Pages.newFormEntry.url({ account: paytoHash })}
+ href={Pages.newFormEntry.url({ account })}
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
>
New AML form
@@ -287,23 +289,22 @@ function ShowConsolidated({
},
Object.entries(cons.kyc).length > 0
? {
- title: "KYC" as TranslatedString,
- fields: Object.entries(cons.kyc).map(([key, field]) => {
- const result: UIFormField = {
- type: "text",
- props: {
- label: key as TranslatedString,
- name: `kyc.${key}.value`,
- help: `${field.provider} since ${
- field.since.t_ms === "never"
- ? "never"
- : format(field.since.t_ms, "dd/MM/yyyy")
+ title: "KYC" as TranslatedString,
+ fields: Object.entries(cons.kyc).map(([key, field]) => {
+ const result: UIFormField = {
+ type: "text",
+ props: {
+ label: key as TranslatedString,
+ name: `kyc.${key}.value`,
+ help: `${field.provider} since ${field.since.t_ms === "never"
+ ? "never"
+ : format(field.since.t_ms, "dd/MM/yyyy")
}` as TranslatedString,
- },
- };
- return result;
- }),
- }
+ },
+ };
+ return result;
+ }),
+ }
: undefined,
],
};
@@ -319,7 +320,7 @@ function ShowConsolidated({
key={`${String(Date.now())}`}
form={form}
initial={cons}
- onUpdate={() => {}}
+ onUpdate={() => { }}
/>
</Fragment>
);
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 990c0d2d4..5f79db71e 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -1,4 +1,5 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+import { TalerError, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util";
+import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { createNewForm } from "../handlers/forms.js";
@@ -7,34 +8,37 @@ import { useOfficer } from "../hooks/useOfficer.js";
import { Pages } from "../pages.js";
import { AmlExchangeBackend } from "../types.js";
import { amlStateConverter } from "./CaseDetails.js";
-import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
-import { buildQuerySignature } from "../account.js";
-import { handleNotOkResult } from "../utils/errors.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
export function Cases() {
- const officer = useOfficer();
const { i18n } = useTranslationContext();
- if (officer.state !== "ready") {
- return <HandleAccountNotReady officer={officer} />;
- }
- const form = createNewForm<{
- state: AmlExchangeBackend.AmlState;
- }>();
- const signature =
- officer.state === "ready"
- ? buildQuerySignature(officer.account.signingKey)
- : undefined;
+ const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>();
+
const initial = AmlExchangeBackend.AmlState.pending;
const [stateFilter, setStateFilter] = useState(initial);
- const list = useCases(officer.account.accountId, stateFilter, signature);
- if (!list.ok && !list.loading) {
- return handleNotOkResult(i18n)(list);
+ const list = useCases(stateFilter);
+
+ if (!list) {
+ return <Loading />
+ }
+
+ if (list instanceof TalerError) {
+ return <ErrorLoading error={list} />
}
- const records = list.loading ? [] : list.data.records
+
+ if (list.data.type === "fail") {
+ switch (list.data.case) {
+ case "unauthorized":
+ case "officer-not-found":
+ case "officer-disabled": return <div />
+ default: assertUnreachable(list.data)
+ }
+ }
+
+ const { records } = list.data.body
+
return (
<div>
<div class="px-4 sm:px-6 lg:px-8">
@@ -52,7 +56,7 @@ export function Cases() {
onUpdate={(v) => {
setStateFilter(v.state ?? initial);
}}
- onSubmit={(v) => {}}
+ onSubmit={(v) => { }}
>
<form.InputChoiceHorizontal
name="state"
diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
index 429cfb9ca..fa79bb476 100644
--- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
+++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
@@ -2,12 +2,11 @@ import { VNode, h } from "preact";
import { allForms } from "./AntiMoneyLaunderingForm.js";
import { Pages } from "../pages.js";
import { NiceForm } from "../NiceForm.js";
-import { AbsoluteTime, Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { AbsoluteTime, Amounts, TalerExchangeApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { AmlExchangeBackend } from "../types.js";
-import { useAmlCasesAPI } from "../hooks/useCaseDetails.js";
import { useOfficer } from "../hooks/useOfficer.js";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
-import { buildDecisionSignature, buildQuerySignature } from "../account.js";
+import { useExchangeApiContext } from "../context/config.js";
export function NewFormEntry({
account,
@@ -40,21 +39,21 @@ export function NewFormEntry({
state: AmlExchangeBackend.AmlState.pending,
threshold: Amounts.parseOrThrow("KUDOS:1000"),
};
- const api = useAmlCasesAPI()
-
+ const { api } = useExchangeApiContext()
+
return (
<NiceForm
initial={initial}
form={showingFrom(initial)}
onSubmit={(formValue) => {
if (formValue.state === undefined || formValue.threshold === undefined) return;
-
+
const justification = {
index: selectedForm,
name: formName,
value: formValue
}
- const decision: AmlExchangeBackend.AmlDecision = {
+ const decision: TalerExchangeApi.AmlDecision = {
justification: JSON.stringify(justification),
decision_time: TalerProtocolTimestamp.now(),
h_payto: account,
@@ -63,9 +62,9 @@ export function NewFormEntry({
officer_sig: "",
kyc_requirements: undefined
}
- const signature = buildDecisionSignature(officer.account.signingKey, decision);
- decision.officer_sig = signature
- api.updateDecision(officer.account.accountId, decision);
+ // const signature = buildDecisionSignature(officer.account.signingKey, decision);
+ // decision.officer_sig = signature
+ api.addDecisionDetails(officer.account, decision);
// alert(JSON.stringify(formValue));
}}
diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx
index 5320369e4..4af34805a 100644
--- a/packages/aml-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx
@@ -14,12 +14,12 @@ export function Officer() {
Public key
</h1>
<div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
- <p class="mt-6 font-mono break-all">{officer.account.accountId}</p>
+ <p class="mt-6 font-mono break-all">{officer.account.id}</p>
</div>
<p>
<a
href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent(
- `I want my AML account\n\n\nPubKey: ${officer.account.accountId}`,
+ `I want my AML account\n\n\nPubKey: ${officer.account.id}`,
)}`}
target="_blank"
rel="noreferrer"
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index 39f8addd3..83d8767fb 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -1,7 +1,6 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util";
import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { UnwrapKeyError } from "../account.js";
import { createNewForm } from "../handlers/forms.js";
export function UnlockAccount({
diff --git a/packages/aml-backoffice-ui/src/utils/errors.tsx b/packages/aml-backoffice-ui/src/utils/errors.tsx
deleted file mode 100644
index b67d61a5f..000000000
--- a/packages/aml-backoffice-ui/src/utils/errors.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
- ErrorType,
- HttpResponse,
- HttpResponsePaginated,
- notifyError,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { Loading } from "./Loading.js";
-import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
-import { AmlExchangeBackend } from "../types.js";
-
-export function handleNotOkResult<Error extends AmlExchangeBackend.AmlError>(
- i18n: ReturnType<typeof useTranslationContext>["i18n"],
-): <T>(
- result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>,
-) => VNode {
- return function handleNotOkResult2<T>(
- result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>,
- ): VNode {
- if (result.loading) return <Loading />;
- if (!result.ok) {
- switch (result.type) {
- case ErrorType.TIMEOUT: {
- notifyError(i18n.str`Request timeout, try again later.`, undefined);
- break;
- }
- case ErrorType.CLIENT: {
- if (result.status === HttpStatusCode.Unauthorized) {
- notifyError(i18n.str`Wrong credentials`, undefined);
- return <div> not authorized</div>;
- }
- const errorData = result.payload;
- notifyError(
- i18n.str`Could not load due to a client error`,
- errorData.hint as TranslatedString,
- JSON.stringify(result),
- );
- break;
- }
- case ErrorType.SERVER: {
- notifyError(
- i18n.str`Server returned with error`,
- result.payload.hint as TranslatedString,
- JSON.stringify(result.payload),
- );
- break;
- }
- case ErrorType.UNREADABLE: {
- notifyError(
- i18n.str`Unexpected error.`,
- `Response from ${result.info?.url} is unreadable, http status: ${result.status}` as TranslatedString,
- JSON.stringify(result),
- );
- break;
- }
- case ErrorType.UNEXPECTED: {
- notifyError(
- i18n.str`Unexpected error.`,
- `Diagnostic from ${result.info?.url} is "${result.message}"` as TranslatedString,
- JSON.stringify(result),
- );
- break;
- }
- default: {
- assertUnreachable(result);
- }
- }
-
- return <div>error</div>;
- }
- return <div />;
- };
-}
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}