commit e1766b10e3cb3c7aa42144cada05b65239c9f976
parent 265be15fdbd47159f79f7e608bf16e184edba740
Author: Sebastian <sebasjm@gmail.com>
Date: Tue, 1 Apr 2025 15:05:40 -0300
fix #9640
Diffstat:
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";