taler-typescript-core

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

commit 5af053bbaaeca254c619f137f8b54757a6d38a18
parent a90cc0b1ac2bd02ee946294bf4d8e3b5e2487109
Author: Florian Dold <dold@taler.net>
Date:   Tue, 25 Aug 2026 21:16:22 +0200

bank web UI: add tri-state demo and public-link overrides

Diffstat:
Mpackages/libeufin-bank-webui/README.md | 2++
Mpackages/libeufin-bank-webui/src/Routing.tsx | 6+-----
Mpackages/libeufin-bank-webui/src/app.tsx | 13++++++++++---
Mpackages/libeufin-bank-webui/src/developer-settings.test.ts | 40+++++++++++++++++++---------------------
Mpackages/libeufin-bank-webui/src/developer-settings.ts | 50++++++++++++++++++++++++++------------------------
Mpackages/libeufin-bank-webui/src/hooks/preferences.test.ts | 9++-------
Mpackages/libeufin-bank-webui/src/hooks/preferences.ts | 23++---------------------
Mpackages/libeufin-bank-webui/src/pages/BankFrame.stories.tsx | 3+--
Mpackages/libeufin-bank-webui/src/pages/BankFrame.tsx | 21++++-----------------
Mpackages/libeufin-bank-webui/src/pages/DeveloperSettings.stories.tsx | 8++++----
Mpackages/libeufin-bank-webui/src/pages/DeveloperSettings.tsx | 172++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/libeufin-bank-webui/src/settings.json | 1+
Apackages/libeufin-bank-webui/src/settings.test.ts | 40++++++++++++++++++++++++++++++++++++++++
Mpackages/libeufin-bank-webui/src/settings.ts | 34+++++++++++++++++++---------------
Mpackages/taler-harness/src/harness/webui-server.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-libeufin-bank-webui.ts | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
16 files changed, 296 insertions(+), 219 deletions(-)

