aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-09-06 13:54:33 -0300
committerSebastian <sebasjm@gmail.com>2024-09-06 13:54:58 -0300
commitc2efc802b2789cb47a6b0a54fc1672b98ee37db2 (patch)
treede5535184952ba20b77dd481ae152a077be8b547 /packages/aml-backoffice-ui/src
parentd671ef0f4cfa7d17b43b265501ae595882549f17 (diff)
downloadwallet-core-c2efc802b2789cb47a6b0a54fc1672b98ee37db2.tar.xz
search account in aml form and remove name from dynamic forms
Diffstat (limited to 'packages/aml-backoffice-ui/src')
-rw-r--r--packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx12
-rw-r--r--packages/aml-backoffice-ui/src/Routing.tsx6
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts9
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts19
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx6
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx151
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx12
-rw-r--r--packages/aml-backoffice-ui/src/pages/Search.tsx298
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx16
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx10
10 files changed, 436 insertions, 103 deletions
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
index ca7f9b0b5..b7741d4c7 100644
--- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
+++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -33,7 +33,12 @@ import {
getLabelForPreferences,
usePreferences,
} from "./hooks/preferences.js";
-import { HomeIcon, PeopleIcon, ToInvestigateIcon } from "./pages/Cases.js";
+import {
+ HomeIcon,
+ PeopleIcon,
+ SearchIcon,
+ ToInvestigateIcon,
+} from "./pages/Cases.js";
/**
* mapping route to view
@@ -236,6 +241,11 @@ function Navigation(): VNode {
label: i18n.str`Investigation`,
},
{ route: privatePages.active, Icon: HomeIcon, label: i18n.str`Active` },
+ {
+ route: privatePages.search,
+ Icon: SearchIcon,
+ label: i18n.str`Search`,
+ },
];
const { path } = useNavigationContext();
return (
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx
index b99314889..c7f9a40fe 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -31,6 +31,7 @@ import { Officer } from "./pages/Officer.js";
import { CaseDetails } from "./pages/CaseDetails.js";
import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js";
import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js";
+import { Search } from "./pages/Search.js";
export function Routing(): VNode {
const session = useOfficer();
@@ -95,6 +96,7 @@ function PublicRounting(): VNode {
export const privatePages = {
profile: urlPattern(/\/profile/, () => "#/profile"),
+ search: urlPattern(/\/search/, () => "#/search"),
investigation: urlPattern(/\/investigation/, () => "#/investigation"),
active: urlPattern(/\/active/, () => "#/active"),
caseUpdate: urlPattern<{ cid: string; type: string }>(
@@ -116,7 +118,6 @@ function PrivateRouting(): VNode {
const location = useCurrentLocation(privatePages);
useEffect(() => {
if (location.name === undefined) {
- console.log("asd")
navigateTo(privatePages.profile.url({}));
}
}, [location]);
@@ -145,6 +146,9 @@ function PrivateRouting(): VNode {
case "active": {
return <Cases />;
}
+ case "search": {
+ return <Search />;
+ }
default:
assertUnreachable(location);
}
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index 4cd781b74..215b0ba51 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -29,8 +29,7 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
fields: [
{
type: "textArea",
- id: ".comment" as UIHandlerId,
- name: "comment",
+ id: "comment" as UIHandlerId,
label: i18n.str`Comment`,
},
],
@@ -59,8 +58,7 @@ export function resolutionSection(
fields: [
{
type: "choiceHorizontal",
- id: ".state" as UIHandlerId,
- name: "state",
+ id: "state" as UIHandlerId,
label: i18n.str`New state`,
converterId: "TalerExchangeApi.AmlState",
choices: [
@@ -80,9 +78,8 @@ export function resolutionSection(
},
{
type: "amount",
- id: ".threshold" as UIHandlerId,
+ id: "threshold" as UIHandlerId,
currency: "NETZBON",
- name: "threshold",
converterId: "Taler.Amount",
label: i18n.str`New threshold`,
},
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index 70b2db571..375dbb190 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -126,14 +126,14 @@ export function useFormState<T>(
shape: Array<UIHandlerId>,
defaultValue: RecursivePartial<FormValues<T>>,
check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
-): [FormHandler<T>, FormStatus<T>] {
+): { handler: FormHandler<T>; status: FormStatus<T> } {
const [form, updateForm] =
useState<RecursivePartial<FormValues<T>>>(defaultValue);
const status = check(form);
const handler = constructFormHandler(shape, form, updateForm, status.errors);
- return [handler, status];
+ return { handler, status };
}
interface Tree<T> extends Record<string, Tree<T> | T> {}
@@ -163,7 +163,10 @@ export function setValueDeeper(object: any, names: string[], value: any): any {
if (object === undefined) {
return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
}
- return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) });
+ return undefinedIfEmpty({
+ ...object,
+ [head]: setValueDeeper(object[head] ?? {}, rest, value),
+ });
}
export function getShapeFromFields(
@@ -179,10 +182,7 @@ export function getShapeFromFields(
}
shape.push(field.id);
} else if (field.type === "group") {
- Array.prototype.push.apply(
- shape,
- getShapeFromFields(field.fields),
- );
+ Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
}
});
return shape;
@@ -204,10 +204,7 @@ export function getRequiredFields(
}
shape.push(field.id);
} else if (field.type === "group") {
- Array.prototype.push.apply(
- shape,
- getRequiredFields(field.fields),
- );
+ Array.prototype.push.apply(shape, getRequiredFields(field.fields));
}
});
return shape;
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index 2708dba4c..87f1aed5f 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -117,7 +117,7 @@ export function CaseUpdate({
);
});
- const [form, state] = useFormState<FormType>(shape, initial, (st) => {
+ const { handler, status } = useFormState<FormType>(shape, initial, (st) => {
const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({
state: st.state === undefined ? i18n.str`required` : undefined,
threshold: !st.threshold ? i18n.str`required` : undefined,
@@ -143,7 +143,7 @@ export function CaseUpdate({
};
});
- const validatedForm = state.status !== "ok" ? undefined : state.result;
+ const validatedForm = status.status !== "ok" ? undefined : status.result;
const submitHandler =
validatedForm === undefined
@@ -224,7 +224,7 @@ export function CaseUpdate({
fields={convertUiField(
i18n,
section.fields,
- form,
+ handler,
getConverterById,
)}
/>
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index c229850b1..c7191332a 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -21,22 +21,18 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
- ErrorLoading,
- InputChoiceHorizontal,
Loading,
- UIHandlerId,
- amlStateConverter,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useCurrentDecisions, useCurrentDecisionsUnderInvestigation } from "../hooks/decisions.js";
+import {
+ useCurrentDecisions,
+ useCurrentDecisionsUnderInvestigation,
+} from "../hooks/decisions.js";
import { privatePages } from "../Routing.js";
-import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
-import { undefinedIfEmpty } from "./CreateAccount.js";
-import { Officer } from "./Officer.js";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
-import { useState } from "preact/hooks";
+import { Officer } from "./Officer.js";
type FormType = {
// state: TalerExchangeApi.AmlState;
@@ -48,7 +44,7 @@ export function CasesUI({
onNext,
filtered,
}: {
- filtered: boolean,
+ filtered: boolean;
onFirstPage?: () => void;
onNext?: () => void;
records: TalerExchangeApi.AmlDecision[];
@@ -93,17 +89,20 @@ export function CasesUI({
return (
<div>
<div class="sm:flex sm:items-center">
- {filtered ?
+ {filtered ? (
<div class="px-2 sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Cases under investigation</i18n.Translate>
</h1>
<p class="mt-2 text-sm text-gray-700 w-80">
<i18n.Translate>
- A list of all the accounts which are waiting for a deicison to be made.
+ A list of all the accounts which are waiting for a deicison to
+ be made.
</i18n.Translate>
</p>
- </div> : <div class="px-2 sm:flex-auto">
+ </div>
+ ) : (
+ <div class="px-2 sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Cases</i18n.Translate>
</h1>
@@ -113,7 +112,7 @@ export function CasesUI({
</i18n.Translate>
</p>
</div>
- }
+ )}
</div>
<div class="mt-8 flow-root">
<div class="overflow-x-auto">
@@ -155,7 +154,11 @@ export function CasesUI({
</div>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
- {r.to_investigate ? <span title="require investigation"><ToInvestigateIcon /></span> : undefined}
+ {r.to_investigate ? (
+ <span title="require investigation">
+ <ToInvestigateIcon />
+ </span>
+ ) : undefined}
</td>
</tr>
);
@@ -189,7 +192,8 @@ export function Cases() {
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account signature is wrong, contact administrator or create a new one.
+ This account signature is wrong, contact administrator or create
+ a new one.
</i18n.Translate>
</Attention>
<Officer />
@@ -200,27 +204,26 @@ export function Cases() {
return (
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This account is not known.
- </i18n.Translate>
- </Attention>
- <Officer />
- </Fragment>
- );
- }
- case HttpStatusCode.Conflict: {
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This account doesn't have access. Request account activation
- sending your public key.
- </i18n.Translate>
+ <i18n.Translate>This account is not known.</i18n.Translate>
</Attention>
<Officer />
</Fragment>
);
}
+ case HttpStatusCode.Conflict:
+ {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
return <Officer />;
default:
assertUnreachable(list);
@@ -233,10 +236,10 @@ export function Cases() {
records={list.body}
onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
onNext={list.isLastPage ? undefined : list.loadNext}
- // filter={stateFilter}
- // onChangeFilter={(d) => {
- // setStateFilter(d);
- // }}
+ // filter={stateFilter}
+ // onChangeFilter={(d) => {
+ // setStateFilter(d);
+ // }}
/>
);
}
@@ -258,7 +261,8 @@ export function CasesUnderInvestigation() {
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account signature is wrong, contact administrator or create a new one.
+ This account signature is wrong, contact administrator or create
+ a new one.
</i18n.Translate>
</Attention>
<Officer />
@@ -269,27 +273,26 @@ export function CasesUnderInvestigation() {
return (
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This account is not known.
- </i18n.Translate>
- </Attention>
- <Officer />
- </Fragment>
- );
- }
- case HttpStatusCode.Conflict: {
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This account doesn't have access. Request account activation
- sending your public key.
- </i18n.Translate>
+ <i18n.Translate>This account is not known.</i18n.Translate>
</Attention>
<Officer />
</Fragment>
);
}
+ case HttpStatusCode.Conflict:
+ {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
return <Officer />;
default:
assertUnreachable(list);
@@ -302,10 +305,10 @@ export function CasesUnderInvestigation() {
records={list.body}
onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
onNext={list.isLastPage ? undefined : list.loadNext}
- // filter={stateFilter}
- // onChangeFilter={(d) => {
- // setStateFilter(d);
- // }}
+ // filter={stateFilter}
+ // onChangeFilter={(d) => {
+ // setStateFilter(d);
+ // }}
/>
);
}
@@ -316,12 +319,23 @@ export function CasesUnderInvestigation() {
// </svg>
// }
export const ToInvestigateIcon = () => (
- <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+ <svg
+ title="requires investigation"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6 w-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
+ />
</svg>
);
-
export const PeopleIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -356,6 +370,23 @@ export const HomeIcon = () => (
</svg>
);
+export const SearchIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
+ />
+ </svg>
+);
+
function Pagination({
onFirstPage,
onNext,
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 87310aa27..328d8459b 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -89,7 +89,9 @@ function createFormValidator(
};
}
-export function undefinedIfEmpty<T extends object | undefined>(obj: T): T | undefined {
+export function undefinedIfEmpty<T extends object | undefined>(
+ obj: T,
+): T | undefined {
if (obj === undefined) return undefined;
return Object.keys(obj).some(
(k) => (obj as Record<string, T>)[k] !== undefined,
@@ -105,7 +107,7 @@ export function CreateAccount(): VNode {
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const [form, status] = useFormState<FormType>(
+ const { handler, status } = useFormState<FormType>(
[".password", ".repeat"] as Array<UIHandlerId>,
{
password: undefined,
@@ -118,7 +120,7 @@ export function CreateAccount(): VNode {
status.status === "fail" || officer.state !== "not-found"
? undefined
: withErrorHandler(
- async () => officer.create(form.password!.value!),
+ async () => officer.create(handler.password!.value!),
() => {},
);
return (
@@ -148,7 +150,7 @@ export function CreateAccount(): VNode {
name="password"
type="password"
required
- handler={form.password}
+ handler={handler.password}
/>
</div>
@@ -158,7 +160,7 @@ export function CreateAccount(): VNode {
name="repeat"
type="password"
required
- handler={form.repeat}
+ handler={handler.repeat}
/>
</div>
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx
new file mode 100644
index 000000000..047e56180
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -0,0 +1,298 @@
+/*
+ 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 {
+ convertUiField,
+ getConverterById,
+ InternationalizationAPI,
+ RenderAllFieldsByUiConfig,
+ UIFormElementConfig,
+ UIHandlerId,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ getShapeFromFields,
+ RecursivePartial,
+ useFormState,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+interface FormType {
+ paytoType: "generic" | "iban" | "x-taler-bank";
+}
+
+export function Search() {
+ const officer = useOfficer();
+ const { i18n } = useTranslationContext();
+
+ const paytoForm = useFormState(
+ getShapeFromFields(paytoTypeField(i18n)),
+ { paytoType: "generic" },
+ createFormValidator(i18n),
+ );
+
+ const secondFieldSet =
+ paytoForm.status.status !== "ok"
+ ? []
+ : paytoForm.status.result.paytoType === "iban"
+ ? ibanFields(i18n)
+ : paytoForm.status.result.paytoType === "x-taler-bank"
+ ? talerBankFields(i18n)
+ : genericFields(i18n);
+
+ const secondForm = useFormState(
+ getShapeFromFields(secondFieldSet),
+ {},
+ createFormValidator(i18n),
+ );
+
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ return (
+ <div>
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ <i18n.Translate>Search account</i18n.Translate>
+ </h1>
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(
+ i18n,
+ paytoTypeField(i18n),
+ paytoForm.handler,
+ getConverterById,
+ )}
+ />
+ </div>
+ </form>
+
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(
+ i18n,
+ secondFieldSet,
+ secondForm.handler,
+ getConverterById,
+ )}
+ />
+ </div>
+ </form>
+ </div>
+ );
+}
+
+function createFormValidator(i18n: InternationalizationAPI) {
+ return function check(
+ state: RecursivePartial<FormValues<FormType>>,
+ ): FormStatus<FormType> {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ paytoType: !state?.paytoType ? i18n.str`required` : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: FormType = {
+ paytoType: state.paytoType! as any,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ paytoType: state?.paytoType,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+const paytoTypeField: (
+ i18n: InternationalizationAPI,
+) => UIFormElementConfig[] = (i18n) => [
+ {
+ id: "paytoType" as UIHandlerId,
+ type: "choiceHorizontal",
+ required: true,
+ choices: [
+ {
+ value: "generic",
+ label: i18n.str`Generic Payto:// URI`,
+ },
+ {
+ value: "iban",
+ label: i18n.str`IBAN`,
+ },
+ {
+ value: "x-taler-bank",
+ label: i18n.str`Taler Bank`,
+ },
+ ],
+ label: `Account type`,
+ },
+];
+
+const receiverName: (i18n: InternationalizationAPI) => UIFormElementConfig = (
+ i18n,
+) => ({
+ id: "receiverName" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: `Owner's name`,
+ help: i18n.str`It should match the bank account name.`,
+ placeholder: i18n.str`John Doe`,
+});
+
+const genericFields: (
+ i18n: InternationalizationAPI,
+) => UIFormElementConfig[] = (i18n) => [
+ {
+ id: "paytoText" as UIHandlerId,
+ type: "textArea",
+ required: true,
+ label: `Payto URI`,
+ help: i18n.str`As defined by RFC 8905`,
+ placeholder: i18n.str`payto://`,
+ },
+ receiverName(i18n),
+];
+const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = (
+ i18n,
+) => [
+ {
+ id: "account" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: `Account`,
+ help: i18n.str`International Bank Account Number`,
+ placeholder: i18n.str`DE1231231231`,
+ validator: (value) => validateIBAN(value, i18n),
+ },
+ receiverName(i18n),
+];
+
+const talerBankFields: (
+ i18n: InternationalizationAPI,
+) => UIFormElementConfig[] = (i18n) => [
+ {
+ id: "account" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: `Bank account`,
+ help: i18n.str`Bank account id`,
+ placeholder: i18n.str`DE123123123`,
+ },
+ {
+ id: "hostname" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: `Hostname`,
+ validator: (value) => validateTalerBank(value, i18n),
+ help: i18n.str`Without the scheme, may include subpath: bank.com, bank.com/path/`,
+ placeholder: i18n.str`bank.demo.taler.net`,
+ },
+ receiverName(i18n),
+];
+
+function validateIBAN(
+ iban: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString | undefined {
+ // Check total length
+ if (iban.length < 4)
+ return i18n.str`IBAN numbers usually have more that 4 digits`;
+ if (iban.length > 34)
+ return i18n.str`IBAN numbers usually have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = iban.toUpperCase();
+
+ // check supported country
+ // const code = IBAN.substr(0, 2);
+ // const found = code in COUNTRY_TABLE;
+ // if (!found) return i18n.str`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substr(4) + iban.substr(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substr(0, 5);
+ const rest = str.substr(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+ }
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1)
+ return i18n.str`IBAN number is invalid, checksum is wrong`;
+ return undefined;
+}
+
+const DOMAIN_REGEX =
+ /^[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9-_](?:\.[a-zA-Z0-9-_]{2,})+(:[0-9]+)?(\/[a-zA-Z0-9-.]+)*\/?$/;
+
+function validateTalerBank(
+ addr: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ try {
+ const valid = DOMAIN_REGEX.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid host.`;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index 21c14fee3..7374125b0 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -20,7 +20,6 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import {
- DefaultForm,
FormConfiguration,
RenderAllFieldsByUiConfig,
UIFormElementConfig,
@@ -31,8 +30,8 @@ import {
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
-import { AmlEvent } from "./CaseDetails.js";
import { getShapeFromFields, useFormState } from "../hooks/form.js";
+import { AmlEvent } from "./CaseDetails.js";
/**
* the exchange doesn't hava a consistent api
@@ -78,7 +77,6 @@ export function ShowConsolidated({
type: "text",
label: key as TranslatedString,
id: `${key}.value` as UIHandlerId,
- name: `${key}.value`,
disabled: true,
help: `At ${
field.since.t_ms === "never"
@@ -92,13 +90,11 @@ export function ShowConsolidated({
: undefined!,
],
};
- const shape: Array<UIHandlerId> = [];
-
- formConfig.design.forEach((section) => {
- Array.prototype.push.apply(shape, getShapeFromFields(section.fields));
- });
+ const shape: Array<UIHandlerId> = formConfig.design.flatMap((field) =>
+ getShapeFromFields(field.fields),
+ );
- const [form, state] = useFormState<{}>(shape, fixed, (result) => {
+ const { handler } = useFormState<{}>(shape, fixed, (result) => {
return { status: "ok", errors: undefined, result };
});
@@ -130,7 +126,7 @@ export function ShowConsolidated({
fields={convertUiField(
i18n,
section.fields,
- form,
+ handler,
getConverterById,
)}
/>
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index 084e639bf..72656bb98 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -19,7 +19,7 @@ import {
LocalNotificationBanner,
UIHandlerId,
useLocalNotificationHandler,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { FormErrors, useFormState } from "../hooks/form.js";
@@ -36,7 +36,7 @@ export function UnlockAccount(): VNode {
const officer = useOfficer();
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const [form, status] = useFormState<FormType>(
+ const { handler, status } = useFormState<FormType>(
[".password"] as Array<UIHandlerId>,
{
password: undefined,
@@ -64,7 +64,7 @@ export function UnlockAccount(): VNode {
status.status === "fail" || officer.state !== "locked"
? undefined
: withErrorHandler(
- async () => officer.tryUnlock(form.password!.value!),
+ async () => officer.tryUnlock(handler.password!.value!),
() => {},
);
@@ -94,14 +94,13 @@ export function UnlockAccount(): VNode {
<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">
-
<div class="mb-4">
<InputLine<FormType, "password">
label={i18n.str`Password`}
name="password"
type="password"
required
- handler={form.password}
+ handler={handler.password}
/>
</div>
@@ -115,7 +114,6 @@ export function UnlockAccount(): VNode {
<i18n.Translate>Unlock</i18n.Translate>
</Button>
</div>
-
</div>
<Button
type="button"