taler-typescript-core

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

commit 3263d18f525ac458a0914119a5e9174480616ae1
parent 7e7395c20506a4e48cdbcf705a4752bf7f26dc88
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 14 Apr 2025 23:41:31 -0300

fix kyc spa: accept-tos form

Diffstat:
Mpackages/challenger-ui/build.mjs | 1+
Mpackages/kyc-ui/build.mjs | 1+
Mpackages/kyc-ui/src/pages/FillForm.tsx | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mpackages/web-util/src/forms/fields/ExternalLink.tsx | 12++++++++++--
Mpackages/web-util/src/forms/fields/InputDownloadLink.tsx | 4++--
Mpackages/web-util/src/forms/fields/InputToggle.stories.tsx | 20++++++++++++++++++++
Mpackages/web-util/src/forms/fields/InputToggle.tsx | 41++++++++++++++++++++++++++++++++++-------
Mpackages/web-util/src/forms/forms-types.ts | 20++++++++++++++++++--
Mpackages/web-util/src/forms/forms-utils.ts | 32++++++++++++++++++++------------
Mpackages/web-util/src/forms/gana/GLS_Onboarding.ts | 2++
Mpackages/web-util/src/forms/gana/accept-tos.stories.tsx | 12++++++++----
Mpackages/web-util/src/forms/gana/accept-tos.ts | 16+++++++++++-----
Mpackages/web-util/src/index.build.ts | 123+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
13 files changed, 308 insertions(+), 115 deletions(-)

