/*
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
*/
import {
AbsoluteTime,
AccessToken,
Amounts,
HttpStatusCode,
KycRequirementInformation,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
Button,
FormMetadata,
InternationalizationAPI,
LocalNotificationBanner,
RenderAllFieldsByUiConfig,
UIFormElementConfig,
UIHandlerId,
convertUiField,
getConverterById,
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";
import { useUiFormsContext } from "../context/ui-forms.js";
type Props = {
token: AccessToken;
formId: string;
requirement: KycRequirementInformation;
onComplete: () => void;
};
type FormType = {
// state: TalerExchangeApi.AmlState;
};
type KycFormMetadata = {
id: string;
version: number;
when: AbsoluteTime;
};
type KycForm = {
header: KycFormMetadata;
payload: object;
};
export function FillForm({
token,
formId,
requirement,
onComplete,
}: Props): VNode {
const { i18n } = useTranslationContext();
const { config, lib } = useExchangeApiContext();
// const { forms } = useUiFormsContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const customForm =
requirement.context && "form" in requirement.context
? ({
id: (requirement.context.form as any).id,
config: requirement.context.form,
label: "Officer defined form",
version: 1,
} as FormMetadata)
: undefined;
const { forms } = useUiFormsContext();
const allForms = customForm ? [...forms, customForm] : forms
const theForm = searchForm(
i18n,
allForms,
formId,
requirement.context,
);
if (!theForm) {
return
form with id {formId} not found
;
}
const reqId = requirement.id;
if (!reqId) {
return
no id for this form, can't upload
;
}
const shape: Array = [];
const requiredFields: Array = [];
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(shape, {}, (st) => {
const partialErrors = undefinedIfEmpty>({});
const errors = undefinedIfEmpty | 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 information: KycForm = {
header: {
id: theForm.id,
version: theForm.version,
when: AbsoluteTime.now(),
},
payload: validatedForm,
};
// const data = new FormData()
// data.set("header", JSON.stringify(information.header))
// data.set("payload", JSON.stringify(information.payload))
const data = new URLSearchParams();
Object.entries(validatedForm as Record).forEach(
([key, value]) => {
if (key === "money") {
data.set(key, Amounts.stringify(value));
} else {
data.set(key, value);
}
},
);
return lib.exchange.uploadKycForm(reqId, data);
},
(res) => {
onComplete();
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.PayloadTooLarge:
return i18n.str`The form is too big for the server, try uploading smaller files.`;
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 (
{theForm.config.design.map((section, i) => {
if (!section) return ;
return (
{section.title}
{section.description && (
{section.description}
)}
);
})}
);
}
function getRequiredFields(fields: UIFormElementConfig[]): Array {
const shape: Array = [];
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 {
const shape: Array = [];
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,
context: object | undefined,
): FormMetadata | undefined {
{
const found = forms.find((v) => v.id === formId);
if (found) return found;
}
{
const pf = preloadedForms(i18n, context);
const found = pf.find((v) => v.id === formId);
if (found) return found;
}
return undefined;
}