taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit ec9d6255e6b6113e60802a643bc68d9b6c0d8d44
parent d9c410a0ce8562032612952babab26302ac770a9
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 19 Aug 2024 15:46:30 -0300

filling form

Diffstat:
Mpackages/kyc-ui/src/Routing.tsx | 25++++++++++++++++++++++---
Apackages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx | 24++++++++++++++++++++++++
Apackages/kyc-ui/src/forms/index.ts | 34++++++++++++++++++++++++++++++++++
Apackages/kyc-ui/src/forms/name_and_dob.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/kyc-ui/src/forms/personal-info.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/kyc-ui/src/forms/simplest.ts | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/kyc-ui/src/hooks/form.ts | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/kyc-ui/src/pages/FillForm.tsx | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/kyc-ui/src/pages/Start.tsx | 99+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/taler-util/src/types-taler-exchange.ts | 2+-
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");