commit ec9d6255e6b6113e60802a643bc68d9b6c0d8d44
parent d9c410a0ce8562032612952babab26302ac770a9
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 19 Aug 2024 15:46:30 -0300
filling form
Diffstat:
10 files changed, 810 insertions(+), 54 deletions(-)
diff --git a/packages/kyc-ui/src/Routing.tsx b/packages/kyc-ui/src/Routing.tsx
@@ -26,6 +26,7 @@ import { useErrorBoundary } from "preact/hooks";
import { CallengeCompleted } from "./pages/CallengeCompleted.js";
import { Frame } from "./pages/Frame.js";
import { Start } from "./pages/Start.js";
+import { FillForm } from "./pages/FillForm.js";
export function Routing(): VNode {
// check session and defined if this is
@@ -40,9 +41,13 @@ export function Routing(): VNode {
const publicPages = {
completed: urlPattern(/\/completed/, () => `#/completed`),
start: urlPattern<{ token: string }>(
- /\/start\/(?<token>[0-9A-Za-z]+)/,
+ /\/info\/(?<token>[0-9A-Za-z]+)/,
({ token }) => `#/start/${token}`,
),
+ fillForm: urlPattern<{ token: string, formId: string }>(
+ /\/fill-form\/(?<token>[0-9A-Za-z]+)\/(?<formId>[0-9A-Za-z\-]+)/,
+ ({ token, formId }) => `#/fill-form/${token}/${formId}`,
+ ),
};
function safeGetParam(
@@ -68,22 +73,36 @@ function PublicRounting(): VNode {
useErrorBoundary((e) => {
console.log("error", e);
});
- console.log("ASD", location.name)
+
switch (location.name) {
case undefined: {
return <div>not found</div>
}
case "start": {
+ const toFillForm = urlPattern<{ formId: string }>(
+ /\/fill-form\/(?<token>[0-9A-Za-z]+)\/(?<formId>[0-9A-Za-z]+)/,
+ ({ formId }) => `#/fill-form/${location.values.token}/${formId}`,
+ )
return (
<Start
- //FIX: validate token
+ //FIX: validate token
token={location.values.token as AccessToken}
+ routeFillForm={toFillForm}
onCreated={() => {
navigateTo(publicPages.completed.url({}));
}}
/>
);
}
+ case "fillForm": {
+ return (
+ <FillForm
+ //FIX: validate token
+ token={location.values.token as AccessToken}
+ formId={location.values.formId}
+ />
+ );
+ }
case "completed": {
return <CallengeCompleted />;
}
diff --git a/packages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx
@@ -0,0 +1,24 @@
+/*
+ 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 { TalerError } from "@gnu-taler/taler-util";
+import { ErrorLoading } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { usePreferences } from "../context/preferences.js";
+
+export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode {
+ const [pref] = usePreferences();
+ return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />;
+}
diff --git a/packages/kyc-ui/src/forms/index.ts b/packages/kyc-ui/src/forms/index.ts
@@ -0,0 +1,33 @@
+/*
+ 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 { FormMetadata, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { simplest } from "./simplest.js";
+
+export const preloadedForms: (i18n: InternationalizationAPI) => Array<FormMetadata> = (i18n) => [
+ {
+ label: i18n.str`Simple comment`,
+ id: "__simple_comment",
+ version: 1,
+ config: simplest(i18n),
+ },
+ {
+ label: i18n.str`Personal info`,
+ id: "personal-info",
+ version: 1,
+ config: simplest(i18n),
+ },
+
+]
+\ No newline at end of file
diff --git a/packages/kyc-ui/src/forms/name_and_dob.ts b/packages/kyc-ui/src/forms/name_and_dob.ts
@@ -0,0 +1,47 @@
+/*
+ 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 type {
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId,
+ } from "@gnu-taler/web-util/browser";
+
+ export const nameAndDob = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ id: ".full_name" as UIHandlerId,
+ name: "full_name",
+ label: i18n.str`Full Name`,
+ },
+ {
+ type: "textArea",
+ id: ".birthdate" as UIHandlerId,
+ name: "birthdate",
+ label: i18n.str`Birthdate`,
+ },
+ ],
+ },
+ ],
+ });
+
+
+\ No newline at end of file
diff --git a/packages/kyc-ui/src/forms/personal-info.ts b/packages/kyc-ui/src/forms/personal-info.ts
@@ -0,0 +1,47 @@
+/*
+ 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 type {
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId,
+ } from "@gnu-taler/web-util/browser";
+
+ export const personalInfo = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ id: ".full_name" as UIHandlerId,
+ name: "full_name",
+ label: i18n.str`Full Name`,
+ },
+ {
+ type: "textArea",
+ id: ".birthdate" as UIHandlerId,
+ name: "birthdate",
+ label: i18n.str`Birthdate`,
+ },
+ ],
+ },
+ ],
+ });
+
+
+\ No newline at end of file
diff --git a/packages/kyc-ui/src/forms/simplest.ts b/packages/kyc-ui/src/forms/simplest.ts
@@ -0,0 +1,79 @@
+/*
+ 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 type {
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+
+export const simplest = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ id: ".comment" as UIHandlerId,
+ name: "comment",
+ label: i18n.str`Comment`,
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+});
+
+export function resolutionSection(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormSection {
+ return {
+ title: i18n.str`Resolution`,
+ fields: [
+ {
+ type: "choiceHorizontal",
+ id: ".state" as UIHandlerId,
+ name: "state",
+ label: i18n.str`New state`,
+ converterId: "TalerExchangeApi.AmlState",
+ choices: [
+ {
+ value: "frozen",
+ label: i18n.str`Frozen`,
+ },
+ {
+ value: "pending",
+ label: i18n.str`Pending`,
+ },
+ {
+ value: "normal",
+ label: i18n.str`Normal`,
+ },
+ ],
+ },
+ {
+ type: "amount",
+ id: ".threshold" as UIHandlerId,
+ currency: "NETZBON",
+ name: "threshold",
+ converterId: "Taler.Amount",
+ label: i18n.str`New threshold`,
+ },
+ ],
+ };
+}
diff --git a/packages/kyc-ui/src/hooks/form.ts b/packages/kyc-ui/src/hooks/form.ts
@@ -0,0 +1,227 @@
+/*
+ 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,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ UIFieldHandler,
+ UIFormElementConfig,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { undefinedIfEmpty } from "../pages/Start.js";
+
+// export type UIField = {
+// value: string | undefined;
+// onUpdate: (s: string) => void;
+// error: TranslatedString | undefined;
+// };
+
+export type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIFieldHandler
+ : T[k] extends AmountJson
+ ? UIFieldHandler
+ : T[k] extends TalerExchangeApi.AmlState
+ ? UIFieldHandler
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TalerExchangeApi.AmlState
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : T[k] extends AbsoluteTime
+ ? TranslatedString
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ shape: Array<UIHandlerId>,
+ form: RecursivePartial<FormValues<T>>,
+ updateForm: (d: RecursivePartial<FormValues<T>>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ const handler = shape.reduce((handleForm, fieldId) => {
+ const path = fieldId.split(".");
+
+ function updater(newValue: unknown) {
+ updateForm(setValueDeeper(form, path, newValue));
+ }
+
+ const currentValue = getValueDeeper<string>(form as any, path, undefined);
+ const currentError = getValueDeeper<TranslatedString>(
+ errors as any,
+ path,
+ undefined,
+ );
+ const field: UIFieldHandler = {
+ error: currentError,
+ value: currentValue,
+ onChange: updater,
+ state: {}, //FIXME: add the state of the field (hidden, )
+ };
+
+ return setValueDeeper(handleForm, path, field);
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ shape: Array<UIHandlerId>,
+ defaultValue: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+): [FormHandler<T>, 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];
+}
+
+interface Tree<T> extends Record<string, Tree<T> | T> {}
+
+export function getValueDeeper<T>(
+ object: Tree<T> | undefined,
+ names: string[],
+ notFoundValue?: T,
+): T | undefined {
+ if (names.length === 0) return object as T;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest, notFoundValue);
+ }
+ if (object === undefined) {
+ return notFoundValue;
+ }
+ return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+}
+
+export function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (!head) {
+ return setValueDeeper(object, rest, value);
+ }
+ if (object === undefined) {
+ return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+ }
+ return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) });
+}
+
+export function getShapeFromFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getShapeFromFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+
+export function getRequiredFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ if (!field.required) {
+ return;
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getRequiredFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+export function validateRequiredFields<FormType>(
+ errors: FormErrors<FormType> | undefined,
+ form: object,
+ fields: Array<UIHandlerId>,
+): FormErrors<FormType> | undefined {
+ let result: FormErrors<FormType> | undefined = errors;
+ fields.forEach((f) => {
+ const path = f.split(".");
+ const v = getValueDeeper(form as any, path);
+ result = setValueDeeper(result, path, !v ? "required" : undefined);
+ });
+ return result;
+}
diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx
@@ -0,0 +1,278 @@
+/*
+ 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,
+ AccessToken,
+ AmountJson,
+ Amounts
+} from "@gnu-taler/taler-util";
+import {
+ Button,
+ convertUiField,
+ FormMetadata,
+ getConverterById,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
+ UIFormElementConfig,
+ UIHandlerId,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { preloadedForms } from "../forms/index.js";
+import { FormErrors, useFormState, validateRequiredFields } from "../hooks/form.js";
+import { undefinedIfEmpty } from "./Start.js";
+
+type Props = {
+ token: AccessToken;
+ formId: string;
+};
+
+type FormType = {
+ when: AbsoluteTime;
+ // state: TalerExchangeApi.AmlState;
+ threshold: AmountJson;
+ comment: string;
+};
+
+export function FillForm({token, formId}:Props):VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useExchangeApiContext();
+ // const { forms } = useUiFormsContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const initial: FormType = {
+ when: AbsoluteTime.now(),
+ threshold: Amounts.zeroOfCurrency(config.currency),
+ comment: "",
+ };
+
+ const theForm = searchForm(i18n, [], formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
+ const shape: Array<UIHandlerId> = [];
+ const requiredFields: Array<UIHandlerId> = [];
+
+ theForm.config.design.forEach((section) => {
+ Array.prototype.push.apply(shape, getShapeFromFields(section.fields));
+ Array.prototype.push.apply(
+ requiredFields,
+ getRequiredFields(section.fields),
+ );
+ });
+ const [form, state] = useFormState<FormType>(shape, initial, (st) => {
+ const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({
+ threshold: !st.threshold ? i18n.str`required` : undefined,
+ when: !st.when ? i18n.str`required` : undefined,
+ });
+
+ const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
+ validateRequiredFields(partialErrors, st, requiredFields),
+ );
+
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ }
+
+ return {
+ status: "fail",
+ result: st as any,
+ errors,
+ };
+ });
+ const validatedForm = state.status !== "ok" ? undefined : state.result;
+
+ const submitHandler =
+ validatedForm === undefined
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ // const justification: Justification = {
+ // id: theForm.id,
+ // label: theForm.label,
+ // version: theForm.version,
+ // value: validatedForm,
+ // };
+
+ // const decision: Omit<TalerExchangeApi.AmlDecisionRequest, "officer_sig"> =
+ // {
+ // justification: JSON.stringify(justification),
+ // decision_time: TalerProtocolTimestamp.now(),
+ // h_payto: account,
+ // keep_investigating: false,
+ // new_rules: {
+ // custom_measures: {},
+ // expiration_time: {
+ // t_s: "never"
+ // },
+ // rules: [],
+ // successor_measure: undefined
+ // },
+ // properties: {},
+ // new_measure: undefined,
+ // };
+
+ return {
+ type: "ok",
+ body: {}
+ }
+ },
+ () => {
+ // window.location.href = privatePages.cases.url({});
+ },
+ // (fail) => {
+ // switch (fail.case) {
+ // case HttpStatusCode.Forbidden:
+ // return i18n.str`Wrong credentials for "${officer.account}"`;
+ // case HttpStatusCode.NotFound:
+ // return i18n.str`The account was not found`;
+ // case HttpStatusCode.Conflict:
+ // return i18n.str`Officer disabled or more recent decision was already submitted.`;
+ // default:
+ // assertUnreachable(fail);
+ // }
+ // },
+ );
+
+ return <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {theForm.config.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={convertUiField(
+ i18n,
+ section.fields,
+ form,
+ getConverterById,
+ )}
+ />
+ </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}
+ // disabled={!submitHandler}
+ class="disabled:opacity-50 disabled:cursor-default 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>
+
+}
+
+function getRequiredFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ if (!field.required) {
+ return;
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getRequiredFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+function getShapeFromFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getShapeFromFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+function searchForm(
+ i18n: InternationalizationAPI,
+ forms: FormMetadata[],
+ formId: string,
+): FormMetadata | undefined {
+ {
+ const found = forms.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ {
+ const pf = preloadedForms(i18n);
+ const found = pf.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ return undefined;
+}
+
diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx
@@ -16,52 +16,47 @@
import {
AccessToken,
HttpStatusCode,
- KycBuiltInFromId,
KycRequirementInformation,
- KycRequirementInformationId,
TalerError,
- assertUnreachable,
- createRFC8959AccessTokenEncoded,
- encodeCrock,
- randomBytes
+ assertUnreachable
} from "@gnu-taler/taler-util";
import {
Attention,
- Button,
+ FormMetadata,
+ InternationalizationAPI,
Loading,
LocalNotificationBanner,
- ShowInputErrorLabel,
- useExchangeApiContext,
+ RouteDefinition,
useLocalNotificationHandler,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { safeToURL } from "../Routing.js";
-import { useSessionState } from "../hooks/session.js";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { preloadedForms } from "../forms/index.js";
import { useKycInfo } from "../hooks/kyc.js";
type Props = {
token: AccessToken;
onCreated: () => void;
- focus?: boolean;
+ routeFillForm: RouteDefinition<{ formId: string }>;
};
+
export function Start({
token,
- focus,
+ routeFillForm,
onCreated,
}: Props): VNode {
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const { lib } = useExchangeApiContext();
- const { state, start } = useSessionState();
+ // const { lib } = useExchangeApiContext();
+ // const { state, start } = useSessionState();
const result = useKycInfo({ accessToken: token })
if (!result) {
return <Loading />;
}
if (result instanceof TalerError) {
- return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.type === "fail") {
@@ -119,22 +114,23 @@ export function Start({
// },
);
- const requirements: typeof result.body.requirements = [{
- description: "this is the form description, click to show the form field bla bla bla",
- form: "asdasd" as KycBuiltInFromId,
- description_i18n: {},
- id: "ASDASD" as KycRequirementInformationId,
- }, {
- description: "this is the description of the link and service provider.",
- form: "LINK",
- description_i18n: {},
- id: "ASDASD" as KycRequirementInformationId,
- }, {
- description: "you can't click this becuase this is only information, wait until AML officer replies.",
- form: "INFO",
- description_i18n: {},
- id: "ASDASD" as KycRequirementInformationId,
- }]
+ // const requirements: typeof result.body.requirements = [{
+ // description: "this is the form description, click to show the form field bla bla bla",
+ // form: "asdasd" as KycBuiltInFromId,
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }, {
+ // description: "this is the description of the link and service provider.",
+ // form: "LINK",
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }, {
+ // description: "you can't click this becuase this is only information, wait until AML officer replies.",
+ // form: "INFO",
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }]
+ const requirements = result.body.requirements;
if (!result.body.requirements.length) {
return <Fragment>
<LocalNotificationBanner notification={notification} />
@@ -146,16 +142,16 @@ export function Start({
</i18n.Translate>
</h2>
</div>
- <div class="m-8">
- <Attention
- title={i18n.str`Kyc completed`}
- type="success"
- >
- <i18n.Translate>
- You can close this now
- </i18n.Translate>
- </Attention>
- </div>
+ <div class="m-8">
+ <Attention
+ title={i18n.str`Kyc completed`}
+ type="success"
+ >
+ <i18n.Translate>
+ You can close this now
+ </i18n.Translate>
+ </Attention>
+ </div>
</div>
@@ -178,7 +174,7 @@ export function Start({
<ul role="list" class=" divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl">
{requirements.map((req, idx) => {
return <li key={idx} class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6">
- <RequirementRow requirement={req} />
+ <RequirementRow requirement={req} routeFillForm={routeFillForm} />
</li>
})}
</ul>
@@ -190,7 +186,7 @@ export function Start({
}
-function RequirementRow({ requirement: req }: { requirement: KycRequirementInformation }): VNode {
+function RequirementRow({ requirement: req, routeFillForm }: { requirement: KycRequirementInformation, routeFillForm: RouteDefinition<{ formId: string }> }): VNode {
const { i18n } = useTranslationContext()
switch (req.form) {
case "INFO": {
@@ -226,7 +222,7 @@ function RequirementRow({ requirement: req }: { requirement: KycRequirementInfor
<p class="text-sm font-semibold leading-6 text-gray-900">
<a href="#">
<span class="absolute inset-x-0 -top-px bottom-0"></span>
- <i18n.Translate>Link</i18n.Translate>
+ <i18n.Translate>Begin KYC process</i18n.Translate>
</a>
</p>
<p class="mt-1 flex text-xs leading-5 text-gray-500">
@@ -236,7 +232,9 @@ function RequirementRow({ requirement: req }: { requirement: KycRequirementInfor
</div>
<div class="flex shrink-0 items-center gap-x-4">
<div class="hidden sm:flex sm:flex-col sm:items-end">
- <p class="text-sm leading-6 text-gray-900">Open link</p>
+ <p class="text-sm leading-6 text-gray-900">
+ <i18n.Translate>Start</i18n.Translate>
+ </p>
</div>
<svg class="h-5 w-5 flex-none text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
@@ -255,7 +253,7 @@ function RequirementRow({ requirement: req }: { requirement: KycRequirementInfor
</div>
<div class="min-w-0 flex-auto">
<p class="text-sm font-semibold leading-6 text-gray-900">
- <a href="#">
+ <a href={routeFillForm.url({ formId: req.form })}>
<span class="absolute inset-x-0 -top-px bottom-0"></span>
<i18n.Translate>Form</i18n.Translate>
</a>
@@ -295,7 +293,8 @@ export function doAutoFocus(element: HTMLElement | null): void {
}
}
-export function undefinedIfEmpty<T extends object>(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,
)
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -2445,7 +2445,7 @@ export const codecForKycRequirementInformation =
buildCodecForObject<KycRequirementInformation>()
.property("form", codecForEither(codecForConstString("LINK"), codecForConstString("INFO"), codecForKycFormId()))
.property("description", codecForString())
- .property("description_i18n", codecForInternationalizedString())
+ .property("description_i18n", codecOptional(codecForInternationalizedString()))
.property("id", codecOptional(codecForKycRequirementInformationId()))
.build("TalerExchangeApi.KycRequirementInformation");