taler-typescript-core

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

commit f7e56e47149b7f22aad71324d72b16ec72661bf8
parent e24400e5206e3c3a579399dff634a89328a64780
Author: Florian Dold <florian@dold.me>
Date:   Wed, 14 May 2025 01:52:10 +0200

work on GLS form

Issue: https://bugs.taler.net/n/9368

Diffstat:
Mpackages/taler-util/src/logging.ts | 4++++
Mpackages/taler-util/src/taler-form-attributes.ts | 64++++++++++++++--------------------------------------------------
Mpackages/web-util/src/forms/FormProvider.tsx | 2+-
Dpackages/web-util/src/forms/form-list.ts | 0
Mpackages/web-util/src/forms/forms-types.ts | 6++----
Mpackages/web-util/src/forms/forms-utils.ts | 45+++++++++++++++++++++------------------------
Mpackages/web-util/src/forms/gana/GLS_Onboarding.ts | 297+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/web-util/src/hooks/useForm.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
8 files changed, 243 insertions(+), 264 deletions(-)

diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts @@ -148,6 +148,10 @@ function writeNodeLog( export class Logger { constructor(private tag: string) {} + getGlobalLogLevel(): string { + return globalLogLevel; + } + shouldLogTrace(): boolean { const level = byTagLogLevel[this.tag] ?? globalLogLevel; switch (level) { diff --git a/packages/taler-util/src/taler-form-attributes.ts b/packages/taler-util/src/taler-form-attributes.ts @@ -471,13 +471,13 @@ export const TalerFormAttributes = { */ INCRISK_RESULT_OTHER: "INCRISK_RESULT_OTHER" as const, /** - * Description: Full legal name of an individual as in the national identity card. + * Description: First name(s) as on the identity document. * * GANA Type: String */ - PERSON_FULL_NAME: "PERSON_FULL_NAME" as const, + PERSON_FIRST_NAMES: "PERSON_FIRST_NAMES" as const, /** - * Description: Last name of an individual as in the national identity card. + * Description: Last name of an individual as on the identity document. * * GANA Type: String */ @@ -489,12 +489,6 @@ export const TalerFormAttributes = { */ PERSON_NATIONAL_ID: "PERSON_NATIONAL_ID" as const, /** - * Description: Date of birth of an individual. Format is YYYY-MM-DD. - * - * GANA Type: AbsoluteDate - */ - PERSON_DATE_OF_BIRTH: "PERSON_DATE_OF_BIRTH" as const, - /** * Description: Scan of a recognized national identity card of an individual. * * GANA Type: File @@ -523,19 +517,19 @@ export const TalerFormAttributes = { * * GANA Type: String */ - BUSINESS_REGISTRATION_ID: "BUSINESS_REGISTRATION_ID" as const, + COMMERCIAL_REGISTER_NUMBER: "COMMERCIAL_REGISTER_NUMBER" as const, /** * Description: City or location where the company or business is registered. * * GANA Type: String */ - BUSINESS_LEGAL_JURISDICTION: "BUSINESS_LEGAL_JURISDICTION" as const, + REGISTER_COURT_LOCATION: "REGISTER_COURT_LOCATION" as const, /** * Description: Registration founding date of the company or business. * * GANA Type: AbsoluteDate */ - BUSINESS_REGISTRATION_DATE: "BUSINESS_REGISTRATION_DATE" as const, + FOUNDING_DATE: "FOUNDING_DATE" as const, /** * Description: True if the company or business is a non-profit. * @@ -553,43 +547,7 @@ export const TalerFormAttributes = { * * GANA Type: GLS_BusinessRepresentative[] */ - BUSINESS_LEGAL_REPRESENTATIVES: "BUSINESS_LEGAL_REPRESENTATIVES" as const, - /** - * Description: - * - * GANA Type: String - */ - GLS_REPRESENTATIVE_FULL_NAME: "GLS_REPRESENTATIVE_FULL_NAME" as const, - /** - * Description: - * - * GANA Type: String - */ - GLS_REPRESENTATIVE_LAST_NAME: "GLS_REPRESENTATIVE_LAST_NAME" as const, - /** - * Description: - * - * GANA Type: String - */ - GLS_REPRESENTATIVE_NATIONAL_ID: "GLS_REPRESENTATIVE_NATIONAL_ID" as const, - /** - * Description: - * - * GANA Type: AbsoluteDate - */ - GLS_REPRESENTATIVE_DATE_OF_BIRTH: "GLS_REPRESENTATIVE_DATE_OF_BIRTH" as const, - /** - * Description: - * - * GANA Type: File - */ - GLS_REPRESENTATIVE_NATIONAL_ID_SCAN: "GLS_REPRESENTATIVE_NATIONAL_ID_SCAN" as const, - /** - * Description: - * - * GANA Type: CountryCode - */ - GLS_REPRESENTATIVE_NATIONALITY: "GLS_REPRESENTATIVE_NATIONALITY" as const, + BUSINESS_PERSONS: "BUSINESS_PERSONS" as const, /** * Description: DNS domain name owned by the individual or business. * @@ -601,7 +559,7 @@ export const TalerFormAttributes = { * * GANA Type: HttpHostnamePath */ - CONTACT_WEB_DOMAIN: "CONTACT_WEB_DOMAIN" as const, + CONTACT_WEBSITE: "CONTACT_WEBSITE" as const, /** * Description: E-mail address to contact the individual or business. Can be validated via E-mail with TAN. * @@ -711,6 +669,12 @@ export const TalerFormAttributes = { */ TAX_IS_DEDUCTED: "TAX_IS_DEDUCTED" as const, /** + * Description: Wirtschafts-Identifikationsnummer / Steuernummer. + * + * GANA Type: Boolean + */ + DE_BUSINESS_OR_TAX_ID: "DE_BUSINESS_OR_TAX_ID" as const, + /** * Description: Name of the version of the terms of service accepted by the customer. * * GANA Type: Boolean diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx @@ -65,7 +65,7 @@ export type UIFieldHandler<T = any> = { /** * Name of the field that this handler is responsible for. */ - name: string; + name: string | undefined; /** * Current value of the field. diff --git a/packages/web-util/src/forms/form-list.ts b/packages/web-util/src/forms/form-list.ts diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -99,7 +99,7 @@ export type UIFormElementConfig = type UIFormVoid = { type: "void"; -}; +} & UIFormFieldBaseConfig; type UIFormFieldAbsoluteTime = { type: "absoluteTimeText"; @@ -461,10 +461,8 @@ const codecForUiFormFieldGroup = (): Codec<UIFormElementGroup> => .build("UiFormFieldGroup"); const codecForUiFormVoid = (): Codec<UIFormVoid> => - buildCodecForObject<UIFormVoid>() + codecForUIFormFieldBaseConfigTemplate<UIFormVoid>() .property("type", codecForConstString("void")) - // eslint-disable-next-line @typescript-eslint/no-use-before-define - // .property("fields", codecForList(codecForUiFormField())) .build("UIFormVoid"); const codecForUiFormFieldInteger = (): Codec<UIFormFieldInteger> => diff --git a/packages/web-util/src/forms/forms-utils.ts b/packages/web-util/src/forms/forms-utils.ts @@ -32,15 +32,18 @@ export function convertFormConfigToUiField( ): UIFormField[] { const result = fieldConfig.map((config, fieldIndex) => { if (config.type === "void") return undefined; - // NON input fields + const uiKey = `${parentKey}.${fieldIndex}`; + const handler = formModel.getHandlerForUiField(uiKey); + const name = handler.name; + // FIXME: first computed prop, all should be computed + const hidden = + config.hidden === true + ? true + : config.hide + ? config.hide(handler.value, handler.formRootResult) + : undefined; + switch (config.type) { - case "caption": { - const resp: UIFormField = { - type: config.type, - properties: convertBaseFieldsProps(i18n_, config), - }; - return resp; - } case "htmlIframe": { const resp: UIFormField = { type: config.type, @@ -66,23 +69,16 @@ export function convertFormConfigToUiField( }; return resp; } - } - const uiKey = `${parentKey}.${fieldIndex}`; - const handler = formModel.getHandlerForUiField(uiKey); - const name = handler.name; - // FIXME: first computed prop, all should be computed - const hidden = - config.hidden === true - ? true - : config.hide - ? config.hide(handler.value, handler.formRootResult) - : undefined; - - // Input Fields - switch (config.type) { + case "caption": { + const resp: UIFormField = { + type: config.type, + properties: { ...convertBaseFieldsProps(i18n_, config), hidden }, + }; + return resp; + } case "array": { return { - type: "array", + type: config.type, properties: { ...convertBaseFieldsProps(i18n_, config), ...convertInputFieldsProps( @@ -369,6 +365,7 @@ export function convertFormConfigToUiField( trueValue: config.trueValue, falseValue: config.falseValue, onlyTrueValue: config.onlyTrueValue, + hidden, }, } as UIFormField; } @@ -410,7 +407,7 @@ function getConverterByFieldType( * @returns */ function convertInputFieldsProps( - name: string, + name: string | undefined, handler: UIFieldHandler, config: UIFormFieldBaseConfig, converter: StringConverter<unknown>, diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts b/packages/web-util/src/forms/gana/GLS_Onboarding.ts @@ -1,3 +1,4 @@ +import { TalerFormAttributes } from "@gnu-taler/taler-util"; import { isFuture, parse } from "date-fns"; import { DoubleColumnFormDesign, @@ -7,7 +8,6 @@ import { countryNameList, countryNationalityList, } from "../../utils/select-ui-lists.js"; -import { TalerFormAttributes } from "@gnu-taler/taler-util"; export function GLS_Onboarding( i18n: InternationalizationAPI, @@ -15,13 +15,14 @@ export function GLS_Onboarding( ): DoubleColumnFormDesign { return { type: "double-column", + title: "Merchant Onboarding Information", sections: [ { - title: i18n.str`Contact information of the company representative`, - // description: i18n.str``, + title: i18n.str`Personal Information`, + description: i18n.str`Personal information of the acting person`, fields: [ { - id: TalerFormAttributes.PERSON_FULL_NAME, + id: TalerFormAttributes.PERSON_FIRST_NAMES, label: i18n.str`First name(s)`, help: i18n.str`As on your ID document`, type: "text", @@ -34,7 +35,7 @@ export function GLS_Onboarding( required: true, }, { - id: TalerFormAttributes.PERSON_DATE_OF_BIRTH, + id: TalerFormAttributes.DATE_OF_BIRTH, label: i18n.str`Date of birth`, type: "isoDateText", placeholder: "dd/MM/yyyy", @@ -57,29 +58,59 @@ export function GLS_Onboarding( required: true, }, { + id: TalerFormAttributes.CONTACT_PHONE, + label: i18n.str`Phone number`, + type: "text", + required: true, + }, + { + id: TalerFormAttributes.CONTACT_EMAIL, + label: i18n.str`E-Mail`, + type: "text", + required: true, + }, + ], + }, + { + title: i18n.str`Terms of Service`, + fields: [ + { + type: "external-link", + id: TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE, + required: true, + url: "https://google.com", + label: i18n.str`Download the term of service`, + media: "text/plain", + }, + { + type: "caption", + label: + "You need to download the terms of service before you can accept them.", + hide(value, root) { + console.log( + `hide caption: ${root["DOWNLOADED_TERMS_OF_SERVICE"] === true}`, + ); + return root["DOWNLOADED_TERMS_OF_SERVICE"] === true; + }, + }, + { type: "toggle", id: TalerFormAttributes.ACCEPTED_TERMS_OF_SERVICE, required: true, - // threeState: true, label: i18n.str`Do you accept the terms of service?`, validator(v) { return !v ? i18n.str`Can't continue without accepting term of service.` : undefined; }, - }, - { - type: "external-link", - id: TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE, - required: true, - url: "https://google.com", - label: i18n.str`Read the term of service here`, - media: "text/plain", + hide(value, root) { + return !root["DOWNLOADED_TERMS_OF_SERVICE"]; + }, }, ], }, { - title: i18n.str`Business company information`, + title: i18n.str`Company information`, fields: [ { id: TalerFormAttributes.BUSINESS_DISPLAY_NAME, @@ -89,25 +120,25 @@ export function GLS_Onboarding( }, { id: TalerFormAttributes.BUSINESS_TYPE, - label: i18n.str`Business type`, + label: i18n.str`Legal form`, type: "text", required: true, }, { - id: TalerFormAttributes.BUSINESS_REGISTRATION_ID, - label: i18n.str`Registration ID`, + id: TalerFormAttributes.COMMERCIAL_REGISTER_NUMBER, + label: i18n.str`Commercial register number`, type: "text", required: true, }, { - id: TalerFormAttributes.BUSINESS_LEGAL_JURISDICTION, - label: i18n.str`Legal jurisdiction`, + id: TalerFormAttributes.REGISTER_COURT_LOCATION, + label: i18n.str`Seat of the register court`, type: "text", required: true, }, { - id: TalerFormAttributes.BUSINESS_REGISTRATION_DATE, - label: i18n.str`Registration date`, + id: TalerFormAttributes.FOUNDING_DATE, + label: i18n.str`Founding date`, type: "isoDateText", placeholder: "dd/MM/yyyy", pattern: "dd/MM/yyyy", @@ -123,174 +154,142 @@ export function GLS_Onboarding( }, { id: TalerFormAttributes.BUSINESS_IS_NON_PROFIT, - label: i18n.str`Is non profit?`, - type: "toggle", - required: true, - }, - { - id: TalerFormAttributes.BUSINESS_INDUSTRY, - label: i18n.str`Industry`, - type: "text", + label: i18n.str`Is the company a non-profit organization?`, required: true, - }, - { - id: TalerFormAttributes.BUSINESS_LEGAL_REPRESENTATIVES, - label: i18n.str`Legal representatives`, - type: "array", - labelFieldId: TalerFormAttributes.GLS_REPRESENTATIVE_FULL_NAME, - fields: [ + type: "choiceHorizontal", + choices: [ { - id: TalerFormAttributes.GLS_REPRESENTATIVE_FULL_NAME, - label: i18n.str`Full name`, - type: "text", - required: true, + label: "Yes", + value: true, }, { - id: TalerFormAttributes.GLS_REPRESENTATIVE_LAST_NAME, - label: i18n.str`Last name`, - type: "text", - required: true, - }, - { - id: TalerFormAttributes.GLS_REPRESENTATIVE_DATE_OF_BIRTH, - label: i18n.str`Date of birth`, - type: "isoDateText", - placeholder: "dd/MM/yyyy", - pattern: "dd/MM/yyyy", - required: true, - }, - { - id: TalerFormAttributes.GLS_REPRESENTATIVE_NATIONALITY, - label: i18n.str`Nationality`, - type: "text", - required: true, + label: "No", + value: false, }, ], - required: true, }, - ], - }, - { - title: i18n.str`Contact information of company headquarters`, - fields: [ { - id: TalerFormAttributes.CONTACT_WEB_DOMAIN, - label: i18n.str`Web site`, - type: "text", - required: true, - }, - { - id: TalerFormAttributes.CONTACT_EMAIL, - label: i18n.str`Email`, - type: "text", - required: true, - }, - { - id: TalerFormAttributes.CONTACT_PHONE, - label: i18n.str`Phone`, + id: TalerFormAttributes.BUSINESS_INDUSTRY, + label: i18n.str`Industry`, type: "text", required: true, }, ], }, { - title: i18n.str`Location information`, + title: i18n.str`Company address`, fields: [ { + id: TalerFormAttributes.DOMICILE_ADDRESS, + label: i18n.str`Address`, + type: "textArea", + required: true, + }, + { id: TalerFormAttributes.ADDRESS_COUNTRY, label: i18n.str`Country`, type: "selectOne", choices: countryNameList(i18n), required: true, }, - { - id: TalerFormAttributes.ADDRESS_COUNTRY_SUBDIVISION, - label: i18n.str`Country subdivision`, - type: "text", - // required: true, - }, - { - id: TalerFormAttributes.ADDRESS_STREET_NAME, - label: i18n.str`Street name`, - type: "text", - required: true, - }, - { - id: TalerFormAttributes.ADDRESS_STREET_NUMBER, - label: i18n.str`Street number`, - type: "text", - required: true, - }, - { - id: TalerFormAttributes.ADDRESS_LINES, - label: i18n.str`Street lines`, - type: "textArea", - // required: true, - }, - { - id: TalerFormAttributes.ADDRESS_BUILDING_NAME, - label: i18n.str`Building name`, - type: "text", - // required: true, - }, - { - id: TalerFormAttributes.ADDRESS_BUILDING_NUMBER, - label: i18n.str`Building number`, - type: "text", - // required: true, - }, - { - id: TalerFormAttributes.ADDRESS_ZIPCODE, - label: i18n.str`Zipcode`, - type: "text", - required: true, - }, - { - id: TalerFormAttributes.ADDRESS_TOWN_LOCATION, - label: i18n.str`Town location`, - type: "text", - // required: true, - }, - { - id: TalerFormAttributes.ADDRESS_TOWN_DISTRICT, - label: i18n.str`Town district`, - type: "text", - // required: true, - }, ], }, { title: i18n.str`Tax information`, fields: [ { - id: TalerFormAttributes.TAX_COUNTRY, - label: i18n.str`Country`, - type: "selectOne", - choices: countryNameList(i18n), - required: true, - }, - { - id: TalerFormAttributes.TAX_ID, - label: i18n.str`ID`, + id: TalerFormAttributes.DE_BUSINESS_OR_TAX_ID, + label: i18n.str`Business identification number or tax number`, type: "text", required: true, }, { id: TalerFormAttributes.TAX_IS_USA_LAW, - label: i18n.str`under USA law?`, - type: "toggle", + label: i18n.str`Was the company incorporated in the USA or under US law?`, required: true, + type: "choiceHorizontal", + choices: [ + { + label: "Yes", + value: true, + }, + { + label: "No", + value: false, + }, + ], }, { id: TalerFormAttributes.TAX_IS_ACTIVE, - label: i18n.str`is economically active?`, - type: "toggle", + label: i18n.str`Economically active or inactive?`, + type: "choiceHorizontal", + choices: [ + { + label: "Active", + value: "ACTIVE", + }, + { + label: "Inactive", + value: "INACTIVE", + }, + ], required: true, }, { id: TalerFormAttributes.TAX_IS_DEDUCTED, - label: i18n.str`entitled to deduct tax?`, - type: "toggle", + label: i18n.str`Eligible for input tax deduction`, + type: "choiceHorizontal", + choices: [ + { + label: "Yes", + value: true, + }, + { + label: "No", + value: false, + }, + ], + required: true, + }, + ], + }, + { + title: i18n.str`Persons`, + description: i18n.str`Please list all legal representatives, shareholders/partners, and authorized signatories.`, + fields: [ + { + id: TalerFormAttributes.BUSINESS_PERSONS, + label: i18n.str`Legal representatives / shareholders / partners / authorized signatories`, + type: "array", + labelFieldId: TalerFormAttributes.PERSON_LAST_NAME, + fields: [ + { + id: TalerFormAttributes.PERSON_FIRST_NAMES, + label: i18n.str`First name(s)`, + type: "text", + required: true, + }, + { + id: TalerFormAttributes.PERSON_LAST_NAME, + label: i18n.str`Last name`, + type: "text", + required: true, + }, + { + id: TalerFormAttributes.DATE_OF_BIRTH, + label: i18n.str`Date of birth`, + type: "isoDateText", + placeholder: "dd/MM/yyyy", + pattern: "dd/MM/yyyy", + required: true, + }, + { + id: TalerFormAttributes.NATIONALITY, + label: i18n.str`Nationality`, + type: "text", + required: true, + }, + ], required: true, }, ], diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -224,7 +224,7 @@ function checkFormFieldIsValid( formElement: UIFormElementConfig, currentValue: string | undefined, i18n: InternationalizationAPI, - secitonTitle: string | undefined, + secitonTitle: string | undefined, form: any, ): ErrorAndLabel | undefined { if (!("id" in formElement)) { @@ -286,44 +286,61 @@ function constructFormHandler<T>( handlerUiPath: string, secitonTitle: string | undefined, ): void { - if (!("id" in formElement)) { - return undefined; - } - let field: UIFieldHandler; - const path = formElement.id.split("."); - - const currentValue = getValueFromPath(formValue as any, path, undefined); - - // compute prop based on state - const hidden = - hiddenSection || - formElement.hidden || - (formElement.hide && formElement.hide(currentValue, result)); - - const currentError: ErrorAndLabel | undefined = !hidden - ? checkFormFieldIsValid(formElement, currentValue, i18n, secitonTitle, formValue) - : undefined; + if ("id" in formElement) { + const path = formElement.id.split("."); + + const currentValue = getValueFromPath(formValue as any, path, undefined); + + // compute prop based on state + const hidden = + hiddenSection || + formElement.hidden || + (formElement.hide && formElement.hide(currentValue, result)); + + const currentError: ErrorAndLabel | undefined = !hidden + ? checkFormFieldIsValid( + formElement, + currentValue, + i18n, + secitonTitle, + formValue, + ) + : undefined; + + if (currentError !== undefined) { + errors = setValueIntoPath(errors, path, currentError); + } - if (currentError !== undefined) { - errors = setValueIntoPath(errors, path, currentError); - } + function updater(newValue: unknown) { + const updated = setValueIntoPath(formValue, path, newValue) ?? {}; + onValueChange(updated); + } - function updater(newValue: unknown) { - const updated = setValueIntoPath(formValue, path, newValue) ?? {}; - onValueChange(updated); - } - field = { - name: formElement.id, - error: currentError?.message, - value: currentValue, - onChange: updater, - formRootResult: result, - hidden, - }; - if (!hidden) { - result = setValueIntoPath(result, path, field.value) ?? {}; + field = { + name: formElement.id, + error: currentError?.message, + value: currentValue, + onChange: updater, + formRootResult: result, + hidden, + }; + if (!hidden) { + result = setValueIntoPath(result, path, field.value) ?? {}; + } + } else { + const hidden = + hiddenSection || + formElement.hidden || + (formElement.hide && formElement.hide({}, result)); + field = { + name: "<none>", + value: undefined, + onChange: () => {}, + formRootResult: result, + hidden, + }; } model.fieldHandlers[handlerUiPath] = field; @@ -336,7 +353,7 @@ function constructFormHandler<T>( if (hidden) { model.hiddenSections.add(`${secIndex}`); } - + sec.fields.forEach((f, fieldIndex) => createFieldHandler(f, hidden, `${secIndex}.${fieldIndex}`, sec.title), );