gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated (219e48f35 -> f42ebb30c)


From: gnunet
Subject: [taler-wallet-core] branch master updated (219e48f35 -> f42ebb30c)
Date: Tue, 06 Dec 2022 16:13:35 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a change to branch master
in repository wallet-core.

    from 219e48f35 fix #7497
     new e382b0220 web-util: utils for developing webapps
     new d3a6544bc using web-utils in anastasis-webui
     new 5969a4439 using web-utils in demobank
     new de9f10cbf using web-utils in merchant-backoffice (not yet completed)
     new 51bbf08d2 implement web-utils in web-extension
     new f42ebb30c update package lock for dependecies update

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/anastasis-webui/build.mjs                 | 147 ++++++
 packages/anastasis-webui/clean_and_build.sh        |  72 ---
 packages/anastasis-webui/dev.mjs                   |  91 +---
 packages/anastasis-webui/html/stories.html         |  72 ---
 packages/anastasis-webui/html/ui-dev.html          |  65 ---
 packages/anastasis-webui/package.json              |  12 +-
 .../src/components/menu/SideBar.tsx                |  33 +-
 .../src/components/picker/DurationPicker.tsx       |  12 +-
 .../anastasis-webui/src/context/translation.ts     |  41 +-
 packages/anastasis-webui/src/hooks/index.ts        |  71 +--
 .../src/{scss/_mixins.scss => hooks/useLang.ts}    |  24 +-
 .../src/hooks/useLocalStorage.ts                   |  16 +-
 packages/anastasis-webui/src/i18n/index.tsx        | 211 --------
 .../{html/ui.html => src/index.html}               |  32 +-
 .../src/{main.test.ts => index.test.ts}            |   0
 packages/anastasis-webui/src/index.ts              |  25 +-
 packages/anastasis-webui/src/main.ts               |  41 --
 .../pages/home/AddingProviderScreen/stories.tsx    |   1 +
 .../src/pages/home/AddingProviderScreen/views.tsx  |   4 +-
 .../pages/home/AttributeEntryScreen.stories.tsx    |   1 +
 .../home/AuthenticationEditorScreen.stories.tsx    |   1 +
 .../pages/home/BackupFinishedScreen.stories.tsx    |   1 +
 .../pages/home/ChallengeOverviewScreen.stories.tsx |   1 +
 .../pages/home/ChallengePayingScreen.stories.tsx   |   1 +
 .../home/ContinentSelectionScreen.stories.tsx      |   1 +
 .../src/pages/home/EditPoliciesScreen.stories.tsx  |   1 +
 .../pages/home/PoliciesPayingScreen.stories.tsx    |   1 +
 .../pages/home/RecoveryFinishedScreen.stories.tsx  |   1 +
 .../pages/home/ReviewPoliciesScreen.stories.tsx    |   1 +
 .../src/pages/home/SecretEditorScreen.stories.tsx  |   1 +
 .../pages/home/SecretSelectionScreen.stories.tsx   |   1 +
 .../src/pages/home/SolveScreen.stories.tsx         |   1 +
 .../src/pages/home/StartScreen.stories.tsx         |   1 +
 .../src/pages/home/TruthsPayingScreen.stories.tsx  |   1 +
 .../authMethod/AuthMethodEmailSetup.stories.tsx    |   1 +
 .../authMethod/AuthMethodEmailSolve.stories.tsx    |   1 +
 .../pages/home/authMethod/AuthMethodEmailSolve.tsx |   6 +-
 .../authMethod/AuthMethodIbanSetup.stories.tsx     |   1 +
 .../authMethod/AuthMethodIbanSolve.stories.tsx     |   1 +
 .../authMethod/AuthMethodPostSetup.stories.tsx     |   1 +
 .../authMethod/AuthMethodPostSolve.stories.tsx     |   1 +
 .../pages/home/authMethod/AuthMethodPostSolve.tsx  |   6 +-
 .../authMethod/AuthMethodQuestionSetup.stories.tsx |   1 +
 .../authMethod/AuthMethodQuestionSolve.stories.tsx |   1 +
 .../home/authMethod/AuthMethodSmsSetup.stories.tsx |   1 +
 .../home/authMethod/AuthMethodSmsSolve.stories.tsx |   1 +
 .../pages/home/authMethod/AuthMethodSmsSolve.tsx   |   6 +-
 .../authMethod/AuthMethodTotpSetup.stories.tsx     |   1 +
 .../authMethod/AuthMethodTotpSolve.stories.tsx     |   1 +
 packages/anastasis-webui/src/scss/_mixins.scss     |   2 +-
 packages/anastasis-webui/src/stories.tsx           | 365 +------------
 packages/anastasis-webui/src/test-utils.ts         |  12 +-
 packages/anastasis-webui/src/utils/index.tsx       |  16 +-
 packages/anastasis-webui/watch/reply.sh            |  18 -
 packages/anastasis-webui/watch/send.sh             |  12 -
 packages/anastasis-webui/watch/send2.sh            |  14 -
 packages/anastasis-webui/watch/serve.sh            |   7 -
 .../watch/web_socket_client.request                |   6 -
 .../anastasis-webui/watch/web_socket_server.reply  |   5 -
 packages/demobank-ui/build.mjs                     |  43 +-
 .../{src/pages/notfound/index.tsx => dev.mjs}      |  26 +-
 packages/demobank-ui/package.json                  |   7 +-
 .../demobank-ui/src/components/menu/SideBar.tsx    |   9 +-
 .../src/components/picker/DurationPicker.tsx       |  12 +-
 packages/demobank-ui/src/context/translation.ts    |  52 +-
 packages/demobank-ui/src/hooks/index.ts            |  79 +--
 .../src/hooks/useLang.ts}                          |  24 +-
 .../src/hooks/useLocalStorage.ts                   |  16 +-
 packages/demobank-ui/src/i18n/index.tsx            | 201 -------
 packages/demobank-ui/src/index.html                |  15 +-
 packages/demobank-ui/src/index.tsx                 |   1 +
 .../src/pages/home/QrCodeSection.stories.tsx}      |  12 +-
 .../demobank-ui/src/pages/home/QrCodeSection.tsx   |  55 ++
 .../demobank-ui/src/pages/home/index.stories.tsx   |   1 +
 packages/demobank-ui/src/pages/home/index.tsx      | 234 ++++-----
 packages/demobank-ui/src/scss/main.scss            |   8 +-
 .../_misc.scss => demobank-ui/src/stories.tsx}     |  44 +-
 packages/demobank-ui/static/index.html             |  15 -
 .../build.mjs                                      |  43 +-
 .../index.tsx => merchant-backoffice-ui/dev.mjs}   |  26 +-
 packages/merchant-backoffice-ui/package.json       |   3 +-
 .../src/paths/index.stories.ts                     |   2 +
 .../src/stories.tsx}                               |  44 +-
 .../build-fast-with-linaria.mjs                    |   7 +-
 packages/taler-wallet-webextension/dev.mjs         | 101 ++--
 packages/taler-wallet-webextension/package.json    |  11 +-
 .../src/components/Amount.stories.tsx              |   2 +-
 .../src/components/AmountField.stories.tsx         |   2 +-
 .../src/components/Banner.stories.tsx              |   2 +-
 .../src/components/PendingTransactions.stories.tsx |   2 +-
 .../src/components/QR.stories.tsx                  |   2 +-
 .../ShowFullContractTermPopup.stories.tsx          |   2 +-
 .../src/components/index.stories.tsx               |  16 +-
 .../src/cta/Deposit/stories.tsx                    |   2 +-
 .../src/cta/InvoiceCreate/stories.tsx              |   2 +-
 .../src/cta/InvoicePay/stories.tsx                 |   2 +-
 .../src/cta/Payment/stories.tsx                    |   2 +-
 .../src/cta/Recovery/stories.tsx                   |   2 +-
 .../src/cta/Refund/stories.tsx                     |   2 +-
 .../src/cta/Tip/stories.tsx                        |   2 +-
 .../src/cta/TransferCreate/stories.tsx             |   2 +-
 .../src/cta/TransferPickup/stories.tsx             |   2 +-
 .../src/cta/Withdraw/stories.tsx                   |   2 +-
 .../src/cta/index.stories.ts                       |  20 +-
 .../src/mui/Alert.stories.tsx                      |   2 +-
 .../src/mui/Button.stories.tsx                     |   3 +-
 .../src/mui/Grid.stories.tsx                       |   2 +-
 .../src/mui/Menu.stories.tsx                       |   2 +-
 .../src/mui/Paper.stories.tsx                      |   2 +-
 .../src/mui/TextField.stories.tsx                  |   2 +-
 .../src/mui/index.stories.tsx                      |  14 +-
 .../src/popup/Balance.stories.tsx                  |   4 +-
 .../src/popup/TalerActionFound.stories.tsx         |   3 +-
 .../src/popup/index.stories.tsx                    |   6 +-
 .../taler-wallet-webextension/src/stories.test.ts  |  44 +-
 packages/taler-wallet-webextension/src/stories.tsx | 499 +-----------------
 .../taler-wallet-webextension/src/test-utils.ts    |  19 +-
 .../src/wallet/AddBackupProvider/stories.tsx       |   2 +-
 .../src/wallet/AddNewActionView.stories.tsx        |   2 +-
 .../src/wallet/Backup.stories.tsx                  |   8 +-
 .../src/wallet/DepositPage/stories.tsx             |   2 +-
 .../src/wallet/DestinationSelection/index.ts       |   2 +-
 .../src/wallet/DestinationSelection/state.ts       |  49 +-
 .../src/wallet/DestinationSelection/stories.tsx    |   2 +-
 .../src/wallet/DestinationSelection/views.tsx      |  18 +-
 .../src/wallet/DeveloperPage.stories.tsx           |   2 +-
 .../src/wallet/ExchangeAddConfirm.stories.tsx      |   2 +-
 .../src/wallet/ExchangeAddSetUrl.stories.tsx       |   8 +-
 .../src/wallet/ExchangeSelection/stories.tsx       |   2 +-
 .../src/wallet/History.stories.tsx                 |   2 +-
 .../src/wallet/ManageAccount/stories.tsx           |   2 +-
 .../src/wallet/Notifications/stories.tsx           |   2 +-
 .../wallet/ProviderAddConfirmProvider.stories.tsx  |   2 +-
 .../src/wallet/ProviderAddSetUrl.stories.tsx       |   2 +-
 .../src/wallet/ProviderDetail.stories.tsx          |   2 +-
 .../src/wallet/QrReader.stories.tsx                |   2 +-
 .../src/wallet/ReserveCreated.stories.tsx          |   2 +-
 .../src/wallet/Settings.stories.tsx                |   2 +-
 .../src/wallet/Transaction.stories.tsx             |   2 +-
 .../src/wallet/Welcome.stories.tsx                 |   2 +-
 .../src/wallet/index.stories.tsx                   |  57 +-
 packages/taler-wallet-webextension/tsconfig.json   |   3 +
 .../pages/notfound/style.css => web-util/README}   |   0
 .../bin/taler-web-cli.mjs}                         |   2 +-
 packages/{taler-wallet-cli => web-util}/build.mjs  |  66 ++-
 packages/web-util/create_certificate.sh            |  48 ++
 packages/web-util/package.json                     |  40 ++
 packages/web-util/src/cli.ts                       |  59 +++
 packages/web-util/src/custom.d.ts                  |  12 +
 packages/web-util/src/index.browser.ts             |  38 ++
 packages/web-util/src/index.node.ts                |   3 +
 packages/web-util/src/index.ts                     |   4 +
 packages/web-util/src/keys/ca.crt                  |  14 +
 packages/web-util/src/keys/ca.key                  |  16 +
 packages/web-util/src/keys/ca.srl                  |   1 +
 packages/web-util/src/keys/localhost.crt           |  15 +
 packages/web-util/src/keys/localhost.csr           |  10 +
 packages/web-util/src/keys/localhost.key           |  16 +
 packages/web-util/src/live-reload.ts               |  52 ++
 packages/web-util/src/serve.ts                     | 108 ++++
 packages/web-util/src/stories.html                 |  17 +
 packages/web-util/src/stories.tsx                  | 580 +++++++++++++++++++++
 .../example/proj1 => web-util}/tsconfig.json       |  27 +-
 pnpm-lock.yaml                                     | 327 ++++++++----
 164 files changed, 2286 insertions(+), 2583 deletions(-)
 create mode 100755 packages/anastasis-webui/build.mjs
 delete mode 100755 packages/anastasis-webui/clean_and_build.sh
 delete mode 100644 packages/anastasis-webui/html/stories.html
 delete mode 100644 packages/anastasis-webui/html/ui-dev.html
 copy packages/anastasis-webui/src/{scss/_mixins.scss => hooks/useLang.ts} (59%)
 copy packages/{taler-wallet-webextension => 
anastasis-webui}/src/hooks/useLocalStorage.ts (77%)
 delete mode 100644 packages/anastasis-webui/src/i18n/index.tsx
 rename packages/anastasis-webui/{html/ui.html => src/index.html} (67%)
 rename packages/anastasis-webui/src/{main.test.ts => index.test.ts} (100%)
 delete mode 100644 packages/anastasis-webui/src/main.ts
 delete mode 100755 packages/anastasis-webui/watch/reply.sh
 delete mode 100755 packages/anastasis-webui/watch/send.sh
 delete mode 100755 packages/anastasis-webui/watch/send2.sh
 delete mode 100755 packages/anastasis-webui/watch/serve.sh
 delete mode 100644 packages/anastasis-webui/watch/web_socket_client.request
 delete mode 100644 packages/anastasis-webui/watch/web_socket_server.reply
 copy packages/demobank-ui/{src/pages/notfound/index.tsx => dev.mjs} (65%)
 mode change 100644 => 100755
 copy packages/{anastasis-webui/src/scss/_mixins.scss => 
demobank-ui/src/hooks/useLang.ts} (59%)
 copy packages/{taler-wallet-webextension => 
demobank-ui}/src/hooks/useLocalStorage.ts (77%)
 delete mode 100644 packages/demobank-ui/src/i18n/index.tsx
 copy 
packages/{taler-wallet-webextension/src/components/TermsOfService/stories.tsx 
=> demobank-ui/src/pages/home/QrCodeSection.stories.tsx} (78%)
 create mode 100644 packages/demobank-ui/src/pages/home/QrCodeSection.tsx
 create mode 100644 packages/demobank-ui/src/pages/home/index.stories.tsx
 copy packages/{anastasis-webui/src/scss/_misc.scss => 
demobank-ui/src/stories.tsx} (61%)
 delete mode 100644 packages/demobank-ui/static/index.html
 copy packages/{demobank-ui => merchant-backoffice-ui}/build.mjs (76%)
 copy packages/{demobank-ui/src/pages/notfound/index.tsx => 
merchant-backoffice-ui/dev.mjs} (65%)
 mode change 100644 => 100755
 create mode 100644 packages/merchant-backoffice-ui/src/paths/index.stories.ts
 copy packages/{anastasis-webui/src/scss/_misc.scss => 
merchant-backoffice-ui/src/stories.tsx} (61%)
 copy packages/{demobank-ui/src/pages/notfound/style.css => web-util/README} 
(100%)
 copy packages/{taler-wallet-cli/bin/taler-wallet-cli.mjs => 
web-util/bin/taler-web-cli.mjs} (92%)
 copy packages/{taler-wallet-cli => web-util}/build.mjs (64%)
 create mode 100644 packages/web-util/create_certificate.sh
 create mode 100644 packages/web-util/package.json
 create mode 100644 packages/web-util/src/cli.ts
 create mode 100644 packages/web-util/src/custom.d.ts
 create mode 100644 packages/web-util/src/index.browser.ts
 create mode 100644 packages/web-util/src/index.node.ts
 create mode 100644 packages/web-util/src/index.ts
 create mode 100644 packages/web-util/src/keys/ca.crt
 create mode 100644 packages/web-util/src/keys/ca.key
 create mode 100644 packages/web-util/src/keys/ca.srl
 create mode 100644 packages/web-util/src/keys/localhost.crt
 create mode 100644 packages/web-util/src/keys/localhost.csr
 create mode 100644 packages/web-util/src/keys/localhost.key
 create mode 100644 packages/web-util/src/live-reload.ts
 create mode 100644 packages/web-util/src/serve.ts
 create mode 100644 packages/web-util/src/stories.html
 create mode 100644 packages/web-util/src/stories.tsx
 copy packages/{pogen/example/proj1 => web-util}/tsconfig.json (59%)

diff --git a/packages/anastasis-webui/build.mjs 
b/packages/anastasis-webui/build.mjs
new file mode 100755
index 000000000..ebe914541
--- /dev/null
+++ b/packages/anastasis-webui/build.mjs
@@ -0,0 +1,147 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
+ terms of the GNU Affero General Public License as published by the Free 
Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more 
details.
+
+ You should have received a copy of the GNU Affero General Public License 
along with
+ GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
+ */
+/* eslint-disable no-undef */
+import esbuild from 'esbuild'
+import fs from 'fs';
+import path from "path"
+import sass from "sass";
+
+// eslint-disable-next-line no-undef
+const BASE = process.cwd();
+
+const preact = path.join(
+  BASE,
+  "node_modules",
+  "preact",
+  "compat",
+  "dist",
+  "compat.module.js",
+);
+
+const preactCompatPlugin = {
+  name: "preact-compat",
+  setup(build) {
+    build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
+      return {
+        path: preact,
+      };
+    });
+  },
+};
+
+
+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_ROOT === '/' ? undefined : git_hash()
+
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
+
+function git_hash() {
+  const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 
'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
+  if (rev.indexOf('/') === -1) {
+    return rev;
+  } else {
+    return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
+  }
+}
+
+const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
+
+const buildSassPlugin = {
+  name: "custom-build-sass",
+  setup(build) {
+
+    build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
+      const resolveDir = path.dirname(file)
+      const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
+
+      return {
+        resolveDir,
+        loader: 'css',
+        contents
+      }
+    });
+
+  },
+};
+
+function copyFilesPlugin(options) {
+  if (!options.basedir) {
+    options.basedir = process.cwd()
+  }
+  return {
+    name: "copy-files",
+    setup(build) {
+      build.onEnd(() => {
+        for (const fop of options) {
+          fs.copyFileSync(path.join(options.basedir, fop.src), 
path.join(options.basedir, fop.dest));
+        }
+      });
+    },
+  };
+}
+
+export const buildConfig = {
+  entryPoints: ['src/index.ts', 'src/stories.tsx'],
+  bundle: true,
+  outdir: 'dist',
+  minify: false,
+  loader: {
+    '.svg': 'dataurl',
+    '.ttf': 'file',
+    '.woff': 'file',
+    '.woff2': 'file',
+    '.eot': 'file',
+  },
+  target: [
+    'es6'
+  ],
+  format: 'esm',
+  platform: 'browser',
+  sourcemap: true,
+  jsxFactory: 'h',
+  jsxFragment: 'Fragment',
+  define: {
+    '__VERSION__': `"${_package.version}"`,
+    '__GIT_HASH__': `"${GIT_HASH}"`,
+  },
+  plugins: [
+    preactCompatPlugin,
+    copyFilesPlugin([
+      {
+        src: "./src/index.html",
+        dest: "./dist/index.html",
+      },
+    ]),
+    buildSassPlugin
+  ],
+}
+
+await esbuild.build(buildConfig)
+
+
+
+
diff --git a/packages/anastasis-webui/clean_and_build.sh 
b/packages/anastasis-webui/clean_and_build.sh
deleted file mode 100755
index 9486848fe..000000000
--- a/packages/anastasis-webui/clean_and_build.sh
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env bash
-
-echo clean
-rm -rf dist
-mkdir -p dist/fonts
-cp \
-       src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf \
-       src/scss/fonts/materialdesignicons-webfont-4.9.95.ttf \
-       src/scss/fonts/materialdesignicons-webfont-4.9.95.woff \
-       src/scss/fonts/materialdesignicons-webfont-4.9.95.woff2 \
-       dist/fonts
-
-VERSION=$(jq -r .version package.json)
-GIT_HASH=$(git rev-parse --short HEAD)
-
-function build_css() {
-       pnpm exec sass -I . ./src/scss/main.scss dist/main.css
-}
-function build_js() {
-       pnpm exec esbuild --log-level=error 
--define:process.env.__VERSION__=\"${VERSION}\" 
--define:process.env.__GIT_HASH__=\"${GIT_HASH}\"  --bundle $1 --outdir=dist 
--target=es6 --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h 
--jsx-fragment=Fragment --platform=browser --minify
-}
-
-function build_html() {
-       cat html/$1.html \
-         | sed -e '/ANASTASIS_SCRIPT_CONTENT/ {' -e 'r dist/main.js' -e 'd' -e 
'}' \
-         | sed -e '/ANASTASIS_STYLE_CONTENT/ {' -e 'r dist/main.css' -e 'd' -e 
'}' \
-         >dist/$1.html
-}
-
-function cleanup {
- trap - SIGHUP SIGINT SIGTERM SIGQUIT
- echo -n "Cleaning up... "
- wait
- kill -- -$$
- exit 1
-}
-trap cleanup SIGHUP SIGINT SIGTERM SIGQUIT
-
-set -e
-echo compile
-build_css &
-build_js src/main.ts &
-build_js src/stories.tsx &
-build_js src/main.test.ts &
-for file in $(find src/ -name test.ts); do build_js $file; done &
-wait -n
-wait -n
-wait -n
-wait -n
-wait -n
-pnpm run --silent test -- -R dot
-
-echo html
-build_html ui
-build_html ui-dev
-build_html stories
-
-if [ "WATCH" == "$1" ]; then
-
-  echo watch mode
-  echo Writing any file in the src directory will trigger a browser reload.
-  echo Be sure that the watcher server is running.
-  echo ./watch/serve.sh
-  inotifywait -e close_write -r src -q -m | while read line; do
-    echo $(date) $line
-    build_js src/main.ts
-    build_html ui-dev
-               build_js src/stories.tsx
-               build_html stories
-    ./watch/send.sh '{"type":"RELOAD"}'
-  done;
-fi
diff --git a/packages/anastasis-webui/dev.mjs b/packages/anastasis-webui/dev.mjs
index 3f4915ffc..0446603dc 100755
--- a/packages/anastasis-webui/dev.mjs
+++ b/packages/anastasis-webui/dev.mjs
@@ -14,87 +14,18 @@
  You should have received a copy of the GNU Affero General Public License 
along with
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
-/* eslint-disable no-undef */
-import esbuild from 'esbuild'
-import fs from 'fs';
-import WebSocket from "ws";
-import chokidar from "chokidar";
 
-const devServerBroadcastDelay = 500
-const devServerPort = 8002
-const wss = new WebSocket.Server({ port: devServerPort });
-const toWatch = ["./src"]
-
-function broadcast(file, event) {
-  setTimeout(() => {
-    wss.clients.forEach((client) => {
-      if (client.readyState === WebSocket.OPEN) {
-        console.log(new Date(), file)
-        client.send(JSON.stringify(event));
-      }
-    });
-  }, devServerBroadcastDelay);
-}
-
-const watcher = chokidar
-  .watch(toWatch, {
-    persistent: true,
-    ignoreInitial: true,
-    awaitWriteFinish: {
-      stabilityThreshold: 100,
-      pollInterval: 100,
-    },
-  })
-  .on("error", (error) => console.error(error))
-  .on("change", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  })
-  .on("add", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  })
-  .on("unlink", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  });
-
-/**
- * Just bundling UI Stories.
- * FIXME: add linaria CSS after implementing Material so CSS will be bundled
- */
-fs.writeFileSync("dist/index.html", fs.readFileSync("html/stories.html"))
-fs.writeFileSync("dist/mocha.css", 
fs.readFileSync("node_modules/mocha/mocha.css"))
-fs.writeFileSync("dist/mocha.js", 
fs.readFileSync("node_modules/mocha/mocha.js"))
-fs.writeFileSync("dist/mocha.js.map", 
fs.readFileSync("node_modules/mocha/mocha.js.map"))
-
-export const buildConfig = {
-  entryPoints: ['src/main.ts', 'src/stories.tsx'],
-  bundle: true,
-  outdir: 'dist',
-  minify: false,
-  loader: {
-    '.svg': 'dataurl',
-  },
-  target: [
-    'es6'
-  ],
-  format: 'iife',
-  platform: 'browser',
-  sourcemap: true,
-  jsxFactory: 'h',
-  jsxFragment: 'Fragment',
-}
-
-const server = await esbuild
-  .serve({ servedir: 'dist' }, {
-    ...buildConfig, outdir: 'dist'
-  })
-  .catch((e) => {
-    console.log(e)
-    process.exit(1)
-  });
-
-console.log(`Dev server is ready at http://localhost:${server.port}/.
-The server is running a using websocket at ${devServerPort} to notify code 
change and live reload.
-`);
+import { serve } from "@gnu-taler/web-util/lib/index.node";
+import esbuild from 'esbuild';
+import { buildConfig } from "./build.mjs";
 
+buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
 
+serve({
+  folder: './dist',
+  port: 8080,
+  source: './src',
+  development: true,
+  onUpdate: async () => esbuild.build(buildConfig)
+})
 
diff --git a/packages/anastasis-webui/html/stories.html 
b/packages/anastasis-webui/html/stories.html
deleted file mode 100644
index 9f41fdeaf..000000000
--- a/packages/anastasis-webui/html/stories.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>Stories</title>
-    <style>
-      /* page css */
-      div.page {
-        margin: 0px;
-        padding: 0px;
-        font-size: 100%;
-        font-family: Arial, Helvetica, sans-serif;
-      }
-      div.page p:not([class]) {
-        margin-bottom: 1em;
-        margin-top: 1em;
-      }
-      div.page {
-        width: 100%;
-        display: flex;
-        flex-direction: row;
-      }
-      /* sidebar css */
-      div.sidebar {
-        min-width: 200px;
-        height: calc(100vh - 20px);
-        overflow-y: visible;
-        overflow-x: hidden;
-        scroll-behavior: smooth;
-      }
-      div.sidebar > ol {
-        padding: 4px;
-      }
-      div.sidebar div:first-child {
-        background-color: lightcoral;
-        cursor: pointer;
-      }
-      div.sidebar div[data-hide="true"] {
-        display: none;
-      }
-      div.sidebar dd {
-        margin-left: 1em;
-        padding: 4px;
-        cursor: pointer;
-        border-radius: 4px;
-        margin-bottom: 4px;
-      }
-      div.sidebar dd:nth-child(even) {
-        background-color: lightgray;
-      }
-      div.sidebar dd:nth-child(odd) {
-        background-color: lightblue;
-      }
-      div.sidebar a {
-        color: black;
-      }
-      div.sidebar dd[data-selected] {
-        background-color: green;
-      }
-
-      /* content css */
-      div.content {
-        width: 100%;
-        padding: 20px;
-      }
-    </style>
-    <script src="./stories.js"></script>
-    <link rel="stylesheet" href="./main.css" />
-  </head>
-  <body>
-    <taler-stories id="container"></taler-stories>
-  </body>
-</html>
diff --git a/packages/anastasis-webui/html/ui-dev.html 
b/packages/anastasis-webui/html/ui-dev.html
deleted file mode 100644
index 2790d5678..000000000
--- a/packages/anastasis-webui/html/ui-dev.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!DOCTYPE html>
-<html
-  lang="en"
-  class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top 
has-aside-expanded"
->
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width,initial-scale=1" />
-    <meta name="mobile-web-app-capable" content="yes" />
-    <meta name="apple-mobile-web-app-capable" content="yes" />
-
-    <link
-      rel="icon"
-      
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/
 [...]
-    />
-    <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
-    <style id="style-id" type="text/css">
-      /* <![CDATA[ */
-      ANASTASIS_STYLE_CONTENT
-      /* <![CDATA[ */
-    </style>
-  </head>
-
-  <body>
-    <div id="container" class="anastasis-container"></div>
-    <script id="code" type="application/javascript">
-      ANASTASIS_SCRIPT_CONTENT;
-    </script>
-    <script type="application/javascript">
-      function setupLiveReload() {
-        const socketPath = `ws://localhost:8003/socket`;
-        console.log("connecting to ", socketPath);
-        const ws = new WebSocket(socketPath);
-        ws.onmessage = (message) => {
-          const event = JSON.parse(message.data);
-          if (event.type === "LOG") {
-            console.log(event.message);
-          }
-          if (event.type === "RELOAD") {
-            window.location.reload();
-          }
-          if (event.type === "UPDATE") {
-            document.body.removeChild(document.getElementById("container"));
-            const d = document.createElement("div");
-            d.setAttribute("id", "container");
-            d.setAttribute("class", "anastasis-container");
-            document.body.appendChild(d);
-            const s = document.createElement("script");
-            s.setAttribute("id", "code");
-            s.setAttribute("type", "application/javascript");
-            s.textContent = atob(event.content);
-            document.body.appendChild(s);
-          }
-        };
-        ws.onerror = (error) => {
-          console.error(error);
-        };
-        ws.onclose = (e) => {
-          setTimeout(setupLiveReload, 500);
-        };
-      }
-      setupLiveReload();
-    </script>
-  </body>
-</html>
diff --git a/packages/anastasis-webui/package.json 
b/packages/anastasis-webui/package.json
index de70d05fc..c01856243 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -15,11 +15,12 @@
   "dependencies": {
     "@gnu-taler/anastasis-core": "workspace:*",
     "@gnu-taler/taler-util": "workspace:*",
+    "@gnu-taler/web-util": "workspace:*",
     "@types/chai": "^4.3.0",
     "chai": "^4.3.6",
     "date-fns": "2.29.2",
     "jed": "1.1.1",
-    "preact": "^10.5.15",
+    "preact": "10.11.3",
     "preact-render-to-string": "^5.1.19",
     "preact-router": "^3.2.1",
     "qrcode-generator": "^1.4.4"
@@ -41,12 +42,9 @@
     "bulma": "^0.9.3",
     "bulma-checkbox": "^1.1.1",
     "bulma-radio": "^1.1.1",
-    "chokidar": "^3.5.3",
-    "eslint-plugin-header": "^3.1.1",
     "jssha": "^3.2.0",
     "mocha": "^9.2.0",
-    "sass": "1.32.13",
-    "typescript": "^4.8.4",
-    "ws": "7.4.5"
+    "sass": "1.56.1",
+    "typescript": "^4.8.4"
   }
-}
+}
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx 
b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 51e854944..3dac73e04 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -22,7 +22,7 @@
 import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core";
 import { Fragment, h, VNode } from "preact";
 import { useAnastasisContext } from "../../context/anastasis.js";
