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 esbuild from "esbuild";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import crypto from "crypto"; import sass from "sass";
import { sassPlugin } from "esbuild-sass-plugin";
// eslint-disable-next-line no-undef
const BASE = process.cwd(); const BASE = process.cwd();
const preact = path.join( 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; let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
GIT_ROOT = path.join(GIT_ROOT, "../"); GIT_ROOT = path.join(GIT_ROOT, "../");
} }
if (GIT_ROOT === "/") { if (GIT_ROOT === "/") {
// eslint-disable-next-line no-undef
console.log("not found"); console.log("not found");
// eslint-disable-next-line no-undef
process.exit(1); process.exit(1);
} }
const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash(); 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 = { export const buildConfig = {
entryPoints: [...entryPoints], entryPoints: [...entryPoints],
bundle: true, bundle: true,
@ -95,6 +117,10 @@ export const buildConfig = {
".svg": "file", ".svg": "file",
".png": "dataurl", ".png": "dataurl",
".jpeg": "dataurl", ".jpeg": "dataurl",
'.ttf': 'file',
'.woff': 'file',
'.woff2': 'file',
'.eot': 'file',
}, },
target: ["es6"], target: ["es6"],
format: "esm", format: "esm",
@ -108,17 +134,14 @@ export const buildConfig = {
}, },
plugins: [ plugins: [
preactCompatPlugin, preactCompatPlugin,
sassPlugin(),
copyFilesPlugin([ copyFilesPlugin([
{ {
src: "static/index.html", src: "./src/index.html",
dest: "dist/index.html", dest: "./dist/index.html",
}, },
]), ]),
buildSassPlugin
], ],
}; };
esbuild.build(buildConfig).catch((e) => { await esbuild.build(buildConfig)
console.log(e);
process.exit(1);
});

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

View File

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

View File

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

View File

@ -19,27 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { i18n, setupI18n } from "@gnu-taler/taler-util";
import { createContext, h, VNode } from "preact"; import { createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks/index.js"; import { useLang } from "../hooks/useLang.js";
import * as jedLib from "jed";
import { strings } from "../i18n/strings.js"; import { strings } from "../i18n/strings.js";
interface Type { interface Type {
lang: string; lang: string;
handler: any; supportedLang: { [id in keyof typeof supportedLang]: string };
changeLanguage: (l: string) => void; 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 = { const initial = {
lang: "en", lang: "en",
handler: null, supportedLang,
changeLanguage: () => { changeLanguage: () => {
/** // do not change anything
* This function will be replaced by one with
* the same signature _but_ coming from the state.
* FIXME: clarify this design.
*/
}, },
i18n,
isSaved: false,
}; };
const Context = createContext<Type>(initial); const Context = createContext<Type>(initial);
@ -55,14 +70,23 @@ export const TranslationProvider = ({
children, children,
forceLang, forceLang,
}: Props): VNode => { }: Props): VNode => {
const [lang, changeLanguage] = useLang(initial); const [lang, changeLanguage, isSaved] = useLang(initial);
useEffect(() => { useEffect(() => {
if (forceLang) changeLanguage(forceLang); if (forceLang) {
changeLanguage(forceLang);
}
}); });
console.log("lang store", strings); useEffect(() => {
const handler = new jedLib.Jed(strings[lang] || strings["en"]); setupI18n(lang, strings);
}, [lang]);
if (forceLang) {
setupI18n(forceLang, strings);
} else {
setupI18n(lang, strings);
}
return h(Context.Provider, { return h(Context.Provider, {
value: { lang, handler, changeLanguage }, value: { lang, changeLanguage, supportedLang, i18n, isSaved },
children, children,
}); });
}; };

View File

@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm) * @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); export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => { const calculateRootPath = () => {
@ -68,79 +69,3 @@ export function useBackendInstanceToken(
return [token, setToken]; 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 @author Sebastian Javier Marchano
--> -->
<!DOCTYPE html> <!DOCTYPE html>
<html <html lang="en">
lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
>
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<link <link
rel="icon" 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==" 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" /> <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> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="index.tsx"></script>
</body> </body>
</html> </html>

View File

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

View File

@ -1,4 +1,4 @@
@import "pure"; @use "pure";
@import "bank"; @use "bank";
@import "demo"; @use "demo";
@import "colors-bank"; @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>