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:
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),
);