diff --git a/packages/libeufin-bank-webui/README.md b/packages/libeufin-bank-webui/README.md @@ -20,6 +20,8 @@ to override the Core Bank API base URL for the current browser. ## Customizing Per-Deployment Settings To customize per-deployment settings, serve `settings.json` alongside the UI. +The optional `enableDemoHeader` boolean controls whether the demo header is +shown. It defaults to `false`. The optional `showPublicAccounts` boolean controls whether public accounts are linked from the bank navigation and footer. It defaults to `false`; the public accounts route remains directly reachable when the links are hidden. diff --git a/packages/libeufin-bank-webui/src/Routing.tsx b/packages/libeufin-bank-webui/src/Routing.tsx @@ -51,7 +51,6 @@ import { WireTransfer } from "./pages/WireTransfer.js"; import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js"; import { ActiveWithdrawal } from "./pages/ActiveWithdrawal.js"; import { useSettingsContext } from "./context/settings.js"; -import { shouldShowPublicAccounts } from "./developer-settings.js"; import { AdminNavigation, AdminPrimarySection, @@ -246,10 +245,7 @@ function PrivateRouting({ const location = useCurrentLocation(privatePages); const { config } = useBankCoreApiContext(); const settings = useSettingsContext(); - const showPublicAccounts = shouldShowPublicAccounts( - settings.showPublicAccounts ?? false, - settings.showPublicAccountsOverride, - ); + const showPublicAccounts = settings.showPublicAccounts ?? false; const adminCurrent = getAdminPrimarySection(location.name); const openOperationId = location.name === "operationDetails" || location.name === "startOperation" diff --git a/packages/libeufin-bank-webui/src/app.tsx b/packages/libeufin-bank-webui/src/app.tsx @@ -58,6 +58,7 @@ import { SolveChallengeDialog } from "./pages/SolveMFA.js"; import { DeveloperOverrides, readDeveloperOverrides, + resolveBooleanOverride, writeDeveloperOverrides, } from "./developer-settings.js"; import { DeveloperSettings } from "./pages/DeveloperSettings.js"; @@ -79,8 +80,14 @@ export function App() { const effectiveSettings: UiSettings = { ...settings, - showDemoBannerOverride: developerOverrides.showDemoBanner, - showPublicAccountsOverride: developerOverrides.showPublicAccounts, + enableDemoHeader: resolveBooleanOverride( + settings.enableDemoHeader ?? false, + developerOverrides.overrideEnableDemoHeader, + ), + showPublicAccounts: resolveBooleanOverride( + settings.showPublicAccounts ?? false, + developerOverrides.overrideShowPublicAccounts, + ), }; const baseUrl = getInitialBackendBaseURL( settings.backendBaseURL, @@ -257,7 +264,7 @@ function SubApp({ return ( <DeveloperSettings configuredBackendBaseUrl={settings.backendBaseURL} - configuredShowDemoBanner={settings.showDemoDescription ?? false} + configuredEnableDemoHeader={settings.enableDemoHeader ?? false} configuredShowPublicAccounts={settings.showPublicAccounts ?? false} overrides={developerOverrides} onApply={onUpdateDeveloperOverrides} diff --git a/packages/libeufin-bank-webui/src/developer-settings.test.ts b/packages/libeufin-bank-webui/src/developer-settings.test.ts @@ -15,11 +15,10 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import { COREBANK_API_BASE_URL_OVERRIDE_KEY, - DEMO_BANNER_OVERRIDE_KEY, + ENABLE_DEMO_HEADER_OVERRIDE_KEY, PUBLIC_ACCOUNTS_OVERRIDE_KEY, readDeveloperOverrides, - shouldShowDemoBanner, - shouldShowPublicAccounts, + resolveBooleanOverride, writeDeveloperOverrides, } from "./developer-settings.js"; @@ -45,53 +44,52 @@ describe("developer settings", () => { readDeveloperOverrides( memoryStorage({ [COREBANK_API_BASE_URL_OVERRIDE_KEY]: " https://bank.example/ ", - [DEMO_BANNER_OVERRIDE_KEY]: "true", + [ENABLE_DEMO_HEADER_OVERRIDE_KEY]: "true", [PUBLIC_ACCOUNTS_OVERRIDE_KEY]: "false", }), ), { corebankApiBaseUrl: "https://bank.example/", - showDemoBanner: true, - showPublicAccounts: false, + overrideEnableDemoHeader: true, + overrideShowPublicAccounts: false, }, ); assert.deepEqual( readDeveloperOverrides( memoryStorage({ - [DEMO_BANNER_OVERRIDE_KEY]: "invalid", + [ENABLE_DEMO_HEADER_OVERRIDE_KEY]: "invalid", [PUBLIC_ACCOUNTS_OVERRIDE_KEY]: "invalid", }), ), {}, ); + assert.deepEqual( + readDeveloperOverrides(memoryStorage({ "bank-demo-banner": "true" })), + {}, + ); }); it("writes and clears all overrides", () => { const storage = memoryStorage(); writeDeveloperOverrides(storage, { corebankApiBaseUrl: "https://bank.example/", - showDemoBanner: false, - showPublicAccounts: true, + overrideEnableDemoHeader: false, + overrideShowPublicAccounts: true, }); assert.deepEqual(readDeveloperOverrides(storage), { corebankApiBaseUrl: "https://bank.example/", - showDemoBanner: false, - showPublicAccounts: true, + overrideEnableDemoHeader: false, + overrideShowPublicAccounts: true, }); writeDeveloperOverrides(storage, {}); assert.deepEqual(readDeveloperOverrides(storage), {}); }); - it("lets the developer override take precedence over configuration and preferences", () => { - assert.equal(shouldShowDemoBanner(true, false, undefined), true); - assert.equal(shouldShowDemoBanner(true, true, undefined), false); - assert.equal(shouldShowDemoBanner(false, false, undefined), false); - assert.equal(shouldShowDemoBanner(false, true, true), true); - assert.equal(shouldShowDemoBanner(true, false, false), false); - assert.equal(shouldShowPublicAccounts(false, undefined), false); - assert.equal(shouldShowPublicAccounts(true, undefined), true); - assert.equal(shouldShowPublicAccounts(false, true), true); - assert.equal(shouldShowPublicAccounts(true, false), false); + it("lets an optional boolean override take precedence over configuration", () => { + assert.equal(resolveBooleanOverride(false, undefined), false); + assert.equal(resolveBooleanOverride(true, undefined), true); + assert.equal(resolveBooleanOverride(false, true), true); + assert.equal(resolveBooleanOverride(true, false), false); }); }); diff --git a/packages/libeufin-bank-webui/src/developer-settings.ts b/packages/libeufin-bank-webui/src/developer-settings.ts @@ -12,13 +12,14 @@ */ export const COREBANK_API_BASE_URL_OVERRIDE_KEY = "corebank-api-base-url"; -export const DEMO_BANNER_OVERRIDE_KEY = "bank-demo-banner"; +export const ENABLE_DEMO_HEADER_OVERRIDE_KEY = + "bank-enable-demo-header-override"; export const PUBLIC_ACCOUNTS_OVERRIDE_KEY = "bank-public-accounts"; export interface DeveloperOverrides { corebankApiBaseUrl?: string; - showDemoBanner?: boolean; - showPublicAccounts?: boolean; + overrideEnableDemoHeader?: boolean; + overrideShowPublicAccounts?: boolean; } type ReadableStorage = Pick<Storage, "getItem">; @@ -30,15 +31,17 @@ export function readDeveloperOverrides( if (!storage) return {}; const corebankApiBaseUrl = storage.getItem(COREBANK_API_BASE_URL_OVERRIDE_KEY)?.trim() || undefined; - const showDemoBannerValue = storage.getItem(DEMO_BANNER_OVERRIDE_KEY); - const showDemoBanner = - showDemoBannerValue === "true" + const enableDemoHeaderValue = storage.getItem( + ENABLE_DEMO_HEADER_OVERRIDE_KEY, + ); + const overrideEnableDemoHeader = + enableDemoHeaderValue === "true" ? true - : showDemoBannerValue === "false" + : enableDemoHeaderValue === "false" ? false : undefined; const showPublicAccountsValue = storage.getItem(PUBLIC_ACCOUNTS_OVERRIDE_KEY); - const showPublicAccounts = + const overrideShowPublicAccounts = showPublicAccountsValue === "true" ? true : showPublicAccountsValue === "false" @@ -46,8 +49,12 @@ export function readDeveloperOverrides( : undefined; return { ...(corebankApiBaseUrl ? { corebankApiBaseUrl } : {}), - ...(showDemoBanner === undefined ? {} : { showDemoBanner }), - ...(showPublicAccounts === undefined ? {} : { showPublicAccounts }), + ...(overrideEnableDemoHeader === undefined + ? {} + : { overrideEnableDemoHeader }), + ...(overrideShowPublicAccounts === undefined + ? {} + : { overrideShowPublicAccounts }), }; } @@ -63,30 +70,25 @@ export function writeDeveloperOverrides( } else { storage.removeItem(COREBANK_API_BASE_URL_OVERRIDE_KEY); } - if (overrides.showDemoBanner === undefined) { - storage.removeItem(DEMO_BANNER_OVERRIDE_KEY); + if (overrides.overrideEnableDemoHeader === undefined) { + storage.removeItem(ENABLE_DEMO_HEADER_OVERRIDE_KEY); } else { - storage.setItem(DEMO_BANNER_OVERRIDE_KEY, String(overrides.showDemoBanner)); + storage.setItem( + ENABLE_DEMO_HEADER_OVERRIDE_KEY, + String(overrides.overrideEnableDemoHeader), + ); } - if (overrides.showPublicAccounts === undefined) { + if (overrides.overrideShowPublicAccounts === undefined) { storage.removeItem(PUBLIC_ACCOUNTS_OVERRIDE_KEY); } else { storage.setItem( PUBLIC_ACCOUNTS_OVERRIDE_KEY, - String(overrides.showPublicAccounts), + String(overrides.overrideShowPublicAccounts), ); } } -export function shouldShowDemoBanner( - configured: boolean, - hiddenByPreference: boolean, - override: boolean | undefined, -): boolean { - return override ?? (configured && !hiddenByPreference); -} - -export function shouldShowPublicAccounts( +export function resolveBooleanOverride( configured: boolean, override: boolean | undefined, ): boolean { diff --git a/packages/libeufin-bank-webui/src/hooks/preferences.test.ts b/packages/libeufin-bank-webui/src/hooks/preferences.test.ts @@ -18,13 +18,11 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { codecForPreferences, - defaultPreferences, getAllBooleanPreferences, } from "./preferences.js"; describe("bank interface preferences", () => { const currentPreferences = { - hideDemo: true, showInstallWallet: true, fastWithdrawalForm: false, }; @@ -36,18 +34,15 @@ describe("bank interface preferences", () => { ); }); - it("shows configured demo information by default", () => { - assert.equal(defaultPreferences.hideDemo, false); - }); - it("accepts legacy storage without exposing the preference in settings", () => { assert.doesNotThrow(() => codecForPreferences().decode({ ...currentPreferences, + hideDemo: true, showWithdrawalSuccess: false, }), ); - assert.deepEqual(getAllBooleanPreferences({}), [ + assert.deepEqual(getAllBooleanPreferences(), [ "showInstallWallet", "fastWithdrawalForm", ]); diff --git a/packages/libeufin-bank-webui/src/hooks/preferences.ts b/packages/libeufin-bank-webui/src/hooks/preferences.ts @@ -25,11 +25,8 @@ import { useLocalStorage, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { UiSettings } from "../settings.js"; -import { codecOptionalDefault } from "@gnu-taler/taler-util"; interface Preferences { - hideDemo: boolean; showInstallWallet: boolean; fastWithdrawalForm: boolean; } @@ -37,13 +34,11 @@ interface Preferences { export const codecForPreferences = (): Codec<Preferences> => buildCodecForObject<Preferences>() .allowExtra() - .property("hideDemo", codecOptionalDefault(codecForBoolean(), false)) .property("showInstallWallet", codecForBoolean()) .property("fastWithdrawalForm", codecForBoolean()) .build("Preferences"); -export const defaultPreferences: Preferences = { - hideDemo: false, +const defaultPreferences: Preferences = { showInstallWallet: true, fastWithdrawalForm: false, }; @@ -73,19 +68,7 @@ export function usePreferences(): [ return [value, updateField]; } -export function getAllBooleanPreferences( - settings: UiSettings, -): Array<keyof Preferences> { - if ( - settings.showDemoBannerOverride === undefined && - settings.showDemoDescription - ) { - return [ - "hideDemo", - "showInstallWallet", - "fastWithdrawalForm", - ]; - } +export function getAllBooleanPreferences(): Array<keyof Preferences> { return ["showInstallWallet", "fastWithdrawalForm"]; } @@ -96,8 +79,6 @@ export function getLabelForPreferences( switch (k) { case "fastWithdrawalForm": return i18n.str`Withdraw without setting amount`; - case "hideDemo": - return i18n.str`Hide demo hint.`; case "showInstallWallet": return i18n.str`Show install wallet first`; // case "showDebugInfo": diff --git a/packages/libeufin-bank-webui/src/pages/BankFrame.stories.tsx b/packages/libeufin-bank-webui/src/pages/BankFrame.stories.tsx @@ -33,8 +33,7 @@ export const DemoLayout = tests.createExample( {}, { settings: { - showDemoDescription: true, - showDemoBannerOverride: true, + enableDemoHeader: true, topNavSites: { Introduction: "https://test.taler.net/", Bank: "https://bank.test.taler.net/", diff --git a/packages/libeufin-bank-webui/src/pages/BankFrame.tsx b/packages/libeufin-bank-webui/src/pages/BankFrame.tsx @@ -48,11 +48,7 @@ import { import { useSessionState } from "../hooks/session.js"; import { DemoBanner } from "../components/DemoBanner.js"; import { OperationError } from "../components/OperationError.js"; -import { - COREBANK_API_BASE_URL_OVERRIDE_KEY, - shouldShowDemoBanner, - shouldShowPublicAccounts, -} from "../developer-settings.js"; +import { COREBANK_API_BASE_URL_OVERRIDE_KEY } from "../developer-settings.js"; const TALER_SCREEN_ID = 103; @@ -88,16 +84,8 @@ export function BankFrame({ const { i18n } = useTranslationContext(); const session = useSessionState(); const settings = useSettingsContext(); - const [preferences] = usePreferences(); - const showDemoBanner = shouldShowDemoBanner( - settings.showDemoDescription ?? false, - preferences.hideDemo, - settings.showDemoBannerOverride, - ); - const showPublicAccounts = shouldShowPublicAccounts( - settings.showPublicAccounts ?? false, - settings.showPublicAccountsOverride, - ); + const showDemoBanner = settings.enableDemoHeader ?? false; + const showPublicAccounts = settings.showPublicAccounts ?? false; const [, , resetBankState] = useBankState(); const path = useOptionalNavigationContext()?.path; const { clear: clearNotifications, clearErrors } = useNotificationContext(); @@ -315,7 +303,6 @@ function BankSettingsDialog({ onSignOut?: () => void | Promise<void>; }): VNode { const { i18n } = useTranslationContext(); - const settings = useSettingsContext(); const [{ showDebugInfo }, updateCommonPreference] = useCommonPreferences(); const [preferences, updatePreference] = usePreferences(); @@ -364,7 +351,7 @@ function BankSettingsDialog({ </div> <ul class="mt-6 divide-y divide-gray-200"> - {getAllBooleanPreferences(settings).map((preference) => { + {getAllBooleanPreferences().map((preference) => { const isOn = !!preferences[preference]; const labelId = `settings-preference-${preference}`; return ( diff --git a/packages/libeufin-bank-webui/src/pages/DeveloperSettings.stories.tsx b/packages/libeufin-bank-webui/src/pages/DeveloperSettings.stories.tsx @@ -20,7 +20,7 @@ export default { export const Configured = tests.createExample(DeveloperSettings, { configuredBackendBaseUrl: "https://bank.example/", - configuredShowDemoBanner: true, + configuredEnableDemoHeader: true, configuredShowPublicAccounts: false, overrides: {}, onApply: () => undefined, @@ -29,12 +29,12 @@ export const Configured = tests.createExample(DeveloperSettings, { export const Overridden = tests.createExample(DeveloperSettings, { configuredBackendBaseUrl: "https://bank.example/", - configuredShowDemoBanner: false, + configuredEnableDemoHeader: false, configuredShowPublicAccounts: false, overrides: { corebankApiBaseUrl: "http://localhost:8082/", - showDemoBanner: true, - showPublicAccounts: true, + overrideEnableDemoHeader: true, + overrideShowPublicAccounts: true, }, onApply: () => undefined, onClear: () => undefined, diff --git a/packages/libeufin-bank-webui/src/pages/DeveloperSettings.tsx b/packages/libeufin-bank-webui/src/pages/DeveloperSettings.tsx @@ -11,7 +11,7 @@ A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ -import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; +import { TranslatedString, canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { Footer, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -22,7 +22,7 @@ import { export interface DeveloperSettingsProps { configuredBackendBaseUrl?: string; - configuredShowDemoBanner: boolean; + configuredEnableDemoHeader: boolean; configuredShowPublicAccounts: boolean; overrides: DeveloperOverrides; onApply(overrides: DeveloperOverrides): void; @@ -38,9 +38,66 @@ function normalizeHttpBaseUrl(value: string): string { return canonical; } +function BooleanOverrideField({ + name, + label, + description, + configured, + value, + onChange, +}: { + name: string; + label: TranslatedString; + description: TranslatedString; + configured: boolean; + value: boolean | undefined; + onChange(value: boolean | undefined): void; +}): VNode { + const { i18n } = useTranslationContext(); + const configuredLabel = configured + ? i18n.str`Use configured value (enabled)` + : i18n.str`Use configured value (disabled)`; + const options: Array<{ + id: string; + label: TranslatedString; + value: boolean | undefined; + }> = [ + { id: "configured", label: configuredLabel, value: undefined }, + { id: "enabled", label: i18n.str`Force enabled`, value: true }, + { id: "disabled", label: i18n.str`Force disabled`, value: false }, + ]; + + return ( + <fieldset> + <legend class="text-sm font-semibold leading-6 text-onBackground"> + {label} + </legend> + <p class="text-sm text-gray-500">{description}</p> + <div class="mt-3 space-y-2"> + {options.map((option) => ( + <label + key={option.id} + class="flex cursor-pointer items-center gap-3 text-sm text-onBackground" + > + <input + type="radio" + name={name} + value={option.id} + checked={value === option.value} + class="h-4 w-4 border-gray-300 text-primary focus:ring-primary" + onChange={() => onChange(option.value)} + /> + <span>{option.label}</span> + </label> + ))} + </div> + </fieldset> + ); +} + export function DeveloperSettings({ configuredBackendBaseUrl, - configuredShowDemoBanner, + configuredEnableDemoHeader, configuredShowPublicAccounts, overrides, onApply, @@ -50,12 +107,12 @@ export function DeveloperSettings({ const [backendBaseUrl, setBackendBaseUrl] = useState( overrides.corebankApiBaseUrl ?? "", ); - const [showDemoBanner, setShowDemoBanner] = useState( - overrides.showDemoBanner ?? configuredShowDemoBanner, - ); - const [showPublicAccounts, setShowPublicAccounts] = useState( - overrides.showPublicAccounts ?? configuredShowPublicAccounts, - ); + const [overrideEnableDemoHeader, setOverrideEnableDemoHeader] = useState< + boolean | undefined + >(overrides.overrideEnableDemoHeader); + const [overrideShowPublicAccounts, setOverrideShowPublicAccounts] = useState< + boolean | undefined + >(overrides.overrideShowPublicAccounts); const [error, setError] = useState<string>(); const [saved, setSaved] = useState(false); @@ -76,15 +133,15 @@ export function DeveloperSettings({ setSaved(true); onApply({ corebankApiBaseUrl: normalizedUrl, - showDemoBanner, - showPublicAccounts, + overrideEnableDemoHeader, + overrideShowPublicAccounts, }); } function clear(): void { setBackendBaseUrl(""); - setShowDemoBanner(configuredShowDemoBanner); - setShowPublicAccounts(configuredShowPublicAccounts); + setOverrideEnableDemoHeader(undefined); + setOverrideShowPublicAccounts(undefined); setError(undefined); setSaved(false); onClear(); @@ -152,74 +209,29 @@ export function DeveloperSettings({ {error && <p class="mt-2 text-sm text-red-600">{error}</p>} </div> - <div class="flex items-center justify-between gap-6"> - <div> - <div - id="show-demo-banner-label" - class="text-sm font-semibold leading-6 text-onBackground" - > - <i18n.Translate>Show demo banner</i18n.Translate> - </div> - <p class="text-sm text-gray-500"> - <i18n.Translate> - Saving this switch overrides the configured banner setting. - </i18n.Translate> - </p> - </div> - <button - type="button" - role="switch" - aria-checked={showDemoBanner} - aria-labelledby="show-demo-banner-label" - data-enabled={showDemoBanner} - class="bg-primary data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 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-primary focus:ring-offset-2" - onClick={() => { - setShowDemoBanner(!showDemoBanner); - setSaved(false); - }} - > - <span - aria-hidden="true" - data-enabled={showDemoBanner} - class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" - /> - </button> - </div> + <BooleanOverrideField + name="enable-demo-header" + label={i18n.str`Demo header`} + description={i18n.str`Control whether the demo header is shown in this browser.`} + configured={configuredEnableDemoHeader} + value={overrideEnableDemoHeader} + onChange={(value) => { + setOverrideEnableDemoHeader(value); + setSaved(false); + }} + /> - <div class="flex items-center justify-between gap-6"> - <div> - <div - id="show-public-accounts-label" - class="text-sm font-semibold leading-6 text-onBackground" - > - <i18n.Translate>Show public accounts</i18n.Translate> - </div> - <p class="text-sm text-gray-500"> - <i18n.Translate> - Saving this switch overrides the configured public accounts - setting. - </i18n.Translate> - </p> - </div> - <button - type="button" - role="switch" - aria-checked={showPublicAccounts} - aria-labelledby="show-public-accounts-label" - data-enabled={showPublicAccounts} - class="bg-primary data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 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-primary focus:ring-offset-2" - onClick={() => { - setShowPublicAccounts(!showPublicAccounts); - setSaved(false); - }} - > - <span - aria-hidden="true" - data-enabled={showPublicAccounts} - class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" - /> - </button> - </div> + <BooleanOverrideField + name="show-public-accounts" + label={i18n.str`Public accounts`} + description={i18n.str`Control whether public account links are shown in this browser.`} + configured={configuredShowPublicAccounts} + value={overrideShowPublicAccounts} + onChange={(value) => { + setOverrideShowPublicAccounts(value); + setSaved(false); + }} + /> {saved && ( <p role="status" class="text-sm font-medium text-green-700"> diff --git a/packages/libeufin-bank-webui/src/settings.json b/packages/libeufin-bank-webui/src/settings.json @@ -3,6 +3,7 @@ "fastWithdrawalForm": true, "defaultSuggestedAmount": 11, "bankName": "Taler DEVELOPMENT Bank", + "enableDemoHeader": false, "showPublicAccounts": false, "topNavSites": { "Introduction": "https://test.taler.net/", diff --git a/packages/libeufin-bank-webui/src/settings.test.ts b/packages/libeufin-bank-webui/src/settings.test.ts @@ -0,0 +1,40 @@ +/* + This file is part of GNU Taler + (C) 2026 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 assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { codecForUISettings, withUiSettingsDefaults } from "./settings.js"; + +describe("bank interface settings", () => { + it("keeps the demo header disabled by default", () => { + const settings = withUiSettingsDefaults(codecForUISettings().decode({})); + assert.equal(settings.enableDemoHeader, false); + }); + + it("accepts the enableDemoHeader deployment setting", () => { + const settings = withUiSettingsDefaults( + codecForUISettings().decode({ enableDemoHeader: true }), + ); + assert.equal(settings.enableDemoHeader, true); + }); + + it("does not recognize the removed showDemoDescription setting", () => { + const settings = withUiSettingsDefaults( + codecForUISettings().decode({ showDemoDescription: true }), + ); + assert.equal(settings.enableDemoHeader, false); + }); +}); diff --git a/packages/libeufin-bank-webui/src/settings.ts b/packages/libeufin-bank-webui/src/settings.ts @@ -42,34 +42,30 @@ export interface UiSettings { // will send a default value that the user can change. // default: 10 defaultSuggestedAmount?: number; - // Show a "This is a demo" info in the home screen. + // Enable the demo header. // default: false - showDemoDescription?: boolean; + enableDemoHeader?: boolean; // Show links to the public account histories. // default: false showPublicAccounts?: boolean; - // Browser-local override set from #/dev. This is not read from settings.json. - showDemoBannerOverride?: boolean; - // Browser-local override set from #/dev. This is not read from settings.json. - showPublicAccountsOverride?: boolean; } /** * Global settings for the bank UI. */ const defaultSettings: UiSettings = { - backendBaseURL: buildDefaultBackendBaseURL(), + backendBaseURL: undefined, iconLinkURL: undefined, - showDemoDescription: false, + enableDemoHeader: false, showPublicAccounts: false, topNavSites: {}, defaultSuggestedAmount: 10, }; -const codecForUISettings = (): Codec<UiSettings> => +export const codecForUISettings = (): Codec<UiSettings> => buildCodecForObject<UiSettings>() .property("backendBaseURL", codecOptional(codecForString())) - .property("showDemoDescription", codecOptional(codecForBoolean())) + .property("enableDemoHeader", codecOptional(codecForBoolean())) .property("showPublicAccounts", codecOptional(codecForBoolean())) .property("defaultSuggestedAmount", codecOptional(codecForNumber())) .property("iconLinkURL", codecOptional(codecForString())) @@ -86,19 +82,27 @@ function removeUndefineField<T extends object>(obj: T): T { }, obj); } +export function withUiSettingsDefaults( + settings: UiSettings, + defaultBackendBaseURL?: string, +): UiSettings { + return { + ...defaultSettings, + backendBaseURL: defaultBackendBaseURL, + ...removeUndefineField(settings), + }; +} + export function fetchSettings(listener: (s: UiSettings) => void): void { fetch("./settings.json") .then((resp) => resp.json()) .then((json) => codecForUISettings().decode(json)) .then((result) => - listener({ - ...defaultSettings, - ...removeUndefineField(result), - }), + listener(withUiSettingsDefaults(result, buildDefaultBackendBaseURL())), ) .catch((e) => { console.log("failed to fetch settings", e); - listener(defaultSettings); + listener(withUiSettingsDefaults({}, buildDefaultBackendBaseURL())); }); } diff --git a/packages/taler-harness/src/harness/webui-server.ts b/packages/taler-harness/src/harness/webui-server.ts @@ -166,7 +166,7 @@ export async function startStaticServerBankWebui( dynamicFiles: { "/settings.json": (serverBaseUrl: string) => ({ backendBaseURL: serverBaseUrl, - showDemoDescription: false, + enableDemoHeader: false, showPublicAccounts: false, topNavSites: {}, defaultSuggestedAmount: 10, diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bank-webui.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bank-webui.ts @@ -335,20 +335,36 @@ export async function runLibeufinBankWebuiCoreTest(t: GlobalTestState) { 0, ); await page.goto(`${webui.url}#/dev`); - const demoBannerSwitch = page.getByRole("switch", { - name: "Show demo banner", + const demoHeaderOverrides = page.getByRole("group", { + name: "Demo header", exact: true, }); - if ((await demoBannerSwitch.getAttribute("aria-checked")) !== "true") { - await demoBannerSwitch.click(); - } - const publicAccountsSwitch = page.getByRole("switch", { - name: "Show public accounts", + const publicAccountOverrides = page.getByRole("group", { + name: "Public accounts", exact: true, }); - if ((await publicAccountsSwitch.getAttribute("aria-checked")) !== "true") { - await publicAccountsSwitch.click(); - } + t.assertTrue( + await demoHeaderOverrides + .getByRole("radio", { + name: "Use configured value (disabled)", + exact: true, + }) + .isChecked(), + ); + t.assertTrue( + await publicAccountOverrides + .getByRole("radio", { + name: "Use configured value (disabled)", + exact: true, + }) + .isChecked(), + ); + await demoHeaderOverrides + .getByRole("radio", { name: "Force enabled", exact: true }) + .click(); + await publicAccountOverrides + .getByRole("radio", { name: "Force enabled", exact: true }) + .click(); await page .getByRole("button", { name: "Apply overrides", exact: true }) .click(); @@ -393,15 +409,10 @@ export async function runLibeufinBankWebuiCoreTest(t: GlobalTestState) { element.ownerDocument.body.style.minHeight = ""; }); await page.goto(`${webui.url}#/dev`); - const enabledDemoBannerSwitch = page.getByRole("switch", { - name: "Show demo banner", - exact: true, - }); - if ( - (await enabledDemoBannerSwitch.getAttribute("aria-checked")) === "true" - ) { - await enabledDemoBannerSwitch.click(); - } + await page + .getByRole("group", { name: "Demo header", exact: true }) + .getByRole("radio", { name: "Force disabled", exact: true }) + .click(); await page .getByRole("button", { name: "Apply overrides", exact: true }) .click(); @@ -420,6 +431,12 @@ export async function runLibeufinBankWebuiCoreTest(t: GlobalTestState) { await page .getByRole("heading", { name: "Create your bank account", exact: true }) .waitFor(); + t.assertDeepEqual( + await page + .getByRole("navigation", { name: "Demo pages", exact: true }) + .count(), + 0, + ); await browser.saveScreenshot("registration"); await register( page, @@ -541,6 +558,30 @@ export async function runLibeufinBankWebuiCoreTest(t: GlobalTestState) { await logout(page); await page.goto(`${webui.url}#/dev`); + const publicAccountOverridesBeforeClear = page.getByRole("group", { + name: "Public accounts", + exact: true, + }); + await publicAccountOverridesBeforeClear + .getByRole("radio", { name: "Force disabled", exact: true }) + .click(); + await page + .getByRole("button", { name: "Apply overrides", exact: true }) + .click(); + await page.goto(`${webui.url}#/login`); + t.assertDeepEqual( + await page + .getByRole("link", { name: "Public accounts", exact: true }) + .count(), + 0, + ); + await page.goto(`${webui.url}#/dev`); + t.assertTrue( + await page + .getByRole("group", { name: "Public accounts", exact: true }) + .getByRole("radio", { name: "Force disabled", exact: true }) + .isChecked(), + ); await page .getByRole("button", { name: "Clear overrides", exact: true }) .click(); @@ -551,6 +592,18 @@ export async function runLibeufinBankWebuiCoreTest(t: GlobalTestState) { .count(), 0, ); + await page.goto(`${webui.url}#/dev`); + for (const groupName of ["Demo header", "Public accounts"]) { + t.assertTrue( + await page + .getByRole("group", { name: groupName, exact: true }) + .getByRole("radio", { + name: "Use configured value (disabled)", + exact: true, + }) + .isChecked(), + ); + } await page.goto(`${webui.url}#/public-accounts`); await page .getByRole("heading", { name: "Public accounts", exact: true })