-import { Translate } from "../../i18n/index.js";
+import { useTranslationContext } from "../../context/translation.js";
 
 interface Props {
   mobile?: boolean;
@@ -34,6 +34,7 @@ const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` 
: VERSION;
 
 export function Sidebar({ mobile }: Props): VNode {
   const reducer = useAnastasisContext()!;
+  const { i18n } = useTranslationContext();
 
   function saveSession(): void {
     const state = reducer.exportState();
@@ -64,7 +65,7 @@ export function Sidebar({ mobile }: Props): VNode {
       <div class="menu is-menu-main">
         {!reducer.currentReducerState && (
           <p class="menu-label">
-            <Translate>Backup or Recorver</Translate>
+            <i18n.Translate>Backup or Recorver</i18n.Translate>
           </p>
         )}
         <ul class="menu-list">
@@ -72,7 +73,7 @@ export function Sidebar({ mobile }: Props): VNode {
             <li>
               <div class="ml-4">
                 <span class="menu-item-label">
-                  <Translate>Select one option</Translate>
+                  <i18n.Translate>Select one option</i18n.Translate>
                 </span>
               </div>
             </li>
@@ -91,7 +92,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Location</Translate>
+                    <i18n.Translate>Location</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -105,7 +106,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Personal information</Translate>
+                    <i18n.Translate>Personal information</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -119,7 +120,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Authorization methods</Translate>
+                    <i18n.Translate>Authorization methods</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -133,7 +134,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Policies</Translate>
+                    <i18n.Translate>Policies</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -147,14 +148,14 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Secret input</Translate>
+                    <i18n.Translate>Secret input</i18n.Translate>
                   </span>
                 </div>
               </li>
               {/* <li class={reducer.currentReducerState.backup_state === 
BackupStates.PoliciesPaying ? 'is-active' : ''}>
               <div class="ml-4">
 
-                <span class="menu-item-label"><Translate>Payment 
(optional)</Translate></span>
+                <span class="menu-item-label"><i18n.Translate>Payment 
(optional)</i18n.Translate></span>
               </div>
             </li> */}
               <li
@@ -167,14 +168,14 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Backup completed</Translate>
+                    <i18n.Translate>Backup completed</i18n.Translate>
                   </span>
                 </div>
               </li>
               {/* <li class={reducer.currentReducerState.backup_state === 
BackupStates.TruthsPaying ? 'is-active' : ''}>
               <div class="ml-4">
 
-                <span class="menu-item-label"><Translate>Truth 
Paying</Translate></span>
+                <span class="menu-item-label"><i18n.Translate>Truth 
Paying</i18n.Translate></span>
               </div>
             </li> */}
               {reducer.currentReducerState.backup_state !==
@@ -219,7 +220,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Location</Translate>
+                      <i18n.Translate>Location</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -233,7 +234,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Personal information</Translate>
+                      <i18n.Translate>Personal information</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -247,7 +248,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Secret selection</Translate>
+                      <i18n.Translate>Secret selection</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -263,7 +264,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Solve Challenges</Translate>
+                      <i18n.Translate>Solve Challenges</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -277,7 +278,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Secret recovered</Translate>
+                      <i18n.Translate>Secret recovered</i18n.Translate>
                     </span>
                   </div>
                 </li>
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx 
b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
index 12ed158dd..c4caaec9f 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
@@ -21,7 +21,7 @@
 
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { useTranslator } from "../../i18n/index.js";
+import { useTranslationContext } from "../../context/translation.js";
 import "../../scss/DurationPicker.scss";
 
 export interface Props {
@@ -46,13 +46,13 @@ export function DurationPicker({
   const ms = ss * 60;
   const hs = ms * 60;
   const ds = hs * 24;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   return (
     <div class="rdp-picker">
       {days && (
         <DurationColumn
-          unit={i18n`days`}
+          unit={i18n.str`days`}
           max={99}
           value={Math.floor(value / ds)}
           onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
@@ -62,7 +62,7 @@ export function DurationPicker({
       )}
       {hours && (
         <DurationColumn
-          unit={i18n`hours`}
+          unit={i18n.str`hours`}
           max={23}
           min={1}
           value={Math.floor(value / hs) % 24}
@@ -73,7 +73,7 @@ export function DurationPicker({
       )}
       {minutes && (
         <DurationColumn
-          unit={i18n`minutes`}
+          unit={i18n.str`minutes`}
           max={59}
           min={1}
           value={Math.floor(value / ms) % 60}
@@ -84,7 +84,7 @@ export function DurationPicker({
       )}
       {seconds && (
         <DurationColumn
-          unit={i18n`seconds`}
+          unit={i18n.str`seconds`}
           max={59}
           value={Math.floor(value / ss) % 60}
           onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
diff --git a/packages/anastasis-webui/src/context/translation.ts 
b/packages/anastasis-webui/src/context/translation.ts
index 87704a13f..44faaa456 100644
--- a/packages/anastasis-webui/src/context/translation.ts
+++ b/packages/anastasis-webui/src/context/translation.ts
@@ -19,23 +19,42 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { i18n, setupI18n } from "@gnu-taler/taler-util";
 import { createContext, h, VNode } from "preact";
 import { useContext, useEffect } from "preact/hooks";
-import { useLang } from "../hooks/index.js";
-import * as jedLib from "jed";
+import { useLang } from "../hooks/useLang.js";
 import { strings } from "../i18n/strings.js";
 
 interface Type {
   lang: string;
-  handler: any;
+  supportedLang: { [id in keyof typeof supportedLang]: string };
   changeLanguage: (l: string) => void;
+  i18n: typeof i18n;
+  isSaved: boolean;
 }
+
+const supportedLang = {
+  es: "Español [es]",
+  ja: "日本語 [ja]",
+  en: "English [en]",
+  fr: "Français [fr]",
+  de: "Deutsch [de]",
+  sv: "Svenska [sv]",
+  it: "Italiano [it]",
+  // ko: "한국어 [ko]",
+  // ru: "Ру́сский язы́к [ru]",
+  tr: "Türk [tr]",
+  navigator: "Defined by navigator",
+};
+
 const initial = {
   lang: "en",
-  handler: null,
+  supportedLang,
   changeLanguage: () => {
     // do not change anything
   },
+  i18n,
+  isSaved: false,
 };
 const Context = createContext<Type>(initial);
 
@@ -50,15 +69,23 @@ export const TranslationProvider = ({
   children,
   forceLang,
 }: Props): VNode => {
-  const [lang, changeLanguage] = useLang(initial);
+  const [lang, changeLanguage, isSaved] = useLang(initial);
   useEffect(() => {
     if (forceLang) {
       changeLanguage(forceLang);
     }
   });
-  const handler = new jedLib.Jed(strings[lang] || strings["en"]);
+  useEffect(() => {
+    setupI18n(lang, strings);
+  }, [lang]);
+  if (forceLang) {
+    setupI18n(forceLang, strings);
+  } else {
+    setupI18n(lang, strings);
+  }
+
   return h(Context.Provider, {
-    value: { lang, handler, changeLanguage },
+    value: { lang, changeLanguage, supportedLang, i18n, isSaved },
     children,
   });
 };
diff --git a/packages/anastasis-webui/src/hooks/index.ts 
b/packages/anastasis-webui/src/hooks/index.ts
index c03e834d7..2dbf4fa5c 100644
--- a/packages/anastasis-webui/src/hooks/index.ts
+++ b/packages/anastasis-webui/src/hooks/index.ts
@@ -19,7 +19,9 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { StateUpdater, useState } from "preact/hooks";
+import { StateUpdater } from "preact/hooks";
+import { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
+
 export type ValueOrFunction<T> = T | ((p: T) => T);
 
 const calculateRootPath = () => {
@@ -69,69 +71,4 @@ export function useBackendInstanceToken(
   }
 
   return [token, setToken];
-}
-
-export function useLang(initial?: string): [string, StateUpdater<string>] {
-  const browserLang =
-    typeof window !== "undefined"
-      ? navigator.language || (navigator as any).userLanguage
-      : undefined;
-  const defaultLang = (browserLang || initial || "en").substring(0, 2);
-  return useNotNullLocalStorage("lang-preference", defaultLang);
-}
-
-export function useLocalStorage(
-  key: string,
-  initialValue?: string,
-): [string | undefined, StateUpdater<string | undefined>] {
-  const [storedValue, setStoredValue] = useState<string | undefined>(
-    (): string | undefined => {
-      return typeof window !== "undefined"
-        ? window.localStorage.getItem(key) || initialValue
-        : initialValue;
-    },
-  );
-
-  const setValue = (
-    value?: string | ((val?: string) => string | undefined),
-  ) => {
-    setStoredValue((p) => {
-      const toStore = value instanceof Function ? value(p) : value;
-      if (typeof window !== "undefined") {
-        if (!toStore) {
-          window.localStorage.removeItem(key);
-        } else {
-          window.localStorage.setItem(key, toStore);
-        }
-      }
-      return toStore;
-    });
-  };
-
-  return [storedValue, setValue];
-}
-
-export function useNotNullLocalStorage(
-  key: string,
-  initialValue: string,
-): [string, StateUpdater<string>] {
-  const [storedValue, setStoredValue] = useState<string>((): string => {
-    return typeof window !== "undefined"
-      ? window.localStorage.getItem(key) || initialValue
-      : initialValue;
-  });
-
-  const setValue = (value: string | ((val: string) => string)) => {
-    const valueToStore = value instanceof Function ? value(storedValue) : 
value;
-    setStoredValue(valueToStore);
-    if (typeof window !== "undefined") {
-      if (!valueToStore) {
-        window.localStorage.removeItem(key);
-      } else {
-        window.localStorage.setItem(key, valueToStore);
-      }
-    }
-  };
-
-  return [storedValue, setValue];
-}
+}
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss 
b/packages/anastasis-webui/src/hooks/useLang.ts
similarity index 59%
copy from packages/anastasis-webui/src/scss/_mixins.scss
copy to packages/anastasis-webui/src/hooks/useLang.ts
index 64315785b..5b02c5255 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/anastasis-webui/src/hooks/useLang.ts
@@ -14,21 +14,17 @@
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { useNotNullLocalStorage } from "./useLocalStorage.js";
 
-@mixin transition($t) {
-  transition: $t 250ms ease-in-out 50ms;
+function getBrowserLang(): string | undefined {
+  if (window.navigator.languages) return window.navigator.languages[0];
+  if (window.navigator.language) return window.navigator.language;
+  return undefined;
 }
 
-@mixin icon-with-update-mark($icon-base-width) {
-  .icon {
-    width: $icon-base-width;
-
-    &.has-update-mark:after {
-      right: ($icon-base-width / 2) - 0.85;
-    }
-  }
+export function useLang(
+  initial?: string,
+): [string, (s: string) => void, boolean] {
+  const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
+  return useNotNullLocalStorage("lang-preference", defaultLang);
 }
diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts 
b/packages/anastasis-webui/src/hooks/useLocalStorage.ts
similarity index 77%
copy from packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
copy to packages/anastasis-webui/src/hooks/useLocalStorage.ts
index 88b7655b6..ed5b491f2 100644
--- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
+++ b/packages/anastasis-webui/src/hooks/useLocalStorage.ts
@@ -1,17 +1,17 @@
 /*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
 
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE.  See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License 
along with
+ GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
 /**
diff --git a/packages/anastasis-webui/src/i18n/index.tsx 
b/packages/anastasis-webui/src/i18n/index.tsx
deleted file mode 100644
index 01e3cdd3a..000000000
--- a/packages/anastasis-webui/src/i18n/index.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- This file is part of GNU Anastasis
- (C) 2021-2022 Anastasis SARL
-
- GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
- terms of the GNU Affero General Public License as published by the Free 
Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Anastasis 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 Affero General Public License for more 
details.
-
- You should have received a copy of the GNU Affero General Public License 
along with
- GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
- */
-
-/**
- * Translation helpers for React components and template literals.
- */
-
-/**
- * Imports
- */
-import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
-
-import { useTranslationContext } from "../context/translation.js";
-
-export function useTranslator() {
-  const ctx = useTranslationContext();
-  const jed = ctx.handler;
-  return function str(
-    stringSeq: TemplateStringsArray,
-    ...values: any[]
-  ): string {
-    const s = toI18nString(stringSeq);
-    if (!s) return s;
-    const tr = jed
-      .translate(s)
-      .ifPlural(1, s)
-      .fetch(...values);
-    return tr;
-  };
-}
-
-/**
- * Convert template strings to a msgid
- */
-function toI18nString(stringSeq: ReadonlyArray<string>): string {
-  let s = "";
-  for (let i = 0; i < stringSeq.length; i++) {
-    s += stringSeq[i];
-    if (i < stringSeq.length - 1) {
-      s += `%${i + 1}$s`;
-    }
-  }
-  return s;
-}
-
-interface TranslateSwitchProps {
-  target: number;
-  children: ComponentChildren;
-}
-
-function stringifyChildren(children: ComponentChildren): string {
-  let n = 1;
-  const ss = (children instanceof Array ? children : [children]).map((c) => {
-    if (typeof c === "string") {
-      return c;
-    }
-    return `%${n++}$s`;
-  });
-  const s = ss.join("").replace(/ +/g, " ").trim();
-  return s;
-}
-
-interface TranslateProps {
-  children: ComponentChildren;
-  /**
-   * Component that the translated element should be wrapped in.
-   * Defaults to "div".
-   */
-  wrap?: any;
-
-  /**
-   * Props to give to the wrapped component.
-   */
-  wrapProps?: any;
-}
-
-function getTranslatedChildren(
-  translation: string,
-  children: ComponentChildren,
-): ComponentChild[] {
-  const tr = translation.split(/%(\d+)\$s/);
-  const childArray = children instanceof Array ? children : [children];
-  // Merge consecutive string children.
-  const placeholderChildren = Array<ComponentChild>();
-  for (let i = 0; i < childArray.length; i++) {
-    const x = childArray[i];
-    if (x === undefined) {
-      continue;
-    } else if (typeof x === "string") {
-      continue;
-    } else {
-      placeholderChildren.push(x);
-    }
-  }
-  const result = Array<ComponentChild>();
-  for (let i = 0; i < tr.length; i++) {
-    if (i % 2 == 0) {
-      // Text
-      result.push(tr[i]);
-    } else {
-      const childIdx = Number.parseInt(tr[i], 10) - 1;
-      result.push(placeholderChildren[childIdx]);
-    }
-  }
-  return result;
-}
-
-/**
- * Translate text node children of this component.
- * If a child component might produce a text node, it must be wrapped
- * in a another non-text element.
- *
- * Example:
- * ```
- * <Translate>
- * Hello.  Your score is <span><PlayerScore player={player} /></span>
- * </Translate>
- * ```
- */
-export function Translate({ children }: TranslateProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation: string = ctx.handler.ngettext(s, s, 1);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
-
-/**
- * Switch translation based on singular or plural based on the target prop.
- * Should only contain TranslateSingular and TransplatePlural as children.
- *
- * Example:
- * ```
- * <TranslateSwitch target={n}>
- *  <TranslateSingular>I have {n} apple.</TranslateSingular>
- *  <TranslatePlural>I have {n} apples.</TranslatePlural>
- * </TranslateSwitch>
- * ```
- */
-export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
-  let singular: VNode<TranslationPluralProps> | undefined;
-  let plural: VNode<TranslationPluralProps> | undefined;
-  // const children = this.props.children;
-  if (children) {
-    (children instanceof Array ? children : [children]).forEach(
-      (child: any) => {
-        if (child.type === TranslatePlural) {
-          plural = child;
-        }
-        if (child.type === TranslateSingular) {
-          singular = child;
-        }
-      },
-    );
-  }
-  if (!singular || !plural) {
-    console.error("translation not found");
-    return h("span", {}, ["translation not found"]);
-  }
-  singular.props.target = target;
-  plural.props.target = target;
-  // We're looking up the translation based on the
-  // singular, even if we must use the plural form.
-  return singular;
-}
-
-interface TranslationPluralProps {
-  children: ComponentChildren;
-  target: number;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslatePlural({
-  children,
-  target,
-}: TranslationPluralProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation = ctx.handler.ngettext(s, s, 1);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslateSingular({
-  children,
-  target,
-}: TranslationPluralProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation = ctx.handler.ngettext(s, s, target);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
diff --git a/packages/anastasis-webui/html/ui.html 
b/packages/anastasis-webui/src/index.html
similarity index 67%
rename from packages/anastasis-webui/html/ui.html
rename to packages/anastasis-webui/src/index.html
index 6672eba6a..90a795ae3 100644
--- a/packages/anastasis-webui/html/ui.html
+++ b/packages/anastasis-webui/src/index.html
@@ -1,28 +1,42 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--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
+-->
 <!DOCTYPE html>
 <html
   lang="en"
   class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top 
has-aside-expanded"
 >
   <head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width,initial-scale=1" />
     <meta name="mobile-web-app-capable" content="yes" />
     <meta name="apple-mobile-web-app-capable" content="yes" />
-
     <link
       rel="icon"
       
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/
 [...]
     />
     <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
-    <style id="style-id">
-      ANASTASIS_STYLE_CONTENT
-    </style>
+    <title>Anastasis</title>
+    <!-- Entry point for the demobank SPA. -->
+    <script type="module" src="index.js"></script>
+    <link rel="stylesheet" href="index.css" />
   </head>
-
   <body>
-    <div id="container" class="anastasis-container"></div>
-    <script>
-      ANASTASIS_SCRIPT_CONTENT;
-    </script>
+    <div id="container"></div>
   </body>
 </html>
diff --git a/packages/anastasis-webui/src/main.test.ts 
b/packages/anastasis-webui/src/index.test.ts
similarity index 100%
rename from packages/anastasis-webui/src/main.test.ts
rename to packages/anastasis-webui/src/index.test.ts
diff --git a/packages/anastasis-webui/src/index.ts 
b/packages/anastasis-webui/src/index.ts
index e04c44a31..d7b2164ab 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -13,7 +13,30 @@
  You should have received a copy of the GNU Affero General Public License 
along with
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
+import { setupI18n } from "@gnu-taler/taler-util";
+import { h, render } from "preact";
 import App from "./components/app.js";
 import "./scss/main.scss";
 
-export default App;
+function main(): void {
+  try {
+    const container = document.getElementById("container");
+    if (!container) {
+      throw Error("container not found, can't mount page contents");
+    }
+    render(h(App, {}), container);
+  } catch (e) {
+    console.error("got error", e);
+    if (e instanceof Error) {
+      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
+    }
+  }
+}
+
+// setupI18n("en", strings);
+
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoaded", main);
+} else {
+  main();
+}
diff --git a/packages/anastasis-webui/src/main.ts 
b/packages/anastasis-webui/src/main.ts
deleted file mode 100644
index 72ab257eb..000000000
--- a/packages/anastasis-webui/src/main.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- This file is part of GNU Anastasis
- (C) 2021-2022 Anastasis SARL
-
- GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
- terms of the GNU Affero General Public License as published by the Free 
Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Anastasis 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 Affero General Public License for more 
details.
-
- You should have received a copy of the GNU Affero General Public License 
along with
- GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
- */
-import { setupI18n } from "@gnu-taler/taler-util";
-import { h, render } from "preact";
-import App from "./components/app.js";
-
-function main(): void {
-  try {
-    const container = document.getElementById("container");
-    if (!container) {
-      throw Error("container not found, can't mount page contents");
-    }
-    render(h(App, {}), container);
-  } catch (e) {
-    console.error("got error", e);
-    if (e instanceof Error) {
-      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
-    }
-  }
-}
-
-// setupI18n("en", strings);
-
-if (document.readyState === "loading") {
-  document.addEventListener("DOMContentLoaded", main);
-} else {
-  main();
-}
diff --git 
a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx 
b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
index dc41d9c1a..268189ed8 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
@@ -24,6 +24,7 @@ import { createExampleWithoutAnastasis } from 
"../../../utils/index.jsx";
 import { WithoutProviderType, WithProviderType } from "./views.jsx";
 
 export default {
+  title: "Adding Provider Screen",
   args: {
     order: 1,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx 
b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
index e397e0b65..19557a12f 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
@@ -23,8 +23,10 @@ import { TextInput } from 
"../../../components/fields/TextInput.js";
 import { Notifications } from "../../../components/Notifications.js";
 import { AnastasisClientFrame } from "../index.js";
 import { testProvider, WithoutType, WithType } from "./index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 
 export function WithProviderType(props: WithType): VNode {
+  const { i18n } = useTranslationContext();
   return (
     <AnastasisClientFrame
       hideNav
@@ -33,7 +35,7 @@ export function WithProviderType(props: WithType): VNode {
     >
       <div>
         <Notifications notifications={props.notifications} />
-        <p>Add a provider url for a {props.providerLabel} service</p>
+        <p>{i18n.str`Add a provider url for a ${props.providerLabel} 
service`}</p>
         <div class="container">
           <TextInput
             label="Provider URL"
diff --git 
a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
index b1569f184..38fc1b56b 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { AttributeEntryScreen as TestedComponent } from 
"./AttributeEntryScreen.js";
 
 export default {
+  title: "Attribute Entry Screen",
   component: TestedComponent,
   args: {
     order: 3,
diff --git 
a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index c4901085d..ba48e2d5c 100644
--- 
a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { AuthenticationEditorScreen as TestedComponent } from 
"./AuthenticationEditorScreen.js";
 
 export default {
+  title: "Authentication Editor Screen",
   component: TestedComponent,
   args: {
     order: 4,
diff --git 
a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index f50a72f8a..8aeaec25c 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { BackupFinishedScreen as TestedComponent } from 
"./BackupFinishedScreen.js";
 
 export default {
+  title: "Backup finish",
   component: TestedComponent,
   args: {
     order: 8,
diff --git 
a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
index 552cb069f..d2471755a 100644
--- 
a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -28,6 +28,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ChallengeOverviewScreen as TestedComponent } from 
"./ChallengeOverviewScreen.js";
 
 export default {
+  title: "Challenge overview",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
index 0d4895a0b..cd41fe03a 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ChallengePayingScreen as TestedComponent } from 
"./ChallengePayingScreen.js";
 
 export default {
+  title: "Challenge paying",
   component: TestedComponent,
   args: {
     order: 10,
diff --git 
a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
index 3994b7377..12a79c56c 100644
--- 
a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ContinentSelectionScreen as TestedComponent } from 
"./ContinentSelectionScreen.js";
 
 export default {
+  title: "Continent selection",
   component: TestedComponent,
   args: {
     order: 2,
diff --git 
a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
index 75619ba05..1e3650300 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { EditPoliciesScreen as TestedComponent } from 
"./EditPoliciesScreen.js";
 
 export default {
+  title: "Edit policies",
   args: {
     order: 6,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index 54833234d..56c224d34 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { PoliciesPayingScreen as TestedComponent } from 
"./PoliciesPayingScreen.js";
 
 export default {
+  title: "Policies paying",
   component: TestedComponent,
   args: {
     order: 9,
diff --git 
a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
index eda8968b2..1eb2ae50c 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -25,6 +25,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { RecoveryFinishedScreen as TestedComponent } from 
"./RecoveryFinishedScreen.js";
 
 export default {
+  title: "Recovery Finished",
   args: {
     order: 7,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
index 036455bce..c5003d6a0 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ReviewPoliciesScreen as TestedComponent } from 
"./ReviewPoliciesScreen.js";
 
 export default {
+  title: "Reviewing Policies",
   args: {
     order: 6,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index 7a03116e7..dbf8bf128 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { SecretEditorScreen as TestedComponent } from 
"./SecretEditorScreen.js";
 
 export default {
+  title: "Secret editor",
   component: TestedComponent,
   args: {
     order: 7,
diff --git 
a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
index b457937f8..7669668ee 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -27,6 +27,7 @@ import {
 } from "./SecretSelectionScreen.js";
 
 export default {
+  title: "Secret selection",
   component: SecretSelectionScreen,
   args: {
     order: 4,
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index 5b3a70dd0..1058ae126 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { SolveScreen as TestedComponent } from "./SolveScreen.js";
 
 export default {
+  title: "Solve Screen",
   component: TestedComponent,
   args: {
     order: 6,
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index 3d54a9fd6..960426098 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { StartScreen as TestedComponent } from "./StartScreen.js";
 
 export default {
+  title: "Start screen",
   component: TestedComponent,
   args: {
     order: 1,
diff --git 
a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index 81bab4868..40ed5117c 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { TruthsPayingScreen as TestedComponent } from 
"./TruthsPayingScreen.js";
 
 export default {
+  title: "Truths Paying",
   component: TestedComponent,
   args: {
     order: 10,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
index 38391d10d..4a2d76ca3 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Email setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
index db9abc86c..cc378d8f6 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
@@ -27,6 +27,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Email solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
index d4e034a37..6a9595a83 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
@@ -23,7 +23,7 @@ import { useState } from "preact/hooks";
 import { AsyncButton } from "../../../components/AsyncButton.js";
 import { TextInput } from "../../../components/fields/TextInput.js";
 import { useAnastasisContext } from "../../../context/anastasis.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 import { AnastasisClientFrame } from "../index.js";
 import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
 import { shouldHideConfirm } from "./helpers.js";
@@ -53,7 +53,7 @@ export function AuthMethodEmailSolve({ id }: 
AuthMethodSolveProps): VNode {
     _setAnswer(result);
   }
   const [expanded, setExpanded] = useState(false);
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const reducer = useAnastasisContext();
   if (!reducer) {
@@ -124,7 +124,7 @@ export function AuthMethodEmailSolve({ id }: 
AuthMethodSolveProps): VNode {
 
   const error =
     answer.length > 21
-      ? i18n`The answer should not be greater than 21 characters.`
+      ? i18n.str`The answer should not be greater than 21 characters.`
       : undefined;
 
   return (
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
index 5f3de47ff..dfe3850f1 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: IBAN setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
index c06611127..8a9a3f7a0 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: IBAN Solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
index 892de6023..8a32c45c1 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Post setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
index 8f7dc5ff9..702ba2810 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Post solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
index 725382c58..8204ab1cf 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
@@ -19,7 +19,7 @@ import { useState } from "preact/hooks";
 import { AsyncButton } from "../../../components/AsyncButton.js";
 import { TextInput } from "../../../components/fields/TextInput.js";
 import { useAnastasisContext } from "../../../context/anastasis.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 import { AnastasisClientFrame } from "../index.js";
 import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
 import { shouldHideConfirm } from "./helpers.js";
@@ -48,7 +48,7 @@ export function AuthMethodPostSolve({ id }: 
AuthMethodSolveProps): VNode {
 
     _setAnswer(result);
   }
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const reducer = useAnastasisContext();
   if (!reducer) {
@@ -119,7 +119,7 @@ export function AuthMethodPostSolve({ id }: 
AuthMethodSolveProps): VNode {
 
   const error =
     answer.length > 21
-      ? i18n`The answer should not be greater than 21 characters.`
+      ? i18n.str`The answer should not be greater than 21 characters.`
       : undefined;
 
   return (
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
index 736e7bfa8..2e108b4e6 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Question setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
index 182538775..f7116bf6f 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
@@ -27,6 +27,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Question solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
index 0d58dbdcf..b2c6cb61d 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: SMS setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
index f1717eff0..2064f12ff 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: SMS solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
index 965efbe60..58bb53c4f 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
@@ -19,7 +19,7 @@ import { useState } from "preact/hooks";
 import { AsyncButton } from "../../../components/AsyncButton.js";
 import { TextInput } from "../../../components/fields/TextInput.js";
 import { useAnastasisContext } from "../../../context/anastasis.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 import { AnastasisClientFrame } from "../index.js";
 import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
 import { shouldHideConfirm } from "./helpers.js";
@@ -48,7 +48,7 @@ export function AuthMethodSmsSolve({ id }: 
AuthMethodSolveProps): VNode {
 
     _setAnswer(result);
   }
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const [expanded, setExpanded] = useState(false);
   const reducer = useAnastasisContext();
@@ -120,7 +120,7 @@ export function AuthMethodSmsSolve({ id }: 
AuthMethodSolveProps): VNode {
 
   const error =
     answer.length > 21
-      ? i18n`The answer should not be greater than 21 characters.`
+      ? i18n.str`The answer should not be greater than 21 characters.`
       : undefined;
 
   return (
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
index e22053b96..5582590f7 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Totp setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
index 354516d80..20cd7e3c9 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Totp solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss 
b/packages/anastasis-webui/src/scss/_mixins.scss
index 64315785b..a0fe6e93e 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/anastasis-webui/src/scss/_mixins.scss
@@ -28,7 +28,7 @@
     width: $icon-base-width;
 
     &.has-update-mark:after {
-      right: ($icon-base-width / 2) - 0.85;
+      right: calc($icon-base-width / 2) - 0.85;
     }
   }
 }
diff --git a/packages/anastasis-webui/src/stories.tsx 
b/packages/anastasis-webui/src/stories.tsx
index 7d22deece..f345f082d 100644
--- a/packages/anastasis-webui/src/stories.tsx
+++ b/packages/anastasis-webui/src/stories.tsx
@@ -18,302 +18,24 @@
  *
  * @author Sebastian Javier Marchano (sebasjm)
  */
-import { setupI18n } from "@gnu-taler/taler-util";
-import { ComponentChild, Fragment, h, render, VNode } from "preact";
-import { useEffect, useErrorBoundary, useState } from "preact/hooks";
 import { strings } from "./i18n/strings.js";
-import * as pages from "./pages/home/index.storiesNo.js";
 
-const url = new URL(window.location.href);
-const lang = url.searchParams.get("lang") || "en";
+import * as pages from "./pages/home/index.storiesNo.js";
 
-setupI18n(lang, strings);
+import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
 
-const Page = ({ children }: any) => <div class="page">{children}</div>;
-const SideBar = ({ children }: any) => <div class="sidebar">{children}</div>;
-const Content = ({ children }: any) => <div class="content">{children}</div>;
-
-function parseExampleImport(
-  group: string,
-  im: any,
-  name?: string,
-): ComponentItem {
-  const component = name || im.default.title;
-  const order: number = im.default.args?.order || 0;
-  return {
-    name: component,
-    order,
-    examples: Object.entries(im)
-      .filter(([k]) => k !== "default")
-      .map(
-        ([name, render]) =>
-          ({
-            group,
-            component,
-            name,
-            render,
-          } as ExampleItem),
-      ),
-  };
-}
+import "./scss/main.scss";
 
 function SortStories(a: any, b: any): number {
   return (a?.order ?? 0) - (b?.order ?? 0);
 }
 
-const allExamples = Object.entries({ pages }).map(([title, value]) => {
-  return {
-    title,
-    list: Object.entries(value)
-      .filter(([name]) => name != "default")
-      .map(([name, value]) => parseExampleImport(title, value, name))
-      .sort(SortStories),
-  };
-});
-
-interface ComponentItem {
-  name: string;
-  order: number;
-  examples: ExampleItem[];
-}
-
-interface ExampleItem {
-  group: string;
-  component: string;
-  name: string;
-  render: {
-    (args: any): VNode;
-    args: any;
-  };
-}
-
-function findByGroupComponentName(
-  group: string,
-  component: string,
-  name: string,
-): ExampleItem | undefined {
-  const gl = allExamples.filter((e) => e.title === group);
-  if (gl.length === 0) {
-    return undefined;
-  }
-  const cl = gl[0].list.filter((l) => l.name === component);
-  if (cl.length === 0) {
-    return undefined;
-  }
-  const el = cl[0].examples.filter((c) => c.name === name);
-  if (el.length === 0) {
-    return undefined;
-  }
-  return el[0];
-}
-
-function getContentForExample(item: ExampleItem | undefined): () => VNode {
-  if (!item)
-    return function SelectExampleMessage() {
-      return <div>select example from the list on the left</div>;
-    };
-  const example = findByGroupComponentName(
-    item.group,
-    item.component,
-    item.name,
-  );
-  if (!example)
-    return function ExampleNotFoundMessage() {
-      return <div>example not found</div>;
-    };
-  return () => example.render(example.render.args);
-}
-
-function ExampleList({
-  name,
-  list,
-  selected,
-  onSelectStory,
-}: {
-  name: string;
-  list: {
-    name: string;
-    examples: ExampleItem[];
-  }[];
-  selected: ExampleItem | undefined;
-  onSelectStory: (i: ExampleItem, id: string) => void;
-}): VNode {
-  const [isOpen, setOpen] = useState(selected && selected.group === name);
-  return (
-    <ol>
-      <div onClick={() => setOpen(!isOpen)}>{name}</div>
-      <div data-hide={!isOpen}>
-        {list.map((k) => (
-          <li key={k.name}>
-            <dl>
-              <dt>{k.name}</dt>
-              {k.examples.map((r) => {
-                const e = encodeURIComponent;
-                const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
-                function doSelection(e: any): void {
-                  e.preventDefault();
-                  location.hash = `#${eId}`;
-                  onSelectStory(r, eId);
-                }
-                const isSelected =
-                  selected &&
-                  selected.component === r.component &&
-                  selected.group === r.group &&
-                  selected.name === r.name;
-                return (
-                  <dd
-                    id={eId}
-                    key={r.name}
-                    data-selected={isSelected}
-                    onClick={doSelection}
-                  >
-                    <a href={`#${eId}`} onClick={doSelection}>
-                      {r.name}
-                    </a>
-                  </dd>
-                );
-              })}
-            </dl>
-          </li>
-        ))}
-      </div>
-    </ol>
-  );
-}
-
-// function getWrapperForGroup(group: string): FunctionComponent {
-//   switch (group) {
-//     case "popup":
-//       return function PopupWrapper({ children }: any) {
-//         return (
-//           <Fragment>
-//             <PopupNavBar />
-//             <PopupBox>{children}</PopupBox>
-//           </Fragment>
-//         );
-//       };
-//     case "wallet":
-//       return function WalletWrapper({ children }: any) {
-//         return (
-//           <Fragment>
-//             <LogoHeader />
-//             <WalletNavBar />
-//             <WalletBox>{children}</WalletBox>
-//           </Fragment>
-//         );
-//       };
-//     case "cta":
-//       return function WalletWrapper({ children }: any) {
-//         return (
-//           <Fragment>
-//             <WalletBox>{children}</WalletBox>
-//           </Fragment>
-//         );
-//       };
-//     default:
-//       return Fragment;
-//   }
-// }
-
-function ErrorReport({
-  children,
-  selected,
-}: {
-  children: ComponentChild;
-  selected: ExampleItem | undefined;
-}): VNode {
-  const [error] = useErrorBoundary();
-  if (error) {
-    return (
-      <div class="error_report">
-        <p>Error was thrown trying to render</p>
-        {selected && (
-          <ul>
-            <li>
-              <b>group</b>: {selected.group}
-            </li>
-            <li>
-              <b>component</b>: {selected.component}
-            </li>
-            <li>
-              <b>example</b>: {selected.name}
-            </li>
-            <li>
-              <b>args</b>:{" "}
-              <pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre>
-            </li>
-          </ul>
-        )}
-        <p>{error.message}</p>
-        <pre>{error.stack}</pre>
-      </div>
-    );
-  }
-  return <Fragment>{children}</Fragment>;
-}
-
-function getSelectionFromLocationHash(hash: string): ExampleItem | undefined {
-  if (!hash) return undefined;
-  const parts = hash.substring(1).split("-");
-  if (parts.length < 3) return undefined;
-  return findByGroupComponentName(
-    decodeURIComponent(parts[0]),
-    decodeURIComponent(parts[1]),
-    decodeURIComponent(parts[2]),
-  );
-}
-
-function Application(): VNode {
-  const initialSelection = getSelectionFromLocationHash(location.hash);
-  const [selected, updateSelected] = useState<ExampleItem | undefined>(
-    initialSelection,
-  );
-  useEffect(() => {
-    if (location.hash) {
-      const hash = location.hash.substring(1);
-      const found = document.getElementById(hash);
-      if (found) {
-        setTimeout(() => {
-          found.scrollIntoView({
-            block: "center",
-          });
-        }, 10);
-      }
-    }
-  }, []);
-
-  const ExampleContent = getContentForExample(selected);
-
-  // const GroupWrapper = getWrapperForGroup(selected?.group || "default");
-
-  return (
-    <Page>
-      <LiveReload />
-      <SideBar>
-        {allExamples.map((e) => (
-          <ExampleList
-            key={e.title}
-            name={e.title}
-            list={e.list}
-            selected={selected}
-            onSelectStory={(item, htmlId) => {
-              document.getElementById(htmlId)?.scrollIntoView({
-                block: "center",
-              });
-              updateSelected(item);
-            }}
-          />
-        ))}
-        <hr />
-      </SideBar>
-      <Content>
-        <ErrorReport selected={selected}>
-          {/* <GroupWrapper> */}
-          <ExampleContent />
-          {/* </GroupWrapper> */}
-        </ErrorReport>
-      </Content>
-    </Page>
+function main(): void {
+  renderStories(
+    { pages },
+    {
+      strings,
+    },
   );
 }
 
@@ -322,72 +44,3 @@ if (document.readyState === "loading") {
 } else {
   main();
 }
-function main(): void {
-  try {
-    const container = document.getElementById("container");
-    if (!container) {
-      throw Error("container not found, can't mount page contents");
-    }
-    render(<Application />, container);
-  } catch (e) {
-    console.error("got error", e);
-    if (e instanceof Error) {
-      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
-    }
-  }
-}
-
-let liveReloadMounted = false;
-function LiveReload({ port = 8002 }: { port?: number }): VNode {
-  const [isReloading, setIsReloading] = useState(false);
-  useEffect(() => {
-    if (!liveReloadMounted) {
-      setupLiveReload(port, () => {
-        setIsReloading(true);
-        window.location.reload();
-      });
-      liveReloadMounted = true;
-    }
-  });
-
-  if (isReloading) {
-    return (
-      <div
-        style={{
-          position: "absolute",
-          width: "100%",
-          height: "100%",
-          backgroundColor: "rgba(0,0,0,0.5)",
-          color: "white",
-          display: "flex",
-          justifyContent: "center",
-        }}
-      >
-        <h1 style={{ margin: "auto" }}>reloading...</h1>
-      </div>
-    );
-  }
-  return <Fragment />;
-}
-
-function setupLiveReload(port: number, onReload: () => void): void {
-  const socketPath = `ws://localhost:8003/socket`;
-  // const socketPath = `${protocol}//${host}:${port}/socket`;
-
-  const ws = new WebSocket(socketPath);
-  ws.onmessage = (message) => {
-    const event = JSON.parse(message.data);
-    if (event.type === "LOG") {
-      console.log(event.message);
-    }
-    if (event.type === "RELOAD") {
-      onReload();
-    }
-  };
-  ws.onerror = (error) => {
-    console.error(error);
-  };
-  ws.onclose = (e) => {
-    console.log("disconnected", e);
-  };
-}
diff --git a/packages/anastasis-webui/src/test-utils.ts 
b/packages/anastasis-webui/src/test-utils.ts
index 1fcc753ee..f220540f1 100644
--- a/packages/anastasis-webui/src/test-utils.ts
+++ b/packages/anastasis-webui/src/test-utils.ts
@@ -41,8 +41,10 @@ export function createExample<Props>(
   // check how we can build evaluatedProps in render time
   const evaluatedProps = typeof props === "function" ? props() : props;
   const Render = (args: any): VNode => create(Component, args);
-  Render.args = evaluatedProps;
-  return Render;
+  return {
+    component: Render,
+    props: evaluatedProps
+  };
 }
 
 export function createExampleWithCustomContext<Props, ContextProps>(
@@ -58,8 +60,10 @@ export function createExampleWithCustomContext<Props, 
ContextProps>(
       ...contextProps,
       children: [Render(args)],
     } as any);
-  WithContext.args = evaluatedProps;
-  return WithContext;
+  return {
+    component: WithContext,
+    props: evaluatedProps
+  };
 }
 
 export function NullLink({
diff --git a/packages/anastasis-webui/src/utils/index.tsx 
b/packages/anastasis-webui/src/utils/index.tsx
index 78973e38f..4cf839473 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -37,16 +37,18 @@ export function createExampleWithoutAnastasis<Props>(
   // check how we can build evaluatedProps in render time
   const evaluatedProps = typeof props === "function" ? props() : props;
   const Render = (args: any): VNode => h(Component, args);
-  Render.args = evaluatedProps;
-  return Render;
+  return {
+    component: Render,
+    props: evaluatedProps,
+  };
 }
 
 export function createExample<Props>(
   Component: FunctionalComponent<Props>,
   currentReducerState?: ReducerState,
   props?: Partial<Props>,
-): { (args: Props): VNode } {
-  const r = (args: Props): VNode => {
+): ComponentChildren {
+  const Render = (args: Props): VNode => {
     return (
       <AnastasisProvider
         value={{
@@ -74,8 +76,10 @@ export function createExample<Props>(
       </AnastasisProvider>
     );
   };
-  r.args = props;
-  return r;
+  return {
+    component: Render,
+    props: props,
+  };
 }
 
 const base = {
diff --git a/packages/anastasis-webui/watch/reply.sh 
b/packages/anastasis-webui/watch/reply.sh
deleted file mode 100755
index 20cbff37e..000000000
--- a/packages/anastasis-webui/watch/reply.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-SERVER_KEY=258EAFA5-E914-47DA-95CA-C5AB0DC85B11
-
-while read line; do
-  LINE=$(echo $line | tr -d '\r')
-  case $LINE in 
-    Sec-WebSocket-Key:*)
-      CLIENT_KEY="${LINE:19}"
-      export WS_ACCEPT=$( echo -n $CLIENT_KEY$SERVER_KEY | sha1sum | xxd -r -p 
| base64 )
-      ;;
-     "") break ;;
-  esac
-done
-
-cat watch/web_socket_server.reply | sed 's/$'"/`echo \\\r`/" | envsubst 
'$WS_ACCEPT'
-
-tail -n 0 -F /tmp/send_signal 2> /dev/null
-
diff --git a/packages/anastasis-webui/watch/send.sh 
b/packages/anastasis-webui/watch/send.sh
deleted file mode 100755
index 184cd2491..000000000
--- a/packages/anastasis-webui/watch/send.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-#https://datatracker.ietf.org/doc/html/rfc6455#page-65
-
-COMMAND=$1
-LEN=$(printf '%x\n' ${#COMMAND})
-
-#text command
-OPCODE=81
-
-cat <(echo -n $OPCODE$LEN | xxd -r -p) <(echo -n $COMMAND) >> /tmp/send_signal
-
diff --git a/packages/anastasis-webui/watch/send2.sh 
b/packages/anastasis-webui/watch/send2.sh
deleted file mode 100755
index 6a2881c19..000000000
--- a/packages/anastasis-webui/watch/send2.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-#https://datatracker.ietf.org/doc/html/rfc6455#page-65
-
-CONTENT=$( cat $1 | base64 -w 0 )
-COMMAND='{"type":"UPDATE","'$CONTENT'"}'
-LEN=$(printf '%0*x\n' 4 ${#COMMAND})
-echo $LEN
-LEN=00000138
-#text command
-OPCODE=81
-
-cat <(echo -n $OPCODE$LEN | xxd -r -p) <(echo -n $COMMAND) >> /tmp/send_signal
-
diff --git a/packages/anastasis-webui/watch/serve.sh 
b/packages/anastasis-webui/watch/serve.sh
deleted file mode 100755
index f4e9595d5..000000000
--- a/packages/anastasis-webui/watch/serve.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-#clean up
-rm /tmp/send_signal
-
-socat TCP-LISTEN:8003,fork,reuseaddr,keepalive EXEC:"./watch/reply.sh"
-
diff --git a/packages/anastasis-webui/watch/web_socket_client.request 
b/packages/anastasis-webui/watch/web_socket_client.request
deleted file mode 100644
index e7077b0cb..000000000
--- a/packages/anastasis-webui/watch/web_socket_client.request
+++ /dev/null
@@ -1,6 +0,0 @@
-GET /socket HTTP/1.1
-Connection: Upgrade
-Upgrade: websocket
-Sec-WebSocket-Version: 13
-Sec-WebSocket-Key: aaaaaaaaaaaaaaaaaaaaaa==
-
diff --git a/packages/anastasis-webui/watch/web_socket_server.reply 
b/packages/anastasis-webui/watch/web_socket_server.reply
deleted file mode 100644
index b4e0db001..000000000
--- a/packages/anastasis-webui/watch/web_socket_server.reply
+++ /dev/null
@@ -1,5 +0,0 @@
-HTTP/1.1 101 Switching Protocols
-Upgrade: websocket
-Connection: Upgrade
-Sec-WebSocket-Accept: $WS_ACCEPT
-
diff --git a/packages/demobank-ui/build.mjs b/packages/demobank-ui/build.mjs
index 63ddc1f25..c93b4eb67 100755
--- a/packages/demobank-ui/build.mjs
+++ b/packages/demobank-ui/build.mjs
@@ -18,9 +18,9 @@
 import esbuild from "esbuild";
 import path from "path";
 import fs from "fs";
-import crypto from "crypto";
-import { sassPlugin } from "esbuild-sass-plugin";
+import sass from "sass";
 
+// eslint-disable-next-line no-undef
 const BASE = process.cwd();
 
 const preact = path.join(
@@ -44,14 +44,16 @@ const preactCompatPlugin = {
   },
 };
 
-const entryPoints = ["src/index.tsx"];
+const entryPoints = ["src/index.tsx", "src/stories.tsx"];
 
 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_ROOT === "/" ? undefined : git_hash();
@@ -86,6 +88,26 @@ function copyFilesPlugin(options) {
   };
 }
 
+const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
+
+const buildSassPlugin = {
+  name: "custom-build-sass",
+  setup(build) {
+
+    build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
+      const resolveDir = path.dirname(file)
+      const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
+
+      return {
+        resolveDir,
+        loader: 'css',
+        contents
+      }
+    });
+
+  },
+};
+
 export const buildConfig = {
   entryPoints: [...entryPoints],
   bundle: true,
@@ -95,6 +117,10 @@ export const buildConfig = {
     ".svg": "file",
     ".png": "dataurl",
     ".jpeg": "dataurl",
+    '.ttf': 'file',
+    '.woff': 'file',
+    '.woff2': 'file',
+    '.eot': 'file',
   },
   target: ["es6"],
   format: "esm",
@@ -108,17 +134,14 @@ export const buildConfig = {
   },
   plugins: [
     preactCompatPlugin,
-    sassPlugin(),
     copyFilesPlugin([
       {
-        src: "static/index.html",
-        dest: "dist/index.html",
+        src: "./src/index.html",
+        dest: "./dist/index.html",
       },
     ]),
+    buildSassPlugin
   ],
 };
 
-esbuild.build(buildConfig).catch((e) => {
-  console.log(e);
-  process.exit(1);
-});
+await esbuild.build(buildConfig)
diff --git a/packages/demobank-ui/src/pages/notfound/index.tsx 
b/packages/demobank-ui/dev.mjs
old mode 100644
new mode 100755
similarity index 65%
copy from packages/demobank-ui/src/pages/notfound/index.tsx
copy to packages/demobank-ui/dev.mjs
index 474451cc6..35a9fa16c
--- a/packages/demobank-ui/src/pages/notfound/index.tsx
+++ b/packages/demobank-ui/dev.mjs
@@ -1,3 +1,4 @@
+#!/usr/bin/env node
 /*
  This file is part of GNU Taler
  (C) 2022 Taler Systems S.A.
@@ -14,19 +15,16 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { FunctionalComponent, h } from "preact";
-import { Link } from "preact-router/match";
+import { serve } from "@gnu-taler/web-util/lib/index.node";
+import esbuild from "esbuild";
+import { buildConfig } from "./build.mjs";
 
-const Notfound: FunctionalComponent = () => {
-  return (
-    <div>
-      <h1>Error 404</h1>
-      <p>That page doesn&apos;t exist.</p>
-      <Link href="/">
-        <h4>Back to Home</h4>
-      </Link>
-    </div>
-  );
-};
+buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
 
-export default Notfound;
+serve({
+  folder: './dist',
+  port: 8080,
+  source: './src',
+  development: true,
+  onUpdate: async () => esbuild.build(buildConfig)
+})
diff --git a/packages/demobank-ui/package.json 
b/packages/demobank-ui/package.json
index cc8048a0f..41031977f 100644
--- a/packages/demobank-ui/package.json
+++ b/packages/demobank-ui/package.json
@@ -11,13 +11,13 @@
   },
   "dependencies": {
     "@gnu-taler/taler-util": "workspace:*",
+    "@gnu-taler/web-util": "workspace:*",
     "date-fns": "2.29.3",
     "history": "4.10.1",
     "jed": "1.1.1",
-    "preact": "10.6.5",
+    "preact": "10.11.3",
     "preact-router": "3.2.1",
     "qrcode-generator": "^1.4.4",
-    "react": "npm:@preact/compat@^17.1.2",
     "swr": "1.3.0"
   },
   "devDependencies": {
@@ -30,11 +30,10 @@
     "bulma-checkbox": "^1.1.1",
     "bulma-radio": "^1.1.1",
     "esbuild": "^0.15.12",
-    "esbuild-sass-plugin": "^2.4.0",
     "eslint": "^8.26.0",
     "eslint-config-preact": "^1.2.0",
     "po2json": "^0.4.5",
-    "sass": "1.32.13",
+    "sass": "1.56.1",
     "typescript": "^4.4.4"
   }
 }
diff --git a/packages/demobank-ui/src/components/menu/SideBar.tsx 
b/packages/demobank-ui/src/components/menu/SideBar.tsx
index d7833df5a..7bfba2a75 100644
--- a/packages/demobank-ui/src/components/menu/SideBar.tsx
+++ b/packages/demobank-ui/src/components/menu/SideBar.tsx
@@ -20,7 +20,7 @@
  */
 
 import { h, VNode } from "preact";
-import { Translate } from "../../i18n";
+import { useTranslationContext } from "../../context/translation.js";
 
 interface Props {
   mobile?: boolean;
@@ -31,6 +31,7 @@ export function Sidebar({ mobile }: Props): VNode {
   const config = { version: "none" };
   // FIXME: add replacement for __VERSION__ with the current version
   const process = { env: { __VERSION__: "0.0.0" } };
+  const { i18n } = useTranslationContext();
 
   return (
     <aside class="aside is-placed-left is-expanded">
@@ -49,20 +50,20 @@ export function Sidebar({ mobile }: Props): VNode {
       </div>
       <div class="menu is-menu-main">
         <p class="menu-label">
-          <Translate>Bank menu</Translate>
+          <i18n.Translate>Bank menu</i18n.Translate>
         </p>
         <ul class="menu-list">
           <li>
             <div class="ml-4">
               <span class="menu-item-label">
-                <Translate>Select option1</Translate>
+                <i18n.Translate>Select option1</i18n.Translate>
               </span>
             </div>
           </li>
           <li>
             <div class="ml-4">
               <span class="menu-item-label">
-                <Translate>Select option2</Translate>
+                <i18n.Translate>Select option2</i18n.Translate>
               </span>
             </div>
           </li>
diff --git a/packages/demobank-ui/src/components/picker/DurationPicker.tsx 
b/packages/demobank-ui/src/components/picker/DurationPicker.tsx
index 94f2326bc..b8a7671c3 100644
--- a/packages/demobank-ui/src/components/picker/DurationPicker.tsx
+++ b/packages/demobank-ui/src/components/picker/DurationPicker.tsx
@@ -21,7 +21,7 @@
 
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { useTranslator } from "../../i18n";
+import { useTranslationContext } from "../../context/translation.js";
 import "../../scss/DurationPicker.scss";
 
 export interface Props {
@@ -46,13 +46,13 @@ export function DurationPicker({
   const ms = ss * 60;
   const hs = ms * 60;
   const ds = hs * 24;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   return (
     <div class="rdp-picker">
       {days && (
         <DurationColumn
-          unit={i18n`days`}
+          unit={i18n.str`days`}
           max={99}
           value={Math.floor(value / ds)}
           onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
@@ -62,7 +62,7 @@ export function DurationPicker({
       )}
       {hours && (
         <DurationColumn
-          unit={i18n`hours`}
+          unit={i18n.str`hours`}
           max={23}
           min={1}
           value={Math.floor(value / hs) % 24}
@@ -73,7 +73,7 @@ export function DurationPicker({
       )}
       {minutes && (
         <DurationColumn
-          unit={i18n`minutes`}
+          unit={i18n.str`minutes`}
           max={59}
           min={1}
           value={Math.floor(value / ms) % 60}
@@ -84,7 +84,7 @@ export function DurationPicker({
       )}
       {seconds && (
         <DurationColumn
-          unit={i18n`seconds`}
+          unit={i18n.str`seconds`}
           max={59}
           value={Math.floor(value / ss) % 60}
           onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
diff --git a/packages/demobank-ui/src/context/translation.ts 
b/packages/demobank-ui/src/context/translation.ts
index a411ecb16..a50f81b86 100644
--- a/packages/demobank-ui/src/context/translation.ts
+++ b/packages/demobank-ui/src/context/translation.ts
@@ -19,27 +19,42 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { i18n, setupI18n } from "@gnu-taler/taler-util";
 import { createContext, h, VNode } from "preact";
 import { useContext, useEffect } from "preact/hooks";
-import { useLang } from "../hooks/index.js";
-import * as jedLib from "jed";
+import { useLang } from "../hooks/useLang.js";
 import { strings } from "../i18n/strings.js";
 
 interface Type {
   lang: string;
-  handler: any;
+  supportedLang: { [id in keyof typeof supportedLang]: string };
   changeLanguage: (l: string) => void;
+  i18n: typeof i18n;
+  isSaved: boolean;
 }
+
+const supportedLang = {
+  es: "Español [es]",
+  ja: "日本語 [ja]",
+  en: "English [en]",
+  fr: "Français [fr]",
+  de: "Deutsch [de]",
+  sv: "Svenska [sv]",
+  it: "Italiano [it]",
+  // ko: "한국어 [ko]",
+  // ru: "Ру́сский язы́к [ru]",
+  tr: "Türk [tr]",
+  navigator: "Defined by navigator",
+};
+
 const initial = {
   lang: "en",
-  handler: null,
+  supportedLang,
   changeLanguage: () => {
-    /**
-     * This function will be replaced by one with
-     * the same signature _but_ coming from the state.
-     * FIXME: clarify this design.
-     */
+    // do not change anything
   },
+  i18n,
+  isSaved: false,
 };
 const Context = createContext<Type>(initial);
 
@@ -55,14 +70,23 @@ export const TranslationProvider = ({
   children,
   forceLang,
 }: Props): VNode => {
-  const [lang, changeLanguage] = useLang(initial);
+  const [lang, changeLanguage, isSaved] = useLang(initial);
   useEffect(() => {
-    if (forceLang) changeLanguage(forceLang);
+    if (forceLang) {
+      changeLanguage(forceLang);
+    }
   });
-  console.log("lang store", strings);
-  const handler = new jedLib.Jed(strings[lang] || strings["en"]);
+  useEffect(() => {
+    setupI18n(lang, strings);
+  }, [lang]);
+  if (forceLang) {
+    setupI18n(forceLang, strings);
+  } else {
+    setupI18n(lang, strings);
+  }
+
   return h(Context.Provider, {
-    value: { lang, handler, changeLanguage },
+    value: { lang, changeLanguage, supportedLang, i18n, isSaved },
     children,
   });
 };
diff --git a/packages/demobank-ui/src/hooks/index.ts 
b/packages/demobank-ui/src/hooks/index.ts
index 94e66e5e3..b4191d182 100644
--- a/packages/demobank-ui/src/hooks/index.ts
+++ b/packages/demobank-ui/src/hooks/index.ts
@@ -19,7 +19,8 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { StateUpdater, useState } from "preact/hooks";
+import { StateUpdater } from "preact/hooks";
+import { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
 export type ValueOrFunction<T> = T | ((p: T) => T);
 
 const calculateRootPath = () => {
@@ -68,79 +69,3 @@ export function useBackendInstanceToken(
 
   return [token, setToken];
 }
-
-export function useLang(initial?: string): [string, StateUpdater<string>] {
-  const browserLang =
-    typeof window !== "undefined"
-      ? navigator.language || (navigator as any).userLanguage
-      : undefined;
-  const defaultLang = (browserLang || initial || "en").substring(0, 2);
-  const [value, setValue] = useNotNullLocalStorage(
-    "lang-preference",
-    defaultLang,
-  );
-  function updateValue(newValue: string | ((v: string) => string)) {
-    if (document.body.parentElement) {
-      const htmlElement = document.body.parentElement;
-      if (typeof newValue === "string") {
-        htmlElement.lang = newValue;
-        setValue(newValue);
-      } else if (typeof newValue === "function")
-        setValue((old) => {
-          const nv = newValue(old);
-          htmlElement.lang = nv;
-          return nv;
-        });
-    } else setValue(newValue);
-  }
-  return [value, updateValue];
-}
-
-export function useLocalStorage(
-  key: string,
-  initialValue?: string,
-): [string | undefined, StateUpdater<string | undefined>] {
-  const [storedValue, setStoredValue] = useState<string | undefined>(
-    (): string | undefined => {
-      return typeof window !== "undefined"
-        ? window.localStorage.getItem(key) || initialValue
-        : initialValue;
-    },
-  );
-
-  const setValue = (
-    value?: string | ((val?: string) => string | undefined),
-  ) => {
-    setStoredValue((p) => {
-      const toStore = value instanceof Function ? value(p) : value;
-      if (typeof window !== "undefined")
-        if (!toStore) window.localStorage.removeItem(key);
-        else window.localStorage.setItem(key, toStore);
-
-      return toStore;
-    });
-  };
-
-  return [storedValue, setValue];
-}
-
-export function useNotNullLocalStorage(
-  key: string,
-  initialValue: string,
-): [string, StateUpdater<string>] {
-  const [storedValue, setStoredValue] = useState<string>((): string => {
-    return typeof window !== "undefined"
-      ? window.localStorage.getItem(key) || initialValue
-      : initialValue;
-  });
-
-  const setValue = (value: string | ((val: string) => string)) => {
-    const valueToStore = value instanceof Function ? value(storedValue) : 
value;
-    setStoredValue(valueToStore);
-    if (typeof window !== "undefined")
-      if (!valueToStore) window.localStorage.removeItem(key);
-      else window.localStorage.setItem(key, valueToStore);
-  };
-
-  return [storedValue, setValue];
-}
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss 
b/packages/demobank-ui/src/hooks/useLang.ts
similarity index 59%
copy from packages/anastasis-webui/src/scss/_mixins.scss
copy to packages/demobank-ui/src/hooks/useLang.ts
index 64315785b..5b02c5255 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/demobank-ui/src/hooks/useLang.ts
@@ -14,21 +14,17 @@
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { useNotNullLocalStorage } from "./useLocalStorage.js";
 
-@mixin transition($t) {
-  transition: $t 250ms ease-in-out 50ms;
+function getBrowserLang(): string | undefined {
+  if (window.navigator.languages) return window.navigator.languages[0];
+  if (window.navigator.language) return window.navigator.language;
+  return undefined;
 }
 
-@mixin icon-with-update-mark($icon-base-width) {
-  .icon {
-    width: $icon-base-width;
-
-    &.has-update-mark:after {
-      right: ($icon-base-width / 2) - 0.85;
-    }
-  }
+export function useLang(
+  initial?: string,
+): [string, (s: string) => void, boolean] {
+  const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
+  return useNotNullLocalStorage("lang-preference", defaultLang);
 }
diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts 
b/packages/demobank-ui/src/hooks/useLocalStorage.ts
similarity index 77%
copy from packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
copy to packages/demobank-ui/src/hooks/useLocalStorage.ts
index 88b7655b6..ed5b491f2 100644
--- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
+++ b/packages/demobank-ui/src/hooks/useLocalStorage.ts
@@ -1,17 +1,17 @@
 /*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
 
- 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
+ GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
+ terms of the GNU Affero 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
+ GNU Anastasis 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.
+ A PARTICULAR PURPOSE.  See the GNU Affero 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/>
+ You should have received a copy of the GNU Affero General Public License 
along with
+ GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
 /**
diff --git a/packages/demobank-ui/src/i18n/index.tsx 
b/packages/demobank-ui/src/i18n/index.tsx
deleted file mode 100644
index 2489184b2..000000000
--- a/packages/demobank-ui/src/i18n/index.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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/>
- */
-
-/**
- * Translation helpers for React components and template literals.
- */
-
-/**
- * Imports
- */
-import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
-
-import { useTranslationContext } from "../context/translation";
-
-export function useTranslator() {
-  const ctx = useTranslationContext();
-  const jed = ctx.handler;
-  return function str(
-    stringSeq: TemplateStringsArray,
-    ...values: any[]
-  ): string {
-    const s = toI18nString(stringSeq);
-    if (!s) return s;
-    const tr = jed
-      .translate(s)
-      .ifPlural(1, s)
-      .fetch(...values);
-    return tr;
-  };
-}
-
-/**
- * Convert template strings to a msgid
- */
-function toI18nString(stringSeq: ReadonlyArray<string>): string {
-  let s = "";
-  for (let i = 0; i < stringSeq.length; i++) {
-    s += stringSeq[i];
-    if (i < stringSeq.length - 1) s += `%${i + 1}$s`;
-  }
-  return s;
-}
-
-interface TranslateSwitchProps {
-  target: number;
-  children: ComponentChildren;
-}
-
-function stringifyChildren(children: ComponentChildren): string {
-  let n = 1;
-  const ss = (children instanceof Array ? children : [children]).map((c) => {
-    if (typeof c === "string") return c;
-
-    return `%${n++}$s`;
-  });
-  const s = ss.join("").replace(/ +/g, " ").trim();
-  return s;
-}
-
-interface TranslateProps {
-  children: ComponentChildren;
-  /**
-   * Component that the translated element should be wrapped in.
-   * Defaults to "div".
-   */
-  wrap?: any;
-
-  /**
-   * Props to give to the wrapped component.
-   */
-  wrapProps?: any;
-}
-
-function getTranslatedChildren(
-  translation: string,
-  children: ComponentChildren,
-): ComponentChild[] {
-  const tr = translation.split(/%(\d+)\$s/);
-  const childArray = children instanceof Array ? children : [children];
-  // Merge consecutive string children.
-  const placeholderChildren = Array<ComponentChild>();
-  for (let i = 0; i < childArray.length; i++) {
-    const x = childArray[i];
-    if (x === undefined) continue;
-    else if (typeof x === "string") continue;
-    else placeholderChildren.push(x);
-  }
-  const result = Array<ComponentChild>();
-  for (let i = 0; i < tr.length; i++)
-    if (i % 2 == 0)
-      // Text
-      result.push(tr[i]);
-    else {
-      const childIdx = Number.parseInt(tr[i], 10) - 1;
-      result.push(placeholderChildren[childIdx]);
-    }
-
-  return result;
-}
-
-/**
- * Translate text node children of this component.
- * If a child component might produce a text node, it must be wrapped
- * in a another non-text element.
- *
- * Example:
- * ```
- * <Translate>
- * Hello.  Your score is <span><PlayerScore player={player} /></span>
- * </Translate>
- * ```
- */
-export function Translate({ children }: TranslateProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation: string = ctx.handler.ngettext(s, s, 1);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
-
-/**
- * Switch translation based on singular or plural based on the target prop.
- * Should only contain TranslateSingular and TransplatePlural as children.
- *
- * Example:
- * ```
- * <TranslateSwitch target={n}>
- *  <TranslateSingular>I have {n} apple.</TranslateSingular>
- *  <TranslatePlural>I have {n} apples.</TranslatePlural>
- * </TranslateSwitch>
- * ```
- */
-export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
-  let singular: VNode<TranslationPluralProps> | undefined;
-  let plural: VNode<TranslationPluralProps> | undefined;
-  // const children = this.props.children;
-  if (children)
-    (children instanceof Array ? children : [children]).forEach(
-      (child: any) => {
-        if (child.type === TranslatePlural) plural = child;
-
-        if (child.type === TranslateSingular) singular = child;
-      },
-    );
-
-  if (!singular || !plural) {
-    console.error("translation not found");
-    return h("span", {}, ["translation not found"]);
-  }
-  singular.props.target = target;
-  plural.props.target = target;
-  // We're looking up the translation based on the
-  // singular, even if we must use the plural form.
-  return singular;
-}
-
-interface TranslationPluralProps {
-  children: ComponentChildren;
-  target: number;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslatePlural({
-  children,
-  target,
-}: TranslationPluralProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation = ctx.handler.ngettext(s, s, 1);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslateSingular({
-  children,
-  target,
-}: TranslationPluralProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation = ctx.handler.ngettext(s, s, target);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
diff --git a/packages/demobank-ui/src/index.html 
b/packages/demobank-ui/src/index.html
index a2154429b..4b3c89a66 100644
--- a/packages/demobank-ui/src/index.html
+++ b/packages/demobank-ui/src/index.html
@@ -16,25 +16,26 @@
  @author Sebastian Javier Marchano
 -->
 <!DOCTYPE html>
-<html
-  lang="en"
-  class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top 
has-aside-expanded"
->
+<html lang="en">
   <head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width,initial-scale=1" />
     <meta name="mobile-web-app-capable" content="yes" />
     <meta name="apple-mobile-web-app-capable" content="yes" />
-
     <link
       rel="icon"
       
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/
 [...]
     />
     <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+    <title>Demobank</title>
+    <!-- Optional customization script.  -->
+    <script src="demobank-ui-settings.js"></script>
+    <!-- Entry point for the demobank SPA. -->
+    <script type="module" src="index.js"></script>
+    <link rel="stylesheet" href="index.css" />
   </head>
-
   <body>
     <div id="app"></div>
-    <script type="module" src="index.tsx"></script>
   </body>
 </html>
diff --git a/packages/demobank-ui/src/index.tsx 
b/packages/demobank-ui/src/index.tsx
index 4302bb33b..0b88b0393 100644
--- a/packages/demobank-ui/src/index.tsx
+++ b/packages/demobank-ui/src/index.tsx
@@ -1,6 +1,7 @@
 import App from "./components/app.js";
 export default App;
 import { render, h } from "preact";
+import "./scss/main.scss";
 
 const app = document.getElementById("app");
 
diff --git 
a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx 
b/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx
similarity index 78%
copy from 
packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
copy to packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx
index 2479274cb..521d4255e 100644
--- 
a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
+++ b/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx
@@ -19,11 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { createExample } from "../../test-utils.js";
-// import { ReadyView } from "./views.js";
+import { QrCodeSection } from "./QrCodeSection.js";
 
 export default {
-  title: "TermsOfService",
+  title: "Qr Code Selection",
 };
 
-// export const Ready = createExample(ReadyView, {});
+export const SimpleExample = {
+  component: QrCodeSection,
+  props: {
+    talerWithdrawUri: "taler://withdraw/asdasdasd",
+  },
+};
diff --git a/packages/demobank-ui/src/pages/home/QrCodeSection.tsx 
b/packages/demobank-ui/src/pages/home/QrCodeSection.tsx
new file mode 100644
index 000000000..1d7b3db10
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/QrCodeSection.tsx
@@ -0,0 +1,55 @@
+/*
+ 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/>
+ */
+
+import { h, VNode } from "preact";
+import { useEffect } from "preact/hooks";
+import { QR } from "../../components/QR.js";
+import { useTranslationContext } from "../../context/translation.js";
+
+export function QrCodeSection({
+  talerWithdrawUri,
+  abortButton,
+}: {
+  talerWithdrawUri: string;
+  abortButton: h.JSX.Element;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  useEffect(() => {
+    //Taler Wallet WebExtension is listening to headers response and tab 
updates.
+    //In the SPA there is no header response with the Taler URI so
+    //this hack manually triggers the tab update after the QR is in the DOM.
+    window.location.hash = `/account/${new Date().getTime()}`;
+  }, []);
+
+  return (
+    <section id="main" class="content">
+      <h1 class="nav">{i18n.str`Transfer to Taler Wallet`}</h1>
+      <article>
+        <div class="qr-div">
+          <p>{i18n.str`Use this QR code to withdraw to your mobile 
wallet:`}</p>
+          {QR({ text: talerWithdrawUri })}
+          <p>
+            Click{" "}
+            <a id="linkqr" href={talerWithdrawUri}>{i18n.str`this link`}</a> to
+            open your Taler wallet!
+          </p>
+          <br />
+          {abortButton}
+        </div>
+      </article>
+    </section>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/home/index.stories.tsx 
b/packages/demobank-ui/src/pages/home/index.stories.tsx
new file mode 100644
index 000000000..e9ac00a76
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/index.stories.tsx
@@ -0,0 +1 @@
+export * as qr from "./QrCodeSection.stories.js";
diff --git a/packages/demobank-ui/src/pages/home/index.tsx 
b/packages/demobank-ui/src/pages/home/index.tsx
index 8f522c07c..8b2ffefac 100644
--- a/packages/demobank-ui/src/pages/home/index.tsx
+++ b/packages/demobank-ui/src/pages/home/index.tsx
@@ -27,13 +27,16 @@ import {
 } from "preact/hooks";
 import talerLogo from "../../assets/logo-white.svg";
 import { LangSelectorLikePy as LangSelector } from 
"../../components/menu/LangSelector.js";
-import { QR } from "../../components/QR.js";
-import { useLocalStorage, useNotNullLocalStorage } from "../../hooks/index.js";
-import { Translate, useTranslator } from "../../i18n/index.js";
-import "../../scss/main.scss";
+import {
+  useLocalStorage,
+  useNotNullLocalStorage,
+} from "../../hooks/useLocalStorage.js";
+// import { Translate, useTranslator } from "../../i18n/index.js";
+import { useTranslationContext } from "../../context/translation.js";
 import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
 import { createHashHistory } from "history";
 import Router, { Route, route } from "preact-router";
+import { QrCodeSection } from "./QrCodeSection.js";
 
 interface BankUiSettings {
   allowRegistrations: boolean;
@@ -987,7 +990,7 @@ async function registrationCall(
 
 function ErrorBanner(Props: any): VNode | null {
   const [pageState, pageStateSetter] = Props.pageState;
-  // const i18n = useTranslator();
+  // const { i18n } = useTranslationContext();
   if (!pageState.error) return null;
 
   const rval = (
@@ -1041,7 +1044,7 @@ function StatusBanner(Props: any): VNode | null {
 }
 
 function BankFrame(Props: any): VNode {
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   const [pageState, pageStateSetter] = useContext(PageContext);
   console.log("BankFrame state", pageState);
   const logOut = (
@@ -1062,7 +1065,7 @@ function BankFrame(Props: any): VNode {
             };
           });
         }}
-      >{i18n`Logout`}</a>
+      >{i18n.str`Logout`}</a>
     </div>
   );
 
@@ -1080,7 +1083,7 @@ function BankFrame(Props: any): VNode {
         class="demobar"
         style="display: flex; flex-direction: row; justify-content: 
space-between;"
       >
-        <a href="#main" class="skip">{i18n`Skip to main content`}</a>
+        <a href="#main" class="skip">{i18n.str`Skip to main content`}</a>
         <div style="max-width: 50em; margin-left: 2em;">
           <h1>
             <span class="it">
@@ -1089,7 +1092,7 @@ function BankFrame(Props: any): VNode {
           </h1>
           {maybeDemoContent(
             <p>
-              <Translate>
+              <i18n.Translate>
                 This part of the demo shows how a bank that supports Taler
                 directly would work. In addition to using your own bank 
account,
                 you can also see the transaction history of some{" "}
@@ -1100,14 +1103,14 @@ function BankFrame(Props: any): VNode {
                   Public Accounts
                 </a>
                 .
-              </Translate>
+              </i18n.Translate>
             </p>,
           )}
         </div>
         <a href="https://taler.net/";>
           <img
             src={talerLogo}
-            alt={i18n`Taler logo`}
+            alt={i18n.str`Taler logo`}
             height="100"
             width="224"
             style="margin: 2em 2em"
@@ -1168,7 +1171,7 @@ function PaytoWireTransfer(Props: any): VNode {
   const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
     undefined,
   );
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   const { focus, backendState } = Props;
   const amountRegex = "^[0-9]+(.[0-9]+)?$";
   const ibanRegex = "^[A-Z][A-Z][0-9]+$";
@@ -1193,17 +1196,17 @@ function PaytoWireTransfer(Props: any): VNode {
     ? undefined
     : undefinedIfEmpty({
         iban: !submitData.iban
-          ? i18n`Missing IBAN`
+          ? i18n.str`Missing IBAN`
           : !/^[A-Z0-9]*$/.test(submitData.iban)
-          ? i18n`IBAN should have just uppercased letters and numbers`
+          ? i18n.str`IBAN should have just uppercased letters and numbers`
           : undefined,
-        subject: !submitData.subject ? i18n`Missing subject` : undefined,
+        subject: !submitData.subject ? i18n.str`Missing subject` : undefined,
         amount: !submitData.amount
-          ? i18n`Missing amount`
+          ? i18n.str`Missing amount`
           : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
-          ? i18n`Amount is not valid`
+          ? i18n.str`Amount is not valid`
           : Amounts.isZero(parsedAmount)
-          ? i18n`Should be greater than 0`
+          ? i18n.str`Should be greater than 0`
           : undefined,
       });
 
@@ -1212,7 +1215,7 @@ function PaytoWireTransfer(Props: any): VNode {
       <div>
         <div class="pure-form" name="wire-transfer-form">
           <p>
-            <label for="iban">{i18n`Receiver IBAN:`}</label>&nbsp;
+            <label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
             <input
               ref={ref}
               type="text"
@@ -1235,7 +1238,7 @@ function PaytoWireTransfer(Props: any): VNode {
               isDirty={submitData?.iban !== undefined}
             />
             <br />
-            <label for="subject">{i18n`Transfer subject:`}</label>&nbsp;
+            <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
             <input
               type="text"
               name="subject"
@@ -1256,7 +1259,7 @@ function PaytoWireTransfer(Props: any): VNode {
               isDirty={submitData?.subject !== undefined}
             />
             <br />
-            <label for="amount">{i18n`Amount:`}</label>&nbsp;
+            <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
             <input
               type="number"
               name="amount"
@@ -1309,7 +1312,7 @@ function PaytoWireTransfer(Props: any): VNode {
                     ...prevState,
 
                     error: {
-                      title: i18n`Field(s) missing.`,
+                      title: i18n.str`Field(s) missing.`,
                     },
                   }));
                   return;
@@ -1358,7 +1361,7 @@ function PaytoWireTransfer(Props: any): VNode {
               }));
             }}
           >
-            {i18n`Want to try the raw payto://-format?`}
+            {i18n.str`Want to try the raw payto://-format?`}
           </a>
         </p>
       </div>
@@ -1366,18 +1369,18 @@ function PaytoWireTransfer(Props: any): VNode {
 
   const errorsPayto = undefinedIfEmpty({
     rawPaytoInput: !rawPaytoInput
-      ? i18n`Missing payto address`
+      ? i18n.str`Missing payto address`
       : !parsePaytoUri(rawPaytoInput)
-      ? i18n`Payto does not follow the pattern`
+      ? i18n.str`Payto does not follow the pattern`
       : undefined,
   });
 
   return (
     <div>
-      <p>{i18n`Transfer money to account identified by payto:// URI:`}</p>
+      <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
       <div class="pure-form" name="payto-form">
         <p>
-          <label for="address">{i18n`payto URI:`}</label>&nbsp;
+          <label for="address">{i18n.str`payto URI:`}</label>&nbsp;
           <input
             name="address"
             type="text"
@@ -1386,7 +1389,7 @@ function PaytoWireTransfer(Props: any): VNode {
             id="address"
             value={rawPaytoInput ?? ""}
             required
-            placeholder={i18n`payto address`}
+            placeholder={i18n.str`payto address`}
             // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 
]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
             onInput={(e): void => {
               rawPaytoInputSetter(e.currentTarget.value);
@@ -1410,7 +1413,7 @@ function PaytoWireTransfer(Props: any): VNode {
             class="pure-button pure-button-primary"
             type="submit"
             disabled={!!errorsPayto}
-            value={i18n`Send`}
+            value={i18n.str`Send`}
             onClick={async () => {
               // empty string evaluates to false.
               if (!rawPaytoInput) {
@@ -1444,7 +1447,7 @@ function PaytoWireTransfer(Props: any): VNode {
               }));
             }}
           >
-            {i18n`Use wire-transfer form?`}
+            {i18n.str`Use wire-transfer form?`}
           </a>
         </p>
       </div>
@@ -1459,7 +1462,7 @@ function PaytoWireTransfer(Props: any): VNode {
 function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
   const [pageState, pageStateSetter] = useContext(PageContext);
   const { backendState } = Props;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   const captchaNumbers = {
     a: Math.floor(Math.random() * 10),
     b: Math.floor(Math.random() * 10),
@@ -1468,15 +1471,15 @@ function TalerWithdrawalConfirmationQuestion(Props: 
any): VNode {
 
   return (
     <Fragment>
-      <h1 class="nav">{i18n`Confirm Withdrawal`}</h1>
+      <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
       <article>
         <div class="challenge-div">
           <form class="challenge-form">
             <div class="pure-form" id="captcha" name="capcha-form">
-              <h2>{i18n`Authorize withdrawal by solving challenge`}</h2>
+              <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
               <p>
                 <label for="answer">
-                  {i18n`What is`}&nbsp;
+                  {i18n.str`What is`}&nbsp;
                   <em>
                     {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
                   </em>
@@ -1514,12 +1517,12 @@ function TalerWithdrawalConfirmationQuestion(Props: 
any): VNode {
                       ...prevState,
 
                       error: {
-                        title: i18n`Answer is wrong.`,
+                        title: i18n.str`Answer is wrong.`,
                       },
                     }));
                   }}
                 >
-                  {i18n`Confirm`}
+                  {i18n.str`Confirm`}
                 </button>
                 &nbsp;
                 <button
@@ -1532,18 +1535,18 @@ function TalerWithdrawalConfirmationQuestion(Props: 
any): VNode {
                     )
                   }
                 >
-                  {i18n`Cancel`}
+                  {i18n.str`Cancel`}
                 </button>
               </p>
             </div>
           </form>
           <div class="hint">
             <p>
-              <Translate>
+              <i18n.Translate>
                 A this point, a <b>real</b> bank would ask for an additional
                 authentication proof (PIN/TAN, one time password, ..), instead
                 of a simple calculation.
-              </Translate>
+              </i18n.Translate>
             </p>
           </div>
         </div>
@@ -1552,40 +1555,6 @@ function TalerWithdrawalConfirmationQuestion(Props: 
any): VNode {
   );
 }
 
-function QrCodeSection({
-  talerWithdrawUri,
-  abortButton,
-}: {
-  talerWithdrawUri: string;
-  abortButton: h.JSX.Element;
-}): VNode {
-  const i18n = useTranslator();
-  useEffect(() => {
-    //Taler Wallet WebExtension is listening to headers response and tab 
updates.
-    //In the SPA there is no header response with the Taler URI so
-    //this hack manually triggers the tab update after the QR is in the DOM.
-    window.location.hash = `/account/${new Date().getTime()}`;
-  }, []);
-
-  return (
-    <section id="main" class="content">
-      <h1 class="nav">{i18n`Transfer to Taler Wallet`}</h1>
-      <article>
-        <div class="qr-div">
-          <p>{i18n`Use this QR code to withdraw to your mobile wallet:`}</p>
-          {QR({ text: talerWithdrawUri })}
-          <p>
-            Click <a id="linkqr" href={talerWithdrawUri}>{i18n`this 
link`}</a>{" "}
-            to open your Taler wallet!
-          </p>
-          <br />
-          {abortButton}
-        </div>
-      </article>
-    </section>
-  );
-}
-
 /**
  * Offer the QR code (and a clickable taler://-link) to
  * permit the passing of exchange and reserve details to
@@ -1595,7 +1564,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
   // turns true when the wallet POSTed the reserve details:
   const [pageState, pageStateSetter] = useContext(PageContext);
   const { withdrawalId, talerWithdrawUri, accountLabel, backendState } = Props;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   const abortButton = (
     <a
       class="pure-button btn-cancel"
@@ -1609,7 +1578,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
           };
         });
       }}
-    >{i18n`Abort`}</a>
+    >{i18n.str`Abort`}</a>
   );
 
   console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
@@ -1629,7 +1598,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
       ...prevState,
 
       error: {
-        title: i18n`withdrawal (${withdrawalId}) was never (correctly) created 
at the bank...`,
+        title: i18n.str`withdrawal (${withdrawalId}) was never (correctly) 
created at the bank...`,
       },
     }));
     return (
@@ -1643,7 +1612,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
 
   // data didn't arrive yet and wallet didn't communicate:
   if (typeof data === "undefined")
-    return <p>{i18n`Waiting the bank to create the operation...`}</p>;
+    return <p>{i18n.str`Waiting the bank to create the operation...`}</p>;
 
   /**
    * Wallet didn't communicate withdrawal details yet:
@@ -1657,7 +1626,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
         withdrawalInProgress: false,
 
         error: {
-          title: i18n`This withdrawal was aborted!`,
+          title: i18n.str`This withdrawal was aborted!`,
         },
       };
     });
@@ -1680,7 +1649,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
 function WalletWithdraw(Props: any): VNode {
   const { backendState, pageStateSetter, focus } = Props;
   const currency = useContext(CurrencyContext);
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   let submitAmount = "5.00";
   const amountRegex = "^[0-9]+(.[0-9]+)?$";
 
@@ -1691,7 +1660,8 @@ function WalletWithdraw(Props: any): VNode {
   return (
     <div id="reserve-form" class="pure-form" name="tform">
       <p>
-        <label for="withdraw-amount">{i18n`Amount to withdraw:`}</label>&nbsp;
+        <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
+        &nbsp;
         <input
           type="number"
           ref={ref}
@@ -1724,7 +1694,7 @@ function WalletWithdraw(Props: any): VNode {
             id="select-exchange"
             class="pure-button pure-button-primary"
             type="submit"
-            value={i18n`Withdraw`}
+            value={i18n.str`Withdraw`}
             onClick={() => {
               submitAmount = validateAmount(submitAmount);
               /**
@@ -1753,7 +1723,7 @@ function WalletWithdraw(Props: any): VNode {
 function PaymentOptions(Props: any): VNode {
   const { backendState, pageStateSetter, focus } = Props;
   const currency = useContext(CurrencyContext);
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
     "charge-wallet",
@@ -1769,7 +1739,7 @@ function PaymentOptions(Props: any): VNode {
               setTab("charge-wallet");
             }}
           >
-            {i18n`Obtain digital cash`}
+            {i18n.str`Obtain digital cash`}
           </button>
           <button
             class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
@@ -1777,12 +1747,12 @@ function PaymentOptions(Props: any): VNode {
               setTab("wire-transfer");
             }}
           >
-            {i18n`Transfer to bank account`}
+            {i18n.str`Transfer to bank account`}
           </button>
         </div>
         {tab === "charge-wallet" && (
           <div id="charge-wallet" class="tabcontent active">
-            <h3>{i18n`Obtain digital cash`}</h3>
+            <h3>{i18n.str`Obtain digital cash`}</h3>
             <WalletWithdraw
               backendState={backendState}
               focus
@@ -1792,7 +1762,7 @@ function PaymentOptions(Props: any): VNode {
         )}
         {tab === "wire-transfer" && (
           <div id="wire-transfer" class="tabcontent active">
-            <h3>{i18n`Transfer to bank account`}</h3>
+            <h3>{i18n.str`Transfer to bank account`}</h3>
             <PaytoWireTransfer
               backendState={backendState}
               focus
@@ -1807,7 +1777,7 @@ function PaymentOptions(Props: any): VNode {
 
 function RegistrationButton(Props: any): VNode {
   const { backendStateSetter, pageStateSetter } = Props;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   if (bankUiSettings.allowRegistrations)
     return (
       <button
@@ -1816,7 +1786,7 @@ function RegistrationButton(Props: any): VNode {
           route("/register");
         }}
       >
-        {i18n`Register`}
+        {i18n.str`Register`}
       </button>
     );
 
@@ -1834,7 +1804,7 @@ function undefinedIfEmpty<T extends object>(obj: T): T | 
undefined {
 function LoginForm(Props: any): VNode {
   const { backendStateSetter, pageStateSetter } = Props;
   const [submitData, submitDataSetter] = useCredentialsRequestType();
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   const ref = useRef<HTMLInputElement>(null);
   useEffect(() => {
     ref.current?.focus();
@@ -1843,17 +1813,17 @@ function LoginForm(Props: any): VNode {
   const errors = !submitData
     ? undefined
     : undefinedIfEmpty({
-        username: !submitData.username ? i18n`Missing username` : undefined,
-        password: !submitData.password ? i18n`Missing password` : undefined,
+        username: !submitData.username ? i18n.str`Missing username` : 
undefined,
+        password: !submitData.password ? i18n.str`Missing password` : 
undefined,
       });
 
   return (
     <div class="login-div">
       <form action="javascript:void(0);" class="login-form">
         <div class="pure-form">
-          <h2>{i18n`Please login!`}</h2>
+          <h2>{i18n.str`Please login!`}</h2>
           <p class="unameFieldLabel loginFieldLabel formFieldLabel">
-            <label for="username">{i18n`Username:`}</label>
+            <label for="username">{i18n.str`Username:`}</label>
           </p>
           <input
             ref={ref}
@@ -1872,7 +1842,7 @@ function LoginForm(Props: any): VNode {
             }}
           />
           <p class="passFieldLabel loginFieldLabel formFieldLabel">
-            <label for="password">{i18n`Password:`}</label>
+            <label for="password">{i18n.str`Password:`}</label>
           </p>
           <input
             type="password"
@@ -1919,7 +1889,7 @@ function LoginForm(Props: any): VNode {
               });
             }}
           >
-            {i18n`Login`}
+            {i18n.str`Login`}
           </button>
           {RegistrationButton(Props)}
         </div>
@@ -1935,30 +1905,30 @@ function RegistrationForm(Props: any): VNode {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [pageState, pageStateSetter] = useContext(PageContext);
   const [submitData, submitDataSetter] = useCredentialsRequestType();
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const errors = !submitData
     ? undefined
     : undefinedIfEmpty({
-        username: !submitData.username ? i18n`Missing username` : undefined,
-        password: !submitData.password ? i18n`Missing password` : undefined,
+        username: !submitData.username ? i18n.str`Missing username` : 
undefined,
+        password: !submitData.password ? i18n.str`Missing password` : 
undefined,
         repeatPassword: !submitData.repeatPassword
-          ? i18n`Missing password`
+          ? i18n.str`Missing password`
           : submitData.repeatPassword !== submitData.password
-          ? i18n`Password don't match`
+          ? i18n.str`Password don't match`
           : undefined,
       });
 
   return (
     <Fragment>
-      <h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1>
+      <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
       <article>
         <div class="register-div">
           <form action="javascript:void(0);" class="register-form">
             <div class="pure-form">
-              <h2>{i18n`Please register!`}</h2>
+              <h2>{i18n.str`Please register!`}</h2>
               <p class="unameFieldLabel registerFieldLabel formFieldLabel">
-                <label for="register-un">{i18n`Username:`}</label>
+                <label for="register-un">{i18n.str`Username:`}</label>
               </p>
               <input
                 id="register-un"
@@ -1976,7 +1946,7 @@ function RegistrationForm(Props: any): VNode {
               />
               <br />
               <p class="unameFieldLabel registerFieldLabel formFieldLabel">
-                <label for="register-pw">{i18n`Password:`}</label>
+                <label for="register-pw">{i18n.str`Password:`}</label>
               </p>
               <input
                 type="password"
@@ -1993,7 +1963,7 @@ function RegistrationForm(Props: any): VNode {
                 }}
               />
               <p class="unameFieldLabel registerFieldLabel formFieldLabel">
-                <label for="register-repeat">{i18n`Repeat Password:`}</label>
+                <label for="register-repeat">{i18n.str`Repeat 
Password:`}</label>
               </p>
               <input
                 type="password"
@@ -2012,7 +1982,7 @@ function RegistrationForm(Props: any): VNode {
               />
               <br />
               {/*
-              <label for="phone">{i18n`Phone number:`}</label>
+              <label for="phone">{i18n.str`Phone number:`}</label>
               // FIXME: add input validation (must start with +, otherwise 
only numbers)
               <input
                 name="phone"
@@ -2054,7 +2024,7 @@ function RegistrationForm(Props: any): VNode {
                   });
                 }}
               >
-                {i18n`Register`}
+                {i18n.str`Register`}
               </button>
               {/* FIXME: should use a different color */}
               <button
@@ -2068,7 +2038,7 @@ function RegistrationForm(Props: any): VNode {
                   route("/account");
                 }}
               >
-                {i18n`Cancel`}
+                {i18n.str`Cancel`}
               </button>
             </div>
           </form>
@@ -2083,7 +2053,7 @@ function RegistrationForm(Props: any): VNode {
  */
 function Transactions(Props: any): VNode {
   const { pageNumber, accountLabel, balanceValue } = Props;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   const { data, error, mutate } = useSWR(
     `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
   );
@@ -2114,10 +2084,10 @@ function Transactions(Props: any): VNode {
       <table class="pure-table pure-table-striped">
         <thead>
           <tr>
-            <th>{i18n`Date`}</th>
-            <th>{i18n`Amount`}</th>
-            <th>{i18n`Counterpart`}</th>
-            <th>{i18n`Subject`}</th>
+            <th>{i18n.str`Date`}</th>
+            <th>{i18n.str`Amount`}</th>
+            <th>{i18n.str`Counterpart`}</th>
+            <th>{i18n.str`Subject`}</th>
           </tr>
         </thead>
         <tbody>
@@ -2178,7 +2148,7 @@ function Account(Props: any): VNode {
     talerWithdrawUri,
     timestamp,
   } = pageState;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   useEffect(() => {
     mutate();
   }, [timestamp]);
@@ -2206,7 +2176,7 @@ function Account(Props: any): VNode {
 
           isLoggedIn: false,
           error: {
-            title: i18n`Username or account label '${accountLabel}' not found. 
 Won't login.`,
+            title: i18n.str`Username or account label '${accountLabel}' not 
found.  Won't login.`,
           },
         }));
 
@@ -2233,7 +2203,7 @@ function Account(Props: any): VNode {
 
           isLoggedIn: false,
           error: {
-            title: i18n`Wrong credentials given.`,
+            title: i18n.str`Wrong credentials given.`,
           },
         }));
         return <p>Wrong credentials...</p>;
@@ -2244,7 +2214,7 @@ function Account(Props: any): VNode {
 
           isLoggedIn: false,
           error: {
-            title: i18n`Account information could not be retrieved.`,
+            title: i18n.str`Account information could not be retrieved.`,
             debug: JSON.stringify(error),
           },
         }));
@@ -2287,14 +2257,14 @@ function Account(Props: any): VNode {
     <BankFrame>
       <div>
         <h1 class="nav welcome-text">
-          <Translate>
+          <i18n.Translate>
             Welcome, {accountLabel} ({getIbanFromPayto(data.paytoUri)})!
-          </Translate>
+          </i18n.Translate>
         </h1>
       </div>
       <section id="assets">
         <div class="asset-summary">
-          <h2>{i18n`Bank account balance`}</h2>
+          <h2>{i18n.str`Bank account balance`}</h2>
           <div class="large-amount amount">
             {data.balance.credit_debit_indicator == "debit" ? <b>-</b> : null}
             <span class="value">{`${balanceValue}`}</span>&nbsp;
@@ -2304,7 +2274,7 @@ function Account(Props: any): VNode {
       </section>
       <section id="payments">
         <div class="payments">
-          <h2>{i18n`Payments`}</h2>
+          <h2>{i18n.str`Payments`}</h2>
           {/* FIXME: turn into button! */}
           <CurrencyContext.Provider value={balance.currency}>
             {Props.children}
@@ -2317,7 +2287,7 @@ function Account(Props: any): VNode {
       </section>
       <section id="main">
         <article>
-          <h2>{i18n`Latest transactions:`}</h2>
+          <h2>{i18n.str`Latest transactions:`}</h2>
           <Transactions
             balanceValue={balanceValue}
             pageNumber="0"
@@ -2379,7 +2349,7 @@ function SWRWithoutCredentials(Props: any): VNode {
 function PublicHistories(Props: any): VNode {
   const [showAccount, setShowAccount] = useShowPublicAccount();
   const { data, error } = useSWR("access-api/public-accounts");
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   if (typeof error !== "undefined") {
     console.log("account error", error);
@@ -2391,7 +2361,7 @@ function PublicHistories(Props: any): VNode {
 
           showPublicHistories: false,
           error: {
-            title: i18n`List of public accounts was not found.`,
+            title: i18n.str`List of public accounts was not found.`,
             debug: JSON.stringify(error),
           },
         }));
@@ -2403,7 +2373,7 @@ function PublicHistories(Props: any): VNode {
 
           showPublicHistories: false,
           error: {
-            title: i18n`List of public accounts could not be retrieved.`,
+            title: i18n.str`List of public accounts could not be retrieved.`,
             debug: JSON.stringify(error),
           },
         }));
@@ -2450,7 +2420,7 @@ function PublicHistories(Props: any): VNode {
 
   return (
     <Fragment>
-      <h1 class="nav">{i18n`History of public accounts`}</h1>
+      <h1 class="nav">{i18n.str`History of public accounts`}</h1>
       <section id="main">
         <article>
           <div class="pure-menu pure-menu-horizontal" name="accountMenu">
@@ -2471,7 +2441,7 @@ function PublicHistories(Props: any): VNode {
 function PublicHistoriesPage(): VNode {
   // const [backendState, backendStateSetter] = useBackendState();
   const [pageState, pageStateSetter] = usePageState();
-  // const i18n = useTranslator();
+  // const { i18n } = useTranslationContext();
   return (
     <SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
       <PageContext.Provider value={[pageState, pageStateSetter]}>
@@ -2499,12 +2469,12 @@ function PublicHistoriesPage(): VNode {
 function RegistrationPage(): VNode {
   const [backendState, backendStateSetter] = useBackendState();
   const [pageState, pageStateSetter] = usePageState();
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
   if (!bankUiSettings.allowRegistrations) {
     return (
       <PageContext.Provider value={[pageState, pageStateSetter]}>
         <BankFrame>
-          <p>{i18n`Currently, the bank is not accepting new 
registrations!`}</p>
+          <p>{i18n.str`Currently, the bank is not accepting new 
registrations!`}</p>
         </BankFrame>
       </PageContext.Provider>
     );
@@ -2521,13 +2491,13 @@ function RegistrationPage(): VNode {
 function AccountPage(): VNode {
   const [backendState, backendStateSetter] = useBackendState();
   const [pageState, pageStateSetter] = usePageState();
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   if (!pageState.isLoggedIn) {
     return (
       <PageContext.Provider value={[pageState, pageStateSetter]}>
         <BankFrame>
-          <h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1>
+          <h1 class="nav">{i18n.str`Welcome to 
${bankUiSettings.bankName}!`}</h1>
           <LoginForm
             pageStateSetter={pageStateSetter}
             backendStateSetter={backendStateSetter}
@@ -2543,7 +2513,7 @@ function AccountPage(): VNode {
 
       isLoggedIn: false,
       error: {
-        title: i18n`Page has a problem: logged in but backend state is lost.`,
+        title: i18n.str`Page has a problem: logged in but backend state is 
lost.`,
       },
     }));
     return <p>Error: waiting for details...</p>;
diff --git a/packages/demobank-ui/src/scss/main.scss 
b/packages/demobank-ui/src/scss/main.scss
index ebe36b9b4..b92260af0 100644
--- a/packages/demobank-ui/src/scss/main.scss
+++ b/packages/demobank-ui/src/scss/main.scss
@@ -1,4 +1,4 @@
-@import "pure";
-@import "bank";
-@import "demo";
-@import "colors-bank";
+@use "pure";
+@use "bank";
+@use "demo";
+@use "colors-bank";
diff --git a/packages/anastasis-webui/src/scss/_misc.scss 
b/packages/demobank-ui/src/stories.tsx
similarity index 61%
copy from packages/anastasis-webui/src/scss/_misc.scss
copy to packages/demobank-ui/src/stories.tsx
index d2aa1e4df..52d42577d 100644
--- a/packages/anastasis-webui/src/scss/_misc.scss
+++ b/packages/demobank-ui/src/stories.tsx
@@ -18,33 +18,29 @@
  *
  * @author Sebastian Javier Marchano (sebasjm)
  */
+import { strings } from "./i18n/strings.js";
 
-.is-user-avatar {
-  &.has-max-width {
-    max-width: $size-base * 7;
-  }
+import * as pages from "./pages/home/index.stories.js";
 
-  &.is-aligned-center {
-    margin: 0 auto;
-  }
+import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
 
-  img {
-    margin: 0 auto;
-    border-radius: $radius-rounded;
-  }
+import "./scss/main.scss";
+
+function SortStories(a: any, b: any): number {
+  return (a?.order ?? 0) - (b?.order ?? 0);
+}
+
+function main(): void {
+  renderStories(
+    { pages },
+    {
+      strings,
+    },
+  );
 }
 
-.icon.has-update-mark {
-  position: relative;
-
-  &:after {
-    content: "";
-    width: $icon-update-mark-size;
-    height: $icon-update-mark-size;
-    position: absolute;
-    top: 1px;
-    right: 1px;
-    background-color: $icon-update-mark-color;
-    border-radius: $radius-rounded;
-  }
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoaded", main);
+} else {
+  main();
 }
diff --git a/packages/demobank-ui/static/index.html 
b/packages/demobank-ui/static/index.html
deleted file mode 100644
index 0fa5215d3..000000000
--- a/packages/demobank-ui/static/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-    <title>Demobank</title>
-    <!-- Optional customization script.  -->
-    <script src="demobank-ui-settings.js"></script>
-    <!-- Entry point for the demobank SPA. -->
-    <script type="module" src="index.js"></script>
-    <link rel="stylesheet" href="index.css" />
-  </head>
-  <body>
-    <div id="app"></div>
-  </body>
-</html>
diff --git a/packages/demobank-ui/build.mjs 
b/packages/merchant-backoffice-ui/build.mjs
similarity index 76%
copy from packages/demobank-ui/build.mjs
copy to packages/merchant-backoffice-ui/build.mjs
index 63ddc1f25..c93b4eb67 100755
--- a/packages/demobank-ui/build.mjs
+++ b/packages/merchant-backoffice-ui/build.mjs
@@ -18,9 +18,9 @@
 import esbuild from "esbuild";
 import path from "path";
 import fs from "fs";
-import crypto from "crypto";
-import { sassPlugin } from "esbuild-sass-plugin";
+import sass from "sass";
 
+// eslint-disable-next-line no-undef
 const BASE = process.cwd();
 
 const preact = path.join(
@@ -44,14 +44,16 @@ const preactCompatPlugin = {
   },
 };
 
-const entryPoints = ["src/index.tsx"];
+const entryPoints = ["src/index.tsx", "src/stories.tsx"];
 
 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_ROOT === "/" ? undefined : git_hash();
@@ -86,6 +88,26 @@ function copyFilesPlugin(options) {
   };
 }
 
+const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
+
+const buildSassPlugin = {
+  name: "custom-build-sass",
+  setup(build) {
+
+    build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
+      const resolveDir = path.dirname(file)
+      const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
+
+      return {
+        resolveDir,
+        loader: 'css',
+        contents
+      }
+    });
+
+  },
+};
+
 export const buildConfig = {
   entryPoints: [...entryPoints],
   bundle: true,
@@ -95,6 +117,10 @@ export const buildConfig = {
     ".svg": "file",
     ".png": "dataurl",
     ".jpeg": "dataurl",
+    '.ttf': 'file',
+    '.woff': 'file',
+    '.woff2': 'file',
+    '.eot': 'file',
   },
   target: ["es6"],
   format: "esm",
@@ -108,17 +134,14 @@ export const buildConfig = {
   },
   plugins: [
     preactCompatPlugin,
-    sassPlugin(),
     copyFilesPlugin([
       {
-        src: "static/index.html",
-        dest: "dist/index.html",
+        src: "./src/index.html",
+        dest: "./dist/index.html",
       },
     ]),
+    buildSassPlugin
   ],
 };
 
-esbuild.build(buildConfig).catch((e) => {
-  console.log(e);
-  process.exit(1);
-});
+await esbuild.build(buildConfig)
diff --git a/packages/demobank-ui/src/pages/notfound/index.tsx 
b/packages/merchant-backoffice-ui/dev.mjs
old mode 100644
new mode 100755
similarity index 65%
copy from packages/demobank-ui/src/pages/notfound/index.tsx
copy to packages/merchant-backoffice-ui/dev.mjs
index 474451cc6..35a9fa16c
--- a/packages/demobank-ui/src/pages/notfound/index.tsx
+++ b/packages/merchant-backoffice-ui/dev.mjs
@@ -1,3 +1,4 @@
+#!/usr/bin/env node
 /*
  This file is part of GNU Taler
  (C) 2022 Taler Systems S.A.
@@ -14,19 +15,16 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { FunctionalComponent, h } from "preact";
-import { Link } from "preact-router/match";
+import { serve } from "@gnu-taler/web-util/lib/index.node";
+import esbuild from "esbuild";
+import { buildConfig } from "./build.mjs";
 
-const Notfound: FunctionalComponent = () => {
-  return (
-    <div>
-      <h1>Error 404</h1>
-      <p>That page doesn&apos;t exist.</p>
-      <Link href="/">
-        <h4>Back to Home</h4>
-      </Link>
-    </div>
-  );
-};
+buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
 
-export default Notfound;
+serve({
+  folder: './dist',
+  port: 8080,
+  source: './src',
+  development: true,
+  onUpdate: async () => esbuild.build(buildConfig)
+})
diff --git a/packages/merchant-backoffice-ui/package.json 
b/packages/merchant-backoffice-ui/package.json
index f7e0972ec..91c4c1857 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -44,6 +44,7 @@
   },
   "dependencies": {
     "@gnu-taler/taler-util": "workspace:*",
+    "@gnu-taler/web-util": "workspace:*",
     "axios": "^0.21.1",
     "date-fns": "2.29.3",
     "history": "4.10.1",
@@ -122,4 +123,4 @@
       
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$":
 "<rootDir>/tests/__mocks__/fileTransformer.js"
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/paths/index.stories.ts 
b/packages/merchant-backoffice-ui/src/paths/index.stories.ts
new file mode 100644
index 000000000..b3811fd4f
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/index.stories.ts
@@ -0,0 +1,2 @@
+export * as a1 from "./admin/create/Create.stories.js";
+export * as a2 from "./instance/details/Details.stories.js";
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/scss/_misc.scss 
b/packages/merchant-backoffice-ui/src/stories.tsx
similarity index 61%
copy from packages/anastasis-webui/src/scss/_misc.scss
copy to packages/merchant-backoffice-ui/src/stories.tsx
index d2aa1e4df..b7136d185 100644
--- a/packages/anastasis-webui/src/scss/_misc.scss
+++ b/packages/merchant-backoffice-ui/src/stories.tsx
@@ -18,33 +18,29 @@
  *
  * @author Sebastian Javier Marchano (sebasjm)
  */
+import { strings } from "./i18n/strings.js";
 
-.is-user-avatar {
-  &.has-max-width {
-    max-width: $size-base * 7;
-  }
+import * as pages from "./paths/index.stories.js";
 
-  &.is-aligned-center {
-    margin: 0 auto;
-  }
+import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
 
-  img {
-    margin: 0 auto;
-    border-radius: $radius-rounded;
-  }
+import "./scss/main.scss";
+
+function SortStories(a: any, b: any): number {
+  return (a?.order ?? 0) - (b?.order ?? 0);
+}
+
+function main(): void {
+  renderStories(
+    { pages },
+    {
+      strings,
+    },
+  );
 }
 
-.icon.has-update-mark {
-  position: relative;
-
-  &:after {
-    content: "";
-    width: $icon-update-mark-size;
-    height: $icon-update-mark-size;
-    position: absolute;
-    top: 1px;
-    right: 1px;
-    background-color: $icon-update-mark-color;
-    border-radius: $radius-rounded;
-  }
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoaded", main);
+} else {
+  main();
 }
diff --git a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs 
b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
index 25e5e78db..1232eac98 100755
--- a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
+++ b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
@@ -40,6 +40,7 @@ function getFilesInDirectory(startPath, regex) {
   return result
 }
 
+// eslint-disable-next-line no-undef
 const BASE = process.cwd()
 const allTestFiles = getFilesInDirectory(path.join(BASE, 'src'), /.test.ts$/)
 
@@ -67,7 +68,9 @@ 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_ROOT === '/' ? undefined : git_hash()
@@ -118,10 +121,12 @@ export const buildConfig = {
   ],
 }
 
-esbuild
+await esbuild
   .build(buildConfig)
   .catch((e) => {
+    // eslint-disable-next-line no-undef
     console.log(e)
+    // eslint-disable-next-line no-undef
     process.exit(1)
   });
 
diff --git a/packages/taler-wallet-webextension/dev.mjs 
b/packages/taler-wallet-webextension/dev.mjs
index 842424a9d..fb5661aa5 100755
--- a/packages/taler-wallet-webextension/dev.mjs
+++ b/packages/taler-wallet-webextension/dev.mjs
@@ -1,67 +1,38 @@
 #!/usr/bin/env node
-/* eslint-disable no-undef */
-
-import linaria from '@linaria/esbuild'
-import esbuild from 'esbuild'
-import { buildConfig } from "./build-fast-with-linaria.mjs"
-import fs from 'fs';
-import WebSocket from "ws";
-import chokidar from "chokidar";
-import path from "path"
-
-const devServerBroadcastDelay = 500
-const devServerPort = 8002
-const wss = new WebSocket.Server({ port: devServerPort });
-const toWatch = ["./src"]
-
-function broadcast(file, event) {
-  setTimeout(() => {
-    wss.clients.forEach((client) => {
-      if (client.readyState === WebSocket.OPEN) {
-        console.log(new Date(), file)
-        client.send(JSON.stringify(event));
-      }
-    });
-  }, devServerBroadcastDelay);
-}
-
-const watcher = chokidar
-  .watch(toWatch, {
-    persistent: true,
-    ignoreInitial: true,
-    awaitWriteFinish: {
-      stabilityThreshold: 100,
-      pollInterval: 100,
-    },
-  })
-  .on("error", (error) => console.error(error))
-  .on("change", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  })
-  .on("add", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  })
-  .on("unlink", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  });
-
-
-fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json"))
-fs.writeFileSync("dev-html/mocha.css", 
fs.readFileSync("node_modules/mocha/mocha.css"))
-fs.writeFileSync("dev-html/mocha.js", 
fs.readFileSync("node_modules/mocha/mocha.js"))
-fs.writeFileSync("dev-html/mocha.js.map", 
fs.readFileSync("node_modules/mocha/mocha.js.map"))
-
-const server = await esbuild
-  .serve({ servedir: 'dev-html' }, {
-    ...buildConfig, outdir: 'dev-html/dist'
-  })
-  .catch((e) => {
-    console.log(e)
-    process.exit(1)
-  });
-
-console.log(`Dev server is ready at http://localhost:${server.port}/.
-http://localhost:${server.port}/stories.html for the components stories.
-The server is running a using websocket at ${devServerPort} to notify code 
change and live reload.
-`);
+/*
+ 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/>
+ */
+
+import { serve } from "@gnu-taler/web-util/lib/index.node";
+import esbuild from 'esbuild';
+import { buildConfig } from "./build-fast-with-linaria.mjs";
+
+buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
+
+serve({
+  folder: './dist',
+  port: 8080,
+  source: './src',
+  development: true,
+  onUpdate: async () => esbuild.build(buildConfig)
+})
+
+// FIXME: create a mocha test in the browser as it was before
+
+// fs.writeFileSync("dev-html/manifest.json", 
fs.readFileSync("manifest-v2.json"))
+// fs.writeFileSync("dev-html/mocha.css", 
fs.readFileSync("node_modules/mocha/mocha.css"))
+// fs.writeFileSync("dev-html/mocha.js", 
fs.readFileSync("node_modules/mocha/mocha.js"))
+// fs.writeFileSync("dev-html/mocha.js.map", 
fs.readFileSync("node_modules/mocha/mocha.js.map"))
 
diff --git a/packages/taler-wallet-webextension/package.json 
b/packages/taler-wallet-webextension/package.json
index cdd0e7372..0deca26cc 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -25,12 +25,11 @@
     "@gnu-taler/taler-wallet-core": "workspace:*",
     "date-fns": "^2.29.2",
     "history": "4.10.1",
-    "preact": "^10.6.5",
+    "preact": "10.11.3",
     "preact-router": "3.2.1",
     "qr-scanner": "^1.4.1",
     "qrcode-generator": "^1.4.4",
-    "tslib": "^2.4.0",
-    "ws": "7.4.5"
+    "tslib": "^2.4.0"
   },
   "eslintConfig": {
     "plugins": [
@@ -44,6 +43,7 @@
     }
   },
   "devDependencies": {
+    "@gnu-taler/web-util": "workspace:*",
     "@babel/core": "7.18.9",
     "@babel/plugin-transform-modules-commonjs": "7.18.6",
     "@babel/plugin-transform-react-jsx-source": "7.18.6",
@@ -54,8 +54,6 @@
     "@linaria/core": "3.0.0-beta.22",
     "@linaria/react": "3.0.0-beta.22",
     "@linaria/webpack-loader": "3.0.0-beta.22",
-    "@testing-library/preact": "^2.0.1",
-    "@testing-library/preact-hooks": "^1.1.0",
     "@types/chai": "^4.3.0",
     "@types/chrome": "0.0.197",
     "@types/history": "^4.7.8",
@@ -64,7 +62,6 @@
     "babel-loader": "^8.2.3",
     "babel-plugin-transform-react-jsx": "^6.24.1",
     "chai": "^4.3.6",
-    "chokidar": "^3.5.3",
     "esbuild": "^0.15.13",
     "mocha": "^9.2.0",
     "nyc": "^15.1.0",
@@ -83,4 +80,4 @@
   "pogen": {
     "domain": "taler-wallet-webex"
   }
-}
+}
\ No newline at end of file
diff --git 
a/packages/taler-wallet-webextension/src/components/Amount.stories.tsx 
b/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
index caf35d57e..095c9be24 100644
--- a/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
@@ -24,7 +24,7 @@ import { Fragment, h, VNode } from "preact";
 import { Amount } from "./Amount.js";
 
 export default {
-  title: "components/amount",
+  title: "amount",
   component: Amount,
 };
 
diff --git 
a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx 
b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
index ff9a71992..9a1d96014 100644
--- a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
@@ -29,7 +29,7 @@ import { AmountFieldHandler, TextFieldHandler } from 
"../mui/handlers.js";
 import { AmountField } from "./AmountField.js";
 
 export default {
-  title: "components/amountField",
+  title: "amountField",
 };
 
 function RenderAmount(): VNode {
diff --git 
a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx 
b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
index 354815313..39012480b 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
@@ -26,7 +26,7 @@ import { Banner } from "./Banner.js";
 import { SvgIcon } from "./styled/index.js";
 import wifiIcon from "../svg/wifi.svg";
 export default {
-  title: "mui/banner",
+  title: "banner",
   component: Banner,
 };
 
diff --git 
a/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
 
b/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
index 12525e2f8..2155c7aa6 100644
--- 
a/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
@@ -28,7 +28,7 @@ import { createExample } from "../test-utils.js";
 import { PendingTransactionsView as TestedComponent } from 
"./PendingTransactions.js";
 
 export default {
-  title: "component/PendingTransactions",
+  title: "PendingTransactions",
   component: TestedComponent,
 };
 
diff --git a/packages/taler-wallet-webextension/src/components/QR.stories.tsx 
b/packages/taler-wallet-webextension/src/components/QR.stories.tsx
index 890cd1941..83365670e 100644
--- a/packages/taler-wallet-webextension/src/components/QR.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/QR.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { QR } from "./QR.js";
 
 export default {
-  title: "wallet/qr",
+  title: "qr",
 };
 
 export const Restore = createExample(QR, {
diff --git 
a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
 
b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
index 1396d8707..841583113 100644
--- 
a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -29,7 +29,7 @@ import {
 } from "./ShowFullContractTermPopup.js";
 
 export default {
-  title: "component/ShowFullContractTermPopup",
+  title: "ShowFullContractTermPopup",
 };
 
 const cd: WalletContractData = {
diff --git 
a/packages/taler-wallet-webextension/src/components/index.stories.tsx 
b/packages/taler-wallet-webextension/src/components/index.stories.tsx
index 2e4e7fa2e..469ed82fa 100644
--- a/packages/taler-wallet-webextension/src/components/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/index.stories.tsx
@@ -19,12 +19,10 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import * as a1 from "./Banner.stories.js";
-import * as a2 from "./PendingTransactions.stories.js";
-import * as a3 from "./Amount.stories.js";
-import * as a4 from "./ShowFullContractTermPopup.stories.js";
-import * as a5 from "./TermsOfService/stories.js";
-import * as a6 from "./QR.stories";
-import * as a7 from "./AmountField.stories.js";
-
-export default [a1, a2, a3, a4, a5, a6, a7];
+export * as a1 from "./Banner.stories.js";
+export * as a2 from "./PendingTransactions.stories.js";
+export * as a3 from "./Amount.stories.js";
+export * as a4 from "./ShowFullContractTermPopup.stories.js";
+export * as a5 from "./TermsOfService/stories.js";
+export * as a6 from "./QR.stories";
+export * as a7 from "./AmountField.stories.js";
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
index a4168bcc2..6d1535953 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
@@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "cta/deposit",
+  title: "deposit",
 };
 
 export const Ready = createExample(ReadyView, {
diff --git 
a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
index 8d4473d8f..05b923c9e 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/invoice create",
+  title: "invoice create",
 };
 
 export const Ready = createExample(ReadyView, {
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
index 38eb2336a..749cd78fc 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
@@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/invoice payment",
+  title: "invoice payment",
 };
 
 export const Ready = createExample(ReadyView, {
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index fd437d5d2..28fcd8db7 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -30,7 +30,7 @@ import { BaseView } from "./views.js";
 import beer from "../../../static-dev/beer.png";
 
 export default {
-  title: "cta/payment",
+  title: "payment",
   component: BaseView,
   argTypes: {},
 };
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
index e1da860fb..9243cc015 100644
--- a/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
@@ -24,5 +24,5 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "cta/recovery",
+  title: "recovery",
 };
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
index f95bfc693..921cf77e6 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
@@ -24,7 +24,7 @@ import beer from "../../../static-dev/beer.png";
 import { createExample } from "../../test-utils.js";
 import { IgnoredView, InProgressView, ReadyView } from "./views.js";
 export default {
-  title: "cta/refund",
+  title: "refund",
 };
 
 export const InProgress = createExample(InProgressView, {
diff --git a/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx
index d200029e7..86bdd27a9 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx
@@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
 import { AcceptedView, ReadyView } from "./views.js";
 
 export default {
-  title: "cta/tip",
+  title: "tip",
 };
 
 export const Accepted = createExample(AcceptedView, {
diff --git 
a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
index de781f008..d0650f562 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/transfer create",
+  title: "transfer create",
 };
 
 export const Ready = createExample(ReadyView, {
diff --git 
a/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
index 23cfe7525..250e99ae1 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/transfer pickup",
+  title: "transfer pickup",
 };
 
 export const Ready = createExample(ReadyView, {
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index 1c3eaaf34..a8031223b 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -25,7 +25,7 @@ import { createExample } from "../../test-utils.js";
 import { SuccessView } from "./views.js";
 
 export default {
-  title: "cta/withdraw",
+  title: "withdraw",
 };
 
 const exchangeList = {
diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts 
b/packages/taler-wallet-webextension/src/cta/index.stories.ts
index d920ff854..84863f84f 100644
--- a/packages/taler-wallet-webextension/src/cta/index.stories.ts
+++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts
@@ -19,14 +19,12 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import * as a1 from "./Deposit/stories.jsx";
-import * as a3 from "./Payment/stories.jsx";
-import * as a4 from "./Refund/stories.jsx";
-import * as a5 from "./Tip/stories.jsx";
-import * as a6 from "./Withdraw/stories.jsx";
-import * as a8 from "./InvoiceCreate/stories.js";
-import * as a9 from "./InvoicePay/stories.js";
-import * as a10 from "./TransferCreate/stories.js";
-import * as a11 from "./TransferPickup/stories.js";
-
-export default [a1, a3, a4, a5, a6, a8, a9, a10, a11];
+export * as a1 from "./Deposit/stories.jsx";
+export * as a3 from "./Payment/stories.jsx";
+export * as a4 from "./Refund/stories.jsx";
+export * as a5 from "./Tip/stories.jsx";
+export * as a6 from "./Withdraw/stories.jsx";
+export * as a8 from "./InvoiceCreate/stories.js";
+export * as a9 from "./InvoicePay/stories.js";
+export * as a10 from "./TransferCreate/stories.js";
+export * as a11 from "./TransferPickup/stories.js";
diff --git a/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
index 66af50c11..62f7a2993 100644
--- a/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
@@ -24,7 +24,7 @@ import { ComponentChildren, Fragment, h, VNode } from 
"preact";
 import { Alert } from "./Alert.jsx";
 
 export default {
-  title: "mui/alert",
+  title: "alert",
   component: Alert,
 };
 
diff --git a/packages/taler-wallet-webextension/src/mui/Button.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
index 385eb1028..65af81849 100644
--- a/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
@@ -26,8 +26,7 @@ import SendIcon from "../svg/send_24px.svg";
 import { styled } from "@linaria/react";
 
 export default {
-  title: "mui/button",
-  component: Button,
+  title: "Button",
 };
 
 const Stack = styled.div`
diff --git a/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx
index 3510fd737..d399cb825 100644
--- a/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx
@@ -23,7 +23,7 @@ import { Grid } from "./Grid.js";
 import { Fragment, h, VNode } from "preact";
 
 export default {
-  title: "mui/grid",
+  title: "grid",
   component: Grid,
 };
 
diff --git a/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
index 60e373be4..e2bba2678 100644
--- a/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
@@ -25,7 +25,7 @@ import { Menu, MenuItem } from "./Menu.jsx";
 import { Paper } from "./Paper.js";
 
 export default {
-  title: "mui/menu",
+  title: "menu",
   component: Menu,
 };
 
diff --git a/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
index f27a5606d..b0e06d137 100644
--- a/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
@@ -23,7 +23,7 @@ import { h, VNode } from "preact";
 import { Paper } from "./Paper.js";
 
 export default {
-  title: "mui/paper",
+  title: "paper",
   component: Paper,
 };
 
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
index 7db6b2964..1c41c2141 100644
--- a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
@@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
 import { TextField, Props } from "./TextField.js";
 
 export default {
-  title: "mui/TextField",
+  title: "TextField",
   component: TextField,
 };
 
diff --git a/packages/taler-wallet-webextension/src/mui/index.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/index.stories.tsx
index b70b56698..aa8dd2526 100644
--- a/packages/taler-wallet-webextension/src/mui/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/index.stories.tsx
@@ -19,11 +19,9 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import * as a1 from "./Button.stories.js";
-import * as a3 from "./Grid.stories.js";
-import * as a4 from "./Paper.stories.js";
-import * as a5 from "./TextField.stories.js";
-import * as a6 from "./Alert.stories.js";
-import * as a7 from "./Menu.stories.js";
-
-export default [a1, a3, a4, a5, a6, a7];
+export * as a1 from "./Button.stories.js";
+export * as a3 from "./Grid.stories.js";
+export * as a4 from "./Paper.stories.js";
+export * as a5 from "./TextField.stories.js";
+export * as a6 from "./Alert.stories.js";
+export * as a7 from "./Menu.stories.js";
diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx 
b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
index 87cc98ea0..8f3762c29 100644
--- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
@@ -23,9 +23,7 @@ import { createExample } from "../test-utils.js";
 import { BalanceView as TestedComponent } from "./BalancePage.js";
 
 export default {
-  title: "popup/balance",
-  component: TestedComponent,
-  argTypes: {},
+  title: "balance",
 };
 
 export const EmptyBalance = createExample(TestedComponent, {
diff --git 
a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx 
b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
index f5020b599..00293a690 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
@@ -23,8 +23,7 @@ import { createExample } from "../test-utils.js";
 import { TalerActionFound as TestedComponent } from "./TalerActionFound.js";
 
 export default {
-  title: "popup/TalerActionFound",
-  component: TestedComponent,
+  title: "TalerActionFound",
 };
 
 export const PayAction = createExample(TestedComponent, {
diff --git a/packages/taler-wallet-webextension/src/popup/index.stories.tsx 
b/packages/taler-wallet-webextension/src/popup/index.stories.tsx
index 540ae5a6a..ea7cee77d 100644
--- a/packages/taler-wallet-webextension/src/popup/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/index.stories.tsx
@@ -19,7 +19,5 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import * as a2 from "./Balance.stories.js";
-import * as a6 from "./TalerActionFound.stories.js";
-
-export default [a2, a6];
+export * as a1 from "./Balance.stories.js";
+export * as a2 from "./TalerActionFound.stories.js";
diff --git a/packages/taler-wallet-webextension/src/stories.test.ts 
b/packages/taler-wallet-webextension/src/stories.test.ts
index bb5abb92c..9277530a3 100644
--- a/packages/taler-wallet-webextension/src/stories.test.ts
+++ b/packages/taler-wallet-webextension/src/stories.test.ts
@@ -19,37 +19,33 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 import { setupI18n } from "@gnu-taler/taler-util";
+import { parseGroupImport } from "@gnu-taler/web-util/lib/index.browser";
+import { setupPlatform } from "./platform/api.js";
+import chromeAPI from "./platform/chrome.js";
+import { renderNodeOrBrowser } from "./test-utils.js";
+
 import * as components from "./components/index.stories.js";
 import * as cta from "./cta/index.stories.js";
 import * as mui from "./mui/index.stories.js";
-import { setupPlatform } from "./platform/api.js";
-import chromeAPI from "./platform/chrome.js";
 import * as popup from "./popup/index.stories.js";
-import { renderNodeOrBrowser } from "./test-utils.js";
 import * as wallet from "./wallet/index.stories.js";
 
 setupI18n("en", { en: {} });
 setupPlatform(chromeAPI);
 
-function testThisStory(st: any): any {
-  describe(`example "${(st as any).default.title}"`, () => {
-    Object.keys(st).forEach((k) => {
-      const Component = (st as any)[k];
-      if (k === "default" || !Component) return;
-
-      it(`example: ${k}`, () => {
-        renderNodeOrBrowser(Component, Component.args);
-      });
-    });
-  });
-}
-
-describe("render every storybook example", () => {
-  [popup, wallet, cta, mui, components].forEach(function testAll(st: any) {
-    if (Array.isArray(st.default)) {
-      st.default.forEach(testAll);
-    } else {
-      testThisStory(st);
-    }
-  });
+describe("All the examples:", () => {
+  const cms = parseGroupImport({ popup, wallet, cta, mui, components })
+  cms.forEach(group => {
+    describe(`Example for group "${group.title}:"`, () => {
+      group.list.forEach(component => {
+        describe(`Component ${component.name}:`, () => {
+          component.examples.forEach(example => {
+            it(`should render example: ${example.name}`, () => {
+              renderNodeOrBrowser(example.render.component, 
example.render.props)
+            })
+          })
+        })
+      })
+    })
+  })
 });
diff --git a/packages/taler-wallet-webextension/src/stories.tsx 
b/packages/taler-wallet-webextension/src/stories.tsx
index 02cc15393..8834b8084 100644
--- a/packages/taler-wallet-webextension/src/stories.tsx
+++ b/packages/taler-wallet-webextension/src/stories.tsx
@@ -18,516 +18,65 @@
  *
  * @author Sebastian Javier Marchano (sebasjm)
  */
-import { setupI18n } from "@gnu-taler/taler-util";
-import { styled } from "@linaria/react";
-import {
-  ComponentChild,
-  ComponentChildren,
-  Fragment,
-  FunctionComponent,
-  h,
-  render,
-  VNode,
-} from "preact";
-import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { Fragment, FunctionComponent, h } from "preact";
 import { LogoHeader } from "./components/LogoHeader.js";
 import { PopupBox, WalletBox } from "./components/styled/index.js";
-import * as mui from "./mui/index.stories.js";
+import { strings } from "./i18n/strings.js";
 import { PopupNavBar, WalletNavBar } from "./NavigationBar.js";
+
+import * as components from "./components/index.stories.js";
+import * as cta from "./cta/index.stories.js";
+import * as mui from "./mui/index.stories.js";
 import * as popup from "./popup/index.stories.js";
 import * as wallet from "./wallet/index.stories.js";
-import * as cta from "./cta/index.stories.js";
-import * as components from "./components/index.stories.js";
-import { strings } from "./i18n/strings.js";
-import { setupPlatform } from "./platform/api.js";
-import chromeAPI from "./platform/chrome.js";
-import firefoxAPI from "./platform/firefox.js";
 
-const url = new URL(window.location.href);
-const lang = url.searchParams.get("lang") || "en";
+import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
 
-setupI18n(lang, strings);
-
-const Page = styled.div`
-  * {
-    font-family: Arial, Helvetica, sans-serif;
-  }
-  p:not([class]) {
-    margin-bottom: 1em;
-    margin-top: 1em;
-  }
-  width: 100%;
-  display: flex;
-  flex-direction: row;
-`;
-
-const SideBar = styled.div`
-  min-width: var(--with-size);
-  height: calc(100vh - 20px);
-  overflow-y: visible;
-  overflow-x: hidden;
-  scroll-behavior: smooth;
-
-  * {
-    margin: 0px;
-    padding: 0px;
-  }
-
-  & > {
-    ol {
-      padding: 4px;
-      div:first-child {
-        background-color: lightcoral;
-        cursor: pointer;
-      }
-      div[data-hide="true"] {
-        display: none;
-      }
-      dd {
-        margin-left: 1em;
-        padding: 4px;
-        cursor: pointer;
-        border-radius: 4px;
-        margin-bottom: 4px;
-      }
-      dd:nth-child(even) {
-        background-color: lightgray;
-      }
-      dd:nth-child(odd) {
-        background-color: lightblue;
-      }
-      a {
-        color: black;
-      }
-      dd[data-selected] {
-        background-color: green;
-      }
-    }
-  }
-`;
-
-const ResizeHandleDiv = styled.div`
-  width: 10px;
-  background: #ddd;
-  cursor: ew-resize;
-`;
-
-function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
-  const [start, setStart] = useState<number | undefined>(undefined);
-  return (
-    <ResizeHandleDiv
-      onMouseDown={(e: any) => {
-        setStart(e.pageX);
-        console.log("active", e.pageX);
-        return false;
-      }}
-      onMouseMove={(e: any) => {
-        if (start !== undefined) {
-          onUpdate(e.pageX - start);
-        }
-        return false;
-      }}
-      onMouseUp={() => {
-        setStart(undefined);
-        return false;
-      }}
-    />
-  );
-}
-
-const Content = styled.div`
-  width: 100%;
-  padding: 20px;
-`;
-
-function parseExampleImport(group: string, im: any): ComponentItem {
-  const component = im.default.title;
-  return {
-    name: component,
-    examples: Object.entries(im)
-      .filter(([k]) => k !== "default")
-      .map(
-        ([name, render]) =>
-          ({
-            group,
-            component,
-            name,
-            render,
-          } as ExampleItem),
-      ),
-  };
-}
-
-const allExamples = Object.entries({ popup, wallet, cta, mui, components 
}).map(
-  ([title, value]) => ({
-    title,
-    list: value.default.map((s) => parseExampleImport(title, s)),
-  }),
-);
-
-interface ComponentItem {
-  name: string;
-  examples: ExampleItem[];
-}
-
-interface ExampleItem {
-  group: string;
-  component: string;
-  name: string;
-  render: {
-    (args: any): VNode;
-    args: any;
-  };
-}
-
-function findByGroupComponentName(
-  group: string,
-  component: string,
-  name: string,
-): ExampleItem | undefined {
-  const gl = allExamples.filter((e) => e.title === group);
-  if (gl.length === 0) {
-    return undefined;
-  }
-  const cl = gl[0].list.filter((l) => l.name === component);
-  if (cl.length === 0) {
-    return undefined;
-  }
-  const el = cl[0].examples.filter((c) => c.name === name);
-  if (el.length === 0) {
-    return undefined;
-  }
-  return el[0];
-}
-
-function getContentForExample(item: ExampleItem | undefined): () => VNode {
-  if (!item)
-    return function SelectExampleMessage() {
-      return <div>select example from the list on the left</div>;
-    };
-  const example = findByGroupComponentName(
-    item.group,
-    item.component,
-    item.name,
-  );
-  if (!example) {
-    return function ExampleNotFoundMessage() {
-      return <div>example not found</div>;
-    };
-  }
-  return () => example.render(example.render.args);
-}
-
-function ExampleList({
-  name,
-  list,
-  selected,
-  onSelectStory,
-}: {
-  name: string;
-  list: {
-    name: string;
-    examples: ExampleItem[];
-  }[];
-  selected: ExampleItem | undefined;
-  onSelectStory: (i: ExampleItem, id: string) => void;
-}): VNode {
-  const [isOpen, setOpen] = useState(selected && selected.group === name);
-  return (
-    <ol>
-      <div onClick={() => setOpen(!isOpen)}>{name}</div>
-      <div data-hide={!isOpen}>
-        {list.map((k) => (
-          <li key={k.name}>
-            <dl>
-              <dt>{k.name.substring(k.name.indexOf("/") + 1)}</dt>
-              {k.examples.map((r) => {
-                const e = encodeURIComponent;
-                const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
-                const isSelected =
-                  selected &&
-                  selected.component === r.component &&
-                  selected.group === r.group &&
-                  selected.name === r.name;
-                return (
-                  <dd id={eId} key={r.name} data-selected={isSelected}>
-                    <a
-                      href={`#${eId}`}
-                      onClick={(e) => {
-                        e.preventDefault();
-                        location.hash = `#${eId}`;
-                        onSelectStory(r, eId);
-                        history.pushState({}, "", `#${eId}`);
-                      }}
-                    >
-                      {r.name}
-                    </a>
-                  </dd>
-                );
-              })}
-            </dl>
-          </li>
-        ))}
-      </div>
-    </ol>
+function main(): void {
+  renderStories(
+    { popup, wallet, cta, mui, components },
+    {
+      strings,
+      getWrapperForGroup,
+    },
   );
 }
 
-/**
- * Prevents the UI from redirecting and inform the dev
- * where the <a /> should have redirected
- * @returns
- */
-function PreventLinkNavigation({
-  children,
-}: {
-  children: ComponentChildren;
-}): VNode {
-  return (
-    <div
-      onClick={(e) => {
-        let t: any = e.target;
-        do {
-          if (t.localName === "a" && t.getAttribute("href")) {
-            alert(`should navigate to: ${t.attributes.href.value}`);
-            e.stopImmediatePropagation();
-            e.stopPropagation();
-            e.preventDefault();
-            return false;
-          }
-        } while ((t = t.parentNode));
-      }}
-    >
-      {children}
-    </div>
-  );
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoaded", main);
+} else {
+  main();
 }
-
 function getWrapperForGroup(group: string): FunctionComponent {
   switch (group) {
     case "popup":
       return function PopupWrapper({ children }: any) {
         return (
-          <PreventLinkNavigation>
+          <Fragment>
             <PopupNavBar />
             <PopupBox>{children}</PopupBox>
-          </PreventLinkNavigation>
+          </Fragment>
         );
       };
     case "wallet":
       return function WalletWrapper({ children }: any) {
         return (
-          <PreventLinkNavigation>
+          <Fragment>
             <LogoHeader />
             <WalletNavBar />
             <WalletBox>{children}</WalletBox>
-          </PreventLinkNavigation>
+          </Fragment>
         );
       };
     case "cta":
       return function WalletWrapper({ children }: any) {
         return (
-          <PreventLinkNavigation>
+          <Fragment>
             <WalletBox>{children}</WalletBox>
-          </PreventLinkNavigation>
+          </Fragment>
         );
       };
     default:
       return Fragment;
   }
 }
-
-function ErrorReport({
-  children,
-  selected,
-}: {
-  children: ComponentChild;
-  selected: ExampleItem | undefined;
-}): VNode {
-  const [error, resetError] = useErrorBoundary();
-  //if there is an error, reset when unloading this component
-  useEffect(() => (error ? resetError : undefined));
-  if (error) {
-    return (
-      <div>
-        <p>Error was thrown trying to render</p>
-        {selected && (
-          <ul>
-            <li>
-              <b>group</b>: {selected.group}
-            </li>
-            <li>
-              <b>component</b>: {selected.component}
-            </li>
-            <li>
-              <b>example</b>: {selected.name}
-            </li>
-            <li>
-              <b>args</b>:{" "}
-              <pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre>
-            </li>
-          </ul>
-        )}
-        <p>{error.message}</p>
-        <pre>{error.stack}</pre>
-      </div>
-    );
-  }
-  return <Fragment>{children}</Fragment>;
-}
-
-function getSelectionFromLocationHash(hash: string): ExampleItem | undefined {
-  if (!hash) return undefined;
-  const parts = hash.substring(1).split("-");
-  if (parts.length < 3) return undefined;
-  return findByGroupComponentName(
-    decodeURIComponent(parts[0]),
-    decodeURIComponent(parts[1]),
-    decodeURIComponent(parts[2]),
-  );
-}
-
-function Application(): VNode {
-  const initialSelection = getSelectionFromLocationHash(location.hash);
-  const [selected, updateSelected] = useState<ExampleItem | undefined>(
-    initialSelection,
-  );
-  useEffect(() => {
-    if (location.hash) {
-      const hash = location.hash.substring(1);
-      const found = document.getElementById(hash);
-      if (found) {
-        setTimeout(() => {
-          found.scrollIntoView({
-            block: "center",
-          });
-        }, 10);
-      }
-    }
-  }, []);
-
-  const ExampleContent = getContentForExample(selected);
-
-  const GroupWrapper = getWrapperForGroup(selected?.group || "default");
-  const [sidebarWidth, setSidebarWidth] = useState(200);
-
-  return (
-    <Page>
-      <LiveReload />
-      <SideBar style={{ "--with-size": `${sidebarWidth}px` }}>
-        {allExamples.map((e) => (
-          <ExampleList
-            key={e.title}
-            name={e.title}
-            list={e.list}
-            selected={selected}
-            onSelectStory={(item, htmlId) => {
-              document.getElementById(htmlId)?.scrollIntoView({
-                block: "center",
-              });
-              updateSelected(item);
-            }}
-          />
-        ))}
-        <hr />
-      </SideBar>
-      <ResizeHandle
-        onUpdate={(x) => {
-          setSidebarWidth((s) => s + x);
-        }}
-      />
-      <Content>
-        <ErrorReport selected={selected}>
-          <GroupWrapper>
-            <ExampleContent />
-          </GroupWrapper>
-        </ErrorReport>
-      </Content>
-    </Page>
-  );
-}
-
-if (document.readyState === "loading") {
-  document.addEventListener("DOMContentLoaded", main);
-} else {
-  main();
-}
-function main(): void {
-  try {
-    const container = document.getElementById("container");
-    if (!container) {
-      throw Error("container not found, can't mount page contents");
-    }
-    render(<Application />, container);
-  } catch (e) {
-    console.error("got error", e);
-    if (e instanceof Error) {
-      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
-    }
-  }
-}
-
-let liveReloadMounted = false;
-function LiveReload({ port = 8002 }: { port?: number }): VNode {
-  const [isReloading, setIsReloading] = useState(false);
-  useEffect(() => {
-    if (!liveReloadMounted) {
-      setupLiveReload(port, () => {
-        setIsReloading(true);
-        window.location.reload();
-      });
-      liveReloadMounted = true;
-    }
-  });
-
-  if (isReloading) {
-    return (
-      <div
-        style={{
-          position: "absolute",
-          width: "100%",
-          height: "100%",
-          backgroundColor: "rgba(0,0,0,0.5)",
-          color: "white",
-          display: "flex",
-          justifyContent: "center",
-        }}
-      >
-        <h1 style={{ margin: "auto" }}>reloading...</h1>
-      </div>
-    );
-  }
-  return <Fragment />;
-}
-
-function setupLiveReload(port: number, onReload: () => void): void {
-  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
-  const host = location.hostname;
-  const socketPath = `${protocol}//${host}:${port}/socket`;
-
-  const ws = new WebSocket(socketPath);
-  ws.onmessage = (message) => {
-    const event = JSON.parse(message.data);
-    if (event.type === "LOG") {
-      console.log(event.message);
-    }
-    if (event.type === "RELOAD") {
-      onReload();
-    }
-  };
-  ws.onerror = (error) => {
-    console.error(error);
-  };
-}
-
-const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
-
-//FIXME: create different entry point for any platform instead of
-//switching in runtime
-if (isFirefox) {
-  console.log("Wallet setup for Firefox API");
-  setupPlatform(firefoxAPI);
-} else {
-  console.log("Wallet setup for Chrome API");
-  setupPlatform(chromeAPI);
-}
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts 
b/packages/taler-wallet-webextension/src/test-utils.ts
index ccc8774e9..b4983b4c2 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -49,8 +49,12 @@ export function createExample<Props>(
   // check how we can build evaluatedProps in render time
   const evaluatedProps = typeof props === "function" ? props() : props;
   const Render = (args: any): VNode => create(Component, args);
-  Render.args = evaluatedProps;
-  return Render;
+  // Render.args = evaluatedProps;
+
+  return {
+    component: Render,
+    props: evaluatedProps
+  };
 }
 
 export function createExampleWithCustomContext<Props, ContextProps>(
@@ -66,8 +70,11 @@ export function createExampleWithCustomContext<Props, 
ContextProps>(
       ...contextProps,
       children: [Render(args)],
     } as any);
-  WithContext.args = evaluatedProps;
-  return WithContext;
+
+  return {
+    component: WithContext,
+    props: evaluatedProps
+  };
 }
 
 export function NullLink({
@@ -334,8 +341,8 @@ export function createWalletApiMock(): {
         callback: cb
           ? cb
           : () => {
-              null;
-            },
+            null;
+          },
       });
       return handler;
     },
diff --git 
a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
index ae3e1b091..887ad235e 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ConfirmProviderView, SelectProviderView } from "./views.js";
 
 export default {
-  title: "wallet/backup/confirm",
+  title: "add backup provider",
 };
 
 export const DemoService = createExample(ConfirmProviderView, {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
index cf1551127..f5db3825d 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { AddNewActionView as TestedComponent } from "./AddNewActionView.js";
 
 export default {
-  title: "wallet/add new action",
+  title: "add new action",
   component: TestedComponent,
   argTypes: {
     setDeviceName: () => Promise.resolve(),
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index 2e19d3944..c3a1ea5d6 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -29,13 +29,7 @@ import { createExample } from "../test-utils.js";
 import { TalerProtocolTimestamp } from "@gnu-taler/taler-util";
 
 export default {
-  title: "wallet/backup/list",
-  component: TestedComponent,
-  argTypes: {
-    onRetry: { action: "onRetry" },
-    onDelete: { action: "onDelete" },
-    onBack: { action: "onBack" },
-  },
+  title: "backup",
 };
 
 export const LotOfProviders = createExample(TestedComponent, {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
index 75c544c84..b4d1060eb 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
@@ -25,7 +25,7 @@ import { labelForAccountType } from "./state.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/deposit",
+  title: "deposit",
 };
 
 // const ac = parsePaytoUri("payto://iban/ES8877998399652238")!;
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
index 492da193b..2f066d744 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
@@ -74,7 +74,7 @@ export namespace State {
 }
 
 export type Contact = {
-  icon: string;
+  icon_type: string;
   name: string;
   description: string;
 };
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
index fe02151de..a67f926bc 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
@@ -21,7 +21,6 @@ import { useAsyncAsHook } from 
"../../hooks/useAsyncAsHook.js";
 import { assertUnreachable, RecursiveState } from "../../utils/index.js";
 import { wxApi } from "../../wxApi.js";
 import { Contact, Props, State } from "./index.js";
-import bankIcon from "../../svg/ri-bank-line.svg";
 
 export function useComponentState(
   props: Props,
@@ -42,22 +41,22 @@ export function useComponentState(
   const previous: Contact[] = true
     ? []
     : [
-        {
-          name: "International Bank",
-          icon: bankIcon, //FIXME: should be decided in the view
-          description: "account ending with 3454",
-        },
-        {
-          name: "Max",
-          icon: bankIcon,
-          description: "account ending with 3454",
-        },
-        {
-          name: "Alex",
-          icon: bankIcon,
-          description: "account ending with 3454",
-        },
-      ];
+      {
+        name: "International Bank",
+        icon_type: 'bank',
+        description: "account ending with 3454",
+      },
+      {
+        name: "Max",
+        icon_type: 'bank',
+        description: "account ending with 3454",
+      },
+      {
+        name: "Alex",
+        icon_type: 'bank',
+        description: "account ending with 3454",
+      },
+    ];
 
   if (!amount) {
     return () => {
@@ -115,15 +114,15 @@ export function useComponentState(
           onClick: invalid
             ? undefined
             : async () => {
-                props.goToWalletBankDeposit(currencyAndAmount);
-              },
+              props.goToWalletBankDeposit(currencyAndAmount);
+            },
         },
         goToWallet: {
           onClick: invalid
             ? undefined
             : async () => {
-                props.goToWalletWalletSend(currencyAndAmount);
-              },
+              props.goToWalletWalletSend(currencyAndAmount);
+            },
         },
         amountHandler: {
           onInput: async (s) => setAmount(s),
@@ -145,15 +144,15 @@ export function useComponentState(
           onClick: invalid
             ? undefined
             : async () => {
-                props.goToWalletManualWithdraw(currencyAndAmount);
-              },
+              props.goToWalletManualWithdraw(currencyAndAmount);
+            },
         },
         goToWallet: {
           onClick: invalid
             ? undefined
             : async () => {
-                props.goToWalletWalletInvoice(currencyAndAmount);
-              },
+              props.goToWalletWalletInvoice(currencyAndAmount);
+            },
         },
         amountHandler: {
           onInput: async (s) => setAmount(s),
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
index b8d868683..ffec8ba36 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView, SelectCurrencyView } from "./views.js";
 
 export default {
-  title: "wallet/destination",
+  title: "destination",
 };
 
 export const GetCash = createExample(ReadyView, {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index ba8d65ffa..a9a4b2e41 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -377,6 +377,7 @@ export function ReadySendView({
     </Container>
   );
 }
+import bankIcon from "../../svg/ri-bank-line.svg";
 
 function RowExample({
   info,
@@ -385,15 +386,22 @@ function RowExample({
   info: Contact;
   disabled?: boolean;
 }): VNode {
+  const icon = info.icon_type === "bank" ? bankIcon : undefined;
   return (
     <MediaExample data-disabled={disabled}>
       <MediaLeft>
         <CircleDiv>
-          <SvgIcon
-            title={info.name}
-            dangerouslySetInnerHTML={{ __html: info.icon }}
-            color="currentColor"
-          />
+          {icon !== undefined ? (
+            <SvgIcon
+              title={info.name}
+              dangerouslySetInnerHTML={{
+                __html: icon,
+              }}
+              color="currentColor"
+            />
+          ) : (
+            <span>A</span>
+          )}
         </CircleDiv>
       </MediaLeft>
       <MediaBody>
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
index 73bcbc6bc..d9a5a8fd7 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
@@ -24,7 +24,7 @@ import { createExample } from "../test-utils.js";
 import { View as TestedComponent } from "./DeveloperPage.js";
 
 export default {
-  title: "wallet/developer",
+  title: "developer",
   component: TestedComponent,
   argTypes: {
     setDeviceName: () => Promise.resolve(),
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx
index b58fce8e6..8fbecfc4c 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { ExchangeAddConfirmPage as TestedComponent } from 
"./ExchangeAddConfirm.js";
 
 export default {
-  title: "wallet/exchange add/confirm",
+  title: "exchange add confirm",
   component: TestedComponent,
   argTypes: {
     onRetry: { action: "onRetry" },
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
index e71b34f31..cd86ad8c6 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
@@ -24,13 +24,7 @@ import { queryToSlashKeys } from "../utils/index.js";
 import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl.js";
 
 export default {
-  title: "wallet/exchange add/set url",
-  component: TestedComponent,
-  argTypes: {
-    onRetry: { action: "onRetry" },
-    onDelete: { action: "onDelete" },
-    onBack: { action: "onBack" },
-  },
+  title: "exchange add set url",
 };
 
 export const ExpectedUSD = createExample(TestedComponent, {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
index dfa8bbd39..3706359a8 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ComparingView, ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/select exchange",
+  title: "select exchange",
 };
 
 export const Bitcoin1 = createExample(ReadyView, {
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 1efd917c8..1674ac135 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -40,7 +40,7 @@ import { HistoryView as TestedComponent } from "./History.js";
 import { createExample } from "../test-utils.js";
 
 export default {
-  title: "wallet/balance",
+  title: "balance",
   component: TestedComponent,
 };
 
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
index 875dec227..ca6db8be9 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/manage account",
+  title: "manage account",
 };
 
 const nullFunction = async () => {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
index e4c7105e9..c4da99909 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
@@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
 import { ReadyView } from "./views.js";
 
 export default {
-  title: "wallet/notifications",
+  title: "notifications",
 };
 
 export const Ready = createExample(ReadyView, {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
 
b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
index bd3b7d2d3..9ca397302 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage.js";
 
 export default {
-  title: "wallet/backup/confirm",
+  title: "confirm",
   component: TestedComponent,
   argTypes: {
     onRetry: { action: "onRetry" },
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
index b728d1e2e..a5528c36b 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
+++ 
b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { SetUrlView as TestedComponent } from "./ProviderAddPage.js";
 
 export default {
-  title: "wallet/backup/add",
+  title: "add",
   component: TestedComponent,
   argTypes: {
     onRetry: { action: "onRetry" },
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
index 854c14ac1..98c68e6bd 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -25,7 +25,7 @@ import { createExample } from "../test-utils.js";
 import { ProviderView as TestedComponent } from "./ProviderDetailPage.js";
 
 export default {
-  title: "wallet/backup/details",
+  title: "provider details",
   component: TestedComponent,
   argTypes: {
     onRetry: { action: "onRetry" },
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
index caf833e79..0fc38e90f 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { QrReaderPage } from "./QrReader.js";
 
 export default {
-  title: "wallet/qr reader",
+  title: "qr reader",
 };
 
 export const Reading = createExample(QrReaderPage, {});
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
index 7d10ad0f4..7ea3b386b 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
@@ -24,7 +24,7 @@ import { createExample } from "../test-utils.js";
 import { ReserveCreated as TestedComponent } from "./ReserveCreated.js";
 
 export default {
-  title: "wallet/manual withdraw/reserve created",
+  title: "reserve created",
   component: TestedComponent,
   argTypes: {},
 };
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
index 4082ca29b..04b7f3e09 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { SettingsView as TestedComponent } from "./Settings.js";
 
 export default {
-  title: "wallet/settings",
+  title: "settings",
   component: TestedComponent,
   argTypes: {
     setDeviceName: () => Promise.resolve(),
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index abbc883bd..868d3b0e6 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -46,7 +46,7 @@ import {
 import { TransactionView as TestedComponent } from "./Transaction.js";
 
 export default {
-  title: "wallet/history/details",
+  title: "transaction details",
   component: TestedComponent,
   argTypes: {
     onRetry: { action: "onRetry" },
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
index 118d261fa..7e52d4270 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
@@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
 import { View as TestedComponent } from "./Welcome.js";
 
 export default {
-  title: "wallet/welcome",
+  title: "welcome",
   component: TestedComponent,
 };
 
diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
index 554527108..05e141dc6 100644
--- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
@@ -19,42 +19,21 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import * as a1 from "./Backup.stories.js";
-import * as a4 from "./DepositPage/stories.js";
-import * as a5 from "./ExchangeAddConfirm.stories.js";
-import * as a6 from "./ExchangeAddSetUrl.stories.js";
-import * as a7 from "./History.stories.js";
-import * as a8 from "./AddBackupProvider/stories.js";
-import * as a10 from "./ProviderDetail.stories.js";
-import * as a11 from "./ReserveCreated.stories.js";
-import * as a12 from "./Settings.stories.js";
-import * as a13 from "./Transaction.stories.js";
-import * as a14 from "./Welcome.stories.js";
-import * as a15 from "./AddNewActionView.stories.js";
-import * as a16 from "./DeveloperPage.stories.js";
-import * as a17 from "./QrReader.stories.js";
-import * as a18 from "./DestinationSelection/stories.js";
-import * as a19 from "./ExchangeSelection/stories.js";
-import * as a20 from "./ManageAccount/stories.js";
-import * as a21 from "./Notifications/stories.js";
-
-export default [
-  a1,
-  a4,
-  a5,
-  a6,
-  a7,
-  a8,
-  a10,
-  a11,
-  a12,
-  a13,
-  a14,
-  a15,
-  a16,
-  a17,
-  a18,
-  a19,
-  a20,
-  a21,
-];
+export * as a1 from "./Backup.stories.js";
+export * as a4 from "./DepositPage/stories.js";
+export * as a5 from "./ExchangeAddConfirm.stories.js";
+export * as a6 from "./ExchangeAddSetUrl.stories.js";
+export * as a7 from "./History.stories.js";
+export * as a8 from "./AddBackupProvider/stories.js";
+export * as a10 from "./ProviderDetail.stories.js";
+export * as a11 from "./ReserveCreated.stories.js";
+export * as a12 from "./Settings.stories.js";
+export * as a13 from "./Transaction.stories.js";
+export * as a14 from "./Welcome.stories.js";
+export * as a15 from "./AddNewActionView.stories.js";
+export * as a16 from "./DeveloperPage.stories.js";
+export * as a17 from "./QrReader.stories.js";
+export * as a18 from "./DestinationSelection/stories.js";
+export * as a19 from "./ExchangeSelection/stories.js";
+export * as a20 from "./ManageAccount/stories.js";
+export * as a21 from "./Notifications/stories.js";
diff --git a/packages/taler-wallet-webextension/tsconfig.json 
b/packages/taler-wallet-webextension/tsconfig.json
index 95e7b34fd..5fc45caae 100644
--- a/packages/taler-wallet-webextension/tsconfig.json
+++ b/packages/taler-wallet-webextension/tsconfig.json
@@ -31,6 +31,9 @@
     {
       "path": "../taler-wallet-core/"
     },
+    {
+      "path": "../web-util/"
+    },
     {
       "path": "../taler-util/"
     }
diff --git a/packages/demobank-ui/src/pages/notfound/style.css 
b/packages/web-util/README
similarity index 100%
copy from packages/demobank-ui/src/pages/notfound/style.css
copy to packages/web-util/README
diff --git a/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs 
b/packages/web-util/bin/taler-web-cli.mjs
similarity index 92%
copy from packages/taler-wallet-cli/bin/taler-wallet-cli.mjs
copy to packages/web-util/bin/taler-web-cli.mjs
index e3378471c..4e89cf46d 100755
--- a/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs
+++ b/packages/web-util/bin/taler-web-cli.mjs
@@ -15,5 +15,5 @@
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { main } from '../dist/taler-wallet-cli.mjs';
+import { main } from '../lib/cli.cjs';
 main();
diff --git a/packages/taler-wallet-cli/build.mjs b/packages/web-util/build.mjs
similarity index 64%
copy from packages/taler-wallet-cli/build.mjs
copy to packages/web-util/build.mjs
index b6b3f4a16..ba277b666 100755
--- a/packages/taler-wallet-cli/build.mjs
+++ b/packages/web-util/build.mjs
@@ -19,22 +19,17 @@ import esbuild from 'esbuild'
 import path from "path"
 import fs from "fs"
 
+// eslint-disable-next-line no-undef
 const BASE = process.cwd()
 
-const preact = path.join(BASE, "node_modules", "preact", "compat", "dist", 
"compat.module.js");
-const preactCompatPlugin = {
-  name: "preact-compat",
-  setup(build) {
-    build.onResolve({ filter: /^(react-dom|react)$/ }, args => ({ path: preact 
}));
-  }
-}
-
 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_ROOT === '/' ? undefined : git_hash()
@@ -51,29 +46,58 @@ function git_hash() {
   }
 }
 
-export const buildConfig = {
-  entryPoints: ["src/index.ts"],
-  outfile: "dist/taler-wallet-cli.cjs",
+const buildConfigBase = {
+  outdir: "lib",
   bundle: true,
   minify: false,
   target: [
     'es6'
   ],
-  format: 'cjs',
-  platform: 'node',
+  loader: {
+    '.key': 'text',
+    '.crt': 'text',
+    '.html': 'text',
+  },
   sourcemap: true,
-  jsxFactory: 'h',
-  jsxFragment: 'Fragment',
   define: {
     '__VERSION__': `"${_package.version}"`,
     '__GIT_HASH__': `"${GIT_HASH}"`,
   },
 }
 
-esbuild
-  .build(buildConfig)
-  .catch((e) => {
-    console.log(e)
-    process.exit(1)
-  });
+const buildConfigNode = {
+  ...buildConfigBase,
+  entryPoints: ["src/index.node.ts", "src/cli.ts"],
+  outExtension: {
+    '.js': '.cjs'
+  },
+  format: 'cjs',
+  platform: 'node',
+  external: ["preact"],
+};
+
+const buildConfigBrowser = {
+  ...buildConfigBase,
+  entryPoints: ["src/index.browser.ts", "src/live-reload.ts", 
'src/stories.tsx'],
+  outExtension: {
+    '.js': '.mjs'
+  },
+  format: 'esm',
+  platform: 'browser',
+  external: ["preact", "@gnu-taler/taler-util", "jed"],
+  jsxFactory: 'h',
+  jsxFragment: 'Fragment',
+};
+
+[buildConfigNode, buildConfigBrowser].forEach((config) => {
+  esbuild
+    .build(config)
+    .catch((e) => {
+      // eslint-disable-next-line no-undef
+      console.log(e)
+      // eslint-disable-next-line no-undef
+      process.exit(1)
+    });
+
+})
 
diff --git a/packages/web-util/create_certificate.sh 
b/packages/web-util/create_certificate.sh
new file mode 100644
index 000000000..980aaf642
--- /dev/null
+++ b/packages/web-util/create_certificate.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+set -eu
+org=localhost-ca
+domain=localhost
+
+rm -rf keys
+mkdir keys
+cd keys
+
+openssl genpkey -algorithm RSA -out ca.key
+openssl req -x509 -key ca.key -out ca.crt \
+    -subj "/CN=$org/O=$org"
+
+openssl genpkey -algorithm RSA -out "$domain".key
+openssl req -new -key "$domain".key -out "$domain".csr \
+    -subj "/CN=$domain/O=$org"
+
+openssl x509 -req -in "$domain".csr -days 365 -out "$domain".crt \
+    -CA ca.crt -CAkey ca.key -CAcreateserial \
+    -extfile <(cat <<END
+basicConstraints = CA:FALSE
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer
+subjectAltName = DNS:$domain
+END
+    )
+
+sudo cp ca.crt /usr/local/share/ca-certificates/testing.crt
+sudo update-ca-certificates
+
+
+echo '
+## Chrome  
+1. go to chrome://settings/certificates
+2. tab "authorities"
+3. button "import" 
+4. choose "ca.crt"
+5. trust for identify websites
+
+## Firefox
+1. go to about:preferences#privacy
+2. button "view certificates"
+3. button "import"
+4. choose "ca.crt"
+5. trust for identify websites
+'
+
+echo done!
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
new file mode 100644
index 000000000..969215f9f
--- /dev/null
+++ b/packages/web-util/package.json
@@ -0,0 +1,40 @@
+{
+  "name": "@gnu-taler/web-util",
+  "version": "0.9.0",
+  "description": "Generic helper functionality for GNU Taler Web Apps",
+  "type": "module",
+  "types": "./lib/index.node.d.ts",
+  "main": "./dist/taler-web-cli.cjs",
+  "bin": {
+    "taler-wallet-cli": "./bin/taler-web-cli.cjs"
+  },
+  "author": "Sebastian Marchano",
+  "license": "AGPL-3.0-or-later",
+  "private": false,
+  "exports": {
+    "./lib/index.browser": "./lib/index.browser.mjs",
+    "./lib/index.node": "./lib/index.node.cjs"
+  },
+  "scripts": {
+    "prepare": "tsc",
+    "compile": "./build.mjs",
+    "clean": "rimraf dist lib tsconfig.tsbuildinfo",
+    "pretty": "prettier --write src"
+  },
+  "devDependencies": {
+    "@gnu-taler/taler-util": "workspace:*",
+    "@types/express": "^4.17.14",
+    "@types/node": "^18.11.9",
+    "@types/web": "^0.0.82",
+    "@types/ws": "^8.5.3",
+    "chokidar": "^3.5.3",
+    "esbuild": "^0.14.21",
+    "express": "^4.18.2",
+    "preact": "10.11.3",
+    "prettier": "^2.5.1",
+    "rimraf": "^3.0.2",
+    "tslib": "^2.4.0",
+    "typescript": "^4.8.4",
+    "ws": "7.4.5"
+  }
+}
diff --git a/packages/web-util/src/cli.ts b/packages/web-util/src/cli.ts
new file mode 100644
index 000000000..59cfe9989
--- /dev/null
+++ b/packages/web-util/src/cli.ts
@@ -0,0 +1,59 @@
+
+import {
+  clk, setGlobalLogLevelFromString
+} from "@gnu-taler/taler-util";
+import { serve } from "./serve.js";
+
+
+export const walletCli = clk
+  .program("wallet", {
+    help: "Command line interface for the GNU Taler wallet.",
+  })
+  .maybeOption("log", ["-L", "--log"], clk.STRING, {
+    help: "configure log level (NONE, ..., TRACE)",
+    onPresentHandler: (x) => {
+      setGlobalLogLevelFromString(x);
+    },
+  })
+  .flag("version", ["-v", "--version"], {
+    onPresentHandler: printVersion,
+  })
+  .flag("verbose", ["-V", "--verbose"], {
+    help: "Enable verbose output.",
+  })
+
+walletCli
+  .subcommand("serve", "serve", { help: "Create a server." })
+  .maybeOption("folder", ["-F", "--folder"], clk.STRING, {
+    help: "should complete",
+    // default: "./dist"
+  })
+  .maybeOption("port", ["-P", "--port"], clk.INT, {
+    help: "should complete",
+    // default: 8000
+  })
+  .flag("development", ["-D", "--dev"], {
+    help: "should complete",
+  })
+  .action(async (args) => {
+    return serve({
+      folder: args.serve.folder || "./dist",
+      port: args.serve.port || 8000,
+      development: args.serve.development
+    })
+  }
+  );
+
+
+
+declare const __VERSION__: string;
+function printVersion(): void {
+  console.log(__VERSION__);
+  process.exit(0);
+}
+
+export function main(): void {
+  walletCli.run();
+}
+
+
diff --git a/packages/web-util/src/custom.d.ts 
b/packages/web-util/src/custom.d.ts
new file mode 100644
index 000000000..6049ac6a9
--- /dev/null
+++ b/packages/web-util/src/custom.d.ts
@@ -0,0 +1,12 @@
+declare module "*.crt" {
+  const content: string;
+  export default content;
+}
+declare module "*.key" {
+  const content: string;
+  export default content;
+}
+declare module "*.html" {
+  const content: string;
+  export default content;
+}
diff --git a/packages/web-util/src/index.browser.ts 
b/packages/web-util/src/index.browser.ts
new file mode 100644
index 000000000..514a2ec42
--- /dev/null
+++ b/packages/web-util/src/index.browser.ts
@@ -0,0 +1,38 @@
+
+//`ws://localhost:8003/socket`
+export function setupLiveReload(wsURL: string | undefined) {
+  if (!wsURL) return;
+  const ws = new WebSocket(wsURL);
+  ws.addEventListener('message', (message) => {
+    const event = JSON.parse(message.data);
+    if (event.type === "LOG") {
+      console.log(event.message);
+    }
+    if (event.type === "RELOAD") {
+      window.location.reload();
+    }
+    if (event.type === "UPDATE") {
+      const c = document.getElementById("container")
+      if (c) {
+        document.body.removeChild(c);
+      }
+      const d = document.createElement("div");
+      d.setAttribute("id", "container");
+      d.setAttribute("class", "app-container");
+      document.body.appendChild(d);
+      const s = document.createElement("script");
+      s.setAttribute("id", "code");
+      s.setAttribute("type", "application/javascript");
+      s.textContent = atob(event.content);
+      document.body.appendChild(s);
+    }
+  });
+  ws.onerror = (error) => {
+    console.error(error);
+  };
+  ws.onclose = (e) => {
+    setTimeout(setupLiveReload, 500);
+  };
+}
+
+export { renderStories, parseGroupImport } from "./stories.js"
diff --git a/packages/web-util/src/index.node.ts 
b/packages/web-util/src/index.node.ts
new file mode 100644
index 000000000..0ef65921b
--- /dev/null
+++ b/packages/web-util/src/index.node.ts
@@ -0,0 +1,3 @@
+export { serve } from "./serve.js"
+
+
diff --git a/packages/web-util/src/index.ts b/packages/web-util/src/index.ts
new file mode 100644
index 000000000..cf0c963ed
--- /dev/null
+++ b/packages/web-util/src/index.ts
@@ -0,0 +1,4 @@
+
+
+
+export default {}
\ No newline at end of file
diff --git a/packages/web-util/src/keys/ca.crt 
b/packages/web-util/src/keys/ca.crt
new file mode 100644
index 000000000..d0fd544a6
--- /dev/null
+++ b/packages/web-util/src/keys/ca.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICODCCAaGgAwIBAgIUH8AY7kGN1yzGEwQOZKeL26ZOQHAwDQYJKoZIhvcNAQEL
+BQAwLjEVMBMGA1UEAwwMbG9jYWxob3N0LWNhMRUwEwYDVQQKDAxsb2NhbGhvc3Qt
+Y2EwHhcNMjIxMTMwMjIwNjAxWhcNMjIxMjMwMjIwNjAxWjAuMRUwEwYDVQQDDAxs
+b2NhbGhvc3QtY2ExFTATBgNVBAoMDGxvY2FsaG9zdC1jYTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAo2gw/oYcKxrSeDbVTTFX8pZA8fojGMwcQlSmeYMUrhtn
++PkXEvCTyMWcreLg2Y4sgdOjvK0ZM7OXnf/jx4fDiMpGy5BHT2ZJRWPzSh6UmNUy
+kyeRAkDB3gCyQSHmmL1rEFOuwmq1yoT0FlIyTQ+mWrs5yg7QTe1rRyFWXHIt1TMC
+AwEAAaNTMFEwHQYDVR0OBBYEFO1Op1KRMkVkzadGy2TZFQlwG9FFMB8GA1UdIwQY
+MBaAFO1Op1KRMkVkzadGy2TZFQlwG9FFMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADgYEAIdePTdDsD8IBFfHze9YVU+VZg3aNO5F/6QJPy/8InejQU0V8
+9Cod19SEh3Kdlpa4QLvZH1cX+ac7bvhL0JaZg0dsz8UaZ8xrkEPx6JJAwgCiv/Ir
+YqhoRd4fv/c6/B0yqD4Dhoy/jGkxfvc8XDnAuAP0uRttGwvsvHS9cSkHYFo=
+-----END CERTIFICATE-----
diff --git a/packages/web-util/src/keys/ca.key 
b/packages/web-util/src/keys/ca.key
new file mode 100644
index 000000000..8699ccb10
--- /dev/null
+++ b/packages/web-util/src/keys/ca.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAKNoMP6GHCsa0ng2
+1U0xV/KWQPH6IxjMHEJUpnmDFK4bZ/j5FxLwk8jFnK3i4NmOLIHTo7ytGTOzl53/
+48eHw4jKRsuQR09mSUVj80oelJjVMpMnkQJAwd4AskEh5pi9axBTrsJqtcqE9BZS
+Mk0Pplq7OcoO0E3ta0chVlxyLdUzAgMBAAECgYABkiDWcYeXynw3d595TH4h8NvS
+96qatGuZH6MyC9aJDe5j8FEOd42UIoItEb9DmCBJZzVtvOQ/IPzWIf2Yj2+LvydI
+qEA6ucroa9F9KG9T9ywNJfqM8fNzARQEAzK4/PglbT+n27hkNIm35BOA8PIUuBiD
+pT6D0L0LHfNs6NkRAQJBAM9RS9ApnRmo4qV8kNJvysBJ/NO8PdLT47XIA2uPaAAT
+O9NjrxGHaP0is+PIuwgTi9T5lyprpQss2yS9O7rN5PMCQQDJx0CMjkPDbelbWeH2
+nOvyxLLCev69ae6zVrMPcE7vRPohlJTSK/kgouLr0F6lomK9HVugD7VgrQHuj9am
+UV7BAkBhCHnlejSvl95M+lqGRBCvo3GUYJzHGqmPoYgIRdy1fEsaC6QbHjfDkwSD
+bqYrh4qBKjjYf/2Fl38SWQelzUyFAkBoht27cl9MN/3xIsjZ1kSsiJUKBmk8ekn7
+gWhVERry/EqPZscJcVonO/pNqq29JDf+O90hN8IACN+9U6ogknqBAkAr3SowHLyD
+LfTrEDxeoAd2+K7gGKyrK3gyIISbuWtluONNPqenuFFHXxehwJ72VplNkpUZP4Bt
+TQcIW9zIYT5r
+-----END PRIVATE KEY-----
diff --git a/packages/web-util/src/keys/ca.srl 
b/packages/web-util/src/keys/ca.srl
new file mode 100644
index 000000000..a53ff9b36
--- /dev/null
+++ b/packages/web-util/src/keys/ca.srl
@@ -0,0 +1 @@
+7488FC4F9D5E2BB55DEA16CF051F4E99ACA25241
diff --git a/packages/web-util/src/keys/localhost.crt 
b/packages/web-util/src/keys/localhost.crt
new file mode 100644
index 000000000..e32f2e24a
--- /dev/null
+++ b/packages/web-util/src/keys/localhost.crt
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICRTCCAa6gAwIBAgIUdIj8T51eK7Vd6hbPBR9OmayiUkEwDQYJKoZIhvcNAQEL
+BQAwLjEVMBMGA1UEAwwMbG9jYWxob3N0LWNhMRUwEwYDVQQKDAxsb2NhbGhvc3Qt
+Y2EwHhcNMjIxMTMwMjIwNjAyWhcNMjMxMTMwMjIwNjAyWjArMRIwEAYDVQQDDAls
+b2NhbGhvc3QxFTATBgNVBAoMDGxvY2FsaG9zdC1jYTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEAvir90pl9q6qUsBsBz7jjdw0r1DDPeViqkSyDDTt4Lw2zYJbn
+Z7kxcTvNHNRWdtsWSzY/43ERCJu6nX60kMiML3NV00ty2VpaYeW9J5ozXgNbb+5P
+esLHrIHmnOIUj46jyiHjDKs+hgrfcrFg7W7ndjW3dCAvkeAV+mncz59pFvkCAwEA
+AaNjMGEwCQYDVR0TBAIwADAdBgNVHQ4EFgQUXADNSPivlIUBpKyd/XirIcqxqFgw
+HwYDVR0jBBgwFoAU7U6nUpEyRWTNp0bLZNkVCXAb0UUwFAYDVR0RBA0wC4IJbG9j
+YWxob3N0MA0GCSqGSIb3DQEBCwUAA4GBAClcLuKFnRJjAgP8652jJscYMLWYEkv3
+j9kChErpKZNKiv+VlWKPiOvhZVAl+/YEsBOKXpRFX3CuLCdGtuv7b6NaH7yEXaZn
+9MVIrYMRub3k0gVAhu3z3VXuvHFXdTms3KRlGdPdQV2xgpQJczDNnd7idp/GyI4j
+KqBo0UxuWZBJ
+-----END CERTIFICATE-----
diff --git a/packages/web-util/src/keys/localhost.csr 
b/packages/web-util/src/keys/localhost.csr
new file mode 100644
index 000000000..5f821f8b5
--- /dev/null
+++ b/packages/web-util/src/keys/localhost.csr
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBajCB1AIBADArMRIwEAYDVQQDDAlsb2NhbGhvc3QxFTATBgNVBAoMDGxvY2Fs
+aG9zdC1jYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvir90pl9q6qUsBsB
+z7jjdw0r1DDPeViqkSyDDTt4Lw2zYJbnZ7kxcTvNHNRWdtsWSzY/43ERCJu6nX60
+kMiML3NV00ty2VpaYeW9J5ozXgNbb+5PesLHrIHmnOIUj46jyiHjDKs+hgrfcrFg
+7W7ndjW3dCAvkeAV+mncz59pFvkCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4GBADJW
+Ww+l4E///54fz82AE5x8U114Yk32EbB1qOfGLyXgoXySGyLuiNu40SXxioKa/Gpn
+Z92o5JIrMVWUroPzMKAMXdAsixkaBGrT5RYzR9ztfy59djxp0f7dlL3ZxDO8JHOw
+aTJXJxKEfYdv0oFhkx/u4ki6BsaqG9mQfsFXtlUp
+-----END CERTIFICATE REQUEST-----
diff --git a/packages/web-util/src/keys/localhost.key 
b/packages/web-util/src/keys/localhost.key
new file mode 100644
index 000000000..c9b1cb6c8
--- /dev/null
+++ b/packages/web-util/src/keys/localhost.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAL4q/dKZfauqlLAb
+Ac+443cNK9Qwz3lYqpEsgw07eC8Ns2CW52e5MXE7zRzUVnbbFks2P+NxEQibup1+
+tJDIjC9zVdNLctlaWmHlvSeaM14DW2/uT3rCx6yB5pziFI+Oo8oh4wyrPoYK33Kx
+YO1u53Y1t3QgL5HgFfpp3M+faRb5AgMBAAECgYEAh1xgqdxZqKzWA3hl1K7dMmus
+q/BGbjCf0JAnhG61QID3EqS3eIxI1jnj6UZ3eUi/WK/3z/Q2VLNMpTiAXKJzrUP0
+8m7yO87AeUxhy0rvtWEVmd8NBQjJKD2iElgy6tR9QUsgTXer9xuQf0sHRQb1psNU
+11WsBnwdzeEEzquORVUCQQDtJx/HjHDVTDF02W5B23J4oqwuu1EDCVDqNJiYSDSt
+2Dh0IdvSKJyh9lXIoY+kbbEui8uPPnhPKM1LIRfiv7FHAkEAzUf1mvTBNUGCwjZu
+qy/oKDR7TlEbdyDJY1F0JPquyim/CenRtM8VAH22Tni8+bSSpnHknytvKfaC0YFb
+VN8VvwJBAKTdJgKbZ3Vg2qDY5wVxgUrMC9cQ8Wii+VVX6x0yVSzlu5lAUIjxIrKV
+hV1Ms4cjmqE5HfIfA5REUTOBdhF0IdECQQC/1lia19Ha7/6/eljP17RQJkN5O+i7
+2kL5crxkdnRz7rFeFUlpfAB3dgOxr7mCbZKCw3rQmKmJAJreKNHuLZBHAkEAwYZ4
+tc4mWjtw4AMDK59o8d8ANObyuVaIy6I54NZ0ogg+0nzrXii9LkZZhAWwVSN9BdXa
+TYVu0J5fGxDZVAm0zQ==
+-----END PRIVATE KEY-----
diff --git a/packages/web-util/src/live-reload.ts 
b/packages/web-util/src/live-reload.ts
new file mode 100644
index 000000000..bae0a5b84
--- /dev/null
+++ b/packages/web-util/src/live-reload.ts
@@ -0,0 +1,52 @@
+/* eslint-disable no-undef */
+
+function setupLiveReload(): void {
+  const ws = new WebSocket("wss://localhost:8080/ws");
+
+  ws.addEventListener("message", (message) => {
+    try {
+      const event = JSON.parse(message.data);
+      if (event.type === "file-updated-start") {
+        showReloadOverlay();
+        return;
+      }
+      if (event.type === "file-updated-done") {
+        window.location.reload();
+        return;
+      }
+    } catch (e) {
+      return
+    }
+    console.log("unsupported", event);
+  });
+
+  ws.addEventListener("error", (error) => {
+    console.error(error);
+  });
+  ws.addEventListener("close", (message) => {
+    setTimeout(setupLiveReload, 1500);
+  });
+}
+setupLiveReload();
+
+
+function showReloadOverlay(): void {
+  const d = document.createElement("div");
+  d.style.position = "absolute";
+  d.style.width = "100%";
+  d.style.height = "100%";
+  d.style.color = "white";
+  d.style.backgroundColor = "rgba(0,0,0,0.5)";
+  d.style.display = "flex";
+  d.style.justifyContent = "center";
+  const h = document.createElement("h1");
+  h.style.margin = "auto";
+  h.innerHTML = "reloading...";
+  d.appendChild(h);
+  if (document.body.firstChild) {
+    document.body.insertBefore(d, document.body.firstChild);
+  } else {
+    document.body.appendChild(d);
+  }
+}
+
diff --git a/packages/web-util/src/serve.ts b/packages/web-util/src/serve.ts
new file mode 100644
index 000000000..11cc6db39
--- /dev/null
+++ b/packages/web-util/src/serve.ts
@@ -0,0 +1,108 @@
+import {
+  Logger
+} from "@gnu-taler/taler-util";
+import chokidar from 'chokidar';
+import express from "express";
+import https from "https";
+import { parse } from 'url';
+import WebSocket, { Server } from 'ws';
+
+
+import locahostCrt from './keys/localhost.crt';
+import locahostKey from './keys/localhost.key';
+import storiesHtml from './stories.html';
+
+import path from "path";
+
+const httpServerOptions = {
+  key: locahostKey,
+  cert: locahostCrt
+};
+
+const logger = new Logger("serve.ts");
+
+const PATHS = {
+  WS: "/ws",
+  NOTIFY: "/notify",
+  EXAMPLE: "/examples",
+  APP: "/app",
+}
+
+export async function serve(opts: {
+  folder: string,
+  port: number,
+  source?: string,
+  development?: boolean,
+  examplesLocationJs?: string,
+  examplesLocationCss?: string,
+  onUpdate?: () => Promise<void>;
+}): Promise<void> {
+
+  const app = express()
+
+  app.use(PATHS.APP, express.static(opts.folder))
+  const server = https.createServer(httpServerOptions, app)
+  server.listen(opts.port);
+  logger.info(`serving ${opts.folder} on ${opts.port}`)
+  logger.info(`  ${PATHS.APP}: application`)
+  logger.info(`  ${PATHS.EXAMPLE}: examples`)
+  logger.info(`  ${PATHS.WS}: websocket`)
+  logger.info(`  ${PATHS.NOTIFY}: broadcast`)
+
+  if (opts.development) {
+
+    const wss = new Server({ noServer: true });
+
+    wss.on('connection', function connection(ws) {
+      ws.send('welcome');
+    });
+
+    server.on('upgrade', function upgrade(request, socket, head) {
+      const { pathname } = parse(request.url || "");
+      if (pathname === PATHS.WS) {
+        wss.handleUpgrade(request, socket, head, function done(ws) {
+          wss.emit('connection', ws, request);
+        });
+      } else {
+        socket.destroy();
+      }
+    });
+
+    const sendToAllClients = function (data: object): void {
+      wss.clients.forEach(function each(client) {
+        if (client.readyState === WebSocket.OPEN) {
+          client.send(JSON.stringify(data));
+        }
+      })
+    }
+    const watchingFolder = opts.source ?? opts.folder
+    logger.info(`watching ${watchingFolder} for change`)
+
+    chokidar.watch(watchingFolder).on('change', (path, stats) => {
+      logger.trace(`changed ${path}`)
+
+      sendToAllClients({ type: 'file-updated-start', data: { path } })
+      if (opts.onUpdate) {
+        opts.onUpdate().then(result => {
+          sendToAllClients({ type: 'file-updated-done', data: { path, result } 
})
+        })
+      } else {
+        sendToAllClients({ type: 'file-change-done', data: { path } })
+      }
+    })
+
+    app.get(PATHS.EXAMPLE, function (req: any, res: any) {
+      res.set('Content-Type', 'text/html')
+      res.send(storiesHtml
+        .replace('__EXAMPLES_JS_FILE_LOCATION__', opts.examplesLocationJs ?? 
`.${PATHS.APP}/stories.js`)
+        .replace('__EXAMPLES_CSS_FILE_LOCATION__', opts.examplesLocationCss ?? 
`.${PATHS.APP}/stories.css`))
+    })
+
+    app.get(PATHS.NOTIFY, function (req: any, res: any) {
+      res.send('ok')
+    })
+
+  }
+}
+
+
diff --git a/packages/web-util/src/stories.html 
b/packages/web-util/src/stories.html
new file mode 100644
index 000000000..4c16ad2ff
--- /dev/null
+++ b/packages/web-util/src/stories.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>WebUtils: Stories</title>
+    <meta charset="utf-8" />
+    <link rel="icon" 
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/V
 [...]
+    <link
+      rel="stylesheet"
+      type="text/css"
+      href="__EXAMPLES_CSS_FILE_LOCATION__"
+    />
+    <script type="module" src="__EXAMPLES_JS_FILE_LOCATION__"></script>
+  </head>
+  <body>
+    <taler-stories id="container"></taler-stories>
+  </body>
+</html>
diff --git a/packages/web-util/src/stories.tsx 
b/packages/web-util/src/stories.tsx
new file mode 100644
index 000000000..a8a9fdf77
--- /dev/null
+++ b/packages/web-util/src/stories.tsx
@@ -0,0 +1,580 @@
+/*
+ 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 { setupI18n } from "@gnu-taler/taler-util";
+import e from "express";
+import {
+  ComponentChild,
+  ComponentChildren,
+  Fragment,
+  FunctionalComponent,
+  FunctionComponent,
+  h,
+  JSX,
+  render,
+  VNode,
+} from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+
+const Page: FunctionalComponent = ({ children }): VNode => {
+  return (
+    <div
+      style={{
+        fontFamily: "Arial, Helvetica, sans-serif",
+        width: "100%",
+        display: "flex",
+        flexDirection: "row",
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+const SideBar: FunctionalComponent<{ width: number }> = ({
+  width,
+  children,
+}): VNode => {
+  return (
+    <div
+      style={{
+        minWidth: width,
+        height: "calc(100vh - 20px)",
+        overflowX: "hidden",
+        overflowY: "visible",
+        scrollBehavior: "smooth",
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+const ResizeHandleDiv: FunctionalComponent<
+  JSX.HTMLAttributes<HTMLDivElement>
+> = ({ children, ...props }): VNode => {
+  return (
+    <div
+      {...props}
+      style={{
+        width: 10,
+        backgroundColor: "#ddd",
+        cursor: "ew-resize",
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+const Content: FunctionalComponent = ({ children }): VNode => {
+  return (
+    <div
+      style={{
+        width: "100%",
+        padding: 20,
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+function findByGroupComponentName(
+  allExamples: Group[],
+  group: string,
+  component: string,
+  name: string,
+): ExampleItem | undefined {
+  const gl = allExamples.filter((e) => e.title === group);
+  if (gl.length === 0) {
+    return undefined;
+  }
+  const cl = gl[0].list.filter((l) => l.name === component);
+  if (cl.length === 0) {
+    return undefined;
+  }
+  const el = cl[0].examples.filter((c) => c.name === name);
+  if (el.length === 0) {
+    return undefined;
+  }
+  return el[0];
+}
+
+function getContentForExample(
+  item: ExampleItem | undefined,
+  allExamples: Group[],
+): FunctionalComponent {
+  if (!item)
+    return function SelectExampleMessage() {
+      return <div>select example from the list on the left</div>;
+    };
+  const example = findByGroupComponentName(
+    allExamples,
+    item.group,
+    item.component,
+    item.name,
+  );
+  if (!example) {
+    return function ExampleNotFoundMessage() {
+      return <div>example not found</div>;
+    };
+  }
+  return () => example.render.component(example.render.props);
+}
+
+function ExampleList({
+  name,
+  list,
+  selected,
+  onSelectStory,
+}: {
+  name: string;
+  list: {
+    name: string;
+    examples: ExampleItem[];
+  }[];
+  selected: ExampleItem | undefined;
+  onSelectStory: (i: ExampleItem, id: string) => void;
+}): VNode {
+  const [isOpen, setOpen] = useState(selected && selected.group === name);
+  return (
+    <ol style={{ padding: 4, margin: 0 }}>
+      <div
+        style={{ backgroundColor: "lightcoral", cursor: "pointer" }}
+        onClick={() => setOpen(!isOpen)}
+      >
+        {name}
+      </div>
+      <div style={{ display: isOpen ? undefined : "none" }}>
+        {list.map((k) => (
+          <li key={k.name}>
+            <dl style={{ margin: 0 }}>
+              <dt>{k.name}</dt>
+              {k.examples.map((r, i) => {
+                const e = encodeURIComponent;
+                const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
+                const isSelected =
+                  selected &&
+                  selected.component === r.component &&
+                  selected.group === r.group &&
+                  selected.name === r.name;
+                return (
+                  <dd
+                    id={eId}
+                    key={r.name}
+                    style={{
+                      backgroundColor: isSelected
+                        ? "green"
+                        : i % 2
+                        ? "lightgray"
+                        : "lightblue",
+                      marginLeft: "1em",
+                      padding: 4,
+                      cursor: "pointer",
+                      borderRadius: 4,
+                      marginBottom: 4,
+                    }}
+                  >
+                    <a
+                      href={`#${eId}`}
+                      style={{ color: "black" }}
+                      onClick={(e) => {
+                        e.preventDefault();
+                        location.hash = `#${eId}`;
+                        onSelectStory(r, eId);
+                        history.pushState({}, "", `#${eId}`);
+                      }}
+                    >
+                      {r.name}
+                    </a>
+                  </dd>
+                );
+              })}
+            </dl>
+          </li>
+        ))}
+      </div>
+    </ol>
+  );
+}
+
+/**
+ * Prevents the UI from redirecting and inform the dev
+ * where the <a /> should have redirected
+ * @returns
+ */
+function PreventLinkNavigation({
+  children,
+}: {
+  children: ComponentChildren;
+}): VNode {
+  return (
+    <div
+      onClick={(e) => {
+        let t: any = e.target;
+        do {
+          if (t.localName === "a" && t.getAttribute("href")) {
+            alert(`should navigate to: ${t.attributes.href.value}`);
+            e.stopImmediatePropagation();
+            e.stopPropagation();
+            e.preventDefault();
+            return false;
+          }
+        } while ((t = t.parentNode));
+        return true;
+      }}
+    >
+      {children}
+    </div>
+  );
+}
+
+function ErrorReport({
+  children,
+  selected,
+}: {
+  children: ComponentChild;
+  selected: ExampleItem | undefined;
+}): VNode {
+  const [error, resetError] = useErrorBoundary();
+  //if there is an error, reset when unloading this component
+  useEffect(() => (error ? resetError : undefined));
+  if (error) {
+    return (
+      <div>
+        <p>Error was thrown trying to render</p>
+        {selected && (
+          <ul>
+            <li>
+              <b>group</b>: {selected.group}
+            </li>
+            <li>
+              <b>component</b>: {selected.component}
+            </li>
+            <li>
+              <b>example</b>: {selected.name}
+            </li>
+            <li>
+              <b>args</b>:{" "}
+              <pre>{JSON.stringify(selected.render.props, undefined, 2)}</pre>
+            </li>
+          </ul>
+        )}
+        <p>{error.message}</p>
+        <pre>{error.stack}</pre>
+      </div>
+    );
+  }
+  return <Fragment>{children}</Fragment>;
+}
+
+function getSelectionFromLocationHash(
+  hash: string,
+  allExamples: Group[],
+): ExampleItem | undefined {
+  if (!hash) return undefined;
+  const parts = hash.substring(1).split("-");
+  if (parts.length < 3) return undefined;
+  return findByGroupComponentName(
+    allExamples,
+    decodeURIComponent(parts[0]),
+    decodeURIComponent(parts[1]),
+    decodeURIComponent(parts[2]),
+  );
+}
+
+function parseExampleImport(
+  group: string,
+  componentName: string,
+  im: MaybeComponent,
+): ComponentItem {
+  const examples: ExampleItem[] = Object.entries(im)
+    .filter(([k]) => k !== "default")
+    .map(([exampleName, exampleValue]): ExampleItem => {
+      if (!exampleValue) {
+        throw Error(
+          `example "${exampleName}" from component "${componentName}" in group 
"${group}" is undefined`,
+        );
+      }
+
+      if (typeof exampleValue === "function") {
+        return {
+          group,
+          component: componentName,
+          name: exampleName,
+          render: {
+            component: exampleValue as FunctionComponent,
+            props: {},
+          },
+        };
+      }
+      const v: any = exampleValue;
+      if (
+        "component" in v &&
+        typeof v.component === "function" &&
+        "props" in v
+      ) {
+        return {
+          group,
+          component: componentName,
+          name: exampleName,
+          render: v,
+        };
+      }
+      throw Error(
+        `example "${exampleName}" from component "${componentName}" in group 
"${group}" doesn't follow one of the two ways of example`,
+      );
+    });
+  return {
+    name: componentName,
+    examples,
+  };
+}
+
+export function parseGroupImport(
+  groups: Record<string, ComponentOrFolder>,
+): Group[] {
+  return Object.entries(groups).map(([groupName, value]) => {
+    return {
+      title: groupName,
+      list: Object.entries(value).flatMap(([key, value]) =>
+        folder(groupName, value),
+      ),
+    };
+  });
+}
+
+export interface Group {
+  title: string;
+  list: ComponentItem[];
+}
+
+export interface ComponentItem {
+  name: string;
+  examples: ExampleItem[];
+}
+
+export interface ExampleItem {
+  group: string;
+  component: string;
+  name: string;
+  render: {
+    component: FunctionalComponent;
+    props: object;
+  };
+}
+
+type ComponentOrFolder = MaybeComponent | MaybeFolder;
+interface MaybeFolder {
+  default?: { title: string };
+  // [exampleName: string]: FunctionalComponent;
+}
+interface MaybeComponent {
+  // default?: undefined;
+  [exampleName: string]: undefined | object;
+}
+
+function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
+  let title: string | undefined = undefined;
+  try {
+    title =
+      typeof value === "object" &&
+      typeof value.default === "object" &&
+      value.default !== undefined &&
+      "title" in value.default &&
+      typeof value.default.title === "string"
+        ? value.default.title
+        : undefined;
+  } catch (e) {
+    throw Error(
+      `Could not defined if it is component or folder ${groupName}: 
${JSON.stringify(
+        value,
+        undefined,
+        2,
+      )}`,
+    );
+  }
+  if (title) {
+    const c = parseExampleImport(groupName, title, value as MaybeComponent);
+    return [c];
+  }
+  return Object.entries(value).flatMap(([subkey, value]) =>
+    folder(groupName, value),
+  );
+}
+
+interface Props {
+  getWrapperForGroup: (name: string) => FunctionComponent;
+  examplesInGroups: Group[];
+  langs: Record<string, object>;
+}
+
+function Application({
+  langs,
+  examplesInGroups,
+  getWrapperForGroup,
+}: Props): VNode {
+  const initialSelection = getSelectionFromLocationHash(
+    location.hash,
+    examplesInGroups,
+  );
+
+  const url = new URL(window.location.href);
+  const currentLang = url.searchParams.get("lang") || "en";
+
+  if (!langs["en"]) {
+    langs["en"] = {};
+  }
+  setupI18n(currentLang, langs);
+
+  const [selected, updateSelected] = useState<ExampleItem | undefined>(
+    initialSelection,
+  );
+  const [sidebarWidth, setSidebarWidth] = useState(200);
+  useEffect(() => {
+    if (location.hash) {
+      const hash = location.hash.substring(1);
+      const found = document.getElementById(hash);
+      if (found) {
+        setTimeout(() => {
+          found.scrollIntoView({
+            block: "center",
+          });
+        }, 10);
+      }
+    }
+  }, []);
+
+  const GroupWrapper = getWrapperForGroup(selected?.group || "default");
+  const ExampleContent = getContentForExample(selected, examplesInGroups);
+
+  //style={{ "--with-size": `${sidebarWidth}px` }}
+  return (
+    <Page>
+      {/* <LiveReload /> */}
+      <SideBar width={sidebarWidth}>
+        <div>
+          Language:
+          <select
+            value={currentLang}
+            onChange={(e) => {
+              const url = new URL(window.location.href);
+              url.searchParams.set("lang", e.currentTarget.value);
+              window.location.href = url.href;
+            }}
+          >
+            {Object.keys(langs).map((l) => (
+              <option key={l}>{l}</option>
+            ))}
+          </select>
+        </div>
+        {examplesInGroups.map((group) => (
+          <ExampleList
+            key={group.title}
+            name={group.title}
+            list={group.list}
+            selected={selected}
+            onSelectStory={(item, htmlId) => {
+              document.getElementById(htmlId)?.scrollIntoView({
+                block: "center",
+              });
+              updateSelected(item);
+            }}
+          />
+        ))}
+        <hr />
+      </SideBar>
+      <ResizeHandle
+        onUpdate={(x) => {
+          setSidebarWidth((s) => s + x);
+        }}
+      />
+      <Content>
+        <ErrorReport selected={selected}>
+          <PreventLinkNavigation>
+            <GroupWrapper>
+              <ExampleContent />
+            </GroupWrapper>
+          </PreventLinkNavigation>
+        </ErrorReport>
+      </Content>
+    </Page>
+  );
+}
+
+export interface Options {
+  id?: string;
+  strings?: any;
+  getWrapperForGroup?: (name: string) => FunctionComponent;
+}
+
+export function renderStories(
+  groups: Record<string, ComponentOrFolder>,
+  options: Options = {},
+): void {
+  const examples = parseGroupImport(groups);
+
+  try {
+    const cid = options.id ?? "container";
+    const container = document.getElementById(cid);
+    if (!container) {
+      throw Error(
+        `container with id ${cid} not found, can't mount page contents`,
+      );
+    }
+    render(
+      <Application
+        examplesInGroups={examples}
+        getWrapperForGroup={options.getWrapperForGroup ?? (() => Fragment)}
+        langs={options.strings ?? { en: {} }}
+      />,
+      container,
+    );
+  } catch (e) {
+    console.error("got error", e);
+    if (e instanceof Error) {
+      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
+    }
+  }
+}
+
+function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
+  const [start, setStart] = useState<number | undefined>(undefined);
+  return (
+    <ResizeHandleDiv
+      onMouseDown={(e: any) => {
+        setStart(e.pageX);
+        console.log("active", e.pageX);
+        return false;
+      }}
+      onMouseMove={(e: any) => {
+        if (start !== undefined) {
+          onUpdate(e.pageX - start);
+        }
+        return false;
+      }}
+      onMouseUp={() => {
+        setStart(undefined);
+        return false;
+      }}
+    />
+  );
+}
diff --git a/packages/pogen/example/proj1/tsconfig.json 
b/packages/web-util/tsconfig.json
similarity index 59%
copy from packages/pogen/example/proj1/tsconfig.json
copy to packages/web-util/tsconfig.json
index 30cb65e1d..aede0a0ac 100644
--- a/packages/pogen/example/proj1/tsconfig.json
+++ b/packages/web-util/tsconfig.json
@@ -1,27 +1,34 @@
 {
-  "compileOnSave": true,
   "compilerOptions": {
     "composite": true,
-    "declaration": true,
-    "declarationMap": false,
     "target": "ES6",
     "module": "ESNext",
-    "moduleResolution": "node",
+    "jsx": "react",
+    "jsxFactory": "h",
+    "jsxFragmentFactory": "Fragment",
+    "moduleResolution": "Node",
     "sourceMap": true,
-    "lib": ["es6"],
-    "types": ["node"],
+    "lib": [
+      "es6"
+    ],
+    "outDir": "lib",
+    "preserveSymlinks": true,
+    "skipLibCheck": true,
     "noImplicitReturns": true,
     "noFallthroughCasesInSwitch": true,
     "strict": true,
     "strictPropertyInitialization": false,
-    "outDir": "lib",
     "noImplicitAny": true,
     "noImplicitThis": true,
     "incremental": true,
     "esModuleInterop": true,
     "importHelpers": true,
     "rootDir": "./src",
-    "typeRoots": ["./node_modules/@types"]
+    "typeRoots": [
+      "./node_modules/@types"
+    ]
   },
-  "include": ["src/**/*"]
-}
+  "include": [
+    "src/**/*"
+  ]
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 06eab8ae5..efa3fb969 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,35 +46,34 @@ importers:
       '@creativebulma/bulma-tooltip': ^1.2.0
       '@gnu-taler/anastasis-core': workspace:*
       '@gnu-taler/taler-util': workspace:*
+      '@gnu-taler/web-util': workspace:*
       '@types/chai': ^4.3.0
       '@types/mocha': ^9.0.0
       bulma: ^0.9.3
       bulma-checkbox: ^1.1.1
       bulma-radio: ^1.1.1
       chai: ^4.3.6
-      chokidar: ^3.5.3
       date-fns: 2.29.2
-      eslint-plugin-header: ^3.1.1
       jed: 1.1.1
       jssha: ^3.2.0
       mocha: ^9.2.0
-      preact: ^10.5.15
+      preact: 10.11.3
       preact-render-to-string: ^5.1.19
       preact-router: ^3.2.1
       qrcode-generator: ^1.4.4
-      sass: 1.32.13
+      sass: 1.56.1
       typescript: ^4.8.4
-      ws: 7.4.5
     dependencies:
       '@gnu-taler/anastasis-core': link:../anastasis-core
       '@gnu-taler/taler-util': link:../taler-util
+      '@gnu-taler/web-util': link:../web-util
       '@types/chai': 4.3.3
       chai: 4.3.6
       date-fns: 2.29.2
       jed: 1.1.1
-      preact: 10.11.2
-      preact-render-to-string: 5.2.6_preact@10.11.2
-      preact-router: 3.2.1_preact@10.11.2
+      preact: 10.11.3
+      preact-render-to-string: 5.2.6_preact@10.11.3
+      preact-router: 3.2.1_preact@10.11.3
       qrcode-generator: 1.4.4
     devDependencies:
       '@creativebulma/bulma-tooltip': 1.2.0
@@ -82,19 +81,17 @@ importers:
       bulma: 0.9.4
       bulma-checkbox: 1.2.1
       bulma-radio: 1.2.0
-      chokidar: 3.5.3
-      eslint-plugin-header: 3.1.1
       jssha: 3.3.0
       mocha: 9.2.2
-      sass: 1.32.13
+      sass: 1.56.1
       typescript: 4.8.4
-      ws: 7.4.5
 
   packages/demobank-ui:
     specifiers:
       '@creativebulma/bulma-tooltip': ^1.2.0
       '@gnu-taler/pogen': ^0.0.5
       '@gnu-taler/taler-util': workspace:*
+      '@gnu-taler/web-util': workspace:*
       '@types/history': ^4.7.8
       '@typescript-eslint/eslint-plugin': ^5.41.0
       '@typescript-eslint/parser': ^5.41.0
@@ -103,29 +100,27 @@ importers:
       bulma-radio: ^1.1.1
       date-fns: 2.29.3
       esbuild: ^0.15.12
-      esbuild-sass-plugin: ^2.4.0
       eslint: ^8.26.0
       eslint-config-preact: ^1.2.0
       history: 4.10.1
       jed: 1.1.1
       po2json: ^0.4.5
-      preact: 10.6.5
+      preact: 10.11.3
       preact-router: 3.2.1
       qrcode-generator: ^1.4.4
-      react: npm:@preact/compat@^17.1.2
-      sass: 1.32.13
+      sass: 1.56.1
       swr: 1.3.0
       typescript: ^4.4.4
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
+      '@gnu-taler/web-util': link:../web-util
       date-fns: 2.29.3
       history: 4.10.1
       jed: 1.1.1
-      preact: 10.6.5
-      preact-router: 3.2.1_preact@10.6.5
+      preact: 10.11.3
+      preact-router: 3.2.1_preact@10.11.3
       qrcode-generator: 1.4.4
-      react: /@preact/compat/17.1.2_preact@10.6.5
-      swr: 1.3.0_@preact+compat@17.1.2
+      swr: 1.3.0
     devDependencies:
       '@creativebulma/bulma-tooltip': 1.2.0
       '@gnu-taler/pogen': link:../pogen
@@ -136,11 +131,10 @@ importers:
       bulma-checkbox: 1.2.1
       bulma-radio: 1.2.0
       esbuild: 0.15.12
-      esbuild-sass-plugin: 2.4.0
       eslint: 8.26.0
       eslint-config-preact: 1.3.0_fy74h4y2g2kkrxhvsefhiowl74
       po2json: 0.4.5
-      sass: 1.32.13
+      sass: 1.56.1
       typescript: 4.8.4
 
   packages/idb-bridge:
@@ -334,6 +328,7 @@ importers:
       '@creativebulma/bulma-tooltip': ^1.2.0
       '@gnu-taler/pogen': ^0.0.5
       '@gnu-taler/taler-util': workspace:*
+      '@gnu-taler/web-util': workspace:*
       '@storybook/addon-a11y': ^6.2.9
       '@storybook/addon-actions': ^6.2.9
       '@storybook/addon-essentials': ^6.2.9
@@ -390,6 +385,7 @@ importers:
       yup: ^0.32.9
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
+      '@gnu-taler/web-util': link:../web-util
       axios: 0.21.4
       date-fns: 2.29.3
       history: 4.10.1
@@ -625,6 +621,7 @@ importers:
       '@gnu-taler/pogen': workspace:*
       '@gnu-taler/taler-util': workspace:*
       '@gnu-taler/taler-wallet-core': workspace:*
+      '@gnu-taler/web-util': workspace:*
       '@linaria/babel-preset': 3.0.0-beta.22
       '@linaria/core': 3.0.0-beta.22
       '@linaria/react': 3.0.0-beta.22
@@ -646,7 +643,7 @@ importers:
       mocha: ^9.2.0
       nyc: ^15.1.0
       polished: ^4.1.4
-      preact: ^10.6.5
+      preact: 10.11.3
       preact-cli: ^3.3.5
       preact-render-to-string: ^5.1.19
       preact-router: 3.2.1
@@ -659,10 +656,11 @@ importers:
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
       '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
+      '@gnu-taler/web-util': link:../web-util
       date-fns: 2.29.3
       history: 4.10.1
-      preact: 10.11.2
-      preact-router: 3.2.1_preact@10.11.2
+      preact: 10.11.3
+      preact-router: 3.2.1_preact@10.11.3
       qr-scanner: 1.4.1
       qrcode-generator: 1.4.4
       tslib: 2.4.0
@@ -678,8 +676,8 @@ importers:
       '@linaria/core': 3.0.0-beta.22
       '@linaria/react': 3.0.0-beta.22
       '@linaria/webpack-loader': 3.0.0-beta.22
-      '@testing-library/preact': 2.0.1_preact@10.11.2
-      '@testing-library/preact-hooks': 1.1.0_aub6lnx45vk623d66chdvib7ry
+      '@testing-library/preact': 2.0.1_preact@10.11.3
+      '@testing-library/preact-hooks': 1.1.0_eng4adldpgibddgycwaukopxga
       '@types/chai': 4.3.3
       '@types/chrome': 0.0.197
       '@types/history': 4.7.11
@@ -693,11 +691,43 @@ importers:
       mocha: 9.2.2
       nyc: 15.1.0
       polished: 4.2.2
-      preact-cli: 3.4.1_mawv7s6bnnlf5iblnjjf6vghau
-      preact-render-to-string: 5.2.6_preact@10.11.2
+      preact-cli: 3.4.1_i2jslynuqxjzp37vlc24guk7gu
+      preact-render-to-string: 5.2.6_preact@10.11.3
       rimraf: 3.0.2
       typescript: 4.8.4
 
+  packages/web-util:
+    specifiers:
+      '@gnu-taler/taler-util': workspace:*
+      '@types/express': ^4.17.14
+      '@types/node': ^18.11.9
+      '@types/web': ^0.0.82
+      '@types/ws': ^8.5.3
+      chokidar: ^3.5.3
+      esbuild: ^0.14.21
+      express: ^4.18.2
+      preact: 10.11.3
+      prettier: ^2.5.1
+      rimraf: ^3.0.2
+      tslib: ^2.4.0
+      typescript: ^4.8.4
+      ws: 7.4.5
+    devDependencies:
+      '@gnu-taler/taler-util': link:../taler-util
+      '@types/express': 4.17.14
+      '@types/node': 18.11.9
+      '@types/web': 0.0.82
+      '@types/ws': 8.5.3
+      chokidar: 3.5.3
+      esbuild: 0.14.54
+      express: 4.18.2
+      preact: 10.11.3
+      prettier: 2.7.1
+      rimraf: 3.0.2
+      tslib: 2.4.1
+      typescript: 4.8.4
+      ws: 7.4.5
+
 packages:
 
   /@ampproject/remapping/2.2.0:
@@ -3450,7 +3480,7 @@ packages:
     engines: {node: '>= 10.14.2'}
     dependencies:
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       chalk: 4.1.2
       jest-message-util: 26.6.2
       jest-util: 26.6.2
@@ -3462,7 +3492,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       '@jest/types': 27.5.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       chalk: 4.1.2
       jest-message-util: 27.5.1
       jest-util: 27.5.1
@@ -3478,7 +3508,7 @@ packages:
       '@jest/test-result': 26.6.2
       '@jest/transform': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       exit: 0.1.2
@@ -3515,7 +3545,7 @@ packages:
     dependencies:
       '@jest/fake-timers': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       jest-mock: 26.6.2
     dev: true
 
@@ -3525,7 +3555,7 @@ packages:
     dependencies:
       '@jest/types': 26.6.2
       '@sinonjs/fake-timers': 6.0.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       jest-message-util: 26.6.2
       jest-mock: 26.6.2
       jest-util: 26.6.2
@@ -3672,7 +3702,7 @@ packages:
     dependencies:
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       '@types/yargs': 15.0.14
       chalk: 4.1.2
     dev: true
@@ -3683,7 +3713,7 @@ packages:
     dependencies:
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       '@types/yargs': 16.0.4
       chalk: 4.1.2
     dev: true
@@ -4077,6 +4107,17 @@ packages:
       preact: 10.11.2
     dev: true
 
+  /@preact/async-loader/3.0.1_preact@10.11.3:
+    resolution: {integrity: 
sha512-BoUN24hxEfAQYnWjliAmkZLuliv+ONQi7AWn+/+VOJHTIHmbFiXrvmSxITf7PDkKiK0a5xy4OErZtVVLlk96Tg==}
+    engines: {node: '>=8'}
+    peerDependencies:
+      preact: '>= 10.0.0'
+    dependencies:
+      kleur: 4.1.5
+      loader-utils: 2.0.3
+      preact: 10.11.3
+    dev: true
+
   /@preact/async-loader/3.0.1_preact@10.6.5:
     resolution: {integrity: 
sha512-BoUN24hxEfAQYnWjliAmkZLuliv+ONQi7AWn+/+VOJHTIHmbFiXrvmSxITf7PDkKiK0a5xy4OErZtVVLlk96Tg==}
     engines: {node: '>=8'}
@@ -4107,6 +4148,14 @@ packages:
       preact: 10.11.2
     dev: true
 
+  /@prefresh/core/1.4.1_preact@10.11.3:
+    resolution: {integrity: 
sha512-og1vaBj3LMJagVncNrDb37Gqc0cWaUcDbpVt5hZtsN4i2Iwzd/5hyTsDHvlMirhSym3wL9ihU0Xa2VhSaOue7g==}
+    peerDependencies:
+      preact: ^10.0.0
+    dependencies:
+      preact: 10.11.3
+    dev: true
+
   /@prefresh/core/1.4.1_preact@10.6.5:
     resolution: {integrity: 
sha512-og1vaBj3LMJagVncNrDb37Gqc0cWaUcDbpVt5hZtsN4i2Iwzd/5hyTsDHvlMirhSym3wL9ihU0Xa2VhSaOue7g==}
     peerDependencies:
@@ -4119,6 +4168,20 @@ packages:
     resolution: {integrity: 
sha512-Mb9abhJTOV4yCfkXrMrcgFiFT7MfNOw8sDa+XyZBdq/Ai2p4Zyxqsb3EgHLOEdHpMj6J9aiZ54W8H6FTam1u+A==}
     dev: true
 
+  /@prefresh/webpack/3.3.4_2ylwkgirq2zgmdyuhwfa4uhfgq:
+    resolution: {integrity: 
sha512-RiXS/hvXDup5cQw/267kxkKie81kxaAB7SFbkr8ppshobDEzwgUN1tbGbHNx6Uari0Ql2XByC6HIgQGpaq2Q7w==}
+    peerDependencies:
+      '@prefresh/babel-plugin': ^0.4.0
+      preact: ^10.4.0
+      webpack: ^4.0.0 || ^5.0.0
+    dependencies:
+      '@prefresh/babel-plugin': 0.4.4
+      '@prefresh/core': 1.4.1_preact@10.11.3
+      '@prefresh/utils': 1.1.3
+      preact: 10.11.3
+      webpack: 4.46.0
+    dev: true
+
   /@prefresh/webpack/3.3.4_kitpfapqi2defymxf2rxzdj6na:
     resolution: {integrity: 
sha512-RiXS/hvXDup5cQw/267kxkKie81kxaAB7SFbkr8ppshobDEzwgUN1tbGbHNx6Uari0Ql2XByC6HIgQGpaq2Q7w==}
     peerDependencies:
@@ -6859,6 +6922,16 @@ packages:
       preact: 10.11.2
     dev: true
 
+  /@testing-library/preact-hooks/1.1.0_eng4adldpgibddgycwaukopxga:
+    resolution: {integrity: 
sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==}
+    peerDependencies:
+      '@testing-library/preact': ^2.0.0
+      preact: ^10.4.8
+    dependencies:
+      '@testing-library/preact': 2.0.1_preact@10.11.3
+      preact: 10.11.3
+    dev: true
+
   /@testing-library/preact-hooks/1.1.0_vfcmu6iy7nffpurikpgxo6gwxi:
     resolution: {integrity: 
sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==}
     peerDependencies:
@@ -6879,6 +6952,16 @@ packages:
       preact: 10.11.2
     dev: true
 
+  /@testing-library/preact/2.0.1_preact@10.11.3:
+    resolution: {integrity: 
sha512-79kwVOY+3caoLgaPbiPzikjgY0Aya7Fc7TvGtR1upCnz2wrtmPDnN2t9vO7I7vDP2zoA+feSwOH5Q0BFErhaaQ==}
+    engines: {node: '>= 10'}
+    peerDependencies:
+      preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0'
+    dependencies:
+      '@testing-library/dom': 7.31.2
+      preact: 10.11.3
+    dev: true
+
   /@testing-library/preact/2.0.1_preact@10.6.5:
     resolution: {integrity: 
sha512-79kwVOY+3caoLgaPbiPzikjgY0Aya7Fc7TvGtR1upCnz2wrtmPDnN2t9vO7I7vDP2zoA+feSwOH5Q0BFErhaaQ==}
     engines: {node: '>= 10'}
@@ -6936,13 +7019,13 @@ packages:
     resolution: {integrity: 
sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
     dependencies:
       '@types/connect': 3.4.35
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/bonjour/3.5.10:
     resolution: {integrity: 
sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/chai/4.3.3:
@@ -6951,7 +7034,7 @@ packages:
   /@types/cheerio/0.22.31:
     resolution: {integrity: 
sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/chrome/0.0.197:
@@ -6965,13 +7048,13 @@ packages:
     resolution: {integrity: 
sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
     dependencies:
       '@types/express-serve-static-core': 4.17.31
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/connect/3.4.35:
     resolution: {integrity: 
sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/enzyme/3.10.12:
@@ -7010,7 +7093,7 @@ packages:
   /@types/express-serve-static-core/4.17.31:
     resolution: {integrity: 
sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       '@types/qs': 6.9.7
       '@types/range-parser': 1.2.4
     dev: true
@@ -7038,20 +7121,20 @@ packages:
     resolution: {integrity: 
sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/glob/8.0.0:
     resolution: {integrity: 
sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==}
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/graceful-fs/4.1.5:
     resolution: {integrity: 
sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/har-format/1.2.9:
@@ -7075,7 +7158,7 @@ packages:
   /@types/http-proxy/1.17.9:
     resolution: {integrity: 
sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/is-function/1.0.1:
@@ -7116,7 +7199,7 @@ packages:
   /@types/keyv/3.1.4:
     resolution: {integrity: 
sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/lodash/4.14.186:
@@ -7151,7 +7234,7 @@ packages:
   /@types/node-fetch/2.6.2:
     resolution: {integrity: 
sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       form-data: 3.0.1
     dev: true
 
@@ -7162,6 +7245,10 @@ packages:
   /@types/node/18.11.5:
     resolution: {integrity: 
sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==}
 
+  /@types/node/18.11.9:
+    resolution: {integrity: 
sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
+    dev: true
+
   /@types/normalize-package-data/2.4.1:
     resolution: {integrity: 
sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
     dev: true
@@ -7217,13 +7304,13 @@ packages:
   /@types/resolve/1.17.1:
     resolution: {integrity: 
sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/responselike/1.0.0:
     resolution: {integrity: 
sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/retry/0.12.0:
@@ -7248,13 +7335,13 @@ packages:
     resolution: {integrity: 
sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==}
     dependencies:
       '@types/mime': 3.0.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/sockjs/0.3.33:
     resolution: {integrity: 
sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/source-list-map/0.1.2:
@@ -7283,6 +7370,10 @@ packages:
     resolution: {integrity: 
sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
     dev: true
 
+  /@types/web/0.0.82:
+    resolution: {integrity: 
sha512-mktv7gA7V9mGKhAR9MXikOeNjsf3UptliH2yBFbNOqqbmse8II8irigyVJrW072ReHzPYSkJms9A5wZq3We5rw==}
+    dev: true
+
   /@types/webpack-env/1.18.0:
     resolution: {integrity: 
sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==}
     dev: true
@@ -7290,7 +7381,7 @@ packages:
   /@types/webpack-sources/3.2.0:
     resolution: {integrity: 
sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       '@types/source-list-map': 0.1.2
       source-map: 0.7.4
     dev: true
@@ -7298,7 +7389,7 @@ packages:
   /@types/webpack/4.41.33:
     resolution: {integrity: 
sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       '@types/tapable': 1.0.8
       '@types/uglify-js': 3.17.1
       '@types/webpack-sources': 3.2.0
@@ -7309,7 +7400,7 @@ packages:
   /@types/ws/8.5.3:
     resolution: {integrity: 
sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /@types/yargs-parser/21.0.0:
@@ -8001,7 +8092,7 @@ packages:
     engines: {node: '>=14.15.0'}
     dependencies:
       js-yaml: 3.14.1
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /@zkochan/js-yaml/0.0.6:
@@ -9611,7 +9702,7 @@ packages:
     resolution: {integrity: 
sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
     dependencies:
       pascal-case: 3.1.2
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /camelcase-css/2.0.1:
@@ -10265,7 +10356,7 @@ packages:
     dev: true
 
   /cookie-signature/1.0.6:
-    resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
+    resolution: {integrity: 
sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
     dev: true
 
   /cookie/0.5.0:
@@ -11279,7 +11370,7 @@ packages:
     resolution: {integrity: 
sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
     dependencies:
       no-case: 3.0.4
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /dot-prop/5.3.0:
@@ -11337,7 +11428,7 @@ packages:
     dev: true
 
   /ee-first/1.1.1:
-    resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
+    resolution: {integrity: 
sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
     dev: true
 
   /ejs-loader/0.5.0:
@@ -12030,14 +12121,6 @@ packages:
     dev: true
     optional: true
 
-  /esbuild-sass-plugin/2.4.0:
-    resolution: {integrity: 
sha512-fJOkKjvsDFQzaraM9G8p0JX+LfcP9DF4lxmbSNErza31d4u8+cv3k6vl5WT5O0+Ya56t+Auzy2cVksuyMy44lA==}
-    dependencies:
-      esbuild: 0.15.12
-      resolve: 1.22.1
-      sass: 1.55.0
-    dev: true
-
   /esbuild-sunos-64/0.14.54:
     resolution: {integrity: 
sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==}
     engines: {node: '>=12'}
@@ -12469,12 +12552,6 @@ packages:
       semver: 7.3.5
     dev: true
 
-  /eslint-plugin-header/3.1.1:
-    resolution: {integrity: 
sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==}
-    peerDependencies:
-      eslint: '>=7.7.0'
-    dev: true
-
   /eslint-plugin-header/3.1.1_eslint@7.32.0:
     resolution: {integrity: 
sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==}
     peerDependencies:
@@ -13605,7 +13682,7 @@ packages:
     dev: true
 
   /fresh/0.5.2:
-    resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=}
+    resolution: {integrity: 
sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
     engines: {node: '>= 0.6'}
     dev: true
 
@@ -15482,7 +15559,7 @@ packages:
       '@jest/environment': 26.6.2
       '@jest/fake-timers': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       jest-mock: 26.6.2
       jest-util: 26.6.2
       jsdom: 16.7.0
@@ -15500,7 +15577,7 @@ packages:
       '@jest/environment': 26.6.2
       '@jest/fake-timers': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       jest-mock: 26.6.2
       jest-util: 26.6.2
     dev: true
@@ -15516,7 +15593,7 @@ packages:
     dependencies:
       '@jest/types': 26.6.2
       '@types/graceful-fs': 4.1.5
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       anymatch: 3.1.2
       fb-watchman: 2.0.2
       graceful-fs: 4.2.10
@@ -15539,7 +15616,7 @@ packages:
     dependencies:
       '@jest/types': 27.5.1
       '@types/graceful-fs': 4.1.5
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       anymatch: 3.1.2
       fb-watchman: 2.0.2
       graceful-fs: 4.2.10
@@ -15562,7 +15639,7 @@ packages:
       '@jest/source-map': 26.6.2
       '@jest/test-result': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       chalk: 4.1.2
       co: 4.6.0
       expect: 26.6.2
@@ -15636,7 +15713,7 @@ packages:
     engines: {node: '>= 10.14.2'}
     dependencies:
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
     dev: true
 
   /jest-pnp-resolver/1.2.2_jest-resolve@26.6.2:
@@ -15742,7 +15819,7 @@ packages:
       '@jest/environment': 26.6.2
       '@jest/test-result': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       chalk: 4.1.2
       emittery: 0.7.2
       exit: 0.1.2
@@ -15810,7 +15887,7 @@ packages:
     resolution: {integrity: 
sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==}
     engines: {node: '>= 10.14.2'}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       graceful-fs: 4.2.10
     dev: true
 
@@ -15818,7 +15895,7 @@ packages:
     resolution: {integrity: 
sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==}
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       graceful-fs: 4.2.10
     dev: true
 
@@ -15851,7 +15928,7 @@ packages:
     engines: {node: '>= 10.14.2'}
     dependencies:
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       chalk: 4.1.2
       graceful-fs: 4.2.10
       is-ci: 2.0.0
@@ -15863,7 +15940,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       '@jest/types': 27.5.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       chalk: 4.1.2
       ci-info: 3.5.0
       graceful-fs: 4.2.10
@@ -15904,7 +15981,7 @@ packages:
     dependencies:
       '@jest/test-result': 26.6.2
       '@jest/types': 26.6.2
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       jest-util: 26.6.2
@@ -15917,7 +15994,7 @@ packages:
     dependencies:
       '@jest/test-result': 27.5.1
       '@jest/types': 27.5.1
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       jest-util: 27.5.1
@@ -15928,7 +16005,7 @@ packages:
     resolution: {integrity: 
sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
     engines: {node: '>= 10.13.0'}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       merge-stream: 2.0.0
       supports-color: 7.2.0
     dev: true
@@ -15937,7 +16014,7 @@ packages:
     resolution: {integrity: 
sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
     engines: {node: '>= 10.13.0'}
     dependencies:
-      '@types/node': 18.11.5
+      '@types/node': 18.11.9
       merge-stream: 2.0.0
       supports-color: 8.1.1
     dev: true
@@ -16446,7 +16523,7 @@ packages:
   /lower-case/2.0.2:
     resolution: {integrity: 
sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
     dependencies:
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /lowercase-keys/1.0.1:
@@ -16624,7 +16701,7 @@ packages:
     dev: true
 
   /media-typer/0.3.0:
-    resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
+    resolution: {integrity: 
sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
     engines: {node: '>= 0.6'}
     dev: true
 
@@ -16682,7 +16759,7 @@ packages:
     optional: true
 
   /merge-descriptors/1.0.1:
-    resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=}
+    resolution: {integrity: 
sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
     dev: true
 
   /merge-stream/2.0.0:
@@ -17102,7 +17179,7 @@ packages:
     resolution: {integrity: 
sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
     dependencies:
       lower-case: 2.0.2
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /node-addon-api/3.2.1:
@@ -17788,7 +17865,7 @@ packages:
     resolution: {integrity: 
sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
     dependencies:
       dot-case: 3.0.4
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /parent-module/1.0.1:
@@ -17884,7 +17961,7 @@ packages:
     resolution: {integrity: 
sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
     dependencies:
       no-case: 3.0.4
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /pascalcase/0.1.1:
@@ -17942,7 +18019,7 @@ packages:
     dev: true
 
   /path-to-regexp/0.1.7:
-    resolution: {integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=}
+    resolution: {integrity: 
sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
     dev: true
 
   /path-type/1.1.0:
@@ -18933,7 +19010,7 @@ packages:
       - webpack-command
     dev: true
 
-  /preact-cli/3.4.1_mawv7s6bnnlf5iblnjjf6vghau:
+  /preact-cli/3.4.1_i2jslynuqxjzp37vlc24guk7gu:
     resolution: {integrity: 
sha512-/4be0PuBmAIAox9u8GLJublFpEymq7Lk4JW4PEPz9ErFH/ncZf/oBPhECtXGq9IPqNOEe4r2l8sA+3uqKVwBfw==}
     engines: {node: '>=12'}
     hasBin: true
@@ -18960,9 +19037,9 @@ packages:
       '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
       '@babel/preset-env': 7.19.4_@babel+core@7.18.9
       '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
-      '@preact/async-loader': 3.0.1_preact@10.11.2
+      '@preact/async-loader': 3.0.1_preact@10.11.3
       '@prefresh/babel-plugin': 0.4.4
-      '@prefresh/webpack': 3.3.4_kitpfapqi2defymxf2rxzdj6na
+      '@prefresh/webpack': 3.3.4_2ylwkgirq2zgmdyuhwfa4uhfgq
       '@types/webpack': 4.41.33
       autoprefixer: 10.4.12_postcss@8.4.18
       babel-esm-plugin: 0.9.0_webpack@4.46.0
@@ -19000,8 +19077,8 @@ packages:
       postcss: 8.4.18
       postcss-load-config: 3.1.4_postcss@8.4.18
       postcss-loader: 4.3.0_dhonik3q6ff6ozbzdscnovq2ka
-      preact: 10.11.2
-      preact-render-to-string: 5.2.6_preact@10.11.2
+      preact: 10.11.3
+      preact-render-to-string: 5.2.6_preact@10.11.3
       progress-bar-webpack-plugin: 2.1.0_webpack@4.46.0
       promise-polyfill: 8.2.3
       prompts: 2.4.2
@@ -19185,6 +19262,15 @@ packages:
     dependencies:
       preact: 10.11.2
       pretty-format: 3.8.0
+    dev: true
+
+  /preact-render-to-string/5.2.6_preact@10.11.3:
+    resolution: {integrity: 
sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
+    peerDependencies:
+      preact: '>=10'
+    dependencies:
+      preact: 10.11.3
+      pretty-format: 3.8.0
 
   /preact-render-to-string/5.2.6_preact@10.6.5:
     resolution: {integrity: 
sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
@@ -19203,6 +19289,14 @@ packages:
       preact: 10.11.2
     dev: false
 
+  /preact-router/3.2.1_preact@10.11.3:
+    resolution: {integrity: 
sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==}
+    peerDependencies:
+      preact: '>=10'
+    dependencies:
+      preact: 10.11.3
+    dev: false
+
   /preact-router/3.2.1_preact@10.6.5:
     resolution: {integrity: 
sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==}
     peerDependencies:
@@ -19214,6 +19308,9 @@ packages:
   /preact/10.11.2:
     resolution: {integrity: 
sha512-skAwGDFmgxhq1DCBHke/9e12ewkhc7WYwjuhHB8HHS8zkdtITXLRmUMTeol2ldxvLwYtwbFeifZ9uDDWuyL4Iw==}
 
+  /preact/10.11.3:
+    resolution: {integrity: 
sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==}
+
   /preact/10.6.5:
     resolution: {integrity: 
sha512-i+LXM6JiVjQXSt2jG2vZZFapGpCuk1fl8o6ii3G84MA3xgj686FKjs4JFDkmUVhtxyq21+4ay74zqPykz9hU6w==}
 
@@ -20278,16 +20375,18 @@ packages:
       semver: 7.3.8
     dev: true
 
-  /sass/1.32.13:
-    resolution: {integrity: 
sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==}
-    engines: {node: '>=8.9.0'}
+  /sass/1.55.0:
+    resolution: {integrity: 
sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==}
+    engines: {node: '>=12.0.0'}
     hasBin: true
     dependencies:
       chokidar: 3.5.3
+      immutable: 4.1.0
+      source-map-js: 1.0.2
     dev: true
 
-  /sass/1.55.0:
-    resolution: {integrity: 
sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==}
+  /sass/1.56.1:
+    resolution: {integrity: 
sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==}
     engines: {node: '>=12.0.0'}
     hasBin: true
     dependencies:
@@ -21345,6 +21444,12 @@ packages:
       dequal: 2.0.2
     dev: false
 
+  /swr/1.3.0:
+    resolution: {integrity: 
sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
+    peerDependencies:
+      react: ^16.11.0 || ^17.0.0 || ^18.0.0
+    dev: false
+
   /swr/1.3.0_@preact+compat@17.1.2:
     resolution: {integrity: 
sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
     peerDependencies:
@@ -21755,7 +21860,7 @@ packages:
     resolution: {integrity: 
sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==}
     engines: {node: '>=8'}
     dependencies:
-      tslib: 2.4.0
+      tslib: 2.4.1
     dev: true
 
   /ts-pnp/1.2.0_typescript@4.4.4:
@@ -21798,6 +21903,10 @@ packages:
   /tslib/2.4.0:
     resolution: {integrity: 
sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
 
+  /tslib/2.4.1:
+    resolution: {integrity: 
sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
+    dev: true
+
   /tsutils/3.21.0_typescript@4.4.4:
     resolution: {integrity: 
sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
     engines: {node: '>= 6'}
@@ -22315,7 +22424,7 @@ packages:
     dev: true
 
   /utils-merge/1.0.1:
-    resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=}
+    resolution: {integrity: 
sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
     dev: true
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]