taler-typescript-core

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

commit e1766b10e3cb3c7aa42144cada05b65239c9f976
parent 265be15fdbd47159f79f7e608bf16e184edba740
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue,  1 Apr 2025 15:05:40 -0300

fix #9640

Diffstat:
Dpackages/web-util/src/forms/DownloadLink.tsx | 71-----------------------------------------------------------------------
Apackages/web-util/src/forms/InputDownloadLink.tsx | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/field-types.ts | 6+++---
Mpackages/web-util/src/forms/fields/InputLine.tsx | 20+++++++++-----------
Mpackages/web-util/src/forms/fields/InputToggle.tsx | 3++-
Mpackages/web-util/src/forms/forms-types.ts | 4++--
Mpackages/web-util/src/forms/forms-utils.ts | 30++++++++++++++++++------------
Mpackages/web-util/src/forms/gana/VQF_902_1_customer.ts | 3+++
Apackages/web-util/src/forms/gana/accept-tos.stories.tsx | 42++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/gana/accept-tos.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/gana/index.stories.ts | 10+---------
Mpackages/web-util/src/forms/index.ts | 1+
12 files changed, 253 insertions(+), 109 deletions(-)

diff --git a/packages/web-util/src/forms/DownloadLink.tsx b/packages/web-util/src/forms/DownloadLink.tsx @@ -1,71 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; -import { RenderAddon } from "./fields/InputLine.js"; -import { Addon } from "./FormProvider.js"; - -interface Props { - label: TranslatedString; - url: string; - media?: string; - tooltip?: TranslatedString; - help?: TranslatedString; - before?: Addon; - after?: Addon; -} - -export function DownloadLink({ - before, - after, - label, - url, - media, - tooltip, - help, -}: Props): VNode { - return ( - <div class="sm:col-span-6"> - {before !== undefined && <RenderAddon addon={before} />} - <a - href="#" - onClick={(e) => { - return ( - fetch(url, { - headers: { - "Content-Type": media ?? "text/html", - }, - cache: "no-cache", - }) - // .then((r) => r.text()) - .then((r) => r.arrayBuffer()) - .then((r) => { - const b64 = window.btoa( - new Uint8Array(r).reduce( - (data, byte) => data + String.fromCharCode(byte), - "", - ), - ); - - const a = document.createElement("a"); - a.href = `data:${media ?? "text/html"};base64,${b64}`; - a.download = ""; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - return; - }) - ); - }} - media={media} - download - > - {label} - </a> - {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/InputDownloadLink.tsx b/packages/web-util/src/forms/InputDownloadLink.tsx @@ -0,0 +1,83 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { RenderAddon } from "./fields/InputLine.js"; +import { Addon, UIFormProps } from "./FormProvider.js"; +import { noHandlerPropsAndNoContextForField } from "../index.browser.js"; + +interface Props { + label: TranslatedString; + url: string; + media?: string; + tooltip?: TranslatedString; + help?: TranslatedString; + before?: Addon; + after?: Addon; +} + +export function InputDownloadLink(props: Props & UIFormProps<boolean>): VNode { + const { + media, + url, + label, + tooltip, + help, + required, + disabled, + before, + after, + } = props; + const { value, onChange, error } = + props.handler ?? noHandlerPropsAndNoContextForField(props.name); + + return ( + <div class="col-span-6" data-downloaded={!!value}> + {before !== undefined && <RenderAddon addon={before} />} + <a + href="#" + onClick={(e) => { + onChange(true); + return ( + fetch(url, { + headers: { + "Content-Type": media ?? "text/html", + }, + cache: "no-cache", + }) + // .then((r) => r.text()) + .then((r) => r.arrayBuffer()) + .then((r) => { + const b64 = window.btoa( + new Uint8Array(r).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); + + const a = document.createElement("a"); + a.href = `data:${media ?? "text/html"};base64,${b64}`; + a.download = ""; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + return; + }) + ); + }} + media={media} + download + > + {label} + </a> + {required ? ( + <span class="text-sm leading-6 text-red-600 pl-2">*</span> + ) : undefined} + + {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/field-types.ts b/packages/web-util/src/forms/field-types.ts @@ -1,6 +1,6 @@ import { VNode } from "preact"; import { Caption } from "./Caption.js"; -import { DownloadLink } from "./DownloadLink.js"; +import { InputDownloadLink } from "./InputDownloadLink.js"; import { ExternalLink } from "./ExternalLink.js"; import { InputAbsoluteTime } from "./fields/InputAbsoluteTime.js"; import { InputAmount } from "./fields/InputAmount.js"; @@ -27,7 +27,7 @@ import { HtmlIframe } from "./HtmlIframe.js"; type FieldType<T extends object = any, K extends keyof T = any> = { group: Parameters<typeof Group>[0]; caption: Parameters<typeof Caption>[0]; - "download-link": Parameters<typeof DownloadLink>[0]; + "download-link": Parameters<typeof InputDownloadLink>[0]; "external-link": Parameters<typeof ExternalLink>[0]; htmlIframe: Parameters<typeof HtmlIframe>[0]; array: Parameters<typeof InputArray>[0]; @@ -108,7 +108,7 @@ type UIFormFieldMap = { */ export const UIFormConfiguration: UIFormFieldMap = { group: Group, - "download-link": DownloadLink, + "download-link": InputDownloadLink, "external-link": ExternalLink, caption: Caption, htmlIframe: HtmlIframe, diff --git a/packages/web-util/src/forms/fields/InputLine.tsx b/packages/web-util/src/forms/fields/InputLine.tsx @@ -33,16 +33,14 @@ export function LabelWithTooltipMaybeRequired({ name?: string; }): VNode { const Label = ( - <Fragment> - <div class="flex justify-between"> - <label - for={name} - class="block text-sm font-medium leading-6 text-gray-900" - > - {label} - </label> - </div> - </Fragment> + <div class="flex justify-between"> + <label + for={name} + class="block text-sm font-medium leading-6 text-gray-900" + > + {label} + </label> + </div> ); const WithTooltip = tooltip ? ( <div class="relative flex flex-grow items-stretch focus-within:z-10"> @@ -170,7 +168,7 @@ function defaultFromString(v: string) { type InputType = "text" | "text-area" | "password" | "email" | "number"; export function InputLine( - props: { type: InputType, defaultValue?: string } & UIFormProps<string>, + props: { type: InputType; defaultValue?: string } & UIFormProps<string>, ): VNode { const { name, diff --git a/packages/web-util/src/forms/fields/InputToggle.tsx b/packages/web-util/src/forms/fields/InputToggle.tsx @@ -12,7 +12,7 @@ import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; export function InputToggle( props: { threeState: boolean; defaultValue?: boolean } & UIFormProps<boolean>, ): VNode { - const { label, tooltip, help, required, threeState } = props; + const { label, tooltip, help, required, threeState, disabled } = props; const { value, onChange, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); const [dirty, setDirty] = useState<boolean>(); @@ -37,6 +37,7 @@ export function InputToggle( data-state={isOn ? "on" : value === undefined ? "undefined" : "off"} class="bg-indigo-600 data-[state=off]:bg-gray-200 data-[state=undefined]:bg-gray-200 relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" + disabled={disabled} aria-labelledby="availability-label" aria-describedby="availability-description" onClick={() => { diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -148,7 +148,7 @@ type UIFormElementDownloadLink = { type: "download-link"; url: string; media?: string; -} & UIFieldElementDescription; +} & UIFormFieldBaseConfig; type UIFormElementExternalLink = { type: "external-link"; @@ -388,7 +388,7 @@ const codecForUiFormFieldCaption = (): Codec<UIFormElementCaption> => .build("UIFormFieldCaption"); const codecForUIFormElementLink = (): Codec<UIFormElementDownloadLink> => - codecForUIFormFieldBaseDescriptionTemplate<UIFormElementDownloadLink>() + codecForUIFormFieldBaseConfigTemplate<UIFormElementDownloadLink>() .property("type", codecForConstString("download-link")) .property("url", codecForString()) .property("media", codecOptional(codecForString())) diff --git a/packages/web-util/src/forms/forms-utils.ts b/packages/web-util/src/forms/forms-utils.ts @@ -41,18 +41,6 @@ export function convertFormConfigToUiField( }; return resp; } - case "download-link": { - const resp: UIFormField = { - type: config.type, - properties: { - ...convertBaseFieldsProps(i18n_, config), - label: i18n_.str`${config.label}`, - url: config.url, - media: config.media, - }, - }; - return resp; - } case "external-link": { const resp: UIFormField = { type: config.type, @@ -121,6 +109,24 @@ export function convertFormConfigToUiField( }, } as UIFormField; } + case "download-link": { + return { + type: config.type, + properties: { + ...convertBaseFieldsProps(i18n_, config), + ...convertInputFieldsProps( + name, + handler, + config, + getConverterByFieldType(config.type, config), + ), + label: i18n_.str`${config.label}`, + url: config.url, + media: config.media, + }, + }; + // return resp; + } case "absoluteTimeText": { return { type: "absoluteTimeText", diff --git a/packages/web-util/src/forms/gana/VQF_902_1_customer.ts b/packages/web-util/src/forms/gana/VQF_902_1_customer.ts @@ -29,6 +29,9 @@ const Descr = { member act as director of a domiciliary company, this domiciliary company is the customer.`, } as const; +// This form is used in KYC spa and AML spa +// that's why we want the description and label to be in one place +// FIXME: check if we wan't all forms description to be in web-utils export const form_vqf_902_1_customer = (i18n: InternationalizationAPI) => ({ label: i18n.str`Identification Form (customer)`, description: i18n.str`The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold.`, diff --git a/packages/web-util/src/forms/gana/accept-tos.stories.tsx b/packages/web-util/src/forms/gana/accept-tos.stories.tsx @@ -0,0 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { i18n, setupI18n, TalerFormAttributes } from "@gnu-taler/taler-util"; +import * as tests from "../../tests/hook.js"; +import { DefaultForm } from "../forms-ui.js"; +import { acceptTos } from "./accept-tos.js"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; + +setupI18n("en", {}); + +export const EmptyForm = tests.createExample(DefaultForm, { + initial: { + // [TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE]: false, + // [TalerFormAttributes.ACCEPTED_TERMS_OF_SERVICE]: false, + }, + design: acceptTos(i18n, { + tos_url: "https://exchange.demo.taler.net/terms", + provider_name: "Demo Exchange", + }), +}); + +export default { title: "accept tos" }; diff --git a/packages/web-util/src/forms/gana/accept-tos.ts b/packages/web-util/src/forms/gana/accept-tos.ts @@ -0,0 +1,89 @@ +/* + 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 { + TalerFormAttributes, + TalerProtocolDuration, +} from "@gnu-taler/taler-util"; +import type { + InternationalizationAPI, + SingleColumnFormDesign, + UIFormElementConfig, +} from "@gnu-taler/web-util/browser"; + +const TALER_SCREEN_ID = 107; + +function filterUndefined<T>(ar: Array<T | undefined>): Array<T> { + return ar.filter((a): a is T => !!a); +} +export type AcceptTermOfServiceContext = { + tos_url: string; + provider_name?: string; + expiration_time?: TalerProtocolDuration; + successor_measure?: string; +}; + +// Example context +// { +// "tos_url":"https://exchange.taler-ops.ch/terms", +// "provider_name":"Taler Operations AG", +// "expiration_time":{"d_us": 157680000000000}, +// "successor_measure":"accept-tos" +// } + +/** + * + * @param i18n + * @param context + * @returns + */ +export const acceptTos = ( + i18n: InternationalizationAPI, + context: AcceptTermOfServiceContext, +): SingleColumnFormDesign => ({ + type: "single-column" as const, + fields: filterUndefined<UIFormElementConfig>([ + { + type: "htmlIframe", + label: context?.provider_name ?? `Link`, + url: context.tos_url, + }, + // { + // type: "download-link", + // id: TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE, + // url: context.tos_url, + // label: "Download text version", + // media: "text/plain", + // required: true, + // help: i18n.str`You must download to proceed` + // }, + { + type: "download-link", + id: TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE, + url: context.tos_url, + label: "Download PDF version", + required: true, + media: "application/pdf", + help: i18n.str`You must download to proceed` + }, + { + type: "toggle", + id: TalerFormAttributes.ACCEPTED_TERMS_OF_SERVICE, + required: true, + label: i18n.str`Do you accept terms of service?`, + }, + ]), +}); diff --git a/packages/web-util/src/forms/gana/index.stories.ts b/packages/web-util/src/forms/gana/index.stories.ts @@ -1,19 +1,11 @@ export * as a01 from "./GLS_Onboarding.stories.js"; - export * as a02 from "./VQF_902_1_customer.stories.js"; - export * as a03 from "./VQF_902_1_officer.stories.js"; - export * as a04 from "./VQF_902_11.stories.js"; - export * as a05 from "./VQF_902_14.stories.js"; - export * as a06 from "./VQF_902_4.stories.js"; - export * as a07 from "./VQF_902_5.stories.js"; - export * as a08 from "./VQF_902_9.stories.js"; - export * as a09 from "./generic_note.stories.js"; - export * as a10 from "./generic_upload.stories.js"; +export * as a11 from "./accept-tos.stories.js"; diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts @@ -27,6 +27,7 @@ export * from "./gana/VQF_902_1_officer.js"; export * from "./gana/VQF_902_4.js"; export * from "./gana/VQF_902_5.js"; export * from "./gana/VQF_902_9.js"; +export * from "./gana/accept-tos.js"; export * from "./Group.js"; export * from "./HtmlIframe.js"; export * from "./TimePicker.js";