commit ba8099d07c95a525544d579d3e132c06a7b95d91 parent 409475eb2847eb278e21d06b9d428773bc057014 Author: Sebastian <sebasjm@gmail.com> Date: Thu, 28 Nov 2024 15:44:33 -0300 accept tos kyc/aml form Diffstat:
16 files changed, 488 insertions(+), 44 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/pages/Measures.tsx b/packages/aml-backoffice-ui/src/pages/Measures.tsx @@ -14,23 +14,16 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - AbsoluteTime, - AmlDecisionRequest, - TalerError, - TalerExchangeApi, + TalerError } from "@gnu-taler/taler-util"; import { - FormMetadata, - InternationalizationAPI, Loading, useExchangeApiContext, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h } from "preact"; -import { useUiFormsContext } from "../context/ui-forms.js"; -import { preloadedForms } from "../forms/index.js"; -import { useServerMeasures } from "../hooks/account.js"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; +import { useServerMeasures } from "../hooks/account.js"; export function Measures({}: {}) { const { config } = useExchangeApiContext(); diff --git a/packages/kyc-ui/src/Routing.tsx b/packages/kyc-ui/src/Routing.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - import { +import { Loading, urlPattern, useCurrentLocation, @@ -28,6 +28,7 @@ import { CallengeCompleted } from "./pages/CallengeCompleted.js"; import { Frame } from "./pages/Frame.js"; import { Start } from "./pages/Start.js"; import { useSessionState } from "./hooks/session.js"; +import { TriggerKyc } from "./pages/TriggerKyc.js"; export function Routing(): VNode { // check session and defined if this is @@ -39,9 +40,10 @@ export function Routing(): VNode { ); } -const publicPages = { +export const publicPages = { completed: urlPattern(/\/completed/, () => `#/completed`), start: urlPattern(/\/start/, () => `#/start`), + triggerKyc: urlPattern(/\/test\/trigger-kyc/, () => `#/test/trigger-kyc`), }; function safeGetParam( @@ -61,12 +63,12 @@ export function safeToURL(s: string | undefined): URL | undefined { } } -const ACCESS_TOKEN_REGEX = new RegExp("[A-Z0-9]{52}") +const ACCESS_TOKEN_REGEX = new RegExp("[A-Z0-9]{52}"); /** * by how the exchange serves the SPA * /kyc-spa/KXAFXEWM7E3EJSYD9GJ30FYK1C17AKZWV119ZJA3XGPBBMZFJ2C0#/start - * + * * by how dev.mjs serves the SPA * /app/?token=KXAFXEWM7E3EJSYD9GJ30FYK1C17AKZWV119ZJA3XGPBBMZFJ2C0#/start * @returns @@ -75,11 +77,11 @@ function getAccessTokenFromURL(): AccessToken | undefined { if (typeof window === "undefined") return undefined; const paths = window.location.pathname.split("/"); if (paths.length < 3) return undefined; - const path = paths[2] + const path = paths[2]; if (path && ACCESS_TOKEN_REGEX.test(path)) { return path as AccessToken; } - const param = new URLSearchParams(window.location.search).get("token") + const param = new URLSearchParams(window.location.search).get("token"); if (param && ACCESS_TOKEN_REGEX.test(param)) { return param as AccessToken; } @@ -93,7 +95,7 @@ function PublicRounting(): VNode { useErrorBoundary((e) => { console.log("error", e); }); - const sessionToken = state?.accessToken + const sessionToken = state?.accessToken; const urlToken = getAccessTokenFromURL(); useEffect(() => { if (!urlToken) { @@ -102,13 +104,9 @@ function PublicRounting(): VNode { } // loading a new session if (urlToken !== sessionToken) { - start(urlToken) + start(urlToken); } - },[sessionToken, urlToken]) - - if (!sessionToken) { - return <div>No access token</div>; - } + }, [sessionToken, urlToken]); switch (location.name) { case undefined: { @@ -116,6 +114,10 @@ function PublicRounting(): VNode { return <Loading />; } case "start": { + if (!sessionToken) { + return <div>No access token</div>; + } + return ( <Start token={sessionToken} @@ -129,6 +131,16 @@ function PublicRounting(): VNode { case "completed": { return <CallengeCompleted />; } + case "triggerKyc": { + return ( + <TriggerKyc + onKycStarted={(token) => { + start(token) + navigateTo(publicPages.start.url({})); + }} + /> + ); + } default: assertUnreachable(location); } diff --git a/packages/kyc-ui/src/forms/accept-tos.ts b/packages/kyc-ui/src/forms/accept-tos.ts @@ -0,0 +1,57 @@ +/* + 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, + InternationalizationAPI, + UIHandlerId, +} from "@gnu-taler/web-util/browser"; + +function filterUndefined<T>(ar: Array<T|undefined>): Array<T> { + return ar.filter((a):a is T => !!a) +} + +export const acceptTos = (i18n: InternationalizationAPI, context?: any): DoubleColumnForm => ({ + type: "double-column" as const, + design: [ + { + title: i18n.str`Accept Term of Service`, + fields: filterUndefined([ + context?.tos_url ? { + type: "htmlIframe", + label: context?.provider_name ?? `Link`, + url: context.tos_url + } : undefined, + { + type: "choiceHorizontal", + id: "asd" as UIHandlerId, + required: true, + label: i18n.str`Do you accept terms of service`, + choices: [ + { + label: i18n.str`Yes`, + value: "yes", + }, + { + label: i18n.str`No`, + value: "no", + }, + ], + }, + ]), + }, + ], +}); diff --git a/packages/kyc-ui/src/forms/index.ts b/packages/kyc-ui/src/forms/index.ts @@ -18,12 +18,14 @@ import { InternationalizationAPI, } from "@gnu-taler/web-util/browser"; import { simplest } from "./simplest.js"; -import { personalInfo } from "./personal-info.js"; +import { acceptTos } from "./accept-tos.js"; import { nameAndDob } from "./nameAndBirthdate.js"; +import { personalInfo } from "./personal-info.js"; export const preloadedForms: ( i18n: InternationalizationAPI, -) => Array<FormMetadata> = (i18n) => [ + context: object | undefined, +) => Array<FormMetadata> = (i18n, context) => [ { label: i18n.str`Simple comment`, id: "__simple_comment", @@ -37,6 +39,12 @@ export const preloadedForms: ( config: personalInfo(i18n), }, { + label: i18n.str`Accept Terms of Service`, + id: "accept-tos", + version: 1, + config: acceptTos(i18n, context), + }, + { label: i18n.str`Name and birthdate`, id: "name_and_dob", version: 1, diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -87,7 +87,7 @@ export function FillForm({ } as FormMetadata) : undefined; - const theForm = searchForm(i18n, customForm ? [customForm] : [], formId); + const theForm = searchForm(i18n, customForm ? [customForm] : [], formId, requirement.context); if (!theForm) { return <div>form with id {formId} not found</div>; } @@ -274,13 +274,14 @@ 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); + const pf = preloadedForms(i18n, context); const found = pf.find((v) => v.id === formId); if (found) return found; } diff --git a/packages/kyc-ui/src/pages/Frame.tsx b/packages/kyc-ui/src/pages/Frame.tsx @@ -31,6 +31,7 @@ import { usePreferences, } from "../context/preferences.js"; import { useSettingsContext } from "../context/settings.js"; +import { publicPages } from "../Routing.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -66,7 +67,9 @@ export function Frame({ children }: { children: ComponentChildren }): VNode { title="KYC" onLogout={undefined} iconLinkURL="#" - sites={[]} + sites={!preferences.showDebugInfo ? [] : [ + ["Test kyc", publicPages.triggerKyc.url({})] + ]} supportedLangs={["en"]} > <li> diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -30,7 +30,7 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useKycInfo } from "../hooks/kyc.js"; import { FillForm } from "./FillForm.js"; @@ -40,7 +40,7 @@ type Props = { token: AccessToken; }; -function ShowReqList({ +export function ShowReqList({ token, onFormSelected, }: { @@ -52,6 +52,18 @@ function ShowReqList({ // const { lib } = useExchangeApiContext(); // const { state, start } = useSessionState(); const result = useKycInfo(token); + + const firstAccount = + result && + !(result instanceof TalerError) && + result.type === "ok" && + result.body.requirements.length > 0 + ? result.body.requirements[0] + : undefined; + useEffect(() => { + if (firstAccount) onFormSelected(firstAccount); + }, [firstAccount]); + if (!result) { return <Loading />; } diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -0,0 +1,272 @@ +/* + 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 { + Attention, + Button, + convertUiField, + FormMetadata, + getConverterById, + LocalNotificationBanner, + RenderAllFieldsByUiConfig, + UIHandlerId, + useExchangeApiContext, + useLocalNotificationHandler, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { + FormErrors, + getRequiredFields, + getShapeFromFields, + useFormState, + validateRequiredFields, +} from "../hooks/form.js"; +import { undefinedIfEmpty } from "./Start.js"; +import { + AbsoluteTime, + AccessToken, + AmountJson, + Amounts, + AmountString, + assertUnreachable, + createNewOfficerAccount, + createNewWalletKycAccount, + HttpStatusCode, +} from "@gnu-taler/taler-util"; +import { useEffect, useMemo, useState } from "preact/hooks"; + +type FormType = { + amount: AmountJson; +}; +type Props = { + onKycStarted: (token: AccessToken) => void; +}; +export function TriggerKyc({ onKycStarted }: Props): VNode { + const { i18n } = useTranslationContext(); + const [notification, withErrorHandler, notify] = + useLocalNotificationHandler(); + const { config, lib } = useExchangeApiContext(); + const [kycAccount, setKycAccount] = useState<string>(); + + const theForm: FormMetadata = { + id: "asd", + version: 1, + label: i18n.str`Trigger KYC balance`, + config: { + type: "double-column", + design: [ + { + title: i18n.str`Trigger KYC Balance`, + fields: [ + { + id: ".amount" as UIHandlerId, + type: "amount", + currency: config.currency, + label: i18n.str`Amount`, + required: true, + converterId: "Taler.Amount", + }, + ], + }, + ], + }, + }; + + 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, + { + amount: Amounts.parseOrThrow(`${config.currency}:1000000`), + }, + (st) => { + const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({}); + + 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 accountPromise = useMemo(async () => { + const resp = await lib.exchange.getSeed(); + const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array(); + return createNewWalletKycAccount(extraEntropy); + }, [1]); + + useEffect(() => { + if (!kycAccount) return; + const paytoHash = kycAccount + async function check() { + const {signingKey} = await accountPromise; + const result = await lib.exchange.checkKycStatus(signingKey, paytoHash); + if (result.type === "ok") { + if (result.body) { + onKycStarted(result.body.access_token) + } else { + console.log("empty body") + } + } else { + switch(result.case) { + case HttpStatusCode.Forbidden:{ + notify({ + type: "error", + title: i18n.str`could not create token`, + description: i18n.str`access denied`, + when: AbsoluteTime.now(), + }) + } + case HttpStatusCode.NotFound: { + notify({ + type: "error", + title: i18n.str`could not create token`, + description: i18n.str`not found`, + when: AbsoluteTime.now(), + }) + } + case HttpStatusCode.Conflict: { + notify({ + type: "error", + title: i18n.str`could not create token`, + description: i18n.str`conflict`, + when: AbsoluteTime.now(), + }) + + } + } + } + } + check() + }, [kycAccount]); + + const submitHandler = + theForm === undefined || state.status === "fail" + ? undefined + : withErrorHandler( + async () => { + const account = await accountPromise; + + return lib.exchange.notifyKycBalanceLimit( + account, + Amounts.stringify(state.result.amount), + ); + }, + (res) => { + notify({ + type: "info", + title: i18n.str`No kyc required`, + when: AbsoluteTime.now(), + }); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.Forbidden: + return i18n.str`Access denied trying to test balance.`; + case HttpStatusCode.UnavailableForLegalReasons: + setKycAccount(fail.body.h_payto); + return i18n.str`Unavailable For Legal Reasons`; + default: + assertUnreachable(fail); + } + }, + ); + + if (kycAccount) { + return <div>loading...</div> + } + + return ( + <div class="rounded-lg bg-white px-5 py-6 shadow m-4"> + <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"> + <button + onClick={() => {}} + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + <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> + </div> + ); +} diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -1447,6 +1447,7 @@ const allAmlPrograms: TalerKycAml.AmlProgramDefinition[] = [ return outcome; }, requiredAttributes: [], + requiredInputs: [], requiredContext: [], }, AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT, diff --git a/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts b/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts @@ -75,6 +75,7 @@ export const AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT: TalerKycAml.AmlProgramDefin return outcome; }, requiredAttributes: [], + requiredInputs: [], requiredContext: [], }; diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -38,6 +38,7 @@ import { import { Codec, codecForAny } from "../codec.js"; import { TalerSignaturePurpose, + amountToBuffer, bufferForUint64, buildSigPS, decodeCrock, @@ -616,7 +617,7 @@ export class TalerExchangeHttpClient { const body: WalletKycRequest = { balance, reserve_pub: account.id, - reserve_sig: encodeCrock(account.signingKey), + reserve_sig: buildKYCWalletBalanceSignature(account.signingKey, balance), }; const resp = await this.httpLib.fetch(url.href, { @@ -1049,6 +1050,17 @@ export class TalerExchangeHttpClient { } } +function buildKYCWalletBalanceSignature( + key: SigningKey, + balance: AmountString, +): string { + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP) + .put(amountToBuffer(balance)) + .build(); + + return encodeCrock(eddsaSign(sigBlob, key)); +} + function buildKYCQuerySignature(key: SigningKey): string { const sigBlob = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build(); diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts @@ -19,6 +19,8 @@ import { LockedAccount, OfficerAccount, OfficerId, + ReserveAccount, + ReservePub, SigningKey, createEddsaKeyPair, decodeCrock, @@ -106,27 +108,25 @@ export async function createNewOfficerAccount( */ export async function createNewWalletKycAccount( extraNonce: EncryptionNonce, - password: string, -): Promise<OfficerAccount & { safe: LockedAccount }> { + password?: string, +): Promise<ReserveAccount & { safe?: LockedAccount }> { const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); - const key = stringToBytes(password); + const mergedRnd: EncryptionNonce = extraNonce && password + ? kdf(24, stringToBytes("aml-officer"), extraNonce, getRandomBytesF(24)) + : getRandomBytesF(24); - const localRnd = getRandomBytesF(24); - const mergedRnd: EncryptionNonce = extraNonce - ? kdf(24, stringToBytes("aml-officer"), extraNonce, localRnd) - : localRnd; - - const protectedPrivKey = await encryptWithDerivedKey( + + const protectedPrivKey = password ? await encryptWithDerivedKey( mergedRnd, - key, + stringToBytes(password), eddsaPriv, password, - ); + ) : undefined; const signingKey = eddsaPriv as SigningKey; - const accountId = encodeCrock(eddsaPub) as OfficerId; - const safe = encodeCrock(protectedPrivKey) as LockedAccount; + const accountId = encodeCrock(eddsaPub) as ReservePub; + const safe = protectedPrivKey ? encodeCrock(protectedPrivKey) as LockedAccount : undefined; return { id: accountId, signingKey, safe }; } diff --git a/packages/web-util/src/forms/HtmlIframe.tsx b/packages/web-util/src/forms/HtmlIframe.tsx @@ -0,0 +1,43 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js"; +import { Addon } from "./FormProvider.js"; + +interface Props { + label: TranslatedString; + url: string; + tooltip?: TranslatedString; + help?: TranslatedString; + before?: Addon; + after?: Addon; +} + +export function HtmlIframe({ + before, + url, + after, + label, + tooltip, + help, +}: Props): VNode { + return ( + <div class="sm:col-span-6"> + {before !== undefined && <RenderAddon addon={before} />} + <a + href={url} + target="_blank" + rel="noreferrer" + class="underline cursor-pointer" + > + <p>{label}</p> + </a> + <iframe src={url} title={label} class="w-full"></iframe> + {after !== undefined && <RenderAddon addon={after} />} + {help && ( + <p class="mt-2 text-sm text-gray-500" id="email-description"> + {help} + </p> + )} + </div> + ); +} diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts @@ -20,12 +20,14 @@ import { } from "../index.browser.js"; import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util"; import { UIFormFieldBaseConfig, UIFormElementConfig } from "./ui-form.js"; +import { HtmlIframe } from "./HtmlIframe.js"; /** * Constrain the type with the ui props */ type FieldType<T extends object = any, K extends keyof T = any> = { group: Parameters<typeof Group>[0]; caption: Parameters<typeof Caption>[0]; + htmlIframe: Parameters<typeof HtmlIframe>[0]; array: Parameters<typeof InputArray<T, K>>[0]; file: Parameters<typeof InputFile<T, K>>[0]; selectOne: Parameters<typeof InputSelectOne<T, K>>[0]; @@ -46,6 +48,7 @@ type FieldType<T extends object = any, K extends keyof T = any> = { export type UIFormField = | { type: "group"; properties: FieldType["group"] } | { type: "caption"; properties: FieldType["caption"] } + | { type: "htmlIframe"; properties: FieldType["htmlIframe"] } | { type: "array"; properties: FieldType["array"] } | { type: "file"; properties: FieldType["file"] } | { type: "amount"; properties: FieldType["amount"] } @@ -85,6 +88,7 @@ type UIFormFieldMap = { const UIFormConfiguration: UIFormFieldMap = { group: Group, caption: Caption, + htmlIframe: HtmlIframe, //@ts-ignore array: InputArray, text: InputText, @@ -173,6 +177,16 @@ export function convertUiField( }; return resp; } + case "htmlIframe": { + const resp: UIFormField = { + type: config.type, + properties: { + ...converBaseFieldsProps(i18n_, config), + url: config.url + }, + }; + return resp; + } case "group": { const resp: UIFormField = { type: config.type, diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts @@ -1,5 +1,6 @@ export * from "./Calendar.js" export * from "./Caption.js" +export * from "./HtmlIframe.js" export * from "./DefaultForm.js" export * from "./Dialog.js" export * from "./FormProvider.js" diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts @@ -3,11 +3,13 @@ import { buildCodecForUnion, Codec, codecForBoolean, + codecForCanonBaseUrl, codecForConstString, codecForLazy, codecForList, codecForNumber, codecForString, + codecForStringURL, codecForTimestamp, codecOptional, Integer, @@ -38,6 +40,7 @@ export type DoubleColumnFormSection = { export type UIFormElementConfig = | UIFormElementGroup | UIFormElementCaption + | UIFormElementHtmlIframe | UIFormFieldAbsoluteTime | UIFormFieldAmount | UIFormFieldArray @@ -73,6 +76,10 @@ type UIFormFieldArray = { } & UIFormFieldBaseConfig; type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription; +type UIFormElementHtmlIframe = { + type: "htmlIframe"; + url: string; +} & UIFieldElementDescription; type UIFormElementGroup = { type: "group"; @@ -227,6 +234,12 @@ const codecForUiFormFieldCaption = (): Codec<UIFormElementCaption> => .property("type", codecForConstString("caption")) .build("UIFormFieldCaption"); +const codecForUiFormFieldHtmlIFrame = (): Codec<UIFormElementHtmlIframe> => + codecForUIFormFieldBaseDescriptionTemplate<UIFormElementHtmlIframe>() + .property("type", codecForConstString("htmlIframe")) + .property("url", codecForStringURL()) + .build("codecForUiFormFieldHtmlIFrame"); + const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> => buildCodecForObject<SelectUiChoice>() .property("description", codecOptional(codecForString())) @@ -309,6 +322,7 @@ const codecForUiFormField = (): Codec<UIFormElementConfig> => .alternative("absoluteTimeText", codecForUiFormFieldAbsoluteTime()) .alternative("amount", codecForUiFormFieldAmount()) .alternative("caption", codecForUiFormFieldCaption()) + .alternative("htmlIframe", codecForUiFormFieldHtmlIFrame()) .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal()) .alternative("choiceStacked", codecForUiFormFieldChoiceStacked()) .alternative("file", codecForUiFormFieldFile())