cherry-pick: demobank-ui: clean up build system
This commit is contained in:
parent
a286649b0a
commit
3685f8cfb8
@ -1,19 +1,11 @@
|
|||||||
# bank web
|
# Taler Demobank UI
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
- `npm install`: Installs dependencies
|
- `pnpm install`: Installs dependencies
|
||||||
|
|
||||||
- `npm run dev`: Run a development, HMR server
|
- `pnpm run build`: Production-ready build
|
||||||
|
|
||||||
- `npm run serve`: Run a production-like server
|
- `pnpm run check`: Run type checker
|
||||||
|
|
||||||
- `npm run build`: Production-ready build
|
- `pnpm run lint`: Pass TypeScript files using ESLint
|
||||||
|
|
||||||
- `npm run lint`: Pass TypeScript files using ESLint
|
|
||||||
|
|
||||||
- `npm run test`: Run Jest and Enzyme with
|
|
||||||
[`enzyme-adapter-preact-pure`](https://github.com/preactjs/enzyme-adapter-preact-pure) for
|
|
||||||
your tests
|
|
||||||
|
|
||||||
For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md).
|
|
||||||
|
160
packages/demobank-ui/build.mjs
Executable file
160
packages/demobank-ui/build.mjs
Executable file
@ -0,0 +1,160 @@
|
|||||||
|
#!/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 esbuild from "esbuild";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { sassPlugin } from "esbuild-sass-plugin";
|
||||||
|
|
||||||
|
const BASE = process.cwd();
|
||||||
|
|
||||||
|
const preact = path.join(
|
||||||
|
BASE,
|
||||||
|
"node_modules",
|
||||||
|
"preact",
|
||||||
|
"compat",
|
||||||
|
"dist",
|
||||||
|
"compat.module.js",
|
||||||
|
);
|
||||||
|
|
||||||
|
const preactCompatPlugin = {
|
||||||
|
name: "preact-compat",
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
|
||||||
|
//console.log("onresolve", JSON.stringify(args, undefined, 2));
|
||||||
|
return {
|
||||||
|
path: preact,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const entryPoints = ["src/index.tsx"];
|
||||||
|
|
||||||
|
let GIT_ROOT = BASE;
|
||||||
|
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
|
||||||
|
GIT_ROOT = path.join(GIT_ROOT, "../");
|
||||||
|
}
|
||||||
|
if (GIT_ROOT === "/") {
|
||||||
|
console.log("not found");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
|
||||||
|
|
||||||
|
let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
|
||||||
|
|
||||||
|
function git_hash() {
|
||||||
|
const rev = fs
|
||||||
|
.readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.split(/.*[: ]/)
|
||||||
|
.slice(-1)[0];
|
||||||
|
if (rev.indexOf("/") === -1) {
|
||||||
|
return rev;
|
||||||
|
} else {
|
||||||
|
return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Put this into some helper library.
|
||||||
|
function copyFilesPlugin(options) {
|
||||||
|
const getDigest = (string) => {
|
||||||
|
const hash = crypto.createHash("md5");
|
||||||
|
const data = hash.update(string, "utf-8");
|
||||||
|
|
||||||
|
return data.digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileDigest = (path) => {
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.statSync(path).isDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDigest(fs.readFileSync(path));
|
||||||
|
};
|
||||||
|
|
||||||
|
function filter(src, dest) {
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.statSync(dest).isDirectory()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFileDigest(src) !== getFileDigest(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "copy-files",
|
||||||
|
setup(build) {
|
||||||
|
let src = options.src || "./static";
|
||||||
|
let dest = options.dest || "./dist";
|
||||||
|
build.onEnd(() =>
|
||||||
|
fs.cpSync(src, dest, {
|
||||||
|
dereference: options.dereference || true,
|
||||||
|
errorOnExist: options.errorOnExist || false,
|
||||||
|
filter: options.filter || filter,
|
||||||
|
force: options.force || true,
|
||||||
|
preserveTimestamps: options.preserveTimestamps || true,
|
||||||
|
recursive: options.recursive || true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildConfig = {
|
||||||
|
entryPoints: [...entryPoints],
|
||||||
|
bundle: true,
|
||||||
|
outdir: "dist",
|
||||||
|
minify: false,
|
||||||
|
loader: {
|
||||||
|
".svg": "text",
|
||||||
|
".png": "dataurl",
|
||||||
|
".jpeg": "dataurl",
|
||||||
|
},
|
||||||
|
target: ["es6"],
|
||||||
|
format: "esm",
|
||||||
|
platform: "browser",
|
||||||
|
sourcemap: true,
|
||||||
|
jsxFactory: "h",
|
||||||
|
jsxFragment: "Fragment",
|
||||||
|
define: {
|
||||||
|
__VERSION__: `"${_package.version}"`,
|
||||||
|
__GIT_HASH__: `"${GIT_HASH}"`,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
preactCompatPlugin,
|
||||||
|
sassPlugin(),
|
||||||
|
copyFilesPlugin({
|
||||||
|
src: "static/index.html",
|
||||||
|
dest: "dist/index.html",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
esbuild.build(buildConfig).catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"register": {},
|
|
||||||
"transactions": {}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
Object.defineProperty(window, 'requestAnimationFrame', {
|
|
||||||
value: function(cb) {} // Silence the browser.
|
|
||||||
})
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'localStorage', {
|
|
||||||
value: {
|
|
||||||
store: {},
|
|
||||||
getItem: function(key) {
|
|
||||||
return this.store[key];
|
|
||||||
},
|
|
||||||
setItem: function(key, value) {
|
|
||||||
return this.store[key] = value;
|
|
||||||
},
|
|
||||||
clear: function() {
|
|
||||||
this.store = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Object.defineProperty(window, 'location', {
|
|
||||||
value: {
|
|
||||||
origin: "http://localhost:8080", /* where taler-local rev proxy listens to */
|
|
||||||
search: "",
|
|
||||||
pathname: "/sandbox/demobanks/default",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default window;
|
|
@ -4,15 +4,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "AGPL-3.0-OR-LATER",
|
"license": "AGPL-3.0-OR-LATER",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "preact watch --port ${PORT:=9090} --no-sw --no-esm -c preact.mock.js",
|
"build": "./build.mjs",
|
||||||
"build": "preact build --no-sw --no-esm -c preact.single-config.js --dest build && sh remove-link-stylesheet.sh",
|
"check": "tsc",
|
||||||
"serve": "sirv build --port ${PORT:=8080} --cors --single",
|
|
||||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
"test": "jest ./tests",
|
"pretty": "prettier --write src"
|
||||||
"build-storybook": "build-storybook",
|
|
||||||
"serve-single": "sirv single --port ${PORT:=8080} --cors --single",
|
|
||||||
"pretty": "prettier --write src",
|
|
||||||
"storybook": "start-storybook -p 6006"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
@ -24,77 +19,62 @@
|
|||||||
"build/"
|
"build/"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-explicit-any": [0],
|
"@typescript-eslint/no-explicit-any": [
|
||||||
"@typescript-eslint/ban-ts-comment": [1],
|
0
|
||||||
"quotes": [2, "single", {"allowTemplateLiterals": true,"avoidEscape": false}],
|
],
|
||||||
"indent": [2,2],
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
"prefer-arrow-callback": [2, {"allowNamedFunctions": false, "allowUnboundThis": true}],
|
1
|
||||||
"curly": [2,"multi"],
|
],
|
||||||
"prefer-template": [1]
|
"quotes": [
|
||||||
|
2,
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"allowTemplateLiterals": true,
|
||||||
|
"avoidEscape": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indent": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"prefer-arrow-callback": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"allowNamedFunctions": false,
|
||||||
|
"allowUnboundThis": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"curly": [
|
||||||
|
2,
|
||||||
|
"multi"
|
||||||
|
],
|
||||||
|
"prefer-template": [
|
||||||
|
1
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-inline-loader": "1.1.1",
|
"date-fns": "2.29.3",
|
||||||
"date-fns": "2.25.0",
|
|
||||||
"jed": "1.1.1",
|
"jed": "1.1.1",
|
||||||
"preact": "^10.5.15",
|
"preact-render-to-string": "^5.2.6",
|
||||||
"preact-render-to-string": "^5.1.19",
|
"preact-router": "^4.1.0",
|
||||||
"preact-router": "^3.2.1",
|
|
||||||
"qrcode-generator": "^1.4.4",
|
"qrcode-generator": "^1.4.4",
|
||||||
"swr": "1.1"
|
"swr": "~1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.9",
|
|
||||||
"@babel/plugin-transform-modules-commonjs": "7.18.6",
|
|
||||||
"@babel/plugin-transform-react-jsx-source": "7.18.6",
|
|
||||||
"@babel/preset-env": "7.18.9",
|
|
||||||
"@creativebulma/bulma-tooltip": "^1.2.0",
|
"@creativebulma/bulma-tooltip": "^1.2.0",
|
||||||
"@gnu-taler/pogen": "^0.0.5",
|
"@gnu-taler/pogen": "^0.0.5",
|
||||||
"@storybook/addon-a11y": "6.2.9",
|
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||||
"@storybook/addon-actions": "6.2.9",
|
"@typescript-eslint/parser": "^5.41.0",
|
||||||
"@storybook/addon-essentials": "6.2.9",
|
"bulma": "^0.9.4",
|
||||||
"@storybook/addon-links": "6.2.9",
|
|
||||||
"@storybook/preact": "6.2.9",
|
|
||||||
"@storybook/preset-scss": "^1.0.3",
|
|
||||||
"@testing-library/jest-dom": "^5.16.1",
|
|
||||||
"@testing-library/preact": "^2.0.1",
|
|
||||||
"@testing-library/preact-hooks": "^1.1.0",
|
|
||||||
"@types/enzyme": "^3.10.10",
|
|
||||||
"@types/jest": "^27.0.2",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
|
||||||
"@typescript-eslint/parser": "^5.3.0",
|
|
||||||
"babel-loader": "^8.2.2",
|
|
||||||
"base64-inline-loader": "^1.1.1",
|
|
||||||
"bulma": "^0.9.3",
|
|
||||||
"bulma-checkbox": "^1.1.1",
|
"bulma-checkbox": "^1.1.1",
|
||||||
"bulma-radio": "^1.1.1",
|
"bulma-radio": "^1.1.1",
|
||||||
"enzyme": "^3.11.0",
|
"esbuild": "^0.15.12",
|
||||||
"enzyme-adapter-preact-pure": "^3.2.0",
|
"esbuild-sass-plugin": "^2.4.0",
|
||||||
"eslint": "^8.1.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-preact": "^1.2.0",
|
"eslint-config-preact": "^1.2.0",
|
||||||
"html-webpack-inline-chunk-plugin": "^1.1.1",
|
|
||||||
"html-webpack-inline-source-plugin": "0.0.10",
|
|
||||||
"html-webpack-skip-assets-plugin": "^1.0.1",
|
|
||||||
"inline-chunk-html-plugin": "^1.1.1",
|
|
||||||
"jest": "^27.3.1",
|
|
||||||
"jest-fetch-mock": "^3.0.3",
|
|
||||||
"jest-preset-preact": "^4.0.5",
|
|
||||||
"jest-watch-typeahead": "^1.0.0",
|
|
||||||
"jest-environment-jsdom": "^27.4.6",
|
|
||||||
"jssha": "^3.2.0",
|
|
||||||
"po2json": "^0.4.5",
|
"po2json": "^0.4.5",
|
||||||
"preact-cli": "3.0.5",
|
"preact": "10.11.2",
|
||||||
"sass": "1.32.13",
|
|
||||||
"sass-loader": "^10",
|
|
||||||
"script-ext-html-webpack-plugin": "^2.1.5",
|
|
||||||
"sirv-cli": "^1.0.14",
|
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^4.4.4"
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"preset": "jest-preset-preact",
|
|
||||||
"setupFiles": [
|
|
||||||
"<rootDir>/tests/__mocks__/browserMocks.ts",
|
|
||||||
"<rootDir>/tests/__mocks__/setupTests.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,70 +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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DefinePlugin } from 'webpack';
|
|
||||||
|
|
||||||
import pack from './package.json';
|
|
||||||
import * as cp from 'child_process';
|
|
||||||
|
|
||||||
const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
|
|
||||||
|
|
||||||
export default {
|
|
||||||
webpack(config, env, helpers) {
|
|
||||||
// ensure that process.env will not be undefined on runtime
|
|
||||||
config.node.process = 'mock'
|
|
||||||
|
|
||||||
// add __VERSION__ to be use in the html
|
|
||||||
config.plugins.push(
|
|
||||||
new DefinePlugin({
|
|
||||||
'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) ,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// suddenly getting out of memory error from build process, error below [1]
|
|
||||||
// FIXME: remove preact-cli, use rollup
|
|
||||||
let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0]
|
|
||||||
config.plugins.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* [1] from this error decided to remove plugin 'webpack-fix-style-only-entries
|
|
||||||
leaving this error for future reference
|
|
||||||
|
|
||||||
|
|
||||||
<--- Last few GCs --->
|
|
||||||
|
|
||||||
[32479:0x2e01870] 19969 ms: Mark-sweep 1869.4 (1950.2) -> 1443.1 (1504.1) MB, 497.5 / 0.0 ms (average mu = 0.631, current mu = 0.455) allocation failure scavenge might not succeed
|
|
||||||
[32479:0x2e01870] 21907 ms: Mark-sweep 2016.9 (2077.9) -> 1628.6 (1681.4) MB, 1596.0 / 0.0 ms (average mu = 0.354, current mu = 0.176) allocation failure scavenge might not succeed
|
|
||||||
|
|
||||||
<--- JS stacktrace --->
|
|
||||||
|
|
||||||
==== JS stack trace =========================================
|
|
||||||
|
|
||||||
0: ExitFrame [pc: 0x13cf099]
|
|
||||||
Security context: 0x2f4ca66c08d1 <JSObject>
|
|
||||||
1: /* anonymous * / [0x35d05555b4b9] [...path/merchant-backoffice/node_modules/.pnpm/webpack-fix-style-only-entries@0.5.2/node_modules/webpack-fix-style-only-entries/index.js:~80] [pc=0x2145e699d1a4](this=0x1149465410e9 <GlobalObject Object map = 0xff481b5b5f9>,0x047e52e36a49 <Dependency map = 0x1ed1fe41cd19>)
|
|
||||||
2: arguments adaptor frame: 3...
|
|
||||||
|
|
||||||
FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory
|
|
||||||
|
|
||||||
*/
|
|
@ -1,55 +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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DefinePlugin, ProvidePlugin } from 'webpack';
|
|
||||||
|
|
||||||
import pack from './package.json';
|
|
||||||
import * as cp from 'child_process';
|
|
||||||
|
|
||||||
const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
webpack(config, env, helpers) {
|
|
||||||
// Ensure that process.env will not be undefined at runtime.
|
|
||||||
config.node.process = 'mock'
|
|
||||||
let DEMO_SITES = {
|
|
||||||
"Blog": process.env.TALER_ENV_URL_MERCHANT_BLOG,
|
|
||||||
"Donations": process.env.TALER_ENV_URL_MERCHANT_DONATIONS,
|
|
||||||
"Survey": process.env.TALER_ENV_URL_MERCHANT_SURVEY,
|
|
||||||
"Landing": process.env.TALER_ENV_URL_INTRO,
|
|
||||||
"Bank": process.env.TALER_ENV_URL_BANK,
|
|
||||||
}
|
|
||||||
console.log("demo links found", DEMO_SITES);
|
|
||||||
// Add __VERSION__ to be use in the html.
|
|
||||||
config.plugins.push(
|
|
||||||
new DefinePlugin({
|
|
||||||
'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) ,
|
|
||||||
}),
|
|
||||||
// 'window' gets mocked to point at a running euFin instance.
|
|
||||||
new ProvidePlugin({window: path.resolve("mocks/window")}),
|
|
||||||
new DefinePlugin({"DEMO_SITES": JSON.stringify(DEMO_SITES)})
|
|
||||||
);
|
|
||||||
|
|
||||||
let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0]
|
|
||||||
config.plugins.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import defaultConfig from './preact.config'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
webpack(config, env, helpers, options) {
|
|
||||||
defaultConfig.webpack(config, env, helpers, options)
|
|
||||||
|
|
||||||
//1. check no file is under /routers or /component/{routers,async} to prevent async components
|
|
||||||
// https://github.com/preactjs/preact-cli#route-based-code-splitting
|
|
||||||
|
|
||||||
//2. remove devtools to prevent sourcemaps
|
|
||||||
config.devtool = false
|
|
||||||
|
|
||||||
//3. change assetLoader to load assets inline
|
|
||||||
const loaders = helpers.getLoaders(config)
|
|
||||||
const assetsLoader = loaders.find(lo => lo.rule.test.test('something.woff'))
|
|
||||||
if (assetsLoader) {
|
|
||||||
assetsLoader.rule.use = 'base64-inline-loader'
|
|
||||||
assetsLoader.rule.loader = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
//4. remove critters
|
|
||||||
//critters remove the css bundle from htmlWebpackPlugin.files.css
|
|
||||||
//for now, pushing all the content into the html is enough
|
|
||||||
const crittersWrapper = helpers.getPluginsByName(config, 'Critters')
|
|
||||||
if (crittersWrapper && crittersWrapper.length > 0) {
|
|
||||||
const [{ index }] = crittersWrapper
|
|
||||||
config.plugins.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
//5. remove favicon from src/assets
|
|
||||||
|
|
||||||
//6. remove performance hints since we now that this is going to be big
|
|
||||||
if (config.performance) {
|
|
||||||
config.performance.hints = false
|
|
||||||
}
|
|
||||||
|
|
||||||
//7. template.html should have a favicon and add js/css content
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
# This script has been placed in the public domain.
|
|
||||||
|
|
||||||
FILE=$(ls build/bundle.*.css)
|
|
||||||
BUNDLE=${FILE#build}
|
|
||||||
grep -q '<link href="'$BUNDLE'" rel="stylesheet">' build/index.html || { echo bundle $BUNDLE not found in index.html; exit 1; }
|
|
||||||
echo -n Removing link from index.html ...
|
|
||||||
sed 's_<link href="'$BUNDLE'" rel="stylesheet">__' -i build/index.html
|
|
||||||
echo done
|
|
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0l-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg>
|
|
Before Width: | Height: | Size: 274 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 15h2v2h-2zM17 11h2v2h-2zM17 7h2v2h-2zM13.74 7l1.26.84V7z"/><path d="M10 3v1.51l2 1.33V5h9v14h-4v2h6V3z"/><path d="M8.17 5.7L15 10.25V21H1V10.48L8.17 5.7zM10 19h3v-7.84L8.17 8.09 3 11.38V19h3v-6h4v6z"/></svg>
|
|
Before Width: | Height: | Size: 359 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 23.59v-3.6c-5.01-.26-9-4.42-9-9.49C2 5.26 6.26 1 11.5 1S21 5.26 21 10.5c0 4.95-3.44 9.93-8.57 12.4l-1.43.69zM11.5 3C7.36 3 4 6.36 4 10.5S7.36 18 11.5 18H13v2.3c3.64-2.3 6-6.08 6-9.8C19 6.36 15.64 3 11.5 3zm-1 11.5h2v2h-2zm2-1.5h-2c0-3.25 3-3 3-5 0-1.1-.9-2-2-2s-2 .9-2 2h-2c0-2.21 1.79-4 4-4s4 1.79 4 4c0 2.5-3 2.75-3 5z"/></svg>
|
|
Before Width: | Height: | Size: 483 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-1.99.9-1.99 2v18c0 1.1.89 2 1.99 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"/></svg>
|
|
Before Width: | Height: | Size: 272 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M18,10.48V6c0-1.1-0.9-2-2-2H4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-4.48l4,3.98v-11L18,10.48z M16,9.69V18H4V6h12V9.69z"/><circle cx="10" cy="10" r="2"/><path d="M14,15.43c0-0.81-0.48-1.53-1.22-1.85C11.93,13.21,10.99,13,10,13c-0.99,0-1.93,0.21-2.78,0.58C6.48,13.9,6,14.62,6,15.43 V16h8V15.43z"/></g></g></svg>
|
|
Before Width: | Height: | Size: 525 B |
@ -1,7 +1,6 @@
|
|||||||
import { FunctionalComponent, h } from 'preact';
|
import { FunctionalComponent } from "preact";
|
||||||
import { TranslationProvider } from '../context/translation';
|
import { TranslationProvider } from "../context/translation";
|
||||||
import { BankHome } from '../pages/home/index';
|
import { BankHome } from "../pages/home/index";
|
||||||
import { Menu } from './menu';
|
|
||||||
|
|
||||||
const App: FunctionalComponent = () => {
|
const App: FunctionalComponent = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -19,14 +19,14 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { StateUpdater, useState } from 'preact/hooks';
|
import { StateUpdater, useState } from "preact/hooks";
|
||||||
export type ValueOrFunction<T> = T | ((p: T) => T);
|
export type ValueOrFunction<T> = T | ((p: T) => T);
|
||||||
|
|
||||||
const calculateRootPath = () => {
|
const calculateRootPath = () => {
|
||||||
const rootPath =
|
const rootPath =
|
||||||
typeof window !== undefined
|
typeof window !== undefined
|
||||||
? window.location.origin + window.location.pathname
|
? window.location.origin + window.location.pathname
|
||||||
: '/';
|
: "/";
|
||||||
return rootPath;
|
return rootPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,14 +34,14 @@ export function useBackendURL(
|
|||||||
url?: string,
|
url?: string,
|
||||||
): [string, boolean, StateUpdater<string>, () => void] {
|
): [string, boolean, StateUpdater<string>, () => void] {
|
||||||
const [value, setter] = useNotNullLocalStorage(
|
const [value, setter] = useNotNullLocalStorage(
|
||||||
'backend-url',
|
"backend-url",
|
||||||
url || calculateRootPath(),
|
url || calculateRootPath(),
|
||||||
);
|
);
|
||||||
const [triedToLog, setTriedToLog] = useLocalStorage('tried-login');
|
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
|
||||||
|
|
||||||
const checkedSetter = (v: ValueOrFunction<string>) => {
|
const checkedSetter = (v: ValueOrFunction<string>) => {
|
||||||
setTriedToLog('yes');
|
setTriedToLog("yes");
|
||||||
return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ''));
|
return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetBackend = () => {
|
const resetBackend = () => {
|
||||||
@ -53,8 +53,8 @@ export function useBackendURL(
|
|||||||
export function useBackendDefaultToken(): [
|
export function useBackendDefaultToken(): [
|
||||||
string | undefined,
|
string | undefined,
|
||||||
StateUpdater<string | undefined>,
|
StateUpdater<string | undefined>,
|
||||||
] {
|
] {
|
||||||
return useLocalStorage('backend-token');
|
return useLocalStorage("backend-token");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBackendInstanceToken(
|
export function useBackendInstanceToken(
|
||||||
@ -64,59 +64,60 @@ export function useBackendInstanceToken(
|
|||||||
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
|
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
|
||||||
|
|
||||||
// instance named 'default' use the default token
|
// instance named 'default' use the default token
|
||||||
if (id === 'default')
|
if (id === "default") return [defaultToken, defaultSetToken];
|
||||||
return [defaultToken, defaultSetToken];
|
|
||||||
|
|
||||||
return [token, setToken];
|
return [token, setToken];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLang(initial?: string): [string, StateUpdater<string>] {
|
export function useLang(initial?: string): [string, StateUpdater<string>] {
|
||||||
const browserLang =
|
const browserLang =
|
||||||
typeof window !== 'undefined'
|
typeof window !== "undefined"
|
||||||
? navigator.language || (navigator as any).userLanguage
|
? navigator.language || (navigator as any).userLanguage
|
||||||
: undefined;
|
: undefined;
|
||||||
const defaultLang = (browserLang || initial || 'en').substring(0, 2);
|
const defaultLang = (browserLang || initial || "en").substring(0, 2);
|
||||||
const [value, setValue] = useNotNullLocalStorage('lang-preference', defaultLang);
|
const [value, setValue] = useNotNullLocalStorage(
|
||||||
function updateValue(newValue: (string | ((v: string) => string))) {
|
"lang-preference",
|
||||||
|
defaultLang,
|
||||||
|
);
|
||||||
|
function updateValue(newValue: string | ((v: string) => string)) {
|
||||||
if (document.body.parentElement) {
|
if (document.body.parentElement) {
|
||||||
const htmlElement = document.body.parentElement
|
const htmlElement = document.body.parentElement;
|
||||||
if (typeof newValue === 'string') {
|
if (typeof newValue === "string") {
|
||||||
htmlElement.lang = newValue;
|
htmlElement.lang = newValue;
|
||||||
setValue(newValue)
|
setValue(newValue);
|
||||||
} else if (typeof newValue === 'function')
|
} else if (typeof newValue === "function")
|
||||||
setValue((old) => {
|
setValue((old) => {
|
||||||
const nv = newValue(old)
|
const nv = newValue(old);
|
||||||
htmlElement.lang = nv;
|
htmlElement.lang = nv;
|
||||||
return nv
|
return nv;
|
||||||
})
|
});
|
||||||
} else setValue(newValue)
|
} else setValue(newValue);
|
||||||
}
|
}
|
||||||
return [value, updateValue]
|
return [value, updateValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLocalStorage(
|
export function useLocalStorage(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue?: string,
|
initialValue?: string,
|
||||||
): [string | undefined, StateUpdater<string | undefined>] {
|
): [string | undefined, StateUpdater<string | undefined>] {
|
||||||
const [storedValue, setStoredValue] = useState<string | undefined>(():
|
const [storedValue, setStoredValue] = useState<string | undefined>(
|
||||||
| string
|
(): string | undefined => {
|
||||||
| undefined => {
|
return typeof window !== "undefined"
|
||||||
return typeof window !== 'undefined'
|
? window.localStorage.getItem(key) || initialValue
|
||||||
? window.localStorage.getItem(key) || initialValue
|
: initialValue;
|
||||||
: initialValue;
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const setValue = (
|
const setValue = (
|
||||||
value?: string | ((val?: string) => string | undefined),
|
value?: string | ((val?: string) => string | undefined),
|
||||||
) => {
|
) => {
|
||||||
setStoredValue((p) => {
|
setStoredValue((p) => {
|
||||||
|
console.log("calling setStoredValue");
|
||||||
|
console.log(window);
|
||||||
const toStore = value instanceof Function ? value(p) : value;
|
const toStore = value instanceof Function ? value(p) : value;
|
||||||
if (typeof window !== 'undefined')
|
if (typeof window !== "undefined")
|
||||||
if (!toStore)
|
if (!toStore) window.localStorage.removeItem(key);
|
||||||
window.localStorage.removeItem(key);
|
else window.localStorage.setItem(key, toStore);
|
||||||
else
|
|
||||||
window.localStorage.setItem(key, toStore);
|
|
||||||
|
|
||||||
|
|
||||||
return toStore;
|
return toStore;
|
||||||
});
|
});
|
||||||
@ -130,7 +131,7 @@ export function useNotNullLocalStorage(
|
|||||||
initialValue: string,
|
initialValue: string,
|
||||||
): [string, StateUpdater<string>] {
|
): [string, StateUpdater<string>] {
|
||||||
const [storedValue, setStoredValue] = useState<string>((): string => {
|
const [storedValue, setStoredValue] = useState<string>((): string => {
|
||||||
return typeof window !== 'undefined'
|
return typeof window !== "undefined"
|
||||||
? window.localStorage.getItem(key) || initialValue
|
? window.localStorage.getItem(key) || initialValue
|
||||||
: initialValue;
|
: initialValue;
|
||||||
});
|
});
|
||||||
@ -138,13 +139,9 @@ export function useNotNullLocalStorage(
|
|||||||
const setValue = (value: string | ((val: string) => string)) => {
|
const setValue = (value: string | ((val: string) => string)) => {
|
||||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||||
setStoredValue(valueToStore);
|
setStoredValue(valueToStore);
|
||||||
if (typeof window !== 'undefined')
|
if (typeof window !== "undefined")
|
||||||
if (!valueToStore)
|
if (!valueToStore) window.localStorage.removeItem(key);
|
||||||
window.localStorage.removeItem(key);
|
else window.localStorage.setItem(key, valueToStore);
|
||||||
else
|
|
||||||
window.localStorage.setItem(key, valueToStore);
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return [storedValue, setValue];
|
return [storedValue, setValue];
|
||||||
|
34
packages/demobank-ui/src/index.html
Normal file
34
packages/demobank-ui/src/index.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<!--
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021--2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
@author Sebastian Javier Marchano
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
|
||||||
|
<head>
|
||||||
|
<meta 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" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,3 +1,7 @@
|
|||||||
import App from './components/app';
|
import App from "./components/app";
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
import { render, h, Fragment } from "preact";
|
||||||
|
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
|
||||||
|
render(<App />, app as any);
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import useSWR, { SWRConfig as _SWRConfig, useSWRConfig } from "swr";
|
import useSWR, { SWRConfig as _SWRConfig, useSWRConfig } from "swr";
|
||||||
import { h, Fragment, VNode, createContext } from "preact";
|
import { h, Fragment, VNode, createContext } from "preact";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -24,20 +25,37 @@ import {
|
|||||||
StateUpdater,
|
StateUpdater,
|
||||||
useContext,
|
useContext,
|
||||||
} from "preact/hooks";
|
} from "preact/hooks";
|
||||||
import { Buffer } from "buffer";
|
import { useTranslator, Translate } from "../../i18n/index.js";
|
||||||
import { useTranslator, Translate } from "../../i18n";
|
import { QR } from "../../components/QR.js";
|
||||||
import { QR } from "../../components/QR";
|
import { useNotNullLocalStorage, useLocalStorage } from "../../hooks/index.js";
|
||||||
import { useNotNullLocalStorage, useLocalStorage } from "../../hooks";
|
|
||||||
import "../../scss/main.scss";
|
import "../../scss/main.scss";
|
||||||
import talerLogo from "../../assets/logo-white.svg";
|
import talerLogo from "../../assets/logo-white.svg";
|
||||||
import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector";
|
import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js";
|
||||||
|
|
||||||
// FIXME: Fix usages of SWRConfig, doing this isn't the best practice (but hey, it works for now)
|
// FIXME: Fix usages of SWRConfig, doing this isn't the best practice (but hey, it works for now)
|
||||||
const SWRConfig = _SWRConfig as any;
|
const SWRConfig = _SWRConfig as any;
|
||||||
|
|
||||||
const UI_ALLOW_REGISTRATIONS = "__LIBEUFIN_UI_ALLOW_REGISTRATIONS__" ?? 1;
|
/**
|
||||||
const UI_IS_DEMO = "__LIBEUFIN_UI_IS_DEMO__" ?? 0;
|
* If the first argument does not look like a placeholder, return it.
|
||||||
const UI_BANK_NAME = "__LIBEUFIN_UI_BANK_NAME__" ?? "Taler Bank";
|
* Otherwise, return the default.
|
||||||
|
*
|
||||||
|
* Useful for placeholder string replacements optionally
|
||||||
|
* done as part of the build system.
|
||||||
|
*/
|
||||||
|
const replacementOrDefault = (x: string, defaultVal: string) => {
|
||||||
|
if (x.startsWith("__")) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UI_ALLOW_REGISTRATIONS =
|
||||||
|
replacementOrDefault("__LIBEUFIN_UI_ALLOW_REGISTRATIONS__", "1") == "1";
|
||||||
|
const UI_IS_DEMO = replacementOrDefault("__LIBEUFIN_UI_IS_DEMO__", "0") == "1";
|
||||||
|
const UI_BANK_NAME = replacementOrDefault(
|
||||||
|
"__LIBEUFIN_UI_BANK_NAME__",
|
||||||
|
"Taler Bank",
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FIXME:
|
* FIXME:
|
||||||
@ -156,15 +174,6 @@ function maybeDemoContent(content: VNode) {
|
|||||||
if (UI_IS_DEMO) return content;
|
if (UI_IS_DEMO) return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetcher(url: string) {
|
|
||||||
return fetch(url).then((r) => r.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function genCaptchaNumbers(): string {
|
|
||||||
return `${Math.floor(Math.random() * 10)} + ${Math.floor(
|
|
||||||
Math.random() * 10,
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Bring the state to show the public accounts page.
|
* Bring the state to show the public accounts page.
|
||||||
*/
|
*/
|
||||||
@ -276,22 +285,26 @@ function prepareHeaders(username: string, password: string) {
|
|||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append(
|
headers.append(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
`Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
|
`Basic ${window.btoa(`${username}:${password}`)}`,
|
||||||
);
|
);
|
||||||
headers.append("Content-Type", "application/json");
|
headers.append("Content-Type", "application/json");
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window can be mocked this way:
|
const getBankBackendBaseUrl = () => {
|
||||||
// https://gist.github.com/theKashey/07090691c0a4680ed773375d8dbeebc1#file-webpack-conf-js
|
const overrideUrl = localStorage.getItem("bank-base-url");
|
||||||
// That allows the app to be pointed to a arbitrary
|
if (overrideUrl) {
|
||||||
// euFin backend when launched via "pnpm dev".
|
console.log(
|
||||||
const getRootPath = () => {
|
`using bank base URL ${overrideUrl} (override via bank-base-url localStorage)`,
|
||||||
|
);
|
||||||
|
return overrideUrl;
|
||||||
|
}
|
||||||
const maybeRootPath =
|
const maybeRootPath =
|
||||||
typeof window !== undefined
|
typeof window !== undefined
|
||||||
? window.location.origin + window.location.pathname
|
? window.location.origin + window.location.pathname
|
||||||
: "/";
|
: "/";
|
||||||
if (!maybeRootPath.endsWith("/")) return `${maybeRootPath}/`;
|
if (!maybeRootPath.endsWith("/")) return `${maybeRootPath}/`;
|
||||||
|
console.log(`using bank base URL (${maybeRootPath})`);
|
||||||
return maybeRootPath;
|
return maybeRootPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -785,7 +798,7 @@ async function loginCall(
|
|||||||
* let the Account component request the balance to check
|
* let the Account component request the balance to check
|
||||||
* whether the credentials are valid. */
|
* whether the credentials are valid. */
|
||||||
pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
|
pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
|
||||||
let baseUrl = getRootPath();
|
let baseUrl = getBankBackendBaseUrl();
|
||||||
if (!baseUrl.endsWith("/")) baseUrl += "/";
|
if (!baseUrl.endsWith("/")) baseUrl += "/";
|
||||||
|
|
||||||
backendStateSetter((prevState) => ({
|
backendStateSetter((prevState) => ({
|
||||||
@ -813,7 +826,7 @@ async function registrationCall(
|
|||||||
backendStateSetter: StateUpdater<BackendStateTypeOpt>,
|
backendStateSetter: StateUpdater<BackendStateTypeOpt>,
|
||||||
pageStateSetter: StateUpdater<PageStateType>,
|
pageStateSetter: StateUpdater<PageStateType>,
|
||||||
) {
|
) {
|
||||||
let baseUrl = getRootPath();
|
let baseUrl = getBankBackendBaseUrl();
|
||||||
/**
|
/**
|
||||||
* If the base URL doesn't end with slash and the path
|
* If the base URL doesn't end with slash and the path
|
||||||
* is not empty, then the concatenation made by URL()
|
* is not empty, then the concatenation made by URL()
|
||||||
@ -873,19 +886,6 @@ async function registrationCall(
|
|||||||
* Functional components. *
|
* Functional components. *
|
||||||
*************************/
|
*************************/
|
||||||
|
|
||||||
function Currency(): VNode {
|
|
||||||
const { data, error } = useSWR(
|
|
||||||
`${getRootPath()}integration-api/config`,
|
|
||||||
fetcher,
|
|
||||||
);
|
|
||||||
if (typeof error !== "undefined")
|
|
||||||
return <b>error: currency could not be retrieved</b>;
|
|
||||||
|
|
||||||
if (typeof data === "undefined") return <Fragment>"..."</Fragment>;
|
|
||||||
console.log("found bank config", data);
|
|
||||||
return data.currency;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = useTranslator();
|
||||||
@ -2043,10 +2043,7 @@ function Account(Props: any): VNode {
|
|||||||
function SWRWithCredentials(props: any): VNode {
|
function SWRWithCredentials(props: any): VNode {
|
||||||
const { username, password, backendUrl } = props;
|
const { username, password, backendUrl } = props;
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append(
|
headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`);
|
||||||
"Authorization",
|
|
||||||
`Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
|
|
||||||
);
|
|
||||||
console.log("Likely backend base URL", backendUrl);
|
console.log("Likely backend base URL", backendUrl);
|
||||||
return (
|
return (
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
@ -2179,13 +2176,11 @@ function PublicHistories(Props: any): VNode {
|
|||||||
export function BankHome(): VNode {
|
export function BankHome(): VNode {
|
||||||
const [backendState, backendStateSetter] = useBackendState();
|
const [backendState, backendStateSetter] = useBackendState();
|
||||||
const [pageState, pageStateSetter] = usePageState();
|
const [pageState, pageStateSetter] = usePageState();
|
||||||
const [accountState, accountStateSetter] = useAccountState();
|
|
||||||
const setTxPageNumber = useTransactionPageNumber()[1];
|
|
||||||
const i18n = useTranslator();
|
const i18n = useTranslator();
|
||||||
|
|
||||||
if (pageState.showPublicHistories)
|
if (pageState.showPublicHistories)
|
||||||
return (
|
return (
|
||||||
<SWRWithoutCredentials baseUrl={getRootPath()}>
|
<SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
|
||||||
<PageContext.Provider value={[pageState, pageStateSetter]}>
|
<PageContext.Provider value={[pageState, pageStateSetter]}>
|
||||||
<BankFrame>
|
<BankFrame>
|
||||||
<PublicHistories pageStateSetter={pageStateSetter}>
|
<PublicHistories pageStateSetter={pageStateSetter}>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,52 +0,0 @@
|
|||||||
<!--
|
|
||||||
This file is part of GNU Taler
|
|
||||||
(C) 2021--2022 Taler Systems S.A.
|
|
||||||
|
|
||||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
||||||
terms of the GNU General Public License as published by the Free Software
|
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
|
||||||
|
|
||||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
|
||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
@author Sebastian Javier Marchano
|
|
||||||
-->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
|
|
||||||
<meta name="theme-color" content="<%= htmlWebpackPlugin.options.manifest.theme_color %>">
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% for (const index in htmlWebpackPlugin.files.css) { %>
|
|
||||||
<% const file = htmlWebpackPlugin.files.css[index] %>
|
|
||||||
<style data-href='<%= file %>' >
|
|
||||||
<%= compilation.assets[file.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
|
|
||||||
</style>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
<%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
<%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
12
packages/demobank-ui/static/index.html
Normal file
12
packages/demobank-ui/static/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<title>Demobank</title>
|
||||||
|
<script type="module" src="index.js"></script>
|
||||||
|
<link rel="stylesheet" href="index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,21 +0,0 @@
|
|||||||
// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
|
|
||||||
/**
|
|
||||||
* An example how to mock localStorage is given below 👇
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
// Mocks localStorage
|
|
||||||
const localStorageMock = (function() {
|
|
||||||
let store = {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getItem: (key) => store[key] || null,
|
|
||||||
setItem: (key, value) => store[key] = value.toString(),
|
|
||||||
clear: () => store = {}
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'localStorage', {
|
|
||||||
value: localStorageMock
|
|
||||||
}); */
|
|
@ -1,3 +0,0 @@
|
|||||||
// This fixed an error related to the CSS and loading gif breaking my Jest test
|
|
||||||
// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
|
|
||||||
export default 'test-file-stub';
|
|
@ -1,6 +0,0 @@
|
|||||||
import { configure } from 'enzyme';
|
|
||||||
import Adapter from 'enzyme-adapter-preact-pure';
|
|
||||||
|
|
||||||
configure({
|
|
||||||
adapter: new Adapter() as any
|
|
||||||
});
|
|
@ -1,466 +0,0 @@
|
|||||||
import "core-js/stable";
|
|
||||||
import "regenerator-runtime/runtime";
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import { BankHome } from '../../src/pages/home';
|
|
||||||
import { h } from 'preact';
|
|
||||||
import { waitFor, cleanup, render, fireEvent, screen } from '@testing-library/preact';
|
|
||||||
import expect from 'expect';
|
|
||||||
import fetchMock from "jest-fetch-mock";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mock makes the translator always return the
|
|
||||||
* english string. It didn't work within the 'beforeAll'
|
|
||||||
* function...
|
|
||||||
*/
|
|
||||||
jest.mock("../../src/i18n")
|
|
||||||
const i18n = require("../../src/i18n")
|
|
||||||
i18n.useTranslator.mockImplementation(() => function(arg) {return arg})
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Object.defineProperty(window, 'location', {
|
|
||||||
value: {
|
|
||||||
origin: "http://localhost",
|
|
||||||
pathname: "/demobanks/default"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
global.Storage.prototype.setItem = jest.fn((key, value) => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
function fillCredentialsForm() {
|
|
||||||
const username = Math.random().toString().substring(2);
|
|
||||||
const u = screen.getByPlaceholderText("username");
|
|
||||||
const p = screen.getByPlaceholderText("password");
|
|
||||||
fireEvent.input(u, {target: {value: username}})
|
|
||||||
fireEvent.input(p, {target: {value: "bar"}})
|
|
||||||
const signinButton = screen.getByText("Login");
|
|
||||||
return {
|
|
||||||
username: username,
|
|
||||||
signinButton: signinButton
|
|
||||||
};
|
|
||||||
}
|
|
||||||
fetchMock.enableMocks();
|
|
||||||
|
|
||||||
function mockSuccessLoginOrRegistration() {
|
|
||||||
fetch.once("{}", {
|
|
||||||
status: 200
|
|
||||||
}).once(JSON.stringify({
|
|
||||||
balance: {
|
|
||||||
amount: "EUR:10",
|
|
||||||
credit_debit_indicator: "credit"
|
|
||||||
},
|
|
||||||
paytoUri: "payto://iban/123/ABC"
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render homepage -> navigate to register page -> submit registration.
|
|
||||||
* 'webMock' is called before submission to mock the server response
|
|
||||||
*/
|
|
||||||
function signUp(context, webMock) {
|
|
||||||
render(<BankHome />);
|
|
||||||
const registerPage = screen.getByText("Register!");
|
|
||||||
fireEvent.click(registerPage);
|
|
||||||
const username = Math.random().toString().substring(2);
|
|
||||||
const u = screen.getByPlaceholderText("username");
|
|
||||||
const p = screen.getByPlaceholderText("password");
|
|
||||||
fireEvent.input(u, {target: {value: username}})
|
|
||||||
fireEvent.input(p, {target: {value: "bar"}})
|
|
||||||
const registerButton = screen.getByText("Register");
|
|
||||||
webMock();
|
|
||||||
fireEvent.click(registerButton);
|
|
||||||
context.username = username;
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("wire transfer", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
signUp({}, mockSuccessLoginOrRegistration); // context unused
|
|
||||||
})
|
|
||||||
test("Wire transfer success", async () => {
|
|
||||||
const transferButton = screen.getByText("Create wire transfer");
|
|
||||||
const payto = screen.getByPlaceholderText("payto address");
|
|
||||||
fireEvent.input(payto, {target: {value: "payto://only-checked-by-the-backend!"}})
|
|
||||||
fetch.once("{}"); // 200 OK
|
|
||||||
fireEvent.click(transferButton);
|
|
||||||
await screen.findByText("wire transfer created", {exact: false})
|
|
||||||
})
|
|
||||||
test("Wire transfer fail", async () => {
|
|
||||||
const transferButton = screen.getByText("Create wire transfer");
|
|
||||||
const payto = screen.getByPlaceholderText("payto address");
|
|
||||||
fireEvent.input(payto, {target: {value: "payto://only-checked-by-the-backend!"}})
|
|
||||||
fetch.once("{}", {status: 400});
|
|
||||||
fireEvent.click(transferButton);
|
|
||||||
// assert this below does NOT appear.
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("wire transfer created", {exact: false})).not.toBeInTheDocument());
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("withdraw", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
fetch.resetMocks();
|
|
||||||
cleanup();
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
let context = {};
|
|
||||||
// Register and land on the profile page.
|
|
||||||
beforeEach(() => {
|
|
||||||
context = signUp(context, mockSuccessLoginOrRegistration);
|
|
||||||
})
|
|
||||||
|
|
||||||
test("network failure before withdrawal creation", async () => {
|
|
||||||
const a = screen.getAllByPlaceholderText("amount")[0];
|
|
||||||
fireEvent.input(a, {target: {value: "10"}});
|
|
||||||
let withdrawButton = screen.getByText("Charge Taler wallet");
|
|
||||||
// mock network failure.
|
|
||||||
fetch.mockReject("API is down");
|
|
||||||
fireEvent.click(withdrawButton);
|
|
||||||
await screen.findByText("could not create withdrawal operation", {exact: false})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("HTTP response error upon withdrawal creation", async () => {
|
|
||||||
const a = screen.getAllByPlaceholderText("amount")[0];
|
|
||||||
fireEvent.input(a, {target: {value: "10,0"}});
|
|
||||||
let withdrawButton = screen.getByText("Charge Taler wallet");
|
|
||||||
fetch.once("{}", {status: 404});
|
|
||||||
fireEvent.click(withdrawButton);
|
|
||||||
await screen.findByText("gave response error", {exact: false})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Abort withdrawal", async () => {
|
|
||||||
const a = screen.getAllByPlaceholderText("amount")[0];
|
|
||||||
fireEvent.input(a, {target: {value: "10,0"}});
|
|
||||||
let withdrawButton = screen.getByText("Charge Taler wallet");
|
|
||||||
fetch.once(JSON.stringify({
|
|
||||||
taler_withdraw_uri: "taler://withdraw/foo",
|
|
||||||
withdrawal_id: "foo"
|
|
||||||
}));
|
|
||||||
/**
|
|
||||||
* After triggering a withdrawal, check if the taler://withdraw URI
|
|
||||||
* rendered, and confirm if so. Lastly, check that a success message
|
|
||||||
* appeared on the screen.
|
|
||||||
*/
|
|
||||||
fireEvent.click(withdrawButton);
|
|
||||||
const abortButton = await screen.findByText("abort withdrawal", {exact: false})
|
|
||||||
fireEvent.click(abortButton);
|
|
||||||
expect(fetch).toHaveBeenLastCalledWith(
|
|
||||||
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals/foo/abort`,
|
|
||||||
expect.anything()
|
|
||||||
)
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("abort withdrawal", {exact: false})).not.toBeInTheDocument());
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Successful withdrawal creation and confirmation", async () => {
|
|
||||||
const a = screen.getAllByPlaceholderText("amount")[0];
|
|
||||||
fireEvent.input(a, {target: {value: "10,0"}});
|
|
||||||
let withdrawButton = await screen.findByText("Charge Taler wallet");
|
|
||||||
fetch.once(JSON.stringify({
|
|
||||||
taler_withdraw_uri: "taler://withdraw/foo",
|
|
||||||
withdrawal_id: "foo"
|
|
||||||
}));
|
|
||||||
/**
|
|
||||||
* After triggering a withdrawal, check if the taler://withdraw URI
|
|
||||||
* rendered, and confirm if so. Lastly, check that a success message
|
|
||||||
* appeared on the screen. */
|
|
||||||
fireEvent.click(withdrawButton);
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals`,
|
|
||||||
expect.objectContaining({body: JSON.stringify({amount: "EUR:10.0"})})
|
|
||||||
)
|
|
||||||
// assume wallet POSTed the payment details.
|
|
||||||
const confirmButton = await screen.findByText("confirm withdrawal", {exact: false})
|
|
||||||
/**
|
|
||||||
* Not expecting a new withdrawal possibility while one is being processed.
|
|
||||||
*/
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("charge taler wallet", {exact: false})).not.toBeInTheDocument());
|
|
||||||
fetch.once("{}")
|
|
||||||
// Confirm currently processed withdrawal.
|
|
||||||
fireEvent.click(confirmButton);
|
|
||||||
/**
|
|
||||||
* After having confirmed above, wait that the
|
|
||||||
* pre-withdrawal elements disappears and a success
|
|
||||||
* message appears.
|
|
||||||
*/
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText(
|
|
||||||
"confirm withdrawal",
|
|
||||||
{exact: false})).not.toBeInTheDocument()
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText(
|
|
||||||
"give this address to the taler wallet",
|
|
||||||
{exact: false})).not.toBeInTheDocument()
|
|
||||||
);
|
|
||||||
expect(fetch).toHaveBeenLastCalledWith(
|
|
||||||
`http://localhost/demobanks/default/access-api/accounts/${context.username}/withdrawals/foo/confirm`,
|
|
||||||
expect.anything())
|
|
||||||
// success message
|
|
||||||
await screen.findByText("withdrawal confirmed", {exact: false})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click on a "return to homepage / close" button, and
|
|
||||||
* check that the withdrawal confirmation is gone, and
|
|
||||||
* the option to withdraw again reappeared.
|
|
||||||
*/
|
|
||||||
const closeButton = await screen.findByText("close", {exact: false})
|
|
||||||
fireEvent.click(closeButton);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After closing the operation, the confirmation message is not expected.
|
|
||||||
*/
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("withdrawal confirmed", {exact: false})).not.toBeInTheDocument()
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After closing the operation, the possibility to withdraw again should be offered.
|
|
||||||
*/
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText(
|
|
||||||
"charge taler wallet",
|
|
||||||
{exact: false})).toBeInTheDocument()
|
|
||||||
);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("home page", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
fetch.resetMocks();
|
|
||||||
cleanup();
|
|
||||||
})
|
|
||||||
test("public histories", async () => {
|
|
||||||
render(<BankHome />);
|
|
||||||
/**
|
|
||||||
* Mock list of public accounts. 'bar' is
|
|
||||||
* the shown account, since it occupies the last
|
|
||||||
* position (and SPA picks it via the 'pop()' method) */
|
|
||||||
fetch.once(JSON.stringify({
|
|
||||||
"publicAccounts" : [ {
|
|
||||||
"balance" : "EUR:1",
|
|
||||||
"iban" : "XXX",
|
|
||||||
"accountLabel" : "foo"
|
|
||||||
}, {
|
|
||||||
"balance" : "EUR:2",
|
|
||||||
"iban" : "YYY",
|
|
||||||
"accountLabel" : "bar"
|
|
||||||
}]
|
|
||||||
})).once(JSON.stringify({
|
|
||||||
transactions: [{
|
|
||||||
debtorIban: "XXX",
|
|
||||||
debtorBic: "YYY",
|
|
||||||
debtorName: "Foo",
|
|
||||||
creditorIban: "AAA",
|
|
||||||
creditorBic: "BBB",
|
|
||||||
creditorName: "Bar",
|
|
||||||
direction: "DBIT",
|
|
||||||
amount: "EUR:5",
|
|
||||||
subject: "Reimbursement",
|
|
||||||
date: "1970-01-01"
|
|
||||||
}, {
|
|
||||||
debtorIban: "XXX",
|
|
||||||
debtorBic: "YYY",
|
|
||||||
debtorName: "Foo",
|
|
||||||
creditorIban: "AAA",
|
|
||||||
creditorBic: "BBB",
|
|
||||||
creditorName: "Bar",
|
|
||||||
direction: "CRDT",
|
|
||||||
amount: "EUR:5",
|
|
||||||
subject: "Bonus",
|
|
||||||
date: "2000-01-01"
|
|
||||||
}]
|
|
||||||
})).once(JSON.stringify({
|
|
||||||
transactions: [{
|
|
||||||
debtorIban: "XXX",
|
|
||||||
debtorBic: "YYY",
|
|
||||||
debtorName: "Foo",
|
|
||||||
creditorIban: "AAA",
|
|
||||||
creditorBic: "BBB",
|
|
||||||
creditorName: "Bar",
|
|
||||||
direction: "DBIT",
|
|
||||||
amount: "EUR:5",
|
|
||||||
subject: "Donation",
|
|
||||||
date: "1970-01-01"
|
|
||||||
}, {
|
|
||||||
debtorIban: "XXX",
|
|
||||||
debtorBic: "YYY",
|
|
||||||
debtorName: "Foo",
|
|
||||||
creditorIban: "AAA",
|
|
||||||
creditorBic: "BBB",
|
|
||||||
creditorName: "Bar",
|
|
||||||
direction: "CRDT",
|
|
||||||
amount: "EUR:5",
|
|
||||||
subject: "Refund",
|
|
||||||
date: "2000-01-01"
|
|
||||||
}]
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Navigate to dedicate public histories page.
|
|
||||||
const publicTxsPage = screen.getByText("transactions");
|
|
||||||
fireEvent.click(publicTxsPage);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that transactions data appears on the page.
|
|
||||||
*/
|
|
||||||
await screen.findByText("reimbursement", {exact: false});
|
|
||||||
await screen.findByText("bonus", {exact: false});
|
|
||||||
/**
|
|
||||||
* The transactions below should not appear, because only
|
|
||||||
* one public account renders.
|
|
||||||
*/
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("refund", {exact: false})).not.toBeInTheDocument());
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("donation", {exact: false})).not.toBeInTheDocument());
|
|
||||||
/**
|
|
||||||
* First HTTP mock:
|
|
||||||
*/
|
|
||||||
await expect(fetch).toHaveBeenCalledWith(
|
|
||||||
"http://localhost/demobanks/default/access-api/public-accounts"
|
|
||||||
)
|
|
||||||
/**
|
|
||||||
* Only expecting this request (second mock), as SWR doesn't let
|
|
||||||
* the unshown history request to the backend:
|
|
||||||
*/
|
|
||||||
await expect(fetch).toHaveBeenCalledWith(
|
|
||||||
"http://localhost/demobanks/default/access-api/accounts/bar/transactions?page=0"
|
|
||||||
)
|
|
||||||
/**
|
|
||||||
* Switch tab:
|
|
||||||
*/
|
|
||||||
let fooTab = await screen.findByText("foo", {exact: false});
|
|
||||||
fireEvent.click(fooTab);
|
|
||||||
/**
|
|
||||||
* Last two HTTP mocks should render now:
|
|
||||||
*/
|
|
||||||
await screen.findByText("refund", {exact: false});
|
|
||||||
await screen.findByText("donation", {exact: false});
|
|
||||||
|
|
||||||
// Expect SWR to have requested 'foo' history
|
|
||||||
// (consuming the last HTTP mock):
|
|
||||||
await expect(fetch).toHaveBeenCalledWith(
|
|
||||||
"http://localhost/demobanks/default/access-api/accounts/foo/transactions?page=0"
|
|
||||||
)
|
|
||||||
let backButton = await screen.findByText("Go back", {exact: false});
|
|
||||||
fireEvent.click(backButton);
|
|
||||||
await waitFor(() => expect(
|
|
||||||
screen.queryByText("donation", {exact: false})).not.toBeInTheDocument());
|
|
||||||
await screen.findByText("welcome to eufin bank", {exact: false})
|
|
||||||
})
|
|
||||||
|
|
||||||
// check page informs about the current balance
|
|
||||||
// after a successful registration.
|
|
||||||
|
|
||||||
test("new registration response error 404", async () => {
|
|
||||||
var context = signUp({}, () => fetch.mockResponseOnce("Not found", {status: 404}));
|
|
||||||
await screen.findByText("has a problem", {exact: false});
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
"http://localhost/demobanks/default/access-api/testing/register",
|
|
||||||
expect.objectContaining(
|
|
||||||
{body: JSON.stringify({username: context.username, password: "bar"}), method: "POST"},
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
test("registration network failure", async () => {
|
|
||||||
let context = signUp({}, ()=>fetch.mockReject("API is down"));
|
|
||||||
await screen.findByText("has a problem", {exact: false});
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
"http://localhost/demobanks/default/access-api/testing/register",
|
|
||||||
expect.objectContaining(
|
|
||||||
{body: JSON.stringify({username: context.username, password: "bar"}), method: "POST"}
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
test("login non existent user", async () => {
|
|
||||||
render(<BankHome />);
|
|
||||||
const { username, signinButton } = fillCredentialsForm();
|
|
||||||
fetch.once("{}", {status: 404});
|
|
||||||
fireEvent.click(signinButton);
|
|
||||||
await screen.findByText("username or account label not found", {exact: false})
|
|
||||||
})
|
|
||||||
test("login wrong credentials", async () => {
|
|
||||||
render(<BankHome />);
|
|
||||||
const { username, signinButton } = fillCredentialsForm();
|
|
||||||
fetch.once("{}", {status: 401});
|
|
||||||
fireEvent.click(signinButton);
|
|
||||||
await screen.findByText("wrong credentials given", {exact: false})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that balance and last transactions get shown
|
|
||||||
* after a successful login.
|
|
||||||
*/
|
|
||||||
test("login success", async () => {
|
|
||||||
render(<BankHome />);
|
|
||||||
const { username, signinButton } = fillCredentialsForm();
|
|
||||||
|
|
||||||
// Response to balance request.
|
|
||||||
fetch.once(JSON.stringify({
|
|
||||||
balance: {
|
|
||||||
amount: "EUR:10",
|
|
||||||
credit_debit_indicator: "credit"
|
|
||||||
},
|
|
||||||
paytoUri: "payto://iban/123/ABC"
|
|
||||||
})).once(JSON.stringify({ // Response to history request.
|
|
||||||
transactions: [{
|
|
||||||
debtorIban: "XXX",
|
|
||||||
debtorBic: "YYY",
|
|
||||||
debtorName: "Foo",
|
|
||||||
creditorIban: "AAA",
|
|
||||||
creditorBic: "BBB",
|
|
||||||
creditorName: "Bar",
|
|
||||||
direction: "DBIT",
|
|
||||||
amount: "EUR:5",
|
|
||||||
subject: "Donation",
|
|
||||||
date: "01-01-1970"
|
|
||||||
}, {
|
|
||||||
debtorIban: "XXX",
|
|
||||||
debtorBic: "YYY",
|
|
||||||
debtorName: "Foo",
|
|
||||||
creditorIban: "AAA",
|
|
||||||
creditorBic: "BBB",
|
|
||||||
creditorName: "Bar",
|
|
||||||
direction: "CRDT",
|
|
||||||
amount: "EUR:5",
|
|
||||||
subject: "Refund",
|
|
||||||
date: "01-01-2000"
|
|
||||||
}]
|
|
||||||
}))
|
|
||||||
fireEvent.click(signinButton);
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`http://localhost/demobanks/default/access-api/accounts/${username}`,
|
|
||||||
expect.anything()
|
|
||||||
)
|
|
||||||
await screen.findByText("balance is 10 EUR", {exact: false})
|
|
||||||
// The two transactions in the history mocked above.
|
|
||||||
await screen.findByText("refund", {exact: false})
|
|
||||||
await screen.findByText("donation", {exact: false})
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`http://localhost/demobanks/default/access-api/accounts/${username}/transactions?page=0`,
|
|
||||||
expect.anything()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("registration success", async () => {
|
|
||||||
let context = signUp({}, mockSuccessLoginOrRegistration);
|
|
||||||
/**
|
|
||||||
* Tests that a balance is shown after the successful
|
|
||||||
* registration.
|
|
||||||
*/
|
|
||||||
await screen.findByText("balance is 10 EUR", {exact: false})
|
|
||||||
/**
|
|
||||||
* The expectation below tests whether the account
|
|
||||||
* balance was requested after the successful registration.
|
|
||||||
*/
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
"http://localhost/demobanks/default/access-api/testing/register",
|
|
||||||
expect.anything() // no need to match auth headers.
|
|
||||||
)
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`http://localhost/demobanks/default/access-api/accounts/${context.username}`,
|
|
||||||
expect.anything() // no need to match auth headers.
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
3
packages/demobank-ui/tests/declarations.d.ts
vendored
3
packages/demobank-ui/tests/declarations.d.ts
vendored
@ -1,3 +0,0 @@
|
|||||||
// Enable enzyme adapter's integration with TypeScript
|
|
||||||
// See: https://github.com/preactjs/enzyme-adapter-preact-pure#usage-with-typescript
|
|
||||||
/// <reference types="enzyme-adapter-preact-pure" />
|
|
@ -1,19 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
|
"target": "ES5",
|
||||||
"module": "ESNext" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
"module": "ES6",
|
||||||
// "lib": [], /* Specify library files to be included in the compilation: */
|
"lib": ["DOM", "ES2016"],
|
||||||
"allowJs": true /* Allow javascript files to be compiled. */,
|
"allowJs": true /* Allow javascript files to be compiled. */,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||||
"jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */,
|
"jsxImportSource": "preact",
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
|
||||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
|
||||||
"noEmit": true /* Do not emit outputs. */,
|
"noEmit": true /* Do not emit outputs. */,
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
@ -21,11 +15,7 @@
|
|||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
|
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
@ -33,14 +23,14 @@
|
|||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
"moduleResolution": "Node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
||||||
"esModuleInterop": true /* */,
|
"esModuleInterop": true /* */,
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
|
||||||
/* Source Map Options */
|
/* Source Map Options */
|
||||||
@ -56,5 +46,5 @@
|
|||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"skipLibCheck": true /* Skip type checking of declaration files. */
|
"skipLibCheck": true /* Skip type checking of declaration files. */
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "tests/**/*"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
5501
pnpm-lock.yaml
5501
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user