using web-utils in demobank

This commit is contained in:
Sebastian 2022-12-06 11:25:05 -03:00
parent d3a6544bc5
commit 5969a44391
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
19 changed files with 477 additions and 474 deletions

View File

@ -18,9 +18,9 @@
import esbuild from "esbuild";
import path from "path";
import fs from "fs";
import crypto from "crypto";
import { sassPlugin } from "esbuild-sass-plugin";
import sass from "sass";
// eslint-disable-next-line no-undef
const BASE = process.cwd();
const preact = path.join(
@ -44,14 +44,16 @@ const preactCompatPlugin = {
},
};
const entryPoints = ["src/index.tsx"];
const entryPoints = ["src/index.tsx", "src/stories.tsx"];
let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
GIT_ROOT = path.join(GIT_ROOT, "../");
}
if (GIT_ROOT === "/") {
// eslint-disable-next-line no-undef
console.log("not found");
// eslint-disable-next-line no-undef
process.exit(1);
}
const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
@ -86,6 +88,26 @@ function copyFilesPlugin(options) {
};
}
const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
const buildSassPlugin = {
name: "custom-build-sass",
setup(build) {
build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
const resolveDir = path.dirname(file)
const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
return {
resolveDir,
loader: 'css',
contents
}
});
},
};
export const buildConfig = {
entryPoints: [...entryPoints],
bundle: true,
@ -95,6 +117,10 @@ export const buildConfig = {
".svg": "file",
".png": "dataurl",
".jpeg": "dataurl",
'.ttf': 'file',
'.woff': 'file',
'.woff2': 'file',
'.eot': 'file',
},
target: ["es6"],
format: "esm",
@ -108,17 +134,14 @@ export const buildConfig = {
},
plugins: [
preactCompatPlugin,
sassPlugin(),
copyFilesPlugin([
{
src: "static/index.html",
dest: "dist/index.html",
src: "./src/index.html",
dest: "./dist/index.html",
},
]),
buildSassPlugin
],
};
esbuild.build(buildConfig).catch((e) => {
console.log(e);
process.exit(1);
});
await esbuild.build(buildConfig)

30
packages/demobank-ui/dev.mjs Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { serve } from "@gnu-taler/web-util/lib/index.node";
import esbuild from "esbuild";
import { buildConfig } from "./build.mjs";
buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
serve({
folder: './dist',
port: 8080,
source: './src',
development: true,
onUpdate: async () => esbuild.build(buildConfig)
})

View File

@ -11,13 +11,13 @@
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
"date-fns": "2.29.3",
"history": "4.10.1",
"jed": "1.1.1",
"preact": "10.6.5",
"preact": "10.11.3",
"preact-router": "3.2.1",
"qrcode-generator": "^1.4.4",
"react": "npm:@preact/compat@^17.1.2",
"swr": "1.3.0"
},
"devDependencies": {
@ -30,11 +30,10 @@
"bulma-checkbox": "^1.1.1",
"bulma-radio": "^1.1.1",
"esbuild": "^0.15.12",
"esbuild-sass-plugin": "^2.4.0",
"eslint": "^8.26.0",
"eslint-config-preact": "^1.2.0",
"po2json": "^0.4.5",
"sass": "1.32.13",
"sass": "1.56.1",
"typescript": "^4.4.4"
}
}

View File

