aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-04-29 17:23:04 -0300
committerSebastian <sebasjm@gmail.com>2024-04-29 17:23:04 -0300
commit22709ff4e2918a8d0e528539d11d761381920b45 (patch)
tree7e01f9115ed44e5e3875e3473eb0d31314380d5a /packages/aml-backoffice-ui
parenteeabe64b3f0ac02818561ea6fca364d619f061b7 (diff)
downloadwallet-core-22709ff4e2918a8d0e528539d11d761381920b45.tar.xz
use exchange api type and start using ui_fields
Diffstat (limited to 'packages/aml-backoffice-ui')
-rw-r--r--packages/aml-backoffice-ui/src/forms/declaration.ts5
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts16
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts46
-rw-r--r--packages/aml-backoffice-ui/src/hooks/officer.ts12
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts6
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx104
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx185
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx107
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx180
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx7
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx125
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx99
-rw-r--r--packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx19
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx14
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx14
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx145
-rw-r--r--packages/aml-backoffice-ui/src/pages/index.stories.ts1
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts34
-rw-r--r--packages/aml-backoffice-ui/src/utils/types.ts124
19 files changed, 505 insertions, 738 deletions
diff --git a/packages/aml-backoffice-ui/src/forms/declaration.ts b/packages/aml-backoffice-ui/src/forms/declaration.ts
index fb7b8f334..c467f537b 100644
--- a/packages/aml-backoffice-ui/src/forms/declaration.ts
+++ b/packages/aml-backoffice-ui/src/forms/declaration.ts
@@ -14,12 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import type { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import type { AmountJson, TalerExchangeApi, TranslatedString } from "@gnu-taler/taler-util";
import type {
FlexibleForm,
InternationalizationAPI,
} from "@gnu-taler/web-util/browser";
-import { AmlExchangeBackend } from "../utils/types.js";
/**
* import entry point without hard reference.
@@ -32,7 +31,7 @@ import { AmlExchangeBackend } from "../utils/types.js";
*/
export interface BaseForm {
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
}
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index bd512546d..6455b6f41 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -13,13 +13,13 @@
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 type {
- TranslatedString
+import {
+ TalerExchangeApi,
+ type TranslatedString
} from "@gnu-taler/taler-util";
import type { DoubleColumnFormSection, FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
import { BaseForm } from "./declaration.js";
export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Simplest.Form> => ({
@@ -44,10 +44,9 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
return {
comment: {
help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as TranslatedString,
-
},
threshold: {
- disabled: v.state === AmlExchangeBackend.AmlState.frozen,
+ disabled: v.state === TalerExchangeApi.AmlState.frozen,
},
};
},
@@ -71,18 +70,17 @@ export function resolutionSection(current: BaseForm, i18n: InternationalizationA
props: {
name: "state",
label: i18n.str`New state`,
- converter: amlStateConverter,
choices: [
{
- value: AmlExchangeBackend.AmlState.frozen,
+ value: TalerExchangeApi.AmlState.frozen,
label: i18n.str`Frozen`,
},
{
- value: AmlExchangeBackend.AmlState.pending,
+ value: TalerExchangeApi.AmlState.pending,
label: i18n.str`Pending`,
},
{
- value: AmlExchangeBackend.AmlState.normal,
+ value: TalerExchangeApi.AmlState.normal,
label: i18n.str`Normal`,
},
],
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index e3d97db8c..e14e29819 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -14,29 +14,32 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ AmountJson,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
+import { UIField } from "@gnu-taler/web-util/browser";
-export type UIField = {
- value: string | undefined;
- onUpdate: (s: string) => void;
- error: TranslatedString | undefined;
-};
+// 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]>;
+ : T[k] extends TalerExchangeApi.AmlState
+ ? 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]>;
+ [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
};
export type RecursivePartial<T> = {
@@ -44,7 +47,9 @@ export type RecursivePartial<T> = {
? string
: T[k] extends AmountJson
? AmountJson
- : RecursivePartial<T[k]>;
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TalerExchangeApi.AmlState
+ : RecursivePartial<T[k]>;
};
export type FormErrors<T> = {
@@ -52,7 +57,9 @@ export type FormErrors<T> = {
? TranslatedString
: T[k] extends AmountJson
? TranslatedString
- : FormErrors<T[k]>;
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TranslatedString
+ : FormErrors<T[k]>;
};
export type FormStatus<T> =
@@ -76,10 +83,15 @@ function constructFormHandler<T>(
const handler = keys.reduce((prev, fieldName) => {
const currentValue: unknown = form[fieldName];
- const currentError: unknown = errors ? errors[fieldName] : undefined;
+ const currentError: unknown =
+ errors !== undefined ? errors[fieldName] : undefined;
function updater(newValue: unknown) {
updateForm({ ...form, [fieldName]: newValue });
}
+ /**
+ * There is no clear way to know if this object is a custom field
+ * or a group of fields
+ */
if (typeof currentValue === "object") {
// @ts-expect-error FIXME better typing
const group = constructFormHandler(currentValue, updater, currentError);
@@ -87,12 +99,14 @@ function constructFormHandler<T>(
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,
+ onChange: updater,
+ state: {},
};
// @ts-expect-error FIXME better typing
prev[fieldName] = field;
diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts
index dabe866d3..1bb73b8fc 100644
--- a/packages/aml-backoffice-ui/src/hooks/officer.ts
+++ b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -66,14 +66,14 @@ interface OfficerNotFound {
}
interface OfficerLocked {
state: "locked";
- forget: () => void;
- tryUnlock: (password: string) => Promise<void>;
+ forget: () => OperationOk<void>;
+ tryUnlock: (password: string) => Promise<OperationOk<void>>;
}
interface OfficerReady {
state: "ready";
account: OfficerAccount;
- forget: () => void;
- lock: () => void;
+ forget: () => OperationOk<void>;
+ lock: () => OperationOk<void>;
}
const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
@@ -133,6 +133,7 @@ export function useOfficer(): OfficerState {
state: "locked",
forget: () => {
officerStorage.reset();
+ return opFixedSuccess(undefined)
},
tryUnlock: async (pwd: string) => {
const ac = await unlockOfficerAccount(officer.account, pwd);
@@ -141,6 +142,7 @@ export function useOfficer(): OfficerState {
id: ac.id,
strKey: encodeCrock(ac.signingKey),
});
+ return opFixedSuccess(undefined)
},
};
}
@@ -150,10 +152,12 @@ export function useOfficer(): OfficerState {
account,
lock: () => {
accountStorage.reset();
+ return opFixedSuccess(undefined)
},
forget: () => {
officerStorage.reset();
accountStorage.reset();
+ return opFixedSuccess(undefined)
},
};
}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index 59d1c9001..d3a1c1018 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -19,11 +19,11 @@ import { useState } from "preact/hooks";
import {
OfficerAccount,
OperationOk,
+ TalerExchangeApi,
TalerExchangeResultByMethod,
TalerHttpError,
} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
-import { AmlExchangeBackend } from "../utils/types.js";
import { useOfficer } from "./officer.js";
import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
@@ -39,7 +39,7 @@ export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
* @param args
* @returns
*/
-export function useCases(state: AmlExchangeBackend.AmlState) {
+export function useCases(state: TalerExchangeApi.AmlState) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
const {
@@ -50,7 +50,7 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
async function fetcher([officer, state, offset]: [
OfficerAccount,
- AmlExchangeBackend.AmlState,
+ TalerExchangeApi.AmlState,
string | undefined,
]) {
return await api.getDecisionsByState(officer, state, {
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
deleted file mode 100644
index 0c82a4a0e..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import {
- AntiMoneyLaunderingForm as TestedComponent,
-} from "./AntiMoneyLaunderingForm.js";
-
-export default {
- title: "aml form",
-};
-
-export const SimpleComment = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "simple_comment",
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const Identification = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.1e",
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const OperationalLegalEntity = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.11e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const Foundations = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.12e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const DelcarationOfTrusts = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.13e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const InformationOnLifeInsurance = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.15e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.9e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const CustomerProfile = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.5e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const RiskProfile = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.4e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
deleted file mode 100644
index db034c996..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- 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 {
- AbsoluteTime,
- AmountJson,
- Amounts,
- Codec,
- OperationFail,
- OperationOk,
- TalerErrorDetail,
- buildCodecForObject,
- codecForNumber,
- codecForString,
- codecOptional,
-} from "@gnu-taler/taler-util";
-import {
- DefaultForm,
- useExchangeApiContext,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { h } from "preact";
-import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-import { privatePages } from "../Routing.js";
-
-export function AntiMoneyLaunderingForm({
- account,
- formId,
- onSubmit,
-}: {
- account: string;
- formId: string;
- onSubmit: (
- justification: Justification,
- state: AmlExchangeBackend.AmlState,
- threshold: AmountJson,
- ) => Promise<void>;
-}) {
- const { i18n } = useTranslationContext();
- const theForm = uiForms.forms(i18n).find((v) => v.id === formId);
- if (!theForm) {
- return <div>form with id {formId} not found</div>;
- }
-
- const { config } = useExchangeApiContext();
-
- const initial = {
- when: AbsoluteTime.now(),
- state: AmlExchangeBackend.AmlState.pending,
- threshold: Amounts.zeroOfCurrency(config.currency),
- };
- return (
- <DefaultForm
- initial={initial}
- form={theForm.impl(initial)}
- onUpdate={() => {}}
- onSubmit={(formValue) => {
- if (
- formValue.state === undefined ||
- formValue.threshold === undefined
- ) {
- return;
- }
- const validatedForm = formValue as BaseForm;
- const st = formValue.state;
- const amount = formValue.threshold;
-
- const justification: Justification = {
- id: theForm.id,
- label: theForm.label,
- version: theForm.version,
- value: validatedForm,
- };
-
- onSubmit(justification, st, amount);
- }}
- >
- <div class="mt-6 flex items-center justify-end gap-x-6">
- <a
- href={privatePages.caseDetails.url({ cid: account })}
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </button>
- </div>
- </DefaultForm>
- );
-}
-
-export type Justification<T extends BaseForm = BaseForm> = {
- // form values
- value: T;
-} & Omit<Omit<FormMetadata<BaseForm>, "icon">, "impl">;
-
-export function stringifyJustification(j: Justification): string {
- return JSON.stringify(j);
-}
-
-type SimpleFormMetadata = {
- version?: number;
- id?: string;
-};
-
-export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
- buildCodecForObject<SimpleFormMetadata>()
- .property("id", codecOptional(codecForString()))
- .property("version", codecOptional(codecForNumber()))
- .build("SimpleFormMetadata");
-
-type ParseJustificationFail =
- | "not-json"
- | "id-not-found"
- | "form-not-found"
- | "version-not-found";
-
-export function parseJustification(
- s: string,
- listOfAllKnownForms: FormMetadata<BaseForm>[],
-):
- | OperationOk<{
- justification: Justification;
- metadata: FormMetadata<BaseForm>;
- }>
- | OperationFail<ParseJustificationFail> {
- try {
- const justification = JSON.parse(s);
- const info = codecForSimpleFormMetadata().decode(justification);
- if (!info.id) {
- return {
- type: "fail",
- case: "id-not-found",
- detail: {} as TalerErrorDetail,
- };
- }
- if (!info.version) {
- return {
- type: "fail",
- case: "version-not-found",
- detail: {} as TalerErrorDetail,
- };
- }
- const found = listOfAllKnownForms.find((f) => {
- return f.id === info.id && f.version === info.version;
- });
- if (!found) {
- return {
- type: "fail",
- case: "form-not-found",
- detail: {} as TalerErrorDetail,
- };
- }
- return {
- type: "ok",
- body: {
- justification,
- metadata: found,
- },
- };
- } catch (e) {
- return {
- type: "fail",
- case: "not-json",
- detail: {} as TalerErrorDetail,
- };
- }
-}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 576cdbbb9..e16a6a103 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -17,10 +17,19 @@ import {
AbsoluteTime,
AmountJson,
Amounts,
+ Codec,
HttpStatusCode,
+ OperationFail,
+ OperationOk,
TalerError,
+ TalerErrorDetail,
+ TalerExchangeApi,
TranslatedString,
assertUnreachable,
+ buildCodecForObject,
+ codecForNumber,
+ codecForString,
+ codecOptional,
} from "@gnu-taler/taler-util";
import {
DefaultForm,
@@ -32,15 +41,10 @@ import {
import { format } from "date-fns";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { privatePages } from "../Routing.js";
import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js";
import { useCaseDetails } from "../hooks/useCaseDetails.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-import {
- Justification,
- parseJustification,
-} from "./AntiMoneyLaunderingForm.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
-import { privatePages } from "../Routing.js";
export type AmlEvent =
| AmlFormEvent
@@ -53,7 +57,7 @@ type AmlFormEvent = {
title: TranslatedString;
justification: Justification;
metadata: FormMetadata<BaseForm>;
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
};
type AmlFormEventError = {
@@ -62,7 +66,7 @@ type AmlFormEventError = {
title: TranslatedString;
justification: undefined;
metadata: undefined;
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
};
type KycCollectionEvent = {
@@ -108,8 +112,8 @@ function titleForJustification(
}
export function getEventsFromAmlHistory(
- aml: AmlExchangeBackend.AmlDecisionDetail[],
- kyc: AmlExchangeBackend.KycDetail[],
+ aml: TalerExchangeApi.AmlDecisionDetail[],
+ kyc: TalerExchangeApi.KycDetail[],
i18n: InternationalizationAPI,
): AmlEvent[] {
const ae: AmlEvent[] = aml.map((a) => {
@@ -242,24 +246,24 @@ export function CaseDetails({ account }: { account: string }) {
function AmlStateBadge({
state,
}: {
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
}): VNode {
switch (state) {
- case AmlExchangeBackend.AmlState.normal: {
+ case TalerExchangeApi.AmlState.normal: {
return (
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
Normal
</span>
);
}
- case AmlExchangeBackend.AmlState.pending: {
+ case TalerExchangeApi.AmlState.pending: {
return (
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
Pending
</span>
);
}
- case AmlExchangeBackend.AmlState.frozen: {
+ case TalerExchangeApi.AmlState.frozen: {
return (
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
Frozen
@@ -384,3 +388,78 @@ function ShowTimeline({
</div>
);
}
+
+
+export type Justification<T extends BaseForm = BaseForm> = {
+ // form values
+ value: T;
+} & Omit<Omit<FormMetadata<BaseForm>, "icon">, "impl">;
+
+type SimpleFormMetadata = {
+ version?: number;
+ id?: string;
+};
+
+export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
+ buildCodecForObject<SimpleFormMetadata>()
+ .property("id", codecOptional(codecForString()))
+ .property("version", codecOptional(codecForNumber()))
+ .build("SimpleFormMetadata");
+
+type ParseJustificationFail =
+ | "not-json"
+ | "id-not-found"
+ | "form-not-found"
+ | "version-not-found";
+
+function parseJustification(
+ s: string,
+ listOfAllKnownForms: FormMetadata<BaseForm>[],
+):
+ | OperationOk<{
+ justification: Justification;
+ metadata: FormMetadata<BaseForm>;
+ }>
+ | OperationFail<ParseJustificationFail> {
+ try {
+ const justification = JSON.parse(s);
+ const info = codecForSimpleFormMetadata().decode(justification);
+ if (!info.id) {
+ return {
+ type: "fail",
+ case: "id-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ if (!info.version) {
+ return {
+ type: "fail",
+ case: "version-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ const found = listOfAllKnownForms.find((f) => {
+ return f.id === info.id && f.version === info.version;
+ });
+ if (!found) {
+ return {
+ type: "fail",
+ case: "form-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ return {
+ type: "ok",
+ body: {
+ justification,
+ metadata: found,
+ },
+ };
+ } catch (e) {
+ return {
+ type: "fail",
+ case: "not-json",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index c4bff1f9f..47c8f8ab4 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -19,24 +19,27 @@ import {
HttpStatusCode,
TalerExchangeApi,
TalerProtocolTimestamp,
- TranslatedString,
+ assertUnreachable
} from "@gnu-taler/taler-util";
import {
+ Button,
LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
useExchangeApiContext,
- useLocalNotification,
- useTranslationContext,
+ useLocalNotificationHandler,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { privatePages } from "../Routing.js";
-import { uiForms } from "../forms/declaration.js";
+import { BaseForm, uiForms } from "../forms/declaration.js";
+import { useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
-import { AntiMoneyLaunderingForm } from "./AntiMoneyLaunderingForm.js";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { Justification } from "./CaseDetails.js";
export function CaseUpdate({
account,
- type,
+ type: formId,
}: {
account: string;
type: string;
@@ -46,67 +49,132 @@ export function CaseUpdate({
const {
lib: { exchange: api },
} = useExchangeApiContext();
- const [notification, notify, handleError] = useLocalNotification();
+
+ // const [notification, notify, handleError] = useLocalNotification();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { config } = useExchangeApiContext();
+
+ const initial = {
+ when: AbsoluteTime.now(),
+ state: TalerExchangeApi.AmlState.pending,
+ threshold: Amounts.zeroOfCurrency(config.currency),
+ };
if (officer.state !== "ready") {
return <HandleAccountNotReady officer={officer} />;
}
+ const theForm = uiForms.forms(i18n).find((v) => v.id === formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
+ const [form, state] = useFormState<BaseForm>(initial, (st) => {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ });
- <AntiMoneyLaunderingForm
- account={account}
- formId={type}
- onSubmit={async (justification, new_state, new_threshold) => {
- const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = {
- justification: JSON.stringify(justification),
- decision_time: TalerProtocolTimestamp.now(),
- h_payto: account,
- new_state,
- new_threshold: Amounts.stringify(new_threshold),
- kyc_requirements: undefined,
- };
- await handleError(async () => {
- const resp = await api.addDecisionDetails(
- officer.account,
- decision,
- );
- if (resp.type === "ok") {
- window.location.href = privatePages.cases.url({});
- return;
- }
- switch (resp.case) {
+ const ff = theForm.impl(state.result as any);
+
+ const validatedForm = state.status === "fail" ? undefined : state.result;
+
+ const submitHandler =
+ validatedForm === undefined
+ ? undefined
+ : withErrorHandler(
+ () => {
+ const justification: Justification = {
+ id: theForm.id,
+ label: theForm.label,
+ version: theForm.version,
+ value: validatedForm,
+ };
+
+ const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> =
+ {
+ justification: JSON.stringify(justification),
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: account,
+ new_state: justification.value.state,
+ new_threshold: Amounts.stringify(justification.value.threshold),
+ kyc_requirements: undefined,
+ };
+
+ return api.addDecisionDetails(officer.account, decision);
+ },
+ () => {
+ window.location.href = privatePages.cases.url({});
+ },
+ (fail) => {
+ switch (fail.case) {
case HttpStatusCode.Forbidden:
case HttpStatusCode.Unauthorized:
- return notify({
- type: "error",
- title: i18n.str`Wrong credentials for "${officer.account}"`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- });
+ return i18n.str`Wrong credentials for "${officer.account}"`;
case HttpStatusCode.NotFound:
- return notify({
- type: "error",
- title: i18n.str`Officer or account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- });
+ return i18n.str`Officer or account not found`;
case HttpStatusCode.Conflict:
- return notify({
- type: "error",
- title: i18n.str`Officer disabled or more recent decision was already submitted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- });
+ return i18n.str`Officer disabled or more recent decision was already submitted.`;
+ default:
+ assertUnreachable(fail);
}
- });
- }}
- />
+ },
+ );
+
+ // const asd = ff.design[0]?.fields[0]?.props
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {ff.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={section.fields}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <a
+ href={privatePages.caseDetails.url({ cid: account })}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <Button
+ type="submit"
+ handler={submitHandler}
+ class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </Button>
+ </div>
</Fragment>
);
}
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index dcbd366a4..22a6d1867 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -21,19 +21,18 @@
import * as tests from "@gnu-taler/web-util/testing";
import { CasesUI as TestedComponent } from "./Cases.js";
-import { AmountString } from "@gnu-taler/taler-util";
-import { AmlExchangeBackend } from "../utils/types.js";
+import { AmountString, TalerExchangeApi } from "@gnu-taler/taler-util";
export default {
title: "cases",
};
export const OneRow = tests.createExample(TestedComponent, {
- filter: AmlExchangeBackend.AmlState.normal,
+ filter: TalerExchangeApi.AmlState.normal,
onChangeFilter: () => null,
records: [
{
- current_state: AmlExchangeBackend.AmlState.normal,
+ current_state: TalerExchangeApi.AmlState.normal,
h_payto: "QWEQWEQWEQWE",
rowid: 1,
threshold: "USD:1" as AmountString,
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 6b59b2736..2e92c111e 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -22,19 +22,23 @@ import {
import {
Attention,
ErrorLoading,
+ InputChoiceHorizontal,
Loading,
- createNewForm,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import { useCases } from "../hooks/useCases.js";
import { privatePages } from "../Routing.js";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
+import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
+type FormType = {
+ state: TalerExchangeApi.AmlState;
+};
+
export function CasesUI({
records,
filter,
@@ -44,13 +48,45 @@ export function CasesUI({
}: {
onFirstPage?: () => void;
onNext?: () => void;
- filter: AmlExchangeBackend.AmlState;
- onChangeFilter: (f: AmlExchangeBackend.AmlState) => void;
+ filter: TalerExchangeApi.AmlState;
+ onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
records: TalerExchangeApi.AmlRecord[];
}): VNode {
const { i18n } = useTranslationContext();
- const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>();
+ const [form, status] = useFormState<FormType>(
+ {
+ state: filter,
+ },
+ (state) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ state: state.state === undefined ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ const result: FormType = {
+ state: state.state!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ state: state.state,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ },
+ );
+ useEffect(() => {
+ if (status.status === "ok" && filter !== status.result.state) {
+ onChangeFilter(status.result.state);
+ }
+ }, [form?.state?.value]);
return (
<div>
@@ -66,33 +102,25 @@ export function CasesUI({
</p>
</div>
<div class="px-2">
- <form.Provider
- initial={{ state: filter }}
- onUpdate={(v) => {
- onChangeFilter(v.state ?? filter);
- }}
- onSubmit={(_v) => {}}
- >
- <form.InputChoiceHorizontal
- name="state"
- label={i18n.str`Filter`}
- converter={amlStateConverter}
- choices={[
- {
- label: i18n.str`Pending`,
- value: AmlExchangeBackend.AmlState.pending,
- },
- {
- label: i18n.str`Frozen`,
- value: AmlExchangeBackend.AmlState.frozen,
- },
- {
- label: i18n.str`Normal`,
- value: AmlExchangeBackend.AmlState.normal,
- },
- ]}
- />
- </form.Provider>
+ <InputChoiceHorizontal<FormType, "state">
+ name="state"
+ label={i18n.str`Filter`}
+ handler={form.state}
+ choices={[
+ {
+ label: i18n.str`Pending`,
+ value: TalerExchangeApi.AmlState.pending,
+ },
+ {
+ label: i18n.str`Frozen`,
+ value: TalerExchangeApi.AmlState.frozen,
+ },
+ {
+ label: i18n.str`Normal`,
+ value: TalerExchangeApi.AmlState.normal,
+ },
+ ]}
+ />
</div>
</div>
<div class="mt-8 flow-root">
@@ -141,23 +169,23 @@ export function CasesUI({
</div>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
- {((state: AmlExchangeBackend.AmlState): VNode => {
+ {((state: TalerExchangeApi.AmlState): VNode => {
switch (state) {
- case AmlExchangeBackend.AmlState.normal: {
+ case TalerExchangeApi.AmlState.normal: {
return (
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
Normal
</span>
);
}
- case AmlExchangeBackend.AmlState.pending: {
+ case TalerExchangeApi.AmlState.pending: {
return (
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
Pending
</span>
);
}
- case AmlExchangeBackend.AmlState.frozen: {
+ case TalerExchangeApi.AmlState.frozen: {
return (
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
Frozen
@@ -186,7 +214,7 @@ export function CasesUI({
export function Cases() {
const [stateFilter, setStateFilter] = useState(
- AmlExchangeBackend.AmlState.pending,
+ TalerExchangeApi.AmlState.pending,
);
const list = useCases(stateFilter);
@@ -204,12 +232,10 @@ export function Cases() {
case HttpStatusCode.Forbidden: {
return (
<Fragment>
- <Attention
- type="danger"
- title={i18n.str`Operation denied`}
- >
+ <Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account doesnt have access. Request account activation sending your public key.
+ This account doesnt have access. Request account activation
+ sending your public key.
</i18n.Translate>
</Attention>
<Officer />
@@ -219,10 +245,7 @@ export function Cases() {
case HttpStatusCode.Unauthorized: {
return (
<Fragment>
- <Attention
- type="danger"
- title={i18n.str`Operation denied`}
- >
+ <Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
This account is not allowed to perform list the cases.
</i18n.Translate>
@@ -245,7 +268,9 @@ export function Cases() {
onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
onNext={list.isLastPage ? undefined : list.loadNext}
filter={stateFilter}
- onChangeFilter={setStateFilter}
+ onChangeFilter={(d) => {
+ setStateFilter(d)
+ }}
/>
);
}
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 094e78531..a8a853bc1 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -15,9 +15,9 @@
*/
import {
Button,
+ InputLine,
InternationalizationAPI,
LocalNotificationBanner,
- ShowInputErrorLabel,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
@@ -66,15 +66,23 @@ function createFormValidator(
});
if (errors === undefined) {
+ const result: FormType = {
+ password: state.password!,
+ repeat: state.repeat!,
+ };
return {
status: "ok",
- result: state as FormType,
+ result,
errors,
};
}
+ const result: RecursivePartial<FormType> = {
+ password: state.password,
+ repeat: state.repeat,
+ };
return {
status: "fail",
- result: state,
+ result,
errors,
};
};
@@ -88,13 +96,8 @@ export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
: undefined;
}
-export function CreateAccount({
- onNewAccount,
-}: {
- onNewAccount: () => void;
-}): VNode {
+export function CreateAccount(): VNode {
const { i18n } = useTranslationContext();
- // const Form = createNewForm<FormType>();
const [settings] = usePreferences();
const officer = useOfficer();
@@ -113,9 +116,9 @@ export function CreateAccount({
? undefined
: withErrorHandler(
async () => officer.create(form.password!.value!),
- onNewAccount,
+ () => {},
);
-
+ form.password;
return (
<div class="flex min-h-full flex-col ">
<LocalNotificationBanner notification={notification} />
@@ -137,66 +140,24 @@ export function CreateAccount({
autoCapitalize="none"
autoCorrect="off"
>
- <div>
- <label
- for="password"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- <i18n.Translate>Password</i18n.Translate>
- </label>
- <div class="mt-2">
- <input
- ref={doAutoFocus}
- type="text"
- name="password"
- id="password"
- class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- value={form.password?.value ?? ""}
- enterkeyhint="next"
- placeholder="strong password"
- autocomplete="password"
- title={i18n.str`Password`}
- required
- onChange={(e) => {
- console.log("ASDASD", form.password?.onUpdate);
- form.password?.onUpdate(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={form.password?.error}
- isDirty={form.password?.value !== undefined}
- />
- </div>
+ <div class="mt-2">
+ <InputLine<FormType, "password">
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ required
+ handler={form.password}
+ />
</div>
- <div>
- <label
- for="repeat"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- <i18n.Translate>Repeat password</i18n.Translate>
- </label>
- <div class="mt-2">
- <input
- type="text"
- name="repeat"
- id="repeat"
- class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- value={form.repeat?.value ?? ""}
- enterkeyhint="next"
- placeholder="identification"
- autocomplete="repeat"
- title={i18n.str`Repeat password`}
- required
- onChange={(e): void => {
- form.repeat?.onUpdate(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={form.repeat?.error}
- isDirty={form.repeat?.value !== undefined}
- />
- </div>
+ <div class="mt-2">
+ <InputLine<FormType, "repeat">
+ label={i18n.str`Repeat password`}
+ name="repeat"
+ type="password"
+ required
+ handler={form.repeat}
+ />
</div>
<div class="mt-8">
diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
index b23798172..3d6e14f22 100644
--- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -25,26 +25,11 @@ export function HandleAccountNotReady({
officer: OfficerNotReady;
}): VNode {
if (officer.state === "not-found") {
- return (
- <CreateAccount
- onNewAccount={(password) => {
- officer.create(password);
- }}
- />
- );
+ return <CreateAccount />;
}
if (officer.state === "locked") {
- return (
- <UnlockAccount
- onRemoveAccount={() => {
- officer.forget();
- }}
- onAccountUnlocked={async (pwd) => {
- await officer.tryUnlock(pwd);
- }}
- />
- );
+ return <UnlockAccount />;
}
assertUnreachable(officer);
}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
index 1cb50efd2..11b25575b 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AbsoluteTime, Duration, TranslatedString } from "@gnu-taler/taler-util";
+import { AbsoluteTime, AmountString, Duration, TranslatedString } from "@gnu-taler/taler-util";
import { InternationalizationAPI } from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
import { getEventsFromAmlHistory } from "./CaseDetails.js";
@@ -48,7 +48,7 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
{
"decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
"justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
+ "new_threshold": "STATER:0" as AmountString,
"new_state": 1,
"decision_time": {
"t_s": 1700208199
@@ -57,7 +57,7 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
{
"decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
"justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
+ "new_threshold": "STATER:0" as AmountString,
"new_state": 1,
"decision_time": {
"t_s": 1700208211
@@ -66,7 +66,7 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
{
"decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
"justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
+ "new_threshold": "STATER:0" as AmountString,
"new_state": 1,
"decision_time": {
"t_s": 1700208220
@@ -75,7 +75,7 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
{
"decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
"justification": "{\"index\":4,\"name\":\"Declaration for trusts (902.13e)\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700208362854},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"contractingPartner\":\"f\",\"knownAs\":\"a\",\"trust\":{\"name\":\"b\",\"type\":\"discretionary\",\"revocability\":\"irrevocable\"}}}",
- "new_threshold": "STATER:0",
+ "new_threshold": "STATER:0" as AmountString,
"new_state": 1,
"decision_time": {
"t_s": 1700208385
@@ -84,7 +84,7 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
{
"decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
"justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488420810},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"qwe\"}}",
- "new_threshold": "STATER:0",
+ "new_threshold": "STATER:0" as AmountString,
"new_state": 1,
"decision_time": {
"t_s": 1700488423
@@ -93,7 +93,7 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
{
"decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
"justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488671251},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"asd asd asd \"}}",
- "new_threshold": "STATER:0",
+ "new_threshold": "STATER:0" as AmountString,
"new_state": 1,
"decision_time": {
"t_s": 1700488677
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index c1f7e02cb..1115414c0 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -16,6 +16,7 @@
import {
AbsoluteTime,
AmountJson,
+ TalerExchangeApi,
TranslatedString,
} from "@gnu-taler/taler-util";
import {
@@ -26,8 +27,6 @@ import {
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
import { AmlEvent } from "./CaseDetails.js";
export function ShowConsolidated({
@@ -73,19 +72,18 @@ export function ShowConsolidated({
props: {
label: i18n.str`State`,
name: "aml.state",
- converter: amlStateConverter,
choices: [
{
label: i18n.str`Frozen`,
- value: AmlExchangeBackend.AmlState.frozen,
+ value: TalerExchangeApi.AmlState.frozen,
},
{
label: i18n.str`Pending`,
- value: AmlExchangeBackend.AmlState.pending,
+ value: TalerExchangeApi.AmlState.pending,
},
{
label: i18n.str`Normal`,
- value: AmlExchangeBackend.AmlState.normal,
+ value: TalerExchangeApi.AmlState.normal,
},
],
},
@@ -135,7 +133,7 @@ export function ShowConsolidated({
interface Consolidated {
aml: {
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
since: AbsoluteTime;
};
@@ -154,7 +152,7 @@ function getConsolidated(
): Consolidated {
const initial: Consolidated = {
aml: {
- state: AmlExchangeBackend.AmlState.normal,
+ state: TalerExchangeApi.AmlState.normal,
threshold: {
currency: "ARS",
value: 1000,
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index de634c9e0..9552f2b0c 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -13,81 +13,116 @@
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 { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util";
-import { createNewForm, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ Button,
+ InputLine,
+ LocalNotificationBanner,
+ useLocalNotificationHandler,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
+import { FormErrors, useFormState } from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
-export function UnlockAccount({
- onAccountUnlocked,
- onRemoveAccount,
-}: {
- onAccountUnlocked: (password: string) => Promise<void>;
- onRemoveAccount: () => void;
-}): VNode {
- const { i18n } = useTranslationContext()
- const Form = createNewForm<{
- password: string;
- }>();
+type FormType = {
+ password: string;
+};
+
+export function UnlockAccount(): VNode {
+ const { i18n } = useTranslationContext();
+
+ const officer = useOfficer();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const [form, status] = useFormState<FormType>(
+ {
+ password: undefined,
+ },
+ (state) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: state as FormType,
+ errors,
+ };
+ }
+ return {
+ status: "fail",
+ result: state,
+ errors,
+ };
+ },
+ );
+
+ const unlockHandler =
+ status.status === "fail" || officer.state !== "locked"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.tryUnlock(form.password!.value!),
+ () => {},
+ );
+
+ const forgetHandler =
+ status.status === "fail" || officer.state !== "locked"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.forget(),
+ () => {},
+ );
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Account locked</i18n.Translate>
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600">
- <i18n.Translate>Your account is normally locked anytime you reload. To unlock type
- your password again.</i18n.Translate>
+ <i18n.Translate>
+ Your account is normally locked anytime you reload. To unlock type
+ your password again.
+ </i18n.Translate>
</p>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- onSubmit={async (v) => {
- try {
- await onAccountUnlocked(v.password!);
- notifyInfo(i18n.str`Account unlocked`);
- } catch (e) {
- if (e instanceof UnwrapKeyError) {
- notifyError(
- i18n.str`Could not unlock account`,
- e.message as TranslatedString,
- );
- } else {
- throw e;
- }
- }
- }}
- >
- <div class="mb-4">
- <Form.InputLine
- label={i18n.str`Password`}
- name="password"
- type="password"
- required
- />
- </div>
- <div class="mt-8">
- <button
- type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Unlock</i18n.Translate>
- </button>
- </div>
- </Form.Provider>
+ <div class="mb-4">
+ <InputLine<FormType, "password">
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ required
+ handler={form.password}
+ />
+ </div>
+
+ <div class="mt-8">
+ <Button
+ type="submit"
+ handler={unlockHandler}
+ disabled={!unlockHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Unlock</i18n.Translate>
+ </Button>
+ </div>
+
</div>
- <button
+ <Button
type="button"
- onClick={() => {
- onRemoveAccount();
- }}
- class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ handler={forgetHandler}
+ disabled={!forgetHandler}
+ class="disabled:opacity-50 disabled:cursor-default m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
>
<i18n.Translate>Forget account</i18n.Translate>
- </button>
+ </Button>
</div>
</div>
);
diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts
index b2cbf485e..f11028de8 100644
--- a/packages/aml-backoffice-ui/src/pages/index.stories.ts
+++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts
@@ -14,5 +14,4 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
export * as a1 from "./ShowConsolidated.stories.js";
-export * as a2 from "./AntiMoneyLaunderingForm.stories.js";
export * as a3 from "./Cases.stories.js";
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
index d2f05ed84..cca764a81 100644
--- a/packages/aml-backoffice-ui/src/utils/converter.ts
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -1,30 +1,46 @@
-import { AmlExchangeBackend } from "./types.js";
+/*
+ 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 { TalerExchangeApi } from "@gnu-taler/taler-util";
export const amlStateConverter = {
toStringUI: stringifyAmlState,
fromStringUI: parseAmlState,
};
-function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string {
+function stringifyAmlState(s: TalerExchangeApi.AmlState | undefined): string {
if (s === undefined) return "";
switch (s) {
- case AmlExchangeBackend.AmlState.normal:
+ case TalerExchangeApi.AmlState.normal:
return "normal";
- case AmlExchangeBackend.AmlState.pending:
+ case TalerExchangeApi.AmlState.pending:
return "pending";
- case AmlExchangeBackend.AmlState.frozen:
+ case TalerExchangeApi.AmlState.frozen:
return "frozen";
}
}
-function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState {
+function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
switch (s) {
case "normal":
- return AmlExchangeBackend.AmlState.normal;
+ return TalerExchangeApi.AmlState.normal;
case "pending":
- return AmlExchangeBackend.AmlState.pending;
+ return TalerExchangeApi.AmlState.pending;
case "frozen":
- return AmlExchangeBackend.AmlState.frozen;
+ return TalerExchangeApi.AmlState.frozen;
default:
throw Error(`unknown AML state: ${s}`);
}
diff --git a/packages/aml-backoffice-ui/src/utils/types.ts b/packages/aml-backoffice-ui/src/utils/types.ts
deleted file mode 100644
index fd70d4e4d..000000000
--- a/packages/aml-backoffice-ui/src/utils/types.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-export namespace AmlExchangeBackend {
- // FIXME: placeholder
- export interface AmlError {
- code: number;
- hint: string;
- }
- export interface AmlDecisionDetails {
- // Array of AML decisions made for this account. Possibly
- // contains only the most recent decision if "history" was
- // not set to 'true'.
- aml_history: AmlDecisionDetail[];
-
- // Array of KYC attributes obtained for this account.
- kyc_attributes: KycDetail[];
- }
-
- type AmlOfficerPublicKeyP = string;
-
- export interface AmlDecisionDetail {
- // What was the justification given?
- justification: string;
-
- // What is the new AML state.
- new_state: Integer;
-
- // When was this decision made?
- decision_time: Timestamp;
-
- // What is the new AML decision threshold (in monthly transaction volume)?
- new_threshold: Amount;
-
- // Who made the decision?
- decider_pub: AmlOfficerPublicKeyP;
- }
- export interface KycDetail {
- // Name of the configuration section that specifies the provider
- // which was used to collect the KYC details
- provider_section: string;
-
- // The collected KYC data. NULL if the attribute data could not
- // be decrypted (internal error of the exchange, likely the
- // attribute key was changed).
- attributes?: Object;
-
- // Time when the KYC data was collected
- collection_time: Timestamp;
-
- // Time when the validity of the KYC data will expire
- expiration_time: Timestamp;
- }
-
- interface Timestamp {
- // Seconds since epoch, or the special
- // value "never" to represent an event that will
- // never happen.
- t_s: number | "never";
- }
-
- type PaytoHash = string;
- type Integer = number;
- type Amount = string;
- // EdDSA signatures are transmitted as 64-bytes base32
- // binary-encoded objects with just the R and S values (base32_ binary-only).
- type EddsaSignature = string;
-
- export interface AmlRecords {
- // Array of AML records matching the query.
- records: AmlRecord[];
- }
-
- interface AmlRecord {
- // Which payto-address is this record about.
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the current AML state.
- current_state: AmlState;
-
- // Monthly transaction threshold before a review will be triggered
- threshold: Amount;
-
- // RowID of the record.
- rowid: Integer;
- }
-
- export enum AmlState {
- normal = 0,
- pending = 1,
- frozen = 2,
- }
-
-
- export interface AmlDecision {
-
- // Human-readable justification for the decision.
- justification: string;
-
- // At what monthly transaction volume should the
- // decision be automatically reviewed?
- new_threshold: Amount;
-
- // Which payto-address is the decision about?
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the new AML state (e.g. frozen, unfrozen, etc.)
- // Numerical values are defined in AmlDecisionState.
- new_state: Integer;
-
- // Signature by the AML officer over a
- // TALER_MasterAmlOfficerStatusPS.
- // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
- officer_sig: EddsaSignature;
-
- // When was the decision made?
- decision_time: Timestamp;
-
- // Optional argument to impose new KYC requirements
- // that the customer has to satisfy to unblock transactions.
- kyc_requirements?: string[];
- }
-
-
-}