[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-typescript-core] branch master updated: fix #9736
From: |
Admin |
Subject: |
[taler-typescript-core] branch master updated: fix #9736 |
Date: |
Thu, 12 Jun 2025 21:20:45 +0200 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository taler-typescript-core.
The following commit(s) were added to refs/heads/master by this push:
new cbf261e2b fix #9736
cbf261e2b is described below
commit cbf261e2b81b4db6a0e55620a8e85b70f21bb52e
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Jun 12 10:19:26 2025 -0300
fix #9736
---
.../aml-backoffice-ui/src/pages/NewMeasure.tsx | 783 +++++++++++++++------
.../src/pages/decision/Events.tsx | 2 +-
.../src/pages/decision/Justification.tsx | 2 +-
.../src/pages/decision/Measures.tsx | 238 +++++--
.../src/pages/decision/Properties.tsx | 2 +-
.../aml-backoffice-ui/src/pages/decision/Rules.tsx | 4 +-
packages/challenger-ui/src/pages/AskChallenge.tsx | 16 +-
packages/web-util/src/forms/forms-types.ts | 2 +-
.../web-util/src/forms/gana/challenger_email.ts | 22 +-
.../web-util/src/forms/gana/challenger_postal.ts | 52 +-
packages/web-util/src/forms/gana/challenger_sms.ts | 22 +-
packages/web-util/src/hooks/useForm.ts | 2 +-
12 files changed, 806 insertions(+), 341 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx
b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx
index edc8688c3..47c905c4b 100644
--- a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx
+++ b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx
@@ -1,5 +1,6 @@
import {
AmlProgramRequirement,
+ assertUnreachable,
AvailableMeasureSummary,
KycCheckInformation,
KycRule,
@@ -7,16 +8,23 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import {
+ design_challenger_email,
+ design_challenger_phone,
+ design_challenger_postal,
ErrorsSummary,
+ form_challenger_email,
FormDesign,
FormUI,
+ InputToggle,
InternationalizationAPI,
+ RecursivePartial,
useForm,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useCurrentDecisionRequest } from "../hooks/decision-request.js";
import { useServerMeasures } from "../hooks/server-info.js";
+import { useState } from "preact/hooks";
export type MeasureDefinition = {
name: string;
@@ -29,6 +37,12 @@ export type MeasureDefinition = {
}[];
};
+type VerificationMeasureDefinition = {
+ name: string;
+ readOnly: boolean;
+ address: any;
+};
+
/**
* Defined new limits for the account
* @param param0
@@ -78,7 +92,7 @@ export function NewMeasure({
);
}
-export function MeasureForm({
+function NormalMeasureForm({
summary,
onCancel,
onAdded,
@@ -94,9 +108,9 @@ export function MeasureForm({
onAdded: (name: string) => void;
onChanged: (name: string) => void;
onRemoved: (name: string) => void;
-}) {
- const { i18n } = useTranslationContext();
+}): VNode {
const [request, updateRequest] = useCurrentDecisionRequest();
+ const { i18n } = useTranslationContext();
const names = {
measures: Object.entries(summary.roots).map(([key, value]) => ({
@@ -125,37 +139,64 @@ export function MeasureForm({
const name = !form.status.result ? undefined : form.status.result.name;
- const program =
- !form.status.result ||
- !form.status.result.program ||
- !summary.programs[form.status.result.program]
- ? undefined
- : {
- ...summary.programs[form.status.result.program],
- name: form.status.result.program,
- };
+ function addNewCustomMeasure() {
+ const newMeasure = form.status.result as MeasureDefinition;
+ const currentMeasures = { ...request.custom_measures };
+ currentMeasures[newMeasure.name] = {
+ check_name: newMeasure.check,
+ prog_name: newMeasure.program,
+ context: (newMeasure.context ?? []).reduce(
+ (prev, cur) => {
+ prev[cur.key] = getContextValueByType(cur.type, cur.value);
+ return prev;
+ },
+ {} as Record<string, any>,
+ ),
+ };
+ updateRequest("add new measure", {
+ custom_measures: currentMeasures,
+ });
+ if (onAdded) {
+ onAdded(newMeasure.name);
+ }
+ }
- const check =
- !form.status.result ||
- !form.status.result.check ||
- !summary.checks[form.status.result.check]
- ? undefined
- : {
- ...summary.checks[form.status.result.check],
- name: form.status.result.check,
- };
+ function updateCurrentCustomMeasure() {
+ const newMeasure = form.status.result as MeasureDefinition;
- const context =
- !form.status.result || !form.status.result.context
- ? []
- : (form.status.result.context as MeasureDefinition["context"]);
+ const CURRENT_MEASURES = { ...request.custom_measures };
+ CURRENT_MEASURES[newMeasure.name] = {
+ check_name: newMeasure.check,
+ prog_name: newMeasure.program,
+ context: (newMeasure.context ?? []).reduce(
+ (prev, cur) => {
+ prev[cur.key] = getContextValueByType(cur.type, cur.value);
+ return prev;
+ },
+ {} as Record<string, any>,
+ ),
+ };
+ updateRequest("update measure", {
+ custom_measures: CURRENT_MEASURES,
+ });
+ if (onChanged) {
+ onChanged(newMeasure.name);
+ }
+ }
- return (
- <div>
- <h2 class="mt-4 mb-2">
- <i18n.Translate>Add measure</i18n.Translate>
- </h2>
+ function removeCustomMeasure() {
+ const currentMeasures = { ...request.custom_measures };
+ delete currentMeasures[name!];
+ updateRequest("remove measure", {
+ custom_measures: currentMeasures,
+ });
+ if (onRemoved) {
+ onRemoved(name!);
+ }
+ }
+ return (
+ <Fragment>
<FormUI design={design} model={form.model} />
<button
@@ -170,27 +211,7 @@ export function MeasureForm({
{addingNew ? (
<button
disabled={form.status.status === "fail"}
- onClick={() => {
- const newMeasure = form.status.result as MeasureDefinition;
- const currentMeasures = { ...request.custom_measures };
- currentMeasures[newMeasure.name] = {
- check_name: newMeasure.check,
- prog_name: newMeasure.program,
- context: (newMeasure.context ?? []).reduce(
- (prev, cur) => {
- prev[cur.key] = getContextValueByType(cur.type, cur.value);
- return prev;
- },
- {} as Record<string, any>,
- ),
- };
- updateRequest("add new measure", {
- custom_measures: currentMeasures,
- });
- if (onAdded) {
- onAdded(newMeasure.name);
- }
- }}
+ onClick={addNewCustomMeasure}
class="m-4 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 disabled:bg-gray-600"
>
<i18n.Translate>Add</i18n.Translate>
@@ -199,43 +220,14 @@ export function MeasureForm({
<Fragment>
<button
disabled={form.status.status === "fail"}
- onClick={() => {
- const newMeasure = form.status.result as MeasureDefinition;
-
- const CURRENT_MEASURES = { ...request.custom_measures };
- CURRENT_MEASURES[newMeasure.name] = {
- check_name: newMeasure.check,
- prog_name: newMeasure.program,
- context: (newMeasure.context ?? []).reduce(
- (prev, cur) => {
- prev[cur.key] = getContextValueByType(cur.type, cur.value);
- return prev;
- },
- {} as Record<string, any>,
- ),
- };
- updateRequest("update measure", {
- custom_measures: CURRENT_MEASURES,
- });
- if (onChanged) {
- onChanged(newMeasure.name);
- }
- }}
+ onClick={updateCurrentCustomMeasure}
class="m-4 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
disabled:bg-gray-600"
>
<i18n.Translate>Update</i18n.Translate>
</button>
+
<button
- onClick={() => {
- const currentMeasures = { ...request.custom_measures };
- delete currentMeasures[name!];
- updateRequest("remove measure", {
- custom_measures: currentMeasures,
- });
- if (onRemoved) {
- onRemoved(name!);
- }
- }}
+ onClick={removeCustomMeasure}
class="m-4 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
disabled:bg-gray-600"
>
<i18n.Translate>Remove</i18n.Translate>
@@ -243,123 +235,287 @@ export function MeasureForm({
</Fragment>
)}
- <h2 class="mt-4 mb-2">
- <i18n.Translate>Description</i18n.Translate>
- </h2>
+ <DescribeMeasure measure={form.status.result} summary={summary} />
+ </Fragment>
+ );
+}
+function VerificationMeasureForm({
+ summary,
+ onCancel,
+ onAdded,
+ onChanged,
+ onRemoved,
+ initial,
+ addingNew,
+ challengeType,
+}: {
+ initial?: Partial<MeasureDefinition>;
+ addingNew?: boolean;
+ summary: AvailableMeasureSummary;
+ onCancel: () => void;
+ onAdded: (name: string) => void;
+ onChanged: (name: string) => void;
+ onRemoved: (name: string) => void;
+ challengeType: "email" | "phone" | "postal";
+}): VNode {
+ const [request, updateRequest] = useCurrentDecisionRequest();
+ const { i18n } = useTranslationContext();
- {!program ? undefined : (
- <div class="rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700
border ring-gray-900/5 ">
- <dl class="flex flex-wrap">
- <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg">
- <dt class="text-sm/6 text-white">
- <i18n.Translate>Program</i18n.Translate>
- </dt>
- <dd class="mt-1 text-base font-semibold text-white">
- {program.name}
- </dd>
- </div>
- <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
- <dt class="flex-none text-gray-500">
- <i18n.Translate>Description</i18n.Translate>
- </dt>
- <dd class="text-sm/6 ">
- <i18n.Translate>{program.description}</i18n.Translate>
- </dd>
- </div>
- <div class="mt-2 flex w-full flex-none gap-x-4 border-t
border-gray-900/5 px-6 pt-2">
- <dt class="flex-none text-gray-500">
- <i18n.Translate>Context</i18n.Translate>
- </dt>
- <dd class="text-sm/6 font-medium text-gray-900">
- <pre>{program.context.join(",")}</pre>
- </dd>
- </div>
- <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
- <dt class="flex-none text-gray-500">
- <i18n.Translate>Inputs</i18n.Translate>
- </dt>
- <dd class="text-sm/6 ">
- <pre class="whitespace-pre-wrap">
- {program.inputs.join(",")}
- </pre>
- </dd>
- </div>
- </dl>
- <div class="px-4 pb-2"></div>
- </div>
+ const design = verificationFormDesign(
+ i18n,
+ summary,
+ !addingNew,
+ challengeType,
+ );
+
+ const initAddr = (initial?.context ?? []).find(
+ (d) => d.key === "initial_address",
+ );
+
+ let readOnly: boolean | undefined;
+ let rest = {};
+ if (initAddr && initAddr.value) {
+ const va = JSON.parse(initAddr.value);
+ readOnly = va.read_only;
+ delete va.read_only;
+ rest = { ...va };
+ }
+
+ const template: Partial<VerificationMeasureDefinition> = {
+ name: initial?.name,
+ readOnly,
+ address: rest,
+ };
+
+ const form = useForm<VerificationMeasureDefinition>(design, template ?? {});
+
+ // const name = !form.status.result ? undefined : form.status.result.name;
+
+ if (!initial) {
+ throw Error("verification doesn't have initial value");
+ }
+ if (!initial.check) {
+ throw Error("verification doesn't have check");
+ }
+ if (!initial.program) {
+ throw Error("verification doesn't have program");
+ }
+ if (!initial.context) {
+ throw Error("verification doesn't have program");
+ }
+ if (!initial.name) {
+ throw Error("verification doesn't have name");
+ }
+
+ const check_name = initial.check;
+ const measure_name = initial.name;
+ const prog_name = initial.program;
+ const context = initial.context.reduce(
+ (prev, cur) => {
+ prev[cur.key] = getContextValueByType(cur.type, cur.value);
+ return prev;
+ },
+ {} as Record<string, any>,
+ );
+
+ function addNewCustomMeasure() {
+ const newMeasure = form.status.result as VerificationMeasureDefinition;
+ const currentMeasures = { ...request.custom_measures };
+ delete currentMeasures[measure_name];
+
+ currentMeasures[newMeasure.name] = {
+ check_name,
+ prog_name,
+ context: {
+ ...context,
+ initial_address: {
+ read_only: newMeasure.readOnly,
+ ...newMeasure.address,
+ },
+ },
+ };
+ updateRequest("add new measure", {
+ custom_measures: currentMeasures,
+ });
+ if (onAdded) {
+ onAdded(newMeasure.name);
+ }
+ }
+
+ function updateCurrentCustomMeasure() {
+ const newMeasure = form.status.result as VerificationMeasureDefinition;
+
+ const CURRENT_MEASURES = { ...request.custom_measures };
+ CURRENT_MEASURES[newMeasure.name] = {
+ check_name,
+ prog_name,
+ context: {
+ ...context,
+ initial_address: {
+ read_only: newMeasure.readOnly,
+ ...newMeasure.address,
+ },
+ },
+ };
+ updateRequest("update measure", {
+ custom_measures: CURRENT_MEASURES,
+ });
+ if (onChanged) {
+ onChanged(newMeasure.name);
+ }
+ }
+
+ function removeCustomMeasure() {
+ const newMeasure = form.status.result as VerificationMeasureDefinition;
+ const currentMeasures = { ...request.custom_measures };
+ delete currentMeasures[newMeasure.name];
+ updateRequest("remove measure", {
+ custom_measures: currentMeasures,
+ });
+ if (onRemoved) {
+ onRemoved(name!);
+ }
+ }
+
+ return (
+ <Fragment>
+ <FormUI design={design} model={form.model} />
+
+ <button
+ onClick={() => {
+ onCancel();
+ }}
+ class="m-4 rounded-md w-fit border-1 px-3 py-2 text-center text-sm
shadow-sm "
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ {addingNew ? (
+ <button
+ disabled={form.status.status === "fail"}
+ onClick={addNewCustomMeasure}
+ class="m-4 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 disabled:bg-gray-600"
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
+ ) : (
+ <Fragment>
+ <button
+ disabled={form.status.status === "fail"}
+ onClick={updateCurrentCustomMeasure}
+ class="m-4 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
disabled:bg-gray-600"
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+
+ <button
+ onClick={removeCustomMeasure}
+ class="m-4 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
disabled:bg-gray-600"
+ >
+ <i18n.Translate>Remove</i18n.Translate>
+ </button>
+ </Fragment>
)}
- {!check ? undefined : (
- <div class="mt-6 rounded-lg bg-gray-150 ring-1 shadow-lg
border-indigo-700 border ring-gray-900/5 ">
- <dl class="flex flex-wrap">
- <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg">
- <dt class="text-sm/6 text-white">
- <i18n.Translate>Check</i18n.Translate>
- </dt>
- <dd class="mt-1 text-base font-semibold text-white">
- {check.name}
- </dd>
- </div>
- <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
- <dt class="flex-none text-gray-500">Description</dt>
- <dd class="text-sm/6 ">
- <i18n.Translate>{check.description}</i18n.Translate>
- </dd>
- </div>
- <div class="mt-2 flex w-full flex-none gap-x-4 border-t
border-gray-900/5 px-6 pt-2">
- <dt class="flex-none text-gray-500">
- <i18n.Translate>Output</i18n.Translate>
- </dt>
- <dd class="text-sm/6 font-medium ">
- <pre class="whitespace-break-spaces">
- {check.outputs.join(", ")}
- </pre>
- </dd>
- </div>
- <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
- <dt class="flex-none text-gray-500">
- <i18n.Translate>Requires</i18n.Translate>
- </dt>
- <dd class="text-sm/6 ">
- <pre>{check.requires.join(",")}</pre>
- </dd>
- </div>
- <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
- <dt class="flex-none text-gray-500">
- <i18n.Translate>Fallback</i18n.Translate>
- </dt>
- <dd class="text-sm/6 ">
- <pre>{check.fallback}</pre>
- </dd>
- </div>
- </dl>
- <div class="px-4 pb-2"></div>
+
+ <DescribeMeasure measure={form.status.result} summary={summary} />
+ </Fragment>
+ );
+}
+
+export function MeasureForm({
+ summary,
+ onCancel,
+ onAdded,
+ onChanged,
+ onRemoved,
+ initial,
+ addingNew,
+}: {
+ initial?: Partial<MeasureDefinition>;
+ addingNew?: boolean;
+ summary: AvailableMeasureSummary;
+ onCancel: () => void;
+ onAdded: (name: string) => void;
+ onChanged: (name: string) => void;
+ onRemoved: (name: string) => void;
+}) {
+ const challengeType = (initial?.context ?? []).find(
+ (c) => c.key === "challenge-type",
+ );
+ const measureIsVerificationType = challengeType !== undefined;
+ const [formType, setFormType] = useState<"verification" | "normal">(
+ measureIsVerificationType ? "verification" : "normal",
+ );
+
+ const { i18n } = useTranslationContext();
+
+ switch (formType) {
+ case "verification": {
+ const cType = JSON.parse(challengeType?.value as any)
+ return (
+ <div>
+ <h2 class="mt-4 mb-2">
+ <i18n.Translate>Configure verification type:
{cType}</i18n.Translate>
+ </h2>
+ <div>
+ <button
+ onClick={async () => {
+ setFormType("normal");
+ }}
+ class="m-4 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"
+ >
+ <i18n.Translate>Show complete form</i18n.Translate>
+ </button>
+ </div>
+
+ <VerificationMeasureForm
+ onAdded={onAdded}
+ onCancel={onCancel}
+ onChanged={onChanged}
+ onRemoved={onRemoved}
+ summary={summary}
+ addingNew={addingNew}
+ initial={initial}
+ challengeType={cType}
+ />
</div>
- )}
- {!context || !context.length ? undefined : (
- <div class="mt-6 rounded-lg bg-gray-150 ring-1 shadow-lg
border-indigo-700 border ring-gray-900/5 ">
- <dl class="flex flex-wrap">
- <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg">
- <dt class="text-sm/6 text-white">
- <i18n.Translate>Context</i18n.Translate>
- </dt>
- <dd class="mt-1 text-base font-semibold text-white"></dd>
+ );
+ }
+ case "normal": {
+ return (
+ <div>
+ <h2 class="mt-4 mb-2">
+ <i18n.Translate>Configure measure</i18n.Translate>
+ </h2>
+ {measureIsVerificationType ? (
+ <div>
+ <button
+ onClick={async () => {
+ setFormType("verification");
+ }}
+ class="m-4 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"
+ >
+ <i18n.Translate>Show as verification</i18n.Translate>
+ </button>
</div>
- {context.map(({ key, value }) => {
- return (
- <div key={key} class="mt-4 flex w-full flex-none gap-x-4 px-6">
- <dt class="flex-none text-gray-500">{key}</dt>
- <dd class="text-sm/6 ">
- <i18n.Translate>{value}</i18n.Translate>
- </dd>
- </div>
- );
- })}
- </dl>
- <div class="px-4 pb-2"></div>
+ ) : undefined}
+
+ <NormalMeasureForm
+ onAdded={onAdded}
+ onCancel={onCancel}
+ onChanged={onChanged}
+ onRemoved={onRemoved}
+ summary={summary}
+ addingNew={addingNew}
+ initial={initial}
+ />
</div>
- )}
- </div>
- );
+ );
+ }
+ default: {
+ assertUnreachable(formType);
+ }
+ }
}
const formDesign = (
@@ -368,7 +524,7 @@ const formDesign = (
checks: { key: string; value: KycCheckInformation }[],
summary: AvailableMeasureSummary,
cantChangeName: boolean,
-): FormDesign<KycRule> => ({
+): FormDesign => ({
type: "single-column",
fields: [
{
@@ -601,3 +757,238 @@ function validateContextValueByType(
}
return undefined;
}
+
+function DescribeProgram({
+ name,
+ program,
+}: {
+ name: string;
+ program: AmlProgramRequirement;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700
border ring-gray-900/5 ">
+ <dl class="flex flex-wrap">
+ <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg">
+ <dt class="text-sm/6 text-white">
+ <i18n.Translate>Program</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-base font-semibold text-white">{name}</dd>
+ </div>
+ <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
+ <dt class="flex-none text-gray-500">
+ <i18n.Translate>Description</i18n.Translate>
+ </dt>
+ <dd class="text-sm/6 ">
+ <i18n.Translate>{program.description}</i18n.Translate>
+ </dd>
+ </div>
+ <div class="mt-2 flex w-full flex-none gap-x-4 border-t
border-gray-900/5 px-6 pt-2">
+ <dt class="flex-none text-gray-500">
+ <i18n.Translate>Context</i18n.Translate>
+ </dt>
+ <dd class="text-sm/6 font-medium text-gray-900">
+ <pre>{program.context.join(",")}</pre>
+ </dd>
+ </div>
+ <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
+ <dt class="flex-none text-gray-500">
+ <i18n.Translate>Inputs</i18n.Translate>
+ </dt>
+ <dd class="text-sm/6 ">
+ <pre class="whitespace-pre-wrap">{program.inputs.join(",")}</pre>
+ </dd>
+ </div>
+ </dl>
+ <div class="px-4 pb-2"></div>
+ </div>
+ );
+}
+function DescribeCheck({
+ name,
+ check,
+}: {
+ name: string;
+ check: KycCheckInformation;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="mt-6 rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700
border ring-gray-900/5 ">
+ <dl class="flex flex-wrap">
+ <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg">
+ <dt class="text-sm/6 text-white">
+ <i18n.Translate>Check</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-base font-semibold text-white">{name}</dd>
+ </div>
+ <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
+ <dt class="flex-none text-gray-500">Description</dt>
+ <dd class="text-sm/6 ">
+ <i18n.Translate>{check.description}</i18n.Translate>
+ </dd>
+ </div>
+ <div class="mt-2 flex w-full flex-none gap-x-4 border-t
border-gray-900/5 px-6 pt-2">
+ <dt class="flex-none text-gray-500">
+ <i18n.Translate>Output</i18n.Translate>
+ </dt>
+ <dd class="text-sm/6 font-medium ">
+ <pre class="whitespace-break-spaces">
+ {check.outputs.join(", ")}
+ </pre>
+ </dd>
+ </div>
+ <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
+ <dt class="flex-none text-gray-500">
+ <i18n.Translate>Requires</i18n.Translate>
+ </dt>
+ <dd class="text-sm/6 ">
+ <pre>{check.requires.join(",")}</pre>
+ </dd>
+ </div>
+ <div class="mt-4 flex w-full flex-none gap-x-4 px-6">
+ <dt class="flex-none text-gray-500">
+ <i18n.Translate>Fallback</i18n.Translate>
+ </dt>
+ <dd class="text-sm/6 ">
+ <pre>{check.fallback}</pre>
+ </dd>
+ </div>
+ </dl>
+ <div class="px-4 pb-2"></div>
+ </div>
+ );
+}
+function DescribeContext({
+ context,
+}: {
+ context: {
+ key: string;
+ type: "string" | "number" | "boolean" | "json";
+ value: string;
+ }[];
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="mt-6 rounded-lg bg-gray-150 ring-1 shadow-lg border-indigo-700
border ring-gray-900/5 ">
+ <dl class="flex flex-wrap">
+ <div class="flex-auto pt-4 pl-4 bg-indigo-600 rounded-t-lg">
+ <dt class="text-sm/6 text-white">
+ <i18n.Translate>Context</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-base font-semibold text-white"></dd>
+ </div>
+ {context.map(({ key, value }) => {
+ return (
+ <div key={key} class="mt-4 flex w-full flex-none gap-x-4 px-6">
+ <dt class="flex-none text-gray-500">{key}</dt>
+ <dd class="text-sm/6 ">
+ <i18n.Translate>{value}</i18n.Translate>
+ </dd>
+ </div>
+ );
+ })}
+ </dl>
+ <div class="px-4 pb-2"></div>
+ </div>
+ );
+}
+function DescribeMeasure({
+ measure,
+ summary,
+}: {
+ measure: RecursivePartial<MeasureDefinition>;
+ summary: AvailableMeasureSummary;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const programName: string | undefined = measure.program;
+ const program: AmlProgramRequirement | undefined =
+ !programName || !summary.programs[programName]
+ ? undefined
+ : summary.programs[programName];
+
+ const checkName: string | undefined = measure.check;
+ const check =
+ !checkName || !summary.checks[checkName]
+ ? undefined
+ : summary.checks[checkName];
+
+ const context =
+ !measure || !measure.context
+ ? []
+ : (measure.context as MeasureDefinition["context"]);
+
+ return (
+ <Fragment>
+ <h2 class="mt-4 mb-2">
+ <i18n.Translate>Description</i18n.Translate>
+ </h2>
+
+ {!program || !programName ? undefined : (
+ <DescribeProgram name={programName} program={program} />
+ )}
+ {!check || !checkName ? undefined : (
+ <DescribeCheck name={checkName} check={check} />
+ )}
+ {!context || !context.length ? undefined : (
+ <DescribeContext context={context} />
+ )}
+ </Fragment>
+ );
+}
+
+const verificationFormDesign = (
+ i18n: InternationalizationAPI,
+ summary: AvailableMeasureSummary,
+ cantChangeName: boolean,
+ challengeType: "email" | "phone" | "postal",
+): FormDesign => {
+ const em =
+ challengeType === "email"
+ ? design_challenger_email(i18n)
+ : challengeType === "phone"
+ ? design_challenger_phone(i18n)
+ : challengeType === "postal"
+ ? design_challenger_postal(i18n)
+ : undefined;
+
+ if (!em) {
+ throw Error(`unkown challenge type ${challengeType} `);
+ }
+
+ const fields = em.fields.map((f) => {
+ f.disabled = false;
+ f.required = false;
+ if ("id" in f) {
+ f.id = `address.${f.id}`;
+ }
+ return f;
+ });
+
+ return {
+ type: "single-column",
+ fields: [
+ {
+ id: "name",
+ type: "text",
+ required: true,
+ disabled: cantChangeName,
+ label: i18n.str`Name`,
+ help: i18n.str`Name of the verfication measure`,
+ validator(value) {
+ return !value
+ ? i18n.str`required`
+ : summary.roots[value]
+ ? i18n.str`There is already a measure with that name`
+ : undefined;
+ },
+ },
+ {
+ type: "toggle",
+ id: "readOnly",
+ label: i18n.str`Read only`,
+ help: i18n.str`Prevent the customer of changing the address`,
+ },
+ ...fields,
+ ],
+ };
+};
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
index 0786ef96a..468f0e272 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
@@ -108,7 +108,7 @@ const formDesign = (
triggered: string[];
rest: string[];
},
-): FormDesign<MeasureInformation> => ({
+): FormDesign => ({
type: "double-column",
sections: [
{
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
index 35eb7a7ff..da0d16a72 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
@@ -54,7 +54,7 @@ type FormType = {
const formDesign = (
i18n: InternationalizationAPI,
unknownAccount: boolean,
-): FormDesign<FormType> => ({
+): FormDesign => ({
type: "single-column",
fields: [
{
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
index 3d503ee55..175798866 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -1,5 +1,6 @@
import {
assertUnreachable,
+ KycCheckInformation,
MeasureInformation,
TalerError,
TalerExchangeApi,
@@ -35,6 +36,34 @@ export function Measures({}: {}): VNode {
isNew: boolean;
template: Partial<MeasureDefinition>;
}>(); //test;
+
+ const measures = useServerMeasures();
+
+ const measureBody =
+ !measures || measures instanceof TalerError || measures.type === "fail"
+ ? undefined
+ : measures.body;
+
+ const measureList = (
+ !measureBody ? [] : Object.entries(measureBody.roots)
+ ).map(convertToMeasureType);
+
+ const requestCustomMeasures = request?.custom_measures ?? {};
+ const customMeasures = Object.entries(requestCustomMeasures).map(
+ convertToMeasureType,
+ );
+
+ const checkList = !measureBody ? [] : Object.entries(measureBody.checks);
+ const simpleChecks = checkList
+ .map(convertCheckToMeasureType)
+ .filter((d): d is MeasureType => d !== undefined);
+
+ const allMeasures: MeasureType[] = [
+ ...measureList,
+ ...customMeasures,
+ ...simpleChecks,
+ ];
+
if (addMeasure) {
return (
<NewMeasure
@@ -67,7 +96,19 @@ export function Measures({}: {}): VNode {
return (
<Fragment>
- <ActiveMeasureForm />
+ {!allMeasures.length ? undefined : (
+ <ActiveMeasureForm
+ editMeasure={(template) => {
+ template.name!
+ setAddMeasure({
+ isNew: true,
+ template,
+ });
+ }}
+ measures={allMeasures}
+ newMeasures={!request.new_measures ? [] : request.new_measures}
+ />
+ )}
<ShowAllMeasures
addNewMeasure={(template) => {
setAddMeasure({
@@ -86,83 +127,76 @@ export function Measures({}: {}): VNode {
);
}
-function ActiveMeasureForm(): VNode {
- const { i18n } = useTranslationContext();
- const [request, updateRequest] = useCurrentDecisionRequest();
- const measures = useServerMeasures();
-
- const measureBody =
- !measures || measures instanceof TalerError || measures.type === "fail"
- ? undefined
- : measures.body;
-
- const measureList = (!measureBody ? [] : Object.keys(measureBody.roots)).map(
- (m) =>
- ({
- type: "normal",
- name: m,
- }) satisfies NormalMeasure,
- );
-
- const requestCustomMeasures = request?.custom_measures ?? {}
- const customMeasures = Object.keys(requestCustomMeasures).map(
- (m) =>
- ({
- type: "normal",
- name: m,
- }) satisfies NormalMeasure,
- );
-
- const checkList = !measureBody ? [] : Object.entries(measureBody.checks);
- const simpleChecks = checkList
- .filter(
- ([, check]) => check.outputs.length > 0 && check.requires.length > 0,
- )
- .map(
- ([key]) =>
- ({
- type: "simple-check-form",
- checkName: key,
- }) satisfies SimpleCheckMeasure,
- );
+function convertCheckToMeasureType([checkName, check]: [
+ string,
+ KycCheckInformation,
+]): MeasureType | undefined {
+ if (check.outputs.length === 0 && check.requires.length === 0) {
+ return {
+ type: "simple-check-form",
+ name: `check-${checkName}`,
+ checkName,
+ };
+ }
+ return undefined;
+}
- const allMeasures: MeasureType[] = [
- ...measureList,
- ...customMeasures,
- ...simpleChecks,
- ];
+const validChallengeType = ["email", "phone", "postal"];
+function convertToMeasureType([name, measure]: [
+ string,
+ MeasureInformation,
+]): MeasureType {
+ if (measure.context) {
+ // @ts-expect-error
+ const challengeType = measure.context["challenge-type"] as string;
+ if (validChallengeType.indexOf(challengeType) !== -1) {
+ return {
+ type: "verify-template",
+ name,
+ measure,
+ // @ts-expect-error challenger type is validated
+ challengerType: challengeType,
+ };
+ }
+ }
+ return {
+ type: "normal",
+ name,
+ };
+}
- const design = formDesign(i18n, allMeasures);
+function ActiveMeasureForm({
+ measures,
+ editMeasure,
+ newMeasures,
+}: {
+ measures: MeasureType[];
+ newMeasures: string[];
+ editMeasure: (m: Partial<MeasureDefinition>) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [request, updateRequest] = useCurrentDecisionRequest();
- const nm = (!request.new_measures ? [] : request.new_measures).map(
- (m) =>
- ({
- type: "normal",
- name: m,
- }) satisfies NormalMeasure,
- );
+ const design = formDesign(i18n, measures);
- const initValue = useMemo<FormType>(
- () => ({ measures: nm }),
- [request.new_measures],
- );
+ const form = useForm<FormType>(design, { measures: newMeasures });
+ const requestCustomMeasures = request?.custom_measures ?? {};
- const form = useForm<FormType>(design, initValue);
onComponentUnload(() => {
const newMeasures: string[] = [];
const formMeasures = form.status.result.measures ?? [];
- for (const m of formMeasures) {
+ for (const name of formMeasures) {
+ newMeasures.push(name);
+ const m = measures.find((d) => d.name === name)!;
switch (m.type) {
- case "normal": {
- newMeasures.push(m.name)
- break;
- }
case "simple-check-form": {
- const generatedId = `check-${m.checkName}`
- requestCustomMeasures[generatedId] = {
+ requestCustomMeasures[m.name] = {
check_name: m.checkName,
- }
- newMeasures.push(generatedId)
+ };
+ break;
+ }
+ case "normal":
+ case "verify-template": {
break;
}
default: {
@@ -176,7 +210,44 @@ function ActiveMeasureForm(): VNode {
});
});
- return <FormUI design={design} model={form.model} />;
+ const selected = form.status.result.measures ?? [];
+
+ const selectedVerifyMeasure = selected
+ .map((s) => measures.find((d) => d.name === s))
+ .filter((d) => d !== undefined && d.type === "verify-template")
+ .filter((c) => requestCustomMeasures[c.name] === undefined);
+
+ return (
+ <Fragment>
+ <FormUI design={design} model={form.model} />
+
+ <div>
+ {selectedVerifyMeasure.map((ver) => {
+ return (
+ <button
+ onClick={() => {
+ editMeasure({
+ check: ver.measure.check_name,
+ context: !ver.measure.context
+ ? []
+ : Object.entries(ver.measure.context).map(([key, value])
=> ({
+ key,
+ type: "json",
+ value: JSON.stringify(value),
+ })),
+ name: ver.name,
+ program: ver.measure.prog_name,
+ });
+ }}
+ class="m-4 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
disabled:bg-gray-600"
+ >
+ <i18n.Translate>Configure verfication measure: "{ver.name}"
</i18n.Translate>
+ </button>
+ );
+ })}
+ </div>
+ </Fragment>
+ );
}
function ShowAllMeasures({
@@ -269,7 +340,7 @@ function ShowAllMeasures({
);
}
-type MeasureType = NormalMeasure | SimpleCheckMeasure;
+type MeasureType = NormalMeasure | SimpleCheckMeasure | VerifyMeasure;
/**
* Normal measures are custom measures or server defined measure.
@@ -288,17 +359,25 @@ type NormalMeasure = {
*/
type SimpleCheckMeasure = {
type: "simple-check-form";
+ name: string;
checkName: string;
};
+type VerifyMeasure = {
+ type: "verify-template";
+ name: string;
+ measure: MeasureInformation,
+ challengeType: "email" | "phone" | "postal";
+};
+
type FormType = {
- measures: MeasureType[];
+ measures: string[];
};
function formDesign(
i18n: InternationalizationAPI,
measureNames: MeasureType[],
-): FormDesign<FormType> {
+): FormDesign {
return {
type: "single-column",
fields: [
@@ -310,19 +389,26 @@ function formDesign(
case "normal": {
return {
label: me.name,
- value: me,
+ value: me.name,
};
}
case "simple-check-form": {
return {
label: `CHECK: ${me.checkName}`,
- value: me,
+ value: `check-${me.checkName}`,
};
}
+ case "verify-template": {
+ return {
+ label: me.name,
+ value: me.name,
+ };
+ }
+ default: {
+ assertUnreachable(me);
+ }
}
- // FIXME: choises should allow value to be any type
- // check: why do we require value to be string?
- }) as any,
+ }),
id: "measures",
label: i18n.str`Active measures`,
help: i18n.str`Measures that the customer will need to satisfy while
the rules are active.`,
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
index 4ac956254..cdf84047f 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
@@ -144,7 +144,7 @@ export type PropertiesForm = {
export const propertiesForm = (
i18n: InternationalizationAPI,
props: UIFormElementConfig[],
-): FormDesign<PropertiesForm> => ({
+): FormDesign => ({
type: "double-column",
sections: [
{
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
index d95bf8a7f..3f83f7e28 100644
--- a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
+++ b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -535,7 +535,7 @@ const ruleFormDesignTemplate = (
currency: string,
measureNames: string[],
isWallet: boolean,
-): FormDesign<KycRule> => ({
+): FormDesign => ({
type: "single-column",
fields: [
{
@@ -595,7 +595,7 @@ const ruleFormDesignTemplate = (
const expirationFormDesignTemplate = (
i18n: InternationalizationAPI,
measureNames: string[],
-): FormDesign<KycRule> => ({
+): FormDesign => ({
type: "single-column",
fields: [
{
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx
b/packages/challenger-ui/src/pages/AskChallenge.tsx
index 4f3f58c07..bb6e934ee 100644
--- a/packages/challenger-ui/src/pages/AskChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -578,10 +578,10 @@ function getFormDesignBasedOnAddressType(
if (restriction.regex && !restriction.regex.test(text)) {
return restriction.hint;
}
- const prev = prevValue[TalerFormAttributes.CONTACT_EMAIL];
- if (prev === text) {
- return i18n.str`Can't use the same address`;
- }
+ // const prev = prevValue[TalerFormAttributes.CONTACT_EMAIL];
+ // if (prev === text) {
+ // return i18n.str`Can't use the same address`;
+ // }
return undefined;
},
},
@@ -606,10 +606,10 @@ function getFormDesignBasedOnAddressType(
if (restriction.regex && !restriction.regex.test(text)) {
return restriction.hint;
}
- const prev = prevValue[TalerFormAttributes.CONTACT_PHONE];
- if (prev === text) {
- return i18n.str`Can't use the same number`;
- }
+ // const prev = prevValue[TalerFormAttributes.CONTACT_PHONE];
+ // if (prev === text) {
+ // return i18n.str`Can't use the same number`;
+ // }
return undefined;
},
},
diff --git a/packages/web-util/src/forms/forms-types.ts
b/packages/web-util/src/forms/forms-types.ts
index 1b54b973f..f85cd82e0 100644
--- a/packages/web-util/src/forms/forms-types.ts
+++ b/packages/web-util/src/forms/forms-types.ts
@@ -34,7 +34,7 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
-export type FormDesign<T = unknown> =
+export type FormDesign =
| DoubleColumnFormDesign
| SingleColumnFormDesign;
diff --git a/packages/web-util/src/forms/gana/challenger_email.ts
b/packages/web-util/src/forms/gana/challenger_email.ts
index 5794e10a5..3920ae706 100644
--- a/packages/web-util/src/forms/gana/challenger_email.ts
+++ b/packages/web-util/src/forms/gana/challenger_email.ts
@@ -20,6 +20,7 @@ import {
DoubleColumnFormDesign,
FormMetadata,
InternationalizationAPI,
+ SingleColumnFormDesign,
} from "../../index.browser.js";
export const form_challenger_email = (
@@ -37,23 +38,18 @@ export const form_challenger_email = (
*/
export function design_challenger_email(
i18n: InternationalizationAPI,
-): DoubleColumnFormDesign {
+): SingleColumnFormDesign {
const today = format(new Date(), "yyyy-MM-dd");
return {
- type: "double-column",
- sections: [
+ type: "single-column",
+ fields: [
{
- title: i18n.str`Challenge`,
- fields: [
- {
- id: TalerFormAttributes.CONTACT_EMAIL,
- label: i18n.str`E-Mail`,
- type: "text",
- required: true,
- disabled: true,
- },
- ],
+ id: TalerFormAttributes.CONTACT_EMAIL,
+ label: i18n.str`E-Mail`,
+ type: "text",
+ required: true,
+ disabled: true,
},
],
};
diff --git a/packages/web-util/src/forms/gana/challenger_postal.ts
b/packages/web-util/src/forms/gana/challenger_postal.ts
index 54b3125cf..7178e0085 100644
--- a/packages/web-util/src/forms/gana/challenger_postal.ts
+++ b/packages/web-util/src/forms/gana/challenger_postal.ts
@@ -21,6 +21,7 @@ import {
DoubleColumnFormDesign,
FormMetadata,
InternationalizationAPI,
+ SingleColumnFormDesign,
} from "../../index.browser.js";
export const form_challenger_postal = (
@@ -38,38 +39,33 @@ export const form_challenger_postal = (
*/
export function design_challenger_postal(
i18n: InternationalizationAPI,
-): DoubleColumnFormDesign {
+): SingleColumnFormDesign {
const today = format(new Date(), "yyyy-MM-dd");
return {
- type: "double-column",
- sections: [
+ type: "single-column",
+ fields: [
{
- title: i18n.str`Challenge`,
- fields: [
- {
- id: TalerFormAttributes.CONTACT_NAME,
- label: i18n.str`Name`,
- type: "text",
- required: true,
- disabled: true,
- },
- {
- id: TalerFormAttributes.ADDRESS_LINES,
- label: i18n.str`Address`,
- type: "textArea",
- required: true,
- disabled: true,
- },
- {
- id: TalerFormAttributes.ADDRESS_COUNTRY,
- label: i18n.str`Country`,
- type: "selectOne",
- choices: countryNameList(i18n),
- required: true,
- disabled: true,
- },
- ],
+ id: TalerFormAttributes.CONTACT_NAME,
+ label: i18n.str`Name`,
+ type: "text",
+ required: true,
+ disabled: true,
+ },
+ {
+ id: TalerFormAttributes.ADDRESS_LINES,
+ label: i18n.str`Address`,
+ type: "textArea",
+ required: true,
+ disabled: true,
+ },
+ {
+ id: TalerFormAttributes.ADDRESS_COUNTRY,
+ label: i18n.str`Country`,
+ type: "selectOne",
+ choices: countryNameList(i18n),
+ required: true,
+ disabled: true,
},
],
};
diff --git a/packages/web-util/src/forms/gana/challenger_sms.ts
b/packages/web-util/src/forms/gana/challenger_sms.ts
index d98d65ed4..050a0552c 100644
--- a/packages/web-util/src/forms/gana/challenger_sms.ts
+++ b/packages/web-util/src/forms/gana/challenger_sms.ts
@@ -20,6 +20,7 @@ import {
DoubleColumnFormDesign,
FormMetadata,
InternationalizationAPI,
+ SingleColumnFormDesign,
} from "../../index.browser.js";
export const form_challenger_sms = (
@@ -37,23 +38,18 @@ export const form_challenger_sms = (
*/
export function design_challenger_phone(
i18n: InternationalizationAPI,
-): DoubleColumnFormDesign {
+): SingleColumnFormDesign {
const today = format(new Date(), "yyyy-MM-dd");
return {
- type: "double-column",
- sections: [
+ type: "single-column",
+ fields: [
{
- title: i18n.str`Challenge`,
- fields: [
- {
- id: TalerFormAttributes.CONTACT_PHONE,
- label: i18n.str`Phone`,
- type: "text",
- required: true,
- disabled: true,
- },
- ],
+ id: TalerFormAttributes.CONTACT_PHONE,
+ label: i18n.str`Phone`,
+ type: "text",
+ required: true,
+ disabled: true,
},
],
};
diff --git a/packages/web-util/src/hooks/useForm.ts
b/packages/web-util/src/hooks/useForm.ts
index 1e5b150d5..b6e1462bf 100644
--- a/packages/web-util/src/hooks/useForm.ts
+++ b/packages/web-util/src/hooks/useForm.ts
@@ -140,7 +140,7 @@ export type FormState<T> = {
* Hook to instantiate a form from its design.
*/
export function useForm<T>(
- design: FormDesign<T>,
+ design: FormDesign,
initialValue: RecursivePartial<FormValues<T>>,
): FormState<T> {
const { i18n } = useTranslationContext();
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-typescript-core] branch master updated: fix #9736,
Admin <=