[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 &
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'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.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-wallet-core] branch master updated: add template from merchant backoffice,
gnunet <=