gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: add template from merchant ba


From: gnunet
Subject: [taler-wallet-core] branch master updated: add template from merchant backoffice
Date: Tue, 19 Oct 2021 16:05:56 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 5883d42d add template from merchant backoffice
5883d42d is described below

commit 5883d42d800c7b444c59d626bcaa5abca7dc83d0
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Oct 19 10:56:52 2021 -0300

    add template from merchant backoffice
---
 packages/anastasis-webui/package.json              |    9 +-
 .../src/assets/icons/languageicon.svg              |   48 +
 packages/anastasis-webui/src/assets/logo.jpeg      |  Bin 0 -> 39336 bytes
 packages/anastasis-webui/src/components/app.tsx    |   11 +-
 .../src/components/menu/LangSelector.tsx           |   73 ++
 .../src/components/menu/NavigationBar.tsx          |   58 ++
 .../src/components/menu/SideBar.tsx                |  101 ++
 .../anastasis-webui/src/components/menu/index.tsx  |  104 ++
 .../anastasis-webui/src/context/translation.ts     |   59 ++
 packages/anastasis-webui/src/hooks/index.ts        |  110 +++
 .../src/hooks/use-anastasis-reducer.ts             |    4 +-
 packages/anastasis-webui/src/i18n/index.tsx        |  203 ++++
 packages/anastasis-webui/src/i18n/poheader         |   27 +
 packages/anastasis-webui/src/i18n/strings-prelude  |   19 +
 packages/anastasis-webui/src/i18n/strings.ts       |   44 +
 .../anastasis-webui/src/i18n/taler-anastasis.pot   |   26 +
 packages/anastasis-webui/src/index.ts              |    2 +-
 .../src/pages/home/AttributeEntryScreen.tsx        |   55 ++
 .../src/pages/home/AuthMethodEmailSetup.tsx        |   41 +
 .../src/pages/home/AuthMethodPostSetup.tsx         |   68 ++
 .../src/pages/home/AuthMethodQuestionSetup.tsx     |   45 +
 .../src/pages/home/AuthMethodSmsSetup.tsx          |   50 +
 .../src/pages/home/AuthenticationEditorScreen.tsx  |  116 +++
 .../src/pages/home/BackupFinishedScreen.tsx        |   23 +
 .../src/pages/home/ChallengeOverviewScreen.tsx     |   63 ++
 .../src/pages/home/ContinentSelectionScreen.tsx    |   19 +
 .../src/pages/home/CountrySelectionScreen.tsx      |   23 +
 .../src/pages/home/PoliciesPayingScreen.tsx        |   27 +
 .../src/pages/home/RecoveryFinishedScreen.tsx      |   17 +
 .../src/pages/home/ReviewPoliciesScreen.tsx        |   43 +
 .../src/pages/home/SecretEditorScreen.tsx          |   53 +
 .../src/pages/home/SecretSelectionScreen.tsx       |   66 ++
 .../src/pages/home/SolveEmailEntry.tsx             |   22 +
 .../src/pages/home/SolvePostEntry.tsx              |   22 +
 .../src/pages/home/SolveQuestionEntry.tsx          |   22 +
 .../anastasis-webui/src/pages/home/SolveScreen.tsx |   41 +
 .../src/pages/home/SolveSmsEntry.tsx               |   22 +
 .../src/pages/home/SolveUnsupportedEntry.tsx       |   12 +
 .../anastasis-webui/src/pages/home/StartScreen.tsx |   14 +
 .../src/pages/home/TruthsPayingScreen.tsx          |   25 +
 packages/anastasis-webui/src/pages/home/index.tsx  |  248 +++++
 .../src/{routes => pages}/home/style.css           |    0
 .../src/{routes => pages}/notfound/index.tsx       |    3 +-
 .../src/{routes => pages}/notfound/style.css       |    0
 .../src/{routes => pages}/profile/index.tsx        |    3 +-
 .../src/{routes => pages}/profile/style.css        |    0
 packages/anastasis-webui/src/routes/home/index.tsx | 1025 --------------------
 .../anastasis-webui/src/scss/DurationPicker.scss   |   71 ++
 packages/anastasis-webui/src/scss/_aside.scss      |  186 ++++
 packages/anastasis-webui/src/scss/_card.scss       |   69 ++
 .../anastasis-webui/src/scss/_custom-calendar.scss |  254 +++++
 packages/anastasis-webui/src/scss/_footer.scss     |   35 +
 packages/anastasis-webui/src/scss/_form.scss       |   64 ++
 packages/anastasis-webui/src/scss/_hero-bar.scss   |   55 ++
 packages/anastasis-webui/src/scss/_loading.scss    |   51 +
 .../anastasis-webui/src/scss/_main-section.scss    |   24 +
 packages/anastasis-webui/src/scss/_misc.scss       |   50 +
 packages/anastasis-webui/src/scss/_mixins.scss     |   34 +
 packages/anastasis-webui/src/scss/_modal.scss      |   35 +
 packages/anastasis-webui/src/scss/_nav-bar.scss    |  144 +++
 packages/anastasis-webui/src/scss/_table.scss      |  173 ++++
 .../anastasis-webui/src/scss/_theme-default.scss   |  136 +++
 packages/anastasis-webui/src/scss/_tiles.scss      |   25 +
 packages/anastasis-webui/src/scss/_title-bar.scss  |   50 +
 .../src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf        |  Bin 0 -> 43752 bytes
 packages/anastasis-webui/src/scss/fonts/nunito.css |   22 +
 .../fonts/materialdesignicons-webfont-4.9.95.eot   |  Bin 0 -> 844600 bytes
 .../fonts/materialdesignicons-webfont-4.9.95.ttf   |  Bin 0 -> 844380 bytes
 .../fonts/materialdesignicons-webfont-4.9.95.woff  |  Bin 0 -> 404384 bytes
 .../fonts/materialdesignicons-webfont-4.9.95.woff2 |  Bin 0 -> 283040 bytes
 .../scss/icons/materialdesignicons-4.9.95.min.css  |    3 +
 packages/anastasis-webui/src/scss/libs/_all.scss   |   29 +
 packages/anastasis-webui/src/scss/main.scss        |  191 ++++
 packages/anastasis-webui/src/template.html         |    2 +-
 pnpm-lock.yaml                                     |  196 +++-
 75 files changed, 3917 insertions(+), 1048 deletions(-)

diff --git a/packages/anastasis-webui/package.json 
b/packages/anastasis-webui/package.json
index 78d8671b..8f771131 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -23,15 +23,20 @@
   "dependencies": {
     "@gnu-taler/taler-util": "workspace:^0.8.3",
     "anastasis-core": "workspace:^0.0.1",
+    "jed": "1.1.1",
     "preact": "^10.3.1",
     "preact-render-to-string": "^5.1.4",
     "preact-router": "^3.2.1"
   },
   "devDependencies": {
+    "@creativebulma/bulma-tooltip": "^1.2.0",
     "@types/enzyme": "^3.10.5",
     "@types/jest": "^26.0.8",
     "@typescript-eslint/eslint-plugin": "^2.25.0",
     "@typescript-eslint/parser": "^2.25.0",
+    "bulma": "^0.9.3",
+    "bulma-checkbox": "^1.1.1",
+    "bulma-radio": "^1.1.1",
     "enzyme": "^3.11.0",
     "enzyme-adapter-preact-pure": "^3.1.0",
     "eslint": "^6.8.0",
@@ -39,6 +44,8 @@
     "jest": "^26.2.2",
     "jest-preset-preact": "^4.0.2",
     "preact-cli": "^3.2.2",
+    "sass": "^1.32.13",
+    "sass-loader": "^10.1.1",
     "sirv-cli": "^1.0.0-next.3",
     "typescript": "^3.7.5"
   },