@ -20,7 +20,7 @@
*/
import { h, VNode } from "preact";
import { Translate } from "../../i18n";
import { useTranslationContext } from "../../context/translation.js";
interface Props {
mobile?: boolean;
@ -31,6 +31,7 @@ export function Sidebar({ mobile }: Props): VNode {
const config = { version: "none" };
// FIXME: add replacement for __VERSION__ with the current version
const process = { env: { __VERSION__: "0.0.0" } };
const { i18n } = useTranslationContext();
return (
<aside class="aside is-placed-left is-expanded">
@ -49,20 +50,20 @@ export function Sidebar({ mobile }: Props): VNode {
</div>
<div class="menu is-menu-main">
<p class="menu-label">
<Translate>Bank menu</Translate>
<i18n.Translate>Bank menu</i18n.Translate>
</p>
<ul class="menu-list">
<li>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Select option1</Translate>
<i18n.Translate>Select option1</i18n.Translate>
</span>
</div>
</li>
<li>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Select option2</Translate>
<i18n.Translate>Select option2</i18n.Translate>
</span>
</div>
</li>

View File

@ -21,7 +21,7 @@
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useTranslator } from "../../i18n";
import { useTranslationContext } from "../../context/translation.js";
import "../../scss/DurationPicker.scss";
export interface Props {
@ -46,13 +46,13 @@ export function DurationPicker({
const ms = ss * 60;
const hs = ms * 60;
const ds = hs * 24;
const i18n = useTranslator();
const { i18n } = useTranslationContext();
return (
<div class="rdp-picker">
{days && (
<DurationColumn
unit={i18n`days`}
unit={i18n.str`days`}
max={99}
value={Math.floor(value / ds)}
onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
@ -62,7 +62,7 @@ export function DurationPicker({
)}
{hours && (
<DurationColumn
unit={i18n`hours`}
unit={i18n.str`hours`}
max={23}
min={1}
value={Math.floor(value / hs) % 24}
@ -73,7 +73,7 @@ export function DurationPicker({
)}
{minutes && (
<DurationColumn
unit={i18n`minutes`}
unit={i18n.str`minutes`}
max={59}
min={1}
value={Math.floor(value / ms) % 60}
@ -84,7 +84,7 @@ export function DurationPicker({
)}
{seconds && (
<DurationColumn
unit={i18n`seconds`}
unit={i18n.str`seconds`}
max={59}
value={Math.floor(value / ss) % 60}
onDecrease={value >= ss ? () => onChange(value - ss) : undefined}

View File

@ -19,27 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { i18n, setupI18n } from "@gnu-taler/taler-util";
import { createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks/index.js";
import * as jedLib from "jed";
import { useLang } from "../hooks/useLang.js";
import { strings } from "../i18n/strings.js";
interface Type {
lang: string;
handler: any;
supportedLang: { [id in keyof typeof supportedLang]: string };
changeLanguage: (l: string) => void;
i18n: typeof i18n;
isSaved: boolean;
}
const supportedLang = {
es: "Español [es]",
ja: "日本語 [ja]",
en: "English [en]",
fr: "Français [fr]",
de: "Deutsch [de]",
sv: "Svenska [sv]",
it: "Italiano [it]",
// ko: "한국어 [ko]",
// ru: "Ру́сский язы́к [ru]",
tr: "Türk [tr]",
navigator: "Defined by navigator",
};
const initial = {
lang: "en",
handler: null,
supportedLang,
changeLanguage: () => {
/**
* This function will be replaced by one with
* the same signature _but_ coming from the state.
* FIXME: clarify this design.
*/
// do not change anything
},
i18n,
isSaved: false,
};
const Context = createContext<Type>(initial);
@ -55,14 +70,23 @@ export const TranslationProvider = ({
children,
forceLang,
}: Props): VNode => {
const [lang, changeLanguage] = useLang(initial);
const [lang, changeLanguage, isSaved] = useLang(initial);
useEffect(() => {
if (forceLang) changeLanguage(forceLang);
if (forceLang) {
changeLanguage(forceLang);
}
});
console.log("lang store", strings);
const handler = new jedLib.Jed(strings[lang] || strings["en"]);
useEffect(() => {
setupI18n(lang, strings);
}, [lang]);
if (forceLang) {
setupI18n(forceLang, strings);
} else {
setupI18n(lang, strings);
}
return h(Context.Provider, {
value: { lang, handler, changeLanguage },
value: { lang, changeLanguage, supportedLang, i18n, isSaved },
children,
});
};

View File

@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { StateUpdater, useState } from "preact/hooks";
import { StateUpdater } from "preact/hooks";
import { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => {
@ -68,79 +69,3 @@ export function useBackendInstanceToken(
return [token, setToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {
const browserLang =
typeof window !== "undefined"
? navigator.language || (navigator as any).userLanguage
: undefined;
const defaultLang = (browserLang || initial || "en").substring(0, 2);
const [value, setValue] = useNotNullLocalStorage(
"lang-preference",
defaultLang,
);
function updateValue(newValue: string | ((v: string) => string)) {
if (document.body.parentElement) {
const htmlElement = document.body.parentElement;
if (typeof newValue === "string") {
htmlElement.lang = newValue;
setValue(newValue);
} else if (typeof newValue === "function")
setValue((old) => {
const nv = newValue(old);
htmlElement.lang = nv;
return nv;
});
} else setValue(newValue);
}
return [value, updateValue];
}
export function useLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>(
(): string | undefined => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
},
);
const setValue = (
value?: string | ((val?: string) => string | undefined),
) => {
setStoredValue((p) => {
const toStore = value instanceof Function ? value(p) : value;
if (typeof window !== "undefined")
if (!toStore) window.localStorage.removeItem(key);
else window.localStorage.setItem(key, toStore);
return toStore;
});
};
return [storedValue, setValue];
}
export function useNotNullLocalStorage(
key: string,
initialValue: string,
): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined")
if (!valueToStore) window.localStorage.removeItem(key);
else window.localStorage.setItem(key, valueToStore);
};
return [storedValue, setValue];
}

View File

@ -0,0 +1,30 @@
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { useNotNullLocalStorage } from "./useLocalStorage.js";
function getBrowserLang(): string | undefined {
if (window.navigator.languages) return window.navigator.languages[0];
if (window.navigator.language) return window.navigator.language;
return undefined;
}
export function useLang(
initial?: string,
): [string, (s: string) => void, boolean] {
const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
return useNotNullLocalStorage("lang-preference", defaultLang);
}

View File

@ -0,0 +1,80 @@
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { StateUpdater, useState } from "preact/hooks";
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),
): void => {
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];
}
//TODO: merge with the above function
export function useNotNullLocalStorage(
key: string,
initialValue: string,
): [string, StateUpdater<string>, boolean] {
const [storedValue, setStoredValue] = useState<string>((): string => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const setValue = (value: string | ((val: string) => string)): void => {
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);
}
}
};
const isSaved = window.localStorage.getItem(key) !== null;
return [storedValue, setValue, isSaved];
}

View File

@ -1,201 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Translation helpers for React components and template literals.
*/
/**
* Imports
*/
import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
import { useTranslationContext } from "../context/translation";
export function useTranslator() {
const ctx = useTranslationContext();
const jed = ctx.handler;
return function str(
stringSeq: TemplateStringsArray,
...values: any[]
): string {
const s = toI18nString(stringSeq);
if (!s) return s;
const tr = jed
.translate(s)
.ifPlural(1, s)
.fetch(...values);
return tr;
};
}
/**
* Convert template strings to a msgid
*/
function toI18nString(stringSeq: ReadonlyArray<string>): string {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
if (i < stringSeq.length - 1) s += `%${i + 1}$s`;
}
return s;
}
interface TranslateSwitchProps {
target: number;
children: ComponentChildren;
}
function stringifyChildren(children: ComponentChildren): string {
let n = 1;
const ss = (children instanceof Array ? children : [children]).map((c) => {
if (typeof c === "string") return c;
return `%${n++}$s`;
});
const s = ss.join("").replace(/ +/g, " ").trim();
return s;
}
interface TranslateProps {
children: ComponentChildren;
/**
* Component that the translated element should be wrapped in.
* Defaults to "div".
*/
wrap?: any;
/**
* Props to give to the wrapped component.
*/
wrapProps?: any;
}
function getTranslatedChildren(
translation: string,
children: ComponentChildren,
): ComponentChild[] {
const tr = translation.split(/%(\d+)\$s/);
const childArray = children instanceof Array ? children : [children];
// Merge consecutive string children.
const placeholderChildren = Array<ComponentChild>();
for (let i = 0; i < childArray.length; i++) {
const x = childArray[i];
if (x === undefined) continue;
else if (typeof x === "string") continue;
else placeholderChildren.push(x);
}
const result = Array<ComponentChild>();
for (let i = 0; i < tr.length; i++)
if (i % 2 == 0)
// Text
result.push(tr[i]);
else {
const childIdx = Number.parseInt(tr[i], 10) - 1;
result.push(placeholderChildren[childIdx]);
}
return result;
}
/**
* Translate text node children of this component.
* If a child component might produce a text node, it must be wrapped
* in a another non-text element.
*
* Example:
* ```
* <Translate>
* Hello. Your score is <span><PlayerScore player={player} /></span>
* </Translate>
* ```
*/
export function Translate({ children }: TranslateProps): VNode {
const s = stringifyChildren(children);
const ctx = useTranslationContext();
const translation: string = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}
/**
* Switch translation based on singular or plural based on the target prop.
* Should only contain TranslateSingular and TransplatePlural as children.
*
* Example:
* ```
* <TranslateSwitch target={n}>
* <TranslateSingular>I have {n} apple.</TranslateSingular>
* <TranslatePlural>I have {n} apples.</TranslatePlural>
* </TranslateSwitch>
* ```
*/
export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
let singular: VNode<TranslationPluralProps> | undefined;
let plural: VNode<TranslationPluralProps> | undefined;
// const children = this.props.children;
if (children)
(children instanceof Array ? children : [children]).forEach(
(child: any) => {
if (child.type === TranslatePlural) plural = child;
if (child.type === TranslateSingular) singular = child;
},
);
if (!singular || !plural) {
console.error("translation not found");
return h("span", {}, ["translation not found"]);
}
singular.props.target = target;
plural.props.target = target;
// We're looking up the translation based on the
// singular, even if we must use the plural form.
return singular;
}
interface TranslationPluralProps {
children: ComponentChildren;
target: number;
}
/**
* See [[TranslateSwitch]].
*/
export function TranslatePlural({
children,
target,
}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}
/**
* See [[TranslateSwitch]].
*/
export function TranslateSingular({
children,
target,
}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, target);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}

View File

@ -16,25 +16,26 @@
@author Sebastian Javier Marchano
-->
<!DOCTYPE html>
<html
lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link
rel="icon"
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
/>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<title>Demobank</title>
<!-- Optional customization script. -->
<script src="demobank-ui-settings.js"></script>
<!-- Entry point for the demobank SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="index.tsx"></script>
</body>
</html>

View File

@ -1,6 +1,7 @@
import App from "./components/app.js";
export default App;
import { render, h } from "preact";
import "./scss/main.scss";
const app = document.getElementById("app");

View File

@ -0,0 +1,33 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { QrCodeSection } from "./QrCodeSection.js";
export default {
title: "Qr Code Selection",
};
export const SimpleExample = {
component: QrCodeSection,
props: {
talerWithdrawUri: "taler://withdraw/asdasdasd",
},
};

View File

@ -0,0 +1,55 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../../components/QR.js";
import { useTranslationContext } from "../../context/translation.js";
export function QrCodeSection({
talerWithdrawUri,
abortButton,
}: {
talerWithdrawUri: string;
abortButton: h.JSX.Element;
}): VNode {
const { i18n } = useTranslationContext();
useEffect(() => {
//Taler Wallet WebExtension is listening to headers response and tab updates.
//In the SPA there is no header response with the Taler URI so
//this hack manually triggers the tab update after the QR is in the DOM.
window.location.hash = `/account/${new Date().getTime()}`;
}, []);
return (
<section id="main" class="content">
<h1 class="nav">{i18n.str`Transfer to Taler Wallet`}</h1>
<article>
<div class="qr-div">
<p>{i18n.str`Use this QR code to withdraw to your mobile wallet:`}</p>
{QR({ text: talerWithdrawUri })}
<p>
Click{" "}
<a id="linkqr" href={talerWithdrawUri}>{i18n.str`this link`}</a> to
open your Taler wallet!
</p>
<br />
{abortButton}
</div>
</article>
</section>
);
}

View File

@ -0,0 +1 @@
export * as qr from "./QrCodeSection.stories.js";

View File

@ -27,13 +27,16 @@ import {
} from "preact/hooks";
import talerLogo from "../../assets/logo-white.svg";
import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js";
import { QR } from "../../components/QR.js";
import { useLocalStorage, useNotNullLocalStorage } from "../../hooks/index.js";
import { Translate, useTranslator } from "../../i18n/index.js";
import "../../scss/main.scss";
import {
useLocalStorage,
useNotNullLocalStorage,
} from "../../hooks/useLocalStorage.js";
// import { Translate, useTranslator } from "../../i18n/index.js";
import { useTranslationContext } from "../../context/translation.js";
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
import { createHashHistory } from "history";
import Router, { Route, route } from "preact-router";
import { QrCodeSection } from "./QrCodeSection.js";
interface BankUiSettings {
allowRegistrations: boolean;
@ -987,7 +990,7 @@ async function registrationCall(
function ErrorBanner(Props: any): VNode | null {
const [pageState, pageStateSetter] = Props.pageState;
// const i18n = useTranslator();
// const { i18n } = useTranslationContext();
if (!pageState.error) return null;
const rval = (
@ -1041,7 +1044,7 @@ function StatusBanner(Props: any): VNode | null {
}
function BankFrame(Props: any): VNode {
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const [pageState, pageStateSetter] = useContext(PageContext);
console.log("BankFrame state", pageState);
const logOut = (
@ -1062,7 +1065,7 @@ function BankFrame(Props: any): VNode {
};
});
}}
>{i18n`Logout`}</a>
>{i18n.str`Logout`}</a>
</div>
);
@ -1080,7 +1083,7 @@ function BankFrame(Props: any): VNode {
class="demobar"
style="display: flex; flex-direction: row; justify-content: space-between;"
>
<a href="#main" class="skip">{i18n`Skip to main content`}</a>
<a href="#main" class="skip">{i18n.str`Skip to main content`}</a>
<div style="max-width: 50em; margin-left: 2em;">
<h1>
<span class="it">
@ -1089,7 +1092,7 @@ function BankFrame(Props: any): VNode {
</h1>
{maybeDemoContent(
<p>
<Translate>
<i18n.Translate>
This part of the demo shows how a bank that supports Taler
directly would work. In addition to using your own bank account,
you can also see the transaction history of some{" "}
@ -1100,14 +1103,14 @@ function BankFrame(Props: any): VNode {
Public Accounts
</a>
.
</Translate>
</i18n.Translate>
</p>,
)}
</div>
<a href="https://taler.net/">
<img
src={talerLogo}
alt={i18n`Taler logo`}
alt={i18n.str`Taler logo`}
height="100"
width="224"
style="margin: 2em 2em"
@ -1168,7 +1171,7 @@ function PaytoWireTransfer(Props: any): VNode {
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined,
);
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const { focus, backendState } = Props;
const amountRegex = "^[0-9]+(.[0-9]+)?$";
const ibanRegex = "^[A-Z][A-Z][0-9]+$";
@ -1193,17 +1196,17 @@ function PaytoWireTransfer(Props: any): VNode {
? undefined
: undefinedIfEmpty({
iban: !submitData.iban
? i18n`Missing IBAN`
? i18n.str`Missing IBAN`
: !/^[A-Z0-9]*$/.test(submitData.iban)
? i18n`IBAN should have just uppercased letters and numbers`
? i18n.str`IBAN should have just uppercased letters and numbers`
: undefined,
subject: !submitData.subject ? i18n`Missing subject` : undefined,
subject: !submitData.subject ? i18n.str`Missing subject` : undefined,
amount: !submitData.amount
? i18n`Missing amount`
? i18n.str`Missing amount`
: !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
? i18n`Amount is not valid`
? i18n.str`Amount is not valid`
: Amounts.isZero(parsedAmount)
? i18n`Should be greater than 0`
? i18n.str`Should be greater than 0`
: undefined,
});
@ -1212,7 +1215,7 @@ function PaytoWireTransfer(Props: any): VNode {
<div>
<div class="pure-form" name="wire-transfer-form">
<p>
<label for="iban">{i18n`Receiver IBAN:`}</label>&nbsp;
<label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
<input
ref={ref}
type="text"
@ -1235,7 +1238,7 @@ function PaytoWireTransfer(Props: any): VNode {
isDirty={submitData?.iban !== undefined}
/>
<br />
<label for="subject">{i18n`Transfer subject:`}</label>&nbsp;
<label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
<input
type="text"
name="subject"
@ -1256,7 +1259,7 @@ function PaytoWireTransfer(Props: any): VNode {
isDirty={submitData?.subject !== undefined}
/>
<br />
<label for="amount">{i18n`Amount:`}</label>&nbsp;
<label for="amount">{i18n.str`Amount:`}</label>&nbsp;
<input
type="number"
name="amount"
@ -1309,7 +1312,7 @@ function PaytoWireTransfer(Props: any): VNode {
...prevState,
error: {
title: i18n`Field(s) missing.`,
title: i18n.str`Field(s) missing.`,
},
}));
return;
@ -1358,7 +1361,7 @@ function PaytoWireTransfer(Props: any): VNode {
}));
}}
>
{i18n`Want to try the raw payto://-format?`}
{i18n.str`Want to try the raw payto://-format?`}
</a>
</p>
</div>
@ -1366,18 +1369,18 @@ function PaytoWireTransfer(Props: any): VNode {
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
? i18n`Missing payto address`
? i18n.str`Missing payto address`
: !parsePaytoUri(rawPaytoInput)
? i18n`Payto does not follow the pattern`
? i18n.str`Payto does not follow the pattern`
: undefined,
});
return (
<div>
<p>{i18n`Transfer money to account identified by payto:// URI:`}</p>
<p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
<div class="pure-form" name="payto-form">
<p>
<label for="address">{i18n`payto URI:`}</label>&nbsp;
<label for="address">{i18n.str`payto URI:`}</label>&nbsp;
<input
name="address"
type="text"
@ -1386,7 +1389,7 @@ function PaytoWireTransfer(Props: any): VNode {
id="address"
value={rawPaytoInput ?? ""}
required
placeholder={i18n`payto address`}
placeholder={i18n.str`payto address`}
// pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
onInput={(e): void => {
rawPaytoInputSetter(e.currentTarget.value);
@ -1410,7 +1413,7 @@ function PaytoWireTransfer(Props: any): VNode {
class="pure-button pure-button-primary"
type="submit"
disabled={!!errorsPayto}
value={i18n`Send`}
value={i18n.str`Send`}
onClick={async () => {
// empty string evaluates to false.
if (!rawPaytoInput) {
@ -1444,7 +1447,7 @@ function PaytoWireTransfer(Props: any): VNode {
}));
}}
>
{i18n`Use wire-transfer form?`}
{i18n.str`Use wire-transfer form?`}
</a>
</p>
</div>
@ -1459,7 +1462,7 @@ function PaytoWireTransfer(Props: any): VNode {
function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
const [pageState, pageStateSetter] = useContext(PageContext);
const { backendState } = Props;
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const captchaNumbers = {
a: Math.floor(Math.random() * 10),
b: Math.floor(Math.random() * 10),
@ -1468,15 +1471,15 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
return (
<Fragment>
<h1 class="nav">{i18n`Confirm Withdrawal`}</h1>
<h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
<article>
<div class="challenge-div">
<form class="challenge-form">
<div class="pure-form" id="captcha" name="capcha-form">
<h2>{i18n`Authorize withdrawal by solving challenge`}</h2>
<h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
<p>
<label for="answer">
{i18n`What is`}&nbsp;
{i18n.str`What is`}&nbsp;
<em>
{captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
</em>
@ -1514,12 +1517,12 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
...prevState,
error: {
title: i18n`Answer is wrong.`,
title: i18n.str`Answer is wrong.`,
},
}));
}}
>
{i18n`Confirm`}
{i18n.str`Confirm`}
</button>
&nbsp;
<button
@ -1532,18 +1535,18 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
)
}
>
{i18n`Cancel`}
{i18n.str`Cancel`}
</button>
</p>
</div>
</form>
<div class="hint">
<p>
<Translate>
<i18n.Translate>
A this point, a <b>real</b> bank would ask for an additional
authentication proof (PIN/TAN, one time password, ..), instead
of a simple calculation.
</Translate>
</i18n.Translate>
</p>
</div>
</div>
@ -1552,40 +1555,6 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
);
}
function QrCodeSection({
talerWithdrawUri,
abortButton,
}: {
talerWithdrawUri: string;
abortButton: h.JSX.Element;
}): VNode {
const i18n = useTranslator();
useEffect(() => {
//Taler Wallet WebExtension is listening to headers response and tab updates.
//In the SPA there is no header response with the Taler URI so
//this hack manually triggers the tab update after the QR is in the DOM.
window.location.hash = `/account/${new Date().getTime()}`;
}, []);
return (
<section id="main" class="content">
<h1 class="nav">{i18n`Transfer to Taler Wallet`}</h1>
<article>
<div class="qr-div">
<p>{i18n`Use this QR code to withdraw to your mobile wallet:`}</p>
{QR({ text: talerWithdrawUri })}
<p>
Click <a id="linkqr" href={talerWithdrawUri}>{i18n`this link`}</a>{" "}
to open your Taler wallet!
</p>
<br />
{abortButton}
</div>
</article>
</section>
);
}
/**
* Offer the QR code (and a clickable taler://-link) to
* permit the passing of exchange and reserve details to
@ -1595,7 +1564,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
// turns true when the wallet POSTed the reserve details:
const [pageState, pageStateSetter] = useContext(PageContext);
const { withdrawalId, talerWithdrawUri, accountLabel, backendState } = Props;
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const abortButton = (
<a
class="pure-button btn-cancel"
@ -1609,7 +1578,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
};
});
}}
>{i18n`Abort`}</a>
>{i18n.str`Abort`}</a>
);
console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
@ -1629,7 +1598,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
...prevState,
error: {
title: i18n`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`,
title: i18n.str`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`,
},
}));
return (
@ -1643,7 +1612,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
// data didn't arrive yet and wallet didn't communicate:
if (typeof data === "undefined")
return <p>{i18n`Waiting the bank to create the operation...`}</p>;
return <p>{i18n.str`Waiting the bank to create the operation...`}</p>;
/**
* Wallet didn't communicate withdrawal details yet:
@ -1657,7 +1626,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
withdrawalInProgress: false,
error: {
title: i18n`This withdrawal was aborted!`,
title: i18n.str`This withdrawal was aborted!`,
},
};
});
@ -1680,7 +1649,7 @@ function TalerWithdrawalQRCode(Props: any): VNode {
function WalletWithdraw(Props: any): VNode {
const { backendState, pageStateSetter, focus } = Props;
const currency = useContext(CurrencyContext);
const i18n = useTranslator();
const { i18n } = useTranslationContext();
let submitAmount = "5.00";
const amountRegex = "^[0-9]+(.[0-9]+)?$";
@ -1691,7 +1660,8 @@ function WalletWithdraw(Props: any): VNode {
return (
<div id="reserve-form" class="pure-form" name="tform">
<p>
<label for="withdraw-amount">{i18n`Amount to withdraw:`}</label>&nbsp;
<label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
&nbsp;
<input
type="number"
ref={ref}
@ -1724,7 +1694,7 @@ function WalletWithdraw(Props: any): VNode {
id="select-exchange"
class="pure-button pure-button-primary"
type="submit"
value={i18n`Withdraw`}
value={i18n.str`Withdraw`}
onClick={() => {
submitAmount = validateAmount(submitAmount);
/**
@ -1753,7 +1723,7 @@ function WalletWithdraw(Props: any): VNode {
function PaymentOptions(Props: any): VNode {
const { backendState, pageStateSetter, focus } = Props;
const currency = useContext(CurrencyContext);
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
"charge-wallet",
@ -1769,7 +1739,7 @@ function PaymentOptions(Props: any): VNode {
setTab("charge-wallet");
}}
>
{i18n`Obtain digital cash`}
{i18n.str`Obtain digital cash`}
</button>
<button
class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
@ -1777,12 +1747,12 @@ function PaymentOptions(Props: any): VNode {
setTab("wire-transfer");
}}
>
{i18n`Transfer to bank account`}
{i18n.str`Transfer to bank account`}
</button>
</div>
{tab === "charge-wallet" && (
<div id="charge-wallet" class="tabcontent active">
<h3>{i18n`Obtain digital cash`}</h3>
<h3>{i18n.str`Obtain digital cash`}</h3>
<WalletWithdraw
backendState={backendState}
focus
@ -1792,7 +1762,7 @@ function PaymentOptions(Props: any): VNode {
)}
{tab === "wire-transfer" && (
<div id="wire-transfer" class="tabcontent active">
<h3>{i18n`Transfer to bank account`}</h3>
<h3>{i18n.str`Transfer to bank account`}</h3>
<PaytoWireTransfer
backendState={backendState}
focus
@ -1807,7 +1777,7 @@ function PaymentOptions(Props: any): VNode {
function RegistrationButton(Props: any): VNode {
const { backendStateSetter, pageStateSetter } = Props;
const i18n = useTranslator();
const { i18n } = useTranslationContext();
if (bankUiSettings.allowRegistrations)
return (
<button
@ -1816,7 +1786,7 @@ function RegistrationButton(Props: any): VNode {
route("/register");
}}
>
{i18n`Register`}
{i18n.str`Register`}
</button>
);
@ -1834,7 +1804,7 @@ function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
function LoginForm(Props: any): VNode {
const { backendStateSetter, pageStateSetter } = Props;
const [submitData, submitDataSetter] = useCredentialsRequestType();
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus();
@ -1843,17 +1813,17 @@ function LoginForm(Props: any): VNode {
const errors = !submitData
? undefined
: undefinedIfEmpty({
username: !submitData.username ? i18n`Missing username` : undefined,
password: !submitData.password ? i18n`Missing password` : undefined,
username: !submitData.username ? i18n.str`Missing username` : undefined,
password: !submitData.password ? i18n.str`Missing password` : undefined,
});
return (
<div class="login-div">
<form action="javascript:void(0);" class="login-form">
<div class="pure-form">
<h2>{i18n`Please login!`}</h2>
<h2>{i18n.str`Please login!`}</h2>
<p class="unameFieldLabel loginFieldLabel formFieldLabel">
<label for="username">{i18n`Username:`}</label>
<label for="username">{i18n.str`Username:`}</label>
</p>
<input
ref={ref}
@ -1872,7 +1842,7 @@ function LoginForm(Props: any): VNode {
}}
/>
<p class="passFieldLabel loginFieldLabel formFieldLabel">
<label for="password">{i18n`Password:`}</label>
<label for="password">{i18n.str`Password:`}</label>
</p>
<input
type="password"
@ -1919,7 +1889,7 @@ function LoginForm(Props: any): VNode {
});
}}
>
{i18n`Login`}
{i18n.str`Login`}
</button>
{RegistrationButton(Props)}
</div>
@ -1935,30 +1905,30 @@ function RegistrationForm(Props: any): VNode {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [pageState, pageStateSetter] = useContext(PageContext);
const [submitData, submitDataSetter] = useCredentialsRequestType();
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const errors = !submitData
? undefined
: undefinedIfEmpty({
username: !submitData.username ? i18n`Missing username` : undefined,
password: !submitData.password ? i18n`Missing password` : undefined,
username: !submitData.username ? i18n.str`Missing username` : undefined,
password: !submitData.password ? i18n.str`Missing password` : undefined,
repeatPassword: !submitData.repeatPassword
? i18n`Missing password`
? i18n.str`Missing password`
: submitData.repeatPassword !== submitData.password
? i18n`Password don't match`
? i18n.str`Password don't match`
: undefined,
});
return (
<Fragment>
<h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1>
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
<article>
<div class="register-div">
<form action="javascript:void(0);" class="register-form">
<div class="pure-form">
<h2>{i18n`Please register!`}</h2>
<h2>{i18n.str`Please register!`}</h2>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-un">{i18n`Username:`}</label>
<label for="register-un">{i18n.str`Username:`}</label>
</p>
<input
id="register-un"
@ -1976,7 +1946,7 @@ function RegistrationForm(Props: any): VNode {
/>
<br />
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-pw">{i18n`Password:`}</label>
<label for="register-pw">{i18n.str`Password:`}</label>
</p>
<input
type="password"
@ -1993,7 +1963,7 @@ function RegistrationForm(Props: any): VNode {
}}
/>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-repeat">{i18n`Repeat Password:`}</label>
<label for="register-repeat">{i18n.str`Repeat Password:`}</label>
</p>
<input
type="password"
@ -2012,7 +1982,7 @@ function RegistrationForm(Props: any): VNode {
/>
<br />
{/*
<label for="phone">{i18n`Phone number:`}</label>
<label for="phone">{i18n.str`Phone number:`}</label>
// FIXME: add input validation (must start with +, otherwise only numbers)
<input
name="phone"
@ -2054,7 +2024,7 @@ function RegistrationForm(Props: any): VNode {
});
}}
>
{i18n`Register`}
{i18n.str`Register`}
</button>
{/* FIXME: should use a different color */}
<button
@ -2068,7 +2038,7 @@ function RegistrationForm(Props: any): VNode {
route("/account");
}}
>
{i18n`Cancel`}
{i18n.str`Cancel`}
</button>
</div>
</form>
@ -2083,7 +2053,7 @@ function RegistrationForm(Props: any): VNode {
*/
function Transactions(Props: any): VNode {
const { pageNumber, accountLabel, balanceValue } = Props;
const i18n = useTranslator();
const { i18n } = useTranslationContext();
const { data, error, mutate } = useSWR(
`access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
);
@ -2114,10 +2084,10 @@ function Transactions(Props: any): VNode {
<table class="pure-table pure-table-striped">
<thead>
<tr>
<th>{i18n`Date`}</th>
<th>{i18n`Amount`}</th>
<th>{i18n`Counterpart`}</th>
<th>{i18n`Subject`}</th>
<th>{i18n.str`Date`}</th>
<th>{i18n.str`Amount`}</th>
<th>{i18n.str`Counterpart`}</th>
<th>{i18n.str`Subject`}</th>
</tr>
</thead>
<tbody>
@ -2178,7 +2148,7 @@ function Account(Props: any): VNode {
talerWithdrawUri,
timestamp,
} = pageState;
const i18n = useTranslator();
const { i18n } = useTranslationContext();
useEffect(() => {
mutate();
}, [timestamp]);
@ -2206,7 +2176,7 @@ function Account(Props: any): VNode {
isLoggedIn: false,
error: {
title: i18n`Username or account label '${accountLabel}' not found. Won't login.`,
title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`,
},
}));
@ -2233,7 +2203,7 @@ function Account(Props: any): VNode {
isLoggedIn: false,
error: {
title: i18n`Wrong credentials given.`,
title: i18n.str`Wrong credentials given.`,
},
}));
return <p>Wrong credentials...</p>;
@ -2244,7 +2214,7 @@ function Account(Props: any): VNode {
isLoggedIn: false,
error: {
title: i18n`Account information could not be retrieved.`,
title: i18n.str`Account information could not be retrieved.`,
debug: JSON.stringify(error),
},
}));
@ -2287,14 +2257,14 @@ function Account(Props: any): VNode {
<BankFrame>
<div>
<h1 class="nav welcome-text">
<Translate>
<i18n.Translate>
Welcome, {accountLabel} ({getIbanFromPayto(data.paytoUri)})!
</Translate>
</i18n.Translate>
</h1>
</div>
<section id="assets">
<div class="asset-summary">
<h2>{i18n`Bank account balance`}</h2>
<h2>{i18n.str`Bank account balance`}</h2>
<div class="large-amount amount">
{data.balance.credit_debit_indicator == "debit" ? <b>-</b> : null}
<span class="value">{`${balanceValue}`}</span>&nbsp;
@ -2304,7 +2274,7 @@ function Account(Props: any): VNode {
</section>
<section id="payments">
<div class="payments">
<h2>{i18n`Payments`}</h2>
<h2>{i18n.str`Payments`}</h2>
{/* FIXME: turn into button! */}
<CurrencyContext.Provider value={balance.currency}>
{Props.children}
@ -2317,7 +2287,7 @@ function Account(Props: any): VNode {
</section>
<section id="main">
<article>
<h2>{i18n`Latest transactions:`}</h2>
<h2>{i18n.str`Latest transactions:`}</h2>
<Transactions
balanceValue={balanceValue}
pageNumber="0"
@ -2379,7 +2349,7 @@ function SWRWithoutCredentials(Props: any): VNode {
function PublicHistories(Props: any): VNode {
const [showAccount, setShowAccount] = useShowPublicAccount();
const { data, error } = useSWR("access-api/public-accounts");
const i18n = useTranslator();
const { i18n } = useTranslationContext();
if (typeof error !== "undefined") {
console.log("account error", error);
@ -2391,7 +2361,7 @@ function PublicHistories(Props: any): VNode {
showPublicHistories: false,
error: {
title: i18n`List of public accounts was not found.`,
title: i18n.str`List of public accounts was not found.`,
debug: JSON.stringify(error),
},
}));
@ -2403,7 +2373,7 @@ function PublicHistories(Props: any): VNode {
showPublicHistories: false,
error: {
title: i18n`List of public accounts could not be retrieved.`,
title: i18n.str`List of public accounts could not be retrieved.`,
debug: JSON.stringify(error),
},
}));
@ -2450,7 +2420,7 @@ function PublicHistories(Props: any): VNode {
return (
<Fragment>
<h1 class="nav">{i18n`History of public accounts`}</h1>
<h1 class="nav">{i18n.str`History of public accounts`}</h1>
<section id="main">
<article>
<div class="pure-menu pure-menu-horizontal" name="accountMenu">
@ -2471,7 +2441,7 @@ function PublicHistories(Props: any): VNode {
function PublicHistoriesPage(): VNode {
// const [backendState, backendStateSetter] = useBackendState();
const [pageState, pageStateSetter] = usePageState();
// const i18n = useTranslator();
// const { i18n } = useTranslationContext();
return (
<SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
<PageContext.Provider value={[pageState, pageStateSetter]}>
@ -2499,12 +2469,12 @@ function PublicHistoriesPage(): VNode {
function RegistrationPage(): VNode {
const [backendState, backendStateSetter] = useBackendState();
const [pageState, pageStateSetter] = usePageState();
const i18n = useTranslator();
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
return (
<PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame>
<p>{i18n`Currently, the bank is not accepting new registrations!`}</p>
<p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
</BankFrame>
</PageContext.Provider>
);
@ -2521,13 +2491,13 @@ function RegistrationPage(): VNode {
function AccountPage(): VNode {
const [backendState, backendStateSetter] = useBackendState();
const [pageState, pageStateSetter] = usePageState();
const i18n = useTranslator();
const { i18n } = useTranslationContext();
if (!pageState.isLoggedIn) {
return (
<PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame>
<h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1>
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
<LoginForm
pageStateSetter={pageStateSetter}
backendStateSetter={backendStateSetter}
@ -2543,7 +2513,7 @@ function AccountPage(): VNode {
isLoggedIn: false,
error: {
title: i18n`Page has a problem: logged in but backend state is lost.`,
title: i18n.str`Page has a problem: logged in but backend state is lost.`,
},
}));
return <p>Error: waiting for details...</p>;

View File

@ -1,4 +1,4 @@
@import "pure";
@import "bank";
@import "demo";
@import "colors-bank";
@use "pure";
@use "bank";
@use "demo";
@use "colors-bank";

View File

@ -0,0 +1,46 @@
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { strings } from "./i18n/strings.js";
import * as pages from "./pages/home/index.stories.js";
import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
import "./scss/main.scss";
function SortStories(a: any, b: any): number {
return (a?.order ?? 0) - (b?.order ?? 0);
}
function main(): void {
renderStories(
{ pages },
{
strings,
},
);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Demobank</title>
<!-- Optional customization script. -->
<script src="demobank-ui-settings.js"></script>
<!-- Entry point for the demobank SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>