diff --git a/packages/challenger-ui/build.mjs b/packages/challenger-ui/build.mjs @@ -19,6 +19,7 @@ import { build } from "@gnu-taler/web-util/build"; await build({ type: "production", + importMeta: import.meta, source: { js: ["src/main.js","src/index.tsx"], assets: [{ diff --git a/packages/kyc-ui/build.mjs b/packages/kyc-ui/build.mjs @@ -19,6 +19,7 @@ import { build } from "@gnu-taler/web-util/build"; await build({ type: "production", + importMeta: import.meta, source: { js: ["src/index.tsx"], assets: [{ diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -23,12 +23,16 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + AcceptTermOfServiceContext, + Attention, Button, ErrorsSummary, FormMetadata, FormUI, InternationalizationAPI, + Loading, LocalNotificationBanner, + useAsyncAsHook, useExchangeApiContext, useForm, useLocalNotificationHandler, @@ -39,6 +43,7 @@ import { usePreferences } from "../context/preferences.js"; import { useUiFormsContext } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; import { TalerFormAttributes } from "@gnu-taler/taler-util"; +import { KycRequirementInformationId } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 103; @@ -66,37 +71,60 @@ type KycForm = { payload: object; }; -export function FillForm({ - token, +async function getContextByFormId( + id: string, + requirement: KycRequirementInformation, +): Promise<object> { + if (id === "accept-tos") { + const reqContx: any = requirement.context; + if (!reqContx) { + throw Error( + "accept-tos form requires context with 'tos_url' property. No context present.", + ); + } + const tos_url = reqContx["tos_url"]; + if (!tos_url) { + throw Error( + "accept-tos form requires context with 'tos_url' property. No URL present.", + ); + } + + const resp = await fetch(tos_url); + const tosVersion = resp.headers.get("Taler-Terms-Version"); + + if (!tosVersion) { + throw Error( + "accept-tos form requires 'Taler-Terms-Version' in request response to the 'tos_url'", + ); + } + + const ctx: AcceptTermOfServiceContext = { + tos_url, + tosVersion, + expiration_time: reqContx["expiration_time"], + provider_name: reqContx["provider_name"], + successor_measure: reqContx["successor_measure"], + }; + return ctx; + } + return requirement.context ?? {}; +} + +function ShowForm({ + theForm, formId, - requirement, + reqId, onComplete, -}: Props): VNode { - const { i18n } = useTranslationContext(); +}: { + theForm: FormMetadata; + formId: string; + reqId: KycRequirementInformationId; + onComplete: () => void; +}): VNode { const { lib } = useExchangeApiContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [preferences] = usePreferences(); - - 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); - const reqId = requirement.id; - if (!theForm) { - return <div>form with id {formId} not found</div>; - } - if (!reqId) { - return <div>no id for this form, can't upload</div>; - } + const { i18n } = useTranslationContext(); const { model: handler, status } = useForm<FormType>(theForm.config, {}); const validatedForm = status.status !== "ok" ? undefined : status.result; @@ -107,8 +135,8 @@ export function FillForm({ : withErrorHandler( async () => { // FIXME: remove this one after https://bugs.gnunet.org/view.php?id=9715 is closed - validatedForm.form_id = formId - validatedForm[TalerFormAttributes.FORM_ID] = formId + validatedForm.form_id = formId; + validatedForm[TalerFormAttributes.FORM_ID] = formId; return lib.exchange.uploadKycForm(reqId, validatedForm); }, (res) => { @@ -170,6 +198,61 @@ export function FillForm({ ); } +export function FillForm({ + token, + formId, + requirement, + onComplete, +}: Props): VNode { + const { i18n } = useTranslationContext(); + + const { forms } = useUiFormsContext(); + const hook = useAsyncAsHook(() => getContextByFormId(formId, requirement)); + if (hook === undefined) { + return <Loading />; + } + if (hook.hasError) { + return ( + <Attention + title={i18n.str`Could not load context information`} + type="danger" + > + {hook.operational ? hook.details.hint : hook.message} + </Attention> + ); + } + const formContext = hook.response; + const theForm = searchForm(i18n, forms, formId, formContext); + const reqId = requirement.id; + if (!theForm) { + return ( + <Attention title={i18n.str`Could not find form`} type="danger"> + <i18n.Translate> + Form with id '${formId}' is not registered in this application. + </i18n.Translate> + </Attention> + ); + } + if (!reqId) { + return ( + <Attention title={i18n.str`Can't upload information`} type="danger"> + <i18n.Translate> + The KYC requirement doesn't have an ID + </i18n.Translate> + </Attention> + ); + } + + return ( + <ShowForm + formId={formId} + onComplete={onComplete} + reqId={reqId} + theForm={theForm} + /> + ); +} + function searchForm( i18n: InternationalizationAPI, forms: FormMetadata[], diff --git a/packages/web-util/src/forms/fields/ExternalLink.tsx b/packages/web-util/src/forms/fields/ExternalLink.tsx @@ -1,7 +1,8 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { RenderAddon } from "./InputLine.js"; -import { Addon } from "../FormProvider.js"; +import { Addon, UIFormProps } from "../FormProvider.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; interface Props { label: TranslatedString; @@ -20,8 +21,12 @@ export function ExternalLink({ url, media, tooltip, + handler, + name, help, -}: Props): VNode { +}: Props & UIFormProps<boolean>): VNode { + const { value, onChange, error } = + handler ?? noHandlerPropsAndNoContextForField(name); return ( <div class="sm:col-span-6"> {before !== undefined && <RenderAddon addon={before} />} @@ -30,6 +35,9 @@ export function ExternalLink({ class="underline text-blue-600 hover:text-blue-900 visited:text-purple-600" target="_blank" rel="noreferrer" + onClick={() => { + onChange(true); + }} > {label} </a> diff --git a/packages/web-util/src/forms/fields/InputDownloadLink.tsx b/packages/web-util/src/forms/fields/InputDownloadLink.tsx @@ -1,8 +1,8 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; -import { RenderAddon } from "./InputLine.js"; -import { Addon, UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "../../index.browser.js"; +import { Addon, UIFormProps } from "../FormProvider.js"; +import { RenderAddon } from "./InputLine.js"; interface Props { label: TranslatedString; diff --git a/packages/web-util/src/forms/fields/InputToggle.stories.tsx b/packages/web-util/src/forms/fields/InputToggle.stories.tsx @@ -88,3 +88,23 @@ export const StartUndefinedOnlyTwoStates = tests.createExample( }, }, ); + +export const UseTrueValue = tests.createExample( + TestedComponent, + { + initial: {}, + design: { + type: "single-column", + fields: [ + { + type: "toggle", + label: "do you accept?" as TranslatedString, + required: true, + id: "accept", + trueValue: "YES", + onlyTrueValue: true, + }, + ], + }, + }, +); diff --git a/packages/web-util/src/forms/fields/InputToggle.tsx b/packages/web-util/src/forms/fields/InputToggle.tsx @@ -10,14 +10,30 @@ import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; * FIXME: Types would be clearer if two/tri state were different types. */ export function InputToggle( - props: { threeState: boolean; defaultValue?: boolean } & UIFormProps<boolean>, + props: { + threeState: boolean; + defaultValue?: boolean; + trueValue?: any; + falseValue?: any; + onlyTrueValue?: boolean; + } & UIFormProps<boolean>, ): VNode { - const { label, tooltip, help, required, threeState, disabled } = props; + const { + label, + tooltip, + help, + required, + threeState, + disabled, + trueValue = true, + falseValue = false, + onlyTrueValue = false, + } = props; const { value, onChange, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); const [dirty, setDirty] = useState<boolean>(); - const isOn = !!value; + const isOn = trueValue === value; if (props.hidden) { return <Fragment />; @@ -42,15 +58,26 @@ export function InputToggle( aria-describedby="availability-description" onClick={() => { setDirty(true); - if (value === false && threeState) { + if (value === falseValue && threeState) { return onChange(undefined as any); - } else { - return onChange(!isOn as any); } + if (onlyTrueValue && value === trueValue) { + return onChange(undefined as any); + } + if (value === trueValue) { + return onChange(falseValue); + } + return onChange(trueValue); }} > <span - data-state={isOn ? "on" : value === undefined && threeState ? "undefined" : "off"} + data-state={ + isOn + ? "on" + : value === undefined && threeState + ? "undefined" + : "off" + } class="translate-x-6 data-[state=off]:translate-x-0 data-[state=undefined]:translate-x-3 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" ></span> </button> diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -18,6 +18,7 @@ import { buildCodecForObject, buildCodecForUnion, Codec, + codecForAny, codecForBoolean, codecForConstString, codecForLazy, @@ -154,7 +155,7 @@ type UIFormElementExternalLink = { type: "external-link"; url: string; media?: string; -} & UIFieldElementDescription; +} & UIFormFieldBaseConfig; type UIFormElementHtmlIframe = { type: "htmlIframe"; @@ -230,6 +231,18 @@ type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig; type UIFormFieldToggle = { type: "toggle"; threeState?: boolean; + /** + * When the toggle is true use the field is going to have this value. + */ + trueValue?: any; + /** + * When the toggle is false use the field is going to have this value. + */ + falseValue?: any; + /** + * Only true value, when the google is false the field is going to be undefined. + */ + onlyTrueValue?: boolean; } & UIFormFieldBaseConfig; export type ComputableFieldConfig = { @@ -396,7 +409,7 @@ const codecForUIFormElementLink = (): Codec<UIFormElementDownloadLink> => const codecForUIFormElementExternalLink = (): Codec<UIFormElementExternalLink> => - codecForUIFormFieldBaseDescriptionTemplate<UIFormElementExternalLink>() + codecForUIFormFieldBaseConfigTemplate<UIFormElementExternalLink>() .property("type", codecForConstString("external-link")) .property("url", codecForString()) .property("media", codecOptional(codecForString())) @@ -502,6 +515,9 @@ const codecForUiFormFieldTextArea = (): Codec<UIFormFieldTextArea> => const codecForUiFormFieldToggle = (): Codec<UIFormFieldToggle> => codecForUIFormFieldBaseConfigTemplate<UIFormFieldToggle>() .property("threeState", codecOptionalDefault(codecForBoolean(), false)) + .property("falseValue", codecForAny()) + .property("trueValue", codecForAny()) + .property("onlyTrueValue", codecOptionalDefault(codecForBoolean(), false)) .property("type", codecForConstString("toggle")) .build("UIFormFieldToggle"); 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 "external-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 "htmlIframe": { const resp: UIFormField = { type: config.type, @@ -109,6 +97,23 @@ export function convertFormConfigToUiField( }, } as UIFormField; } + case "external-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, + }, + }; + } case "download-link": { return { type: config.type, @@ -361,6 +366,9 @@ export function convertFormConfigToUiField( getConverterByFieldType(config.type, config), ), threeState: config.threeState, + trueValue: config.trueValue, + falseValue: config.falseValue, + onlyTrueValue: config.onlyTrueValue, }, } as UIFormField; } diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts b/packages/web-util/src/forms/gana/GLS_Onboarding.ts @@ -61,6 +61,8 @@ export function GLS_Onboarding( }, { 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", diff --git a/packages/web-util/src/forms/gana/accept-tos.stories.tsx b/packages/web-util/src/forms/gana/accept-tos.stories.tsx @@ -31,10 +31,14 @@ export const EmptyForm = tests.createExample(DefaultForm, { // [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: "Taler Operations AG", - }), + design: acceptTos( + i18n, + { + tos_url: "https://exchange.demo.taler.net/terms", + provider_name: "Taler Operations AG", + tosVersion: "v1", + }, + ), }); 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 @@ -35,6 +35,7 @@ export type AcceptTermOfServiceContext = { provider_name?: string; expiration_time?: TalerProtocolDuration; successor_measure?: string; + tosVersion: string; }; // Example context @@ -56,17 +57,20 @@ export const acceptTos = ( context: AcceptTermOfServiceContext, ): SingleColumnFormDesign => ({ type: "single-column" as const, - fields: filterUndefined<UIFormElementConfig>([ + fields: [ { type: "external-link", + id: TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE, + required: true, url: context.tos_url, - label: context.provider_name ?? context.tos_url, + //label: context.provider_name ?? context.tos_url, + label: i18n.str`View in Browser`, }, { type: "download-link", id: TalerFormAttributes.DOWNLOADED_TERMS_OF_SERVICE, url: context.tos_url, - label: "Download PDF version", + label: i18n.str`Download PDF version`, required: true, media: "application/pdf", help: i18n.str`You must download to proceed`, @@ -75,7 +79,9 @@ export const acceptTos = ( type: "toggle", id: TalerFormAttributes.ACCEPTED_TERMS_OF_SERVICE, required: true, - label: i18n.str`Do you accept terms of service?`, + trueValue: context.tosVersion, + onlyTrueValue: true, + label: i18n.str`Do you accept the terms of service?`, }, - ]), + ], }); diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts @@ -6,10 +6,6 @@ import postcss from "postcss"; import sass from "sass"; import postcssrc from "postcss-load-config"; -// this should give us the current directory where -// the project is being built -const BASE = process.cwd(); - type Assets = { base: string; files: string[]; @@ -44,24 +40,9 @@ export function getFilesInDirectory(startPath: string, regex?: RegExp): Assets { }; } -let GIT_ROOT = BASE; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} -if (GIT_ROOT === "/") { - // eslint-disable-next-line no-undef - console.log("not found"); - // eslint-disable-next-line no-undef - process.exit(1); -} -const GIT_HASH = git_hash(); - -const buf = fs.readFileSync(path.join(BASE, "package.json")); -let _package = JSON.parse(buf.toString("utf-8")); - -function git_hash() { +function git_hash(root: string) { const rev = fs - .readFileSync(path.join(GIT_ROOT, ".git", "HEAD")) + .readFileSync(path.join(root, ".git", "HEAD")) .toString() .trim() .split(/.*[: ]/) @@ -69,7 +50,10 @@ function git_hash() { if (rev.indexOf("/") === -1) { return rev; } else { - return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim(); + return fs + .readFileSync(path.join(root, ".git", rev)) + .toString() + .trim(); } } @@ -121,50 +105,48 @@ const sassPlugin: esbuild.Plugin = { }, }; - /** - * Problem: + * Problem: * No loader is configured for ".node" files: ../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node - * + * * Reference: * https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487 */ const nativeNodeModulesPlugin: esbuild.Plugin = { - name: 'native-node-modules', + name: "native-node-modules", setup(build) { - // If a ".node" file is imported within a module in the "file" namespace, resolve + // If a ".node" file is imported within a module in the "file" namespace, resolve // it to an absolute path and put it into the "node-file" virtual namespace. - build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({ + build.onResolve({ filter: /\.node$/, namespace: "file" }, (args) => ({ path: require.resolve(args.path, { paths: [args.resolveDir] }), - namespace: 'node-file', - })) + namespace: "node-file", + })); // Files in the "node-file" virtual namespace call "require()" on the // path from esbuild of the ".node" file in the output directory. - build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({ + build.onLoad({ filter: /.*/, namespace: "node-file" }, (args) => ({ contents: ` import path from ${JSON.stringify(args.path)} try { module.exports = require(path) } catch {} `, - })) + })); // If a ".node" file is imported within a module in the "node-file" namespace, put // it in the "file" namespace where esbuild's default loading behavior will handle // it. It is already an absolute path since we resolved it to one above. - build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({ + build.onResolve({ filter: /\.node$/, namespace: "node-file" }, (args) => ({ path: args.path, - namespace: 'file', - })) + namespace: "file", + })); // Tell esbuild's default loading behavior to use the "file" loader for // these ".node" files. - let opts = build.initialOptions - opts.loader = opts.loader || {} - opts.loader['.node'] = 'file' + let opts = build.initialOptions; + opts.loader = opts.loader || {}; + opts.loader[".node"] = "file"; }, -} - +}; const postCssPlugin: esbuild.Plugin = { name: "custom-build-postcss", @@ -227,25 +209,54 @@ const defaultEsBuildConfig: esbuild.BuildOptions = { react: "preact/compat", "react-dom": "preact/compat", }, - define: { - __VERSION__: `"${_package.version}"`, - __GIT_HASH__: `"${GIT_HASH}"`, - }, }; - +import nodePath from "node:path"; +import nodeUrl from "node:url"; export interface BuildParams { type: "development" | "test" | "production"; + /** + * Assign this to import.meta so the script can be called from any directory + */ + importMeta?: ImportMeta; source: { assets: Assets | Assets[]; js: string[]; }; public?: string; + /** + * Location directory of the output. Can be redirected with INSTALL_DIR environment + */ destination: string; css: "sass" | "postcss" | "linaria"; linariaPlugin?: () => esbuild.Plugin; } +function getPackageAndGitRoot(meta: undefined | ImportMeta) { + if (meta) { + const root = nodePath.dirname(nodeUrl.fileURLToPath(meta.url)); + process.chdir(root); + } + const baseDir = process.cwd(); + + let GIT_ROOT = baseDir; + while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { + GIT_ROOT = path.join(GIT_ROOT, "../"); + } + if (GIT_ROOT === "/") { + throw Error(`could not found git root from ${meta}`); + } + const hash = git_hash(GIT_ROOT); + + const buf = fs.readFileSync(path.join(baseDir, "package.json")); + const pkg = JSON.parse(buf.toString("utf-8")); + return { pkg, hash, baseDir }; +} + export function computeConfig(params: BuildParams): esbuild.BuildOptions { + const { pkg: _package, hash: GIT_HASH, baseDir } = getPackageAndGitRoot( + params.importMeta, + ); + const plugins: Array<esbuild.Plugin> = [ copyFilesPlugin(params.source.assets), ]; @@ -289,15 +300,17 @@ export function computeConfig(params: BuildParams): esbuild.BuildOptions { return { ...defaultEsBuildConfig, + absWorkingDir: baseDir, entryPoints: params.source.js, publicPath: params.public, - outdir: params.destination, + outdir: process.env.INSTALL_DIR ?? params.destination, treeShaking: true, minify: false, //params.type === "production", sourcemap: true, //params.type !== "production", define: { - ...defaultEsBuildConfig.define, "process.env.NODE_ENV": JSON.stringify(params.type), + __VERSION__: `"${_package.version}"`, + __GIT_HASH__: `"${GIT_HASH}"`, }, plugins, }; @@ -307,17 +320,22 @@ export function computeConfig(params: BuildParams): esbuild.BuildOptions { * Build sources for prod environment */ export async function build(config: BuildParams) { - const res = await esbuild.build(computeConfig(config)); - fs.writeFileSync(`${config.destination}/version.txt`, `${_package.version}`); + const options = computeConfig(config); + const res = await esbuild.build(options); + fs.writeFileSync( + `${config.destination}/version.txt`, + options.define ? options.define["__VERSION__"] : "-", + ); return res; } -const LIVE_RELOAD_SCRIPT = "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs"; +const LIVE_RELOAD_SCRIPT = + "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs"; const LIVE_RELOAD_SCRIPT_LOCALLY = "./lib/live-reload.mjs"; /** * Do startup for development environment - * + * * To be used from web-utils project */ export function initializeDevOnWebUtils( @@ -331,10 +349,9 @@ export function initializeDevOnWebUtils( return buildDevelopment; } - /** * Do startup for development environment - * + * * To be used when web-utils is a library */ export function initializeDev(