@@ -49,4 +56,4 @@
       "<rootDir>/tests/__mocks__/setupTests.ts"
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/assets/icons/languageicon.svg 
b/packages/anastasis-webui/src/assets/icons/languageicon.svg
new file mode 100644
index 00000000..22d58da6
--- /dev/null
+++ b/packages/anastasis-webui/src/assets/icons/languageicon.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 
6.00 Build 0)  -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; x="0px" y="0px"
+        viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 
2794;" xml:space="preserve">
+<style type="text/css">
+       .st0{fill:#FFFFFF;}
+       .st1{fill-rule:evenodd;clip-rule:evenodd;}
+       .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+       <g>
+               <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 
1204.6,1758.3               "/>
+               <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 
1182.2,1757.3           "/>
+               <polygon class="st0" points="30,2415.4 1182.2,2031.4 
1182.2,357.9 30,742                "/>
+               <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8      
        "/>
+               <g>
+                       <path 
d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+                               
c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+                               
c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+                       <path 
d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+                               
c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+                               
c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+                               
c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+                               
c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+                               
c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+                               
c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+                               C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+               </g>
+               <path class="st1" 
d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+                       
c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+                       
c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+                       
c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+                       
c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+               <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z 
M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+                       
c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+               <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z 
M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+                       
c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+                       
c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+               <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 
2411.2,757.2          "/>
+               <g>
+                       <path class="st2" 
d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z 
M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+                               
l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+               </g>
+       </g>
+</g>
+</svg>
diff --git a/packages/anastasis-webui/src/assets/logo.jpeg 
b/packages/anastasis-webui/src/assets/logo.jpeg
new file mode 100644
index 00000000..489832f7
Binary files /dev/null and b/packages/anastasis-webui/src/assets/logo.jpeg 
differ
diff --git a/packages/anastasis-webui/src/components/app.tsx 
b/packages/anastasis-webui/src/components/app.tsx
index 45c9035f..c6b4cfc1 100644
--- a/packages/anastasis-webui/src/components/app.tsx
+++ b/packages/anastasis-webui/src/components/app.tsx
@@ -1,12 +1,15 @@
 import { FunctionalComponent, h } from "preact";
+import { TranslationProvider } from "../context/translation";
 
-import AnastasisClient from "../routes/home";
+import AnastasisClient from "../pages/home";
 
 const App: FunctionalComponent = () => {
   return (
-    <div id="preact_root">
-      <AnastasisClient />
-    </div>
+    <TranslationProvider>
+      <div id="app" class="has-navbar-fixed-top">
+        <AnastasisClient />
+      </div>
+    </TranslationProvider>
   );
 };
 
diff --git a/packages/anastasis-webui/src/components/menu/LangSelector.tsx 
b/packages/anastasis-webui/src/components/menu/LangSelector.tsx
new file mode 100644
index 00000000..41d08a58
--- /dev/null
+++ b/packages/anastasis-webui/src/components/menu/LangSelector.tsx
@@ -0,0 +1,73 @@
+/*
+ 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/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import langIcon from '../../assets/icons/languageicon.svg';
+import { useTranslationContext } from "../../context/translation";
+import { strings as messages } from '../../i18n/strings'
+
+type LangsNames = {
+  [P in keyof typeof messages]: string
+}
+
+const names: LangsNames = {
+  es: 'Español [es]',
+  en: 'English [en]',
+  fr: 'Français [fr]',
+  de: 'Deutsch [de]',
+  sv: 'Svenska [sv]',
+  it: 'Italiano [it]',
+}
+
+function getLangName(s: keyof LangsNames | string) {
+  if (names[s]) return names[s]
+  return s
+}
+
+export function LangSelector(): VNode {
+  const [updatingLang, setUpdatingLang] = useState(false)
+  const { lang, changeLanguage } = useTranslationContext()
+
+  return <div class="dropdown is-active ">
+    <div class="dropdown-trigger">
+      <button class="button has-tooltip-left" 
+        data-tooltip="change language selection"
+        aria-haspopup="true" 
+        aria-controls="dropdown-menu" onClick={() => 
setUpdatingLang(!updatingLang)}>
+        <div class="icon is-small is-left">
+          <img src={langIcon} />
+        </div>
+        <span>{getLangName(lang)}</span>
+        <div class="icon is-right">
+          <i class="mdi mdi-chevron-down" />
+        </div>
+      </button>
+    </div>
+    {updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
+      <div class="dropdown-content">
+        {Object.keys(messages)
+          .filter((l) => l !== lang)
+          .map(l => <a key={l} class="dropdown-item" value={l} onClick={() => 
{ changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
+      </div>
+    </div>}
+  </div>
+}
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx 
b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
new file mode 100644
index 00000000..e1bb4c7c
--- /dev/null
+++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
@@ -0,0 +1,58 @@
+/*
+ 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/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from 'preact';
+import logo from '../../assets/logo.jpeg';
+import { LangSelector } from './LangSelector';
+
+interface Props {
+  onMobileMenu: () => void;
+  title: string;
+}
+
+export function NavigationBar({ onMobileMenu, title }: Props): VNode {
+  return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main 
navigation">
+    <div class="navbar-brand">
+      <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 
}}>{title}</span>
+
+      <a role="button" class="navbar-burger" aria-label="menu" 
aria-expanded="false" onClick={(e) => {
+        onMobileMenu()
+        e.stopPropagation()
+      }}>
+        <span aria-hidden="true" />
+        <span aria-hidden="true" />
+        <span aria-hidden="true" />
+      </a>
+    </div>
+
+    <div class="navbar-menu ">
+      <a class="navbar-start is-justify-content-center is-flex-grow-1" 
href="https://taler.net";>
+        <img src={logo} style={{ height: 50, maxHeight: 50 }} />
+      </a>
+      <div class="navbar-end">
+        <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
+          <LangSelector />
+        </div>
+      </div>
+    </div>
+  </nav>
+  );
+}
\ 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
new file mode 100644
index 00000000..628adb57
--- /dev/null
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -0,0 +1,101 @@
+/*
+ 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/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+
+import { h, VNode } from 'preact';
+import { Translate } from '../../i18n';
+import { LangSelector } from './LangSelector';
+
+interface Props {
+  mobile?: boolean;
+}
+
+export function Sidebar({ mobile }: Props): VNode {
+  // const config = useConfigContext();
+  const config = { version: 'none' }
+  const process = { env : { __VERSION__: '0.0.0'}}
+  
+  return (
+    <aside class="aside is-placed-left is-expanded">
+      {mobile && <div class="footer" onClick={(e) => { return 
e.stopImmediatePropagation() }}>
+        <LangSelector />
+      </div>}
+      <div class="aside-tools">
+        <div class="aside-tools-label">
+          <div><b>Anastasis</b> Reducer</div>
+          <div class="is-size-7 has-text-right" style={{ lineHeight: 0, 
marginTop: -10 }}>
+            {process.env.__VERSION__} ({config.version})
+          </div>
+        </div>
+      </div>
+      <div class="menu is-menu-main">
+        <p class="menu-label">
+          <Translate>Back up a secret</Translate>
+        </p>
+        <ul class="menu-list">
+          <li>
+            <div class="has-icon">
+              <span class="icon"><i class="mdi mdi-square-edit-outline" 
/></span>
+              <span class="menu-item-label"><Translate>Location &amp; 
Currency</Translate></span>
+            </div>
+          </li>
+          <li class="is-active">
+            <div class="has-icon">
+              <span class="icon"><i class="mdi mdi-cash-register" /></span>
+              <span class="menu-item-label"><Translate>Personal 
information</Translate></span>
+            </div>
+          </li>
+          <li>
+            <div class="has-icon">
+              <span class="icon"><i class="mdi mdi-shopping" /></span>
+              <span class="menu-item-label"><Translate>Authorization 
methods</Translate></span>
+            </div>
+          </li>
+          <li>
+            <div  class="has-icon">
+              <span class="icon"><i class="mdi mdi-bank" /></span>
+              <span class="menu-item-label"><Translate>Recovery 
policies</Translate></span>
+            </div>
+          </li>
+          <li>
+            <div  class="has-icon">
+              <span class="icon"><i class="mdi mdi-bank" /></span>
+              <span class="menu-item-label"><Translate>Enter 
secrets</Translate></span>
+            </div>
+          </li>
+          <li>
+            <div  class="has-icon">
+              <span class="icon"><i class="mdi mdi-bank" /></span>
+              <span class="menu-item-label"><Translate>Payment 
(optional)</Translate></span>
+            </div>
+          </li>
+          <li>
+            <div  class="has-icon">
+              <span class="icon"><i class="mdi mdi-cash" /></span>
+              <span class="menu-item-label">Backup completed</span>
+            </div>
+          </li>
+        </ul>
+      </div>
+    </aside>
+  );
+}
+
diff --git a/packages/anastasis-webui/src/components/menu/index.tsx 
b/packages/anastasis-webui/src/components/menu/index.tsx
new file mode 100644
index 00000000..d15bf926
--- /dev/null
+++ b/packages/anastasis-webui/src/components/menu/index.tsx
@@ -0,0 +1,104 @@
+/*
+ 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/>
+ */
+
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import Match from 'preact-router/match';
+import { useEffect, useState } from "preact/hooks";
+import { NavigationBar } from "./NavigationBar";
+import { Sidebar } from "./SideBar";
+
+
+
+
+interface MenuProps {
+  title: string;
+}
+
+function WithTitle({ title, children }: { title: string; children: 
ComponentChildren }): VNode {
+  useEffect(() => {
+    document.title = `Taler Backoffice: ${title}`
+  }, [title])
+  return <Fragment>{children}</Fragment>
+}
+
+export function Menu({ title }: MenuProps): VNode {
+  const [mobileOpen, setMobileOpen] = useState(false)
+
+  return <Match>{({ path }: { path: string }) => {
+    const titleWithSubtitle = title // title ? title : (!admin ? 
getInstanceTitle(path, instance) : getAdminTitle(path, instance))
+    return (<WithTitle title={titleWithSubtitle}>
+      <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() 
=> setMobileOpen(false)}>
+        <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} 
title={titleWithSubtitle} />
+
+        <Sidebar mobile={mobileOpen} />
+
+      </div>
+    </WithTitle>
+    )
+  }}</Match>
+
+}
+
+interface NotYetReadyAppMenuProps {
+  title: string;
+  onLogout?: () => void;
+}
+
+interface NotifProps {
+  notification?: Notification;
+}
+export function NotificationCard({ notification: n }: NotifProps): VNode | 
null {
+  if (!n) return null
+  return <div class="notification">
+    <div class="columns is-vcentered">
+      <div class="column is-12">
+        <article class={n.type === 'ERROR' ? "message is-danger" : (n.type === 
'WARN' ? "message is-warning" : "message is-info")}>
+          <div class="message-header">
+            <p>{n.message}</p>
+          </div>
+          {n.description &&
+            <div class="message-body">
+              {n.description}
+            </div>}
+        </article>
+      </div>
+    </div>
+  </div>
+}
+
+export function NotYetReadyAppMenu({ onLogout, title }: 
NotYetReadyAppMenuProps): VNode {
+  const [mobileOpen, setMobileOpen] = useState(false)
+
+  useEffect(() => {
+    document.title = `Taler Backoffice: ${title}`
+  }, [title])
+
+  return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} 
onClick={() => setMobileOpen(false)}>
+    <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} 
title={title} />
+    {onLogout && <Sidebar onLogout={onLogout} mobile={mobileOpen} />}
+  </div>
+
+}
+
+export interface Notification {
+  message: string;
+  description?: string | VNode;
+  type: MessageType;
+}
+
+export type ValueOrFunction<T> = T | ((p: T) => T)
+export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
+
diff --git a/packages/anastasis-webui/src/context/translation.ts 
b/packages/anastasis-webui/src/context/translation.ts
new file mode 100644
index 00000000..f724c691
--- /dev/null
+++ b/packages/anastasis-webui/src/context/translation.ts
@@ -0,0 +1,59 @@
+/*
+ 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/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createContext, h, VNode } from 'preact'
+import { useContext, useEffect } from 'preact/hooks'
+import { useLang } from '../hooks'
+import * as jedLib from "jed";
+import { strings } from "../i18n/strings";
+
+interface Type {
+  lang: string;
+  handler: any;
+  changeLanguage: (l: string) => void;
+}
+const initial = {
+  lang: 'en',
+  handler: null,
+  changeLanguage: () => {
+    // do not change anything
+  }
+}
+const Context = createContext<Type>(initial)
+
+interface Props {
+  initial?: string;
+  children: any;
+  forceLang?: string;
+}
+
+export const TranslationProvider = ({ initial, children, forceLang }: Props): 
VNode => {
+  const [lang, changeLanguage] = useLang(initial)
+  useEffect(() => {
+    if (forceLang) {
+      changeLanguage(forceLang)
+    }
+  })
+  const handler = new jedLib.Jed(strings[lang]);
+  return h(Context.Provider, { value: { lang, handler, changeLanguage }, 
children });
+}
+
+export const useTranslationContext = (): Type => useContext(Context);
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/hooks/index.ts 
b/packages/anastasis-webui/src/hooks/index.ts
new file mode 100644
index 00000000..15df4f15
--- /dev/null
+++ b/packages/anastasis-webui/src/hooks/index.ts
@@ -0,0 +1,110 @@
+/*
+ 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/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { StateUpdater, useState } from "preact/hooks";
+export type ValueOrFunction<T> = T | ((p: T) => T)
+
+
+const calculateRootPath = () => {
+  const rootPath = typeof window !== undefined ? window.location.origin + 
window.location.pathname : '/'
+  return rootPath
+}
+
+export function useBackendURL(url?: string): [string, boolean, 
StateUpdater<string>, () => void] {
+  const [value, setter] = useNotNullLocalStorage('backend-url', url || 
calculateRootPath())
+  const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
+
+  const checkedSetter = (v: ValueOrFunction<string>) => {
+    setTriedToLog('yes')
+    return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
+  }
+
+  const resetBackend = () => {
+    setTriedToLog(undefined)
+  }
+  return [value, !!triedToLog, checkedSetter, resetBackend]
+}
+
+export function useBackendDefaultToken(): [string | undefined, 
StateUpdater<string | undefined>] {
+  return useLocalStorage('backend-token')
+}
+
+export function useBackendInstanceToken(id: string): [string | undefined, 
StateUpdater<string | undefined>] {
+  const [token, setToken] = useLocalStorage(`backend-token-${id}`)
+  const [defaultToken, defaultSetToken] = useBackendDefaultToken()
+
+  // instance named 'default' use the default token
+  if (id === 'default') {
+    return [defaultToken, defaultSetToken]
+  }
+
+  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];
+}
+
+
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts 
b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index be68ba6e..72424e82 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -3,7 +3,7 @@ import { BackupStates, getBackupStartState, 
getRecoveryStartState, RecoveryState
 import { useState } from "preact/hooks";
 
 const reducerBaseUrl = "http://localhost:5000/";;
-let remoteReducer = true;
+const remoteReducer = true;
 
 interface AnastasisState {
   reducerState: ReducerState | undefined;
@@ -123,7 +123,7 @@ function storageSet(key: string, value: any): void {
 function restoreState(): any {
   let state: any;
   try {
-    let s = storageGet("anastasisReducerState");
+    const s = storageGet("anastasisReducerState");
     if (s === "undefined") {
       state = undefined;
     } else if (s) {
diff --git a/packages/anastasis-webui/src/i18n/index.tsx 
b/packages/anastasis-webui/src/i18n/index.tsx
new file mode 100644
index 00000000..63c8e193
--- /dev/null
+++ b/packages/anastasis-webui/src/i18n/index.tsx
@@ -0,0 +1,203 @@
+/*
+ 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/anastasis-webui/src/i18n/poheader 
b/packages/anastasis-webui/src/i18n/poheader
new file mode 100644
index 00000000..ee3fcd7b
--- /dev/null
+++ b/packages/anastasis-webui/src/i18n/poheader
@@ -0,0 +1,27 @@
+#  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/>
+
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/anastasis-webui/src/i18n/strings-prelude 
b/packages/anastasis-webui/src/i18n/strings-prelude
new file mode 100644
index 00000000..cca13afa
--- /dev/null
+++ b/packages/anastasis-webui/src/i18n/strings-prelude
@@ -0,0 +1,19 @@
+/*
+ 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/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: {[s: string]: any} = {};
+
diff --git a/packages/anastasis-webui/src/i18n/strings.ts 
b/packages/anastasis-webui/src/i18n/strings.ts
new file mode 100644
index 00000000..b4f376ce
--- /dev/null
+++ b/packages/anastasis-webui/src/i18n/strings.ts
@@ -0,0 +1,44 @@
+/*
+ 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/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: {[s: string]: any} = {};
+
+strings['de'] = {
+  "domain": "messages",
+  "locale_data": {
+    "messages": {
+      "": {
+        "domain": "messages",
+        "plural_forms": "nplurals=2; plural=(n != 1);",
+        "lang": ""
+      },
+    }
+  }
+};
+
+strings['en'] = {
+  "domain": "messages",
+  "locale_data": {
+    "messages": {
+      "": {
+        "domain": "messages",
+        "plural_forms": "nplurals=2; plural=(n != 1);",
+        "lang": ""
+      },
+    }
+  }
+};
diff --git a/packages/anastasis-webui/src/i18n/taler-anastasis.pot 
b/packages/anastasis-webui/src/i18n/taler-anastasis.pot
new file mode 100644
index 00000000..b8c3be80
--- /dev/null
+++ b/packages/anastasis-webui/src/i18n/taler-anastasis.pot
@@ -0,0 +1,26 @@
+#  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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Anastasis\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
diff --git a/packages/anastasis-webui/src/index.ts 
b/packages/anastasis-webui/src/index.ts
index 3b3f7844..e78b9c19 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -1,4 +1,4 @@
-import './style/index.css';
 import App from './components/app';
+import './scss/main.scss';
 
 export default App;
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx 
b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
new file mode 100644
index 00000000..4df99db9
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -0,0 +1,55 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AnastasisReducerApi, ReducerStateRecovery, ReducerStateBackup } from 
"../../hooks/use-anastasis-reducer";
+import { AnastasisClientFrame, withProcessLabel, LabeledInput } from "./index";
+
+export function AttributeEntryScreen(props: AttributeEntryProps): VNode {
+  const { reducer, reducerState: backupState } = props;
+  const [attrs, setAttrs] = useState<Record<string, string>>(
+    props.reducerState.identity_attributes ?? {}
+  );
+  return (
+    <AnastasisClientFrame
+      title={withProcessLabel(reducer, "Select Country")}
+      onNext={() => reducer.transition("enter_user_attributes", {
+        identity_attributes: attrs,
+      })}
+    >
+      {backupState.required_attributes.map((x: any, i: number) => {
+        return (
+          <AttributeEntryField
+            key={i}
+            isFirst={i == 0}
+            setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
+            spec={x}
+            value={attrs[x.name]} />
+        );
+      })}
+    </AnastasisClientFrame>
+  );
+}
+
+interface AttributeEntryProps {
+  reducer: AnastasisReducerApi;
+  reducerState: ReducerStateRecovery | ReducerStateBackup;
+}
+
+export interface AttributeEntryFieldProps {
+  isFirst: boolean;
+  value: string;
+  setValue: (newValue: string) => void;
+  spec: any;
+}
+
+export function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
+  return (
+    <div>
+      <LabeledInput
+        grabFocus={props.isFirst}
+        label={props.spec.label}
+        bind={[props.value, props.setValue]}
+      />
+    </div>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx 
b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx
new file mode 100644
index 00000000..9aa6855f
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx
@@ -0,0 +1,41 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+  encodeCrock,
+  stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethodSetupProps, AnastasisClientFrame, LabeledInput } from 
"./index";
+
+export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
+  const [email, setEmail] = useState("");
+  return (
+    <AnastasisClientFrame hideNav title="Add email authentication">
+      <p>
+        For email authentication, you need to provide an email address. When
+        recovering your secret, you will need to enter the code you receive by
+        email.
+      </p>
+      <div>
+        <LabeledInput
+          label="Email address"
+          grabFocus
+          bind={[email, setEmail]} />
+      </div>
+      <div>
+        <button onClick={() => props.cancel()}>Cancel</button>
+        <button
+          onClick={() => props.addAuthMethod({
+            authentication_method: {
+              type: "email",
+              instructions: `Email to ${email}`,
+              challenge: encodeCrock(stringToBytes(email)),
+            },
+          })}
+        >
+          Add
+        </button>
+      </div>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx 
b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx
new file mode 100644
index 00000000..43dcde33
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx
@@ -0,0 +1,68 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+  canonicalJson, encodeCrock,
+  stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethodSetupProps, LabeledInput } from "./index";
+
+export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {
+  const [fullName, setFullName] = useState("");
+  const [street, setStreet] = useState("");
+  const [city, setCity] = useState("");
+  const [postcode, setPostcode] = useState("");
+  const [country, setCountry] = useState("");
+
+  const addPostAuth = () => {
+    const challengeJson = {
+      full_name: fullName,
+      street,
+      city,
+      postcode,
+      country,
+    };
+    props.addAuthMethod({
+      authentication_method: {
+        type: "email",
+        instructions: `Letter to address in postal code ${postcode}`,
+        challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
+      },
+    });
+  };
+
+  return (
+    <div class={style.home}>
+      <h1>Add {props.method} authentication</h1>
+      <div>
+        <p>
+          For postal letter authentication, you need to provide a postal
+          address. When recovering your secret, you will be asked to enter a
+          code that you will receive in a letter to that address.
+        </p>
+        <div>
+          <LabeledInput
+            grabFocus
+            label="Full Name"
+            bind={[fullName, setFullName]} />
+        </div>
+        <div>
+          <LabeledInput label="Street" bind={[street, setStreet]} />
+        </div>
+        <div>
+          <LabeledInput label="City" bind={[city, setCity]} />
+        </div>
+        <div>
+          <LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
+        </div>
+        <div>
+          <LabeledInput label="Country" bind={[country, setCountry]} />
+        </div>
+        <div>
+          <button onClick={() => props.cancel()}>Cancel</button>
+          <button onClick={() => addPostAuth()}>Add</button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git 
a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx 
b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx
new file mode 100644
index 00000000..7a0da7eb
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx
@@ -0,0 +1,45 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+  encodeCrock,
+  stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethodSetupProps, AnastasisClientFrame, LabeledInput } from 
"./index";
+
+export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
+  const [questionText, setQuestionText] = useState("");
+  const [answerText, setAnswerText] = useState("");
+  const addQuestionAuth = (): void => props.addAuthMethod({
+    authentication_method: {
+      type: "question",
+      instructions: questionText,
+      challenge: encodeCrock(stringToBytes(answerText)),
+    },
+  });
+  return (
+    <AnastasisClientFrame hideNav title="Add Security Question">
+      <div>
+        <p>
+          For security question authentication, you need to provide a question
+          and its answer. When recovering your secret, you will be shown the
+          question and you will need to type the answer exactly as you typed it
+          here.
+        </p>
+        <div>
+          <LabeledInput
+            label="Security question"
+            grabFocus
+            bind={[questionText, setQuestionText]} />
+        </div>
+        <div>
+          <LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
+        </div>
+        <div>
+          <button onClick={() => props.cancel()}>Cancel</button>
+          <button onClick={() => addQuestionAuth()}>Add</button>
+        </div>
+      </div>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx 
b/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx
new file mode 100644
index 00000000..d193f6eb
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx
@@ -0,0 +1,50 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+  encodeCrock,
+  stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState, useRef, useLayoutEffect } from "preact/hooks";
+import { AuthMethodSetupProps, AnastasisClientFrame } from "./index";
+
+export function AuthMethodSmsSetup(props: AuthMethodSetupProps): VNode {
+  const [mobileNumber, setMobileNumber] = useState("");
+  const addSmsAuth = (): void => {
+    props.addAuthMethod({
+      authentication_method: {
+        type: "sms",
+        instructions: `SMS to ${mobileNumber}`,
+        challenge: encodeCrock(stringToBytes(mobileNumber)),
+      },
+    });
+  };
+  const inputRef = useRef<HTMLInputElement>(null);
+  useLayoutEffect(() => {
+    inputRef.current?.focus();
+  }, []);
+  return (
+    <AnastasisClientFrame hideNav title="Add SMS authentication">
+      <div>
+        <p>
+          For SMS authentication, you need to provide a mobile number. When
+          recovering your secret, you will be asked to enter the code you
+          receive via SMS.
+        </p>
+        <label>
+          Mobile number:{" "}
+          <input
+            value={mobileNumber}
+            ref={inputRef}
+            style={{ display: "block" }}
+            autoFocus
+            onChange={(e) => setMobileNumber((e.target as any).value)}
+            type="text" />
+        </label>
+        <div>
+          <button onClick={() => props.cancel()}>Cancel</button>
+          <button onClick={() => addSmsAuth()}>Add</button>
+        </div>
+      </div>
+    </AnastasisClientFrame>
+  );
+}
diff --git 
a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx 
b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
new file mode 100644
index 00000000..5357891a
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -0,0 +1,116 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethod, ReducerStateBackup } from "anastasis-core";
+import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
+import { AuthMethodEmailSetup } from "./AuthMethodEmailSetup";
+import { AuthMethodPostSetup } from "./AuthMethodPostSetup";
+import { AuthMethodQuestionSetup } from "./AuthMethodQuestionSetup";
+import { AuthMethodSmsSetup } from "./AuthMethodSmsSetup";
+import { AnastasisClientFrame } from "./index";
+
+export function AuthenticationEditorScreen(props: AuthenticationEditorProps): 
VNode {
+  const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
+    undefined
+  );
+  const { reducer, backupState } = props;
+  const providers = backupState.authentication_providers!;
+  const authAvailableSet = new Set<string>();
+  for (const provKey of Object.keys(providers)) {
+    const p = providers[provKey];
+    if ("http_status" in p && (!("error_code" in p)) && p.methods) {
+      for (const meth of p.methods) {
+        authAvailableSet.add(meth.type);
+      }
+    }
+  }
+  if (selectedMethod) {
+    const cancel = (): void => setSelectedMethod(undefined);
+    const addMethod = (args: any): void => {
+      reducer.transition("add_authentication", args);
+      setSelectedMethod(undefined);
+    };
+    const methodMap: Record<
+      string, (props: AuthMethodSetupProps) => h.JSX.Element
+    > = {
+      sms: AuthMethodSmsSetup,
+      question: AuthMethodQuestionSetup,
+      email: AuthMethodEmailSetup,
+      post: AuthMethodPostSetup,
+    };
+    const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
+    return (
+      <AuthSetup
+        cancel={cancel}
+        addAuthMethod={addMethod}
+        method={selectedMethod} />
+    );
+  }
+  function MethodButton(props: { method: string; label: string }): VNode {
+    return (
+      <button
+        disabled={!authAvailableSet.has(props.method)}
+        onClick={() => {
+          setSelectedMethod(props.method);
+          reducer.dismissError();
+        }}
+      >
+        {props.label}
+      </button>
+    );
+  }
+  const configuredAuthMethods: AuthMethod[] = 
backupState.authentication_methods ?? [];
+  const haveMethodsConfigured = configuredAuthMethods.length;
+  return (
+    <AnastasisClientFrame title="Backup: Configure Authentication Methods">
+      <div>
+        <MethodButton method="sms" label="SMS" />
+        <MethodButton method="email" label="Email" />
+        <MethodButton method="question" label="Question" />
+        <MethodButton method="post" label="Physical Mail" />
+        <MethodButton method="totp" label="TOTP" />
+        <MethodButton method="iban" label="IBAN" />
+      </div>
+      <h2>Configured authentication methods</h2>
+      {haveMethodsConfigured ? (
+        configuredAuthMethods.map((x, i) => {
+          return (
+            <p key={i}>
+              {x.type} ({x.instructions}){" "}
+              <button
+                onClick={() => reducer.transition("delete_authentication", {
+                  authentication_method: i,
+                })}
+              >
+                Delete
+              </button>
+            </p>
+          );
+        })
+      ) : (
+        <p>No authentication methods configured yet.</p>
+      )}
+    </AnastasisClientFrame>
+  );
+}
+
+interface AuthMethodSetupProps {
+  method: string;
+  addAuthMethod: (x: any) => void;
+  cancel: () => void;
+}
+
+function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
+  return (
+    <AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
+      <p>This auth method is not implemented yet, please choose another 
one.</p>
+      <button onClick={() => props.cancel()}>Cancel</button>
+    </AnastasisClientFrame>
+  );
+}
+
+interface AuthenticationEditorProps {
+  reducer: AnastasisReducerApi;
+  backupState: ReducerStateBackup;
+}
+
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx 
b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
new file mode 100644
index 00000000..6c277094
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
@@ -0,0 +1,23 @@
+import { h, VNode } from "preact";
+import { BackupReducerProps, AnastasisClientFrame } from "./index";
+
+export function BackupFinishedScreen(props: BackupReducerProps): VNode {
+  return (<AnastasisClientFrame hideNext title="Backup finished">
+    <p>
+      Your backup of secret "{props.backupState.secret_name ?? "??"}" was
+      successful.
+    </p>
+    <p>The backup is stored by the following providers:</p>
+    <ul>
+      {Object.keys(props.backupState.success_details!).map((x, i) => {
+        const sd = props.backupState.success_details![x];
+        return (
+          <li key={i}>
+            {x} (Policy version {sd.policy_version})
+          </li>
+        );
+      })}
+    </ul>
+    <button onClick={() => props.reducer.reset()}>Back to start</button>
+  </AnastasisClientFrame>);
+}
diff --git 
a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx 
b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
new file mode 100644
index 00000000..1f108ce6
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
@@ -0,0 +1,63 @@
+import { h, VNode } from "preact";
+import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
+
+export function ChallengeOverviewScreen(props: RecoveryReducerProps): VNode {
+  const { recoveryState, reducer } = props;
+  const policies = recoveryState.recovery_information!.policies;
+  const chArr = recoveryState.recovery_information!.challenges;
+  const challenges: {
+    [uuid: string]: {
+      type: string;
+      instructions: string;
+      cost: string;
+    };
+  } = {};
+  for (const ch of chArr) {
+    challenges[ch.uuid] = {
+      type: ch.type,
+      cost: ch.cost,
+      instructions: ch.instructions,
+    };
+  }
+  return (
+    <AnastasisClientFrame title="Recovery: Solve challenges">
+      <h2>Policies</h2>
+      {policies.map((x, i) => {
+        return (
+          <div key={i}>
+            <h3>Policy #{i + 1}</h3>
+            {x.map((x, j) => {
+              const ch = challenges[x.uuid];
+              const feedback = recoveryState.challenge_feedback?.[x.uuid];
+              return (
+                <div key={j}
+                  style={{
+                    borderLeft: "2px solid gray",
+                    paddingLeft: "0.5em",
+                    borderRadius: "0.5em",
+                    marginTop: "0.5em",
+                    marginBottom: "0.5em",
+                  }}
+                >
+                  <h4>
+                    {ch.type} ({ch.instructions})
+                  </h4>
+                  <p>Status: {feedback?.state ?? "unknown"}</p>
+                  {feedback?.state !== "solved" ? (
+                    <button
+                      onClick={() => reducer.transition("select_challenge", {
+                        uuid: x.uuid,
+                      })}
+                    >
+                      Solve
+                    </button>
+                  ) : null}
+                </div>
+              );
+            })}
+          </div>
+        );
+      })}
+    </AnastasisClientFrame>
+  );
+}
diff --git 
a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx 
b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
new file mode 100644
index 00000000..2fed23d4
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -0,0 +1,19 @@
+import { h, VNode } from "preact";
+import { CommonReducerProps, AnastasisClientFrame, withProcessLabel } from 
"./index";
+
+export function ContinentSelectionScreen(props: CommonReducerProps): VNode {
+  const { reducer, reducerState } = props;
+  const sel = (x: string): void => reducer.transition("select_continent", { 
continent: x });
+  return (
+    <AnastasisClientFrame
+      hideNext
+      title={withProcessLabel(reducer, "Select Continent")}
+    >
+      {reducerState.continents.map((x: any) => (
+        <button onClick={() => sel(x.name)} key={x.name}>
+          {x.name}
+        </button>
+      ))}
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx 
b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx
new file mode 100644
index 00000000..dbe4b761
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx
@@ -0,0 +1,23 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { h, VNode } from "preact";
+import { CommonReducerProps, AnastasisClientFrame, withProcessLabel } from 
"./index";
+
+export function CountrySelectionScreen(props: CommonReducerProps): VNode {
+  const { reducer, reducerState } = props;
+  const sel = (x: any): void => reducer.transition("select_country", {
+    country_code: x.code,
+    currencies: [x.currency],
+  });
+  return (
+    <AnastasisClientFrame
+      hideNext
+      title={withProcessLabel(reducer, "Select Country")}
+    >
+      {reducerState.countries.map((x: any) => (
+        <button onClick={() => sel(x)} key={x.name}>
+          {x.name} ({x.currency})
+        </button>
+      ))}
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx 
b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
new file mode 100644
index 00000000..be74729e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
@@ -0,0 +1,27 @@
+import { h, VNode } from "preact";
+import { BackupReducerProps, AnastasisClientFrame } from "./index";
+
+export function PoliciesPayingScreen(props: BackupReducerProps): VNode {
+  const payments = props.backupState.policy_payment_requests ?? [];
+
+  return (
+    <AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
+      <p>
+        Some of the providers require a payment to store the encrypted
+        recovery document.
+      </p>
+      <ul>
+        {payments.map((x, i) => {
+          return (
+            <li key={i}>
+              {x.provider}: {x.payto}
+            </li>
+          );
+        })}
+      </ul>
+      <button onClick={() => props.reducer.transition("pay", {})}>
+        Check payment status now
+      </button>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx 
b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
new file mode 100644
index 00000000..7ef9f345
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -0,0 +1,17 @@
+import {
+  bytesToString,
+  decodeCrock
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
+
+export function RecoveryFinishedScreen(props: RecoveryReducerProps): VNode {
+  return (
+    <AnastasisClientFrame title="Recovery Finished" hideNext>
+      <h1>Recovery Finished</h1>
+      <p>
+        Secret: 
{bytesToString(decodeCrock(props.recoveryState.core_secret?.value!))}
+      </p>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx 
b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
new file mode 100644
index 00000000..b898bb39
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
@@ -0,0 +1,43 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { h, VNode } from "preact";
+import { BackupReducerProps, AnastasisClientFrame } from "./index";
+
+export function ReviewPoliciesScreen(props: BackupReducerProps): VNode {
+  const { reducer, backupState } = props;
+  const authMethods = backupState.authentication_methods!;
+  return (
+    <AnastasisClientFrame title="Backup: Review Recovery Policies">
+      {backupState.policies?.map((p, i) => {
+        const policyName = p.methods
+          .map((x, i) => authMethods[x.authentication_method].type)
+          .join(" + ");
+        return (
+          <div key={i}>
+          {/* <div key={i} class={style.policy}> */}
+            <h3>
+              Policy #{i + 1}: {policyName}
+            </h3>
+            Required Authentications:
+            <ul>
+              {p.methods.map((x, i) => {
+                const m = authMethods[x.authentication_method];
+                return (
+                  <li key={i}>
+                    {m.type} ({m.instructions}) at provider {x.provider}
+                  </li>
+                );
+              })}
+            </ul>
+            <div>
+              <button
+                onClick={() => reducer.transition("delete_policy", { 
policy_index: i })}
+              >
+                Delete Policy
+              </button>
+            </div>
+          </div>
+        );
+      })}
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx 
b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
new file mode 100644
index 00000000..2963930f
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
@@ -0,0 +1,53 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+  encodeCrock,
+  stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { BackupReducerProps, AnastasisClientFrame, LabeledInput } from 
"./index";
+
+export function SecretEditorScreen(props: BackupReducerProps): VNode {
+  const { reducer } = props;
+  const [secretName, setSecretName] = useState(
+    props.backupState.secret_name ?? ""
+  );
+  const [secretValue, setSecretValue] = useState(
+    props.backupState.core_secret?.value ?? "" ?? ""
+  );
+  const secretNext = (): void => {
+    reducer.runTransaction(async (tx) => {
+      await tx.transition("enter_secret_name", {
+        name: secretName,
+      });
+      await tx.transition("enter_secret", {
+        secret: {
+          value: encodeCrock(stringToBytes(secretValue)),
+          mime: "text/plain",
+        },
+        expiration: {
+          t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
+        },
+      });
+      await tx.transition("next", {});
+    });
+  };
+  return (
+    <AnastasisClientFrame
+      title="Backup: Provide secret"
+      onNext={() => secretNext()}
+    >
+      <div>
+        <LabeledInput
+          label="Secret Name:"
+          grabFocus
+          bind={[secretName, setSecretName]} />
+      </div>
+      <div>
+        <LabeledInput
+          label="Secret Value:"
+          bind={[secretValue, setSecretValue]} />
+      </div>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx 
b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
new file mode 100644
index 00000000..bbdcf8c2
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
+
+export function SecretSelectionScreen(props: RecoveryReducerProps): VNode {
+  const { reducer, recoveryState } = props;
+  const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
+  const [otherVersion, setOtherVersion] = useState<number>(
+    recoveryState.recovery_document?.version ?? 0
+  );
+  const recoveryDocument = recoveryState.recovery_document!;
+  const [otherProvider, setOtherProvider] = useState<string>("");
+  function selectVersion(p: string, n: number): void {
+    reducer.runTransaction(async (tx) => {
+      await tx.transition("change_version", {
+        version: n,
+        provider_url: p,
+      });
+      setSelectingVersion(false);
+    });
+  }
+  if (selectingVersion) {
+    return (
+      <AnastasisClientFrame hideNav title="Recovery: Select secret">
+        <p>Select a different version of the secret</p>
+        <select onChange={(e) => setOtherProvider((e.target as any).value)}>
+          {Object.keys(recoveryState.authentication_providers ?? {}).map(
+            (x, i) => (
+              <option key={i} selected={x === recoveryDocument.provider_url} 
value={x}>
+                {x}
+              </option>
+            )
+          )}
+        </select>
+        <div>
+          <input
+            value={otherVersion}
+            onChange={(e) => setOtherVersion(Number((e.target as 
HTMLInputElement).value))}
+            type="number" />
+          <button onClick={() => selectVersion(otherProvider, otherVersion)}>
+            Use this version
+          </button>
+        </div>
+        <div>
+          <button onClick={() => selectVersion(otherProvider, 0)}>
+            Use latest version
+          </button>
+        </div>
+        <div>
+          <button onClick={() => setSelectingVersion(false)}>Cancel</button>
+        </div>
+      </AnastasisClientFrame>
+    );
+  }
+  return (
+    <AnastasisClientFrame title="Recovery: Select secret">
+      <p>Provider: {recoveryDocument.provider_url}</p>
+      <p>Secret version: {recoveryDocument.version}</p>
+      <p>Secret name: {recoveryDocument.version}</p>
+      <button onClick={() => setSelectingVersion(true)}>
+        Select different secret
+      </button>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx 
b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx
new file mode 100644
index 00000000..6296dc02
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx
@@ -0,0 +1,22 @@
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AnastasisClientFrame, LabeledInput } from "./index";
+import { SolveEntryProps } from "./SolveScreen";
+
+export function SolveEmailEntry(props: SolveEntryProps): VNode {
+  const [answer, setAnswer] = useState("");
+  const { reducer, challenge, feedback } = props;
+  const next = (): void => reducer.transition("solve_challenge", {
+    answer,
+  });
+  return (
+    <AnastasisClientFrame
+      title="Recovery: Solve challenge"
+      onNext={() => next()}
+    >
+      <p>Feedback: {JSON.stringify(feedback)}</p>
+      <p>{challenge.instructions}</p>
+      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx 
b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx
new file mode 100644
index 00000000..b11ceed2
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx
@@ -0,0 +1,22 @@
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AnastasisClientFrame, LabeledInput } from "./index";
+import { SolveEntryProps } from "./SolveScreen";
+
+export function SolvePostEntry(props: SolveEntryProps): VNode {
+  const [answer, setAnswer] = useState("");
+  const { reducer, challenge, feedback } = props;
+  const next = (): void => reducer.transition("solve_challenge", {
+    answer,
+  });
+  return (
+    <AnastasisClientFrame
+      title="Recovery: Solve challenge"
+      onNext={() => next()}
+    >
+      <p>Feedback: {JSON.stringify(feedback)}</p>
+      <p>{challenge.instructions}</p>
+      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx 
b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx
new file mode 100644
index 00000000..6393958b
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx
@@ -0,0 +1,22 @@
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AnastasisClientFrame, LabeledInput } from "./index";
+import { SolveEntryProps } from "./SolveScreen";
+
+export function SolveQuestionEntry(props: SolveEntryProps): VNode {
+  const [answer, setAnswer] = useState("");
+  const { reducer, challenge, feedback } = props;
+  const next = (): void => reducer.transition("solve_challenge", {
+    answer,
+  });
+  return (
+    <AnastasisClientFrame
+      title="Recovery: Solve challenge"
+      onNext={() => next()}
+    >
+      <p>Feedback: {JSON.stringify(feedback)}</p>
+      <p>Question: {challenge.instructions}</p>
+      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx 
b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
new file mode 100644
index 00000000..46ff8227
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
@@ -0,0 +1,41 @@
+import { h, VNode } from "preact";
+import { AnastasisReducerApi, ChallengeFeedback, ChallengeInfo } from 
"../../hooks/use-anastasis-reducer";
+import { SolveEmailEntry } from "./SolveEmailEntry";
+import { SolvePostEntry } from "./SolvePostEntry";
+import { SolveQuestionEntry } from "./SolveQuestionEntry";
+import { SolveSmsEntry } from "./SolveSmsEntry";
+import { SolveUnsupportedEntry } from "./SolveUnsupportedEntry";
+import { RecoveryReducerProps } from "./index";
+
+export function SolveScreen(props: RecoveryReducerProps): VNode {
+  const chArr = props.recoveryState.recovery_information!.challenges;
+  const challengeFeedback = props.recoveryState.challenge_feedback ?? {};
+  const selectedUuid = props.recoveryState.selected_challenge_uuid!;
+  const challenges: {
+    [uuid: string]: ChallengeInfo;
+  } = {};
+  for (const ch of chArr) {
+    challenges[ch.uuid] = ch;
+  }
+  const selectedChallenge = challenges[selectedUuid];
+  const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
+    question: SolveQuestionEntry,
+    sms: SolveSmsEntry,
+    email: SolveEmailEntry,
+    post: SolvePostEntry,
+  };
+  const SolveDialog = dialogMap[selectedChallenge.type] ?? 
SolveUnsupportedEntry;
+  return (
+    <SolveDialog
+      challenge={selectedChallenge}
+      reducer={props.reducer}
+      feedback={challengeFeedback[selectedUuid]} />
+  );
+}
+
+export interface SolveEntryProps {
+  reducer: AnastasisReducerApi;
+  challenge: ChallengeInfo;
+  feedback?: ChallengeFeedback;
+}
+
diff --git a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx 
b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx
new file mode 100644
index 00000000..d0cd4133
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx
@@ -0,0 +1,22 @@
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AnastasisClientFrame, LabeledInput } from "./index";
+import { SolveEntryProps } from "./SolveScreen";
+
+export function SolveSmsEntry(props: SolveEntryProps): VNode {
+  const [answer, setAnswer] = useState("");
+  const { reducer, challenge, feedback } = props;
+  const next = (): void => reducer.transition("solve_challenge", {
+    answer,
+  });
+  return (
+    <AnastasisClientFrame
+      title="Recovery: Solve challenge"
+      onNext={() => next()}
+    >
+      <p>Feedback: {JSON.stringify(feedback)}</p>
+      <p>{challenge.instructions}</p>
+      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx 
b/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx
new file mode 100644
index 00000000..7f538d24
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx
@@ -0,0 +1,12 @@
+import { h, VNode } from "preact";
+import { AnastasisClientFrame } from "./index";
+import { SolveEntryProps } from "./SolveScreen";
+
+export function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
+  return (
+    <AnastasisClientFrame hideNext title="Recovery: Solve challenge">
+      <p>{JSON.stringify(props.challenge)}</p>
+      <p>Challenge not supported.</p>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx 
b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
new file mode 100644
index 00000000..38124887
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
@@ -0,0 +1,14 @@
+import { h, VNode } from "preact";
+import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
+import { AnastasisClientFrame } from "./index";
+
+export function StartScreen(props: { reducer: AnastasisReducerApi; }): VNode {
+  return (
+    <AnastasisClientFrame hideNav title="Home">
+      <button autoFocus onClick={() => props.reducer.startBackup()}>
+        Backup
+      </button>
+      <button onClick={() => props.reducer.startRecover()}>Recover</button>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx 
b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
new file mode 100644
index 00000000..5b8a835b
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
@@ -0,0 +1,25 @@
+import { h, VNode } from "preact";
+import { BackupReducerProps, AnastasisClientFrame } from "./index";
+
+export function TruthsPayingScreen(props: BackupReducerProps): VNode {
+  const payments = props.backupState.payments ?? [];
+  return (
+    <AnastasisClientFrame
+      hideNext
+      title="Backup: Authentication Storage Payments"
+    >
+      <p>
+        Some of the providers require a payment to store the encrypted
+        authentication information.
+      </p>
+      <ul>
+        {payments.map((x, i) => {
+          return <li key={i}>{x}</li>;
+        })}
+      </ul>
+      <button onClick={() => props.reducer.transition("pay", {})}>
+        Check payment status now
+      </button>
+    </AnastasisClientFrame>
+  );
+}
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx 
b/packages/anastasis-webui/src/pages/home/index.tsx
new file mode 100644
index 00000000..ab63553c
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -0,0 +1,248 @@
+import {
+  ComponentChildren, createContext,
+  Fragment, FunctionalComponent, h, VNode
+} from "preact";
+import { useContext, useLayoutEffect, useRef } from "preact/hooks";
+import { Menu } from "../../components/menu";
+import {
+  BackupStates, RecoveryStates,
+  ReducerStateBackup,
+  ReducerStateRecovery,
+} from "anastasis-core";
+import {
+  AnastasisReducerApi,
+  useAnastasisReducer
+} from "../../hooks/use-anastasis-reducer";
+import { AttributeEntryScreen } from "./AttributeEntryScreen";
+import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
+import { BackupFinishedScreen } from "./BackupFinishedScreen";
+import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
+import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
+import { CountrySelectionScreen } from "./CountrySelectionScreen";
+import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
+import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
+import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
+import { SecretEditorScreen } from "./SecretEditorScreen";
+import { SecretSelectionScreen } from "./SecretSelectionScreen";
+import { SolveScreen } from "./SolveScreen";
+import { StartScreen } from "./StartScreen";
+import { TruthsPayingScreen } from "./TruthsPayingScreen";
+
+const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
+
+function isBackup(reducer: AnastasisReducerApi): boolean {
+  return !!reducer.currentReducerState?.backup_state;
+}
+
+export interface CommonReducerProps {
+  reducer: AnastasisReducerApi;
+  reducerState: ReducerStateBackup | ReducerStateRecovery;
+}
+
+export function withProcessLabel(reducer: AnastasisReducerApi, text: string): 
string {
+  if (isBackup(reducer)) {
+    return `Backup: ${text}`;
+  }
+  return `Recovery: ${text}`;
+}
+
+export interface BackupReducerProps {
+  reducer: AnastasisReducerApi;
+  backupState: ReducerStateBackup;
+}
+
+export interface RecoveryReducerProps {
+  reducer: AnastasisReducerApi;
+  recoveryState: ReducerStateRecovery;
+}
+
+interface AnastasisClientFrameProps {
+  onNext?(): void;
+  title: string;
+  children: ComponentChildren;
+  /**
+   * Should back/next buttons be provided?
+   */
+  hideNav?: boolean;
+  /**
+   * Hide only the "next" button.
+   */
+  hideNext?: boolean;
+}
+
+export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
+  const reducer = useContext(WithReducer);
+  if (!reducer) {
+    return <p>Fatal: Reducer must be in context.</p>;
+  }
+  const next = (): void => {
+    if (props.onNext) {
+      props.onNext();
+    } else {
+      reducer.transition("next", {});
+    }
+  };
+  const handleKeyPress = (e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>): 
void => {
+    console.log("Got key press", e.key);
+    // FIXME: By default, "next" action should be executed here
+  };
+  return (<Fragment>
+    <Menu title="Anastasis" />
+    <section class="section">
+      <div onKeyPress={(e) => handleKeyPress(e)}> {/* class={style.home} */}
+        <button onClick={() => reducer.reset()}>Reset session</button>
+        <h1>{props.title}</h1>
+        <ErrorBanner reducer={reducer} />
+        {props.children}
+        {!props.hideNav ? (
+          <div>
+            <button onClick={() => reducer.back()}>Back</button>
+            {!props.hideNext ? (
+              <button onClick={next}>Next</button>
+            ) : null}
+          </div>
+        ) : null}
+      </div>
+    </section>
+  </Fragment>
+  );
+}
+
+const AnastasisClient: FunctionalComponent = () => {
+  const reducer = useAnastasisReducer();
+  return (
+    <WithReducer.Provider value={reducer}>
+      <AnastasisClientImpl />
+    </WithReducer.Provider>
+  );
+};
+
+const AnastasisClientImpl: FunctionalComponent = () => {
+  const reducer = useContext(WithReducer)!;
+  const reducerState = reducer.currentReducerState;
+  if (!reducerState) {
+    return <StartScreen reducer={reducer} />;
+  }
+  console.log("state", reducer.currentReducerState);
+
+  if (
+    reducerState.backup_state === BackupStates.ContinentSelecting ||
+    reducerState.recovery_state === RecoveryStates.ContinentSelecting
+  ) {
+    return <ContinentSelectionScreen reducer={reducer} 
reducerState={reducerState} />;
+  }
+  if (
+    reducerState.backup_state === BackupStates.CountrySelecting ||
+    reducerState.recovery_state === RecoveryStates.CountrySelecting
+  ) {
+    return <CountrySelectionScreen reducer={reducer} 
reducerState={reducerState} />;
+  }
+  if (
+    reducerState.backup_state === BackupStates.UserAttributesCollecting ||
+    reducerState.recovery_state === RecoveryStates.UserAttributesCollecting
+  ) {
+    return <AttributeEntryScreen reducer={reducer} reducerState={reducerState} 
/>;
+  }
+  if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
+    return (
+      <AuthenticationEditorScreen backupState={reducerState} reducer={reducer} 
/>
+    );
+  }
+  if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
+    return <ReviewPoliciesScreen reducer={reducer} backupState={reducerState} 
/>;
+  }
+  if (reducerState.backup_state === BackupStates.SecretEditing) {
+    return <SecretEditorScreen reducer={reducer} backupState={reducerState} />;
+  }
+
+  if (reducerState.backup_state === BackupStates.BackupFinished) {
+    const backupState: ReducerStateBackup = reducerState;
+    return <BackupFinishedScreen reducer={reducer} backupState={backupState} 
/>;
+  }
+
+  if (reducerState.backup_state === BackupStates.TruthsPaying) {
+    return <TruthsPayingScreen reducer={reducer} backupState={reducerState} />
+
+  }
+
+  if (reducerState.backup_state === BackupStates.PoliciesPaying) {
+    const backupState: ReducerStateBackup = reducerState;
+    return <PoliciesPayingScreen reducer={reducer} backupState={backupState} />
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.SecretSelecting) {
+    return <SecretSelectionScreen reducer={reducer} 
recoveryState={reducerState} />;
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
+    return <ChallengeOverviewScreen reducer={reducer} 
recoveryState={reducerState} />;
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
+    return <SolveScreen reducer={reducer} recoveryState={reducerState} />
+  }
+
+  if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
+    return <RecoveryFinishedScreen reducer={reducer} 
recoveryState={reducerState} />
+  }
+
+  console.log("unknown state", reducer.currentReducerState);
+  return (
+    <AnastasisClientFrame hideNav title="Bug">
+      <p>Bug: Unknown state.</p>
+      <button onClick={() => reducer.reset()}>Reset</button>
+    </AnastasisClientFrame>
+  );
+};
+
+
+interface LabeledInputProps {
+  label: string;
+  grabFocus?: boolean;
+  bind: [string, (x: string) => void];
+}
+
+export function LabeledInput(props: LabeledInputProps): VNode {
+  const inputRef = useRef<HTMLInputElement>(null);
+  useLayoutEffect(() => {
+    if (props.grabFocus) {
+      inputRef.current?.focus();
+    }
+  }, [props.grabFocus]);
+  return (
+    <label>
+      {props.label}
+      <input
+        value={props.bind[0]}
+        onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)}
+        ref={inputRef}
+        style={{ display: "block" }}
+      />
+    </label>
+  );
+}
+
+
+interface ErrorBannerProps {
+  reducer: AnastasisReducerApi;
+}
+
+/**
+ * Show a dismissable error banner if there is a current error.
+ */
+function ErrorBanner(props: ErrorBannerProps): VNode | null {
+  const currentError = props.reducer.currentError;
+  if (currentError) {
+    return (
+      <div id="error"> {/* style.error */}
+        <p>Error: {JSON.stringify(currentError)}</p>
+        <button onClick={() => props.reducer.dismissError()}>
+          Dismiss Error
+        </button>
+      </div>
+    );
+  }
+  return null;
+}
+
+export default AnastasisClient;
diff --git a/packages/anastasis-webui/src/routes/home/style.css 
b/packages/anastasis-webui/src/pages/home/style.css
similarity index 100%
rename from packages/anastasis-webui/src/routes/home/style.css
rename to packages/anastasis-webui/src/pages/home/style.css
diff --git a/packages/anastasis-webui/src/routes/notfound/index.tsx 
b/packages/anastasis-webui/src/pages/notfound/index.tsx
similarity index 84%
rename from packages/anastasis-webui/src/routes/notfound/index.tsx
rename to packages/anastasis-webui/src/pages/notfound/index.tsx
index 444e03d4..4e74d1d9 100644
--- a/packages/anastasis-webui/src/routes/notfound/index.tsx
+++ b/packages/anastasis-webui/src/pages/notfound/index.tsx
@@ -1,10 +1,9 @@
 import { FunctionalComponent, h } from 'preact';
 import { Link } from 'preact-router/match';
-import style from './style.css';
 
 const Notfound: FunctionalComponent = () => {
     return (
-        <div class={style.notfound}>
+        <div>
             <h1>Error 404</h1>
             <p>That page doesn&apos;t exist.</p>
             <Link href="/">
diff --git a/packages/anastasis-webui/src/routes/notfound/style.css 
b/packages/anastasis-webui/src/pages/notfound/style.css
similarity index 100%
rename from packages/anastasis-webui/src/routes/notfound/style.css
rename to packages/anastasis-webui/src/pages/notfound/style.css
diff --git a/packages/anastasis-webui/src/routes/profile/index.tsx 
b/packages/anastasis-webui/src/pages/profile/index.tsx
similarity index 94%
rename from packages/anastasis-webui/src/routes/profile/index.tsx
rename to packages/anastasis-webui/src/pages/profile/index.tsx
index 023b56c9..859a83ed 100644
--- a/packages/anastasis-webui/src/routes/profile/index.tsx
+++ b/packages/anastasis-webui/src/pages/profile/index.tsx
@@ -1,6 +1,5 @@
 import { FunctionalComponent, h } from 'preact';
 import { useEffect, useState } from 'preact/hooks';
-import style from './style.css';
 
 interface Props {
     user: string;
@@ -27,7 +26,7 @@ const Profile: FunctionalComponent<Props> = (props: Props) => 
{
     };
 
     return (
-        <div class={style.profile}>
+        <div>
             <h1>Profile: {user}</h1>
             <p>This is the user profile for a user named {user}.</p>
 
diff --git a/packages/anastasis-webui/src/routes/profile/style.css 
b/packages/anastasis-webui/src/pages/profile/style.css
similarity index 100%
rename from packages/anastasis-webui/src/routes/profile/style.css
rename to packages/anastasis-webui/src/pages/profile/style.css
diff --git a/packages/anastasis-webui/src/routes/home/index.tsx 
b/packages/anastasis-webui/src/routes/home/index.tsx
deleted file mode 100644
index 1351775b..00000000
--- a/packages/anastasis-webui/src/routes/home/index.tsx
+++ /dev/null
@@ -1,1025 +0,0 @@
-import {
-  bytesToString,
-  canonicalJson,
-  decodeCrock,
-  encodeCrock,
-  stringToBytes,
-} from "@gnu-taler/taler-util";
-import {
-  AuthMethod,
-  BackupStates,
-  ChallengeFeedback,
-  ChallengeInfo,
-  RecoveryStates,
-  ReducerStateBackup,
-  ReducerStateRecovery,
-} from "anastasis-core";
-import {
-  FunctionalComponent,
-  ComponentChildren,
-  h,
-  createContext,
-} from "preact";
-import { useState, useContext, useRef, useLayoutEffect } from "preact/hooks";
-import {
-  AnastasisReducerApi,
-  useAnastasisReducer,
-} from "../../hooks/use-anastasis-reducer";
-import style from "./style.css";
-
-const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
-
-function isBackup(reducer: AnastasisReducerApi) {
-  return !!reducer.currentReducerState?.backup_state;
-}
-
-interface CommonReducerProps {
-  reducer: AnastasisReducerApi;
-  reducerState: ReducerStateBackup | ReducerStateRecovery;
-}
-
-function withProcessLabel(reducer: AnastasisReducerApi, text: string): string {
-  if (isBackup(reducer)) {
-    return "Backup: " + text;
-  }
-  return "Recovery: " + text;
-}
-
-function ContinentSelection(props: CommonReducerProps) {
-  const { reducer, reducerState } = props;
-  const sel = (x: string) =>
-    reducer.transition("select_continent", { continent: x });
-  return (
-    <AnastasisClientFrame
-      hideNext
-      title={withProcessLabel(reducer, "Select Continent")}
-    >
-      {reducerState.continents.map((x: any) => (
-        <button onClick={() => sel(x.name)} key={x.name}>
-          {x.name}
-        </button>
-      ))}
-    </AnastasisClientFrame>
-  );
-}
-
-function CountrySelection(props: CommonReducerProps) {
-  const { reducer, reducerState } = props;
-  const sel = (x: any) =>
-    reducer.transition("select_country", {
-      country_code: x.code,
-      currencies: [x.currency],
-    });
-  return (
-    <AnastasisClientFrame
-      hideNext
-      title={withProcessLabel(reducer, "Select Country")}
-    >
-      {reducerState.countries.map((x: any) => (
-        <button onClick={() => sel(x)} key={x.name}>
-          {x.name} ({x.currency})
-        </button>
-      ))}
-    </AnastasisClientFrame>
-  );
-}
-
-interface SolveEntryProps {
-  reducer: AnastasisReducerApi;
-  challenge: ChallengeInfo;
-  feedback?: ChallengeFeedback;
-}
-
-function SolveQuestionEntry(props: SolveEntryProps) {
-  const [answer, setAnswer] = useState("");
-  const { reducer, challenge, feedback } = props;
-  const next = () =>
-    reducer.transition("solve_challenge", {
-      answer,
-    });
-  return (
-    <AnastasisClientFrame
-      title="Recovery: Solve challenge"
-      onNext={() => next()}
-    >
-      <p>Feedback: {JSON.stringify(feedback)}</p>
-      <p>Question: {challenge.instructions}</p>
-      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
-    </AnastasisClientFrame>
-  );
-}
-
-function SolveSmsEntry(props: SolveEntryProps) {
-  const [answer, setAnswer] = useState("");
-  const { reducer, challenge, feedback } = props;
-  const next = () =>
-    reducer.transition("solve_challenge", {
-      answer,
-    });
-  return (
-    <AnastasisClientFrame
-      title="Recovery: Solve challenge"
-      onNext={() => next()}
-    >
-      <p>Feedback: {JSON.stringify(feedback)}</p>
-      <p>{challenge.instructions}</p>
-      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
-    </AnastasisClientFrame>
-  );
-}
-
-function SolvePostEntry(props: SolveEntryProps) {
-  const [answer, setAnswer] = useState("");
-  const { reducer, challenge, feedback } = props;
-  const next = () =>
-    reducer.transition("solve_challenge", {
-      answer,
-    });
-  return (
-    <AnastasisClientFrame
-      title="Recovery: Solve challenge"
-      onNext={() => next()}
-    >
-      <p>Feedback: {JSON.stringify(feedback)}</p>
-      <p>{challenge.instructions}</p>
-      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
-    </AnastasisClientFrame>
-  );
-}
-
-function SolveEmailEntry(props: SolveEntryProps) {
-  const [answer, setAnswer] = useState("");
-  const { reducer, challenge, feedback } = props;
-  const next = () =>
-    reducer.transition("solve_challenge", {
-      answer,
-    });
-  return (
-    <AnastasisClientFrame
-      title="Recovery: Solve challenge"
-      onNext={() => next()}
-    >
-      <p>Feedback: {JSON.stringify(feedback)}</p>
-      <p>{challenge.instructions}</p>
-      <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
-    </AnastasisClientFrame>
-  );
-}
-
-function SolveUnsupportedEntry(props: SolveEntryProps) {
-  return (
-    <AnastasisClientFrame hideNext title="Recovery: Solve challenge">
-      <p>{JSON.stringify(props.challenge)}</p>
-      <p>Challenge not supported.</p>
-    </AnastasisClientFrame>
-  );
-}
-
-function SecretEditor(props: BackupReducerProps) {
-  const { reducer } = props;
-  const [secretName, setSecretName] = useState(
-    props.backupState.secret_name ?? "",
-  );
-  const [secretValue, setSecretValue] = useState(
-    props.backupState.core_secret?.value ?? "" ?? "",
-  );
-  const secretNext = () => {
-    reducer.runTransaction(async (tx) => {
-      await tx.transition("enter_secret_name", {
-        name: secretName,
-      });
-      await tx.transition("enter_secret", {
-        secret: {
-          value: encodeCrock(stringToBytes(secretValue)),
-          mime: "text/plain",
-        },
-        expiration: {
-          t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
-        },
-      });
-      await tx.transition("next", {});
-    });
-  };
-  return (
-    <AnastasisClientFrame
-      title="Backup: Provide secret"
-      onNext={() => secretNext()}
-    >
-      <div>
-        <LabeledInput
-          label="Secret Name:"
-          grabFocus
-          bind={[secretName, setSecretName]}
-        />
-      </div>
-      <div>
-        <LabeledInput
-          label="Secret Value:"
-          bind={[secretValue, setSecretValue]}
-        />
-      </div>
-    </AnastasisClientFrame>
-  );
-}
-
-export interface BackupReducerProps {
-  reducer: AnastasisReducerApi;
-  backupState: ReducerStateBackup;
-}
-
-function ReviewPolicies(props: BackupReducerProps) {
-  const { reducer, backupState } = props;
-  const authMethods = backupState.authentication_methods!;
-  return (
-    <AnastasisClientFrame title="Backup: Review Recovery Policies">
-      {backupState.policies?.map((p, i) => {
-        const policyName = p.methods
-          .map((x) => authMethods[x.authentication_method].type)
-          .join(" + ");
-        return (
-          <div class={style.policy}>
-            <h3>
-              Policy #{i + 1}: {policyName}
-            </h3>
-            Required Authentications:
-            <ul>
-              {p.methods.map((x) => {
-                const m = authMethods[x.authentication_method];
-                return (
-                  <li>
-                    {m.type} ({m.instructions}) at provider {x.provider}
-                  </li>
-                );
-              })}
-            </ul>
-            <div>
-              <button
-                onClick={() =>
-                  reducer.transition("delete_policy", { policy_index: i })
-                }
-              >
-                Delete Policy
-              </button>
-            </div>
-          </div>
-        );
-      })}
-    </AnastasisClientFrame>
-  );
-}
-
-export interface RecoveryReducerProps {
-  reducer: AnastasisReducerApi;
-  recoveryState: ReducerStateRecovery;
-}
-
-function SecretSelection(props: RecoveryReducerProps) {
-  const { reducer, recoveryState } = props;
-  const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
-  const [otherVersion, setOtherVersion] = useState<number>(
-    recoveryState.recovery_document?.version ?? 0,
-  );
-  const recoveryDocument = recoveryState.recovery_document!;
-  const [otherProvider, setOtherProvider] = useState<string>("");
-  function selectVersion(p: string, n: number) {
-    reducer.runTransaction(async (tx) => {
-      await tx.transition("change_version", {
-        version: n,
-        provider_url: p,
-      });
-      setSelectingVersion(false);
-    });
-  }
-  if (selectingVersion) {
-    return (
-      <AnastasisClientFrame hideNav title="Recovery: Select secret">
-        <p>Select a different version of the secret</p>
-        <select onChange={(e) => setOtherProvider((e.target as any).value)}>
-          {Object.keys(recoveryState.authentication_providers ?? {}).map(
-            (x) => (
-              <option selected={x === recoveryDocument.provider_url} value={x}>
-                {x}
-              </option>
-            ),
-          )}
-        </select>
-        <div>
-          <input
-            value={otherVersion}
-            onChange={(e) =>
-              setOtherVersion(Number((e.target as HTMLInputElement).value))
-            }
-            type="number"
-          />
-          <button onClick={() => selectVersion(otherProvider, otherVersion)}>
-            Use this version
-          </button>
-        </div>
-        <div>
-          <button onClick={() => selectVersion(otherProvider, 0)}>
-            Use latest version
-          </button>
-        </div>
-        <div>
-          <button onClick={() => setSelectingVersion(false)}>Cancel</button>
-        </div>
-      </AnastasisClientFrame>
-    );
-  }
-  return (
-    <AnastasisClientFrame title="Recovery: Select secret">
-      <p>Provider: {recoveryDocument.provider_url}</p>
-      <p>Secret version: {recoveryDocument.version}</p>
-      <p>Secret name: {recoveryDocument.version}</p>
-      <button onClick={() => setSelectingVersion(true)}>
-        Select different secret
-      </button>
-    </AnastasisClientFrame>
-  );
-}
-
-interface AnastasisClientFrameProps {
-  onNext?(): void;
-  title: string;
-  children: ComponentChildren;
-  /**
-   * Should back/next buttons be provided?
-   */
-  hideNav?: boolean;
-  /**
-   * Hide only the "next" button.
-   */
-  hideNext?: boolean;
-}
-
-function AnastasisClientFrame(props: AnastasisClientFrameProps) {
-  const reducer = useContext(WithReducer);
-  if (!reducer) {
-    return <p>Fatal: Reducer must be in context.</p>;
-  }
-  const next = () => {
-    if (props.onNext) {
-      props.onNext();
-    } else {
-      reducer.transition("next", {});
-    }
-  };
-  const handleKeyPress = (e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>) => {
-    console.log("Got key press", e.key);
-    // FIXME: By default, "next" action should be executed here
-  };
-  return (
-    <div class={style.home} onKeyPress={(e) => handleKeyPress(e)}>
-      <button onClick={() => reducer.reset()}>Reset session</button>
-      <h1>{props.title}</h1>
-      <ErrorBanner reducer={reducer} />
-      {props.children}
-      {!props.hideNav ? (
-        <div>
-          <button onClick={() => reducer.back()}>Back</button>
-          {!props.hideNext ? (
-            <button onClick={() => next()}>Next</button>
-          ) : null}
-        </div>
-      ) : null}
-    </div>
-  );
-}
-
-function ChallengeOverview(props: RecoveryReducerProps) {
-  const { recoveryState, reducer } = props;
-  const policies = recoveryState.recovery_information!.policies;
-  const chArr = recoveryState.recovery_information!.challenges;
-  const challenges: {
-    [uuid: string]: {
-      type: string;
-      instructions: string;
-      cost: string;
-    };
-  } = {};
-  for (const ch of chArr) {
-    challenges[ch.uuid] = {
-      type: ch.type,
-      cost: ch.cost,
-      instructions: ch.instructions,
-    };
-  }
-  return (
-    <AnastasisClientFrame title="Recovery: Solve challenges">
-      <h2>Policies</h2>
-      {policies.map((x, i) => {
-        return (
-          <div>
-            <h3>Policy #{i + 1}</h3>
-            {x.map((x) => {
-              const ch = challenges[x.uuid];
-              const feedback = recoveryState.challenge_feedback?.[x.uuid];
-              return (
-                <div
-                  style={{
-                    borderLeft: "2px solid gray",
-                    paddingLeft: "0.5em",
-                    borderRadius: "0.5em",
-                    marginTop: "0.5em",
-                    marginBottom: "0.5em",
-                  }}
-                >
-                  <h4>
-                    {ch.type} ({ch.instructions})
-                  </h4>
-                  <p>Status: {feedback?.state ?? "unknown"}</p>
-                  {feedback?.state !== "solved" ? (
-                    <button
-                      onClick={() =>
-                        reducer.transition("select_challenge", {
-                          uuid: x.uuid,
-                        })
-                      }
-                    >
-                      Solve
-                    </button>
-                  ) : null}
-                </div>
-              );
-            })}
-          </div>
-        );
-      })}
-    </AnastasisClientFrame>
-  );
-}
-
-const AnastasisClient: FunctionalComponent = () => {
-  const reducer = useAnastasisReducer();
-  return (
-    <WithReducer.Provider value={reducer}>
-      <AnastasisClientImpl />
-    </WithReducer.Provider>
-  );
-};
-
-const AnastasisClientImpl: FunctionalComponent = () => {
-  const reducer = useContext(WithReducer)!;
-  const reducerState = reducer.currentReducerState;
-  if (!reducerState) {
-    return (
-      <AnastasisClientFrame hideNav title="Home">
-        <button autoFocus onClick={() => reducer.startBackup()}>
-          Backup
-        </button>
-        <button onClick={() => reducer.startRecover()}>Recover</button>
-      </AnastasisClientFrame>
-    );
-  }
-  console.log("state", reducer.currentReducerState);
-
-  if (
-    reducerState.backup_state === BackupStates.ContinentSelecting ||
-    reducerState.recovery_state === RecoveryStates.ContinentSelecting
-  ) {
-    return <ContinentSelection reducer={reducer} reducerState={reducerState} 
/>;
-  }
-  if (
-    reducerState.backup_state === BackupStates.CountrySelecting ||
-    reducerState.recovery_state === RecoveryStates.CountrySelecting
-  ) {
-    return <CountrySelection reducer={reducer} reducerState={reducerState} />;
-  }
-  if (
-    reducerState.backup_state === BackupStates.UserAttributesCollecting ||
-    reducerState.recovery_state === RecoveryStates.UserAttributesCollecting
-  ) {
-    return <AttributeEntry reducer={reducer} reducerState={reducerState} />;
-  }
-  if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
-    return (
-      <AuthenticationEditor backupState={reducerState} reducer={reducer} />
-    );
-  }
-  if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
-    return <ReviewPolicies reducer={reducer} backupState={reducerState} />;
-  }
-  if (reducerState.backup_state === BackupStates.SecretEditing) {
-    return <SecretEditor reducer={reducer} backupState={reducerState} />;
-  }
-
-  if (reducerState.backup_state === BackupStates.BackupFinished) {
-    const backupState: ReducerStateBackup = reducerState;
-    return (
-      <AnastasisClientFrame hideNext title="Backup finished">
-        <p>
-          Your backup of secret "{backupState.secret_name ?? "??"}" was
-          successful.
-        </p>
-        <p>The backup is stored by the following providers:</p>
-        <ul>
-          {Object.keys(backupState.success_details!).map((x, i) => {
-            const sd = backupState.success_details![x];
-            return (
-              <li>
-                {x} (Policy version {sd.policy_version})
-              </li>
-            );
-          })}
-        </ul>
-        <button onClick={() => reducer.reset()}>Back to start</button>
-      </AnastasisClientFrame>
-    );
-  }
-
-  if (reducerState.backup_state === BackupStates.TruthsPaying) {
-    const backupState: ReducerStateBackup = reducerState;
-    const payments = backupState.payments ?? [];
-    return (
-      <AnastasisClientFrame
-        hideNext
-        title="Backup: Authentication Storage Payments"
-      >
-        <p>
-          Some of the providers require a payment to store the encrypted
-          authentication information.
-        </p>
-        <ul>
-          {payments.map((x) => {
-            return <li>{x}</li>;
-          })}
-        </ul>
-        <button onClick={() => reducer.transition("pay", {})}>
-          Check payment status now
-        </button>
-      </AnastasisClientFrame>
-    );
-  }
-
-  if (reducerState.backup_state === BackupStates.PoliciesPaying) {
-    const backupState: ReducerStateBackup = reducerState;
-    const payments = backupState.policy_payment_requests ?? [];
-
-    return (
-      <AnastasisClientFrame hideNext title="Backup: Recovery Document 
Payments">
-        <p>
-          Some of the providers require a payment to store the encrypted
-          recovery document.
-        </p>
-        <ul>
-          {payments.map((x) => {
-            return (
-              <li>
-                {x.provider}: {x.payto}
-              </li>
-            );
-          })}
-        </ul>
-        <button onClick={() => reducer.transition("pay", {})}>
-          Check payment status now
-        </button>
-      </AnastasisClientFrame>
-    );
-  }
-
-  if (reducerState.recovery_state === RecoveryStates.SecretSelecting) {
-    return <SecretSelection reducer={reducer} recoveryState={reducerState} />;
-  }
-
-  if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
-    return <ChallengeOverview reducer={reducer} recoveryState={reducerState} 
/>;
-  }
-
-  if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
-    const chArr = reducerState.recovery_information!.challenges;
-    const challengeFeedback = reducerState.challenge_feedback ?? {};
-    const selectedUuid = reducerState.selected_challenge_uuid!;
-    const challenges: {
-      [uuid: string]: ChallengeInfo;
-    } = {};
-    for (const ch of chArr) {
-      challenges[ch.uuid] = ch;
-    }
-    const selectedChallenge = challenges[selectedUuid];
-    const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
-      question: SolveQuestionEntry,
-      sms: SolveSmsEntry,
-      email: SolveEmailEntry,
-      post: SolvePostEntry,
-    };
-    const SolveDialog =
-      dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
-    return (
-      <SolveDialog
-        challenge={selectedChallenge}
-        reducer={reducer}
-        feedback={challengeFeedback[selectedUuid]}
-      />
-    );
-  }
-
-  if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
-    return (
-      <AnastasisClientFrame title="Recovery Finished" hideNext>
-        <h1>Recovery Finished</h1>
-        <p>
-          Secret: 
{bytesToString(decodeCrock(reducerState.core_secret?.value!))}
-        </p>
-      </AnastasisClientFrame>
-    );
-  }
-
-  console.log("unknown state", reducer.currentReducerState);
-  return (
-    <AnastasisClientFrame hideNav title="Bug">
-      <p>Bug: Unknown state.</p>
-      <button onClick={() => reducer.reset()}>Reset</button>
-    </AnastasisClientFrame>
-  );
-};
-
-interface AuthMethodSetupProps {
-  method: string;
-  addAuthMethod: (x: any) => void;
-  cancel: () => void;
-}
-
-function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
-  const [mobileNumber, setMobileNumber] = useState("");
-  const addSmsAuth = () => {
-    props.addAuthMethod({
-      authentication_method: {
-        type: "sms",
-        instructions: `SMS to ${mobileNumber}`,
-        challenge: encodeCrock(stringToBytes(mobileNumber)),
-      },
-    });
-  };
-  const inputRef = useRef<HTMLInputElement>(null);
-  useLayoutEffect(() => {
-    inputRef.current?.focus();
-  }, []);
-  return (
-    <AnastasisClientFrame hideNav title="Add SMS authentication">
-      <div>
-        <p>
-          For SMS authentication, you need to provide a mobile number. When
-          recovering your secret, you will be asked to enter the code you
-          receive via SMS.
-        </p>
-        <label>
-          Mobile number:{" "}
-          <input
-            value={mobileNumber}
-            ref={inputRef}
-            style={{ display: "block" }}
-            autoFocus
-            onChange={(e) => setMobileNumber((e.target as any).value)}
-            type="text"
-          />
-        </label>
-        <div>
-          <button onClick={() => props.cancel()}>Cancel</button>
-          <button onClick={() => addSmsAuth()}>Add</button>
-        </div>
-      </div>
-    </AnastasisClientFrame>
-  );
-}
-
-function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
-  const [questionText, setQuestionText] = useState("");
-  const [answerText, setAnswerText] = useState("");
-  const addQuestionAuth = () =>
-    props.addAuthMethod({
-      authentication_method: {
-        type: "question",
-        instructions: questionText,
-        challenge: encodeCrock(stringToBytes(answerText)),
-      },
-    });
-  return (
-    <AnastasisClientFrame hideNav title="Add Security Question">
-      <div>
-        <p>
-          For security question authentication, you need to provide a question
-          and its answer. When recovering your secret, you will be shown the
-          question and you will need to type the answer exactly as you typed it
-          here.
-        </p>
-        <div>
-          <LabeledInput
-            label="Security question"
-            grabFocus
-            bind={[questionText, setQuestionText]}
-          />
-        </div>
-        <div>
-          <LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
-        </div>
-        <div>
-          <button onClick={() => props.cancel()}>Cancel</button>
-          <button onClick={() => addQuestionAuth()}>Add</button>
-        </div>
-      </div>
-    </AnastasisClientFrame>
-  );
-}
-
-function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
-  const [email, setEmail] = useState("");
-  return (
-    <AnastasisClientFrame hideNav title="Add email authentication">
-      <p>
-        For email authentication, you need to provide an email address. When
-        recovering your secret, you will need to enter the code you receive by
-        email.
-      </p>
-      <div>
-        <LabeledInput
-          label="Email address"
-          grabFocus
-          bind={[email, setEmail]}
-        />
-      </div>
-      <div>
-        <button onClick={() => props.cancel()}>Cancel</button>
-        <button
-          onClick={() =>
-            props.addAuthMethod({
-              authentication_method: {
-                type: "email",
-                instructions: `Email to ${email}`,
-                challenge: encodeCrock(stringToBytes(email)),
-              },
-            })
-          }
-        >
-          Add
-        </button>
-      </div>
-    </AnastasisClientFrame>
-  );
-}
-
-function AuthMethodPostSetup(props: AuthMethodSetupProps) {
-  const [fullName, setFullName] = useState("");
-  const [street, setStreet] = useState("");
-  const [city, setCity] = useState("");
-  const [postcode, setPostcode] = useState("");
-  const [country, setCountry] = useState("");
-
-  const addPostAuth = () => {
-    const challengeJson = {
-      full_name: fullName,
-      street,
-      city,
-      postcode,
-      country,
-    };
-    props.addAuthMethod({
-      authentication_method: {
-        type: "email",
-        instructions: `Letter to address in postal code ${postcode}`,
-        challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
-      },
-    });
-  };
-
-  return (
-    <div class={style.home}>
-      <h1>Add {props.method} authentication</h1>
-      <div>
-        <p>
-          For postal letter authentication, you need to provide a postal
-          address. When recovering your secret, you will be asked to enter a
-          code that you will receive in a letter to that address.
-        </p>
-        <div>
-          <LabeledInput
-            grabFocus
-            label="Full Name"
-            bind={[fullName, setFullName]}
-          />
-        </div>
-        <div>
-          <LabeledInput label="Street" bind={[street, setStreet]} />
-        </div>
-        <div>
-          <LabeledInput label="City" bind={[city, setCity]} />
-        </div>
-        <div>
-          <LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
-        </div>
-        <div>
-          <LabeledInput label="Country" bind={[country, setCountry]} />
-        </div>
-        <div>
-          <button onClick={() => props.cancel()}>Cancel</button>
-          <button onClick={() => addPostAuth()}>Add</button>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function AuthMethodNotImplemented(props: AuthMethodSetupProps) {
-  return (
-    <AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
-      <p>This auth method is not implemented yet, please choose another 
one.</p>
-      <button onClick={() => props.cancel()}>Cancel</button>
-    </AnastasisClientFrame>
-  );
-}
-
-export interface AuthenticationEditorProps {
-  reducer: AnastasisReducerApi;
-  backupState: ReducerStateBackup;
-}
-
-function AuthenticationEditor(props: AuthenticationEditorProps) {
-  const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
-    undefined,
-  );
-  const { reducer, backupState } = props;
-  const providers = backupState.authentication_providers!;
-  const authAvailableSet = new Set<string>();
-  for (const provKey of Object.keys(providers)) {
-    const p = providers[provKey];
-    if ("http_status" in p && (!("error_code" in p)) && p.methods) {
-      for (const meth of p.methods) {
-        authAvailableSet.add(meth.type);
-      }
-    }
-  }
-  if (selectedMethod) {
-    const cancel = () => setSelectedMethod(undefined);
-    const addMethod = (args: any) => {
-      reducer.transition("add_authentication", args);
-      setSelectedMethod(undefined);
-    };
-    const methodMap: Record<
-      string,
-      (props: AuthMethodSetupProps) => h.JSX.Element
-    > = {
-      sms: AuthMethodSmsSetup,
-      question: AuthMethodQuestionSetup,
-      email: AuthMethodEmailSetup,
-      post: AuthMethodPostSetup,
-    };
-    const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
-    return (
-      <AuthSetup
-        cancel={cancel}
-        addAuthMethod={addMethod}
-        method={selectedMethod}
-      />
-    );
-  }
-  function MethodButton(props: { method: string; label: String }) {
-    return (
-      <button
-        disabled={!authAvailableSet.has(props.method)}
-        onClick={() => {
-          setSelectedMethod(props.method);
-          reducer.dismissError();
-        }}
-      >
-        {props.label}
-      </button>
-    );
-  }
-  const configuredAuthMethods: AuthMethod[] =
-    backupState.authentication_methods ?? [];
-  const haveMethodsConfigured = configuredAuthMethods.length;
-  return (
-    <AnastasisClientFrame title="Backup: Configure Authentication Methods">
-      <div>
-        <MethodButton method="sms" label="SMS" />
-        <MethodButton method="email" label="Email" />
-        <MethodButton method="question" label="Question" />
-        <MethodButton method="post" label="Physical Mail" />
-        <MethodButton method="totp" label="TOTP" />
-        <MethodButton method="iban" label="IBAN" />
-      </div>
-      <h2>Configured authentication methods</h2>
-      {haveMethodsConfigured ? (
-        configuredAuthMethods.map((x, i) => {
-          return (
-            <p>
-              {x.type} ({x.instructions}){" "}
-              <button
-                onClick={() =>
-                  reducer.transition("delete_authentication", {
-                    authentication_method: i,
-                  })
-                }
-              >
-                Delete
-              </button>
-            </p>
-          );
-        })
-      ) : (
-        <p>No authentication methods configured yet.</p>
-      )}
-    </AnastasisClientFrame>
-  );
-}
-
-export interface AttributeEntryProps {
-  reducer: AnastasisReducerApi;
-  reducerState: ReducerStateRecovery | ReducerStateBackup;
-}
-
-function AttributeEntry(props: AttributeEntryProps) {
-  const { reducer, reducerState: backupState } = props;
-  const [attrs, setAttrs] = useState<Record<string, string>>(
-    props.reducerState.identity_attributes ?? {},
-  );
-  return (
-    <AnastasisClientFrame
-      title={withProcessLabel(reducer, "Select Country")}
-      onNext={() =>
-        reducer.transition("enter_user_attributes", {
-          identity_attributes: attrs,
-        })
-      }
-    >
-      {backupState.required_attributes.map((x: any, i: number) => {
-        return (
-          <AttributeEntryField
-            isFirst={i == 0}
-            setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
-            spec={x}
-            value={attrs[x.name]}
-          />
-        );
-      })}
-    </AnastasisClientFrame>
-  );
-}
-
-interface LabeledInputProps {
-  label: string;
-  grabFocus?: boolean;
-  bind: [string, (x: string) => void];
-}
-
-function LabeledInput(props: LabeledInputProps) {
-  const inputRef = useRef<HTMLInputElement>(null);
-  useLayoutEffect(() => {
-    if (props.grabFocus) {
-      inputRef.current?.focus();
-    }
-  }, []);
-  return (
-    <label>
-      {props.label}
-      <input
-        value={props.bind[0]}
-        onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)}
-        ref={inputRef}
-        style={{ display: "block" }}
-      />
-    </label>
-  );
-}
-
-export interface AttributeEntryFieldProps {
-  isFirst: boolean;
-  value: string;
-  setValue: (newValue: string) => void;
-  spec: any;
-}
-
-function AttributeEntryField(props: AttributeEntryFieldProps) {
-  return (
-    <div>
-      <LabeledInput
-        grabFocus={props.isFirst}
-        label={props.spec.label}
-        bind={[props.value, props.setValue]}
-      />
-    </div>
-  );
-}
-
-interface ErrorBannerProps {
-  reducer: AnastasisReducerApi;
-}
-
-/**
- * Show a dismissable error banner if there is a current error.
- */
-function ErrorBanner(props: ErrorBannerProps) {
-  const currentError = props.reducer.currentError;
-  if (currentError) {
-    return (
-      <div id={style.error}>
-        <p>Error: {JSON.stringify(currentError)}</p>
-        <button onClick={() => props.reducer.dismissError()}>
-          Dismiss Error
-        </button>
-      </div>
-    );
-  }
-  return null;
-}
-
-export default AnastasisClient;
diff --git a/packages/anastasis-webui/src/scss/DurationPicker.scss 
b/packages/anastasis-webui/src/scss/DurationPicker.scss
new file mode 100644
index 00000000..a3557532
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/DurationPicker.scss
@@ -0,0 +1,71 @@
+
+.rdp-picker {
+  display: flex;
+  height: 175px;
+}
+
+@media (max-width: 400px) {
+  .rdp-picker {
+    width: 250px;
+  }
+}
+
+.rdp-masked-div {
+  overflow: hidden;
+  height: 175px;
+  position: relative;
+}
+
+.rdp-column-container {
+  flex-grow: 1;
+  display: inline-block;
+}
+
+.rdp-column {
+  position: absolute;
+  z-index: 0;
+  width: 100%;
+}
+
+.rdp-reticule {
+  border: 0;
+  border-top: 2px solid rgba(109, 202, 236, 1);
+  height: 2px;
+  position: absolute;
+  width: 80%;
+  margin: 0;
+  z-index: 100;
+  left: 50%;
+  -webkit-transform: translateX(-50%);
+  transform: translateX(-50%);
+}
+
+.rdp-text-overlay {
+  position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 35px;
+  font-size: 20px;
+  left: 50%;
+  -webkit-transform: translateX(-50%);
+  transform: translateX(-50%);
+}
+
+.rdp-cell div {
+  font-size: 17px;
+  color: gray;
+  font-style: italic;
+}
+
+.rdp-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 35px;
+  font-size: 18px;
+}
+
+.rdp-center {
+  font-size: 25px;
+}
diff --git a/packages/anastasis-webui/src/scss/_aside.scss 
b/packages/anastasis-webui/src/scss/_aside.scss
new file mode 100644
index 00000000..c9332b25
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_aside.scss
@@ -0,0 +1,186 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+@include desktop {
+  html {
+    &.has-aside-left {
+      &.has-aside-expanded {
+        nav.navbar,
+        body {
+          padding-left: $aside-width;
+        }
+      }
+      aside.is-placed-left {
+        display: block;
+      }
+    }
+  }
+
+  aside.aside.is-expanded {
+    width: $aside-width;
+
+    .menu-list {
+      @include icon-with-update-mark($aside-icon-width);
+
+      span.menu-item-label {
+        display: inline-block;
+      }
+
+      li.is-active {
+        ul {
+          display: block;
+        }
+        background-color: $body-background-color;
+      }
+    }
+  }
+}
+
+aside.aside {
+  display: none;
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 40;
+  height: 100vh;
+  padding: 0;
+  box-shadow: $aside-box-shadow;
+  background: $aside-background-color;
+
+  .aside-tools {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+    background-color: $aside-tools-background-color;
+    color: $aside-tools-color;
+    line-height: $navbar-height;
+    height: $navbar-height;
+    padding-left: $default-padding * 0.5;
+    flex: 1;
+
+    .icon {
+      margin-right: $default-padding * 0.5;
+    }
+  }
+
+  .menu-list {
+    li {
+      a {
+        &.has-dropdown-icon {
+          position: relative;
+          padding-right: $aside-icon-width;
+
+          .dropdown-icon {
+            position: absolute;
+            top: $size-base * 0.5;
+            right: 0;
+          }
+        }
+      }
+      ul {
+        display: none;
+        border-left: 0;
+        background-color: darken($base-color, 2.5%);
+        padding-left: 0;
+        margin: 0 0 $default-padding * 0.5;
+
+        li {
+          a {
+            padding: $default-padding * 0.5 0 $default-padding * 0.5
+              $default-padding * 0.5;
+            font-size: $aside-submenu-font-size;
+
+            &.has-icon {
+              padding-left: 0;
+            }
+            &.is-active {
+              &:not(:hover) {
+                background: transparent;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .menu-label {
+    padding: 0 $default-padding * 0.5;
+    margin-top: $default-padding * 0.5;
+    margin-bottom: $default-padding * 0.5;
+  }
+}
+
+@include touch {
+  nav.navbar {
+    @include transition(margin-left);
+  }
+  aside.aside {
+    @include transition(left);
+  }
+  html.has-aside-mobile-transition {
+    body {
+      overflow-x: hidden;
+    }
+    body,
+    nav.navbar {
+      width: 100vw;
+    }
+    aside.aside {
+      width: $aside-mobile-width;
+      display: block;
+      left: $aside-mobile-width * -1;
+
+      .image {
+        img {
+          max-width: $aside-mobile-width * 0.33;
+        }
+      }
+
+      .menu-list {
+        li.is-active {
+          ul {
+            display: block;
+          }
+          background-color: $body-background-color;
+        }
+        li {
+          @include icon-with-update-mark($aside-icon-width);
+          margin-top: 8px;
+          margin-bottom: 8px;
+        }
+        a {
+          span.menu-item-label {
+            display: inline-block;
+          }
+        }
+      }
+    }
+  }
+  div.has-aside-mobile-expanded {
+    nav.navbar {
+      margin-left: $aside-mobile-width;
+    }
+    aside.aside {
+      left: 0;
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_card.scss 
b/packages/anastasis-webui/src/scss/_card.scss
new file mode 100644
index 00000000..b2eec27a
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_card.scss
@@ -0,0 +1,69 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+.card:not(:last-child) {
+  margin-bottom: $default-padding;
+}
+
+.card {
+  border-radius: $radius-large;
+  border: $card-border;
+
+  &.has-table {
+    .card-content {
+      padding: 0;
+    }
+    .b-table {
+      border-radius: $radius-large;
+      overflow: hidden;
+    }
+  }
+
+  &.is-card-widget {
+    .card-content {
+      padding: $default-padding * .5;
+    }
+  }
+
+  .card-header {
+    border-bottom: 1px solid $base-color-light;
+  }
+
+  .card-content {
+    hr {
+      margin-left: $card-content-padding * -1;
+      margin-right: $card-content-padding * -1;
+    }
+  }
+
+  .is-widget-icon {
+    .icon {
+      width: 5rem;
+      height: 5rem;
+    }
+  }
+
+  .is-widget-label {
+    .subtitle {
+      color: $grey;
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_custom-calendar.scss 
b/packages/anastasis-webui/src/scss/_custom-calendar.scss
new file mode 100644
index 00000000..9ac877ce
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_custom-calendar.scss
@@ -0,0 +1,254 @@
+/*
+ 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/>
+ */
+
+:root {
+  --primary-color: #3298dc;
+  
+  --primary-text-color-dark: rgba(0,0,0,.87);
+  --secondary-text-color-dark: rgba(0,0,0,.57);
+  --disabled-text-color-dark: rgba(0,0,0,.13);
+  
+  --primary-text-color-light: rgba(255,255,255,.87);
+  --secondary-text-color-light: rgba(255,255,255,.57);
+  --disabled-text-color-light: rgba(255,255,255,.13);
+  
+  --font-stack: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  
+  --primary-card-color: #fff;
+  --primary-background-color: #f2f2f2;
+  
+  --box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
+                      0 1px 2px rgba(0, 0, 0, 0.24);
+  --box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
+                      0 3px 6px rgba(0, 0, 0, 0.23);
+  --box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
+                      0 6px 6px rgba(0, 0, 0, 0.23);
+  --box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
+                      0 10px 10px rgba(0, 0, 0, 0.22);
+}
+
+
+.datePicker {
+  text-align: left;
+  background: var(--primary-card-color);
+  border-radius: 3px;
+  z-index: 200;
+  position: fixed;
+  height: auto;
+  max-height: 90vh;
+  width: 90vw;
+  max-width: 448px;
+  transform-origin: top left;
+  transition: transform .22s ease-in-out, opacity .22s ease-in-out;
+  top: 50%;
+  left: 50%;
+  opacity: 0;
+  transform: scale(0) translate(-50%, -50%);
+  user-select: none;
+
+  &.datePicker--opened {
+    opacity: 1;
+    transform: scale(1) translate(-50%, -50%);
+  }
+  
+  .datePicker--titles {
+    border-top-left-radius: 3px;
+    border-top-right-radius: 3px;
+    padding: 24px;
+    height: 100px;
+    background: var(--primary-color);
+
+    h2, h3 {
+      cursor: pointer;
+      color: #fff;
+      line-height: 1;
+      padding: 0;
+      margin: 0;
+      font-size: 32px;
+    }
+
+    h3 {
+      color: rgba(255,255,255,.57);
+      font-size: 18px;
+      padding-bottom: 2px;
+    }
+  }
+
+  nav {
+    padding: 20px;
+    height: 56px;
+
+    h4 {
+      width: calc(100% - 60px);
+      text-align: center;
+      display: inline-block;
+      padding: 0;
+      font-size: 14px;
+      line-height: 24px;
+      margin: 0;
+      position: relative;
+      top: -9px;
+      color: var(--primary-text-color);
+    }
+
+    i {
+      cursor: pointer;
+      color: var(--secondary-text-color);
+      font-size: 26px;
+      user-select: none;
+      border-radius: 50%;
+      
+      &:hover {
+        background: var(--disabled-text-color-dark);
+      }
+    }
+  }
+  
+  .datePicker--scroll {
+    overflow-y: auto;
+    max-height: calc(90vh - 56px - 100px);
+  }
+
+  .datePicker--calendar {
+    padding: 0 20px;
+
+    .datePicker--dayNames {
+      width: 100%;
+      display: grid;
+      text-align: center;
+      
+      // there's probably a better way to do this, but wanted to try out CSS 
grid
+      grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) 
calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+
+      span {
+        color: var(--secondary-text-color-dark);
+        font-size: 14px;
+        line-height: 42px;
+        display: inline-grid;
+      }
+    }
+
+    .datePicker--days {
+      width: 100%;
+      display: grid;
+      text-align: center;
+      grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) 
calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+
+      span {
+        color: var(--primary-text-color-dark);
+        line-height: 42px;
+        font-size: 14px;
+        display: inline-grid;
+        transition: color .22s;
+        height: 42px;
+        position: relative;
+        cursor: pointer;
+        user-select: none;
+        border-radius: 50%;
+
+        &::before {
+          content: '';
+          position: absolute;
+          z-index: -1;
+          height: 42px;
+          width: 42px;
+          left: calc(50% - 21px);
+          background: var(--primary-color);
+          border-radius: 50%;
+          transition: transform .22s, opacity .22s;
+          transform: scale(0);
+          opacity: 0;
+        }
+        
+        &[disabled=true] {
+          cursor: unset;
+        }
+
+        &.datePicker--today {
+          font-weight: 700;
+        }
+
+        &.datePicker--selected {
+          color: rgba(255,255,255,.87);
+
+          &:before {
+            transform: scale(1);
+            opacity: 1;
+          }
+        }
+      }
+    }
+  }
+  
+  .datePicker--selectYear {
+    padding: 0 20px;
+    display: block;
+    width: 100%;
+    text-align: center;
+    max-height: 362px;
+    
+    span {
+      display: block;
+      width: 100%;
+      font-size: 24px;
+      margin: 20px auto;
+      cursor: pointer;
+      
+      &.selected {
+        font-size: 42px;
+        color: var(--primary-color);
+      }
+    }
+  }
+
+  div.datePicker--actions {
+    width: 100%;
+    padding: 8px;
+    text-align: right;
+
+    button {
+      margin-bottom: 0;
+      font-size: 15px;
+      cursor: pointer;
+      color: var(--primary-text-color);
+      border: none;
+      margin-left: 8px;
+      min-width: 64px;
+      line-height: 36px;
+      background-color: transparent;
+      appearance: none;
+      padding: 0 16px;
+      border-radius: 3px;
+      transition: background-color .13s;
+
+      &:hover, &:focus {
+        outline: none;
+        background-color: var(--disabled-text-color-dark);
+      }
+    }
+  }
+}
+
+.datePicker--background {
+  z-index: 199;
+  position: fixed;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: rgba(0,0,0,.52);
+  animation: fadeIn .22s forwards;
+}
diff --git a/packages/anastasis-webui/src/scss/_footer.scss 
b/packages/anastasis-webui/src/scss/_footer.scss
new file mode 100644
index 00000000..027a5ca8
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_footer.scss
@@ -0,0 +1,35 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+footer.footer {
+  .logo {
+    img {
+      width: auto;
+      height: $footer-logo-height;
+    }
+  }
+}
+
+@include mobile {
+  .footer-copyright {
+    text-align: center;
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_form.scss 
b/packages/anastasis-webui/src/scss/_form.scss
new file mode 100644
index 00000000..71f0d4da
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_form.scss
@@ -0,0 +1,64 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+.field {
+  &.has-check {
+    .field-body {
+      margin-top: $default-padding * .125;
+    }
+  }
+  .control {
+    .mdi-24px.mdi-set, .mdi-24px.mdi:before {
+      font-size: inherit;
+    }
+  }
+}
+.upload {
+  .upload-draggable {
+    display: block;
+  }
+}
+
+.input, .textarea, select {
+  box-shadow: none;
+
+  &:focus, &:active {
+    box-shadow: none!important;
+  }
+}
+
+.switch input[type=checkbox]+.check:before {
+  box-shadow: none;
+}
+
+.switch, .b-checkbox.checkbox {
+  input[type=checkbox] {
+    &:focus + .check, &:focus:checked + .check {
+      box-shadow: none!important;
+    }
+  }
+}
+
+.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
+  &+.check {
+    border: $checkbox-border;
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_hero-bar.scss 
b/packages/anastasis-webui/src/scss/_hero-bar.scss
new file mode 100644
index 00000000..90b67a2e
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_hero-bar.scss
@@ -0,0 +1,55 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+section.hero.is-hero-bar {
+  background-color: $hero-bar-background;
+  border-bottom: $light-border;
+
+  .hero-body {
+    padding: $default-padding;
+
+    .level-item {
+      &.is-hero-avatar-item {
+        margin-right: $default-padding;
+      }
+
+      > div > .level {
+        margin-bottom: $default-padding * .5;
+      }
+
+      .subtitle + p {
+        margin-top: $default-padding * .5;
+      }
+    }
+
+    .button {
+      &.is-hero-button {
+        background-color: rgba($white, .5);
+        font-weight: 300;
+        @include transition(background-color);
+
+        &:hover {
+          background-color: $white;
+        }
+      }
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_loading.scss 
b/packages/anastasis-webui/src/scss/_loading.scss
new file mode 100644
index 00000000..d25bf804
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_loading.scss
@@ -0,0 +1,51 @@
+/*
+ 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/>
+ */
+
+.lds-ring {
+  display: inline-block;
+  position: relative;
+  width: 80px;
+  height: 80px;
+}
+.lds-ring div {
+  box-sizing: border-box;
+  display: block;
+  position: absolute;
+  width: 64px;
+  height: 64px;
+  margin: 8px;
+  border: 8px solid black;
+  border-radius: 50%;
+  animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+  border-color: black transparent transparent transparent;
+}
+.lds-ring div:nth-child(1) {
+  animation-delay: -0.45s;
+}
+.lds-ring div:nth-child(2) {
+  animation-delay: -0.3s;
+}
+.lds-ring div:nth-child(3) {
+  animation-delay: -0.15s;
+}
+@keyframes lds-ring {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_main-section.scss 
b/packages/anastasis-webui/src/scss/_main-section.scss
new file mode 100644
index 00000000..1a4fad81
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_main-section.scss
@@ -0,0 +1,24 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+section.section.is-main-section {
+  padding-top: $default-padding;
+}
diff --git a/packages/anastasis-webui/src/scss/_misc.scss 
b/packages/anastasis-webui/src/scss/_misc.scss
new file mode 100644
index 00000000..65bd28db
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_misc.scss
@@ -0,0 +1,50 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+.is-user-avatar {
+  &.has-max-width {
+    max-width: $size-base * 7;
+  }
+
+  &.is-aligned-center {
+    margin: 0 auto;
+  }
+
+  img {
+    margin: 0 auto;
+    border-radius: $radius-rounded;
+  }
+}
+
+.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;
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss 
b/packages/anastasis-webui/src/scss/_mixins.scss
new file mode 100644
index 00000000..0809033e
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_mixins.scss
@@ -0,0 +1,34 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+@mixin transition($t) {
+  transition: $t 250ms ease-in-out 50ms;
+}
+
+@mixin icon-with-update-mark ($icon-base-width) {
+  .icon {
+    width: $icon-base-width;
+
+    &.has-update-mark:after {
+      right: ($icon-base-width / 2) - .85;
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_modal.scss 
b/packages/anastasis-webui/src/scss/_modal.scss
new file mode 100644
index 00000000..3edbb8d3
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_modal.scss
@@ -0,0 +1,35 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+.modal-card {
+  width: $modal-card-width;
+}
+
+.modal-card-foot {
+  background-color: $modal-card-foot-background-color;
+}
+
+@include mobile {
+  .modal .animation-content .modal-card {
+    width: $modal-card-width-mobile;
+    margin: 0 auto;
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_nav-bar.scss 
b/packages/anastasis-webui/src/scss/_nav-bar.scss
new file mode 100644
index 00000000..09f1e232
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_nav-bar.scss
@@ -0,0 +1,144 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+nav.navbar {
+  box-shadow: $navbar-box-shadow;
+
+  .navbar-item {
+    &.has-user-avatar {
+      .is-user-avatar {
+        margin-right: $default-padding * .5;
+        display: inline-flex;
+        width: $navbar-avatar-size;
+        height: $navbar-avatar-size;
+      }
+    }
+
+    &.has-divider {
+      border-right: $navbar-divider-border;
+    }
+
+    &.no-left-space {
+      padding-left: 0;
+    }
+
+    &.has-dropdown {
+      padding-right: 0;
+      padding-left: 0;
+
+      .navbar-link {
+        padding-right: $navbar-item-h-padding;
+        padding-left: $navbar-item-h-padding;
+      }
+    }
+
+    &.has-control {
+      padding-top: 0;
+      padding-bottom: 0;
+    }
+
+    .control {
+      .input {
+        color: $navbar-input-color;
+        border: 0;
+        box-shadow: none;
+        background: transparent;
+
+        &::placeholder {
+          color: $navbar-input-placeholder-color;
+        }
+      }
+    }
+  }
+}
+
+@include touch {
+  nav.navbar {
+    display: flex;
+    padding-right: 0;
+
+    .navbar-brand {
+      flex: 1;
+
+      &.is-right {
+        flex: none;
+      }
+    }
+
+    .navbar-item {
+      &.no-left-space-touch {
+        padding-left: 0;
+      }
+    }
+
+    .navbar-menu {
+      position: absolute;
+      width: 100vw;
+      padding-top: 0;
+      top: $navbar-height;
+      left: 0;
+
+      .navbar-item {
+        .icon:first-child {
+          margin-right: $default-padding * .5;
+        }
+
+        &.has-dropdown {
+          >.navbar-link {
+            background-color: $white-ter;
+            .icon:last-child {
+              display: none;
+            }
+          }
+        }
+
+        &.has-user-avatar {
+          >.navbar-link {
+            display: flex;
+            align-items: center;
+            padding-top: $default-padding * .5;
+            padding-bottom: $default-padding * .5;
+          }
+        }
+      }
+    }
+  }
+}
+
+@include desktop {
+  nav.navbar {
+    .navbar-item {
+      padding-right: $navbar-item-h-padding;
+      padding-left: $navbar-item-h-padding;
+
+      &:not(.is-desktop-icon-only) {
+        .icon:first-child {
+          margin-right: $default-padding * .5;
+        }
+      }
+      &.is-desktop-icon-only {
+        span:not(.icon) {
+          display: none;
+        }
+      }
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_table.scss 
b/packages/anastasis-webui/src/scss/_table.scss
new file mode 100644
index 00000000..9cf6f4dc
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_table.scss
@@ -0,0 +1,173 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+table.table {
+  thead {
+    th {
+      border-bottom-width: 1px;
+    }
+  }
+
+  td, th {
+    &.checkbox-cell {
+      .b-checkbox.checkbox:not(.button) {
+        margin-right: 0;
+        width: 20px;
+
+        .control-label {
+          display: none;
+          padding: 0;
+        }
+      }
+    }
+  }
+
+  td {
+    .image {
+      margin: 0 auto;
+      width: $table-avatar-size;
+      height: $table-avatar-size;
+    }
+
+    &.is-progress-col {
+      min-width: 5rem;
+      vertical-align: middle;
+    }
+  }
+}
+
+.b-table {
+  .table {
+    border: 0;
+    border-radius: 0;
+  }
+
+  /* This stylizes buefy's pagination */
+  .table-wrapper {
+    margin-bottom: 0;
+  }
+
+  .table-wrapper + .level {
+    padding: $notification-padding;
+    padding-left: $card-content-padding;
+    padding-right: $card-content-padding;
+    margin: 0;
+    border-top: $base-color-light;
+    background: $notification-background-color;
+
+    .pagination-link {
+      background: $button-background-color;
+      color: $button-color;
+      border-color: $button-border-color;
+
+      &.is-current {
+        border-color: $button-active-border-color;
+      }
+    }
+
+    .pagination-previous, .pagination-next, .pagination-link {
+      border-color: $button-border-color;
+      color: $base-color;
+
+      &[disabled] {
+        background-color: transparent;
+      }
+    }
+  }
+}
+
+@include mobile {
+  .card {
+    &.has-table {
+      .b-table {
+        .table-wrapper + .level {
+          .level-left + .level-right {
+            margin-top: 0;
+          }
+        }
+      }
+    }
+    &.has-mobile-sort-spaced {
+      .b-table {
+        .field.table-mobile-sort {
+          padding-top: $default-padding * .5;
+        }
+      }
+    }
+  }
+  .b-table {
+    .field.table-mobile-sort {
+      padding: 0 $default-padding * .5;
+    }
+
+    .table-wrapper.has-mobile-cards {
+      tr {
+        box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
+        margin-bottom: 3px!important;
+      }
+      td {
+        &.is-progress-col {
+          span, progress {
+            display: flex;
+            width: 45%;
+            align-items: center;
+            align-self: center;
+          }
+        }
+
+        &.checkbox-cell, &.is-image-cell {
+          border-bottom: 0!important;
+        }
+
+        &.checkbox-cell, &.is-actions-cell {
+          &:before {
+            display: none;
+          }
+        }
+
+        &.has-no-head-mobile {
+          &:before {
+            display: none;
+          }
+
+          span {
+            display: block;
+            width: 100%;
+          }
+
+          &.is-progress-col {
+            progress {
+              width: 100%;
+            }
+          }
+
+          &.is-image-cell {
+            .image {
+              width: $table-avatar-size-mobile;
+              height: auto;
+              margin: 0 auto $default-padding * .25;
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_theme-default.scss 
b/packages/anastasis-webui/src/scss/_theme-default.scss
new file mode 100644
index 00000000..538dfd4d
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_theme-default.scss
@@ -0,0 +1,136 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+/* We'll need some initial vars to use here */
+@import "node_modules/bulma/sass/utilities/initial-variables";
+
+/* Base: Size  */
+$size-base: 1rem;
+$default-padding: $size-base * 1.5;
+
+/* Default font */
+$family-sans-serif: "Nunito", sans-serif;
+
+/* Base color */
+$base-color: #2e323a;
+$base-color-light: rgba(24, 28, 33, 0.06);
+
+/* General overrides */
+$primary: $turquoise;
+$body-background-color: #f8f8f8;
+$link: $blue;
+$link-visited: $purple;
+$light-border: 1px solid $base-color-light;
+$hr-height: 1px;
+
+/* NavBar: specifics */
+$navbar-input-color: $grey-darker;
+$navbar-input-placeholder-color: $grey-lighter;
+$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, 0.04);
+$navbar-divider-border: 1px solid rgba($grey-lighter, 0.25);
+$navbar-item-h-padding: $default-padding * 0.75;
+$navbar-avatar-size: 1.75rem;
+
+/* Aside: Bulma override */
+$menu-item-radius: 0;
+$menu-list-link-padding: $size-base * 0.5 0;
+$menu-label-color: lighten($base-color, 25%);
+$menu-item-color: lighten($base-color, 30%);
+$menu-item-hover-color: $white;
+$menu-item-hover-background-color: darken($base-color, 3.5%);
+$menu-item-active-color: $white;
+$menu-item-active-background-color: darken($base-color, 2.5%);
+
+/* Aside: specifics */
+$aside-width: $size-base * 14;
+$aside-mobile-width: $size-base * 15;
+$aside-icon-width: $size-base * 3;
+$aside-submenu-font-size: $size-base * 0.95;
+$aside-box-shadow: none;
+$aside-background-color: $base-color;
+$aside-tools-background-color: darken($aside-background-color, 10%);
+$aside-tools-color: $white;
+
+/* Title Bar: specifics */
+$title-bar-color: $grey;
+$title-bar-active-color: $black-ter;
+
+/* Hero Bar: specifics */
+$hero-bar-background: $white;
+
+/* Card: Bulma override */
+$card-shadow: none;
+$card-header-shadow: none;
+
+/* Card: specifics */
+$card-border: 1px solid $base-color-light;
+$card-header-border-bottom-color: $base-color-light;
+
+/* Table: Bulma override */
+$table-cell-border: 1px solid $white-bis;
+
+/* Table: specifics */
+$table-avatar-size: $size-base * 1.5;
+$table-avatar-size-mobile: 25vw;
+
+/* Form */
+$checkbox-border: 1px solid $base-color;
+
+/* Modal card: Bulma override */
+$modal-card-head-background-color: $white-ter;
+$modal-card-title-size: $size-base;
+$modal-card-body-padding: $default-padding 20px;
+$modal-card-head-border-bottom: 1px solid $white-ter;
+$modal-card-foot-border-top: 0;
+
+/* Modal card: specifics */
+$modal-card-width: 80vw;
+$modal-card-width-mobile: 90vw;
+$modal-card-foot-background-color: $white-ter;
+
+/* Notification: Bulma override */
+$notification-padding: $default-padding * 0.75 $default-padding;
+
+/* Footer: Bulma override */
+$footer-background-color: $white;
+$footer-padding: $default-padding * 0.33 $default-padding;
+
+/* Footer: specifics */
+$footer-logo-height: $size-base * 2;
+
+/* Progress: Bulma override */
+$progress-bar-background-color: $grey-lighter;
+
+/* Icon: specifics */
+$icon-update-mark-size: $size-base * 0.5;
+$icon-update-mark-color: $yellow;
+
+$input-disabled-border-color: $grey-lighter;
+$table-row-hover-background-color: hsl(0, 0%, 80%);
+
+.menu-list {
+  div {
+    border-radius: $menu-item-radius;
+    color: $menu-item-color;
+    display: block;
+    padding: $menu-list-link-padding;
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/_tiles.scss 
b/packages/anastasis-webui/src/scss/_tiles.scss
new file mode 100644
index 00000000..94fc04e7
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_tiles.scss
@@ -0,0 +1,25 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+
+.is-tiles-wrapper {
+  margin-bottom: $default-padding;
+}
diff --git a/packages/anastasis-webui/src/scss/_title-bar.scss 
b/packages/anastasis-webui/src/scss/_title-bar.scss
new file mode 100644
index 00000000..736f26cb
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/_title-bar.scss
@@ -0,0 +1,50 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+section.section.is-title-bar {
+  padding: $default-padding;
+  border-bottom: $light-border;
+
+  ul {
+    li {
+      display: inline-block;
+      padding: 0 $default-padding * .5 0 0;
+      font-size: $default-padding;
+      color: $title-bar-color;
+
+      &:after {
+        display: inline-block;
+        content: '/';
+        padding-left: $default-padding * .5;
+      }
+
+      &:last-child {
+        padding-right: 0;
+        font-weight: 900;
+        color: $title-bar-active-color;
+
+        &:after {
+          display: none;
+        }
+      }
+    }
+  }
+}
diff --git a/packages/anastasis-webui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf 
b/packages/anastasis-webui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
new file mode 100644
index 00000000..7665ee33
Binary files /dev/null and 
b/packages/anastasis-webui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf differ
diff --git a/packages/anastasis-webui/src/scss/fonts/nunito.css 
b/packages/anastasis-webui/src/scss/fonts/nunito.css
new file mode 100644
index 00000000..ab30db36
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/fonts/nunito.css
@@ -0,0 +1,22 @@
+/*
+ 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/>
+ */
+
+@font-face {
+  font-family: 'Nunito';
+  font-style: normal;
+  font-weight: 400;
+  src: url(./XRXV3I6Li01BKofINeaE.ttf) format('truetype');
+}
diff --git 
a/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
new file mode 100644
index 00000000..ab6b25de
Binary files /dev/null and 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
 differ
diff --git 
a/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
new file mode 100644
index 00000000..824be10f
Binary files /dev/null and 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
 differ
diff --git 
a/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
new file mode 100644
index 00000000..7e087c1d
Binary files /dev/null and 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
 differ
diff --git 
a/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
new file mode 100644
index 00000000..b5caa4dd
Binary files /dev/null and 
b/packages/anastasis-webui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
 differ
diff --git 
a/packages/anastasis-webui/src/scss/icons/materialdesignicons-4.9.95.min.css 
b/packages/anastasis-webui/src/scss/icons/materialdesignicons-4.9.95.min.css
new file mode 100644
index 00000000..24a89d63
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/icons/materialdesignicons-4.9.95.min.css
@@ -0,0 +1,3 @@
+@font-face{font-family:"Material Design 
Icons";src:url("./fonts/materialdesignicons-webfont-4.9.95.eot");src:url("./fonts/materialdesignicons-webfont-4.9.95.woff2")
 format("woff2"),url("./fonts/materialdesignicons-webfont-4.9.95.woff") 
format("woff"),url("./fonts/materialdesignicons-webfont-4.9.95.ttf") 
format("truetype");font-weight:normal;font-style:normal}.mdi:before,.mdi-set{display:inline-block;font:normal
 normal normal 24px/1 "Material Design Icons";font-size:inherit;text-rendering 
[...]
+
+/*# sourceMappingURL=materialdesignicons.css.map */
diff --git a/packages/anastasis-webui/src/scss/libs/_all.scss 
b/packages/anastasis-webui/src/scss/libs/_all.scss
new file mode 100644
index 00000000..08bd76cd
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/libs/_all.scss
@@ -0,0 +1,29 @@
+/*
+ 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/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+@import "node_modules/bulma-radio/bulma-radio";
+// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
+@import "node_modules/bulma-checkbox/bulma-checkbox";
+// @import "node_modules/bulma-switch-control/bulma-switch-control";
+// @import "node_modules/bulma-upload-control/bulma-upload-control";
+
+/* Bulma */
+@import "node_modules/bulma/bulma";
diff --git a/packages/anastasis-webui/src/scss/main.scss 
b/packages/anastasis-webui/src/scss/main.scss
new file mode 100644
index 00000000..30b7f5d7
--- /dev/null
+++ b/packages/anastasis-webui/src/scss/main.scss
@@ -0,0 +1,191 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+/* Theme style (colors & sizes) */
+@import "theme-default";
+
+/* Core Libs & Lib configs */
+@import "libs/all";
+
+/* Mixins */
+@import "mixins";
+
+/* Theme components */
+@import "nav-bar";
+@import "aside";
+@import "title-bar";
+@import "hero-bar";
+@import "card";
+@import "table";
+@import "tiles";
+@import "form";
+@import "main-section";
+@import "modal";
+@import "footer";
+@import "misc";
+@import "custom-calendar";
+@import "loading";
+
+@import "fonts/nunito.css";
+@import "icons/materialdesignicons-4.9.95.min.css";
+
+$tooltip-color: red;
+
+@import 
"../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
+// @import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
+
+.notification {
+  background-color: transparent;
+}
+
+.timeline .timeline-item .timeline-content {
+  padding-top: 0;
+}
+
+.timeline .timeline-item:last-child::before {
+  display: none;
+}
+
+.timeline .timeline-item .timeline-marker {
+  top: 0;
+}
+
+.toast {
+  position: absolute;
+  width: 60%;
+  margin-left: 10%;
+  margin-right: 10%;
+  z-index: 999;
+
+  display: flex;
+  flex-direction: column;
+  padding: 15px;
+  text-align: center;
+  pointer-events: none;
+}
+
+.toast > .message {
+  white-space: pre-wrap;
+  opacity: 80%;
+}
+
+div {
+  &.is-loading {
+    position: relative;
+    pointer-events: none;
+    opacity: 0.5;
+    &:after {
+      // @include loader;
+      position: absolute;
+      top: calc(50% - 2.5em);
+      left: calc(50% - 2.5em);
+      width: 5em;
+      height: 5em;
+      border-width: 0.25em;
+    }
+  }
+}
+
+input[type="checkbox"]:indeterminate + .check {
+  background: red !important;
+}
+
+.right-sticky {
+  position: sticky;
+  right: 0px;
+  background-color: $white;
+}
+
+.right-sticky .buttons {
+  flex-wrap: nowrap;
+}
+
+.table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky {
+  background-color: #fafafa;
+}
+
+tr:hover .right-sticky {
+  background-color: hsl(0, 0%, 80%);
+}
+.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
+  background-color: hsl(0, 0%, 95%);
+}
+
+.content-full-size {
+  height: calc(100% - 3rem);
+  position: absolute;
+  width: calc(100% - 14rem);
+  display: flex;
+}
+
+.content-full-size .column .card {
+  min-width: 200px;
+}
+
+@include touch {
+  .content-full-size {
+    height: 100%;
+    position: absolute;
+    width: 100%;
+  }
+}
+
+.column.is-half {
+  flex: none;
+  width: 50%;
+}
+
+input:read-only {
+  cursor: initial;
+}
+
+[data-tooltip]:before {
+  max-width: 15rem;
+  width: max-content;
+  text-align: left;
+  transition: opacity 0.1s linear 1s;
+  // transform: inherit !important;
+  white-space: pre-wrap !important;
+  font-weight: normal;
+  // position: relative;
+}
+
+.icon[data-tooltip]:before {
+  transition: none;
+  z-index: 5;
+}
+
+span[data-tooltip] {
+  border-bottom: none;
+}
+
+div[data-tooltip]::before {
+  position: absolute;
+}
+
+.modal-card-body > p {
+  padding: 1em;
+}
+
+.modal-card-body > p.warning {
+  background-color: #fffbdd;
+  border: solid 1px #f2e9bf;
+}
diff --git a/packages/anastasis-webui/src/template.html 
b/packages/anastasis-webui/src/template.html
index 770c48b2..351f1829 100644
--- a/packages/anastasis-webui/src/template.html
+++ b/packages/anastasis-webui/src/template.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="en" class="has-aside-left has-aside-mobile-transition 
has-navbar-fixed-top has-aside-expanded">
        <head>
                <meta charset="utf-8">
                <title><% preact.title %></title>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 84fcccce..30b9e8d0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,42 +31,56 @@ importers:
 
   packages/anastasis-webui:
     specifiers:
+      '@creativebulma/bulma-tooltip': ^1.2.0
       '@gnu-taler/taler-util': workspace:^0.8.3
       '@types/enzyme': ^3.10.5
       '@types/jest': ^26.0.8
       '@typescript-eslint/eslint-plugin': ^2.25.0
       '@typescript-eslint/parser': ^2.25.0
       anastasis-core: workspace:^0.0.1
+      bulma: ^0.9.3
+      bulma-checkbox: ^1.1.1
+      bulma-radio: ^1.1.1
       enzyme: ^3.11.0
       enzyme-adapter-preact-pure: ^3.1.0
       eslint: ^6.8.0
       eslint-config-preact: ^1.1.1
+      jed: 1.1.1
       jest: ^26.2.2
       jest-preset-preact: ^4.0.2
       preact: ^10.3.1
       preact-cli: ^3.2.2
       preact-render-to-string: ^5.1.4
       preact-router: ^3.2.1
+      sass: ^1.32.13
+      sass-loader: ^10.1.1
       sirv-cli: ^1.0.0-next.3
       typescript: ^3.7.5
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
       anastasis-core: link:../anastasis-core
+      jed: 1.1.1
       preact: 10.5.14
       preact-render-to-string: 5.1.19_preact@10.5.14
       preact-router: 3.2.1_preact@10.5.14
     devDependencies:
+      '@creativebulma/bulma-tooltip': 1.2.0
       '@types/enzyme': 3.10.9
       '@types/jest': 26.0.24
       '@typescript-eslint/eslint-plugin': 
2.34.0_2b015b1c4b7c4a3ed9a197dc233b1a35
       '@typescript-eslint/parser': 2.34.0_eslint@6.8.0+typescript@3.9.10
+      bulma: 0.9.3
+      bulma-checkbox: 1.1.1
+      bulma-radio: 1.1.1
       enzyme: 3.11.0
       enzyme-adapter-preact-pure: 3.1.0_enzyme@3.11.0+preact@10.5.14
       eslint: 6.8.0
       eslint-config-preact: 1.1.4_eslint@6.8.0+typescript@3.9.10
       jest: 26.6.3
       jest-preset-preact: 4.0.2_9b3f24ae35a87c3c82fffbe3fdf70e1e
-      preact-cli: 3.2.2_517d24bd855b57d7e424aceed04e063b
+      preact-cli: 3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7
+      sass: 1.43.2
+      sass-loader: 10.2.0_sass@1.43.2
       sirv-cli: 1.0.14
       typescript: 3.9.10
 
@@ -3570,6 +3584,10 @@ packages:
       arrify: 1.0.1
     dev: true
 
+  /@creativebulma/bulma-tooltip/1.2.0:
+    resolution: {integrity: 
sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==}
+    dev: true
+
   /@emotion/cache/10.0.29:
     resolution: {integrity: 
sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==}
     dependencies:
@@ -4607,7 +4625,7 @@ packages:
     dependencies:
       '@types/estree': 0.0.39
       estree-walker: 1.0.1
-      picomatch: 2.3.0
+      picomatch: 2.2.2
       rollup: 2.56.2
     dev: true
 
@@ -8365,6 +8383,22 @@ packages:
     resolution: {integrity: sha1-y5T662HIaWRR2zZTThQi+U8K7og=}
     dev: true
 
+  /bulma-checkbox/1.1.1:
+    resolution: {integrity: 
sha512-16aTRbXQBCdfk8nrWSVJCasD28FudeVF+G+mZfMJc2N/xTcU4XXjzQ6Iya1neKOgXkXQMx9nJOH2n8H7LRztNg==}
+    dependencies:
+      bulma: 0.9.3
+    dev: true
+
+  /bulma-radio/1.1.1:
+    resolution: {integrity: 
sha512-aIHuMbpBGyZYx8KxbQRdjIy/0M9WHWz5VyxMggwxmCadnN0gd7gC/G96WUy9mhaoIfo9yX/Cf8pKQNinKH+w7w==}
+    dependencies:
+      bulma: 0.9.3
+    dev: true
+
+  /bulma/0.9.3:
+    resolution: {integrity: 
sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==}
+    dev: true
+
   /bytes/3.0.0:
     resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=}
     engines: {node: '>= 0.8'}
@@ -10784,18 +10818,18 @@ packages:
     peerDependencies:
       eslint: ^3 || ^4 || ^5 || ^6 || ^7
     dependencies:
-      array-includes: 3.1.3
+      array-includes: 3.1.2
       array.prototype.flatmap: 1.2.4
       doctrine: 2.1.0
       eslint: 6.8.0
       has: 1.0.3
       jsx-ast-utils: 3.2.0
-      object.entries: 1.1.4
-      object.fromentries: 2.0.4
-      object.values: 1.1.4
+      object.entries: 1.1.3
+      object.fromentries: 2.0.3
+      object.values: 1.1.2
       prop-types: 15.7.2
-      resolve: 1.20.0
-      string.prototype.matchall: 4.0.5
+      resolve: 1.19.0
+      string.prototype.matchall: 4.0.3
     dev: true
 
   /eslint-plugin-react/7.22.0_eslint@7.18.0:
@@ -16852,6 +16886,116 @@ packages:
       - webpack-command
     dev: true
 
+  /preact-cli/3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7:
+    resolution: {integrity: 
sha512-42aUanAb/AqHHvnfb/IwJw9UhY5iuHkGRBv3TrTsQMrq0Ee8Z84r+HS8wjGI0aHHb0R8tnHI0hhllWgmNhjB/Q==}
+    engines: {node: '>=12'}
+    hasBin: true
+    peerDependencies:
+      less-loader: ^7.3.0
+      preact: '*'
+      preact-render-to-string: '*'
+      sass-loader: ^10.2.0
+      stylus-loader: ^4.3.3
+    peerDependenciesMeta:
+      less-loader:
+        optional: true
+      sass-loader:
+        optional: true
+      stylus-loader:
+        optional: true
+    dependencies:
+      '@babel/core': 7.15.0
+      '@babel/plugin-proposal-class-properties': 7.14.5_@babel+core@7.15.0
+      '@babel/plugin-proposal-decorators': 7.14.5_@babel+core@7.15.0
+      '@babel/plugin-proposal-object-rest-spread': 7.14.7_@babel+core@7.15.0
+      '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.15.0
+      '@babel/plugin-transform-object-assign': 7.14.5_@babel+core@7.15.0
+      '@babel/plugin-transform-react-jsx': 7.14.9_@babel+core@7.15.0
+      '@babel/preset-env': 7.15.0_@babel+core@7.15.0
+      '@babel/preset-typescript': 7.15.0_@babel+core@7.15.0
+      '@preact/async-loader': 3.0.1_preact@10.5.14
+      '@prefresh/babel-plugin': 0.4.1
+      '@prefresh/webpack': 3.3.2_b4d84c08f02729896cbfdece19209372
+      autoprefixer: 10.3.1_postcss@8.3.6
+      babel-esm-plugin: 0.9.0_webpack@4.46.0
+      babel-loader: 8.2.2_be352a5a80662835a7707f972edfcfde
+      babel-plugin-macros: 3.1.0
+      babel-plugin-transform-react-remove-prop-types: 0.4.24
+      browserlist: 1.0.1
+      browserslist: 4.16.8
+      compression-webpack-plugin: 6.1.1_webpack@4.46.0
+      console-clear: 1.1.1
+      copy-webpack-plugin: 6.4.1_webpack@4.46.0
+      critters-webpack-plugin: 2.5.0
+      cross-spawn-promise: 0.10.2
+      css-loader: 5.2.7_webpack@4.46.0
+      ejs-loader: 0.5.0
+      envinfo: 7.8.1
+      esm: 3.2.25
+      fast-async: 6.3.8
+      file-loader: 6.2.0_webpack@4.46.0
+      fork-ts-checker-webpack-plugin: 4.1.6
+      get-port: 5.1.1
+      gittar: 0.1.1
+      glob: 7.1.7
+      html-webpack-exclude-assets-plugin: 0.0.7
+      html-webpack-plugin: 3.2.0_webpack@4.46.0
+      ip: 1.1.5
+      isomorphic-unfetch: 3.1.0
+      kleur: 4.1.4
+      loader-utils: 2.0.0
+      mini-css-extract-plugin: 1.6.2_webpack@4.46.0
+      minimatch: 3.0.4
+      native-url: 0.3.4
+      optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
+      ora: 5.4.1
+      pnp-webpack-plugin: 1.7.0_typescript@4.4.3
+      postcss: 8.3.6
+      postcss-load-config: 3.1.0
+      postcss-loader: 4.3.0_postcss@8.3.6+webpack@4.46.0
+      preact: 10.5.14
+      preact-render-to-string: 5.1.19_preact@10.5.14
+      progress-bar-webpack-plugin: 2.1.0_webpack@4.46.0
+      promise-polyfill: 8.2.0
+      prompts: 2.4.1
+      raw-loader: 4.0.2_webpack@4.46.0
+      react-refresh: 0.10.0
+      rimraf: 3.0.2
+      sade: 1.7.4
+      sass-loader: 10.2.0_sass@1.43.2
+      size-plugin: 3.0.0_webpack@4.46.0
+      source-map: 0.7.3
+      stack-trace: 0.0.10
+      style-loader: 2.0.0_webpack@4.46.0
+      terser-webpack-plugin: 4.2.3_webpack@4.46.0
+      typescript: 4.4.3
+      update-notifier: 5.1.0
+      url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0
+      validate-npm-package-name: 3.0.0
+      webpack: 4.46.0
+      webpack-bundle-analyzer: 4.4.2
+      webpack-dev-server: 3.11.2_webpack@4.46.0
+      webpack-fix-style-only-entries: 0.6.1
+      webpack-merge: 5.8.0
+      webpack-plugin-replace: 1.2.0
+      which: 2.0.2
+      workbox-cacheable-response: 6.2.4
+      workbox-core: 6.2.4
+      workbox-precaching: 6.2.4
+      workbox-routing: 6.2.4
+      workbox-strategies: 6.2.4
+      workbox-webpack-plugin: 6.2.4_webpack@4.46.0
+    transitivePeerDependencies:
+      - '@types/babel__core'
+      - bufferutil
+      - debug
+      - supports-color
+      - ts-node
+      - utf-8-validate
+      - webpack-cli
+      - webpack-command
+    dev: true
+
   /preact-render-to-string/5.1.19_preact@10.5.14:
     resolution: {integrity: 
sha512-bj8sn/oytIKO6RtOGSS/1+5CrQyRSC99eLUnEVbqUa6MzJX5dYh7wu9bmT0d6lm/Vea21k9KhCQwvr2sYN3rrQ==}
     peerDependencies:
@@ -18075,11 +18219,11 @@ packages:
     peerDependencies:
       rollup: ^2.0.0
     dependencies:
-      '@babel/code-frame': 7.14.5
+      '@babel/code-frame': 7.12.13
       jest-worker: 26.6.2
       rollup: 2.56.2
       serialize-javascript: 4.0.0
-      terser: 5.7.1
+      terser: 5.4.0
     dev: true
 
   /rollup/2.37.1:
@@ -18188,6 +18332,38 @@ packages:
       walker: 1.0.7
     dev: true
 
+  /sass-loader/10.2.0_sass@1.43.2:
+    resolution: {integrity: 
sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw==}
+    engines: {node: '>= 10.13.0'}
+    peerDependencies:
+      fibers: '>= 3.1.0'
+      node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0
+      sass: ^1.3.0
+      webpack: ^4.36.0 || ^5.0.0
+    peerDependenciesMeta:
+      fibers:
+        optional: true
+      node-sass:
+        optional: true
+      sass:
+        optional: true
+    dependencies:
+      klona: 2.0.4
+      loader-utils: 2.0.0
+      neo-async: 2.6.2
+      sass: 1.43.2
+      schema-utils: 3.1.1
+      semver: 7.3.5
+    dev: true
+
+  /sass/1.43.2:
+    resolution: {integrity: 
sha512-DncYhjl3wBaPMMJR0kIUaH3sF536rVrOcqqVGmTZHQRRzj7LQlyGV7Mb8aCKFyILMr5VsPHwRYtyKpnKYlmQSQ==}
+    engines: {node: '>=8.9.0'}
+    hasBin: true
+    dependencies:
+      chokidar: 3.5.2
+    dev: true
+
   /sax/1.2.4:
     resolution: {integrity: 
sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
     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]